Skip to content

Commit 5bc6260

Browse files
committed
feat: add meta.reducers for explicit reducer targeting without prefix stripping
Add support for explicitly targeting specific reducers via meta.reducers, which bypasses normal prefix routing and passes the full action type unchanged (without prefix stripping). Features: - meta.reducers accepts list of reducer names (atoms) - Bypasses all normal prefix routing when specified - Passes full action type to reducers WITHOUT prefix stripping - Logs warning when targeting non-existent reducers - Empty list results in no-op (no reducers called) Implementation: - Modified strip_action_prefix/2 to accept skip_strip parameter - Updated apply_combined_reducer/5 to detect explicit targeting - Enhanced filter_reducers_for_action/2 with missing reducer warnings - Updated documentation for dispatch/4 and Action module Examples: # Normal dispatch (prefix stripped) dispatch(session_id, "user.reload") # → UserReducer receives "reload" # Explicit targeting (prefix NOT stripped) dispatch(session_id, "user.reload", nil, reducers: [:user, :cart]) # → Only :user and :cart called, both receive "user.reload" # Warning for missing reducers dispatch(session_id, "reload", nil, reducers: [:nonexistent]) # → Logs: "Missing reducers: [:nonexistent]" All existing tests pass (148/148).
1 parent 01bfef0 commit 5bc6260

File tree

2 files changed

+74
-24
lines changed

2 files changed

+74
-24
lines changed

lib/phoenix/session_process.ex

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -623,26 +623,47 @@ defmodule Phoenix.SessionProcess do
623623
- `action_type` - Action type identifier (binary string, required)
624624
- `payload` - Action payload (any term, defaults to nil)
625625
- `meta` - Action metadata (keyword list, defaults to []):
626-
- `:reducers` - List of reducer names to target
627-
- `:reducer_prefix` - Prefix to filter reducers by
626+
- `:reducers` - List of reducer names (atoms) to target explicitly. When specified:
627+
* Bypasses normal prefix routing entirely
628+
* Only calls the specified reducers
629+
* Passes full action type WITHOUT prefix stripping
630+
* Logs warning if any reducers don't exist
631+
- `:reducer_prefix` - Prefix to filter reducers by (when `:reducers` not specified)
628632
- `:async` - Route to handle_async/3 (true) or handle_action/2 (false, default)
629633
634+
## Action Type Stripping Behavior
635+
636+
**Normal dispatch** (without `meta.reducers`):
637+
- Reducers with `@action_prefix "user"` receive action type with prefix stripped
638+
- Example: `dispatch(id, "user.reload")` → reducer sees `"reload"`
639+
640+
**Explicit targeting** (with `meta.reducers`):
641+
- Action type is passed unchanged to specified reducers
642+
- Example: `dispatch(id, "user.reload", nil, reducers: [:user])` → reducer sees `"user.reload"`
643+
630644
## Returns
631645
- `:ok` - Action dispatched successfully
632646
- `{:error, {:session_not_found, session_id}}` - If session doesn't exist
633647
634648
## Examples
635649
636-
# Simple action
650+
# Simple action with prefix routing (type stripped)
637651
:ok = SessionProcess.dispatch(session_id, "user.reload")
652+
# → UserReducer receives "reload" (prefix stripped)
638653
639654
# Action with payload
640655
:ok = SessionProcess.dispatch(session_id, "user.update", %{name: "Alice"})
641656
642-
# Target specific reducers
643-
:ok = SessionProcess.dispatch(session_id, "reload", nil, reducers: [:user, :cart])
657+
# Force specific reducers (type NOT stripped)
658+
:ok = SessionProcess.dispatch(session_id, "user.reload", nil, reducers: [:user, :cart])
659+
# → ONLY :user and :cart reducers called
660+
# → Both receive "user.reload" (prefix NOT stripped)
644661
645-
# Use prefix routing
662+
# Warning logged for missing reducers
663+
:ok = SessionProcess.dispatch(session_id, "reload", nil, reducers: [:nonexistent])
664+
# → Logs warning: "Missing reducers: [:nonexistent]"
665+
666+
# Use prefix filter
646667
:ok = SessionProcess.dispatch(session_id, "fetch-data", nil, reducer_prefix: "user")
647668
648669
# Async action (routed to handle_async/3)
@@ -1577,19 +1598,24 @@ defmodule Phoenix.SessionProcess do
15771598
end
15781599

