# nexus-rt

Single-threaded, event-driven runtime primitives with pre-resolved dispatch.

`nexus-rt` provides the building blocks for constructing runtimes where
user code runs as handlers dispatched over shared state. It is **not** an
async runtime — there is no task scheduler, no work stealing, no `Future`
polling. Your `main()` is the executor.

## Philosophy

`nexus-rt` is a lightweight, single-threaded runtime for event-driven
systems. It provides the state container, dependency injection, lifecycle
management, and dispatch infrastructure — but no implicit executor. Your
`main()` is the event loop. You decide what polls, in what order, and when.

The core idea: **declare what your functions need, and the framework wires
it up at build time.** Write plain Rust functions with `Res<T>` and
`ResMut<T>` parameters. The framework resolves those dependencies once when
you build the handler, then dispatches with zero framework overhead — a
single pointer deref per resource, no hashing, no lookups, no allocation.

**What nexus-rt is:**
- A typed singleton store (`World`) with direct-pointer access
- A dependency injection system for plain functions
- Composable handler and pipeline abstractions
- Single-threaded by design — for latency, not by accident

**What nexus-rt is not:**
- Not an async runtime (no `Future`, no `async`/`await`)
- Not a game engine ECS (no entities, no components, no archetypes)
- Not opinionated about IO, networking, or wire protocols — bring your own

If you need an analogy: it's the Bevy `SystemParam` + `World` model,
stripped down to singletons and adapted for sequential event processing
instead of parallel frame-based simulation.

## What in the World?!

### The World

Everything in `nexus-rt` revolves around the `World` — a typed singleton
store where each registered type gets exactly one value:

```rust
use nexus_rt::WorldBuilder;

let mut builder = WorldBuilder::new();
builder.register::<u64>(0);                   // one u64, initialized to 0
builder.register::<String>("hello".into());   // one String
let mut world = builder.build();              // freeze — no more registration
```

`WorldBuilder` is mutable — you register types into it. `build()` produces
a frozen `World`. After that, no types can be added or removed. This
constraint enables direct pointer access: each type gets a `ResourceId`
that is a direct pointer to its storage, and dispatch-time access is a
single pointer deref — zero framework overhead.

Outside of handlers, you can read and write resources directly:

```rust
let mut builder = nexus_rt::WorldBuilder::new();
builder.register::<u64>(0);
let mut world = builder.build();

assert_eq!(*world.resource::<u64>(), 0);
*world.resource_mut::<u64>() = 42;
assert_eq!(*world.resource::<u64>(), 42);
```

### Res\<T\> and ResMut\<T\> — Dependency Injection

The real power is that handler functions **declare** their dependencies in
their signatures. You don't pass resources manually — the framework
resolves them:

```rust
use nexus_rt::{Res, ResMut};

fn process(config: Res<u64>, mut state: ResMut<String>, event: f64) {
    if *config > 10 {
        *state = format!("processed {event}");
    }
}
```

This function declares:
- `Res<u64>` — "I need shared read access to the `u64` resource"
- `ResMut<String>` — "I need exclusive write access to the `String` resource"
- `event: f64` — "I receive an `f64` as my event" (always the last parameter)

When you convert this function into a handler, the framework resolves each
parameter against the `World`'s registry. At dispatch time, it fetches
the resources by direct pointer — no `HashMap` lookup, no type checking,
just a pointer deref.

`ResMut<T>` also participates in **change detection**: the act of writing
(via `DerefMut`) stamps a sequence number on the resource. Other handlers
can check `Res<T>::is_changed()` to see if the resource was modified during
the current event. This is automatic — no manual marking.

### Handlers — Connecting Functions to the World

`IntoHandler` converts a plain function into a `Handler` — the object-safe
dispatch trait. The conversion resolves parameters; after that, calling
`.run()` is a direct dispatch through pre-resolved indices:

```rust
use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Handler};

fn tick(mut counter: ResMut<u64>, event: u32) {
    *counter += event as u64;
}

let mut builder = WorldBuilder::new();
builder.register::<u64>(0);
let mut world = builder.build();

let mut handler = tick.into_handler(world.registry());

handler.run(&mut world, 10u32);
handler.run(&mut world, 20u32);
assert_eq!(*world.resource::<u64>(), 30);
```

The event parameter is always last. Everything before it is resolved as a
`Param` from the registry. If a required resource isn't registered,
`into_handler` panics at build time — not at dispatch time. Fail fast.

> **Named functions only.** Closures do not work with `IntoHandler` for
> arity-1+ (functions with `Param` arguments). This is a Rust type
> inference limitation with HRTBs and GATs — the same limitation Bevy has.
> Arity-0 pipeline steps (no `Param`) do accept closures.

### Plugins — Composable Registration

When you have a group of related resources, package them as a `Plugin`:

```rust
use nexus_rt::{Plugin, WorldBuilder};

struct PriceCache { prices: Vec<f64> }
struct RiskLimits { max_position: u64 }

struct TradingPlugin {
    risk_cap: u64,
}

impl Plugin for TradingPlugin {
    fn build(self, world: &mut WorldBuilder) {
        world.register(PriceCache { prices: Vec::new() });
        world.register(RiskLimits { max_position: self.risk_cap });
    }
}

let mut builder = WorldBuilder::new();
builder.install_plugin(TradingPlugin { risk_cap: 1000 });
// PriceCache and RiskLimits are now registered
```

Plugins are consumed by value — fire and forget. They're for organizing
registration, not for runtime behavior. Compose your system from multiple
plugins, each owning a domain's resources.

### Lifecycle — Startup, Run, Shutdown

After `build()`, you often need to initialize state that depends on
multiple resources being present. `run_startup` runs a handler once with
full dependency injection:

```rust
use nexus_rt::{WorldBuilder, Res, ResMut};

struct PriceCache { prices: Vec<f64> }
struct RiskLimits { max_position: u64 }

let mut builder = WorldBuilder::new();
builder.register(PriceCache { prices: Vec::new() });
builder.register(RiskLimits { max_position: 100 });

fn initialize(mut cache: ResMut<PriceCache>, config: Res<RiskLimits>, _event: ()) {
    // Both resources are available — set up initial state
    cache.prices.extend_from_slice(&[100.0, 200.0, 300.0]);
}

let mut world = builder.build();
world.run_startup(initialize);
```

For the event loop itself, `world.run()` polls until a handler triggers
shutdown:

