first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

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