first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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.