Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to Level Up in Elixir

259f23c3b129f07b0c496b9f0495f07e?s=47 jeg2
June 23, 2021

How to Level Up in Elixir

The July 2021 talk at Denver Erlang and Elixir by James Edward Gray II.

259f23c3b129f07b0c496b9f0495f07e?s=128

jeg2

June 23, 2021
Tweet

Transcript

  1. How to Level Up in Elixir James Edward Gray II

    — 6/5/2021 Designing a Chat Application
  2. James Edward Gray II • A longtime Rubyist • Creator

    of the Ruby Rogues podcast • An Elixir community regular for years now • I wrote a book about Elixir @JEG2
  3. I Work With Brett Who is on vacation… while I'm

    running his group?!
  4. None
  5. None
  6. None
  7. None
  8. How would you do it? The Evils of Thought Leadership

  9. The Communication Layer

  10. Socket Programming • A program has one "socket" connection with

    each remote system it is communicating with • Sockets are two-way, supporting both the sending and receiving of data • Data can arrive on any socket at anytime
  11. Multitasking

  12. Starting Processes Conway's Game of Life with one process per

    cell All socket handling in one GenServer
  13. Starting Processes Conway's Game of Life with one process per

    cell All socket handling in one GenServer
  14. Supervisor Supervisor ConnectionManager

  15. Supervisor Supervisor ConnectionManager Listener Connection Connection

  16. defmodule ChatApp.Application do @moduledoc false use Application @impl true def

    start(_type, _args) do children = [ { DynamicSupervisor , strategy: :one_for_one, name: ChatApp.ConnectionSupervisor } , {ChatApp.ConnectionManager, ui: ChatApp.GUI} , ChatApp.GUI ] opts = [strategy: :one_for_one, name: ChatApp.Supervisor] Supervisor.start_link(children, opts) end end
  17. defmodule ChatApp.Application do @moduledoc false use Application @impl true def

    start(_type, _args) do children = [ { DynamicSupervisor , strategy: :one_for_one, name: ChatApp.ConnectionSupervisor } , {ChatApp.ConnectionManager, ui: ChatApp.GUI} , ChatApp.GUI ] opts = [strategy: :one_for_one, name: ChatApp.Supervisor] Supervisor.start_link(children, opts) end end
  18. defmodule ChatApp.ConnectionManager do use GenServer alias ChatApp.{Connection, ConnectionSupervisor, Listener} #

    .. . defstruct mode: :ready, name: nil, ui: nil, me: nil def start_link(options) do GenServer.start_link( __MODULE__ , options , name: Keyword.get(options, :name, __MODULE__) ) end def init(options) do case Keyword.get(options, :ui) do ui when is_atom(ui) - > me = Keyword.get(options, :name, __MODULE__) || self() {:ok, %__MODULE__{ui: ui, me: me}} _no_ui - > {:stop, "ConnectionManager must be started with a UI module"} end end end
  19. defmodule ChatApp.ConnectionManager do use GenServer alias ChatApp.{Connection, ConnectionSupervisor, Listener} #

    .. . defstruct mode: :ready, name: nil, ui: nil, me: nil def start_link(options) do GenServer.start_link( __MODULE__ , options , name: Keyword.get(options, :name, __MODULE__) ) end def init(options) do case Keyword.get(options, :ui) do ui when is_atom(ui) - > me = Keyword.get(options, :name, __MODULE__) || self() {:ok, %__MODULE__{ui: ui, me: me}} _no_ui - > {:stop, "ConnectionManager must be started with a UI module"} end end end
  20. defmodule ChatApp.ConnectionManager do use GenServer alias ChatApp.{Connection, ConnectionSupervisor, Listener} #

    .. . defstruct mode: :ready, name: nil, ui: nil, me: nil def start_link(options) do GenServer.start_link( __MODULE__ , options , name: Keyword.get(options, :name, __MODULE__) ) end def init(options) do case Keyword.get(options, :ui) do ui when is_atom(ui) - > me = Keyword.get(options, :name, __MODULE__) || self() {:ok, %__MODULE__{ui: ui, me: me}} _no_ui - > {:stop, "ConnectionManager must be started with a UI module"} end end end
  21. defmodule ChatApp.ConnectionManager do use GenServer alias ChatApp.{Connection, ConnectionSupervisor, Listener} #

    .. . defstruct mode: :ready, name: nil, ui: nil, me: nil def start_link(options) do GenServer.start_link( __MODULE__ , options , name: Keyword.get(options, :name, __MODULE__) ) end def init(options) do case Keyword.get(options, :ui) do ui when is_atom(ui) - > me = Keyword.get(options, :name, __MODULE__) || self() {:ok, %__MODULE__{ui: ui, me: me}} _no_ui - > {:stop, "ConnectionManager must be started with a UI module"} end end end
  22. defmodule ChatApp.ConnectionManager do use GenServer alias ChatApp.{Connection, ConnectionSupervisor, Listener} #

    .. . defstruct mode: :ready, name: nil, ui: nil, me: nil def start_link(options) do GenServer.start_link( __MODULE__ , options , name: Keyword.get(options, :name, __MODULE__) ) end def init(options) do case Keyword.get(options, :ui) do ui when is_atom(ui) - > me = Keyword.get(options, :name, __MODULE__) || self() {:ok, %__MODULE__{ui: ui, me: me}} _no_ui - > {:stop, "ConnectionManager must be started with a UI module"} end end end
  23. defmodule ChatApp.ConnectionManager do # .. . def listen(manager \\ __MODULE__,

    port, name) do GenServer.call(manager, {:listen, port, name}) end def handle_call({:listen, port, name}, _from, state) do case start_listening(port, state.me) do :ok - > {:reply, :ok, %__MODULE__{state | mode: :host, name: name}} error - > {:reply, error, state} end end end
  24. defmodule ChatApp.ConnectionManager do # .. . def listen(manager \\ __MODULE__,

    port, name) do GenServer.call(manager, {:listen, port, name}) end def handle_call({:listen, port, name}, _from, state) do case start_listening(port, state.me) do :ok - > {:reply, :ok, %__MODULE__{state | mode: :host, name: name}} error - > {:reply, error, state} end end end
  25. defmodule ChatApp.ConnectionManager do # .. . def listen(manager \\ __MODULE__,

    port, name) do GenServer.call(manager, {:listen, port, name}) end def handle_call({:listen, port, name}, _from, state) do case start_listening(port, state.me) do :ok - > {:reply, :ok, %__MODULE__{state | mode: :host, name: name}} error - > {:reply, error, state} end end end
  26. defmodule ChatApp.ConnectionManager do # .. . def listen(manager \\ __MODULE__,

    port, name) do GenServer.call(manager, {:listen, port, name}) end def handle_call({:listen, port, name}, _from, state) do case start_listening(port, state.me) do :ok - > {:reply, :ok, %__MODULE__{state | mode: :host, name: name}} error - > {:reply, error, state} end end end
  27. Listening for Connections • A listening socket queues connections as

    they come in • A program can "accept" them to complete the connection • The result of accepting a connection is a read/write socket 
 used to communicate with the remote program • Erlang (now) provides a low-level `socket`, 
 but the higher-level `gen_tcp` adds several niceties
  28. defmodule ChatApp.ConnectionManager do # .. . @packet_size 2 defp start_listening(port,

    me) do case :gen_tcp.listen( port , [:binary, packet: @packet_size, active: false, reuseaddr: true] ) do {:ok, listening_socket} - > Listener.listen(listening_socket, me) error - > erro r end end end
  29. defmodule ChatApp.ConnectionManager do # .. . @packet_size 2 defp start_listening(port,

    me) do case :gen_tcp.listen( port , [:binary, packet: @packet_size, active: false, reuseaddr: true] ) do {:ok, listening_socket} - > Listener.listen(listening_socket, me) error - > erro r end end end
  30. defmodule ChatApp.ConnectionManager do # .. . @packet_size 2 defp start_listening(port,

    me) do case :gen_tcp.listen( port , [:binary, packet: @packet_size, active: false, reuseaddr: true] ) do {:ok, listening_socket} - > Listener.listen(listening_socket, me) error - > erro r end end end
  31. defmodule ChatApp.ConnectionManager do # .. . @packet_size 2 defp start_listening(port,

    me) do case :gen_tcp.listen( port , [:binary, packet: @packet_size, active: false, reuseaddr: true] ) do {:ok, listening_socket} - > Listener.listen(listening_socket, me) error - > erro r end end end
  32. defmodule ChatApp.Listener do use GenServer, restart: :transient alias ChatApp.{Connection, ConnectionSupervisor}

    # .. . def listen(listening_socket, manager) do case DynamicSupervisor.start_child( ConnectionSupervisor , {__MODULE__, [listening_socket, manager]} ) do {:ok, listener} - > transfer_control(listening_socket, listener) error - > erro r end end end
  33. defmodule ChatApp.Listener do use GenServer, restart: :transient alias ChatApp.{Connection, ConnectionSupervisor}

    # .. . def listen(listening_socket, manager) do case DynamicSupervisor.start_child( ConnectionSupervisor , {__MODULE__, [listening_socket, manager]} ) do {:ok, listener} - > transfer_control(listening_socket, listener) error - > erro r end end end
  34. defmodule ChatApp.Listener do use GenServer, restart: :transient alias ChatApp.{Connection, ConnectionSupervisor}

    # .. . def listen(listening_socket, manager) do case DynamicSupervisor.start_child( ConnectionSupervisor , {__MODULE__, [listening_socket, manager]} ) do {:ok, listener} - > transfer_control(listening_socket, listener) error - > erro r end end end
  35. defmodule ChatApp.Listener do # .. . defp transfer_control(listening_socket, listener) do

    case :gen_tcp.controlling_process(listening_socket, listener) do :ok - > accept(listener) :ok error - > close(listener) erro r end end defp accept(listener), do: GenServer.cast(listener, :accept) end
  36. defmodule ChatApp.Listener do # .. . defp transfer_control(listening_socket, listener) do

    case :gen_tcp.controlling_process(listening_socket, listener) do :ok - > accept(listener) :ok error - > close(listener) erro r end end defp accept(listener), do: GenServer.cast(listener, :accept) end
  37. defmodule ChatApp.Listener do # .. . defp transfer_control(listening_socket, listener) do

    case :gen_tcp.controlling_process(listening_socket, listener) do :ok - > accept(listener) :ok error - > close(listener) erro r end end defp accept(listener), do: GenServer.cast(listener, :accept) end
  38. defmodule ChatApp.Listener do # .. . defp transfer_control(listening_socket, listener) do

    case :gen_tcp.controlling_process(listening_socket, listener) do :ok - > accept(listener) :ok error - > close(listener) erro r end end defp accept(listener), do: GenServer.cast(listener, :accept) end
  39. A `GenServer` Loop • Keep triggering `handle_cast/ 2` or `handle_info/2`

    • Timeout long running code or execute it in a linked `Task` • `Process.send_after/2` and `:timer.send_interval/2` can repeat message sends • Process other messages between calls Danger Zone!
  40. defmodule ChatApp.Listener do # .. . def handle_cast(:accept, state) do

    case :gen_tcp.accept(state.listening_socket, 1_000) do {:ok, socket} - > Connection.listen(socket, state.manager) accept(self()) {:noreply, state} {:error, :closed} - > {:stop, :normal, %__MODULE__{state | listening_socket: nil}} _error - > accept(self()) {:noreply, state} end end end
  41. defmodule ChatApp.Listener do # .. . def handle_cast(:accept, state) do

    case :gen_tcp.accept(state.listening_socket, 1_000) do {:ok, socket} - > Connection.listen(socket, state.manager) accept(self()) {:noreply, state} {:error, :closed} - > {:stop, :normal, %__MODULE__{state | listening_socket: nil}} _error - > accept(self()) {:noreply, state} end end end
  42. defmodule ChatApp.Listener do # .. . def handle_cast(:accept, state) do

    case :gen_tcp.accept(state.listening_socket, 1_000) do {:ok, socket} - > Connection.listen(socket, state.manager) accept(self()) {:noreply, state} {:error, :closed} - > {:stop, :normal, %__MODULE__{state | listening_socket: nil}} _error - > accept(self()) {:noreply, state} end end end
  43. defmodule ChatApp.Listener do # .. . def handle_cast(:accept, state) do

    case :gen_tcp.accept(state.listening_socket, 1_000) do {:ok, socket} - > Connection.listen(socket, state.manager) accept(self()) {:noreply, state} {:error, :closed} - > {:stop, :normal, %__MODULE__{state | listening_socket: nil}} _error - > accept(self()) {:noreply, state} end end end
  44. defmodule ChatApp.Listener do # .. . def handle_cast(:accept, state) do

    case :gen_tcp.accept(state.listening_socket, 1_000) do {:ok, socket} - > Connection.listen(socket, state.manager) accept(self()) {:noreply, state} {:error, :closed} - > {:stop, :normal, %__MODULE__{state | listening_socket: nil}} _error - > accept(self()) {:noreply, state} end end end
  45. defmodule ChatApp.Listener do # .. . defstruct ~w[listening_socket manager] a

    def start_link([listening_socket, manager]) do GenServer.start_link(__MODULE__, [listening_socket, manager]) end def close(listener), do: GenServer.cast(listener, :close) def init([listening_socket, manager]) do {:ok, %__MODULE__{listening_socket: listening_socket, manager: manager}} end def handle_cast(:close, state) do :gen_tcp.close(state.listening_socket) {:stop, :normal, %__MODULE__{state | listening_socket: nil}} end end
  46. defmodule ChatApp.Listener do # .. . defstruct ~w[listening_socket manager] a

    def start_link([listening_socket, manager]) do GenServer.start_link(__MODULE__, [listening_socket, manager]) end def close(listener), do: GenServer.cast(listener, :close) def init([listening_socket, manager]) do {:ok, %__MODULE__{listening_socket: listening_socket, manager: manager}} end def handle_cast(:close, state) do :gen_tcp.close(state.listening_socket) {:stop, :normal, %__MODULE__{state | listening_socket: nil}} end end
  47. defmodule ChatApp.ConnectionManager do # .. . def connect(manager \\ __MODULE__,

    host, port, name) do GenServer.call(manager, {:connect, host, port, name}) end def handle_call({:connect, host, port, name}, _from, state) do case listen_to_connection(host, port, state.me) do :ok - > new_state = %__MODULE__{state | mode: :client, name: name} queue_all_sends(:connected, new_state) {:reply, :ok, new_state} error - > {:reply, error, state} end end end
  48. defmodule ChatApp.ConnectionManager do # .. . def connect(manager \\ __MODULE__,

    host, port, name) do GenServer.call(manager, {:connect, host, port, name}) end def handle_call({:connect, host, port, name}, _from, state) do case listen_to_connection(host, port, state.me) do :ok - > new_state = %__MODULE__{state | mode: :client, name: name} queue_all_sends(:connected, new_state) {:reply, :ok, new_state} error - > {:reply, error, state} end end end
  49. defmodule ChatApp.ConnectionManager do # .. . def connect(manager \\ __MODULE__,

    host, port, name) do GenServer.call(manager, {:connect, host, port, name}) end def handle_call({:connect, host, port, name}, _from, state) do case listen_to_connection(host, port, state.me) do :ok - > new_state = %__MODULE__{state | mode: :client, name: name} queue_all_sends(:connected, new_state) {:reply, :ok, new_state} error - > {:reply, error, state} end end end
  50. defmodule ChatApp.ConnectionManager do # .. . def connect(manager \\ __MODULE__,

    host, port, name) do GenServer.call(manager, {:connect, host, port, name}) end def handle_call({:connect, host, port, name}, _from, state) do case listen_to_connection(host, port, state.me) do :ok - > new_state = %__MODULE__{state | mode: :client, name: name} queue_all_sends(:connected, new_state) {:reply, :ok, new_state} error - > {:reply, error, state} end end end
  51. defmodule ChatApp.ConnectionManager do # .. . defp listen_to_connection(host, port, me)

    do case :gen_tcp.connect( String.to_charlist(host) , port , [:binary, packet: @packet_size, active: false] ) do {:ok, socket} - > Connection.listen(socket, me) error - > erro r end end end
  52. defmodule ChatApp.ConnectionManager do # .. . defp listen_to_connection(host, port, me)

    do case :gen_tcp.connect( String.to_charlist(host) , port , [:binary, packet: @packet_size, active: false] ) do {:ok, socket} - > Connection.listen(socket, me) error - > erro r end end end
  53. defmodule ChatApp.ConnectionManager do # .. . defp listen_to_connection(host, port, me)

    do case :gen_tcp.connect( String.to_charlist(host) , port , [:binary, packet: @packet_size, active: false] ) do {:ok, socket} - > Connection.listen(socket, me) error - > erro r end end end
  54. Protocols in Protocols • TCP/IP is a protocol for reliably

    delivering messages over an unreliable network • How do we know what a full message is though? • Delimit messages with something like newlines • Send the length of the message, then the message 
 (`:gen_tcp` does this!) • How do we know what's in a message? • Erlang's `term_to_binary/1`
  55. None
  56. None
  57. None
  58. None
  59. None
  60. None
  61. None
  62. defmodule ChatApp.Connection do use GenServer, restart: :transient alias ChatApp.{ConnectionManager, ConnectionSupervisor}

    # .. . defstruct ~w[socket manager] a def start_link([socket, manager]) do GenServer.start_link(__MODULE__, [socket, manager]) end def close(connection), do: GenServer.cast(connection, :close) def init([socket, manager]) do {:ok, %__MODULE__{socket: socket, manager: manager}} end def handle_cast(:close, state) do :gen_tcp.close(state.socket) {:stop, :normal, %__MODULE__{state | socket: nil}} end end
  63. defmodule ChatApp.Connection do use GenServer, restart: :transient alias ChatApp.{ConnectionManager, ConnectionSupervisor}

    # .. . defstruct ~w[socket manager] a def start_link([socket, manager]) do GenServer.start_link(__MODULE__, [socket, manager]) end def close(connection), do: GenServer.cast(connection, :close) def init([socket, manager]) do {:ok, %__MODULE__{socket: socket, manager: manager}} end def handle_cast(:close, state) do :gen_tcp.close(state.socket) {:stop, :normal, %__MODULE__{state | socket: nil}} end end
  64. defmodule ChatApp.Connection do # .. . def listen(socket, manager) do

    case DynamicSupervisor.start_child( ConnectionSupervisor , {__MODULE__, [socket, manager]} ) do {:ok, connection} - > transfer_control(socket, connection) error - > erro r end end end
  65. defmodule ChatApp.Connection do # .. . def listen(socket, manager) do

    case DynamicSupervisor.start_child( ConnectionSupervisor , {__MODULE__, [socket, manager]} ) do {:ok, connection} - > transfer_control(socket, connection) error - > erro r end end end
  66. defmodule ChatApp.Connection do # .. . def handle_cast(:activate, state) do

    :inet.setopts(state.socket, active: :once) {:noreply, state} end defp transfer_control(socket, connection) do case :gen_tcp.controlling_process(socket, connection) do :ok - > activate(connection) :ok error - > close(connection) erro r end end defp activate(connection), do: GenServer.cast(connection, :activate) end
  67. defmodule ChatApp.Connection do # .. . def handle_cast(:activate, state) do

    :inet.setopts(state.socket, active: :once) {:noreply, state} end defp transfer_control(socket, connection) do case :gen_tcp.controlling_process(socket, connection) do :ok - > activate(connection) :ok error - > close(connection) erro r end end defp activate(connection), do: GenServer.cast(connection, :activate) end
  68. defmodule ChatApp.Connection do # .. . def handle_cast(:activate, state) do

    :inet.setopts(state.socket, active: :once) {:noreply, state} end defp transfer_control(socket, connection) do case :gen_tcp.controlling_process(socket, connection) do :ok - > activate(connection) :ok error - > close(connection) erro r end end defp activate(connection), do: GenServer.cast(connection, :activate) end
  69. TCP Event Messages • `gen_tcp` can deliver events—data arriving, a

    socket closing, 
 and the like—as messages to a process • Of course, a process could drown in incoming messages • `active: :once` controls the f low
  70. defmodule ChatApp.Connection do # .. . def handle_info({:tcp_closed, _socket}, state)

    do {:stop, :normal, %__MODULE__{state | socket: nil}} end def handle_info({:tcp, _socket, message}, state) do ConnectionManager.receive_message(state.manager, message, self()) activate(self()) {:noreply, state} end def handle_info(_unexpected_message, state), do: {:noreply, state} end
  71. defmodule ChatApp.Connection do # .. . def handle_info({:tcp_closed, _socket}, state)

    do {:stop, :normal, %__MODULE__{state | socket: nil}} end def handle_info({:tcp, _socket, message}, state) do ConnectionManager.receive_message(state.manager, message, self()) activate(self()) {:noreply, state} end def handle_info(_unexpected_message, state), do: {:noreply, state} end
  72. defmodule ChatApp.Connection do # .. . def handle_info({:tcp_closed, _socket}, state)

    do {:stop, :normal, %__MODULE__{state | socket: nil}} end def handle_info({:tcp, _socket, message}, state) do ConnectionManager.receive_message(state.manager, message, self()) activate(self()) {:noreply, state} end def handle_info(_unexpected_message, state), do: {:noreply, state} end
  73. defmodule ChatApp.Connection do # .. . def handle_info({:tcp_closed, _socket}, state)

    do {:stop, :normal, %__MODULE__{state | socket: nil}} end def handle_info({:tcp, _socket, message}, state) do ConnectionManager.receive_message(state.manager, message, self()) activate(self()) {:noreply, state} end def handle_info(_unexpected_message, state), do: {:noreply, state} end
  74. defmodule ChatApp.Connection do # .. . def handle_info({:tcp_closed, _socket}, state)

    do {:stop, :normal, %__MODULE__{state | socket: nil}} end def handle_info({:tcp, _socket, message}, state) do ConnectionManager.receive_message(state.manager, message, self()) activate(self()) {:noreply, state} end def handle_info(_unexpected_message, state), do: {:noreply, state} end
  75. defmodule ChatApp.Connection do # .. . def handle_info({:tcp_closed, _socket}, state)

    do {:stop, :normal, %__MODULE__{state | socket: nil}} end def handle_info({:tcp, _socket, message}, state) do ConnectionManager.receive_message(state.manager, message, self()) activate(self()) {:noreply, state} end def handle_info(_unexpected_message, state), do: {:noreply, state} end
  76. defmodule ChatApp.Connection do # .. . def handle_info({:tcp_closed, _socket}, state)

    do {:stop, :normal, %__MODULE__{state | socket: nil}} end def handle_info({:tcp, _socket, message}, state) do ConnectionManager.receive_message(state.manager, message, self()) activate(self()) {:noreply, state} end def handle_info(_unexpected_message, state), do: {:noreply, state} end
  77. defmodule ChatApp.ConnectionManager do # .. . def receive_message(manager \\ __MODULE__,

    message, from) do GenServer.cast(manager, {:receive_message, message, from}) end def handle_cast({:receive_message, message, from}, state) do if state.mode == :host do for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} when pid != from - > Connection.queue_send(pid, message) _listener_or_from - > :ok end) end {name, content} = :erlang.binary_to_term(message) state.ui.show_chat_message(name, content) {:noreply, state} end end
  78. defmodule ChatApp.ConnectionManager do # .. . def receive_message(manager \\ __MODULE__,

    message, from) do GenServer.cast(manager, {:receive_message, message, from}) end def handle_cast({:receive_message, message, from}, state) do if state.mode == :host do for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} when pid != from - > Connection.queue_send(pid, message) _listener_or_from - > :ok end) end {name, content} = :erlang.binary_to_term(message) state.ui.show_chat_message(name, content) {:noreply, state} end end
  79. defmodule ChatApp.ConnectionManager do # .. . def receive_message(manager \\ __MODULE__,

    message, from) do GenServer.cast(manager, {:receive_message, message, from}) end def handle_cast({:receive_message, message, from}, state) do if state.mode == :host do for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} when pid != from - > Connection.queue_send(pid, message) _listener_or_from - > :ok end) end {name, content} = :erlang.binary_to_term(message) state.ui.show_chat_message(name, content) {:noreply, state} end end
  80. defmodule ChatApp.ConnectionManager do # .. . def receive_message(manager \\ __MODULE__,

    message, from) do GenServer.cast(manager, {:receive_message, message, from}) end def handle_cast({:receive_message, message, from}, state) do if state.mode == :host do for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} when pid != from - > Connection.queue_send(pid, message) _listener_or_from - > :ok end) end {name, content} = :erlang.binary_to_term(message) state.ui.show_chat_message(name, content) {:noreply, state} end end
  81. defmodule ChatApp.ConnectionManager do # .. . def receive_message(manager \\ __MODULE__,

    message, from) do GenServer.cast(manager, {:receive_message, message, from}) end def handle_cast({:receive_message, message, from}, state) do if state.mode == :host do for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} when pid != from - > Connection.queue_send(pid, message) _listener_or_from - > :ok end) end {name, content} = :erlang.binary_to_term(message) state.ui.show_chat_message(name, content) {:noreply, state} end end
  82. defmodule ChatApp.ConnectionManager do # .. . defp for_active_connections(func) do ConnectionSupervisor

    |> DynamicSupervisor.which_children() |> Enum. fi lter(fn {:unde fi ned, pid_or_restarting, :worker, _modules} - > is_pid(pid_or_restarting) end) |> Enum.each(func) end end
  83. defmodule ChatApp.ConnectionManager do # .. . defp for_active_connections(func) do ConnectionSupervisor

    |> DynamicSupervisor.which_children() |> Enum. fi lter(fn {:unde fi ned, pid_or_restarting, :worker, _modules} - > is_pid(pid_or_restarting) end) |> Enum.each(func) end end
  84. defmodule ChatApp.ConnectionManager do # .. . defp for_active_connections(func) do ConnectionSupervisor

    |> DynamicSupervisor.which_children() |> Enum. fi lter(fn {:unde fi ned, pid_or_restarting, :worker, _modules} - > is_pid(pid_or_restarting) end) |> Enum.each(func) end end
  85. Nesting Cast and Call • You can't nest a "call"

    to the same `GenServer` in another call (deadlock) • When nesting calls to other processes, consider the effect on timeouts • You can always nest a "cast" or `send/2` (`handle_info/2`) • Remember to report cast failure out-of-band when needed • Consider adding backpressure to protect the receiver from drowning
  86. defmodule ChatApp.ConnectionManager do # .. . def send_to_all(manager \\ __MODULE__,

    message) do GenServer.call(manager, {:send_to_all, message}) end def handle_call({:send_to_all, message}, _from, state) do result = queue_all_sends(message, state) {:reply, result, state} end end
  87. defmodule ChatApp.ConnectionManager do # .. . def send_to_all(manager \\ __MODULE__,

    message) do GenServer.call(manager, {:send_to_all, message}) end def handle_call({:send_to_all, message}, _from, state) do result = queue_all_sends(message, state) {:reply, result, state} end end
  88. defmodule ChatApp.ConnectionManager do # .. . defp queue_all_sends(message, %__MODULE__{mode: mode}

    = state) when mode in ~w[host client]a do ref = make_ref() prepared_message = :erlang.term_to_binary({state.name, message}) for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} - > Connection.queue_send(pid, ref, prepared_message) _listener - > :ok end) {ref, state.name} end defp queue_all_sends(_message, _state), do: nil end
  89. defmodule ChatApp.ConnectionManager do # .. . defp queue_all_sends(message, %__MODULE__{mode: mode}

    = state) when mode in ~w[host client]a do ref = make_ref() prepared_message = :erlang.term_to_binary({state.name, message}) for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} - > Connection.queue_send(pid, ref, prepared_message) _listener - > :ok end) {ref, state.name} end defp queue_all_sends(_message, _state), do: nil end
  90. defmodule ChatApp.ConnectionManager do # .. . defp queue_all_sends(message, %__MODULE__{mode: mode}

    = state) when mode in ~w[host client]a do ref = make_ref() prepared_message = :erlang.term_to_binary({state.name, message}) for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} - > Connection.queue_send(pid, ref, prepared_message) _listener - > :ok end) {ref, state.name} end defp queue_all_sends(_message, _state), do: nil end
  91. defmodule ChatApp.ConnectionManager do # .. . defp queue_all_sends(message, %__MODULE__{mode: mode}

    = state) when mode in ~w[host client]a do ref = make_ref() prepared_message = :erlang.term_to_binary({state.name, message}) for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} - > Connection.queue_send(pid, ref, prepared_message) _listener - > :ok end) {ref, state.name} end defp queue_all_sends(_message, _state), do: nil end
  92. defmodule ChatApp.ConnectionManager do # .. . defp queue_all_sends(message, %__MODULE__{mode: mode}

    = state) when mode in ~w[host client]a do ref = make_ref() prepared_message = :erlang.term_to_binary({state.name, message}) for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} - > Connection.queue_send(pid, ref, prepared_message) _listener - > :ok end) {ref, state.name} end defp queue_all_sends(_message, _state), do: nil end
  93. defmodule ChatApp.ConnectionManager do # .. . defp queue_all_sends(message, %__MODULE__{mode: mode}

    = state) when mode in ~w[host client]a do ref = make_ref() prepared_message = :erlang.term_to_binary({state.name, message}) for_active_connections(fn {:unde fi ned, pid, :worker, [Connection]} - > Connection.queue_send(pid, ref, prepared_message) _listener - > :ok end) {ref, state.name} end defp queue_all_sends(_message, _state), do: nil end
  94. defmodule ChatApp.Connection do # .. . def queue_send(connection, message), do:

    queue_send(connection, nil, message) def queue_send(connection, message_id, message) do GenServer.cast(connection, {:queue_send, message_id, message}) end def handle_cast({:queue_send, message_id, message}, state) do case :gen_tcp.send(state.socket, message) do :ok - > :ok error - > if is_reference(message_id) do ConnectionManager.receive_send_error(state.manager, message_id, error) end end {:noreply, state} end end
  95. defmodule ChatApp.Connection do # .. . def queue_send(connection, message), do:

    queue_send(connection, nil, message) def queue_send(connection, message_id, message) do GenServer.cast(connection, {:queue_send, message_id, message}) end def handle_cast({:queue_send, message_id, message}, state) do case :gen_tcp.send(state.socket, message) do :ok - > :ok error - > if is_reference(message_id) do ConnectionManager.receive_send_error(state.manager, message_id, error) end end {:noreply, state} end end
  96. defmodule ChatApp.Connection do # .. . def queue_send(connection, message), do:

    queue_send(connection, nil, message) def queue_send(connection, message_id, message) do GenServer.cast(connection, {:queue_send, message_id, message}) end def handle_cast({:queue_send, message_id, message}, state) do case :gen_tcp.send(state.socket, message) do :ok - > :ok error - > if is_reference(message_id) do ConnectionManager.receive_send_error(state.manager, message_id, error) end end {:noreply, state} end end
  97. defmodule ChatApp.ConnectionManager do # .. . def receive_send_error(manager \\ __MODULE__,

    message_id, error) do GenServer.cast(manager, {:receive_send_error, message_id, error}) end def handle_cast({:receive_send_error, message_id, _error}, state) do state.ui.show_send_failure(message_id) {:noreply, state} end end
  98. defmodule ChatApp.ConnectionManager do # .. . def receive_send_error(manager \\ __MODULE__,

    message_id, error) do GenServer.cast(manager, {:receive_send_error, message_id, error}) end def handle_cast({:receive_send_error, message_id, _error}, state) do state.ui.show_send_failure(message_id) {:noreply, state} end end
  99. One More Thing…

  100. defmodule ChatApp.ConnectionManager do # .. . def reset(manager \\ __MODULE__),

    do: GenServer.call(manager, :reset) def handle_call(:reset, _from, state) do queue_all_sends(:disconnected, state) for_active_connections(fn {:unde fi ned, pid, :worker, [module]} - > apply(module, :close, [pid]) end) {:reply, :ok, %__MODULE__{}} end end
  101. defmodule ChatApp.ConnectionManager do # .. . def reset(manager \\ __MODULE__),

    do: GenServer.call(manager, :reset) def handle_call(:reset, _from, state) do queue_all_sends(:disconnected, state) for_active_connections(fn {:unde fi ned, pid, :worker, [module]} - > apply(module, :close, [pid]) end) {:reply, :ok, %__MODULE__{}} end end
  102. defmodule ChatApp.ConnectionManager do # .. . def reset(manager \\ __MODULE__),

    do: GenServer.call(manager, :reset) def handle_call(:reset, _from, state) do queue_all_sends(:disconnected, state) for_active_connections(fn {:unde fi ned, pid, :worker, [module]} - > apply(module, :close, [pid]) end) {:reply, :ok, %__MODULE__{}} end end
  103. Let's Try It! I'm sure Brett won't mind if we

    give him a call…
  104. None
  105. None
  106. None
  107. None
  108. None
  109. None
  110. The User Interface

  111. The UI Problem • Will a Terminal interface work for

    this challenge? • What happens when a message arrives while we're typing? • This is another side of the multitasking challenge
  112. None
  113. None
  114. None
  115. Cheating? • It is possible to use a Terminal •

    Terminals default to operation in "cooked" mode • This reads lines of input at a time and more • It's possible to switch to "raw" mode, say by shelling out to `stty` • In raw mode, you can read a character at a time • This allows keeping track of what has been entered so you can clear the screen and rerender as messages arrive
  116. Plan B

  117. defmodule ChatApp.GUI do use GenServer use Bitwise require Record alias

    ChatApp.ConnectionManager # constants from wx/include/wx.hr l @default 70 @multiline 32 @rich 32768 @horizontal 4 @vertical 8 @left 16 @right 32 @up 64 @down 128 @all @left ||| @right ||| @up ||| @down @expand 8192 @return_key 13 # .. . end
  118. defmodule ChatApp.GUI do use GenServer use Bitwise require Record alias

    ChatApp.ConnectionManager # constants from wx/include/wx.hr l @default 70 @multiline 32 @rich 32768 @horizontal 4 @vertical 8 @left 16 @right 32 @up 64 @down 128 @all @left ||| @right ||| @up ||| @down @expand 8192 @return_key 13 # .. . end
  119. defmodule ChatApp.GUI do # .. . defstruct window: nil ,

    chat: nil , bold: nil , italic: nil , input: nil , button: nil , active_sends: Map.new() def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) def init([]) do :timer.send_interval(5 * 60 * 1_000, :prune_active_sends) {:ok, %__MODULE__{}, {:continue, :show_gui}} end end
  120. defmodule ChatApp.GUI do # .. . defstruct window: nil ,

    chat: nil , bold: nil , italic: nil , input: nil , button: nil , active_sends: Map.new() def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) def init([]) do :timer.send_interval(5 * 60 * 1_000, :prune_active_sends) {:ok, %__MODULE__{}, {:continue, :show_gui}} end end
  121. defmodule ChatApp.GUI do # .. . defstruct window: nil ,

    chat: nil , bold: nil , italic: nil , input: nil , button: nil , active_sends: Map.new() def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) def init([]) do :timer.send_interval(5 * 60 * 1_000, :prune_active_sends) {:ok, %__MODULE__{}, {:continue, :show_gui}} end end
  122. defmodule ChatApp.GUI do # .. . defstruct window: nil ,

    chat: nil , bold: nil , italic: nil , input: nil , button: nil , active_sends: Map.new() def start_link([]), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) def init([]) do :timer.send_interval(5 * 60 * 1_000, :prune_active_sends) {:ok, %__MODULE__{}, {:continue, :show_gui}} end end
  123. defmodule ChatApp.GUI do # .. . def handle_continue(:show_gui, state) do

    wx = :wx.new() gui = :wx.batch(fn -> prepare_gui(wx) end) :wxWindow.show(gui.window) { :noreply , %__MODULE__{ stat e | window: gui.window , chat: gui.chat , bold: gui.bold , italic: gui.italic , input: gui.input , button: gui.butto n } } end defp prepare_gui(wx) do w x |> build_gui() |> layout_gui() |> setup_events() end end
  124. defmodule ChatApp.GUI do # .. . def handle_continue(:show_gui, state) do

    wx = :wx.new() gui = :wx.batch(fn -> prepare_gui(wx) end) :wxWindow.show(gui.window) { :noreply , %__MODULE__{ stat e | window: gui.window , chat: gui.chat , bold: gui.bold , italic: gui.italic , input: gui.input , button: gui.butto n } } end defp prepare_gui(wx) do w x |> build_gui() |> layout_gui() |> setup_events() end end
  125. defmodule ChatApp.GUI do # .. . def handle_continue(:show_gui, state) do

    wx = :wx.new() gui = :wx.batch(fn -> prepare_gui(wx) end) :wxWindow.show(gui.window) { :noreply , %__MODULE__{ stat e | window: gui.window , chat: gui.chat , bold: gui.bold , italic: gui.italic , input: gui.input , button: gui.butto n } } end defp prepare_gui(wx) do w x |> build_gui() |> layout_gui() |> setup_events() end end
  126. defmodule ChatApp.GUI do # .. . defp build_gui(wx) do window

    = :wxFrame.new(wx, -1, "Chat", size: {800, 600}) controls = :wxPanel.new(window) chat = :wxTextCtrl.new(controls, 1, style: @multiline ||| @rich) :wxTextCtrl.setEditable(chat, false) bold = :wxNORMAL_FONT |> :wxe_util.get_const() |> :wxFont.new() :wxFont.setWeight(bold, :wxe_util.get_const(:wxFONTWEIGHT_BOLD)) italic = :wxe_util.get_const(:wxITALIC_FONT) c = "Commands:\n /listen PORT NAME\n /connect HOST PORT NAME\n /quit\n" append_text_with_font(chat, c, italic) form = :wxPanel.new(controls) input = :wxTextCtrl.new(form, 2, style: @default) button = :wxButton.new(form, 3, label: "Send") %{ window: window , controls: controls , chat: chat , bold: bold , italic: italic , form: form , input: input , button: butto n } end end
  127. defmodule ChatApp.GUI do # .. . defp build_gui(wx) do window

    = :wxFrame.new(wx, -1, "Chat", size: {800, 600}) controls = :wxPanel.new(window) chat = :wxTextCtrl.new(controls, 1, style: @multiline ||| @rich) :wxTextCtrl.setEditable(chat, false) bold = :wxNORMAL_FONT |> :wxe_util.get_const() |> :wxFont.new() :wxFont.setWeight(bold, :wxe_util.get_const(:wxFONTWEIGHT_BOLD)) italic = :wxe_util.get_const(:wxITALIC_FONT) c = "Commands:\n /listen PORT NAME\n /connect HOST PORT NAME\n /quit\n" append_text_with_font(chat, c, italic) form = :wxPanel.new(controls) input = :wxTextCtrl.new(form, 2, style: @default) button = :wxButton.new(form, 3, label: "Send") %{ window: window , controls: controls , chat: chat , bold: bold , italic: italic , form: form , input: input , button: butto n } end end
  128. defmodule ChatApp.GUI do # .. . defp build_gui(wx) do window

    = :wxFrame.new(wx, -1, "Chat", size: {800, 600}) controls = :wxPanel.new(window) chat = :wxTextCtrl.new(controls, 1, style: @multiline ||| @rich) :wxTextCtrl.setEditable(chat, false) bold = :wxNORMAL_FONT |> :wxe_util.get_const() |> :wxFont.new() :wxFont.setWeight(bold, :wxe_util.get_const(:wxFONTWEIGHT_BOLD)) italic = :wxe_util.get_const(:wxITALIC_FONT) c = "Commands:\n /listen PORT NAME\n /connect HOST PORT NAME\n /quit\n" append_text_with_font(chat, c, italic) form = :wxPanel.new(controls) input = :wxTextCtrl.new(form, 2, style: @default) button = :wxButton.new(form, 3, label: "Send") %{ window: window , controls: controls , chat: chat , bold: bold , italic: italic , form: form , input: input , button: butto n } end end
  129. defmodule ChatApp.GUI do # .. . defp append_text(ctrl, text) do

    :wxTextCtrl.appendText(ctrl, text) end defp append_text_with_font(ctrl, text, font) do style = :wxTextCtrl.getDefaultStyle(ctrl) :wxTextAttr.setFont(style, font) :wxTextCtrl.setDefaultStyle(ctrl, style) append_text(ctrl, text) :wxTextAttr.setFont(style, :wxe_util.get_const(:wxNORMAL_FONT)) :wxTextCtrl.setDefaultStyle(ctrl, style) end end
  130. defmodule ChatApp.GUI do # .. . defp append_text(ctrl, text) do

    :wxTextCtrl.appendText(ctrl, text) end defp append_text_with_font(ctrl, text, font) do style = :wxTextCtrl.getDefaultStyle(ctrl) :wxTextAttr.setFont(style, font) :wxTextCtrl.setDefaultStyle(ctrl, style) append_text(ctrl, text) :wxTextAttr.setFont(style, :wxe_util.get_const(:wxNORMAL_FONT)) :wxTextCtrl.setDefaultStyle(ctrl, style) end end
  131. defmodule ChatApp.GUI do # .. . defp layout_gui(gui) do window_sizer

    = :wxBoxSizer.new(@vertical) :wxSizer.add( window_sizer , gui.controls , border: 4 , proportion: 1 , fl ag: @expand ||| @all ) controls_sizer = :wxBoxSizer.new(@vertical) :wxSizer.add(controls_sizer, gui.chat, proportion: 1, fl ag: @expand) :wxSizer.addSpacer(controls_sizer, 4) :wxSizer.add(controls_sizer, gui.form, proportion: 0, fl ag: @expand) form_sizer = :wxBoxSizer.new(@horizontal) :wxSizer.add(form_sizer, gui.input, proportion: 1, fl ag: @expand) :wxSizer.addSpacer(controls_sizer, 4) :wxSizer.add(form_sizer, gui.button) :wxWindow.setSizer(gui.form, form_sizer) :wxWindow.setSizer(gui.controls, controls_sizer) :wxWindow.setSizer(gui.window, window_sizer) gu i end end
  132. defmodule ChatApp.GUI do # .. . defp layout_gui(gui) do window_sizer

    = :wxBoxSizer.new(@vertical) :wxSizer.add( window_sizer , gui.controls , border: 4 , proportion: 1 , fl ag: @expand ||| @all ) controls_sizer = :wxBoxSizer.new(@vertical) :wxSizer.add(controls_sizer, gui.chat, proportion: 1, fl ag: @expand) :wxSizer.addSpacer(controls_sizer, 4) :wxSizer.add(controls_sizer, gui.form, proportion: 0, fl ag: @expand) form_sizer = :wxBoxSizer.new(@horizontal) :wxSizer.add(form_sizer, gui.input, proportion: 1, fl ag: @expand) :wxSizer.addSpacer(controls_sizer, 4) :wxSizer.add(form_sizer, gui.button) :wxWindow.setSizer(gui.form, form_sizer) :wxWindow.setSizer(gui.controls, controls_sizer) :wxWindow.setSizer(gui.window, window_sizer) gu i end end
  133. defmodule ChatApp.GUI do # .. . defp setup_events(gui) do :wxWindow.setFocus(gui.input)

    :wxEvtHandler.connect(gui.window, :close_window) :wxEvtHandler.connect(gui.input, :key_down, skip: true) :wxEvtHandler.connect(gui.button, :command_button_clicked) gu i end end
  134. defmodule ChatApp.GUI do # .. . defp setup_events(gui) do :wxWindow.setFocus(gui.input)

    :wxEvtHandler.connect(gui.window, :close_window) :wxEvtHandler.connect(gui.input, :key_down, skip: true) :wxEvtHandler.connect(gui.button, :command_button_clicked) gu i end end
  135. defmodule ChatApp.GUI do # .. . defp setup_events(gui) do :wxWindow.setFocus(gui.input)

    :wxEvtHandler.connect(gui.window, :close_window) :wxEvtHandler.connect(gui.input, :key_down, skip: true) :wxEvtHandler.connect(gui.button, :command_button_clicked) gu i end end
  136. Records

  137. Records

  138. Records

  139. Records

  140. Records

  141. Records

  142. defmodule ChatApp.GUI do # .. . Record.extract_all(from_lib: "wx/include/wx.hrl") |> Enum.map(fn

    {name, fi elds} -> Record.defrecordp(name, fi elds) end) def handle_info( wx( id: _id , obj: window , userData: _userData , event: wxClose(type: :close_window) ) , %__MODULE__{window: window} = stat e ) do quit(state) {:noreply, state} end defp quit(state) do ConnectionManager.reset() :wxWindow.destroy(state.window) :wx.destroy() System.stop(0) end end
  143. defmodule ChatApp.GUI do # .. . Record.extract_all(from_lib: "wx/include/wx.hrl") |> Enum.map(fn

    {name, fi elds} -> Record.defrecordp(name, fi elds) end) def handle_info( wx( id: _id , obj: window , userData: _userData , event: wxClose(type: :close_window) ) , %__MODULE__{window: window} = stat e ) do quit(state) {:noreply, state} end defp quit(state) do ConnectionManager.reset() :wxWindow.destroy(state.window) :wx.destroy() System.stop(0) end end
  144. defmodule ChatApp.GUI do # .. . Record.extract_all(from_lib: "wx/include/wx.hrl") |> Enum.map(fn

    {name, fi elds} -> Record.defrecordp(name, fi elds) end) def handle_info( wx( id: _id , obj: window , userData: _userData , event: wxClose(type: :close_window) ) , %__MODULE__{window: window} = stat e ) do quit(state) {:noreply, state} end defp quit(state) do ConnectionManager.reset() :wxWindow.destroy(state.window) :wx.destroy() System.stop(0) end end
  145. defmodule ChatApp.GUI do # .. . Record.extract_all(from_lib: "wx/include/wx.hrl") |> Enum.map(fn

    {name, fi elds} -> Record.defrecordp(name, fi elds) end) def handle_info( wx( id: _id , obj: window , userData: _userData , event: wxClose(type: :close_window) ) , %__MODULE__{window: window} = stat e ) do quit(state) {:noreply, state} end defp quit(state) do ConnectionManager.reset() :wxWindow.destroy(state.window) :wx.destroy() System.stop(0) end end
  146. defmodule ChatApp.GUI do # .. . def handle_info( wx( id:

    _id , obj: input , userData: _userData , event: wxKey( type: :key_down , x: _x , y: _y , keyCode: @return_key , controlDown: false , shiftDown: false , altDown: false , metaDown: false , uniChar: _uniChar , rawCode: _rawCode , rawFlags: _rawFlags ) ) , %__MODULE__{input: input} = stat e ) do new_active_sends = process_input(state) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end end
  147. defmodule ChatApp.GUI do # .. . def handle_info( wx( id:

    _id , obj: input , userData: _userData , event: wxKey( type: :key_down , x: _x , y: _y , keyCode: @return_key , controlDown: false , shiftDown: false , altDown: false , metaDown: false , uniChar: _uniChar , rawCode: _rawCode , rawFlags: _rawFlags ) ) , %__MODULE__{input: input} = stat e ) do new_active_sends = process_input(state) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end end
  148. defmodule ChatApp.GUI do # .. . def handle_info( wx( id:

    _id , obj: input , userData: _userData , event: wxKey( type: :key_down , x: _x , y: _y , keyCode: @return_key , controlDown: false , shiftDown: false , altDown: false , metaDown: false , uniChar: _uniChar , rawCode: _rawCode , rawFlags: _rawFlags ) ) , %__MODULE__{input: input} = stat e ) do new_active_sends = process_input(state) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end end
  149. defmodule ChatApp.GUI do # .. . def handle_info( wx( id:

    _id , obj: button , userData: _userData , event: wxCommand( type: :command_button_clicked , cmdString: _cmdString , commandInt: _commandInt , extraLong: _extraLong ) ) , %__MODULE__{button: button} = stat e ) do new_active_sends = process_input(state) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end end
  150. defmodule ChatApp.GUI do # .. . def handle_info( wx( id:

    _id , obj: button , userData: _userData , event: wxCommand( type: :command_button_clicked , cmdString: _cmdString , commandInt: _commandInt , extraLong: _extraLong ) ) , %__MODULE__{button: button} = stat e ) do new_active_sends = process_input(state) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end end
  151. defmodule ChatApp.GUI do # .. . def handle_info( wx( id:

    _id , obj: button , userData: _userData , event: wxCommand( type: :command_button_clicked , cmdString: _cmdString , commandInt: _commandInt , extraLong: _extraLong ) ) , %__MODULE__{button: button} = stat e ) do new_active_sends = process_input(state) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end end
  152. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  153. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  154. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  155. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  156. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  157. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  158. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  159. defmodule ChatApp.GUI do # .. . defp process_input(state) do new_active_sends

    = case state.input |> :wxTextCtrl.getValue() |> to_string() do "/" <> command - > process_command(command, state) state.active_send s message when byte_size(message) > 0 - > case ConnectionManager.send_to_all(message) do {ref, name} - > append_message(name, message, state) now = System.monotonic_time(:second) Map.put(state.active_sends, ref, {message, now}) nil - > state.active_send s end "" - > state.active_send s end :wxTextCtrl.clear(state.input) new_active_send s end end
  160. defmodule ChatApp.GUI do # .. . defp append_message(name, message, state)

    do append_text_with_font(state.chat, name, state.bold) append_text(state.chat, ": #{message}\n") end end
  161. defmodule ChatApp.GUI do # .. . defp append_message(name, message, state)

    do append_text_with_font(state.chat, name, state.bold) append_text(state.chat, ": #{message}\n") end end
  162. defmodule ChatApp.GUI do # .. . defp append_message(name, message, state)

    do append_text_with_font(state.chat, name, state.bold) append_text(state.chat, ": #{message}\n") end end
  163. defmodule ChatApp.GUI do # .. . defp process_command("listen" <> args,

    state) do case Regex.named_captures(~r{\A\s+(?<port>\d+)\s+(?<name>\S.*)\z}, args) do %{"port" => port, "name" => name} - > case ConnectionManager.listen(String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Listening\n", state.italic) _error - > append_text_with_font( state.chat , "Error: listening failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /listen PORT NAME\n" , state.itali c ) end end end
  164. defmodule ChatApp.GUI do # .. . defp process_command("listen" <> args,

    state) do case Regex.named_captures(~r{\A\s+(?<port>\d+)\s+(?<name>\S.*)\z}, args) do %{"port" => port, "name" => name} - > case ConnectionManager.listen(String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Listening\n", state.italic) _error - > append_text_with_font( state.chat , "Error: listening failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /listen PORT NAME\n" , state.itali c ) end end end
  165. defmodule ChatApp.GUI do # .. . defp process_command("listen" <> args,

    state) do case Regex.named_captures(~r{\A\s+(?<port>\d+)\s+(?<name>\S.*)\z}, args) do %{"port" => port, "name" => name} - > case ConnectionManager.listen(String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Listening\n", state.italic) _error - > append_text_with_font( state.chat , "Error: listening failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /listen PORT NAME\n" , state.itali c ) end end end
  166. defmodule ChatApp.GUI do # .. . defp process_command("listen" <> args,

    state) do case Regex.named_captures(~r{\A\s+(?<port>\d+)\s+(?<name>\S.*)\z}, args) do %{"port" => port, "name" => name} - > case ConnectionManager.listen(String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Listening\n", state.italic) _error - > append_text_with_font( state.chat , "Error: listening failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /listen PORT NAME\n" , state.itali c ) end end end
  167. defmodule ChatApp.GUI do # .. . defp process_command("connect" <> args,

    state) do case Regex.named_captures( ~r{\A\s+(?<host>\S+)\s+(?<port>\d+)\s+(?<name>\S.*)\z} , arg s ) do %{"host" => host, "port" => port, "name" => name} - > case ConnectionManager.connect(host, String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Connected\n", state.italic) _error - > append_text_with_font( state.chat , "Error: connecting failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /connect HOST PORT NAME\n" , state.itali c ) end end end
  168. defmodule ChatApp.GUI do # .. . defp process_command("connect" <> args,

    state) do case Regex.named_captures( ~r{\A\s+(?<host>\S+)\s+(?<port>\d+)\s+(?<name>\S.*)\z} , arg s ) do %{"host" => host, "port" => port, "name" => name} - > case ConnectionManager.connect(host, String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Connected\n", state.italic) _error - > append_text_with_font( state.chat , "Error: connecting failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /connect HOST PORT NAME\n" , state.itali c ) end end end
  169. defmodule ChatApp.GUI do # .. . defp process_command("connect" <> args,

    state) do case Regex.named_captures( ~r{\A\s+(?<host>\S+)\s+(?<port>\d+)\s+(?<name>\S.*)\z} , arg s ) do %{"host" => host, "port" => port, "name" => name} - > case ConnectionManager.connect(host, String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Connected\n", state.italic) _error - > append_text_with_font( state.chat , "Error: connecting failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /connect HOST PORT NAME\n" , state.itali c ) end end end
  170. defmodule ChatApp.GUI do # .. . defp process_command("connect" <> args,

    state) do case Regex.named_captures( ~r{\A\s+(?<host>\S+)\s+(?<port>\d+)\s+(?<name>\S.*)\z} , arg s ) do %{"host" => host, "port" => port, "name" => name} - > case ConnectionManager.connect(host, String.to_integer(port), name) do :ok - > append_text_with_font(state.chat, "Connected\n", state.italic) _error - > append_text_with_font( state.chat , "Error: connecting failed\n" , state.itali c ) end nil - > append_text_with_font( state.chat , "Usage: /connect HOST PORT NAME\n" , state.itali c ) end end end
  171. Not Very DRY Beware the trap!

  172. defmodule ChatApp.GUI do # .. . defp process_command("quit", state), do:

    quit(state) defp process_command(_unknown_command, state) do append_text_with_font( state.chat , "Error: unknown command\n" , state.itali c ) end end
  173. defmodule ChatApp.GUI do # .. . defp process_command("quit", state), do:

    quit(state) defp process_command(_unknown_command, state) do append_text_with_font( state.chat , "Error: unknown command\n" , state.itali c ) end end
  174. defmodule ChatApp.GUI do # .. . defp process_command("quit", state), do:

    quit(state) defp process_command(_unknown_command, state) do append_text_with_font( state.chat , "Error: unknown command\n" , state.itali c ) end end
  175. defmodule ChatApp.GUI do # .. . def show_send_failure(ref) do GenServer.cast(__MODULE__,

    {:show_send_failure, ref}) end def handle_cast({:show_send_failure, ref}, state) do case Map.pop(state.active_sends, ref) do {{message, _timestamp}, new_active_sends} - > append_text_with_font( state.chat , "The following message was not received by all participants: " < > "#{message}\n" , state.itali c ) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} {nil, _active_sends} - > {:noreply, state} end end end
  176. defmodule ChatApp.GUI do # .. . def show_send_failure(ref) do GenServer.cast(__MODULE__,

    {:show_send_failure, ref}) end def handle_cast({:show_send_failure, ref}, state) do case Map.pop(state.active_sends, ref) do {{message, _timestamp}, new_active_sends} - > append_text_with_font( state.chat , "The following message was not received by all participants: " < > "#{message}\n" , state.itali c ) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} {nil, _active_sends} - > {:noreply, state} end end end
  177. defmodule ChatApp.GUI do # .. . def show_send_failure(ref) do GenServer.cast(__MODULE__,

    {:show_send_failure, ref}) end def handle_cast({:show_send_failure, ref}, state) do case Map.pop(state.active_sends, ref) do {{message, _timestamp}, new_active_sends} - > append_text_with_font( state.chat , "The following message was not received by all participants: " < > "#{message}\n" , state.itali c ) {:noreply, %__MODULE__{state | active_sends: new_active_sends}} {nil, _active_sends} - > {:noreply, state} end end end
  178. defmodule ChatApp.GUI do # .. . def handle_info(:prune_active_sends, state) do

    expired = System.monotonic_time(:second) - 5 * 60 new_active_sends = state.active_send s |> Enum.reject(fn {_ref, {_message, timestamp}} - > timestamp < expire d end) |> Map.new() {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end def handle_info(_message, state), do: {:noreply, state} end
  179. defmodule ChatApp.GUI do # .. . def handle_info(:prune_active_sends, state) do

    expired = System.monotonic_time(:second) - 5 * 60 new_active_sends = state.active_send s |> Enum.reject(fn {_ref, {_message, timestamp}} - > timestamp < expire d end) |> Map.new() {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end def handle_info(_message, state), do: {:noreply, state} end
  180. defmodule ChatApp.GUI do # .. . def handle_info(:prune_active_sends, state) do

    expired = System.monotonic_time(:second) - 5 * 60 new_active_sends = state.active_send s |> Enum.reject(fn {_ref, {_message, timestamp}} - > timestamp < expire d end) |> Map.new() {:noreply, %__MODULE__{state | active_sends: new_active_sends}} end def handle_info(_message, state), do: {:noreply, state} end
  181. Pardon My Mess I'm a Web Developer, not a GUI

    expert!
  182. defmodule ChatApp.GUI do # .. . def show_chat_message(name, content) do

    GenServer.cast(__MODULE__, {:show_chat_message, name, content}) end def handle_cast({:show_chat_message, name, action}, state) when is_atom(action) do append_text_with_font(state.chat, "#{name} #{action}\n", state.italic) {:noreply, state} end def handle_cast({:show_chat_message, name, content}, state) do append_message(name, content, state) {:noreply, state} end end
  183. defmodule ChatApp.GUI do # .. . def show_chat_message(name, content) do

    GenServer.cast(__MODULE__, {:show_chat_message, name, content}) end def handle_cast({:show_chat_message, name, action}, state) when is_atom(action) do append_text_with_font(state.chat, "#{name} #{action}\n", state.italic) {:noreply, state} end def handle_cast({:show_chat_message, name, content}, state) do append_message(name, content, state) {:noreply, state} end end
  184. defmodule ChatApp.GUI do # .. . def show_chat_message(name, content) do

    GenServer.cast(__MODULE__, {:show_chat_message, name, content}) end def handle_cast({:show_chat_message, name, action}, state) when is_atom(action) do append_text_with_font(state.chat, "#{name} #{action}\n", state.italic) {:noreply, state} end def handle_cast({:show_chat_message, name, content}, state) do append_message(name, content, state) {:noreply, state} end end
  185. defmodule ChatApp.GUI do # .. . def show_chat_message(name, content) do

    GenServer.cast(__MODULE__, {:show_chat_message, name, content}) end def handle_cast({:show_chat_message, name, action}, state) when is_atom(action) do append_text_with_font(state.chat, "#{name} #{action}\n", state.italic) {:noreply, state} end def handle_cast({:show_chat_message, name, content}, state) do append_message(name, content, state) {:noreply, state} end end
  186. Shall We Try it Out? Let's phone a friend…

  187. Big Finish Time Let's phone two friends!

  188. None
  189. None
  190. None
  191. Thank You

  192. Questions? • These slides • https://speakerdeck.com/jeg2/how-to-level-up-in-elixir • The Code •

    https://github.com/JEG2/chat_app • Forum Posts • https://elixirforum.com/t/becoming-an-intermediate-elixir-developer/ 37991 • https://elixirforum.com/t/how-to-become-a-senior-in-elixir/40430