first commit
This commit is contained in:
107
docs/foundation/data/web-workers/src/streaming/stream.md
Normal file
107
docs/foundation/data/web-workers/src/streaming/stream.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# `stream`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`stream.ts` exposes `sendStream`, a streaming RPC primitive that returns
|
||||
an `AsyncIterable<TChunk>` driven by frames the worker pushes back to the
|
||||
main thread. It complements the `sendRequest` request/response API by
|
||||
letting workers stream partial results — useful for progressive renders,
|
||||
chunked compression, log tails, or any long-running operation that
|
||||
produces incremental output.
|
||||
|
||||
The protocol is intentionally minimal: the consumer posts an initial
|
||||
envelope `{ streamId, payload }` to the worker; the worker pushes
|
||||
`{ id: streamId, payload: { chunk } }` frames, and signals completion
|
||||
with `{ id: streamId, payload: { done: true } }`. Aborting the consumer's
|
||||
signal posts a `stream:cancel` frame to the worker and throws
|
||||
`WEB_WORKERS.STREAM_ABORTED`.
|
||||
|
||||
## Features
|
||||
|
||||
- Async-iterable interface: `for await (const chunk of stream) ...`.
|
||||
- `AbortSignal` cancellation: aborts both the iterator and posts a cancel
|
||||
frame to the worker.
|
||||
- Pre-aborted signal short-circuits with `WEB_WORKERS.STREAM_ABORTED` and
|
||||
no initial message is posted.
|
||||
- Frame correlation by stream id: foreign-worker frames and foreign-id
|
||||
frames are ignored, so multiple concurrent streams over the same
|
||||
manager coexist cleanly.
|
||||
- Optional `idFactory` for deterministic ids in tests.
|
||||
- Optional `transfer` list for zero-copy initial payload handoff.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { sendStream, WorkerManager } from "@suaveplan/web-workers";
|
||||
|
||||
const manager = new WorkerManager({ factory });
|
||||
manager.create({ name: "tail", url: WORKER_URL });
|
||||
|
||||
for await (const line of sendStream<string, string>(
|
||||
manager,
|
||||
"tail",
|
||||
"tail-log",
|
||||
"/var/log/app.log"
|
||||
)) {
|
||||
appendLine(line);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { sendStream, WEB_WORKERS_CODES } from "@suaveplan/web-workers";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const buf = new ArrayBuffer(8 * 1024 * 1024);
|
||||
const stream = sendStream<ArrayBuffer, ArrayBuffer>(
|
||||
manager,
|
||||
"compress",
|
||||
"lz4-stream",
|
||||
buf,
|
||||
{ signal: ctrl.signal, transfer: [buf] }
|
||||
);
|
||||
|
||||
const chunks: ArrayBuffer[] = [];
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
if (cancelRequested) ctrl.abort();
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
(err as { code?: string }).code === WEB_WORKERS_CODES.STREAM_ABORTED
|
||||
) {
|
||||
// expected when ctrl.abort() fires
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `sendStream<TReq, TChunk>(manager, name, type, payload, options?): AsyncIterable<TChunk>`.
|
||||
- `interface StreamCapableManager { events; postMessage<T>(name, type, payload, options?) }`.
|
||||
- `interface StreamRequestEnvelope<TReq> { streamId: string; payload: TReq }`.
|
||||
- `type StreamIdFactory = () => string`.
|
||||
- `interface SendStreamOptions { signal?: AbortSignal; transfer?: Transferable[]; idFactory?: StreamIdFactory }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The iterator uses an internal queue + `resolveNext` continuation pattern:
|
||||
every received frame pushes onto the queue and calls `resolveNext` if the
|
||||
consumer was waiting; the consumer's next-iteration loop drains the queue
|
||||
and then awaits on a fresh promise. This avoids buffering the whole stream
|
||||
in memory and gives the consumer natural backpressure (the consumer pulls).
|
||||
The initial payload is wrapped in a `StreamRequestEnvelope` so the worker
|
||||
sees both the `streamId` and the original payload — without that wrapper
|
||||
the worker would have no way to correlate frame ids to incoming requests.
|
||||
The cancel frame `stream:cancel` is sent via the same `postMessage` channel
|
||||
so the worker side can stop producing without needing a separate control
|
||||
channel.
|
||||
Reference in New Issue
Block a user