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

How to Level Up in Elixir

jeg2
June 23, 2021

How to Level Up in Elixir

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

jeg2

June 23, 2021
Tweet

More Decks by jeg2

Other Decks in Programming

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. 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
  4. Starting Processes Conway's Game of Life with one process per

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

    cell All socket handling in one GenServer
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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!
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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`
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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
  97. 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
  98. 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
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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
  106. 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
  107. 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
  108. 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
  109. 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
  110. 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
  111. 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
  112. 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
  113. 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
  114. 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
  115. 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
  116. 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
  117. 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
  118. 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
  119. 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
  120. 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
  121. 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
  122. 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
  123. 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
  124. 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
  125. 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
  126. 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
  127. 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
  128. 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
  129. 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
  130. 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
  131. 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
  132. 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
  133. 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
  134. 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
  135. 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
  136. 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
  137. 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
  138. 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
  139. 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
  140. 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
  141. 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
  142. 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
  143. 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
  144. 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
  145. 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
  146. 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
  147. 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
  148. 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