3.6 KiB
stream
(no submodules)
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) .... AbortSignalcancellation: aborts both the iterator and posts a cancel frame to the worker.- Pre-aborted signal short-circuits with
WEB_WORKERS.STREAM_ABORTEDand 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
idFactoryfor deterministic ids in tests. - Optional
transferlist for zero-copy initial payload handoff.
Usage: Basic Example
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
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.