15791600
# Strip action prefix before passing to reducer if reducer has a prefix
1580-
defp strip_action_prefix(action, reducer_prefix) do
1581-
if reducer_prefix && reducer_prefix != "" do
1582-
case String.split(action.type, ".", parts: 2) do
1583-
[^reducer_prefix, local_type] ->
1584-
%{action | type: local_type}
1585-
1586-
_ ->
1587-
# Prefix doesn't match, keep as-is
1588-
action
1589-
end
1590-
else
1591-
# No prefix or catch-all reducer, pass unchanged
1601+
defp strip_action_prefix(action, reducer_prefix, skip_strip \\ false) do
1602+
if skip_strip do
1603+
# When using meta.reducers for explicit targeting, don't strip prefix
15921604
action
1605+
else
1606+
if reducer_prefix && reducer_prefix != "" do
1607+
case String.split(action.type, ".", parts: 2) do
1608+
[^reducer_prefix, local_type] ->
1609+
%{action | type: local_type}
1610+
1611+
_ ->
1612+
# Prefix doesn't match, keep as-is
1613+
action
1614+
end
1615+
else
1616+
# No prefix or catch-all reducer, pass unchanged
1617+
action
1618+
end
15931619
end
15941620
end
15951621

@@ -1601,8 +1627,10 @@ defmodule Phoenix.SessionProcess do
16011627
slice_state = Map.get(app_state, slice_key, %{})
16021628

16031629
# Get reducer's action prefix and strip it from action type
1630+
# Skip stripping when using explicit reducer targeting (meta.reducers)
16041631
reducer_prefix = module.__reducer_action_prefix__()
1605-
local_action = strip_action_prefix(action, reducer_prefix)
1632+
skip_strip = not is_nil(Action.target_reducers(action))
1633+
local_action = strip_action_prefix(action, reducer_prefix, skip_strip)
16061634

16071635
# Check throttle first
16081636
if ActionRateLimiter.should_throttle?(module, action, internal_state) do
@@ -1750,6 +1778,7 @@ defmodule Phoenix.SessionProcess do
17501778
# Filter reducers based on action routing metadata
17511779
defp filter_reducers_for_action(action, all_reducers) do
17521780
alias Phoenix.SessionProcess.Action
1781+
require Logger
17531782

17541783
# Check explicit reducer targeting (highest priority)
17551784
case Action.target_reducers(action) do
@@ -1759,7 +1788,23 @@ defmodule Phoenix.SessionProcess do
17591788

17601789
target_list when is_list(target_list) ->
17611790
# Explicit list of reducer names to target
1762-
Enum.filter(all_reducers, fn {name, _} -> name in target_list end)
1791+
filtered_reducers = Enum.filter(all_reducers, fn {name, _} -> name in target_list end)
1792+
1793+
# Log warning if any requested reducers are missing
1794+
requested_names = MapSet.new(target_list)
1795+
found_names = MapSet.new(Enum.map(filtered_reducers, fn {name, _} -> name end))
1796+
missing = MapSet.difference(requested_names, found_names)
1797+
1798+
if MapSet.size(missing) > 0 do
1799+
Logger.warning("""
1800+
Action dispatched with meta.reducers targeting non-existent reducers.
1801+
Action type: #{action.type}
1802+
Missing reducers: #{inspect(MapSet.to_list(missing))}
1803+
Available reducers: #{inspect(Map.keys(all_reducers))}
1804+
""")
1805+
end
1806+
1807+
filtered_reducers
17631808
end
17641809
end
17651810

lib/phoenix/session_process/action.ex

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ defmodule Phoenix.SessionProcess.Action do
1111
- `payload` - Action data (any term)
1212
- `meta` - Action metadata:
1313
- `async: true` - Route to handle_async/3 instead of handle_action/2
14-
- `reducers: [:user, :cart]` - Only call these named reducers
15-
- `reducer_prefix: "user"` - Only call reducers with this prefix
14+
- `reducers: [:user, :cart]` - List of reducer names (atoms) to target explicitly.
15+
* Bypasses normal prefix routing
16+
* Only specified reducers are called
17+
* Action type passed WITHOUT prefix stripping
18+
* Warning logged if reducer doesn't exist
19+
- `reducer_prefix: "user"` - Only call reducers with this prefix (when `reducers` not specified)
1620
- Custom metadata for middleware/logging
1721
1822
## Usage
@@ -28,8 +32,9 @@ defmodule Phoenix.SessionProcess.Action do
2832
# With meta (async) - note: meta is a keyword list
2933
dispatch(session_id, "user.fetch", %{page: 1}, async: true)
3034
31-
# Target specific reducers
32-
dispatch(session_id, "reload", nil, reducers: [:user, :cart])
35+
# Target specific reducers (bypasses prefix routing, no prefix stripping)
36+
dispatch(session_id, "user.reload", nil, reducers: [:user, :cart])
37+
# → Only :user and :cart called, both receive "user.reload" unchanged
3338
3439
Reducers pattern match on the normalized Action struct for fast, consistent matching.
3540
"""

0 commit comments

Comments
 (0)