Skip to content

Commit 8894a1e

Browse files
committed
feat: implement PubSub-based LiveView integration and consolidate process macros
BREAKING CHANGE: :process_link macro is deprecated in favor of :process This commit introduces a modern PubSub-based LiveView integration and consolidates the session process macros for a simpler, more maintainable API. Features: - Add PubSub-based LiveView integration with mount/unmount lifecycle - Consolidate :process and :process_link macros (all processes now linked by default) - Add broadcast_state_change/1-2 helper for session state broadcasts - Add Phoenix.SessionProcess.LiveView helper module with mount_session/unmount_session - Support distributed LiveView subscriptions across nodes - Remove manual LiveView tracking from process state Breaking Changes: - :process_link macro deprecated (use :process instead) - Removed handle_cast({:monitor, pid}) from :process macro - Removed handle_info({:DOWN, ...}) for LiveView monitoring - Removed :session_expired message on process termination - LiveView integration now requires PubSub configuration Migration: - Change "use Phoenix.SessionProcess, :process_link" to ":process" - Use Phoenix.SessionProcess.LiveView.mount_session/3 in LiveView mount - Call broadcast_state_change(state) in session processes to notify LiveViews - Add pubsub configuration: config :phoenix_session_process, pubsub: MyApp.PubSub Documentation: - Update CLAUDE.md with new architecture and LiveView patterns - Update README.md with PubSub integration examples - Add comprehensive example file: examples/liveview_integration_example.ex - Update MIGRATION_GUIDE.md with Redux usage fixes Tests: - Add 49 new tests for PubSub LiveView integration - Test coverage: mount/unmount, broadcast, error handling, distributed scenarios - All 176 tests passing - Test files: live_view_test.exs, pubsub_broadcast_test.exs, live_view_integration_test.exs
1 parent 7e7a8da commit 8894a1e

File tree

12 files changed

+2189
-81
lines changed

12 files changed

+2189
-81
lines changed

CLAUDE.md

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,14 @@ The library is organized into several logical groups:
6666
- `Phoenix.SessionProcess.Cleanup` - TTL-based cleanup
6767
- `Phoenix.SessionProcess.DefaultSessionProcess` - Default session implementation
6868

69+
**LiveView Integration**:
70+
- `Phoenix.SessionProcess.LiveView` - PubSub-based LiveView integration helpers (recommended)
71+
6972
**State Management Utilities**:
7073
- `Phoenix.SessionProcess.Redux` - Optional Redux-style state with actions/reducers, subscriptions, and selectors (advanced use cases)
7174
- `Phoenix.SessionProcess.Redux.Selector` - Memoized selectors for efficient derived state
7275
- `Phoenix.SessionProcess.Redux.Subscription` - Subscription management for reactive state changes
73-
- `Phoenix.SessionProcess.Redux.LiveView` - LiveView integration helpers
76+
- `Phoenix.SessionProcess.Redux.LiveView` - Redux-specific LiveView integration helpers
7477
- `Phoenix.SessionProcess.MigrationExamples` - Migration examples for Redux
7578
- `Phoenix.SessionProcess.ReduxExamples` - Comprehensive Redux usage examples
7679

@@ -88,7 +91,9 @@ The library is organized into several logical groups:
8891
1. **Phoenix.SessionProcess** (lib/phoenix/session_process.ex:1)
8992
- Main module providing the public API
9093
- Delegates to ProcessSupervisor for actual process management
91-
- Provides two macros: `:process` (basic) and `:process_link` (with LiveView monitoring)
94+
- Provides the `:process` macro which includes PubSub broadcast helpers
95+
- The macro injects: `broadcast_state_change/1-2` and `session_topic/0` functions
96+
- Note: `:process_link` is deprecated - use `:process` instead
9297
- Key functions: `start/1-3`, `call/2-3`, `cast/2`, `terminate/1`, `started?/1`, `list_session/0`
9398

9499
2. **Phoenix.SessionProcess.Supervisor** (lib/phoenix/session_process/superviser.ex:1)
@@ -112,12 +117,20 @@ The library is organized into several logical groups:
112117
- Schedules session expiration on creation
113118
- Runs cleanup tasks periodically
114119

115-
6. **Phoenix.SessionProcess.Redux** (lib/phoenix/session_process/redux.ex:1)
120+
6. **Phoenix.SessionProcess.LiveView** (lib/phoenix/session_process/live_view.ex:1)
121+
- PubSub-based LiveView integration for session processes
122+
- **Recommended approach** for connecting LiveViews to session state
123+
- Key functions: `mount_session/3-4`, `unmount_session/1`, `dispatch/2`, `subscribe/2`
124+
- Automatically handles subscription/unsubscription and initial state loading
125+
- Works across distributed nodes via Phoenix.PubSub
126+
- Simpler and more maintainable than manual process monitoring
127+
128+
7. **Phoenix.SessionProcess.Redux** (lib/phoenix/session_process/redux.ex:1)
116129
- Optional Redux-style state management with actions, reducers, subscriptions, and selectors
117130
- Provides time-travel debugging, middleware support, and action history
118131
- **Redux.Selector**: Memoized selectors with reselect-style composition for efficient derived state
119132
- **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
133+
- **Redux.LiveView**: Helper module for Redux-specific LiveView integration
121134
- **Phoenix.PubSub integration**: Broadcast state changes across nodes for distributed applications
122135
- **Comprehensive telemetry**: Monitor Redux operations (dispatch, subscribe, selector cache hits/misses, PubSub broadcasts)
123136
- Best for complex applications requiring reactive UIs, predictable state updates, audit trails, or distributed state
@@ -139,8 +152,12 @@ The library is organized into several logical groups:
139152

