Jido.Signal.Router (Jido v1.1.0-rc.2)
View SourceThe Router module implements a high-performance, trie-based signal routing system designed specifically for agent-based architectures. It provides sophisticated message routing capabilities with support for exact matches, wildcards, pattern matching functions, and multiple dispatch targets.
Core Concepts
The Router organizes signal handlers into an efficient trie (prefix tree) structure that enables:
- Fast path-based routing using dot-notation (e.g., "user.created.verified")
- Priority-based handler execution (-100 to 100)
- Complexity-based ordering for wildcard resolution
- Dynamic route management (add/remove at runtime)
- Pattern matching through custom functions
- Multiple dispatch targets per route
Path Patterns
Routes use a dot-notation pattern system supporting:
- Exact matches:
"user.created"
- Single wildcards:
"user.*.updated"
(matches one segment) - Multi-level wildcards:
"audit.**"
(matches zero or more segments)
Pattern rules:
- Paths must match:
^[a-zA-Z0-9.*_-]+(.[a-zA-Z0-9.*_-]+)*$
- Cannot contain consecutive dots (..)
- Cannot contain consecutive multi-wildcards (
**...**
)
Handler Priority
Handlers execute in order based on:
- Path complexity (more specific paths execute first)
- Priority (-100 to 100, higher executes first)
- Registration order (for equal priority/complexity)
Dispatch Configurations
The router supports two types of targets:
- Instructions - Standard action instructions with parameters
- Dispatch Configs - Adapter-based dispatch configurations
Dispatch configs can be specified in two formats:
- Single adapter:
{adapter, opts}
- Multiple adapters:
[{adapter1, opts1}, {adapter2, opts2}]
Usage Examples
Basic route creation:
{:ok, router} = Router.new([
# Simple route with default priority
{"user.created", %Instruction{action: HandleUserCreated}},
# High-priority audit logging
{"audit.**", %Instruction{action: AuditLogger}, 100},
# Pattern matching for large payments
{"payment.processed",
fn signal -> signal.data.amount > 1000 end,
%Instruction{action: HandleLargePayment}},
# Single dispatch target
{"metrics.collected", {MetricsAdapter, [type: :counter]}},
# Multiple dispatch targets
{"system.error", [
{MetricsAdapter, [type: :error]},
{AlertAdapter, [priority: :high]},
{LogAdapter, [level: :error]}
]}
])
Dynamic route management:
# Add routes
{:ok, router} = Router.add(router, [
{"metrics.**", {MetricsAdapter, [type: :gauge]}}
])
# Remove routes
{:ok, router} = Router.remove(router, "metrics.**")
Signal routing:
# Route to instruction
{:ok, [%Instruction{action: HandleUserCreated}]} = Router.route(router, %Signal{
type: "user.created",
data: %{id: "123"}
})
# Route to multiple dispatch targets
{:ok, [
{MetricsAdapter, [type: :error]},
{AlertAdapter, [priority: :high]},
{LogAdapter, [level: :error]}
]} = Router.route(router, %Signal{
type: "system.error",
data: %{message: "Critical error"}
})
Path Complexity Scoring
The router uses a sophisticated scoring system to determine handler execution order:
- Base score from segment count (length * 2000)
- Exact match bonuses (3000 per segment, weighted by position)
- Wildcard penalties:
- Single wildcard (): 1000 - position_index 100
- Multi-wildcard (*): 2000 - position_index 200
This ensures more specific routes take precedence over wildcards, while maintaining predictable execution order.
Best Practices
Route Design
- Use consistent, hierarchical path patterns
- Prefer exact matches over wildcards when possible
- Keep path segments meaningful and well-structured
- Document your path hierarchy
Priority Management
- Reserve high priorities (75-100) for critical handlers
- Use default priority (0) for standard business logic
- Reserve low priorities (-100 to -75) for metrics/logging
- Document priority ranges for your application
Pattern Matching
- Keep match functions simple and fast
- Handle nil/missing data gracefully
- Avoid side effects in match functions
- Test edge cases thoroughly
Dispatch Configuration
- Use single dispatch for simple routing
- Use multiple dispatch for cross-cutting concerns
- Keep adapter options minimal and focused
- Document adapter requirements
Performance Considerations
- Monitor route count in production
- Use pattern matching sparingly
- Consider complexity scores when designing paths
- Profile routing performance under load
Error Handling
The router provides detailed error feedback for:
- Invalid path patterns
- Priority out of bounds
- Invalid match functions
- Missing handlers
- Malformed signals
- Invalid dispatch configurations
Implementation Details
The router uses several specialized structs:
Route
- Defines a single routing ruleTrieNode
- Internal trie structure nodeHandlerInfo
- Stores handler metadataPatternMatch
- Encapsulates pattern matching rules
See the corresponding typespecs for detailed field information.
See Also
Jido.Signal
- Signal structure and validationJido.Instruction
- Handler instruction formatJido.Error
- Error types and handlingJido.Signal.Dispatch
- Dispatch adapter interface
Summary
Functions
Adds one or more routes to the router.
Filters a list of signals based on a pattern.
Checks if a route with the given ID exists in the router.
Lists all routes currently registered in the router.
Checks if a signal type matches a pattern.
Merges two routers by combining their routes.
Creates a new router with the given routes.
Creates a new router with the given routes, raising on error.
Normalizes route specifications into Route structs.
Removes one or more routes from the router.
Routes a signal through the router to find and execute matching handlers.
Validates one or more Route structs.
Types
@type match() :: (Jido.Signal.t() -> boolean())
@type path() :: String.t()
@type priority() :: non_neg_integer()
@type target() :: Jido.Instruction.t() | Jido.Signal.Dispatch.dispatch_config()
@type wildcard_type() :: :single | :multi
Functions
@spec add( Jido.Signal.Router.Router.t(), route_spec() | Jido.Signal.Router.Route.t() | [route_spec()] | [Jido.Signal.Router.Route.t()] ) :: {:ok, Jido.Signal.Router.Router.t()} | {:error, term()}
Adds one or more routes to the router.
Parameters
- router: The existing router struct
- routes: A route specification or list of route specifications in one of these formats:
- %Route{}
- {path, instruction}
- {path, instruction, priority}
- {path, [match: match_fn], instruction}
- {path, [match: match_fn], instruction, priority}
Returns
{:ok, updated_router}
or {:error, reason}
@spec filter([Jido.Signal.t()] | nil | any(), String.t() | nil | any()) :: [ Jido.Signal.t() ]
Filters a list of signals based on a pattern.
Parameters
- signals: List of signals to filter
- pattern: Pattern to filter by (e.g. "user." or "audit.*")
Returns
- List of signals whose types match the pattern
Examples
iex> signals = [
...> %Signal{type: "user.created"},
...> %Signal{type: "payment.processed"},
...> %Signal{type: "user.updated"}
...> ]
iex> Router.filter(signals, "user.*")
[%Signal{type: "user.created"}, %Signal{type: "user.updated"}]
iex> Router.filter(nil, "user.*")
[]
iex> Router.filter([], nil)
[]
iex> Router.filter("not a list", "user.*")
[]
@spec has_route?(Jido.Signal.Router.Router.t(), String.t()) :: boolean()
Checks if a route with the given ID exists in the router.
Parameters
- router: The router struct to check
- route_id: The ID of the route to check for
Returns
true
if the route existsfalse
otherwise
@spec list(Jido.Signal.Router.Router.t()) :: {:ok, [Jido.Signal.Router.Route.t()]}
Lists all routes currently registered in the router.
Returns a list of Route structs containing the path, instruction, priority and match function for each registered route.
Returns
{:ok, [%Route{}]}
- List of Route structs
Examples
{:ok, routes} = Router.list(router)
# Returns:
[
%Route{
path: "user.created",
instruction: %Instruction{action: MyApp.Actions.HandleUserCreated},
priority: 0,
match: nil
},
%Route{
path: "payment.processed",
instruction: %Instruction{action: MyApp.Actions.HandleLargePayment},
priority: 90,
match: #Function<1.123456789/1>
}
]
Checks if a signal type matches a pattern.
Parameters
- type: The signal type to check (e.g. "user.created")
- pattern: The pattern to match against (e.g. "user." or "audit.*")
Returns
true
if the type matches the patternfalse
otherwise
Examples
iex> Router.matches?("user.created", "user.*")
true
iex> Router.matches?("audit.user.created", "audit.**")
true
iex> Router.matches?("user.created", "payment.*")
false
iex> Router.matches?("user.profile.updated", "user.*")
false
iex> Router.matches?(nil, "user.*")
false
iex> Router.matches?("user.created", nil)
false
@spec merge(Jido.Signal.Router.Router.t(), [Jido.Signal.Router.Route.t()]) :: {:ok, Jido.Signal.Router.Router.t()} | {:error, term()}
Merges two routers by combining their routes.
Takes a target router and a list of routes from another router (obtained via list/1
) and
merges them together, preserving priorities and match functions.
Parameters
- router: The target Router struct to merge into
- routes: List of Route structs to merge in (from Router.list/1)
Returns
{:ok, merged_router}
or {:error, reason}
Examples
{:ok, router1} = Router.new([{"user.created", instruction1}])
{:ok, router2} = Router.new([{"payment.processed", instruction2}])
{:ok, routes2} = Router.list(router2)
# Merge router2's routes into router1
{:ok, merged} = Router.merge(router1, routes2)
@spec new(route_spec() | [route_spec()] | [Jido.Signal.Router.Route.t()] | nil) :: {:ok, Jido.Signal.Router.Router.t()} | {:error, term()}
Creates a new router with the given routes.
@spec new!(route_spec() | [route_spec()] | [Jido.Signal.Router.Route.t()] | nil) :: Jido.Signal.Router.Router.t()
Creates a new router with the given routes, raising on error.
@spec normalize( Jido.Signal.Router.Route.t() | [Jido.Signal.Router.Route.t()] | route_spec() | [route_spec()] ) :: {:ok, [Jido.Signal.Router.Route.t()]} | {:error, term()}
Normalizes route specifications into Route structs.
Parameters
input
- One of:- Single Route struct
- List of Route structs
- List of route_spec tuples
- {path, target} tuple where target is:
- %Instruction{}
- {adapter, opts}
- [{adapter, opts}, ...]
- {path, target, priority} tuple
- {path, match_fn, target} tuple
- {path, match_fn, target, priority} tuple
Returns
{:ok, [%Route{}]}
- List of normalized Route structs{:error, term()}
- If normalization fails
@spec remove(Jido.Signal.Router.Router.t(), String.t() | [String.t()]) :: {:ok, Jido.Signal.Router.Router.t()}
Removes one or more routes from the router.
Parameters
- router: The Router struct to modify
- paths: A path string or list of path strings to remove
Returns
{:ok, updated_router}
- Routes removed successfully
Examples
{:ok, router} = Router.remove(router, "metrics.collected")
{:ok, router} = Router.remove(router, ["user.created", "user.updated"])
@spec route(Jido.Signal.Router.Router.t(), Jido.Signal.t()) :: {:ok, [Jido.Instruction.t()]} | {:error, term()}
Routes a signal through the router to find and execute matching handlers.
Parameters
- router: The router struct to use for routing
- signal: The signal to route
Returns
{:ok, [instruction]}
- List of matching instructions, may be empty if no matches{:error, term()}
- Other errors that occurred during routing
Examples
{:ok, results} = Router.route(router, %Signal{
type: "payment.processed",
data: %{amount: 100}
})
@spec validate(Jido.Signal.Router.Route.t() | [Jido.Signal.Router.Route.t()]) :: {:ok, Jido.Signal.Router.Route.t() | [Jido.Signal.Router.Route.t()]} | {:error, term()}
Validates one or more Route structs.
Parameters
- routes: A %Route{} struct or list of %Route{} structs to validate
Returns
{:ok, %Route{}}
- Single validated Route struct{:ok, [%Route{}]}
- List of validated Route structs{:error, term()}
- If validation fails