Jido.Signal.Router (Jido v1.1.0-rc)

View Source

The 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, and pattern matching functions.

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

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:

  1. Path complexity (more specific paths execute first)
  2. Priority (-100 to 100, higher executes first)
  3. Registration order (for equal priority/complexity)

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}}
])

Dynamic route management:

# Add routes
{:ok, router} = Router.add(router, [
  {"metrics.**", %Instruction{action: CollectMetrics}}
])

# Remove routes
{:ok, router} = Router.remove(router, "metrics.**")

Signal routing:

{:ok, instructions} = Router.route(router, %Signal{
  type: "payment.processed",
  data: %{amount: 2000}
})

Path Complexity Scoring

The router uses a sophisticated scoring system to determine handler execution order:

  1. Base score from segment count (length * 2000)
  2. Exact match bonuses (3000 per segment, weighted by position)
  3. 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

  1. 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
  2. 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
  3. Pattern Matching

    • Keep match functions simple and fast
    • Handle nil/missing data gracefully
    • Avoid side effects in match functions
    • Test edge cases thoroughly
  4. 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

Implementation Details

The router uses several specialized structs:

  • Route - Defines a single routing rule
  • TrieNode - Internal trie structure node
  • HandlerInfo - Stores handler metadata
  • PatternMatch - Encapsulates pattern matching rules

See the corresponding typespecs for detailed field information.

See Also

Summary

Functions

Adds one or more routes to the router.

Lists all routes currently registered in the router.

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

match()

@type match() :: (Jido.Signal.t() -> boolean())

priority()

@type priority() :: non_neg_integer()

route_spec()

wildcard_type()

@type wildcard_type() :: :single | :multi

Functions

add(router, routes)

@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}

list(router)

@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>
  }
]

merge(router, routes)

@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)

new(routes \\ nil)

@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.

new!(routes \\ nil)

@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.

normalize(input)

@spec normalize(route_spec() | [route_spec()] | [Jido.Signal.Router.Route.t()]) ::
  {: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, instruction} tuple
    • {path, instruction, priority} tuple
    • {path, match_fn, instruction} tuple
    • {path, match_fn, instruction, priority} tuple

Returns

  • {:ok, [%Route{}]} - List of normalized Route structs
  • {:error, term()} - If normalization fails

remove(router, paths)

@spec remove(Jido.Signal.Router.Router.t(), String.t() | [String.t()]) ::
  {:ok, Jido.Signal.Router.Router.t()} | {:error, term()}

Removes one or more routes from the router.

Parameters

  • router: The existing router struct
  • paths: A path string or list of path strings to remove

Returns

{:ok, updated_router} or {:error, reason}

Examples

# Remove a single route
{:ok, router} = Router.remove(router, "metrics.**")

# Remove multiple routes
{:ok, router} = Router.remove(router, ["audit.*", "user.created"])

route(router, signal)

@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

  • - List of matching instructions, may be empty if no matches
  • - Other errors that occurred during routing

Examples

{:ok, results} = Router.route(router, %Signal{
  type: "payment.processed",
  data: %{amount: 100}
})

validate(route)

@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

  • } - Single validated Route struct
  • ]} - List of validated Route structs
  • - If validation fails