Packages
phoenix_session_process
1.0.0-rc.0
Session isolation and state management for Phoenix applications
Current section
Files
Jump to
Current section
Files
lib/phoenix/session_process/reducer_compiler.ex
defmodule Phoenix.SessionProcess.ReducerCompiler do
@moduledoc """
Compile-time support for reducer modules.
This module handles the compilation of `@throttle` and `@debounce` module attributes
into metadata functions that can be queried at runtime.
## Module Attributes
### @throttle
Throttle an action - execute immediately, then block for duration:
@throttle {"fetch-list", "3000ms"}
def handle_action(%{type: "fetch-list"}, state) do
# Only executes once per 3 seconds
end
### @debounce
Debounce an action - delay execution until duration passes since last call:
@debounce {"update-query", "500ms"}
def handle_action(%{type: "update-query", payload: query}, state) do
# Waits 500ms after last call before executing
end
## Generated Functions
The compiler generates the following metadata functions:
- `__reducer_throttles__/0` - Returns list of `{action_pattern, duration}` tuples
- `__reducer_debounces__/0` - Returns list of `{action_pattern, duration}` tuples
- `__reducer_module__/0` - Returns `true` to mark as a reducer module
- `__reducer_action_prefix__/0` - Returns the action prefix for routing (can be nil)
"""
@doc """
Called before module compilation completes.
Generates metadata functions based on accumulated module attributes.
"""
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defmacro __before_compile__(env) do
throttles = Module.get_attribute(env.module, :action_throttles) || []
debounces = Module.get_attribute(env.module, :action_debounces) || []
name = Module.get_attribute(env.module, :name)
action_prefix = Module.get_attribute(env.module, :action_prefix)
# Reverse to maintain declaration order
throttles = Enum.reverse(throttles)
debounces = Enum.reverse(debounces)
# Validate name is set and is an atom
unless name do
raise CompileError,
file: env.file,
line: env.line,
description: """
Reducer module #{inspect(env.module)} must define @name attribute.
Example:
defmodule MyReducer do
use Phoenix.SessionProcess, :reducer
@name :my_reducer
@action_prefix "my" # Optional, defaults to @name, can be nil or "" for no prefix
def handle_action(%Action{type: "my.action"}, state) do
# ...
end
end
"""
end
unless is_atom(name) do
raise CompileError,
file: env.file,
line: env.line,
description: """
Reducer @name must be an atom, got: #{inspect(name)}
Example:
@name :my_reducer # Correct - atom
@name "my_reducer" # Wrong - string
"""
end
# Default action_prefix to name (convert atom to string), unless explicitly set
# action_prefix can be nil or "" to indicate no prefix
action_prefix =
if action_prefix == nil and
Module.get_attribute(env.module, :action_prefix, :__not_set__) == :__not_set__ do
# @action_prefix not set at all, default to stringified name
to_string(name)
else
# @action_prefix was explicitly set (could be nil, "", or a string)
action_prefix
end
# Validate action_prefix is binary (or nil/"" for no prefix)
unless is_nil(action_prefix) or action_prefix == "" or is_binary(action_prefix) do
raise CompileError,
file: env.file,
line: env.line,
description: """
Reducer @action_prefix must be a binary string, nil, or "", got: #{inspect(action_prefix)}
Examples:
@action_prefix "user" # Correct - binary string
@action_prefix nil # Correct - no prefix
@action_prefix "" # Correct - no prefix
@action_prefix :user # Wrong - atom
"""
end
quote do
@doc """
Returns the reducer's name.
This is used to identify the reducer when routing actions.
"""
def __reducer_name__, do: unquote(name)
@doc """
Returns the reducer's action prefix.
Actions with type starting with this prefix will be routed to this reducer.
Can be nil or empty string to indicate no prefix routing.
## Examples
# With @action_prefix "user"
"user.reload" -> matches
"user.login" -> matches
"cart.add" -> no match
# With @action_prefix nil or ""
All actions without explicit routing are eligible
"""
def __reducer_action_prefix__, do: unquote(action_prefix)
@doc """
Returns the list of throttle configurations for this reducer.
Each entry is a tuple: `{action_pattern, duration_string}`
## Examples
iex> MyReducer.__reducer_throttles__()
[{"fetch-list", "3000ms"}]
"""
def __reducer_throttles__, do: unquote(Macro.escape(throttles))
@doc """
Returns the list of debounce configurations for this reducer.
Each entry is a tuple: `{action_pattern, duration_string}`
## Examples
iex> MyReducer.__reducer_debounces__()
[{"update-query", "500ms"}]
"""
def __reducer_debounces__, do: unquote(Macro.escape(debounces))
@doc """
Marks this module as a reducer module.
Used by the framework to identify reducer modules vs regular modules.
"""
def __reducer_module__, do: true
end
end
@doc """
Register a throttle attribute.
Called automatically when `@throttle` is used in a reducer module.
Should not be called directly.
"""
def register_throttle(module, {action_pattern, duration}) when is_binary(duration) do
Module.put_attribute(module, :action_throttles, {action_pattern, duration})
end
@doc """
Register a debounce attribute.
Called automatically when `@debounce` is used in a reducer module.
Should not be called directly.
"""
def register_debounce(module, {action_pattern, duration}) when is_binary(duration) do
Module.put_attribute(module, :action_debounces, {action_pattern, duration})
end
end