first commit
This commit is contained in:
151
docs/foundation/flow/streams/src/primitives/duplex/duplex.md
Normal file
151
docs/foundation/flow/streams/src/primitives/duplex/duplex.md
Normal 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.
|
||||
Reference in New Issue
Block a user