Current section
13 Versions
Jump to
Current section
13 Versions
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…