Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
# BaseDuplexStream
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<R>` (start / pull / cancel) and `StreamSink<W>` (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<number, number>(
{
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<Message, Message> {
const outgoing: Message[] = [];
let pullController:
| ReadableStreamDefaultController<Message>
| undefined;
const source: StreamSource<Message> = {
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<Message> = {
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<R, W>`
#### `constructor(source: StreamSource<R>, sink: StreamSink<W>, 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<R>`
The readable side, suitable for `pipeThrough` / `pipeTo` composition.
#### `writable: WritableStream<W>`
The writable side. Use `.getWriter()` for direct control or `.pipeTo` from another readable.
### `createDuplexStream<R, W>(source, sink, options?): DuplexStream<R, W>`
Inline factory equivalent to `new BaseDuplexStream(...)`.
### Types
- `StreamSource<R>` — `{ start?, pull?, cancel? }` with WHATWG controller signatures
- `StreamSink<W>` — `{ start?, write?, abort?, close? }` with WHATWG controller signatures
- `DuplexStreamOptions` — `{ objectMode?, highWaterMark? }`
- `DuplexStream<R, W>` — `{ 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.