Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.19.0-rc.0-otp-27
elixir 1.19.5-otp-27
erlang 27.2.4
96 changes: 42 additions & 54 deletions lib/live_state/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ defmodule LiveState.Channel do
```
use LiveState.Channel, web_module: MyAppWeb, max_version: 100
```
- `message_builder` optional, defaults to `{LiveState.MessageBuilder, ignore_keys: [:__meta__]}`. If
set, should be a tuple whose first element is a module that defines `update_state_message/4` and `new_state_message/3` and
and second element contains any options. Options are passed as final arg to both functions when invoked.
See `LiveState.MessageBuilder` for details
- `encoder_opts` optional, defaults to `[ignore_keys: [:__meta__]]`. Options passed to
`LiveState.Encoder.encode/2` when encoding state for diffing.
- `diff_function` optional, defaults to `&JSONDiff.diff/2`. A function that takes two encoded
states (old, new) and returns a JSON patch list.
- `max_version` optional, defaults to 1000. This is the maximum version number, after which it will
reset to 0 and begin incrementing again. Version numbers are used to detect a patch message arriving
out of order. If such a condition is detected by `phx-live-state` a new copy of state is requested.
"""
import Phoenix.Socket

alias LiveState.Event
alias LiveState.Encoder

@doc """
Returns the initial application state. Called just after connection
Expand Down Expand Up @@ -94,16 +95,8 @@ defmodule LiveState.Channel do
@dialyzer {:nowarn_function, update_state: 2}

@behaviour unquote(__MODULE__)
@message_builder (case unquote(
Keyword.get(
opts,
:message_builder,
{LiveState.MessageBuilder, ignore_keys: [:__meta__]}
)
) do
{mod, opts} when is_atom(mod) -> {mod, opts}
mod when is_atom(mod) -> {mod, []}
end)
@encoder_opts unquote(Keyword.get(opts, :encoder_opts, [ignore_keys: [:__meta__]]))
@diff_function unquote(Keyword.get(opts, :diff_function, &JSONDiff.diff/2))
@max_version unquote(Keyword.get(opts, :max_version, 1000))

def join(channel, payload, socket) do
Expand Down Expand Up @@ -131,9 +124,13 @@ defmodule LiveState.Channel do
end

defp initialize_state(state, socket) do
{event_name, message} = build_new_state_message(state, 0)
push(socket, event_name, message)
socket |> assign(state_key(), state) |> assign(state_version_key(), 0)
encoded = Encoder.encode(state, @encoder_opts)

socket
|> assign(state_key(), state)
|> assign(state_version_key(), 0)
|> assign(state_cache_key(), encoded)
|> tap(&push_state_change/1)
end

def handle_info(message, %{assigns: assigns} = socket) do
Expand All @@ -145,8 +142,8 @@ defmodule LiveState.Channel do
|> maybe_handle_reply(socket)
end

def handle_in("lvs_refresh", _payload, %{assigns: assigns} = socket) do
push_state_change(socket, Map.get(assigns, state_key()), Map.get(assigns, state_version_key()))
def handle_in("lvs_refresh", _payload, socket) do
push_state_change(socket)
{:noreply, socket}
end

Expand All @@ -170,37 +167,33 @@ defmodule LiveState.Channel do

def state_version_key, do: :version

def state_cache_key, do: :_encoded_state_cache

def handle_message(_message, state), do: {:noreply, state}

def handle_event(_message, _payload, state), do: {:noreply, state}

defp update_state(%{assigns: assigns} = socket, new_state) do
current_state = Map.get(assigns, state_key())
new_state_version = increment_version(assigns)
{event_name, message} = build_update_message(current_state, new_state, new_state_version)
push(socket, event_name, message)

{:noreply,
socket
|> assign(state_key(), new_state)
|> assign(state_version_key(), new_state_version)}
end

defp build_update_message(current_state, new_state, version) do
{mod, opts} = @message_builder
if function_exported?(mod, :update_state_message, 4) do
mod.update_state_message(current_state, new_state, version, opts)
else
mod.update_state_message(current_state, new_state, version)
end
end

defp build_new_state_message(new_state, version) do
{mod, opts} = @message_builder
if function_exported?(mod, :new_state_message, 3) do
mod.new_state_message(new_state, version, opts)
else
mod.new_state_message(new_state, version)
old_encoded = Map.fetch!(assigns, state_cache_key())
new_encoded = Encoder.encode(new_state, @encoder_opts)
patch = @diff_function.(old_encoded, new_encoded)

case patch do
[] ->
{:noreply,
socket
|> assign(state_key(), new_state)
|> assign(state_cache_key(), new_encoded)}

_ ->
new_state_version = increment_version(assigns)
push(socket, "state:patch", %{patch: patch, version: new_state_version})

{:noreply,
socket
|> assign(state_key(), new_state)
|> assign(state_version_key(), new_state_version)
|> assign(state_cache_key(), new_encoded)}
end
end

Expand Down Expand Up @@ -247,15 +240,10 @@ defmodule LiveState.Channel do
push(socket, "error", error)
end

defp push_state_change(socket, state, version) do
payload = %{} |> Map.put(state_key(), state) |> Map.put(state_version_key(), version)
push(socket, "state:change", payload)
end

