Packages
Event-driven notification system for Ash Framework with multiple transport types
Current section
Files
Jump to
Current section
Files
lib/phoenix/user_channel.ex
defmodule AshDispatch.Phoenix.UserChannel do
@moduledoc """
Macro for creating a user channel with all AshDispatch functionality built-in.
Provides real-time updates for:
- Notifications
- Counter updates
- Query invalidation
## Usage
Create your channel with just 3 lines:
defmodule MyAppWeb.UserChannel do
use AshDispatch.Phoenix.UserChannel,
endpoint: MyAppWeb.Endpoint
end
Add to your socket:
channel "user:*", MyAppWeb.UserChannel
## What You Get
- `join/3` - Authorizes and initializes channel
- `handle_info(:after_join, socket)` - Sends initial counters/notifications
- `handle_in("refresh_counters", ...)` - Client can request counter refresh
- `broadcast_notification/2` - Push notifications to user
- `broadcast_counter/4` - Push counter updates with metadata
- `broadcast_counters/2` - Push multiple counter updates
## Configuration
The endpoint option is required. It's used for broadcasting.
use AshDispatch.Phoenix.UserChannel,
endpoint: MyAppWeb.Endpoint
## Customization
You can override any callback by defining it in your module:
defmodule MyAppWeb.UserChannel do
use AshDispatch.Phoenix.UserChannel,
endpoint: MyAppWeb.Endpoint
# Custom join logic
def join("user:" <> user_id, payload, socket) do
# Your custom authorization
if authorized?(socket, user_id) do
send(self(), :after_join)
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
end
## Frontend Integration
Use the generated SDK hooks to connect:
import { useChannel } from '@/lib/ash-dispatch'
function App() {
useChannel({
channel: userChannel,
onNotification: (notification) => {
// Handle new notification
}
})
}
"""
defmacro __using__(opts) do
endpoint = Keyword.fetch!(opts, :endpoint)
quote do
use Phoenix.Channel
alias AshDispatch.Helpers.{ChannelState, CounterLoader}
@endpoint unquote(endpoint)
@impl true
def join("user:" <> user_id, _payload, socket) do
# Verify the user is authorized (token verification already done in UserSocket)
if socket.assigns.user_id == user_id do
send(self(), :after_join)
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
@impl true
def handle_info(:after_join, socket) do
user_id = socket.assigns.user_id
# Build complete initial state (counters + notifications in parallel)
initial_state = ChannelState.build(user_id)
push(socket, "initial_state", initial_state)
{:noreply, socket}
end
# Client requests to refresh counters
@impl true
def handle_in("refresh_counters", _payload, socket) do
user_id = socket.assigns.user_id
counters = CounterLoader.load_counters_for_user(user_id)
{:reply, {:ok, %{counters: counters}}, socket}
end
## Broadcaster functions called from other parts of the app
@doc """
Broadcast a new notification to a user.
Called automatically by the notification dispatcher.
"""
def broadcast_notification(user_id, notification) do
@endpoint.broadcast("user:#{user_id}", "new_notification", notification)
end
@doc """
Broadcast counter updates to a user.
Call this whenever a counter changes (e.g., new ticket, cart updated, etc.)
"""
def broadcast_counters(user_id, counters) do
@endpoint.broadcast("user:#{user_id}", "counters_updated", %{counters: counters})
end
@doc """
Broadcast a counter update to a specific user.
Unified method for all counter types - user, admin, system, custom.
Counter routing is determined by the counter name itself.
Options:
- `:metadata` - Map with optional `invalidate_queries` list
"""
def broadcast_counter(user_id, counter_name, value, opts \\ []) do
metadata = Keyword.get(opts, :metadata, %{})
@endpoint.broadcast("user:#{user_id}", "counter_updated", %{
counter: counter_name,
value: value,
metadata: metadata
})
end
# Allow overriding any function
defoverridable join: 3,
handle_info: 2,
handle_in: 3,
broadcast_notification: 2,
broadcast_counters: 2,
broadcast_counter: 4
end
end
end