@statewalker/webrun-streams 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022-2026 statewalker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # @statewalker/webrun-streams
2
+
3
+ Async-iterator and `ReadableStream` primitives: `collect` / `collectBytes` / `collectString`, text and JSONL codecs, line splitting/joining, a backpressure-aware queue-based generator, a chunk protocol for pushing iterators across transports, conversions between async iterators and WHATWG `ReadableStream<Uint8Array>`, and serialisable `Error` objects.
4
+
5
+ ## Why it exists
6
+
7
+ Every higher-level package in the `webrun-*` family (and its consumers — scanners, indexers, chat pipelines) needs the same small set of building blocks:
8
+
9
+ 1. **Collectors** — turn any async iterable into a concrete array / `Uint8Array` / `string` without boilerplate; zero-copy short-circuit when a single chunk is produced.
10
+ 2. A **callback-to-async-iterator** bridge — turn incoming `{done, value, error}` callbacks into a `for await` loop, with backpressure so producers know when consumers have stopped listening.
11
+ 3. A **chunk protocol** — a tiny `{done, value?, error?}` envelope that can travel across any transport (MessagePort, WebSocket, IPC, in-memory) and rebuild the original iterator on the other side.
12
+ 4. **WHATWG ↔ async-iterator** conversions for body bytes, so code written against `fetch` (`ReadableStream<Uint8Array>`) can interoperate with `for await` code and back.
13
+ 5. **Error (de)serialisation** for passing exceptions across structured-clone / JSON boundaries without losing stacks or extra fields.
14
+ 6. **Line / JSONL / text codecs** so stream-processing code doesn't re-invent split/join/encode/decode in every consumer.
15
+
16
+ The MessagePack codec that previously rode along here is split out to [`@statewalker/webrun-msgpack`](../webrun-msgpack) so consumers that don't need framing don't pull in `@ygoe/msgpack`.
17
+
18
+ ## How to use
19
+
20
+ ```sh
21
+ npm install @statewalker/webrun-streams
22
+ ```
23
+
24
+ | Export | Purpose |
25
+ | --- | --- |
26
+ | `collect(it)` | Drain `AsyncIterable<T>` into `T[]`. |
27
+ | `collectBytes(it)` | Concatenate `AsyncIterable<Uint8Array>` into one `Uint8Array` (zero-copy when a single chunk). |
28
+ | `collectString(it)` | Concatenate `AsyncIterable<string>` into one `string`. |
29
+ | `encodeText(it)` / `decodeText(it)` | UTF-8 `AsyncIterable<string>` ↔ `AsyncIterable<Uint8Array>`. |
30
+ | `splitLines(it)` / `joinLines(it)` | Line splitting over `string` streams (handles cross-chunk lines) and reverse. |
31
+ | `encodeJsonl(it)` / `decodeJsonl(it)` | JSON values ↔ `\n`-delimited JSON string stream. |
32
+ | `map(it, fn)` | Stream-map an `AsyncIterable<T>` through `fn: T => U \| Promise<U>`. |
33
+ | `newAsyncGenerator(init, skipValues?)` | Bridge imperative `next/done` callbacks into an `AsyncGenerator<T>`; returns `Promise<boolean>` for backpressure. |
34
+ | `sendIterator(send, iterable)` | Drain an (async) iterable into `send({done, value, error})` chunk calls; completes with one trailing `{done: true}` chunk. |
35
+ | `recieveIterator(installer)` | Inverse of `sendIterator`: wire an installer's chunk callback into a new `AsyncGenerator<T>`. |
36
+ | `toReadableStream(it)` | Wrap an `AsyncIterator<Uint8Array>` in a `ReadableStream<Uint8Array>`. |
37
+ | `fromReadableStream(stream)` | Iterate a `ReadableStream<Uint8Array>` as `AsyncGenerator<Uint8Array>`. |
38
+ | `serializeError(error)` | Turn an `Error` (or anything) into a plain `{message, stack, …}` object preserving subclass fields. |
39
+ | `deserializeError(obj \| string)` | Reconstruct an `Error` from a serialised form, restoring extra fields. |
40
+
41
+ ## Examples
42
+
43
+ ### Collectors
44
+
45
+ ```ts
46
+ import { collect, collectBytes, collectString } from "@statewalker/webrun-streams";
47
+
48
+ async function* numbers() { yield 1; yield 2; yield 3; }
49
+ await collect(numbers()); // [1, 2, 3]
50
+
51
+ async function* bytes() {
52
+ yield new Uint8Array([1, 2]);
53
+ yield new Uint8Array([3]);
54
+ }
55
+ await collectBytes(bytes()); // Uint8Array(3) [1, 2, 3]
56
+
57
+ async function* strings() { yield "a"; yield "bc"; }
58
+ await collectString(strings()); // "abc"
59
+ ```
60
+
61
+ ### Text / JSONL / lines codecs
62
+
63
+ ```ts
64
+ import {
65
+ decodeJsonl,
66
+ decodeText,
67
+ encodeJsonl,
68
+ encodeText,
69
+ joinLines,
70
+ splitLines,
71
+ } from "@statewalker/webrun-streams";
72
+
73
+ async function* chunks() {
74
+ yield new Uint8Array([0x7b, 0x22, 0x61]); // partial
75
+ yield new Uint8Array([0x22, 0x3a, 0x31, 0x7d, 0x0a]);
76
+ }
77
+
78
+ const values = decodeJsonl<{ a: number }>(splitLines(decodeText(chunks())));
79
+ for await (const v of values) console.log(v); // { a: 1 }
80
+
81
+ // inverse
82
+ const jsonl = encodeText(joinLines(encodeJsonl([{ a: 1 }, { a: 2 }])));
83
+ ```
84
+
85
+ ### Callback → AsyncGenerator bridge
86
+
87
+ ```ts
88
+ import { newAsyncGenerator } from "@statewalker/webrun-streams";
89
+
90
+ function tickEverySecond(): AsyncGenerator<number> {
91
+ return newAsyncGenerator<number>((next, done) => {
92
+ let n = 0;
93
+ const id = setInterval(() => {
94
+ if (n < 5) void next(n++);
95
+ else {
96
+ void done();
97
+ clearInterval(id);
98
+ }
99
+ }, 1000);
100
+ return () => clearInterval(id); // cleanup if consumer breaks early
101
+ });
102
+ }
103
+
104
+ for await (const n of tickEverySecond()) console.log(n); // 0 … 4
105
+ ```
106
+
107
+ ### Iterator chunk protocol
108
+
109
+ ```ts
110
+ import { sendIterator, recieveIterator } from "@statewalker/webrun-streams";
111
+
112
+ // Drain an iterable across any transport.
113
+ async function transport<T>(chunk: { done: boolean; value?: T; error?: unknown }) {
114
+ // …send `chunk` over your channel.
115
+ }
116
+ await sendIterator(transport, [1, 2, 3]);
117
+
118
+ // On the other side, rebuild the original iterator.
119
+ const iter = recieveIterator<number>((deliver) => {
120
+ myChannel.onMessage = (chunk) => deliver(chunk);
121
+ });
122
+ for await (const v of iter) console.log(v); // 1, 2, 3
123
+ ```
124
+
125
+ ### WHATWG streams ↔ async iterators
126
+
127
+ ```ts
128
+ import { fromReadableStream, toReadableStream } from "@statewalker/webrun-streams";
129
+
130
+ async function* encoded() {
131
+ const e = new TextEncoder();
132
+ yield e.encode("hello ");
133
+ yield e.encode("world");
134
+ }
135
+
136
+ // Give an iterable a ReadableStream face for fetch / Response.
137
+ const response = new Response(toReadableStream(encoded()));
138
+
139
+ // …and the other way around.
140
+ const reqBody = new Request("/x", { method: "POST", body: response.body }).body!;
141
+ for await (const chunk of fromReadableStream(reqBody)) {
142
+ // chunk: Uint8Array
143
+ }
144
+ ```
145
+
146
+ ### Error roundtrip
147
+
148
+ ```ts
149
+ import { serializeError, deserializeError } from "@statewalker/webrun-streams";
150
+
151
+ class NotFoundError extends Error {
152
+ status = 404;
153
+ }
154
+
155
+ const wire = serializeError(new NotFoundError("missing"));
156
+ // { message: "missing", stack: "…", status: 404 }
157
+
158
+ const restored = deserializeError(wire) as Error & { status?: number };
159
+ console.log(restored instanceof Error); // true
160
+ console.log(restored.status); // 404
161
+ ```
162
+
163
+ ## Internals
164
+
165
+ ### `newAsyncGenerator` — backpressure queue
166
+
167
+ A singly-linked queue of slots; each slot carries either a value or a
168
+ terminal `{done: true, error?}`. Producers call `next(value)` or
169
+ `done(error?)`, both returning `Promise<boolean>` that resolves once the
170
+ consumer has dequeued the slot — so producers can apply backpressure by
171
+ `await`ing.
172
+
173
+ If the consumer breaks out of the `for await` early, the finally block
174
+ drains remaining slots and resolves each pending `next/done` promise
175
+ with `false`, letting the producer observe that its value wasn't
176
+ consumed and stop. Cleanup function (if the `init` returned one) runs
177
+ on the same exit path.
178
+
179
+ `skipValues: true` switches the queue into latest-only mode: pushing a
180
+ new value drops any unconsumed older ones. Useful for "show the most
181
+ recent state" scenarios (live previews, resizing, etc.) where missing
182
+ values is fine but lagging isn't.
183
+
184
+ ### Chunk protocol
185
+
186
+ One object per message:
187
+
188
+ ```
189
+ { done: false, value: T } — a value
190
+ { done: true, error?: E } — termination (error if present rethrows)
191
+ ```
192
+
193
+ `sendIterator` guarantees exactly one `done` chunk and never throws
194
+ itself — errors from the source iterator end up in the trailing chunk's
195
+ `error` field. `recieveIterator` rethrows them into the `for await`
196
+ loop on the other side.
197
+
198
+ ### `readable-streams`
199
+
200
+ `toReadableStream` uses the default (non-byte) ReadableStream type to
201
+ sidestep the strict `ArrayBuffer`-not-`SharedArrayBuffer` typing the
202
+ byte-controller requires in recent TS libs. Both functions are
203
+ strict one-way converters: no queuing strategy tricks, no transform.
204
+
205
+ ### Design notes
206
+
207
+ - **Zero runtime dependencies.** Only platform builtins
208
+ (`Promise`, `ReadableStream`, `TextEncoder`/`Decoder` if needed,
209
+ `setTimeout` via `newAsyncGenerator` consumers).
210
+ - **British/American spelling kept.** `recieveIterator` uses the
211
+ historical misspelling to stay wire-compatible with `webrun-ports`
212
+ consumers.
213
+ - **No tight coupling to any transport.** Nothing here mentions
214
+ `MessagePort`, `fetch`, `Worker`, etc. Those belong to the consuming
215
+ packages.
216
+
217
+ ### Constraints
218
+
219
+ - `toReadableStream` / `fromReadableStream` assume `Uint8Array` chunks —
220
+ the usual shape for HTTP bodies. Generic byte-agnostic use isn't
221
+ supported.
222
+ - `newAsyncGenerator`'s backpressure Promise resolves with `false` both
223
+ on early break and on skip; consumers can't distinguish the two.
224
+ That's intentional — both mean "wasn't consumed".
225
+
226
+ ### Dependencies
227
+
228
+ **Zero runtime dependencies.**
229
+
230
+ Dev: TypeScript, vitest, tsdown, rimraf, `@types/node`
231
+ (catalog versions from the monorepo root).
232
+
233
+ ## Scripts
234
+
235
+ ```sh
236
+ pnpm test # vitest run
237
+ pnpm run build # tsdown (publishes src + compiled dist)
238
+ pnpm lint # biome check
239
+ ```
240
+
241
+ ## License
242
+
243
+ MIT © statewalker