Current section

Files

Jump to
phoenix_session_process lib phoenix session_process redux subscription.ex
Raw

lib/phoenix/session_process/redux/subscription.ex

defmodule Phoenix.SessionProcess.Redux.Subscription do
@moduledoc """
Subscription management for Redux state changes.
Allows subscribing to Redux state changes with optional selectors for
fine-grained notifications. Only notifies when selected values change.
## Basic Usage
# Subscribe to all state changes
redux = Redux.subscribe(redux, fn state ->
IO.inspect(state, label: "State changed")
end)
# Or with subscription ID for later unsubscribe
{redux, sub_id} = Subscription.subscribe_to_struct(redux, nil, fn state ->
IO.inspect(state, label: "State changed")
end)
# Unsubscribe
redux = Redux.unsubscribe(redux, sub_id)
## With Selectors (Recommended)
# Only notify when user changes
user_selector = fn state -> state.user end
redux = Redux.subscribe(redux, user_selector, fn user ->
IO.inspect(user, label: "User changed")
end)
## Advanced Example
# Subscribe with composed selector
alias Phoenix.SessionProcess.Redux.Selector
filtered_items = Selector.create_selector(
[
fn state -> state.items end,
fn state -> state.filter end
],
fn items, filter ->
Enum.filter(items, &(&1.type == filter))
end
)
redux = Redux.subscribe(redux, filtered_items, fn items ->
# Only called when filtered items actually change
update_ui(items)
end)
## Use Cases
- **LiveView Integration**: Update assigns when relevant state changes
- **Phoenix Channels**: Broadcast updates to connected clients
- **Audit Trail**: Log state changes for debugging
- **Side Effects**: Trigger actions based on state changes
## Performance
Subscriptions with selectors use shallow equality checks to determine
if the selected value has changed. This prevents unnecessary callbacks
when unrelated state changes.
"""
alias Phoenix.SessionProcess.Redux
alias Phoenix.SessionProcess.Redux.Selector
@type subscription_id :: reference()
@type selector :: Selector.selector()
@type callback :: (any() -> any())
@type subscription :: %{
id: subscription_id(),
selector: selector() | nil,
callback: callback(),
last_value: any()
}
@doc """
Subscribe to state changes on a Redux struct.
Returns `{redux, subscription_id}` tuple.
## Examples
# Subscribe to all changes
{redux, id} = Subscription.subscribe_to_struct(redux, nil, fn state ->
Logger.info("State: \#{inspect(state)}")
end)
# Subscribe with selector
{redux, id} = Subscription.subscribe_to_struct(
redux,
fn state -> state.user end,
fn user -> Logger.info("User: \#{inspect(user)}") end
)
"""
@spec subscribe_to_struct(Redux.t(), selector() | nil, callback()) ::
{Redux.t(), subscription_id()}
def subscribe_to_struct(redux, selector, callback) when is_function(callback, 1) do
# Get current selected value
current_value =
if selector do
Selector.select(redux, selector)
else
Redux.get_state(redux)
end
# Create subscription
subscription = %{
id: make_ref(),
selector: selector,
callback: callback,
last_value: current_value
}
# Invoke callback immediately with current value
invoke_callback(callback, current_value)
# Add subscription to Redux
new_redux = %{redux | subscriptions: [subscription | redux.subscriptions]}
{new_redux, subscription.id}
end
@doc """
Unsubscribe from state changes.
## Examples
{redux, id} = Subscription.subscribe_to_struct(redux, nil, callback)
redux = Subscription.unsubscribe_from_struct(redux, id)
"""
@spec unsubscribe_from_struct(Redux.t(), subscription_id()) :: Redux.t()
def unsubscribe_from_struct(redux, subscription_id) do
new_subscriptions =
Enum.reject(redux.subscriptions, fn sub -> sub.id == subscription_id end)
%{redux | subscriptions: new_subscriptions}
end
@doc """
Notify all subscriptions of a state change.
This is called automatically by Redux.dispatch/2-3.
## Examples
redux = Subscription.notify_all_struct(redux)
"""
@spec notify_all_struct(Redux.t()) :: Redux.t()
def notify_all_struct(redux) do
new_state = Redux.get_state(redux)
# Notify each subscription and update last_value if changed
updated_subscriptions =
Enum.map(redux.subscriptions, fn sub ->
notify_subscription(sub, new_state)
end)
%{redux | subscriptions: updated_subscriptions}
end
@doc """
Get all active subscriptions from a Redux struct.
Useful for debugging and monitoring.
## Examples
subscriptions = Subscription.list_subscriptions(redux)
IO.inspect(length(subscriptions), label: "Active subscriptions")
"""
@spec list_subscriptions(Redux.t()) :: [subscription()]
def list_subscriptions(redux) do
redux.subscriptions
end
@doc """
Clear all subscriptions from a Redux struct.
Useful for testing or cleanup.
## Examples
redux = Subscription.clear_all_struct(redux)
"""
@spec clear_all_struct(Redux.t()) :: Redux.t()
def clear_all_struct(redux) do
%{redux | subscriptions: []}
end
# Private functions
defp notify_subscription(%{selector: nil, callback: callback} = sub, new_state) do
# No selector, always notify
invoke_callback(callback, new_state)
%{sub | last_value: new_state}
end
defp notify_subscription(
%{selector: selector, callback: callback, last_value: last_value} = sub,
new_state
) do
# Extract new value using selector
# Wrap in try/catch to handle selector errors
# credo:disable-for-next-line Credo.Check.Readability.PreferImplicitTry
try do
new_value = apply_selector(selector, new_state)
# Check if value changed (shallow equality)
if new_value != last_value do
invoke_callback(callback, new_value)
%{sub | last_value: new_value}
else
sub
end
rescue
error ->
require Logger
Logger.error(
"Selector error in subscription: #{inspect(error)}\n" <>
Exception.format_stacktrace(__STACKTRACE__)
)
# Return subscription unchanged on error
sub
end
end
defp apply_selector(selector, state) when is_function(selector, 1) do
selector.(state)
end
defp apply_selector(selector, state) when is_map(selector) do
# For composed selectors, use Selector.select/2
# We need to wrap state in a minimal Redux struct
redux = %Phoenix.SessionProcess.Redux{
current_state: state,
initial_state: state,
history: [],
reducer: nil,
middleware: [],
max_history_size: 0,
pubsub: nil,
pubsub_topic: nil,
subscriptions: []
}
Selector.select(redux, selector)
end
defp invoke_callback(callback, value) do
# credo:disable-for-next-line Credo.Check.Readability.PreferImplicitTry
try do
callback.(value)
rescue
error ->
require Logger
Logger.error(
"Subscription callback error: #{inspect(error)}\n" <>
Exception.format_stacktrace(__STACKTRACE__)
)
end
end
end