Skip to content

Commit 959f713

Browse files
committed
feat: add Redux subscriptions, selectors, and LiveView integration
Add comprehensive state change subscription system with memoized selectors and automatic LiveView integration for reactive UIs. New Features: - Redux.Selector: Memoized selectors with reselect-style composition * create_selector/2 for composed selectors with automatic caching * select/2 for extracting state with memoization * Recursive selector composition support * Process-isolated caching for thread safety * Cache management (clear_cache/0, cache_stats/0) - Redux.Subscription: State change notification system * subscribe_to_struct/3 for subscribing to state changes * Optional selector support for fine-grained notifications * Shallow equality checks to prevent unnecessary callbacks * Automatic notification on dispatch/time_travel/reset * Error handling for failing callbacks and selectors - Redux.LiveView: LiveView integration helpers * assign_from_session/3 for automatic assign updates * subscribe_to_session/2-4 for subscribing to Redux changes * subscribe_to_pubsub/3 for distributed state listening * dispatch_to_session/2 and get_session_state/1 helpers - Phoenix.PubSub Integration: * enable_pubsub/3 and disable_pubsub/1 for distributed state * Automatic broadcasting on dispatch when configured * subscribe_to_broadcasts/3 for cross-node listening - Redux.ReduxExamples: Comprehensive usage examples * LiveView integration patterns * Phoenix Channels examples * Selectors and subscriptions examples * PubSub and distributed state examples Enhancements: - Redux module: * Add subscriptions field to Redux struct * Add pubsub and pubsub_topic fields * Add subscribe/2-3, unsubscribe/2, notify_subscriptions/1 * Automatic subscription notification after dispatch * Time travel and reset now notify subscriptions * Enhanced documentation with comprehensive examples - Telemetry: * Add 8 new Redux telemetry events: - [:phoenix, :session_process, :redux, :dispatch] - [:phoenix, :session_process, :redux, :subscribe] - [:phoenix, :session_process, :redux, :unsubscribe] - [:phoenix, :session_process, :redux, :notification] - [:phoenix, :session_process, :redux, :selector_cache_hit] - [:phoenix, :session_process, :redux, :selector_cache_miss] - [:phoenix, :session_process, :redux, :pubsub_broadcast] - [:phoenix, :session_process, :redux, :pubsub_receive] Tests: - Add 51 comprehensive tests across 3 test files - Selector tests: 25 tests covering memoization, composition, cache - Subscription tests: 18 tests covering lifecycle, notifications, errors - Integration tests: 8 tests covering workflows, performance, recovery - All tests passing with 100% coverage of new features Documentation: - Update README.md with Redux subscription/selector examples - Update CLAUDE.md with new module descriptions - Add comprehensive Redux.ReduxExamples module - Add LiveView and Channels integration examples - Document all new telemetry events Dependencies: - Add phoenix_pubsub ~> 2.1 for distributed state support Breaking Changes: None Files Changed: - 7 new files (4 modules + 3 test files) - 6 modified files (Redux, Telemetry, docs, mix.exs) - +3,466 lines total
1 parent 46408c5 commit 959f713

File tree

13 files changed

+3466
-25
lines changed

13 files changed

+3466
-25
lines changed

CLAUDE.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ The library is organized into several logical groups:
6767
- `Phoenix.SessionProcess.DefaultSessionProcess` - Default session implementation
6868

6969
**State Management Utilities**:
70-
- `Phoenix.SessionProcess.Redux` - Optional Redux-style state with actions/reducers (advanced use cases)
70+
- `Phoenix.SessionProcess.Redux` - Optional Redux-style state with actions/reducers, subscriptions, and selectors (advanced use cases)
71+
- `Phoenix.SessionProcess.Redux.Selector` - Memoized selectors for efficient derived state
72+
- `Phoenix.SessionProcess.Redux.Subscription` - Subscription management for reactive state changes
73+
- `Phoenix.SessionProcess.Redux.LiveView` - LiveView integration helpers
7174
- `Phoenix.SessionProcess.MigrationExamples` - Migration examples for Redux
75+
- `Phoenix.SessionProcess.ReduxExamples` - Comprehensive Redux usage examples
7276

7377
**Configuration & Error Handling**:
7478
- `Phoenix.SessionProcess.Config` - Configuration management
@@ -109,9 +113,14 @@ The library is organized into several logical groups:
109113
- Runs cleanup tasks periodically
110114

