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,151 @@
# streaming-chunk-queue
## Purpose
`StreamingChunkQueue` is the public class that reconstitutes
out-of-order chunked file transfers into a sequential
`ReadableStream<Uint8Array>`. It composes the package's pluggable
validator, decryptor, persister, rate limiter, hooks, event emitter,
and OTel telemetry into a single coherent reception primitive — the
piece every chunked-file consumer (drive-mesh, drive-webrtc,
drive-transport-https) plugs into when chunks arrive via a jittery or
out-of-order transport.
## Features
- Out-of-order ingestion: chunks may arrive in any order; duplicates
are silently ignored.
- Sequential output: the readable stream emits chunks strictly in
ascending `chunkId` order.
- Auto-start: the first `addChunk` automatically materialises the
stream (configurable via `autoStart: false`).
- Pluggable validator + decryptor strategies with adapters for
`@suaveplan/packer` `ChunkVerifier`.
- Optional `TokenBucketRateLimiterAdapter` integration for bandwidth
throttling without dropping chunks.
- Optional `StorageChunkPersister` for resumable transfers.
- Eight lifecycle hooks, ten event types, and wildcard listener
support.
- Binary chunk-request protocol exposed through `requestChunks` and
`requestMissing`.
- Missing-chunk timeout that errors the readable stream after
`chunkTimeout` ms (default 30 000).
- Frozen status snapshot via `getStatus`.
- OTel counters and a histogram (`stream_duration_ms`).
## Usage: Basic
```ts
import { StreamingChunkQueue } from "@suaveplan/streams-chunk-queue";
const queue = new StreamingChunkQueue({
fileId: 12345n,
totalChunks: 5n,
});
await queue.addChunk(0n, bytes0);
await queue.addChunk(4n, bytes4);
await queue.addChunk(1n, bytes1);
await queue.addChunk(2n, bytes2);
await queue.addChunk(3n, bytes3);
const reader = queue.getStream().getReader();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
process(value);
}
```
## Usage: Advanced
```ts
import { ChunkVerifier } from "@suaveplan/packer";
import { TokenBucket } from "@suaveplan/rate-limiter";
import { MemoryStorage } from "@suaveplan/storage";
import {
ChunkVerifierValidator,
StorageChunkPersister,
StreamingChunkQueue,
TokenBucketRateLimiterAdapter,
} from "@suaveplan/streams-chunk-queue";
const verifier = new ChunkVerifier(serverPubKey);
const bucket = new TokenBucket({ capacity: 1_048_576, refillRate: 1024 });
const storage = new MemoryStorage();
const queue = new StreamingChunkQueue({
fileId: 12345n,
totalChunks: 1024n,
validator: new ChunkVerifierValidator(verifier),
rateLimiter: new TokenBucketRateLimiterAdapter(bucket),
persister: new StorageChunkPersister({ storage, fileId: 12345n }),
chunkTimeout: 60_000,
hooks: {
onStreamProgress: (pct) => statusBar.set(pct),
onError: (err) => toaster.error(err.message),
},
});
queue.addEventListener("*", (event) =>
telemetry.record("chunk_queue.event", { type: event.type }),
);
const stream = queue.getStream();
await stream.pipeTo(diskWriter);
```
## API Reference
### `new StreamingChunkQueue(options)`
`options` is `StreamingChunkQueueOptions`. The constructor throws
`StreamsChunkQueueError(INVALID_TOTAL_CHUNKS)` when `totalChunks <= 0n`.
### `addChunk(chunkId, data)` → `Promise<void>`
Persists, validates, decrypts, and enqueues the chunk. Throws
`INVALID_CHUNK_ID` on out-of-range ids, or
`VALIDATION_FAILED` / `DECRYPTION_FAILED` if the pipeline rejects.
### `getStream()` → `ReadableStream<Uint8Array>`
Idempotent: subsequent calls return the same stream.
### `requestChunks(ids?)` / `requestMissing()` → `Uint8Array`
Encode the binary chunk-request frame.
### `getMissingChunkIds()` → `bigint[]`
Ascending list of un-received chunk ids.
### `getStatus()` → `StreamingQueueStatus`
Frozen snapshot.
### `loadChunk(chunkId)` / `loadAllChunks()` → `Promise<void>`
Hydrate from the configured persister. Throw
`STORAGE_NOT_CONFIGURED` / `STORAGE_CHUNK_MISSING` on failure.
### `isAllChunksReceived()` / `isStreamingComplete()` → `boolean`
### `addEventListener(type, listener)` / `removeEventListener(type, listener)`
`type` is any `StreamEvent['type']` or `'*'`.
### `clear()` → `void`
Reset internal state for reuse.
## Implementation Notes
The validator and decryptor stages are extracted to `pipeline.ts` so
the queue file stays close to the per-file line budget. Storage guards
live in `storage-ops.ts` for the same reason. Rate-limiting denial is
non-destructive — the queue sleeps for the bucket's `retryAfter` (or
the adapter's fallback delay) and retries the same chunk; no bytes are
ever dropped. Missing-chunk timeout polls the in-memory map at a
configurable interval (default 100 ms) so deterministic tests can
inject a custom `clock` + `sleep`.