140153
- Uses Registry for bidirectional lookups (session_id ↔ pid, pid ↔ module)
141154
- DynamicSupervisor for on-demand process creation
142-
- Macros inject GenServer boilerplate and provide `get_session_id/0` helper
143-
- `:process_link` macro adds LiveView monitoring: sessions monitor LiveView processes and send `:session_expired` message on termination
155+
- `:process` macro injects GenServer boilerplate and PubSub broadcast helpers
156+
- **PubSub-based LiveView integration** (recommended):
157+
- Session processes broadcast state changes via `broadcast_state_change/1-2`
158+
- LiveViews subscribe using `Phoenix.SessionProcess.LiveView.mount_session/3-4`
159+
- Works across distributed nodes
160+
- Cleaner separation of concerns vs manual process monitoring
144161
- Telemetry events for all lifecycle operations (start, stop, call, cast, cleanup, errors)
145162
- Comprehensive error handling with Phoenix.SessionProcess.Error module
146163

@@ -152,22 +169,80 @@ config :phoenix_session_process,
152169
session_process: MySessionProcess, # Default session module
153170
max_sessions: 10_000, # Maximum concurrent sessions
154171
session_ttl: 3_600_000, # Session TTL in milliseconds (1 hour)
155-
rate_limit: 100 # Sessions per minute limit
172+
rate_limit: 100, # Sessions per minute limit
173+
pubsub: MyApp.PubSub # Optional: PubSub module for LiveView integration
156174
```
157175

158176
Configuration options:
159177
- `session_process`: Default module for session processes (defaults to `Phoenix.SessionProcess.DefaultSessionProcess`)
160178
- `max_sessions`: Maximum concurrent sessions (defaults to 10,000)
161179
- `session_ttl`: Session TTL in milliseconds (defaults to 1 hour)
162180
- `rate_limit`: Sessions per minute limit (defaults to 100)
181+
- `pubsub`: PubSub module for broadcasting state changes (optional, required for LiveView integration)
163182

164183
## Usage in Phoenix Applications
165184

166185
1. Add supervisor to application supervision tree
167186
2. Add SessionId plug after fetch_session in router
168-
3. Define custom session process modules using `:process` or `:process_link` macros
187+
3. Define custom session process modules using the `:process` macro
169188
4. Start processes with session IDs
170189
5. Communicate using call/cast operations
190+
6. For LiveView integration, use `Phoenix.SessionProcess.LiveView` helpers
191+
192+
### LiveView Integration Example
193+
194+
**Session Process:**
195+
```elixir
196+
defmodule MyApp.SessionProcess do
197+
use Phoenix.SessionProcess, :process
198+
199+
@impl true
200+
def init(_) do
201+
{:ok, %{count: 0, user: nil}}
202+
end
203+
204+
@impl true
205+
def handle_cast({:increment}, state) do
206+
new_state = %{state | count: state.count + 1}
207+
# Broadcast to LiveViews
208+
broadcast_state_change(new_state)
209+
{:noreply, new_state}
210+
end
211+
212+
@impl true
213+
def handle_call(:get_state, _from, state) do
214+
{:reply, {:ok, state}, state}
215+
end
216+
end
217+
```
218+
219+
**LiveView:**
220+
```elixir
221+
defmodule MyAppWeb.DashboardLive do
222+
use Phoenix.LiveView
223+
alias Phoenix.SessionProcess.LiveView, as: SessionLV
224+
225+
def mount(_params, %{"session_id" => session_id}, socket) do
226+
# Subscribe and get initial state
227+
case SessionLV.mount_session(socket, session_id, MyApp.PubSub) do
228+
{:ok, socket, state} ->
229+
{:ok, assign(socket, state: state)}
230+
{:error, _} ->
231+
{:ok, socket}
232+
end
233+
end
234+
235+
# Receive state updates
236+
def handle_info({:session_state_change, new_state}, socket) do
237+
{:noreply, assign(socket, state: new_state)}
238+
end
239+
240+
def terminate(_reason, socket) do
241+
SessionLV.unmount_session(socket)
242+
:ok
243+
end
244+
end
245+
```
171246

172247
## State Management Options
173248

MIGRATION_GUIDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Add the Redux module to your session process:
3434
```elixir
3535
defmodule MyApp.SessionProcess do
3636
use Phoenix.SessionProcess, :process
37-
use Phoenix.SessionProcess.Redux
38-
37+
alias Phoenix.SessionProcess.Redux
38+
3939
# ... rest of your module
4040
end
4141
```
@@ -127,7 +127,7 @@ Replace all state manipulation with Redux actions:
127127
```elixir
128128
defmodule MyApp.ShoppingCartProcess do
129129
use Phoenix.SessionProcess, :process
130-
use Phoenix.SessionProcess.Redux
130+
alias Phoenix.SessionProcess.Redux
131131