111115
6. **Phoenix.SessionProcess.Redux** (lib/phoenix/session_process/redux.ex:1)
112-
- Optional Redux-style state management with actions and reducers
116+
- Optional Redux-style state management with actions, reducers, subscriptions, and selectors
113117
- Provides time-travel debugging, middleware support, and action history
114-
- Best for complex applications requiring predictable state updates and audit trails
118+
- **Redux.Selector**: Memoized selectors with reselect-style composition for efficient derived state
119+
- **Redux.Subscription**: Subscribe to state changes with optional selectors (only notifies when selected values change)
120+
- **Redux.LiveView**: Helper module for LiveView integration with automatic assign updates
121+
- **Phoenix.PubSub integration**: Broadcast state changes across nodes for distributed applications
122+
- **Comprehensive telemetry**: Monitor Redux operations (dispatch, subscribe, selector cache hits/misses, PubSub broadcasts)
123+
- Best for complex applications requiring reactive UIs, predictable state updates, audit trails, or distributed state
115124
- Note: Most applications don't need this - standard GenServer state is sufficient
116125

117126
### Process Management Flow

README.md

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,14 +401,184 @@ Phoenix.SessionProcess.cast("session_123", {:dispatch, {:add_to_cart, %{id: 101,
401401
{:ok, history} = Phoenix.SessionProcess.call("session_123", :get_history)
402402
```
403403

404+
#### Redux with Subscriptions and Selectors
405+
406+
React to specific state changes with subscriptions and selectors:
407+
408+
```elixir
409+
defmodule MyApp.ReactiveSession do
410+
use Phoenix.SessionProcess, :process
411+
alias Phoenix.SessionProcess.Redux
412+
alias Phoenix.SessionProcess.Redux.Selector
413+
414+
@impl true
415+
def init(_init_arg) do
416+
redux = Redux.init_state(%{user: nil, cart: [], total: 0})
417+
418+
# Subscribe to user changes
419+
redux =
420+
Redux.subscribe(redux, fn state -> state.user end, fn user ->
421+
IO.inspect(user, label: "User changed")
422+
end)
423+
424+
# Subscribe with memoized selector for cart total
425+
cart_total_selector =
426+
Selector.create_selector(
427+
[fn state -> state.cart end],
428+
fn cart ->
429+
Enum.reduce(cart, 0, fn item, acc -> acc + item.price end)
430+
end
431+
)
432+
433+
redux =
434+
Redux.subscribe(redux, cart_total_selector, fn total ->
435+
IO.inspect(total, label: "Cart total")
436+
end)
437+
438+
{:ok, %{redux: redux}}
439+
end
440+
441+
@impl true
442+
def handle_call({:dispatch, action}, _from, state) do
443+
new_redux = Redux.dispatch(state.redux, action, &reducer/2)
444+
{:reply, {:ok, Redux.get_state(new_redux)}, %{state | redux: new_redux}}
445+
end
446+
447+
defp reducer(state, action) do
448+
case action do
449+
{:set_user, user} -> %{state | user: user}
450+
{:add_to_cart, item} -> %{state | cart: [item | state.cart]}
451+
{:clear_cart} -> %{state | cart: []}
452+
_ -> state
453+
end
454+
end
455+
end
456+
```
457+
458+
#### Redux with LiveView
459+
460+
Automatically update LiveView assigns from Redux state:
461+
462+
```elixir
463+
defmodule MyAppWeb.ShoppingCartLive do
464+
use Phoenix.LiveView
465+
alias Phoenix.SessionProcess.Redux.LiveView, as: ReduxLV
466+
alias Phoenix.SessionProcess.Redux.Selector
467+
468+
def mount(_params, %{"session_id" => session_id}, socket) do
469+
if connected?(socket) do
470+
# Define selectors
471+
cart_count_selector = Selector.create_selector(
472+
[fn state -> state.cart end],
473+
fn cart -> length(cart) end
474+
)
475+
476+
cart_total_selector = Selector.create_selector(
477+
[fn state -> state.cart end],
478+
fn cart -> Enum.reduce(cart, 0, &(&1.price + &2)) end
479+
)
480+
481+
# Auto-subscribe to Redux changes
482+
socket =
483+
ReduxLV.assign_from_session(socket, session_id, %{
484+
user: fn state -> state.user end,
485+
cart_count: cart_count_selector,
486+
cart_total: cart_total_selector
487+
})
488+
489+
{:ok, assign(socket, session_id: session_id)}
490+
else
491+
{:ok, assign(socket, session_id: session_id, user: nil, cart_count: 0, cart_total: 0)}
492+
end
493+
end
494+
495+
# Handle automatic Redux assign updates
496+
def handle_info({:redux_assign_update, key, value}, socket) do
497+
{:noreply, ReduxLV.handle_assign_update(socket, key, value)}
498+
end
499+
500+
def handle_event("add_item", %{"item" => item}, socket) do
501+
ReduxLV.dispatch_to_session(socket.assigns.session_id, {:add_to_cart, item})
502+
{:noreply, socket}
503+
end
504+
505+
def render(assigns) do
506+
~H\"\"\"
507+
<div>
508+
<h2>Welcome, <%= @user.name %></h2>
509+
<p>Cart: <%= @cart_count %> items</p>
510+
<p>Total: $<%= @cart_total %></p>
511+
</div>
512+
\"\"\"
513+
end
514+
end
515+
```
516+
517+
#### Redux with PubSub (Distributed)
518+
519+
Share state across nodes with Phoenix.PubSub:
520+
521+
```elixir
522+
defmodule MyApp.DistributedSession do
523+
use Phoenix.SessionProcess, :process
524+
alias Phoenix.SessionProcess.Redux
525+
526+
@impl true
527+
def init(arg) do
528+
session_id = Keyword.get(arg, :session_id)
529+
530+
# Enable PubSub broadcasting
531+
redux =
532+
Redux.init_state(
533+
%{data: %{}},
534+
pubsub: MyApp.PubSub,
535+
pubsub_topic: "session:\#{session_id}"
536+
)
537+
538+
{:ok, %{redux: redux}}
539+
end
540+
541+
@impl true
542+
def handle_call({:dispatch, action}, _from, state) do
543+
# Dispatch automatically broadcasts via PubSub
544+
new_redux = Redux.dispatch(state.redux, action, &reducer/2)
545+
{:reply, {:ok, Redux.get_state(new_redux)}, %{state | redux: new_redux}}
546+
end
547+
548+
defp reducer(state, action) do
549+
case action do
550+
{:update, data} -> %{state | data: Map.merge(state.data, data)}
551+
_ -> state
552+
end
553+
end
554+
end
555+
556+
# Listen from any node
557+
defmodule MyApp.RemoteListener do
558+
def listen(session_id) do
559+
Redux.subscribe_to_broadcasts(
560+
MyApp.PubSub,
561+
"session:\#{session_id}",
562+
fn %{action: action, state: state} ->
563+
IO.inspect({action, state}, label: "Remote state change")
564+
end
565+
)
566+
end
567+
end
568+
```
569+
404570
**Redux Features:**
405571
- **Time-travel debugging** - Access complete action history
406572
- **Middleware support** - Add logging, validation, side effects
573+
- **Subscriptions** - React to specific state changes with callbacks
574+
- **Selectors with memoization** - Efficient derived state computation
575+
- **LiveView integration** - Automatic assign updates
576+
- **Phoenix.PubSub support** - Distributed state notifications across nodes
407577
- **State persistence** - Serialize and restore state
408578
- **Predictable updates** - All changes through explicit actions
409-
- **Developer tools** - Inspect actions and state changes
579+
- **Comprehensive telemetry** - Monitor Redux operations
410580

411-
**Best for:** Complex applications, team collaboration, debugging requirements, state persistence needs.
581+
**Best for:** Complex applications, team collaboration, debugging requirements, state persistence needs, real-time reactive UIs.
412582

413583
### Comparison
414584

@@ -451,6 +621,16 @@ The library emits comprehensive telemetry events for monitoring and debugging:
451621
- `[:phoenix, :session_process, :cleanup]` - When a session is cleaned up
452622
- `[:phoenix, :session_process, :cleanup_error]` - When cleanup fails
453623

624+
### Redux State Management Events
625+
- `[:phoenix, :session_process, :redux, :dispatch]` - When a Redux action is dispatched
626+
- `[:phoenix, :session_process, :redux, :subscribe]` - When a subscription is created
627+
- `[:phoenix, :session_process, :redux, :unsubscribe]` - When a subscription is removed
628+
- `[:phoenix, :session_process, :redux, :notification]` - When subscriptions are notified
629+
- `[:phoenix, :session_process, :redux, :selector_cache_hit]` - When selector cache is hit
630+
- `[:phoenix, :session_process, :redux, :selector_cache_miss]` - When selector cache misses
631+
- `[:phoenix, :session_process, :redux, :pubsub_broadcast]` - When state is broadcast via PubSub
632+
- `[:phoenix, :session_process, :redux, :pubsub_receive]` - When PubSub broadcast is received
633+
454634
### Example Telemetry Setup
455635

456636
```elixir

0 commit comments

Comments
 (0)