VitalRouter Documentation

repository·main·Indexed 18 days ago

https://github.com/hadashia/vitalrouter

A high-performance, zero-allocation in-memory messaging library for C# and Unity. It features a unidirectional flow consisting of Publishers, Interceptors, and Handlers, utilizing Roslyn Source Generators for routing via [Routes] and [Route] attributes. Supports advanced command ordering (Parallel, Sequential, Drop, Switch), command pooling via IPoolableCommand, and integration with VContainer and Microsoft.Extensions.DependencyInjection.

Tokens
23.6K
Snippets
75
Records
89
Agent score
62%

What's inside VitalRouter

  1. Introduction to VitalRouter.MRuby

    main

    VitalRouter.MRuby is an extension that allows you to use Ruby scripting to control the publishing of commands within VitalRouter. This is particularly useful for implementing game scenarios, branching logic, or complex state management using a Domain Specific Language (DSL) that is more readable than C# code.

    Key benefits include:

    • Natural Language DSLs: Ruby's syntax allows for creating highly readable scripts.
    • Non-blocking Async/Await: While C# handlers are executing async/await, the mruby side suspends and waits using mruby's Fiber without blocking the main thread. This makes it compatible with single-threaded environments like Unity WebGL.
    • Platform Support: Starting with version 2, the extension uses ChibiRuby (a pure C# implementation), meaning it runs on any platform where C# is supported.
  2. Supported Dependency Injection (DI) libraries in VitalRouter

    main

    VitalRouter supports several DI libraries to manage object lifecycles, dependencies, and state (such as Subscribe/Unsubscribe). Using DI allows for automatic Interceptor dependency resolution.

    Supported libraries include:

    • Unity:
      • VContainer
    • .NET:
      • Microsoft.Extensions.DependencyInjection
  3. How Router inheritance and isolation work in VContainer scopes

    main

    VitalRouter supports VContainer's hierarchical scope system:

    • Inheritance (Default): By default, a RegisterVitalRouter call in a child scope inherits the Router defined in the parent scope. If a parent publishes via ICommandPublisher, both parent and child presenters receive the command. If a child publishes, both parent and child presenters receive it.
    • Isolation: If you want a child scope to have its own dedicated Router that does not share commands with the parent, set routing.Isolated = true during registration.

    Use routing.Isolated = true when you want to prevent child scope commands from leaking to the parent or vice versa.

    // In a ChildLifetimeScope
    builder.RegisterVitalRouter(routing =>  
    {
        // Creates a dedicated Router for this scope only
        routing.Isolated = true;
        routing.Map<PresenterB>();  
    });
  4. UniTask integration optimizations

    main

    When UniTask integration is enabled via the VITALROUTER_UNITASK_INTEGRATION compiler switch, VitalRouter applies the following optimization:

    • Locking Mechanism: Instead of the default SemaphoreSlim used for locking async methods, the framework selects IUniTaskSource-based implementations to improve performance in UniTask-heavy environments.
  5. Apply Filters to a Router

    main

    Filters allow you to execute common logic before or after a command reaches its handler. You can use WithFilter to wrap a router with interceptor logic.

    How Filters Work

    • Derived Routers: WithFilter returns a derived child router. Subscribers on this child receive commands published on the parent, but only after passing through the filter.
    • Cumulative Chain: When publishing on a child router, the entire filter chain from the root down to that child is executed. Each filter in the tree is invoked exactly once per publish.
    • Snapshot Behavior: The cumulative filter chain is snapshotted at the moment WithFilter is called. Adding a filter to an ancestor after a child has been created will not affect that existing child's chain.
    • Conditional Execution: You can use filters to either perform side effects (like logging) or to drop commands by choosing whether or not to call next(cmd, context).
    Router.Default
        .WithFilter(async (cmd, context, next) =>
        {
            if (condition) await next(cmd, context);
        })
       .Subscribe((cmd, context) => { /* ... */ });
  6. Apply Filters using the [Filter] attribute

    main

    The [Filter] attribute allows you to insert processing logic (similar to middleware or interceptors) before and after a command is delivered to a handler.

    Filters can be applied at two levels:

    1. Class-wide: Applied to the class declaration, affecting all routes within that class.
    2. Method-level: Applied to a specific [Route] method, affecting only that handler.
    [Routes]
    [Filter(typeof(Filter1))] // Class-wide filter
    public partial class FooPresenter
    {
        [Route]
        [Filter(typeof(Filter2))] // Method-specific filter
        async ValueTask On(FooCommand cmd)
        {
            // ...
        }
    }
  7. Optimize VitalRouter performance with zero-allocation publishing

    main

    VitalRouter is designed for high-performance messaging, comparable to plain event dispatching. To achieve zero extra heap memory allocation during publishing, ensure that your commands implement ICommand as a struct. This prevents boxing when the command is passed to the router. This makes it suitable for granular messaging in performance-critical environments like games.

    // Using a struct for ICommand ensures zero-allocation publishing
    public record struct MyCommand(int Id) : ICommand;
  8. Control async handler execution with CommandOrdering

    main

    VitalRouter provides the CommandOrdering enum to control how asynchronous handlers behave when multiple commands are published. This is critical for managing concurrency, such as ensuring dialogue or cutscenes play in a specific order without overlapping.

    Available Ordering Modes

    OrderingBehaviorTypical Use Case
    Parallel (default)Run all handlers concurrently.Independent reactions (e.g., multiple UI elements reacting to one event).
    SequentialQueue commands and run them one at a time in the order they were received.Dialogue, cutscenes, tutorials.
    DropIgnore new incoming commands while a handler is currently running.Debouncing buttons or preventing double-firing of actions.
    SwitchCancel the currently running handler and immediately start the new one."Latest wins" scenarios like re-targeting or search-as-you-type.

    Implementation Example: Sequential Cutscenes

    To ensure a sequence of commands (like walking, speaking, and waiting) executes in order, apply CommandOrdering.Sequential to the presenter class using the [Routes] attribute.

    public readonly record struct WalkCommand(Vector3 To) : ICommand;
    public readonly record struct SpeakCommand(string Text) : ICommand;
    public readonly record struct WaitCommand(float Seconds) : ICommand;
    
    // `Sequential`: each command waits for the previous handler to finish.
    [Routes(CommandOrdering.Sequential)]
    public partial class CutscenePresenter : MonoBehaviour
    {
        [Route]
        async UniTask On(WalkCommand cmd) => await character.WalkToAsync(cmd.To);
    
        [Route]
        async UniTask On(SpeakCommand cmd) => await dialogueView.ShowAsync(cmd.Text);
    
        [Route]
        async UniTask On(WaitCommand cmd) => await UniTask.Delay(TimeSpan.FromSeconds(cmd.Seconds));
    }
    
    // Usage:
    router.PublishAsync(new WalkCommand(stage.Center));
    router.PublishAsync(new SpeakCommand("Hello there!"));
    router.PublishAsync(new WaitCommand(0.5f));
    router.PublishAsync(new SpeakCommand("Welcome to our little town."));
  9. Configure Command Ordering (Sequential Control)

    main

    VitalRouter allows you to declaratively control how concurrent async handlers run. This is useful for preventing overlapping logic in scenarios like game cutscenes or dialogue. You can set the ordering globally, per Router, per [Routes] class, per [Route] method, or per SubscribeAwait call.

    OrderingBehaviorTypical use
    Parallel (default)Run all handlers concurrentlyIndependent reactions
    SequentialQueue, then run one at a time in orderDialogue, cutscenes, tutorials
    DropIgnore new commands while one is still runningDebounce buttons, prevent double-firing
    SwitchCancel the running handler, start the new one"Latest wins" — re-targeting, search-as-you-type
    // `Sequential`: each command waits for the previous handler to finish.
    [Routes(CommandOrdering.Sequential)]
    public partial class CutscenePresenter : MonoBehaviour
    {
        [Route]
        async UniTask On(WalkCommand cmd) => await character.WalkToAsync(cmd.To);
    
        [Route]
        async UniTask On(SpeakCommand cmd) => await dialogueView.ShowAsync(cmd.Text);
    
        [Route]
        async UniTask On(WaitCommand cmd) => await UniTask.Delay(TimeSpan.FromSeconds(cmd.Seconds));
    }
  10. How VitalRouter messaging works

    main

    VitalRouter follows a unidirectional messaging flow designed for high performance and zero allocation. The flow consists of three main stages:

    1. Publisher: Sends a Command (a lightweight data structure) to a Router using PublishAsync.
    2. Pipeline (Interceptors): The Router passes the command through an optional chain of Interceptors (middleware) that can process, filter, or modify the flow.
    3. Handler (Presenter): The command is finally dispatched to one or more Handlers that implement the logic for that specific command.

    Commands should ideally be defined as readonly record struct to ensure zero-allocation messaging.

    // Commands are lightweight data structures representing events or actions.
    // record structs are recommended for zero-allocation messaging
    public readonly record struct MoveCommand(Vector3 Destination) : ICommand;
  11. Control Async Handler Concurrency with CommandOrdering

    main

    VitalRouter allows you to declaratively control how concurrent async handlers execute. This is critical for maintaining order in sequences like cutscenes or tutorials.

    Available strategies include:

    • CommandOrdering.Sequential: Each command waits for the previous handler to finish. This ensures handlers run strictly in order (e.g., WalkCommand finishes before SpeakCommand starts).
    • Drop: Ignores overlapping commands (useful for debouncing).
    • Switch: Cancels the currently running handler and lets the latest command win.
    // Example of sequential execution for a cutscene
    [Routes(CommandOrdering.Sequential)]
    public partial class CutscenePresenter : MonoBehaviour
    {
        [Route]
        async UniTask On(WalkCommand cmd) => await character.WalkToAsync(cmd.To);
    
        [Route]
        async UniTask On(SpeakCommand cmd) => await dialogueView.ShowAsync(cmd.Text);
    }
    
    // Usage: These will be queued and executed one after another
    router.PublishAsync(new WalkCommand(stage.Center));
    router.PublishAsync(new SpeakCommand("Hello!"));
  12. Configure Command Ordering with [Routes(CommandOrdering)]

    main

    By default, VitalRouter delivers the next command immediately, even if a previous asynchronous command handler has not yet completed. To change this behavior and ensure commands are processed one after another, use the CommandOrdering parameter in the [Routes] attribute.

    Use CommandOrdering.Sequential to hold off the delivery of the next command until the current [Route] method has completed. This is useful for managing sequences or conversations in game logic.

    [Routes(CommandOrdering.Sequential)]
    public partial class FooPresenter
    {
        async ValueTask On(FooCommand cmd)
        {
            // The next command will wait for this to finish
        }
    }