Current section

13 Versions

Jump to

Compare versions

7 files changed
+86 additions
-38 deletions
  @@ -0,0 +1,20 @@
1 + # Changelog
2 +
3 + ## [0.9.0]
4 +
5 + ### Added
6 + - New `handle_message/3` callback for more flexible message handling with additional context (#32, #33)
7 + - Implementation for `lvs_refresh` functionality
8 + - Comprehensive documentation for `LiveState.Encoder` explaining encoding and serialization behavior
9 +
10 + ### Fixed
11 + - Bug where `Ecto.Association.NotLoaded` structs were incorrectly passed through `LiveState.Encoder` instead of being properly handled
12 + - Elixir 1.19 type system compatibility warnings (#31)
13 + - Documentation corrections for `gen.element`
14 + - Unused variable warnings
15 +
16 + ### Changed
17 + - Updated tutorial documentation with improved examples and socket handling (#26)
18 + - Upgraded ExDoc dependency
19 + - Updated to use Elixir 1.19 in local development and CI builds
20 +
  @@ -52,7 +52,6 @@ defmodule MyAppWeb.Socket do
52 52 end
53 53 ```
54 54 3. Create your channel using the `LiveState.Channel` behaviour:
55 -
56 55 ```elixir
57 56 defmodule MyAppWeb.Channel do
58 57 use LiveState.Channel, web_module: MyAppWeb
  @@ -60,13 +59,15 @@ defmodule MyAppWeb.Channel do
60 59 ```
61 60
62 61 4. Then define your initial state using the `c:LiveState.Channel.init/3` callback, which will be called after channel joins and is expected to return the initial state:
63 -
64 62 ```elixir
65 63 def init(_channel, _payload, _socket), do: {:ok, %{foo: "bar"}}
66 64 ```
67 65
68 - State must be a map. It will be sent down as JSON, so anything in it
69 - must have a `Jason.Encoder` implementation.
66 + # State encoding and serialization
67 +
68 + To send the state to the client, it is encoded and then serialized as JSON before being pushed over the channel. The encoding is handled by the `c:LiveState.Encoder` protocol, the default implementation of which will handle all Elixir terms including converting structs to maps and removing the the `__meta__` field for `Ecto.Schema` structs. You can optionally implement the protocol to gain control over this encoding. It is important to understand that this encoding happens before JSON serialization and diffing occurs.
69 +
70 + After the initial state is sent, subsequent state updates will be diffed against previous state using the [jsondiff](https://hexdocs.pm/json_diff/JSONDiff.html#content) library and sent in [jsonpatch](https://datatracker.ietf.org/doc/html/rfc6902) format. You can, if you wish, provide an alternate implementation of the encoding and serialization process by specifying a module as the `message_builder` option when calling `use LiveState.Channel`. See `c:LiveState.MessageBuilder` for an example.
70 71
71 72 ## Events
72 73
  @@ -83,7 +84,7 @@ For events emitted from the client, you implement the `c:LiveState.Channel.handl
83 84
84 85 * event name
85 86 * payload
86 - * current
87 + * current state
87 88
88 89 And returns a tuple whose last element is the new state. It can also return
89 90 one or many events to dispatch on the calling DOM Element:
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"Github">>,<<"https://github.com/launchscout/live_state">>}]}.
2 2 {<<"name">>,<<"live_state">>}.
3 - {<<"version">>,<<"0.8.2">>}.
3 + {<<"version">>,<<"0.9.0">>}.
4 4 {<<"description">>,<<"A package to manage web application state">>}.
5 5 {<<"elixir">>,<<">= 1.12.0">>}.
6 6 {<<"app">>,<<"live_state">>}.
  @@ -35,5 +35,5 @@
35 35 <<"lib/live_state/message_builder.ex">>,<<"priv">>,<<"priv/native">>,
36 36 <<"priv/templates">>,<<"priv/templates/channel.ex">>,
37 37 <<"priv/templates/custom-element.ts">>,<<".formatter.exs">>,<<"mix.exs">>,
38 - <<"README.md">>,<<"LICENSE">>]}.
38 + <<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}.
39 39 {<<"build_tools">>,[<<"mix">>]}.
  @@ -58,7 +58,7 @@ defmodule LiveState.Channel do