```rust,ignore
use nexus_rt::shutdown::Shutdown;

// Handler triggers shutdown when done
fn check_done(counter: Res<u64>, shutdown: Res<Shutdown>, _event: ()) {
    if *counter >= 100 {
        shutdown.shutdown();
    }
}

world.run(|world| {
    // Your poll loop — called every iteration until shutdown
    timer.poll(world, Instant::now());
    io.poll(world, timeout);
    scheduler.run(world);
});
```

`world.run()` is a convenience — it's just `while !shutdown { f(self) }`.
You can also write the loop yourself if you need access to the shutdown
handle, custom exit conditions, or pre/post-iteration bookkeeping. Both
patterns are equivalent; `world.run()` is shorter when a shutdown flag is
all you need.

`Shutdown` is automatically registered by `WorldBuilder::build()`. The
event loop owns a `ShutdownHandle` (obtained via `world.shutdown_handle()`
if needed outside `world.run()`). With the `signals` feature,
`shutdown.enable_signals()` registers SIGINT/SIGTERM handlers
automatically.

The full lifecycle:

```text
WorldBuilder::new()
    → register resources
    → install_plugin(plugin)
    → install_driver(installer) → returns poller
    → build()
    → World (frozen)
        → run_startup(init_fn)     // one-shot init
        → run(|world| { ... })     // poll loop until shutdown
```

## Design