defp push_json_patch(socket, current_state, new_state, version) do
push(socket, "state:patch", %{
patch: JSONDiff.diff(current_state, new_state),
version: version
defp push_state_change(%{assigns: assigns} = socket) do
push(socket, "state:change", %{
state: Map.fetch!(assigns, state_cache_key()),
version: Map.fetch!(assigns, state_version_key())
})
end

Expand Down
29 changes: 0 additions & 29 deletions lib/live_state/message_builder.ex

This file was deleted.

24 changes: 13 additions & 11 deletions test/live_state_channel_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ defmodule LiveState.LiveStateChannelTest do

test "handle_event", %{socket: socket} do
send_event(socket, "add_todo", %{"description" => "Do the thing"})
assert_state_change %{todos: [%{"description" => "Do the thing"}]}
assert_state_patch [
%{"op" => "add", "path" => "/todos/0", "value" => %{"description" => "Do the thing"}}
]
end

test "handle_message" do
Expand All @@ -41,27 +43,27 @@ defmodule LiveState.LiveStateChannelTest do

assert_push("reply_event", %{foo: "bar"})

assert_push("state:change", %{
state: %{todos: [%{"description" => "And another one"}]},
assert_push("state:patch", %{
patch: [
%{"op" => "add", "path" => "/todos/0", "value" => %{"description" => "And another one"}}
],
version: 1
})
end

test "handle_event with reply", %{socket: socket} do
push(socket, "lvs_evt:add_todo_with_one_reply", %{"description" => "Do the thing"})
assert_push("state:change", %{
state: %{todos: [%{"description" => "Do the thing"}]},
version: 1
})
assert_state_patch [
%{"op" => "add", "path" => "/todos/0", "value" => %{"description" => "Do the thing"}}
]
assert_push("reply_event", %{foo: "bar"})
end

test "handle_event with multi event reply", %{socket: socket} do
push(socket, "lvs_evt:add_todo_with_two_replies", %{"description" => "Do the thing"})
assert_push("state:change", %{
state: %{todos: [%{"description" => "Do the thing"}]},
version: 1
})
assert_state_patch [
%{"op" => "add", "path" => "/todos/0", "value" => %{"description" => "Do the thing"}}
]
assert_push("reply_event1", %{foo: "bar"})
assert_push("reply_event2", %{bing: "baz"})
end
Expand Down
49 changes: 0 additions & 49 deletions test/message_builder_test.exs

This file was deleted.

2 changes: 1 addition & 1 deletion test/support/patch_channel.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule LiveState.Test.PatchChannel do
@moduledoc false

use LiveState.Channel, web_module: LiveState.Test.Web, json_patch: true, max_version: 10
use LiveState.Channel, web_module: LiveState.Test.Web, max_version: 10

def init(_channel, _params, _socket) do
{:ok, %{foo: "bar"}}
Expand Down
33 changes: 9 additions & 24 deletions test/support/todo_channel.ex
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
defmodule LiveState.Test.TodoChannel do
@moduledoc false

alias LiveState.MessageBuilder

defmodule MyMessageBuilder do
@moduledoc false
def update_state_message(_current_state, new_state, version),
do: MessageBuilder.new_state_message(new_state, version)

def new_state_message(new_state, version),
do: MessageBuilder.new_state_message(new_state, version)
end

alias LiveState.MessageBuilder

use LiveState.Channel,
web_module: LiveState.Test.Web,
message_builder: MyMessageBuilder
use LiveState.Channel, web_module: LiveState.Test.Web

alias LiveState.Event

Expand All @@ -27,26 +12,26 @@ defmodule LiveState.Test.TodoChannel do
end

@impl true
def handle_event("add_todo", todo, %{todos: todos}) do
{:noreply, %{todos: [todo | todos]}}
def handle_event("add_todo", todo, %{todos: todos} = state) do
{:noreply, %{state | todos: [todo | todos]}}
end

@impl true
def handle_event("add_todo_with_one_reply", todo, %{todos: todos}) do
{:reply, %Event{name: "reply_event", detail: %{foo: "bar"}}, %{todos: [todo | todos]}}
def handle_event("add_todo_with_one_reply", todo, %{todos: todos} = state) do
{:reply, %Event{name: "reply_event", detail: %{foo: "bar"}}, %{state | todos: [todo | todos]}}
end

@impl true
def handle_event("add_todo_with_two_replies", todo, %{todos: todos}) do
def handle_event("add_todo_with_two_replies", todo, %{todos: todos} = state) do
{:reply,
[
%Event{name: "reply_event1", detail: %{foo: "bar"}},
%Event{name: "reply_event2", detail: %{bing: "baz"}}
], %{todos: [todo | todos]}}
], %{state | todos: [todo | todos]}}
end

@impl true
def handle_message({:todo_added, todo}, %{todos: todos}) do
{:reply, %Event{name: "reply_event", detail: %{foo: "bar"}}, %{todos: [todo | todos]}}
def handle_message({:todo_added, todo}, %{todos: todos} = state) do
{:reply, %Event{name: "reply_event", detail: %{foo: "bar"}}, %{state | todos: [todo | todos]}}
end
end