58 58 Socket.t()}
59 59 | {:noreply, new_state :: map(), Socket.t()}
60 60
61 - @optional_callbacks handle_event: 4, handle_event: 3
61 + @optional_callbacks handle_event: 4, handle_event: 3, handle_message: 3
62 62
63 63 @doc """
64 64 The key on assigns to hold application state. Defaults to `:state`.
  @@ -77,6 +77,14 @@ defmodule LiveState.Channel do
77 77 {:reply, reply :: %LiveState.Event{} | list(%LiveState.Event{}), new_state :: any()}
78 78 | {:noreply, new_state :: term}
79 79
80 + @doc """
81 + Receives pubsub message and current state. Use this callback if you need to
82 + receive the socket as well as the state. Returns new state.
83 + """
84 + @callback handle_message(message :: term(), state :: term(), socket :: Socket.t()) ::
85 + {:reply, reply :: %LiveState.Event{} | list(%LiveState.Event{}), new_state :: any(), Socket.t()}
86 + | {:noreply, new_state :: term, Socket.t()}
87 +
80 88 defmacro __using__(opts) do
81 89 quote do
82 90 use unquote(Keyword.get(opts, :web_module)), :channel
  @@ -86,34 +94,36 @@ defmodule LiveState.Channel do
86 94 @dialyzer {:nowarn_function, update_state: 2}
87 95
88 96 @behaviour unquote(__MODULE__)
89 - @message_builder unquote(
97 + @message_builder (case unquote(
90 98 Keyword.get(
91 99 opts,
92 100 :message_builder,
93 101 {LiveState.MessageBuilder, ignore_keys: [:__meta__]}
94 102 )
95 - )
103 + ) do
104 + {mod, opts} when is_atom(mod) -> {mod, opts}
105 + mod when is_atom(mod) -> {mod, []}
106 + end)
96 107 @max_version unquote(Keyword.get(opts, :max_version, 1000))
97 108
98 109 def join(channel, payload, socket) do
99 - case authorize(channel, payload, socket) do
100 - {:ok, socket} ->
110 + with {:ok, socket} <- authorize(channel, payload, socket) do
101 111 send(self(), {:after_join, channel, payload})
102 112 {:ok, socket}
103 -
104 - {:error, reason} ->
105 - {:error, reason}
106 113 end
107 114 end
108 115
109 116 def handle_info({:after_join, channel, payload}, socket) do
110 - case init(channel, payload, socket) do
111 - {:ok, state, socket} ->
112 - {:noreply, initialize_state(state, socket)}
117 + init(channel, payload, socket)
118 + |> handle_init_result(socket)
119 + end
113 120
114 - {:ok, state} ->
121 + defp handle_init_result(result, socket) do
122 + case result do
123 + {:ok, state, new_socket} when is_map(state) ->
124 + {:noreply, initialize_state(state, new_socket)}
125 + {:ok, state} when is_map(state) ->
115 126 {:noreply, initialize_state(state, socket)}
116 -
117 127 {:error, error} ->
118 128 push_error(socket, error)
119 129 {:noreply, socket}
  @@ -127,7 +137,17 @@ defmodule LiveState.Channel do
127 137 end
128 138
129 139 def handle_info(message, %{assigns: assigns} = socket) do
130 - handle_message(message, Map.get(assigns, state_key())) |> maybe_handle_reply(socket)
140 + if function_exported?(__MODULE__, :handle_message, 3) do
141 + apply(__MODULE__, :handle_message, [message, Map.get(assigns, state_key()), socket])
142 + else
143 + apply(__MODULE__, :handle_message, [message, Map.get(assigns, state_key())])
144 + end
145 + |> maybe_handle_reply(socket)
146 + end
147 +
148 + def handle_in("lvs_refresh", _payload, %{assigns: assigns} = socket) do
149 + push_state_change(socket, Map.get(assigns, state_key()), Map.get(assigns, state_version_key()))
150 + {:noreply, socket}
131 151 end
132 152
133 153 def handle_in("lvs_evt:" <> event_name, payload, %{assigns: assigns} = socket) do
  @@ -167,16 +187,20 @@ defmodule LiveState.Channel do
167 187 end
168 188
169 189 defp build_update_message(current_state, new_state, version) do
170 - case @message_builder do
171 - {mod, opts} -> mod.update_state_message(current_state, new_state, version, opts)
172 - mod -> mod.update_state_message(current_state, new_state, version)
190 + {mod, opts} = @message_builder
191 + if function_exported?(mod, :update_state_message, 4) do
192 + mod.update_state_message(current_state, new_state, version, opts)
193 + else
194 + mod.update_state_message(current_state, new_state, version)
173 195 end
174 196 end
175 197
176 198 defp build_new_state_message(new_state, version) do
177 - case @message_builder do
178 - {mod, opts} -> mod.new_state_message(new_state, version, opts)
179 - mod -> mod.new_state_message(new_state, version)
199 + {mod, opts} = @message_builder
200 + if function_exported?(mod, :new_state_message, 3) do
201 + mod.new_state_message(new_state, version, opts)
202 + else
203 + mod.new_state_message(new_state, version)
180 204 end
181 205 end
  @@ -85,3 +85,9 @@ defimpl LiveState.Encoder, for: List do
85 85 Enum.map(list, &Encoder.encode(&1, opts))
86 86 end
87 87 end
88 +
89 + if Code.ensure_compiled(Ecto.Association.NotLoaded) do
90 + defimpl LiveState.Encoder, for: Ecto.Association.NotLoaded do
91 + def encode(_, _), do: nil
92 + end
93 + end
Loading more files…