first commit
This commit is contained in:
115
docs/foundation/flow/streams/src/pipeline/route/route.md
Normal file
115
docs/foundation/flow/streams/src/pipeline/route/route.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# route / routeBy
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`route` and `routeBy` split a single `ReadableStream<T>` into N independent
|
||||
output branches, but unlike `fanOut` each chunk is delivered to **exactly one**
|
||||
branch — the one selected by a caller-supplied `keyFn`. They are the
|
||||
single-pass partitioning primitive of the streams pipeline: a chunk is read
|
||||
from the source once and dispatched, never duplicated.
|
||||
|
||||
This is the idiomatic way to model demultiplexing: a tagged event stream that
|
||||
should be processed differently per kind (writes → DB, audits → log,
|
||||
notifications → fan-out), without paying the buffering/backpressure-coupling
|
||||
cost of `fanOut`. The two ops differ only in container: `route` returns a
|
||||
`Record<K, ReadableStream<T>>` for `string` keys, `routeBy` returns a
|
||||
`Map<K, ReadableStream<T>>` for arbitrary key types (numbers, symbols,
|
||||
objects).
|
||||
|
||||
## Features
|
||||
|
||||
- Single-pass: each source chunk is dispatched to exactly one branch.
|
||||
- Per-key ordering is preserved (FIFO within each branch).
|
||||
- Configurable unknown-key policy: `'drop'` (silent, default) or `'error'`
|
||||
(every branch errors with `StreamRouteUnknownKeyError`).
|
||||
- Source errors propagate to every branch.
|
||||
- `routeBy` accepts non-string keys (number, symbol, object identity).
|
||||
- Zero external dependencies; pure WHATWG Streams.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { route } from "@suaveplan/streams/pipeline/route/route";
|
||||
import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
|
||||
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";
|
||||
|
||||
const events = fromArray([
|
||||
{ kind: "a" as const, n: 1 },
|
||||
{ kind: "b" as const, n: 2 },
|
||||
{ kind: "a" as const, n: 3 },
|
||||
]);
|
||||
|
||||
const branches = route(events, (e) => e.kind, ["a", "b"] as const);
|
||||
const [as, bs] = await Promise.all([
|
||||
toArray(branches.a),
|
||||
toArray(branches.b),
|
||||
]);
|
||||
// as -> [{ kind: "a", n: 1 }, { kind: "a", n: 3 }]
|
||||
// bs -> [{ kind: "b", n: 2 }]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { routeBy } from "@suaveplan/streams/pipeline/route/route";
|
||||
import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
|
||||
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";
|
||||
|
||||
// Symbol-keyed routing with strict unknown-key handling.
|
||||
const PRIORITY_HIGH = Symbol("high");
|
||||
const PRIORITY_LOW = Symbol("low");
|
||||
|
||||
const m = routeBy<{ pri: symbol; payload: string }, symbol>(
|
||||
fromArray([
|
||||
{ pri: PRIORITY_HIGH, payload: "urgent" },
|
||||
{ pri: PRIORITY_LOW, payload: "later" },
|
||||
]),
|
||||
(e) => e.pri,
|
||||
[PRIORITY_HIGH, PRIORITY_LOW],
|
||||
{ onUnknownKey: "error" },
|
||||
);
|
||||
|
||||
const high = m.get(PRIORITY_HIGH)!;
|
||||
const low = m.get(PRIORITY_LOW)!;
|
||||
const [hi, lo] = await Promise.all([toArray(high), toArray(low)]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `route<T, K extends string>(source, keyFn, keys, options?)`
|
||||
|
||||
- `source: ReadableStream<T>` — input stream to dispatch.
|
||||
- `keyFn: (chunk: T) => K` — extracts the routing key from each chunk.
|
||||
- `keys: readonly K[]` — declared key set; one branch is created per key.
|
||||
- `options.onUnknownKey?: 'drop' | 'error'` — default `'drop'`.
|
||||
|
||||
**Returns**: `Record<K, ReadableStream<T>>` — one branch per declared key.
|
||||
|
||||
**Throws** (per branch, asynchronously): `StreamRouteUnknownKeyError`
|
||||
(`STREAM.ROUTE_UNKNOWN_KEY`) when a chunk's key is not in `keys` and
|
||||
`onUnknownKey === 'error'`.
|
||||
|
||||
### `routeBy<T, K>(source, keyFn, keys, options?)`
|
||||
|
||||
Same contract as `route`, but returns `Map<K, ReadableStream<T>>` so the key
|
||||
type is unconstrained (numbers, symbols, objects).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The implementation creates one `ReadableStream<T>` per declared key whose
|
||||
controller is captured during `start()`. A single reader loop pulls from the
|
||||
source on the next microtask after construction and dispatches each chunk to
|
||||
the controller selected by `keyFn`. On unknown key:
|
||||
|
||||
- `'drop'` — the chunk is consumed and discarded.
|
||||
- `'error'` — every branch's controller is errored with
|
||||
`StreamRouteUnknownKeyError`, the source is cancelled, and the dispatch loop
|
||||
terminates.
|
||||
|
||||
Branches use the default WHATWG queuing strategy (CountQueuingStrategy with
|
||||
`highWaterMark: 1`); if a downstream consumer falls far behind, the branch's
|
||||
internal queue grows. Compose with `limit` or `throttle` if an unbounded
|
||||
producer must be guarded.
|
||||
Reference in New Issue
Block a user