@zakkster/lite-stream 1.0.0 → 1.1.1
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/CHANGELOG.md +155 -0
- package/README.md +119 -63
- package/ROADMAP.md +679 -159
- package/Stream.js +316 -80
- package/llms.txt +101 -18
- package/package.json +3 -3
package/llms.txt
CHANGED
|
@@ -14,7 +14,7 @@ Peer dep: `@zakkster/lite-signal ^1.2.0`.
|
|
|
14
14
|
|
|
15
15
|
## Imports
|
|
16
16
|
|
|
17
|
-
import { fromAsyncIterable, pipeToSignal, toAsyncIterable } from "@zakkster/lite-stream";
|
|
17
|
+
import { fromAsyncIterable, pipeToSignal, toAsyncIterable, TimeoutError } from "@zakkster/lite-stream";
|
|
18
18
|
|
|
19
19
|
## Exports
|
|
20
20
|
|
|
@@ -61,25 +61,64 @@ const stop = pipeToSignal(source, targetSig, {
|
|
|
61
61
|
});
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Does NOT dispose the target signal. The caller owns its lifetime.
|
|
64
|
+
Does NOT dispose the target signal. The caller owns its lifetime. Disposing
|
|
65
|
+
the target signal does NOT stop the pump: call the returned stop fn or abort
|
|
66
|
+
opts.signal.
|
|
65
67
|
|
|
66
|
-
### `toAsyncIterable(sig, opts?) -> AsyncIterable<T> & { droppedCount }`
|
|
68
|
+
### `toAsyncIterable(sig, opts?) -> AsyncIterable<T> & { droppedCount, overflowCount }`
|
|
67
69
|
|
|
68
|
-
Reverse direction: yield signal changes as an async iterable.
|
|
69
|
-
|
|
70
|
+
Reverse direction: yield signal changes as an async iterable.
|
|
71
|
+
|
|
72
|
+
**1.0.0 shape (still the default):** bounded FIFO ring buffer, drop-oldest
|
|
73
|
+
overflow, `droppedCount` observable.
|
|
74
|
+
|
|
75
|
+
**1.1 additions (all opt-in, no default changes):**
|
|
76
|
+
- `mode: "latest"` -- single-slot overwrite semantics; use for reactive
|
|
77
|
+
state / "current frame" style consumers
|
|
78
|
+
- `filter` -- per-value gate; throwing filter rejects pending next() and
|
|
79
|
+
terminates (no writer surface)
|
|
80
|
+
- `timeout` -- overall deadline; pending next() rejects with `TimeoutError`
|
|
81
|
+
- `Symbol.asyncDispose` on Node 20+ -- enables `await using iter = ...`
|
|
82
|
+
- `overflowCount` -- alias for `droppedCount`, mode-neutral vocabulary
|
|
83
|
+
- Multi-waiter queue -- concurrent .next() calls resolve in FIFO order
|
|
84
|
+
(1.0.0 silently overwrote the first resolver)
|
|
70
85
|
|
|
71
86
|
```js
|
|
87
|
+
// 1.0.0-style: bounded FIFO buffer (default)
|
|
72
88
|
for await (const v of toAsyncIterable(sig, {
|
|
73
89
|
signal: abortCtrl.signal,
|
|
74
|
-
emitInitial: true,
|
|
75
|
-
maxBuffer: 1024
|
|
90
|
+
emitInitial: true, // default true
|
|
91
|
+
maxBuffer: 1024 // default 1024
|
|
92
|
+
})) {
|
|
93
|
+
// ...
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 1.1: latest-wins slot for reactive state
|
|
97
|
+
for await (const v of toAsyncIterable(sig, {
|
|
98
|
+
mode: "latest", // opt in to single-slot overwrite
|
|
99
|
+
filter: (v) => v.priority > 0,
|
|
100
|
+
timeout: 5000 // overall deadline
|
|
76
101
|
})) {
|
|
77
102
|
// ...
|
|
78
103
|
}
|
|
79
104
|
```
|
|
80
105
|
|
|
81
|
-
|
|
82
|
-
`
|
|
106
|
+
Constraints:
|
|
107
|
+
- `mode: "latest"` + `maxBuffer` throws TypeError (single slot has no buffer size)
|
|
108
|
+
- Negative or non-finite `timeout` throws RangeError
|
|
109
|
+
- Non-function `filter` throws TypeError
|
|
110
|
+
|
|
111
|
+
The iterator naturally completes when `opts.signal` aborts (resolves as
|
|
112
|
+
done, not rejection -- graceful termination). Consumer-side `break`
|
|
113
|
+
triggers `iterator.return()` and cleans up. `timeout` elapsing rejects
|
|
114
|
+
the pending next() with `TimeoutError`; subsequent calls return done.
|
|
115
|
+
|
|
116
|
+
### `TimeoutError` (added 1.1)
|
|
117
|
+
|
|
118
|
+
Thrown when the `timeout` option on `toAsyncIterable` elapses.
|
|
119
|
+
Structurally identical to `@zakkster/lite-await`'s TimeoutError so
|
|
120
|
+
`e.name === "TimeoutError"` duck-checks work across both packages. Not
|
|
121
|
+
imported from lite-await -- lite-stream stays zero-dep.
|
|
83
122
|
|
|
84
123
|
## Cleanup termination triplet
|
|
85
124
|
|
|
@@ -92,9 +131,13 @@ The iterator naturally completes when `opts.signal` aborts. Consumer-side
|
|
|
92
131
|
The abort listener is always removed on any of the three paths -- no
|
|
93
132
|
AbortSignal accumulates dangling listeners.
|
|
94
133
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
134
|
+
Disposing the result signal does NOT stop the pump: the iterator keeps
|
|
135
|
+
pulling. AbortSignal or natural completion are the only stop mechanisms.
|
|
136
|
+
`iterator.return()` is never called and neither `onError` nor `onDone` ever
|
|
137
|
+
fire, because lite-signal's `set()` after `dispose()` is a silent no-op (in
|
|
138
|
+
1.2.2 and 1.5.0), so the pump never learns of the disposal. For an infinite
|
|
139
|
+
source (SSE, a live feed) this turns the stream into an unbounded background
|
|
140
|
+
loop. Abort `opts.signal` to stop early. See ROADMAP LS-01.
|
|
98
141
|
|
|
99
142
|
## Key invariants
|
|
100
143
|
|
|
@@ -188,18 +231,58 @@ states: {
|
|
|
188
231
|
- Not a replacement for lite-clock for frame-rate loops.
|
|
189
232
|
- Not for one-shot promises -- use lite-await's `fromPromise`.
|
|
190
233
|
|
|
234
|
+
## Anti-patterns
|
|
235
|
+
|
|
236
|
+
- DO NOT combine `mode: "latest"` with `maxBuffer` on `toAsyncIterable`.
|
|
237
|
+
It throws TypeError at construction -- a single-slot mode with a buffer
|
|
238
|
+
size is user error. Pick one or the other.
|
|
239
|
+
- DO NOT reach for lite-stream for one-shot Signal<->Promise coordination.
|
|
240
|
+
That is `@zakkster/lite-await`'s job (`whenSignal`, `fromPromise`,
|
|
241
|
+
`allOf`/`anyOf`/`raceOf`). lite-stream owns the AsyncIterable boundary;
|
|
242
|
+
lite-await owns the Promise boundary.
|
|
243
|
+
- DO NOT wrap `toAsyncIterable` with an async-generator debounce / throttle
|
|
244
|
+
for reactive-state use cases. Use `mode: "latest"` directly -- it
|
|
245
|
+
already gives you "consume newest, discard intermediates" semantics with
|
|
246
|
+
zero per-value allocation.
|
|
247
|
+
- DO NOT rely on the value passed to `iter.return(value)` reaching pending
|
|
248
|
+
waiters. Pending `next()` calls resolve with `{value: undefined, done:
|
|
249
|
+
true}` per the async iteration protocol; only `return()`'s own returned
|
|
250
|
+
Promise carries the value.
|
|
251
|
+
- DO NOT dispose the source signal while an iterable is still consuming
|
|
252
|
+
from it. Call `iter.return()` first. A pending `next()` at the moment of
|
|
253
|
+
disposal never settles -- it neither resolves nor rejects; the only escape
|
|
254
|
+
hatch is the `timeout` option, which rejects the pending `next()` with
|
|
255
|
+
`TimeoutError` once its deadline elapses. See ROADMAP LS-04.
|
|
256
|
+
|
|
191
257
|
## Performance
|
|
192
258
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
259
|
+
Measured 2026-09-01 on node v26.3.1 (macOS arm64, laptop class), 150ms
|
|
260
|
+
warmup / 500ms runs, via npm run bench. Your numbers will differ -- re-run
|
|
261
|
+
npm run bench on your target.
|
|
262
|
+
|
|
263
|
+
The six scenarios `bench/bench.mjs` actually runs (ops/s, retained B/op):
|
|
264
|
+
|
|
265
|
+
- `from-latest-10`: 68,156 ops/s, 0.3218 B/op
|
|
266
|
+
- `from-buffer-20-drop`: 61,960 ops/s, 0.4715 B/op
|
|
267
|
+
- `abort-cycle`: 29,078 ops/s, 1.5132 B/op
|
|
268
|
+
- `pipe-to-signal`: 50,098 ops/s, 0.0853 B/op
|
|
269
|
+
- `to-async-iterable`: 198,948 ops/s, -8.4923 B/op
|
|
270
|
+
- `to-async-iterable-overflow`: 208,790 ops/s, 0.1013 B/op
|
|
271
|
+
|
|
272
|
+
The negative B/op on `to-async-iterable` is a GC-timing artifact (the
|
|
273
|
+
post-run gc() reclaims more than the pre-run baseline held); it is not a
|
|
274
|
+
zero-alloc guarantee. Per-scenario numbers only -- there is no "sub-byte
|
|
275
|
+
across all scenarios" claim.
|
|
196
276
|
|
|
197
|
-
Per-yield, latest mode allocates one wrapper state
|
|
198
|
-
adds one snapshot array
|
|
277
|
+
Per-yield, `fromAsyncIterable` latest mode allocates one wrapper state
|
|
278
|
+
object; buffer mode adds one snapshot array. `toAsyncIterable` `mode:
|
|
279
|
+
"latest"` allocates zero on the steady-state hot path when the consumer
|
|
280
|
+
is caught up; `mode: "buffer"` also zero when no waiter is pending
|
|
281
|
+
(structural claim; gated by the alloc tier in 1.2.0).
|
|
199
282
|
|
|
200
283
|
## Files
|
|
201
284
|
|
|
202
|
-
- `Stream.js` -- single-file ESM implementation (~
|
|
285
|
+
- `Stream.js` -- single-file ESM implementation (~875 lines)
|
|
203
286
|
- `Stream.d.ts` -- TypeScript types with discriminated state union
|
|
204
287
|
- `README.md` -- full docs with integration recipes
|
|
205
288
|
- `llms.txt` -- this file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-stream",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Zero-GC bridge between async iterators and @zakkster/lite-signal. Project async streams (paginated APIs, SSE, network frames, pubsub topics) into signals. Bounded buffering with explicit overflow diagnostics, structural cleanup on three termination paths (iterator done,
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Zero-GC bridge between async iterators and @zakkster/lite-signal. Project async streams (paginated APIs, SSE, network frames, pubsub topics) into signals. Bounded buffering with explicit overflow diagnostics, structural cleanup on three termination paths (iterator done, iterator throws, AbortSignal abort). The multi-shot dual of lite-await's fromPromise.",
|
|
5
5
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"ROADMAP.md"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node --test --test-reporter=spec",
|
|
29
|
+
"test": "node --expose-gc --test --test-reporter=spec",
|
|
30
30
|
"test:gc": "node --expose-gc --test --test-reporter=spec",
|
|
31
31
|
"bench": "node --expose-gc bench/bench.mjs",
|
|
32
32
|
"verify": "npm test && npm run test:gc && npm run bench"
|