Packages

Event-driven notification system for Ash Framework with multiple transport types

Current section

Files

Jump to
ash_dispatch lib helpers channel_state.ex
Raw

lib/helpers/channel_state.ex

defmodule AshDispatch.Helpers.ChannelState do
@moduledoc """
Helper for building initial Phoenix Channel state.
Combines counters and notifications into a single payload ready for
Phoenix Channel's `push/2`. Loads data in parallel for performance.
## Usage
alias AshDispatch.Helpers.ChannelState
# Build complete initial state
initial_state = ChannelState.build(user_id)
#=> %{"counters" => %{...}, "notifications" => [...]}
push(socket, "initial_state", initial_state)
## Custom Options
# Limit notifications
ChannelState.build(user_id, notification_limit: 10)
# Load counters from specific domains
ChannelState.build(user_id, counter_domains: [MyApp.Orders])
# Custom notification serializer
ChannelState.build(user_id,
notification_serializer: &MyApp.serialize_notification/1
)
"""
alias AshDispatch.Helpers.{CounterLoader, NotificationLoader}
@doc """
Build complete initial state for a user.
Loads counters and notifications in parallel for optimal performance.
Returns a map with:
- `"counters"` - All counters as camelCase JSON (strings for JS)
- `"notifications"` - Recent notifications
## Options
- `:notification_limit` - Number of recent notifications (default: 50)
- `:notification_serializer` - Custom notification serializer function
- `:counter_domains` - Specific Ash domains to load counters from
- `:parallel` - Load in parallel (default: true)
## Examples
# Standard usage
ChannelState.build("user-123")
#=> %{
# "counters" => %{"pending_orders" => 5, "cart_items" => 3},
# "notifications" => [%{id: "...", ...}, ...]
# }
# With options
ChannelState.build("user-123",
notification_limit: 10,
counter_domains: [MyApp.Orders, MyApp.Tickets]
)
# Custom serializer
ChannelState.build("user-123",
notification_serializer: &MyApp.serialize_notification/1
)
"""
def build(user_id, opts \\ []) do
notification_limit = Keyword.get(opts, :notification_limit, 50)
notification_serializer = Keyword.get(opts, :notification_serializer)
parallel = Keyword.get(opts, :parallel, true)
if parallel do
build_parallel(user_id, opts, notification_limit, notification_serializer)
else
build_sequential(user_id, opts, notification_limit, notification_serializer)
end
end
# Load counters and notifications in parallel
defp build_parallel(user_id, opts, notification_limit, notification_serializer) do
counters_task =
Task.async(fn ->
CounterLoader.load_counters_for_user(user_id, opts)
end)
notifications_task =
Task.async(fn ->
NotificationLoader.load_recent(user_id,
limit: notification_limit,
serializer: notification_serializer
)
end)
counters = Task.await(counters_task)
notifications = Task.await(notifications_task)
%{
"counters" => counters_to_json(counters),
"notifications" => notifications
}
end
# Load counters and notifications sequentially (useful for testing or debugging)
defp build_sequential(user_id, opts, notification_limit, notification_serializer) do
counters = CounterLoader.load_counters_for_user(user_id, opts)
notifications =
NotificationLoader.load_recent(user_id,
limit: notification_limit,
serializer: notification_serializer
)
%{
"counters" => counters_to_json(counters),
"notifications" => notifications
}
end
@doc """
Convert counter map to JSON-friendly format.
Converts atom keys to strings for JavaScript consumption.
## Examples
ChannelState.counters_to_json(%{pending_orders: 5, cart_items: 3})
#=> %{"pending_orders" => 5, "cart_items" => 3}
"""
def counters_to_json(counters) when is_map(counters) do
Enum.into(counters, %{}, fn {key, value} ->
{Atom.to_string(key), value}
end)
end
def counters_to_json(_), do: %{}
end