# BaseDuplexStream _(no submodules)_ ## Purpose `BaseDuplexStream` is the primitive for bidirectional WHATWG streams. It bundles a `ReadableStream` and a `WritableStream` whose `source` and `sink` are supplied by the caller, with sensible queuing-strategy defaults for byte vs. object mode. It exists as the foundation for higher-level primitives (`NetworkStreamImpl` for WebRTC/WebSocket peers, future `WorkerStream` for transferable-buffer transport, etc.) and as a clean boundary for any consumer that needs "write to one end, read from the other" semantics without re-implementing the WHATWG plumbing. The two sides are independent by design — a write to `writable` does NOT echo back on `readable` unless the supplied `source` is wired to do so. This decoupling is what makes the primitive reusable for echoing flows (test doubles), proxy flows (network bridges), and asymmetric flows (encode-on-write, decode-on-read). ## Features - WHATWG-native `ReadableStream` + `WritableStream` exposed as `readable` / `writable` properties - Caller-supplied `StreamSource` (start / pull / cancel) and `StreamSink` (start / write / abort / close) - Default queuing strategy chooses between count-based (`objectMode: true`) and byte-size based (default) automatically - `highWaterMark` is forwarded to both sides - `cancel(reason)` on `readable` propagates `reason` to the source's `cancel` callback verbatim - `abort(reason)` on `writable` propagates to the sink's `abort` callback - All four sink hooks (`start`, `write`, `abort`, `close`) and three source hooks (`start`, `pull`, `cancel`) are optional — callers supply only what they need - `createDuplexStream(source, sink, options?)` factory for inline usage ## Usage: Basic Example ```ts import { createDuplexStream } from "@suaveplan/streams/primitives/duplex/duplex"; let pending: number | null = null; const stream = createDuplexStream( { pull(controller) { if (pending !== null) { controller.enqueue(pending); pending = null; } }, }, { write(chunk) { // Echo the doubled value back to the readable side pending = chunk * 2; }, } ); const writer = stream.writable.getWriter(); await writer.write(7); await writer.close(); const reader = stream.readable.getReader(); console.log((await reader.read()).value); // 14 ``` ## Usage: Advanced Example ```ts import { BaseDuplexStream } from "@suaveplan/streams/primitives/duplex/duplex"; import type { StreamSink, StreamSource, DuplexStreamOptions, } from "@suaveplan/streams/primitives/duplex/duplex.types"; interface Message { seq: number; payload: Uint8Array; } function createPeerStream( onIncoming: (m: Message) => void ): BaseDuplexStream { const outgoing: Message[] = []; let pullController: | ReadableStreamDefaultController | undefined; const source: StreamSource = { start(controller) { pullController = controller; }, pull(controller) { const next = outgoing.shift(); if (next !== undefined) controller.enqueue(next); }, cancel(reason) { outgoing.length = 0; pullController = undefined; console.warn("peer stream cancelled", reason); }, }; const sink: StreamSink = { write(chunk) { onIncoming(chunk); }, close() { pullController?.close(); }, abort(reason) { pullController?.error(reason); }, }; const options: DuplexStreamOptions = { objectMode: true, highWaterMark: 32, }; return new BaseDuplexStream(source, sink, options); } ``` ## API Reference ### `BaseDuplexStream` #### `constructor(source: StreamSource, sink: StreamSink, options?: DuplexStreamOptions)` Builds the bidirectional stream. Every callback on `source` and `sink` is optional. - `options.objectMode` — `true` selects count-based queuing; `false` (default) selects byte-size queuing where each chunk's `byteLength` (or 1 if absent) counts toward `highWaterMark`. - `options.highWaterMark` — applied to both sides when set; left to WHATWG defaults when omitted. #### `readable: ReadableStream` The readable side, suitable for `pipeThrough` / `pipeTo` composition. #### `writable: WritableStream` The writable side. Use `.getWriter()` for direct control or `.pipeTo` from another readable. ### `createDuplexStream(source, sink, options?): DuplexStream` Inline factory equivalent to `new BaseDuplexStream(...)`. ### Types - `StreamSource` — `{ start?, pull?, cancel? }` with WHATWG controller signatures - `StreamSink` — `{ start?, write?, abort?, close? }` with WHATWG controller signatures - `DuplexStreamOptions` — `{ objectMode?, highWaterMark? }` - `DuplexStream` — `{ readonly readable, readonly writable }` interface ## Implementation Notes The constructor builds the `UnderlyingSource` and `UnderlyingSink` shapes by **conditionally** copying only the callbacks that exist on the caller-supplied objects. This avoids the WHATWG quirk where supplying `start: undefined` is materially different from omitting `start`, and it keeps the underlying-source object minimal so spec-compliant runtimes can apply their fast-path optimisations. The `size` function lives outside the conditional copy because it's strategy-level, not source-level: a `byteLength`-aware function is installed when `objectMode` is false, and a default count-based strategy is installed when it is true. The chunk-size lookup uses optional chaining and falls back to 1 for non-byte chunks (`{ byteLength?: number } ?? 1`), which is the same approach used by `node:stream` and the WHATWG reference implementation. `createDuplexStream` is a thin alias rather than a method on the class because the JS-style "create-some-stream" naming reads more idiomatically inside pipelines than `new BaseDuplexStream(...)`. The two have identical behaviour and runtime cost. This primitive is intentionally minimal — it does not own a benchmark suite, an abort surface beyond the standard WHATWG plumbing, or an event surface. Higher-level primitives (`NetworkStreamImpl`) layer those concerns on top via composition.