132132
@impl true
133133
def init(_args) do
@@ -180,7 +180,7 @@ Add logging and validation middleware:
180180
```elixir
181181
defmodule MyApp.SessionProcess do
182182
use Phoenix.SessionProcess, :process
183-
use Phoenix.SessionProcess.Redux
183+
alias Phoenix.SessionProcess.Redux
184184

185185
@impl true
186186
def init(_args) do
@@ -209,7 +209,7 @@ Use action history for debugging:
209209
```elixir
210210
defmodule MyApp.DebugSessionProcess do
211211
use Phoenix.SessionProcess, :process
212-
use Phoenix.SessionProcess.Redux
212+
alias Phoenix.SessionProcess.Redux
213213

214214
@impl true
215215
def init(_args) do

README.md

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ config :phoenix_session_process,
110110
session_process: MyApp.SessionProcess, # Default session module
111111
max_sessions: 10_000, # Maximum concurrent sessions
112112
session_ttl: 3_600_000, # Session TTL in milliseconds (1 hour)
113-
rate_limit: 100 # Sessions per minute limit
113+
rate_limit: 100, # Sessions per minute limit
114+
pubsub: MyApp.PubSub # PubSub module for LiveView integration
114115
```
115116

116117
## Usage Examples
@@ -142,46 +143,88 @@ end
142143

143144
### With LiveView Integration
144145

145-
Create a session process that monitors LiveView processes:
146+
Phoenix.SessionProcess provides PubSub-based LiveView integration for real-time state synchronization.
147+
148+
#### Session Process with Broadcasting
146149

147150
```elixir
148-
defmodule MyApp.SessionProcessWithLiveView do
149-
use Phoenix.SessionProcess, :process_link
151+
defmodule MyApp.SessionProcess do
152+
use Phoenix.SessionProcess, :process
150153

151154
@impl true
152155
def init(_init_arg) do
153-
{:ok, %{user: nil, live_views: []}}
156+
{:ok, %{user: nil, count: 0}}
154157
end
155158

156159
@impl true
157-
def handle_call(:get_user, _from, state) do
158-
{:reply, state.user, state}
160+
def handle_call(:get_state, _from, state) do
161+
{:reply, {:ok, state}, state}
159162
end
160163

161164
@impl true
162165
def handle_cast({:set_user, user}, state) do
163-
{:noreply, %{state | user: user}}
166+
new_state = %{state | user: user}
167+
# Broadcast state changes to all subscribers
168+
broadcast_state_change(new_state)
169+
{:noreply, new_state}
170+
end
171+
172+
@impl true
173+
def handle_cast(:increment, state) do
174+
new_state = %{state | count: state.count + 1}
175+
broadcast_state_change(new_state)
176+
{:noreply, new_state}
164177
end
165178
end
166179
```
167180

168-
In your LiveView:
181+
#### LiveView with Session Integration
169182

170183
```elixir
171-
defmodule MyAppWeb.UserLive do
172-
use MyAppWeb, :live_view
184+
defmodule MyAppWeb.DashboardLive do
185+
use Phoenix.LiveView
186+
alias Phoenix.SessionProcess.LiveView, as: SessionLV
187+
188+
def mount(_params, %{"session_id" => session_id}, socket) do
189+
# Subscribe to session state and get initial state
190+
case SessionLV.mount_session(socket, session_id, MyApp.PubSub) do
191+
{:ok, socket, state} ->
192+
{:ok, assign(socket, state: state, session_id: session_id)}
173193

174-
def mount(_params, %{"session_id" => session_id} = _session, socket) do
175-
Phoenix.SessionProcess.cast(session_id, {:monitor, self()})
176-
{:ok, assign(socket, session_id: session_id)}
194+
{:error, _reason} ->
195+
{:ok, redirect(socket, to: "/login")}
196+
end
197+
end
198+
199+
# Automatically receive state updates
200+
def handle_info({:session_state_change, new_state}, socket) do
201+
{:noreply, assign(socket, state: new_state)}
202+
end
203+
204+
# Send messages to session
205+
def handle_event("increment", _params, socket) do
206+
SessionLV.dispatch_async(socket.assigns.session_id, :increment)
207+
{:noreply, socket}
177208
end
178209

179-
def handle_info(:session_expired, socket) do
180-
{:noreply, redirect(socket, to: "/login")}
210+
# Clean up subscription on terminate
211+
def terminate(_reason, socket) do
212+
SessionLV.unmount_session(socket)
213+
:ok
181214
end
182215
end
183216
```
184217

218+
#### Configuration for LiveView
219+
220+
Add PubSub module to your config:
221+
222+
```elixir
223+
# config/config.exs
224+
config :phoenix_session_process,
225+
pubsub: MyApp.PubSub # Required for LiveView integration
226+
```
227+
185228
## API Reference
186229

187230
### Starting Sessions

0 commit comments

Comments
 (0)