Outbound state-update pushes for channels: drop no-op updates, and optionally coalesce bursts into one message.
State events re-send the current state of an object the subscriber already has, so two things are worth doing before pushing:
- Deduplicate. If nothing changed since the last push on this connection, send nothing at all.
- Debounce. When
REALTIME_DEBOUNCE_MSis set, hold updates for that long and push only the latest state per object when the timer fires. A burst of ten writes to one lobby becomes one message.
Debouncing is off by default (0), which pushes immediately and only
deduplicates.
Coalescing beats shrinking here: each WebSocket message costs roughly 76
bytes of framing, TLS, TCP and IP headers before any payload, and the server
runs with TCP_NODELAY, so every message is its own packet. Removing a
message saves more than compressing one ever can.
Usage
def handle_info({:lobby_updated, lobby}, socket) do
payload = Serializers.serialize_lobby(lobby)
{:noreply, ChannelUpdates.push(socket, "lobby_updated", payload.id, payload)}
end
# required once per channel that pushes updates
def handle_info({:channel_updates_flush, _}, socket),
do: {:noreply, ChannelUpdates.flush(socket)}key scopes the dedupe/coalesce slot within the channel. Channels that track
one object can pass any constant; channels that multiplex (lobbies,
groups, member lists) must pass the object id.
Summary
Functions
Milliseconds to hold updates before pushing. 0 disables debouncing.
Pushes every pending update. Call from the channel's handler for
:channel_updates_flush messages.
Forgets everything remembered for event/key, so the next push is treated
as the first one. Use when the object is deleted or the client leaves it.
The last payload pushed for event/key, or nil.
Pushes payload for event/key, unless it is identical to the last one
pushed on this socket. Returns the updated socket.
Records payload as already delivered without pushing it, so a later
identical update is suppressed. Use when the payload went out under a
different event name (a create that doubles as the first update).
Functions
@spec debounce_ms() :: non_neg_integer()
Milliseconds to hold updates before pushing. 0 disables debouncing.
@spec flush(Phoenix.Socket.t()) :: Phoenix.Socket.t()
Pushes every pending update. Call from the channel's handler for
:channel_updates_flush messages.
@spec forget(Phoenix.Socket.t(), String.t(), term()) :: Phoenix.Socket.t()
Forgets everything remembered for event/key, so the next push is treated
as the first one. Use when the object is deleted or the client leaves it.
@spec last(Phoenix.Socket.t(), String.t(), term()) :: map() | nil
The last payload pushed for event/key, or nil.
@spec push(Phoenix.Socket.t(), String.t(), term(), map(), (map() -> map())) :: Phoenix.Socket.t()
Pushes payload for event/key, unless it is identical to the last one
pushed on this socket. Returns the updated socket.
wrap builds the message body from the payload, for events that nest it
(friend_updated sends %{friends: %{id => payload}}). Deduplication always
compares the unwrapped payload.
@spec remember(Phoenix.Socket.t(), String.t(), term(), map()) :: Phoenix.Socket.t()
Records payload as already delivered without pushing it, so a later
identical update is suppressed. Use when the payload went out under a
different event name (a create that doubles as the first update).