`nexus-rt` is heavily inspired by [Bevy ECS](https://bevyengine.org/).
Handlers as plain functions, `Param` for declarative dependency
injection, `Res<T>` / `ResMut<T>` wrappers, change detection via
sequence stamps, the `Plugin` trait for composable registration — these
are Bevy's ideas, and in many cases the implementation follows Bevy's
patterns closely (including the HRTB double-bound trick that makes
`IntoHandler` work). Credit where it's due: Bevy's system model is
an excellent piece of API design.

Where `nexus-rt` diverges is the target workload. Bevy is built for
simulation: many entities mutated per frame, parallel schedules,
component queries over archetypes. `nexus-rt` is built for event-driven
systems: singleton resources, sequential dispatch, and monotonic sequence
numbers instead of frame ticks. There are no entities, no components,
no archetypes — just a typed resource store where each event advances
a sequence counter and causality is tracked per-resource.

The result is a much smaller surface area tuned for low-latency event
processing rather than game-world state management.

## Architecture

```text
                        Build Time                          Dispatch Time
                   ┌──────────────────┐               ┌──────────────────────┐
                   │                  │               │                      │
                   │  WorldBuilder    │               │       World          │
                   │                  │               │                      │
                   │  ┌────────────┐  │    build()    │  ┌────────────────┐  │
                   │  │ Registry   │──┼──────────────►│  │ ResourceSlot[] │  │
                   │  │ TypeId→Idx │  │               │  │ ptr+changed_at │  │
                   │  └────────────┘  │               │  └───────┬────────┘  │
                   │                  │               │          │           │
                   │  install_plugin  │               │    get(id) ~3 cyc    │
                   │  install_driver  │               │          │           │
                   └──────────────────┘               └──────────┼───────────┘
                          │                                      │
                          │ returns Poller                       │
                          ▼                                      ▼
                   ┌──────────────────┐               ┌──────────────────────┐
                   │  Driver Poller   │               │  poll(&mut World)    │
                   │                  │               │                      │
                   │  Pre-resolved    │──────────────►│  1. next_sequence()  │
                   │  ResourceIds     │               │  2. get resources    │
                   │                  │               │  3. poll IO source   │
                   │  Owns pipeline   │               │  4. dispatch events  │
                   │  or handlers     │               │     via pipeline     │
                   └──────────────────┘               └──────────────────────┘
```

### Flow

1. **Build** — Register resources into `WorldBuilder`. Install plugins
   (fire-and-forget resource registration) and drivers (returns a poller).
2. **Freeze** — `builder.build()` produces an immutable `World`. All
   `ResourceId` values are direct pointers, valid for the lifetime of the World.
3. **Poll loop** — Your code calls `driver.poll(&mut world)` in a loop.
   Each driver owns its event lifecycle internally: poll IO, decode events,
   dispatch through its pipeline, mutate world state.
4. **Sequence** — Each event gets a monotonic sequence number via
   `world.next_sequence()`. **Drivers are responsible for calling this**
   before dispatching each event — the built-in timer and mio pollers
   do this automatically. `world.run()` does not advance the sequence;
   it is purely a shutdown-checked loop. Change detection is causal:
   `changed_at` records which event caused the mutation.

### Dispatch tiers

| Tier | Purpose | Overhead |
|------|---------|----------|
| **Pipeline** | Pre-resolved step chains inside drivers. The workhorse. | ~2 cycles p50 |
| **Callback** | Dynamic per-instance context + pre-resolved params. | ~2 cycles p50 |
| **Handler** | `Box<dyn Handler<E>>` for type-erased dispatch. | ~2 cycles p50 |
| **Template** | Pre-resolved handler stamping for re-registration. | ~1 cycle p50 (generate) |
| **DAG** | Monomorphized fan-out / merge data-flow graphs. | ~1-3 cycles p50 |
| **FanOut / Broadcast** | Static or dynamic fan-out by reference. | ~2 cycles p50 |

All tiers resolve `Param` state at build time. Dispatch-time cost is
a direct pointer deref — no hashing, no searching, no bounds check,
no Vec indirection.

## Driver Model

Drivers are event sources. The `Installer` trait handles installation;
the returned poller is a concrete type with its own `poll()` signature.

```rust
use nexus_rt::{Installer, WorldBuilder, World, ResourceId};

struct TimerInstaller { resolution_ms: u64 }
struct TimerPoller { timers_id: ResourceId }

impl Installer for TimerInstaller {
    type Poller = TimerPoller;

    fn install(self, world: &mut WorldBuilder) -> TimerPoller {
        world.register(Vec::<u64>::new());
        // ... register other resources ...
        let timers_id = world.registry().id::<Vec<u64>>();
        TimerPoller { timers_id }
    }
}

// Poller defines its own poll signature — NOT a trait method.
impl TimerPoller {
    fn poll(&mut self, world: &mut World, now_ms: u64) {
        // get resources via pre-resolved IDs, fire expired timers
    }
}
```

The executor is your `main()`:

```rust
let mut wb = WorldBuilder::new();
wb.install_plugin(TradingPlugin { /* config */ });
let timer = wb.install_driver(TimerInstaller { resolution_ms: 100 });
let io = wb.install_driver(IoInstaller::new());
let mut world = wb.build();

loop {
    let now = std::time::Instant::now();
    timer.poll(&mut world, now);
    io.poll(&mut world);
}
```

## Features

> **For Bevy users:** Many concepts map directly — `World` (singletons
> only, no entities/archetypes), `Res<T>`/`ResMut<T>` (same semantics),
> `SystemParam` → `Param`, `IntoSystem`/`System` → `IntoHandler`/`Handler`,
> `Plugin` (same pattern), `Local<T>` (same). The divergence is the
> execution model: sequential event dispatch instead of parallel
> frame-based schedules.

### World — typed singleton store

Type-erased resource storage with direct `ResourceId` pointers.
Dispatch-time access is a single pointer deref — zero framework overhead.
Frozen after build — no inserts, no removes.

### Res / ResMut — resource parameters

Declare resource dependencies in function signatures. `Res<T>` for shared
reads, `ResMut<T>` for exclusive writes. `ResMut` stamps `changed_at` on
`DerefMut` — the act of writing is the change signal. See
[Dependency Injection](#rest-and-resmut--dependency-injection) above.

### Optional resources

`Option<Res<T>>` and `Option<ResMut<T>>` resolve to `None` if the type
was not registered, rather than panicking at build time. Useful for
handlers that can operate with or without a particular resource.

```rust
fn maybe_log(logger: Option<Res<Logger>>, event: u32) {
    if let Some(log) = logger {
        log.info(event);
    }
}
```

### Param — build-time / dispatch-time resolution

The `Param` trait is the mechanism behind `Res<T>`, `ResMut<T>`,
`Local<T>`, and all other handler parameters. Two-phase resolution:

1. **Build time** — `Param::init(registry)` resolves opaque state (e.g.
   a `ResourceId`) and panics if the required type isn't registered.
2. **Dispatch time** — `Param::fetch(world, state)` uses the cached state
   to produce a reference via a single pointer deref — zero framework
   overhead.

Built-in impls: `Res<T>`, `ResMut<T>`, `Option<Res<T>>`,
`Option<ResMut<T>>`, `Local<T>`, `RegistryRef`, `()`, and tuples up to
8 params.

**Access conflicts** are caught at build time. If two parameters in the
same handler would borrow the same resource (e.g. `Res<T>` + `ResMut<T>`,
or two `ResMut<T>` for the same `T`), `into_handler` / `.then()` panics
with `"conflicting access"`. Pipeline and DAG steps enforce the same check
per-step. This is a build-time guarantee — dispatch never hits a conflict.

### Handler / IntoHandler — fn-to-handler conversion

`IntoHandler` converts a plain `fn` into a `Handler` trait object.
Event `E` is always the last parameter; everything before it is resolved
as `Param` from a `Registry`. Named functions only — closures do not
work with `IntoHandler` due to Rust's HRTB inference limitations with
GATs. See [Handlers](#handlers--connecting-functions-to-the-world)
above.

### Pipeline — pre-resolved processing chains

Typed composition chains where each step is a named function with
`Param` dependencies resolved at build time.

```rust
let reg = world.registry();
let mut pipeline = PipelineStart::<Order>::new()
    .then(validate, &reg)            // Order → Result<Order, Error>
    .and_then(enrich, &reg)          // Order → Result<Order, Error>
    .catch(log_error, &reg)          // Error → () (side effect)
    .map(submit, &reg)              // Order → Receipt
    .build();                        // → Pipeline<Order, _> (concrete)

pipeline.run(&mut world, order);
```

Option and Result combinators (`.map()`, `.and_then()`, `.catch()`,
`.filter()`, `.unwrap_or()`, etc.) enable typed flow control without
runtime overhead. `.splat()` destructures a tuple output (2-5 elements)
into individual function arguments for the next step — see
[Splat](#splat--tuple-destructuring) below. `Pipeline` implements
`Handler<In>`, so it can be boxed or stored alongside other handlers.

### Batch pipeline — per-item processing over a buffer

`build_batch(capacity)` produces a `BatchPipeline` that owns a
pre-allocated input buffer. Each item flows through the same chain
independently — errors are handled per-item, not per-batch.

```rust
let reg = world.registry();
let mut batch = PipelineStart::<Order>::new()
    .then(validate, &reg)            // Order → Result<Order, Error>
    .catch(log_error, &reg)          // handle error, continue batch
    .map(enrich, &reg)              // runs for valid items only
    .then(submit, &reg)
    .build_batch(1024);

// Driver fills input buffer
batch.input_mut().extend_from_slice(&orders);
batch.run(&mut world);  // drains buffer, no allocation
```

No intermediate buffers between steps. The compiler monomorphizes
the per-item chain identically to the single-event pipeline.

### DAG Pipeline — fan-out, merge, and data-flow graphs

`DagStart` builds a monomorphized data-flow graph where topology is
encoded in the type system. After monomorphization the entire DAG is
a single flat function — all values are stack locals, no arena, no
vtable dispatch.

```rust
use nexus_rt::{WorldBuilder, ResMut, Handler};
use nexus_rt::dag::DagStart;

let mut wb = WorldBuilder::new();
wb.register::<u64>(0);
let mut world = wb.build();
let reg = world.registry();

fn decode(raw: u32) -> u64 { raw as u64 * 2 }
fn add_one(val: &u64) -> u64 { *val + 1 }
fn mul3(val: &u64) -> u64 { *val * 3 }
fn merge_add(a: &u64, b: &u64) -> u64 { *a + *b }
fn store(mut out: ResMut<u64>, val: &u64) { *out = *val; }

let mut dag = DagStart::<u32>::new()
    .root(decode, reg)
    .fork()
    .arm(|a| a.then(add_one, reg))
    .arm(|b| b.then(mul3, reg))
    .merge(merge_add, reg)
    .then(store, reg)
    .build();

dag.run(&mut world, 5u32);
// root: 10, arm_a: 11, arm_b: 30, merge: 41
assert_eq!(*world.resource::<u64>(), 41);
```

Fan-out arms borrow the fork output by reference — no `Clone` needed.
Option and Result combinators (`.map()`, `.and_then()`, `.catch()`,
etc.) work on both the main chain and within arms. `Dag` implements
`Handler<E>`, so it can be boxed or stored alongside other handlers.

For linear chains without fan-out, prefer
[Pipeline](#pipeline--pre-resolved-processing-chains).

#### DAG combinator quick reference

| Category | Combinator | Signature | Effect |
|----------|-----------|-----------|--------|
| **Topology** | `.root(fn, reg)` | `E → T` | Entry point — takes event by value |
| | `.then(fn, reg)` | `&T → U` | Chain step — input by reference |
| | `.fork()` | | Begin fan-out — arms observe `&T` |
| | `.arm(\|a\| a.then(...))` | | Build one arm of a fork |
| | `.merge(fn, reg)` | `&A, &B → T` | Combine arm outputs |
| | `.join()` | | Terminate fork without merge (all arms → `()`) |
| **Flow control** | `.guard(fn, reg)` | `&T → Option<T>` | Wrap in Option via predicate |
| | `.tap(fn, reg)` | `&T → &T` | Observe without consuming |
| | `.route(pred, reg, arm_t, arm_f)` | `&T → U` | Binary conditional routing |
| | `.tee(arm)` | `&T → &T` | Side-effect arm, chain continues |
| | `.scan(init, fn, reg)` | `&mut Acc, &T → U` | Stateful transform with accumulator |
| | `.dedup()` | `T → Option<T>` | Suppress consecutive duplicates |
| **Option\<T\>** | `.map(fn, reg)` | `&T → U` | Map inner value (Some only) |
| | `.filter(fn, reg)` | `&T → Option<T>` | Keep on true, None on false |
| | `.inspect(fn, reg)` | `&T → &T` | Observe Some values |
| | `.and_then(fn, reg)` | `&T → Option<U>` | Flat-map inner value |
| | `.on_none(fn, reg)` | | Side effect on None |
| | `.ok_or(fn, reg)` | `→ Result<T, E>` | Convert None to Err |
| | `.ok_or_else(fn, reg)` | `→ Result<T, E>` | Convert None to Err (produced) |
| | `.unwrap_or(default)` | `→ T` | Unwrap with fallback |
| | `.unwrap_or_else(fn, reg)` | `→ T` | Unwrap with produced fallback |
| **Result\<T, E\>** | `.map(fn, reg)` | `&T → U` | Map Ok value |
| | `.and_then(fn, reg)` | `&T → Result<U, E>` | Flat-map Ok value |
| | `.catch(fn, reg)` | `E → ()` | Handle Err, continue with Ok |
| | `.map_err(fn, reg)` | `E → E2` | Transform error type |
| | `.or_else(fn, reg)` | `E → Result<T, E2>` | Recover from error |
| | `.inspect(fn, reg)` | `&T → &T` | Observe Ok values |
| | `.inspect_err(fn, reg)` | `&E → &E` | Observe Err values |
| | `.ok()` | `→ Option<T>` | Discard Err |
| | `.unwrap_or(default)` | `→ T` | Unwrap with fallback |
| | `.unwrap_or_else(fn, reg)` | `→ T` | Unwrap Err with produced fallback |
| **Bool** | `.not()` | `bool → bool` | Logical NOT |
| | `.and(fn, reg)` | `bool → bool` | Short-circuit AND |
| | `.or(fn, reg)` | `bool → bool` | Short-circuit OR |
| | `.xor(fn, reg)` | `bool → bool` | Logical XOR |
| **Tuple** | `.splat()` | `&(A, B, ...) → (&A, &B, ...)` | Destructure tuple so next `.then()` sees `&A, &B, ...` args |
| **Terminal** | `.dispatch(handler)` | `&T → ()` | Hand off to a Handler |
| | `.cloned()` | `&T → T` | Clone reference to owned |
| | `.build()` | | Finalize into `Dag<E>` |

All combinators accepting functions resolve `Param` dependencies at build
time via `IntoStep`, `IntoRefStep`, or `IntoProducer` — named functions
get direct-pointer access. Arity-0 closures work everywhere. Raw
`&mut World` closures are available as an escape hatch via `Opaque`.

#### Splat — tuple destructuring

Pipeline and DAG steps follow a single-value-in, single-value-out convention.
When a step naturally produces multiple outputs (e.g. splitting an order into
an ID and a price), `.splat()` destructures the tuple so the next step
receives individual arguments instead of the whole tuple:

```rust
// Pipeline (by value): fn(Params..., A, B) -> Out
fn split(order: Order) -> (OrderId, f64) { (order.id, order.price) }
fn process(id: OrderId, price: f64) -> bool { price > 0.0 }

PipelineStart::<Order>::new()
    .then(split, reg)
    .splat()            // (OrderId, f64) → individual args
    .then(process, reg) // receives OrderId, f64 separately
    .build();

// DAG (by reference): fn(Params..., &A, &B) -> Out
fn process_ref(id: &OrderId, price: &f64) -> bool { *price > 0.0 }

DagStart::<Order>::new()
    .root(split, reg)
    .splat()                // (OrderId, f64) → &OrderId, &f64
    .then(process_ref, reg)
    .build();
```

Supported for tuples of 2-5 elements. Beyond 5 arguments, use a named struct
— if a combinator stage needs that many inputs, the data likely deserves its
own type.

### FanOut / Broadcast — handler-level fan-out

`FanOut` dispatches the same event by reference to a fixed set of
handlers. Zero allocation, concrete types, monomorphizes to direct
calls. Macro-generated for arities 2-8.

`Broadcast` is the dynamic variant — stores `Vec<Box<dyn RefHandler<E>>>`
for runtime-determined handler counts.

```rust
use nexus_rt::{WorldBuilder, ResMut, IntoHandler, Handler};
use nexus_rt::{fan_out, Broadcast, Cloned};

fn write_a(mut sink: ResMut<u64>, event: &u32) { *sink += *event as u64; }
fn write_b(mut sink: ResMut<i64>, event: &u32) { *sink += *event as i64; }

let mut builder = WorldBuilder::new();
builder.register::<u64>(0);
builder.register::<i64>(0);
let mut world = builder.build();

let h1 = write_a.into_handler(world.registry());
let h2 = write_b.into_handler(world.registry());
let mut fan = fan_out!(h1, h2);
fan.run(&mut world, 5u32);
assert_eq!(*world.resource::<u64>(), 5);
assert_eq!(*world.resource::<i64>(), 5);
```

Handlers inside combinators receive `&E`. Use `Cloned` or `Owned`
adapters for handlers that expect owned events.

For fan-out with merge (data flowing back together), use
[DagStart](#dag-pipeline--fan-out-merge-and-data-flow-graphs).

### Change detection

Each resource tracks a `changed_at` sequence number. Drivers call
`world.next_sequence()` before each event dispatch to advance the
sequence counter.

```rust
// Read side — check if a resource was modified this sequence
fn observer(val: Res<u64>, _event: ()) {
    if val.is_changed() {
        // val was written during the current sequence
    }
}

// Write side — ResMut stamps changed_at on DerefMut automatically
fn writer(mut val: ResMut<u64>, _event: ()) {
    *val = 42; // stamps changed_at = current_sequence
}
```

### Local — per-handler state

`Local<T>` is state stored inside the handler instance, not in World.
Initialized with `Default::default()` at handler creation time. Each
handler instance gets its own independent copy — two handlers created
from the same function have separate `Local` values.

```rust
fn count_events(mut count: Local<u64>, mut total: ResMut<u64>, _event: u32) {
    *count += 1;
    *total = *count;
}

let mut handler_a = count_events.into_handler(registry);
let mut handler_b = count_events.into_handler(registry);

handler_a.run(&mut world, 0);  // handler_a local=1
handler_b.run(&mut world, 0);  // handler_b local=1 (independent)
handler_a.run(&mut world, 0);  // handler_a local=2
```

### Callback — context-owning handlers

`Callback<C, F, Params>` is a handler with per-instance owned context.
Use it when each handler instance needs private state that isn't shared
via World — per-timer metadata, per-connection codec state, protocol
state machines.

Convention: `fn handler(ctx: &mut C, params..., event: E)` — context
first, `Param`-resolved resources in the middle, event last.

```rust
struct TimerCtx { order_id: u64, fires: u64 }

fn on_timeout(ctx: &mut TimerCtx, mut counter: ResMut<u64>, _event: ()) {
    ctx.fires += 1;
    *counter += ctx.order_id;
}

let mut cb = on_timeout.into_callback(
    TimerCtx { order_id: 42, fires: 0 },
    registry,
);
cb.run(&mut world, ());

// Context is pub — accessible outside dispatch
assert_eq!(cb.ctx.fires, 1);
```

### HandlerTemplate / CallbackTemplate — resolve once, stamp many

When handlers are created repeatedly on the hot path — IO readiness
re-registration, timer rescheduling, connection accept loops — each
`into_handler(registry)` call pays for HashMap lookups to resolve the
same `ResourceId` values every time.

Templates resolve parameters once, then `generate()` stamps out
handlers by copying pre-resolved state — a flat memcpy vs ~20-70 cycles
of HashMap lookups for `into_handler`.

A [`Blueprint`] declares the event and parameter types. The template
resolves them against the registry once:

```rust
use nexus_rt::{WorldBuilder, ResMut, Handler};
use nexus_rt::template::{Blueprint, HandlerTemplate, CallbackTemplate, CallbackBlueprint};

struct OnTick;
impl Blueprint for OnTick {
    type Event = u32;
    type Params = (ResMut<'static, u64>,);
}

fn tick(mut counter: ResMut<u64>, event: u32) {
    *counter += event as u64;
}

let mut builder = WorldBuilder::new();
builder.register::<u64>(0);
let mut world = builder.build();

let template = HandlerTemplate::<OnTick>::new(tick, world.registry());

// Stamp out handlers — no HashMap lookups, just Copy.
let mut h1 = template.generate();
let mut h2 = template.generate();

h1.run(&mut world, 10);
h2.run(&mut world, 5);
assert_eq!(*world.resource::<u64>(), 15);
```

For context-owning handlers, `CallbackTemplate` works the same way —
each `generate(ctx)` takes an owned context value:

```rust
struct TimerCtx { order_id: u64 }

struct OnTimeout;
impl Blueprint for OnTimeout {
    type Event = ();
    type Params = (ResMut<'static, u64>,);
}
impl CallbackBlueprint for OnTimeout {
    type Context = TimerCtx;
}

fn on_timeout(ctx: &mut TimerCtx, mut counter: ResMut<u64>, _event: ()) {
    *counter += ctx.order_id;
}

let mut builder = WorldBuilder::new();
builder.register::<u64>(0);
let mut world = builder.build();

let cb_template = CallbackTemplate::<OnTimeout>::new(on_timeout, world.registry());
let mut cb = cb_template.generate(TimerCtx { order_id: 42 });
cb.run(&mut world, ());
assert_eq!(*world.resource::<u64>(), 42);
```

Convenience macros reduce Blueprint boilerplate:

```rust
use nexus_rt::handler_blueprint;
handler_blueprint!(OnTick, Event = u32, Params = (ResMut<'static, u64>,));
```

**Constraints:**
- `P::State: Copy` — excludes `Local<T>` with non-Copy state
  (incompatible with template stamping). All World-backed params
  (`Res`, `ResMut`, `Option` variants) have `State = ResourceId`
  which is `Copy`.
- Zero-sized callables only — named functions and captureless closures.
  Capturing closures and function pointers are rejected at compile time.

**Handler state sizes** (for capacity planning with inline storage):

`ResourceId` is pointer-sized (8 bytes on 64-bit). Each resource param
(`Res<T>`, `ResMut<T>`, `Option<Res<T>>`, `Option<ResMut<T>>`) stores
one `ResourceId` (8 bytes). Handler base overhead is 16 bytes (`&str`
name). Callbacks add the context size.

| Handler type | 0 params | 1 param | 2 params | 4 params | 8 params |
|-------------|----------|---------|----------|----------|----------|
| HandlerFn (no ctx) | 16 B | 24 B | 32 B | 48 B | 80 B |
| Callback (8 B ctx) | 24 B | 32 B | 40 B | 56 B | 88 B |

Formula: `16 + (8 × params) + context_size`. All fit comfortably
within 256-byte inline buffers (`FlatVirtual`, `InlineTimerWheel`).

### RegistryRef — runtime handler creation

`RegistryRef` is a `Param` that provides read-only access to the
`Registry` during handler dispatch. Enables handlers to create new
handlers at runtime via `IntoHandler::into_handler` or
`IntoCallback::into_callback`.

```rust
fn spawner(reg: RegistryRef, _event: ()) {
    let handler = some_fn.into_handler(&reg);
    // store handler somewhere...
}
```

### Installer — event source installation

`Installer` is the install-time trait for event sources. The installer
registers its resources into `WorldBuilder` and returns a concrete
poller whose `poll()` method drives the event lifecycle. See the
[Driver Model](#driver-model) section for the full pattern.

### Timer driver (feature: `timer`)

Integrates `nexus_timer::Wheel` as a driver. `TimerInstaller` registers
the wheel into `WorldBuilder` and returns a `TimerPoller`.

- `TimerPoller::poll(world, now)` drains expired timers and fires handlers
- Handlers reschedule themselves via `ResMut<TimerWheel<S>>`
- `Periodic` helper for recurring timers
- Inline storage variants behind `smartptr` feature: `InlineTimerWheel`,
  `FlexTimerWheel`

### Mio driver (feature: `mio`)

Integrates `mio` as an IO driver. `MioInstaller` registers the
`MioDriver` (wrapping `mio::Poll` + handler slab) and returns a
`MioPoller`.

- `MioPoller::poll(world, timeout)` polls for readiness and fires handlers
- Move-out-fire pattern: handler is removed from slab, fired, and must
  re-insert itself to receive more events
- Stale tokens (already removed) are silently skipped
- Inline storage variants behind `smartptr` feature: `InlineMio`, `FlexMio`

### Virtual / FlatVirtual / FlexVirtual — storage aliases

Type aliases for type-erased handler storage:

```rust
use nexus_rt::Virtual;

// Heap-allocated (default)
let handler: Virtual<Event> = Box::new(my_handler.into_handler(registry));

// Behind "smartptr" feature — inline storage via nexus-smartptr
// use nexus_rt::FlatVirtual;
// let handler: FlatVirtual<Event> = flat!(my_handler.into_handler(registry));
```

`Virtual<E>` for heap-allocated. `FlatVirtual<E>` for fixed inline
(panics if handler doesn't fit). `FlexVirtual<E>` for inline with
heap fallback.

### System / IntoSystem — reconciliation logic

Handlers react to individual events. But some computations need to run
*after* a batch of events has been processed — recomputing a theoretical
price after market data updates, checking risk limits after fills, etc.
These are reconciliation passes: they read the current state of the world,
decide if anything changed, and propagate downstream if so.

`System` is the dispatch trait for this. Distinct from `Handler<E>`,
systems take no event parameter and return `bool` to control downstream
propagation in a DAG scheduler.

| | Handler | System |
|---|---------|--------|
| Trigger | Per-event | Per-scheduler-pass |
| Event param | Yes (`E`) | No |
| Return | `()` | `bool` |
| Purpose | React | Reconcile |

```rust
fn compute_theo(mid: Res<MidPrice>, mut theo: ResMut<TheoValue>) -> bool {
    if mid.is_changed() {
        theo.0 = mid.0 * 1.001;
        true  // outputs changed — run downstream
    } else {
        false // nothing changed — skip downstream
    }
}
```

Convert via `IntoSystem` (same HRTB pattern as `IntoHandler`):

```rust
use nexus_rt::{IntoSystem, System};
let mut system = compute_theo.into_system(registry);
let changed = system.run(&mut world);
```

### DAG Scheduler — topological system execution

`SchedulerInstaller` builds a DAG of `System`s executed in topological order.
Root systems (no upstreams) always run. Non-root systems run only if at
least one upstream returned `true` (OR semantics).

```rust
use nexus_rt::scheduler::{SchedulerInstaller, SchedulerTick};

let mut installer = SchedulerInstaller::new();
let theo = installer.add(compute_theo, registry);
let quotes = installer.add(compute_quotes, registry);
let risk = installer.add(check_risk, registry);
installer.after(quotes, theo);   // quotes runs after theo
installer.after(risk, quotes);   // risk runs after quotes

let mut scheduler = wb.install_driver(installer);
let mut world = wb.build();

// In event loop: run scheduler after event processing
let systems_run = scheduler.run(&mut world);
```

`SchedulerTick` tracks the sequence at the last scheduler pass. Systems
use `Res::changed_after(tick.last())` to detect resources modified since
the previous pass — enabling skip-detection without manual bookkeeping.

Propagation is tracked via a `u64` bitmask (one bit per system), limiting
the scheduler to `MAX_SYSTEMS` (64) systems.

### Startup & Lifecycle

`Shutdown` is an interior-mutable flag automatically registered by
`WorldBuilder::build()`. Handlers trigger shutdown via `Res<Shutdown>`;
the event loop checks via `ShutdownHandle`:

```rust
use nexus_rt::{Res, WorldBuilder};
use nexus_rt::shutdown::Shutdown;

// Handler side
fn on_fatal(shutdown: Res<Shutdown>, _event: ()) {
    shutdown.shutdown();
}

// Event loop side
let mut world = WorldBuilder::new().build();
let shutdown = world.shutdown_handle();

while !shutdown.is_shutdown() {
    // poll drivers ...
    break; // (for example only)
}
```

With the `signals` feature, `ShutdownHandle::enable_signals()` registers
SIGINT/SIGTERM handlers (Linux only) that flip the shutdown flag
automatically.

### CatchAssertUnwindSafe — panic resilience

Wraps a handler to catch panics during `run()`, ensuring the handler is
never lost during move-out-fire dispatch (timer wheels, IO slabs). The
caller asserts that the handler and resources can tolerate partial writes.

```rust
use nexus_rt::{CatchAssertUnwindSafe, IntoHandler, Handler, Virtual};

let handler = tick.into_handler(registry);
let guarded = CatchAssertUnwindSafe::new(handler);
let mut boxed: Virtual<u32> = Box::new(guarded);
// Panics inside run() are caught — handler survives for re-dispatch
```

### Testing — TestHarness and TestTimerDriver

`TestHarness` provides isolated handler testing without wiring up drivers.
It owns a `World` and auto-advances the sequence counter before each dispatch.

```rust
use nexus_rt::testing::TestHarness;
use nexus_rt::{WorldBuilder, ResMut, IntoHandler};

fn accumulate(mut counter: ResMut<u64>, event: u64) {
    *counter += event;
}

let mut builder = WorldBuilder::new();
builder.register::<u64>(0);
let mut harness = TestHarness::new(builder);

let mut handler = accumulate.into_handler(harness.registry());
harness.dispatch(&mut handler, 10u64);
harness.dispatch(&mut handler, 5u64);

assert_eq!(*harness.world().resource::<u64>(), 15);
```

`TestTimerDriver` (feature: `timer`) wraps `TimerPoller` with virtual time
control — `advance(duration)`, `set_now(instant)`, `poll(world)` — for
deterministic timer testing without wall-clock waits.

### ByRef / Cloned / Owned — event-type adapters

Adapters bridge between owned and reference event types:

- `ByRef<H>` — wraps `Handler<&E>` to implement `Handler<E>` (borrow before dispatch)
- `Cloned<H>` — wraps `Handler<E>` to implement `Handler<&E>` (clone before dispatch)
- `Owned<H, E>` — wraps `Handler<E::Owned>` to implement `Handler<&E>` via `ToOwned`

Primary use: including owned-event handlers in reference-based contexts
(`FanOut`, `Broadcast`), or vice versa.

```rust
use nexus_rt::{Cloned, Owned, fan_out, IntoHandler, Handler};

// Handler expects owned u32
fn process(mut n: ResMut<u64>, event: u32) { *n += event as u64; }

// Adapt for &u32 context (FanOut dispatches by reference)
let h = process.into_handler(registry);
let adapted = Cloned(h);  // now implements Handler<&u32>

// For &str → String:
fn append(mut buf: ResMut<String>, event: String) { buf.push_str(&event); }
let h = append.into_handler(registry);
let adapted = Owned::<_, str>::new(h);  // implements Handler<&str>
```

`Adapt<F, H>` is a separate adapter for wire-format decoding: `F: FnMut(Wire) -> Option<T>`
filters and transforms before dispatching to `Handler<T>`.

## When to Use What

| Situation | Use | Why |
|-----------|-----|-----|
| One-time setup, test harness | `IntoHandler` / `IntoCallback` | Simple, direct. Construction cost paid once. |
| Pipeline steps inside a driver | `Pipeline` / `BatchPipeline` | Zero-cost monomorphized chains, typed flow control. |
| IO re-registration (accept, echo) | `HandlerTemplate` / `CallbackTemplate` | Handler recreated every event — template eliminates per-event HashMap lookups. |
| Timer rescheduling | `HandlerTemplate` / `CallbackTemplate` | Same pattern — recurring handlers should not pay construction cost repeatedly. |
| Type-erased handler storage | `Box<dyn Handler<E>>` / `Virtual<E>` | When you need heterogeneous collections (driver slabs, timer wheels). |
| Per-instance private state | `Callback` (via `IntoCallback`) or `CallbackTemplate` | Context-owning handlers for connection state, timer metadata, etc. |
| Composable resource registration | `Plugin` | Fire-and-forget, consumed by `WorldBuilder`. |
| Fan-out with merge | `DagStart` → `Dag` | Monomorphized data-flow graph. Zero vtable, all stack locals. |
| Static fan-out (known count) | `FanOut` / `fan_out!` | Dispatch `&E` to N handlers. Zero allocation, concrete types. |
| Dynamic fan-out (runtime count) | `Broadcast` | `Vec<Box<dyn RefHandler>>`. One heap alloc per handler, zero clones. |

**Rule of thumb:** If a handler is created once, use `IntoHandler`. If
it's created repeatedly on every event (move-out-fire pattern), use a
template. For data that must fan out and merge back, use `DagStart`.
For fire-and-forget fan-out, use `FanOut` (static) or `Broadcast`
(dynamic).

## Practical Guidance

### Boxing recommendation

Pipeline, DAG, and composed handler types are fully monomorphized — the
concrete types are deeply nested generics, often unnameable, and can be
very large. **Strongly recommend `Box<dyn Handler<E>>` (or `Virtual<E>`)
for storage.**

The cost is a single vtable dispatch at the handler boundary. All internal
dispatch within the handler/pipeline/DAG remains zero-cost monomorphized.
One vtable call amortized over many internal steps is the design:

```rust
// Concrete type is unnameable — box it
let handler: Box<dyn Handler<Order>> = Box::new(
    DagStart::<Order>::new()
        .root(decode, reg)
        .fork()
        .arm(|a| a.then(process, reg))
        .arm(|b| b.then(log, reg))
        .merge(combine, reg)
        .build()
);
```

### Named functions vs closures

Arity-0 closures work in Pipeline and DAG steps. Arity-1+ (with `Param`
arguments) requires named functions. This is a feature, not a limitation:

- Named functions are **testable** in isolation
- Named functions are **inspectable** (handler `.name()` returns the function path)
- Named functions are **reusable** across pipelines

For cases where you need `&mut World` access in a closure (e.g. dynamic
resource lookup), pass a `|world: &mut World, input| { ... }` closure —
it resolves via the `Opaque` marker with no `Param` overhead. The same
pattern works for `OpaqueHandler` (closures as `Handler<E>`).

Keep step functions small and focused — one function per transformation.

### Pipeline vs DAG

| | Pipeline | DAG |
|---|----------|-----|
| Topology | Linear chain | Fan-out / merge |
| Value flow | By value (move) | By reference within arms |
| Clone needed | No | No (shared `&T`) |
| Use when | Steps are sequential | Data needs to go to multiple places |

Both compose into `Handler<E>` via `.build()`. Use Pipeline for the common
case; reach for DAG when you need `.fork()`.

## Performance

All measurements in CPU cycles, pinned to a single core with turbo
boost disabled.

### Dispatch (hot path)

| Operation | p50 | p99 | p999 |
|-----------|-----|-----|------|
| Baseline hand-written fn | 2 | 3 | 4 |
| 3-stage pipeline (bare) | 2 | 2 | 4 |
| 3-stage pipeline (Res\<T\>) | 2 | 3 | 5 |
| Handler + Res\<T\> (read) | 2 | 4 | 5 |
| Handler + ResMut\<T\> (write) | 3 | 8 | 8 |
| Box\<dyn Handler\> | 2 | 9 | 9 |

Pipeline dispatch matches hand-written code — zero-cost abstraction
confirmed.

### Batch throughput

Total cycles for 100 items through the same pipeline chain.

| Operation | p50 | p99 | p999 |
|-----------|-----|-----|------|
| Batch bare (100 items) | 130 | 264 | 534 |
| Linear bare (100 calls) | 196 | 512 | 528 |
| Batch Res\<T\> (100 items) | 390 | 466 | 612 |
| Linear Res\<T\> (100 calls) | 406 | 550 | 720 |

Batch dispatch amortizes to ~1.3 cycles/item for compute-heavy chains
(~1.5x faster than individual calls).

### Construction (cold path)

| Operation | p50 | p99 | p999 |
|-----------|-----|-----|------|
| into_handler (1 param) | 21 | 30 | 79 |
| into_handler (4 params) | 45 | 86 | 147 |
| into_handler (8 params) | 93 | 156 | 221 |
| .then() (2 params) | 28 | 48 | 96 |

Construction cost is paid once at build time, never on the dispatch
hot path.

### Template generation (hot path handler creation)

| Operation | p50 | p99 | p999 |
|-----------|-----|-----|------|
| generate (1 param) | 1 | 1 | 2 |
| generate (2 params) | 1 | 1 | 2 |
| generate (4 params) | 1 | 1 | 1 |
| generate (8 params) | 1 | 1 | 1 |
| generate callback (2 params) | 1 | 2 | 2 |
| generate callback (4 params) | 1 | 1 | 1 |

`generate()` copies pre-resolved `ResourceId` values — a flat memcpy
at every arity. Compare with `into_handler` above: 24-70x faster for
handlers created on every event (IO re-registration, timer rescheduling).

### Running benchmarks

```bash
taskset -c 0 cargo run --release -p nexus-rt --example perf_pipeline
taskset -c 0 cargo run --release -p nexus-rt --example perf_construction
taskset -c 0 cargo run --release -p nexus-rt --example perf_template
taskset -c 0 cargo run --release -p nexus-rt --example perf_dag
taskset -c 0 cargo run --release -p nexus-rt --example perf_scheduler
taskset -c 0 cargo run --release -p nexus-rt --example mio_timer --features mio,timer
```

### DAG dispatch (hot path)

| Operation | p50 | p99 | p999 |
|-----------|-----|-----|------|
| DAG linear 3 stages | 1 | 2 | 3 |
| DAG linear 5 stages | 1 | 2 | 3 |
| DAG diamond fan=2 (5 stages) | 1 | 3 | 5 |
| DAG fan-out 2 (join) | 2 | 6 | 9 |
| DAG complex (fan+linear+merge) | 1 | 4 | 5 |
| DAG complex+Res\<T\> (Param fetch) | 3 | 3 | 5 |
| DAG linear 3 via Box\<dyn Handler\> | 1 | 4 | 4 |
| DAG diamond-2 via Box\<dyn Handler\> | 2 | 2 | 5 |

DAG dispatch matches Pipeline dispatch — topology adds no measurable
overhead. Boxing adds ~1 cycle at the boundary.

### Scheduler dispatch

| Operation | p50 | p99 | p999 |
|-----------|-----|-----|------|
| Flat 1 system | 11 | 20 | 48 |
| Flat 4 systems | 25 | 41 | 82 |
| Flat 8 systems | 43 | 67 | 124 |
| Chain 4 systems (all propagate) | 25 | 42 | 84 |
| Chain 8 systems (all propagate) | 44 | 73 | 124 |
| Diamond fan=4 (6 systems) | 35 | 53 | 93 |
| Skipped chain 8 (1 runs, 7 skip) | 17 | 28 | 68 |
| Skipped chain 32 (1 runs, 31 skip) | 46 | 76 | 118 |

Scheduler overhead is ~8-12 cycles per system. Skipped systems
(upstream returned `false`) cost ~2 cycles each (bitmask check).

## Limitations

### Named functions only

`IntoHandler`, `IntoCallback`, and `IntoStep` (arity 1+) require named
`fn` items — closures do not work due to Rust's HRTB inference limitations
with GATs. This is the same limitation as Bevy's system registration.

Arity-0 pipeline steps (no `Param`) do accept closures:
```rust
// Works — arity-0 closure
pipeline.then(|x: u32| x * 2, registry);

// Does NOT work — arity-1 closure with Param
// pipeline.then(|config: Res<Config>, x: u32| x, registry);

// Works — named function
fn transform(config: Res<Config>, x: u32) -> u32 { x + *config as u32 }
pipeline.then(transform, registry);
```

### Single-threaded

`World` is `!Sync` by design. All dispatch is single-threaded, sequential.
This is intentional — for latency-sensitive event processing, eliminating
coordination overhead matters more than parallelism.

### Frozen after build

No resources can be added or removed after `WorldBuilder::build()`. All
registration happens at build time. This enables stable pointers and
eliminates runtime bookkeeping.

## Examples

- [`mock_runtime`](examples/mock_runtime.rs) — Complete driver model:
  plugin registration, driver installation, explicit poll loop
- [`pipeline`](examples/pipeline.rs) — Pipeline composition: bare value,
  Option, Result with catch, combinators, build into Handler
- [`dag`](examples/dag.rs) — DAG pipeline: linear, diamond, fan-out,
  route, tap, tee, dedup, guard, boxing
- [`scheduler_dag`](examples/scheduler_dag.rs) — DAG scheduler:
  reconciliation systems, boolean propagation, change detection
- [`handlers`](examples/handlers.rs) — Handler composition: IntoHandler,
  Callback, boxing, FanOut, Broadcast, adapters
- [`change_detection`](examples/change_detection.rs) — Change detection:
  is_changed, changed_after, ResMut stamping, SchedulerTick
- [`templates`](examples/templates.rs) — Template generation:
  HandlerTemplate, CallbackTemplate, handler_blueprint macro
- [`testing_example`](examples/testing_example.rs) — TestHarness usage
  for isolated handler unit testing
- [`local_state`](examples/local_state.rs) — Per-handler state with
  `Local<T>`, independent across handler instances
- [`optional_resources`](examples/optional_resources.rs) — Optional
  dependencies with `Option<Res<T>>` / `Option<ResMut<T>>`
- [`perf_pipeline`](examples/perf_pipeline.rs) — Dispatch latency
  benchmarks with codegen inspection probes
- [`perf_dag`](examples/perf_dag.rs) — DAG dispatch latency benchmarks
  across topologies
- [`perf_scheduler`](examples/perf_scheduler.rs) — Scheduler dispatch
  latency benchmarks
- [`perf_construction`](examples/perf_construction.rs) — Construction-time
  latency benchmarks at various arities
- [`perf_template`](examples/perf_template.rs) — Template generation
  vs `into_handler` construction benchmarks
- [`perf_fetch`](examples/perf_fetch.rs) — Fetch dispatch strategy
  benchmarks
- [`mio_timer`](examples/mio_timer.rs) — Echo server combining mio
  and timer drivers with template construction benchmarks

## License

See workspace root for license details.
