Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Web of today — Alexander Khokhlov

126 views

Published on

Elixir Club 12
September 22, 2018
Kyiv

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Web of today — Alexander Khokhlov

  1. 1. Alexander Khokhlov @nots_ioNots.io Web of the future today
  2. 2. Founder Nots.io 01
  3. 3. https://www.youtube.com/watch?v=rmueBVrLKcY
  4. 4. Notes For block of code, function, module, file, commit or branch. 01
  5. 5. Web of the future today
  6. 6. It started with… 02
  7. 7. 03 • It started with… 📜
  8. 8. 03 • It started with… cgi-bin
  9. 9. 03 • It started with… #!/usr/bin/perl -w
  10. 10. 03 • It started with… <?= $crap
  11. 11. 03 • It started with… Any language
  12. 12. 03 • It started with… DHTML In browser
  13. 13. 03 • It started with… AJAX
  14. 14. 03 • It started with… In essence 3-way handshake → GET/POST request → Receive response → Close socket
  15. 15. 04 Nowadays
  16. 16. 04 • Nowadays SPA Server-Side Rendering Live Updates Be ⌘+R compatible
  17. 17. 04 • Nowadays Typically
  18. 18. 04 • Nowadays The most interesting part
  19. 19. 04 • Nowadays Websockets
  20. 20. 04 • Nowadays New server requirements c10k problem Keep state Manage connections
  21. 21. 05 Phoenix in Nots.io
  22. 22. 05 • Phoenix in Nots
  23. 23. Typing Notification 05 • Phoenix in Nots
  24. 24. Sockets (ns notsapp.sockets 
 (:require 
 phoenix
 [notsapp.commons :refer [q]])) 
 
 (defonce socket (phoenix/Socket. "/socket")) 
 
 (defn connect [callback] 
 (.onOpen socket callback) 
 (.connect 
 socket 
 (clj->js 
 {:guardian_token 
 (-> 
 (q "meta[name='guardian_token']") 
 (.getAttribute "content")) 
 :token 
 (-> 
 (q "meta[name='token']") 
 (.getAttribute "content"))}))) 05 • Phoenix in Nots
  25. 25. Channels (def notify-typing-channel (doto (.channel notsapp.sockets/socket "notify-typing" #js {}) (.join)))CLJS side 05 • Phoenix in Nots
  26. 26. Channels defmodule NotsappWeb.AppSocket do use Phoenix.Socket import Guardian.Phoenix.Socket channel "notify-typing", NotsappWeb.TypingNotificationChannel def connect(%{"guardian_token" => jwt} = params, socket) do case Guardian.Phoenix.Socket.authenticate(socket, Notsapp.Guardian, jwt) do {:ok, authed_socket} -> {:ok, authed_socket} _ -> :error end end def connect(_params, _socket) do :error end end Elixir side 05 • Phoenix in Nots
  27. 27. Subscribe phase When card is shown subscribe on typing notification channel for that card 05 • Phoenix in Nots
  28. 28. Subscription (defn subscribe-typing-state-on-topic [topic] (.push notify-typing-channel "subscribe" topic) (.on notify-typing-channel topic (fn [msg] (update-state [:notify-typing topic] {(aget msg "user_id") (aget msg "message")} false true)))) CLJS side 05 • Phoenix in Nots
  29. 29. Now what? We need to know who sends notifications and whom to deliver them to 05 • Phoenix in Nots
  30. 30. 🎓 05 • Phoenix in Nots
  31. 31. 🎓 05 • Phoenix in Nots
  32. 32. Subscription defmodule NotsappWeb.TypingNotificationChannel do use Phoenix.Channel def join("notify-typing", _auth_message, socket) do user = Guardian.Phoenix.Socket.current_resource(socket) {:ok, Phoenix.Socket.assign(socket, :user_id, user.id)} end def handle_in("subscribe", topic, socket) do user_id = socket.assigns[:user_id] Notsapp.TypingState.subscribe( user_id, topic, socket) {:noreply, socket} end end Elixir side 05 • Phoenix in Nots
  33. 33. Subscription defmodule Notsapp.TypingState do use GenServer @interval 3_600_000 # 1h def init(_args) do Process.send_after __MODULE__, :cleanup_sockets, @interval {:ok, %{subscriptions: %{}, timers: %{}}} end def subscribe(user_id, topic, socket) do GenServer.cast(__MODULE__, {:subscribe, user_id, topic, socket}) end Elixir side TypingState 05 • Phoenix in Nots
  34. 34. Subscription def handle_cast({:subscribe, user_id, topic, socket}, %{subscriptions: subscriptions} = state) do new_subscriptions = Map.update(subscriptions, topic, [{socket, user_id}], &([{socket, user_id} | &1])) {:noreply, %{state | subscriptions: new_subscriptions}} endElixir side TypingState 05 • Phoenix in Nots
  35. 35. 05 • Phoenix in Nots
  36. 36. Fire event 🍰 (defn notify-typing [topic] (.push notify-typing-channel "notify-typing" topic)) (defn notify-stop-typing [topic] (.push notify-typing-channel "notify-stop-typing" topic)) 05 • Phoenix in Nots CLJS side
  37. 37. Into WS def handle_in("notify-typing", topic, socket) do user_id = socket.assigns[:user_id] Notsapp.TypingState.notify_typing(user_id, topic) {:noreply, socket} end def handle_in("notify-stop-typing", topic, socket) do user_id = socket.assigns[:user_id] Notsapp.TypingState.notify_stop_typing(user_id, topic) {:noreply, socket} end 05 • Phoenix in Nots Elixir side
  38. 38. Fan-out an event def handle_cast({:notify_typing, user_id, topic}, %{subscriptions: subscriptions, timers: timers} = state) do rec = Map.get(subscriptions, topic, []) for {socket, s_user_id} when s_user_id != user_id <- rec do Phoenix.Channel.push(socket, topic, %{user_id: user_id, message: "t"}) end timer = Process.send_after(__MODULE__, {:force_stop_typing, user_id, topic}, 30_000) {:noreply, %{state | timers: Map.update(timers, {topic, user_id}, timer, fn(old_timer)-> Process.cancel_timer(old_timer) timer end)}} end 05 • Phoenix in Nots Elixir side
  39. 39. Fan-out an event def handle_cast({:notify_stop_typing, user_id, topic}, %{subscriptions: subscriptions, timers: timers} = state) do rec = Map.get(subscriptions, topic, []) for {socket, s_user_id} when s_user_id != user_id <- rec do Phoenix.Channel.push(socket, topic, %{user_id: user_id, message: "s"}) end if timer = Map.get(timers, {topic, user_id}) do Process.cancel_timer(timer) end new_timers = Map.delete(timers, {topic, user_id}) {:noreply, %{state | timers: new_timers}} end 05 • Phoenix in Nots Elixir side
  40. 40. Cleaning up def handle_cast({:unsubscribe, user_id, topic}, %{subscriptions: subscriptions} = state) do rec = Map.get(subscriptions, topic, []) for {socket, s_user_id} when s_user_id != user_id <- rec do Phoenix.Channel.push(socket, topic, %{user_id: user_id, message: "s"}) end new_subscriptions = Map.update(subscriptions, topic, [], &(Enum.reject(&1, fn({_, s_user_id})-> user_id == s_user_id end))) {:noreply, %{state | subscriptions: new_subscriptions}} end 05 • Phoenix in Nots Elixir side
  41. 41. Cleaning up def handle_info(:cleanup_sockets, %{subscriptions: subscriptions} = state) do new_subscriptions = for {topic, subs} <- subscriptions, into: %{} do new_subs = Enum.filter(subs, fn({socket,_})-> Process.alive?(socket.transport_pid) end) if Enum.any?(new_subs), do: {topic, new_subs}, else: {nil, []} end |> Map.delete(nil) Process.send_after __MODULE__, :cleanup_sockets, @interval {:noreply, %{state | subscriptions: new_subscriptions}} end 05 • Phoenix in Nots Elixir side
  42. 42. Another way: Registry 05 • Phoenix in Nots {:ok, _} = Registry.start_link( keys: :duplicate, name: Registry.DispatcherTest) {:ok, _} = Registry.register( Registry.DispatcherTest, "hello", {IO, :inspect}) Registry.dispatch( Registry.DispatcherTest, "hello", fn entries -> for {pid, {module, function}} <- entries do apply(module, function, [pid]) end end)
  43. 43. Asynchronous Push not pull Via Websocket or H/2 push Stateful 05 • Phoenix in Nots
  44. 44. Phoenix.LiveView 05 • Phoenix in Nots https://www.youtube.com/watch?v=Z2DU0qLfPIY
  45. 45. Thank you 🤘
  46. 46. Alexander Khokhlov point@nots.io Nots.io nots.io blog.nots.io @nots_io facebook.com/nots.io

×