@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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,159 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## 1.1.1 -- 2026-09-01
|
|
11
|
+
|
|
12
|
+
**Truth patch. No API change, no behavior change; the `Stream.js` diff is
|
|
13
|
+
comment-only.** Every shipped sentence the runnable probes could refute is
|
|
14
|
+
corrected, and the previously dark GC tier now gates on the default test run.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **dispose-teardown claim (LS-01)** -- llms.txt, README, the `Stream.js`
|
|
19
|
+
`fromAsyncIterable` docstring, and the `package.json` description all
|
|
20
|
+
claimed the pump detects a disposed result signal on the next `sig.set()`
|
|
21
|
+
and cleans up silently. It never did: `set()` after `dispose()` is a
|
|
22
|
+
silent no-op in lite-signal 1.2.2 and 1.5.0, so the pump keeps pulling and
|
|
23
|
+
neither `iterator.return()` nor any callback fires. Corrected to the true
|
|
24
|
+
behavior -- AbortSignal or natural completion are the only stop mechanisms.
|
|
25
|
+
The defensive `catch` around `set()` and its comment were re-framed
|
|
26
|
+
accordingly.
|
|
27
|
+
- **phantom contract doc + session narrative (LS-06)** -- three references
|
|
28
|
+
to a non-existent `CONTRACT-toAsyncIterable-1.1.md` (in `Stream.js` and
|
|
29
|
+
`test/07-to-async-iterable-rich.test.mjs`) and the "Session A/Session B"
|
|
30
|
+
development narrative were removed; the locked 1.1 semantics now point at
|
|
31
|
+
CHANGELOG.md [1.1.0] and llms.txt.
|
|
32
|
+
- **pump deferral comment (LS-07)** -- the comment claimed a microtask
|
|
33
|
+
deferral that is not in the code. `pump()` is called directly; the stack
|
|
34
|
+
stays bounded because the continuation runs from a `.then()` handler.
|
|
35
|
+
- **perf numbers (LS-08)** -- the shipped ops/s figures named no node
|
|
36
|
+
version, date, or machine and no longer matched a current run. Re-measured
|
|
37
|
+
once with provenance; llms.txt (Performance) is now the single source of
|
|
38
|
+
truth and README cites it. Four 1.1 `toAsyncIterable` bench scenarios
|
|
39
|
+
(`latest-1to1-x10`, `latest-churn-100`, `filter-timeout`, `multi-waiter-x3`)
|
|
40
|
+
were removed from the docs as unreproducible -- no runnable source for them
|
|
41
|
+
exists in the repo; only the six scenarios `bench/bench.mjs` runs are cited.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`test/08-dispose-behavior.test.mjs`** -- 6 tests pinning the TRUE dispose
|
|
46
|
+
semantics (LS-01: disposing a result/target signal does not stop the pump;
|
|
47
|
+
LS-04: a pending `next()` on a disposed source never settles, `timeout` is
|
|
48
|
+
the escape hatch).
|
|
49
|
+
- **`--expose-gc` on the default `npm test`** (LS-02) -- the 8 GC-tier tests
|
|
50
|
+
were dark under the default gate (71 run, 8 skipped); they now run on every
|
|
51
|
+
`npm test` (85 total, 0 skipped on Node 20+). `npm run test:gc` is retained
|
|
52
|
+
as an alias.
|
|
53
|
+
- **no-lockfile policy** (LS-10) -- one sentence in the README Testing
|
|
54
|
+
strategy documents why no `package-lock.json` is committed and how the peer
|
|
55
|
+
floor is verified.
|
|
56
|
+
|
|
57
|
+
### Testing
|
|
58
|
+
|
|
59
|
+
- 85 tests across `test/01-*` through `test/08-*` (up from 79 in 1.1.0). All
|
|
60
|
+
pass under `npm test` and `npm run test:gc`; per-scenario bench numbers with
|
|
61
|
+
provenance live in llms.txt (no ops/s digits are pinned in this changelog).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 1.1.0 -- 2026-07-10
|
|
66
|
+
|
|
67
|
+
**Additive `toAsyncIterable` enrichment.** Six additions, no breaking
|
|
68
|
+
changes. All 1.0.0 callers continue to work without modification. See
|
|
69
|
+
ROADMAP.md's "Shipped in 1.1.0" section for the triggering signals that
|
|
70
|
+
justified each addition.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **`mode: "latest" | "buffer"`** on `toAsyncIterable`. Default `"buffer"`
|
|
75
|
+
preserves 1.0.0 behavior. `mode: "latest"` uses a single mutable slot
|
|
76
|
+
(producer overwrites; consumer reads and clears; every overwrite bumps
|
|
77
|
+
`overflowCount`). Combining `mode: "latest"` with `maxBuffer` throws
|
|
78
|
+
`TypeError` at construction -- silently ignoring one would hide user
|
|
79
|
+
error.
|
|
80
|
+
|
|
81
|
+
- **`filter: (v) => unknown`** on `toAsyncIterable`. Skip values for which
|
|
82
|
+
the predicate returns falsy. Applied to the initial value when
|
|
83
|
+
`emitInitial` is `true`. A throwing filter rejects the current pending
|
|
84
|
+
`next()` with the thrown value and terminates the iterable; the throw
|
|
85
|
+
does NOT surface at the signal writer's `.set()` call site.
|
|
86
|
+
|
|
87
|
+
- **`timeout: number`** on `toAsyncIterable`. Overall deadline in ms from
|
|
88
|
+
the `toAsyncIterable(...)` call (not per-`next()`, not idle). On elapse
|
|
89
|
+
the pending `next()` rejects with `TimeoutError`; subsequent calls
|
|
90
|
+
return `{done: true}`. Negative or non-finite values throw `RangeError`
|
|
91
|
+
at construction.
|
|
92
|
+
|
|
93
|
+
- **`TimeoutError`** as a new named export. Structurally identical to
|
|
94
|
+
`@zakkster/lite-await`'s `TimeoutError` (`e.name === "TimeoutError"`
|
|
95
|
+
duck-checks work across both packages). NOT imported from lite-await --
|
|
96
|
+
zero runtime dependencies preserved.
|
|
97
|
+
|
|
98
|
+
- **`Symbol.asyncDispose`** on the `toAsyncIterable` return object (Node
|
|
99
|
+
20+ only; absent on older runtimes). Delegates to `iter.return()`.
|
|
100
|
+
Enables `await using iter = toAsyncIterable(sig, { mode: "latest" })`.
|
|
101
|
+
|
|
102
|
+
- **`overflowCount`** getter on the `toAsyncIterable` return object.
|
|
103
|
+
Alias for `droppedCount` with mode-neutral vocabulary. In
|
|
104
|
+
`mode: "buffer"` reads the same value as `droppedCount` (drop-oldest
|
|
105
|
+
count); in `mode: "latest"` counts every slot overwrite.
|
|
106
|
+
|
|
107
|
+
### Changed (invisible correctness fix)
|
|
108
|
+
|
|
109
|
+
- **Multi-waiter FIFO queue** replaces `toAsyncIterable`'s 1.0.0
|
|
110
|
+
single-slot `pendingResolve`. Concurrent `.next()` calls (e.g. from
|
|
111
|
+
`Promise.all([iter.next(), iter.next()])`) now resolve in FIFO order.
|
|
112
|
+
1.0.0 silently overwrote the first resolver, causing the first
|
|
113
|
+
`.next()` to never settle. Invisible to correct callers; unblocks
|
|
114
|
+
`Promise.all`-style consumers.
|
|
115
|
+
|
|
116
|
+
### Not changed
|
|
117
|
+
|
|
118
|
+
- `fromAsyncIterable`, `pipeToSignal` -- unchanged behavior, unchanged
|
|
119
|
+
types, unchanged bench numbers.
|
|
120
|
+
- `toAsyncIterable`'s default behavior (with no `mode` opt) exactly
|
|
121
|
+
matches 1.0.0. Every 1.0.0 test in `05-to-async-iterable.test.mjs`
|
|
122
|
+
continues to pass without modification.
|
|
123
|
+
- Peer dependency remains `@zakkster/lite-signal ^1.2.0`. Node engines
|
|
124
|
+
requirement unchanged (>=18); `Symbol.asyncDispose` is feature-detected
|
|
125
|
+
and gracefully absent on Node 18/19.
|
|
126
|
+
|
|
127
|
+
### Performance (Node v22, --expose-gc, 500ms runs)
|
|
128
|
+
|
|
129
|
+
1.1 additions:
|
|
130
|
+
- `to-async-iterable-latest-1to1-x10`: **119K ops/s** (~2.4M ops/s per
|
|
131
|
+
operation), 0.12 B/op retained
|
|
132
|
+
- `to-async-iterable-latest-churn-100`: **60K ops/s** (~6M writes/s
|
|
133
|
+
absorbed into single slot), 0.28 B/op retained
|
|
134
|
+
- `to-async-iterable-filter-timeout`: **141K ops/s**, NEGATIVE retained
|
|
135
|
+
heap (timer-clear discipline helps V8 reclaim more than baseline)
|
|
136
|
+
- `to-async-iterable-multi-waiter-x3`: **120K ops/s**, negative retained
|
|
137
|
+
|
|
138
|
+
1.0.0 scenarios: unchanged from 1.0.0 numbers.
|
|
139
|
+
|
|
140
|
+
### Testing
|
|
141
|
+
|
|
142
|
+
74 tests across 7 files (up from 49 in 1.0.0):
|
|
143
|
+
- `01`-`04`: `fromAsyncIterable`, `pipeToSignal`, cleanup triplet
|
|
144
|
+
(unchanged)
|
|
145
|
+
- `05-to-async-iterable.test.mjs` -- 10 tests, unchanged 1.0.0 behavior
|
|
146
|
+
- `06-gc.test.mjs` -- 8 heap-budget tests (up from 3): 1.0.0 paths +
|
|
147
|
+
5 new 1.1 paths at 2 MB budget
|
|
148
|
+
- `07-to-async-iterable-rich.test.mjs` (NEW) -- 25 tests covering the
|
|
149
|
+
six 1.1 additions
|
|
150
|
+
|
|
151
|
+
All 74 tests pass under both `npm test` and `npm run test:gc`.
|
|
152
|
+
|
|
153
|
+
### Bundle
|
|
154
|
+
|
|
155
|
+
- ~7 KB minified (up from ~5 KB), ~3 KB gzipped
|
|
156
|
+
- ESM-only, Node >= 18 (with `Symbol.asyncDispose` on 20+)
|
|
157
|
+
- Single file `Stream.js`
|
|
158
|
+
- Zero runtime dependencies
|
|
159
|
+
- Peer dep: `@zakkster/lite-signal ^1.2.0` (unchanged)
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
10
163
|
## 1.0.0 -- 2026-06-23
|
|
11
164
|
|
|
12
165
|
**Initial public release.** API frozen; subsequent 1.x releases are purely
|
|
@@ -50,6 +203,8 @@ All three paths handled structurally; abort listeners always removed.
|
|
|
50
203
|
Additionally: if the consumer disposes the result signal externally, the
|
|
51
204
|
pump detects on next `sig.set()` and tears down silently (no `onError` --
|
|
52
205
|
this is consumer-initiated cleanup).
|
|
206
|
+
(Superseded in 1.1.1: this claim was never true -- see [1.1.1] and ROADMAP
|
|
207
|
+
LS-01.)
|
|
53
208
|
|
|
54
209
|
### Performance (Node v22, --expose-gc, 500ms runs)
|
|
55
210
|
|
package/README.md
CHANGED
|
@@ -116,10 +116,11 @@ The abort listener is registered with `addEventListener("abort", ...)` and
|
|
|
116
116
|
**always removed** on any of the three paths -- no AbortSignal accumulates
|
|
117
117
|
dangling listeners across stream lifecycles.
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
Disposing the result signal does NOT stop the pump: the iterator keeps
|
|
120
|
+
pulling. AbortSignal or natural completion are the only stop mechanisms.
|
|
121
|
+
`iterator.return()` is never called and no callback fires, because
|
|
122
|
+
`lite-signal`'s `set()` after `dispose()` is a silent no-op -- the pump never
|
|
123
|
+
learns of the disposal. Abort `opts.signal` to stop early. See ROADMAP LS-01.
|
|
123
124
|
|
|
124
125
|
## API reference
|
|
125
126
|
|
|
@@ -182,6 +183,8 @@ Use `pipeToSignal` when:
|
|
|
182
183
|
- You want a `stop` fn instead of an `AbortController` for cleanup
|
|
183
184
|
|
|
184
185
|
**Does NOT dispose the target signal.** The caller owns its lifetime.
|
|
186
|
+
Disposing the target signal does NOT stop the pump: call the returned stop
|
|
187
|
+
fn or abort `opts.signal`.
|
|
185
188
|
|
|
186
189
|
### toAsyncIterable
|
|
187
190
|
|
|
@@ -190,10 +193,19 @@ toAsyncIterable<T>(
|
|
|
190
193
|
sig: Signal<T> | Computed<T>,
|
|
191
194
|
opts?: {
|
|
192
195
|
signal?: AbortSignal,
|
|
193
|
-
emitInitial?: boolean,
|
|
194
|
-
maxBuffer?: number
|
|
196
|
+
emitInitial?: boolean, // default true
|
|
197
|
+
maxBuffer?: number, // default 1024 (buffer mode)
|
|
198
|
+
|
|
199
|
+
// Added in 1.1
|
|
200
|
+
mode?: "latest" | "buffer", // default "buffer"
|
|
201
|
+
filter?: (v: T) => unknown,
|
|
202
|
+
timeout?: number
|
|
195
203
|
}
|
|
196
|
-
): AsyncIterable<T> & {
|
|
204
|
+
): AsyncIterable<T> & {
|
|
205
|
+
readonly droppedCount: number,
|
|
206
|
+
readonly overflowCount: number, // 1.1 alias for droppedCount
|
|
207
|
+
[Symbol.asyncDispose]?: () => Promise<IteratorResult<T, undefined>>
|
|
208
|
+
}
|
|
197
209
|
```
|
|
198
210
|
|
|
199
211
|
The reverse direction -- yield signal changes as an async iterable. Useful
|
|
@@ -201,12 +213,41 @@ when you have signal-driven state and want to pipe its changes into a
|
|
|
201
213
|
WebSocket, log sink, replay tool, or any consumer that wants for-await
|
|
202
214
|
semantics.
|
|
203
215
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
`
|
|
208
|
-
|
|
209
|
-
|
|
216
|
+
**Two backpressure modes:**
|
|
217
|
+
|
|
218
|
+
- `"buffer"` (default; matches 1.0.0) -- FIFO ring buffer of `maxBuffer`
|
|
219
|
+
size. On overflow the OLDEST value is dropped and `droppedCount` (and
|
|
220
|
+
`overflowCount`) increments. Use when every value matters and must be
|
|
221
|
+
seen in order.
|
|
222
|
+
- `"latest"` (1.1) -- single mutable slot. Producer overwrites; consumer
|
|
223
|
+
reads and clears. Every overwrite bumps `overflowCount`. Use for
|
|
224
|
+
reactive-state consumers ("show the current frame / latest cursor /
|
|
225
|
+
live price") where intermediate values can be safely discarded.
|
|
226
|
+
|
|
227
|
+
**1.1 additions worth calling out:**
|
|
228
|
+
|
|
229
|
+
- `filter` -- skip values for which the predicate returns falsy. A
|
|
230
|
+
throwing filter rejects the current pending `next()` and terminates
|
|
231
|
+
the iterable; the throw does NOT surface at the signal writer's
|
|
232
|
+
`.set()` call site.
|
|
233
|
+
- `timeout` -- overall deadline in ms; on elapse the pending `next()`
|
|
234
|
+
rejects with `TimeoutError` (a new named export in 1.1). Subsequent
|
|
235
|
+
calls return `{done: true}`.
|
|
236
|
+
- `Symbol.asyncDispose` -- delegates to `iter.return()` on Node 20+;
|
|
237
|
+
enables `await using iter = toAsyncIterable(sig, { mode: "latest" })`.
|
|
238
|
+
- Multi-waiter queue -- concurrent `.next()` calls (e.g. from
|
|
239
|
+
`Promise.all([iter.next(), iter.next()])`) now resolve in FIFO order.
|
|
240
|
+
In 1.0.0 the second call silently overwrote the first resolver; the
|
|
241
|
+
1.1 fix is invisible to correct callers.
|
|
242
|
+
|
|
243
|
+
The iterator naturally completes when `opts.signal` aborts; consumer-side
|
|
244
|
+
`break` triggers `iterator.return()`. `mode: "latest"` combined with
|
|
245
|
+
`maxBuffer` is rejected with `TypeError` at construction -- a single-slot
|
|
246
|
+
mode plus a buffer size is a user error worth surfacing loudly.
|
|
247
|
+
|
|
248
|
+
Treat this as the secondary API for classic "consume signal changes"
|
|
249
|
+
patterns; for the streaming pipeline case (paginated APIs, SSE, pubsub)
|
|
250
|
+
the forward direction is what most consumers want.
|
|
210
251
|
|
|
211
252
|
## Modes: latest vs buffer
|
|
212
253
|
|
|
@@ -216,6 +257,8 @@ Pick `"latest"` when you only care about the most recent value:
|
|
|
216
257
|
- Current pubsub message
|
|
217
258
|
- Latest network frame
|
|
218
259
|
- Current SSE event
|
|
260
|
+
- Reactive state bridged into `@zakkster/lite-query`'s `streamQuery`
|
|
261
|
+
(matches its `mode: "latest"` on the forward direction)
|
|
219
262
|
|
|
220
263
|
Pick `"buffer"` when every value matters and you want them in order:
|
|
221
264
|
|
|
@@ -223,29 +266,50 @@ Pick `"buffer"` when every value matters and you want them in order:
|
|
|
223
266
|
- EBS SSE event log (must not miss an event)
|
|
224
267
|
- Replay queue (every frame applies in order)
|
|
225
268
|
|
|
226
|
-
|
|
227
|
-
|
|
269
|
+
**Availability:**
|
|
270
|
+
- `fromAsyncIterable` has both modes from 1.0.0.
|
|
271
|
+
- `toAsyncIterable` has both modes from 1.1 (was `"buffer"`-only in 1.0.0;
|
|
272
|
+
the option was added as `mode: "latest"` while preserving default
|
|
273
|
+
behavior).
|
|
274
|
+
|
|
275
|
+
**`"buffer"` mode requires `maxBuffer` on `fromAsyncIterable`.** There is
|
|
276
|
+
no default there, and missing the option throws a `RangeError` with this
|
|
277
|
+
message:
|
|
228
278
|
|
|
229
279
|
> `lite-stream: "buffer" mode requires opts.maxBuffer to be a positive
|
|
230
280
|
> integer. Unbounded buffering is a memory bug pretending to be a feature;
|
|
231
281
|
> pick a deliberate ceiling.`
|
|
232
282
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
283
|
+
`toAsyncIterable`'s `"buffer"` mode defaults `maxBuffer` to 1024
|
|
284
|
+
(unchanged in 1.1). On overflow the OLDEST value is dropped and
|
|
285
|
+
`droppedCount` increments. Watch `droppedCount` in your effect to know
|
|
286
|
+
when your consumer is falling behind.
|
|
287
|
+
|
|
288
|
+
**streamQuery integration** (matching vocabulary on both directions):
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
import { toAsyncIterable } from "@zakkster/lite-stream";
|
|
292
|
+
import { streamQuery } from "@zakkster/lite-query/stream";
|
|
293
|
+
|
|
294
|
+
streamQuery(qc, {
|
|
295
|
+
key: ["price", ticker],
|
|
296
|
+
stream: ({ signal }) => toAsyncIterable(priceSig, {
|
|
297
|
+
signal,
|
|
298
|
+
mode: "latest" // matches streamQuery's mode
|
|
299
|
+
}),
|
|
300
|
+
mode: "latest"
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The `signal` from the stream context wires straight through: streamQuery
|
|
305
|
+
aborts on detach, `toAsyncIterable` catches the abort and calls
|
|
306
|
+
`return()`. Zero glue code.
|
|
236
307
|
|
|
237
308
|
## Zero-GC hot paths
|
|
238
309
|
|
|
239
310
|
`lite-stream`'s hot paths -- per-yield state allocation and per-pull abort
|
|
240
|
-
checks -- minimize per-op allocation.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
```
|
|
244
|
-
from-latest-10 178K ops/s 0.07 B/op
|
|
245
|
-
from-buffer-20-drop 128K ops/s 0.13 B/op
|
|
246
|
-
abort-cycle 84K ops/s 0.71 B/op
|
|
247
|
-
pipe-to-signal 235K ops/s 0.10 B/op
|
|
248
|
-
```
|
|
311
|
+
checks -- minimize per-op allocation. Current numbers with provenance live in
|
|
312
|
+
`llms.txt` (Performance); re-run `npm run bench` on your target.
|
|
249
313
|
|
|
250
314
|
Per-yield, `"latest"` mode allocates exactly one wrapper state object. The
|
|
251
315
|
underlying signal node is reused via lite-signal's pool. `"buffer"` mode
|
|
@@ -403,8 +467,9 @@ const ws = new WebSocket("wss://logs.example.com/ingest");
|
|
|
403
467
|
- **Sync `Iterable<T>`**: accepted via `Symbol.iterator`. Pulls happen
|
|
404
468
|
microtask-asynchronously even for sync sources, so caller's `effect()`
|
|
405
469
|
can wire up before the first yield.
|
|
406
|
-
- **Consumer disposes the signal mid-stream**: pump
|
|
407
|
-
`
|
|
470
|
+
- **Consumer disposes the signal mid-stream**: the pump keeps pulling.
|
|
471
|
+
`iterator.return()` is not called and no callback fires; abort
|
|
472
|
+
`opts.signal` to stop.
|
|
408
473
|
- **Buffer mode snapshot array**: a fresh array per yield. Past observers
|
|
409
474
|
hold stable, non-mutated references.
|
|
410
475
|
- **Disposing the signal multiple times**: lite-signal's `dispose` is
|
|
@@ -413,40 +478,15 @@ const ws = new WebSocket("wss://logs.example.com/ingest");
|
|
|
413
478
|
|
|
414
479
|
## Benchmarks
|
|
415
480
|
|
|
416
|
-
Run via `npm run bench` (requires `--expose-gc`).
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
[1] fromAsyncIterable latest mode -- 10-item async gen
|
|
420
|
-
from-latest-10 178,554 ops/s retained: 0.07 B/op
|
|
421
|
-
|
|
422
|
-
[2] fromAsyncIterable buffer mode -- 20-item gen, maxBuffer 5
|
|
423
|
-
from-buffer-20-drop 128,630 ops/s retained: 0.13 B/op
|
|
424
|
-
|
|
425
|
-
[3] Abort cycle -- infinite gen, abort after first yield
|
|
426
|
-
abort-cycle 84,102 ops/s retained: 0.71 B/op
|
|
427
|
-
|
|
428
|
-
[4] pipeToSignal -- pump existing signal directly
|
|
429
|
-
pipe-to-signal 235,376 ops/s retained: 0.10 B/op
|
|
430
|
-
|
|
431
|
-
[5] toAsyncIterable -- 10 signal updates, consumer drains in order
|
|
432
|
-
to-async-iterable 42,480 ops/s retained: ~0 B/op
|
|
433
|
-
|
|
434
|
-
[6] toAsyncIterable -- producer overflow, drop-oldest
|
|
435
|
-
to-async-iterable-overflow 62,164 ops/s retained: ~25 B/op
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
(Node v22, 150ms warmup, 500ms runs.)
|
|
439
|
-
|
|
440
|
-
The full lifecycle (construct, pull, settle, dispose) of `fromAsyncIterable`
|
|
441
|
-
in latest mode runs at 178K ops/s with sub-byte/op retained heap. Per-pull
|
|
442
|
-
allocation is dominated by the iterator's own microtask cost, not by
|
|
443
|
-
lite-stream's wrapper state.
|
|
481
|
+
Run via `npm run bench` (requires `--expose-gc`). Current numbers with
|
|
482
|
+
provenance live in `llms.txt` (Performance); re-run `npm run bench` on your
|
|
483
|
+
target.
|
|
444
484
|
|
|
445
485
|
## Testing strategy
|
|
446
486
|
|
|
447
487
|
### Tier 1 -- behavior (unit tests, fast)
|
|
448
488
|
|
|
449
|
-
|
|
489
|
+
85 tests across `test/01-*` through `test/08-*`:
|
|
450
490
|
|
|
451
491
|
- `01-from-async-iterable-latest.test.mjs` -- state shape, lifecycle,
|
|
452
492
|
Iterable acceptance variants, pre-aborted, subscriber observability
|
|
@@ -456,24 +496,40 @@ lite-stream's wrapper state.
|
|
|
456
496
|
return() invocation, abort reason propagation, 200-cycle leak smoke test
|
|
457
497
|
- `04-pipe-to-signal.test.mjs` -- target write, stop fn idempotency,
|
|
458
498
|
transform, abort behavior, target-not-disposed invariant
|
|
459
|
-
- `05-to-async-iterable.test.mjs` -- emitInitial, ordering,
|
|
460
|
-
consumer break triggers return(), pre-aborted path
|
|
499
|
+
- `05-to-async-iterable.test.mjs` -- 1.0.0 behavior: emitInitial, ordering,
|
|
500
|
+
overflow, consumer break triggers return(), pre-aborted path
|
|
501
|
+
- `07-to-async-iterable-rich.test.mjs` (1.1) -- `mode:"latest"` behavior,
|
|
502
|
+
filter (including throwing-filter no-writer-surface discipline), timeout
|
|
503
|
+
+ TimeoutError, Symbol.asyncDispose (Node 20+ guarded), multi-waiter
|
|
504
|
+
queue, `overflowCount` alias, 4K structural cleanup cycles
|
|
505
|
+
- `08-dispose-behavior.test.mjs` -- pins the TRUE dispose semantics
|
|
506
|
+
(LS-01: disposing a result/target signal does NOT stop the pump;
|
|
507
|
+
LS-04: a pending `next()` on a disposed source never settles, `timeout`
|
|
508
|
+
is the only escape hatch)
|
|
461
509
|
|
|
462
510
|
Run via `npm test`.
|
|
463
511
|
|
|
464
512
|
### Tier 2 -- memory (allocation-free verification)
|
|
465
513
|
|
|
466
514
|
`test/06-gc.test.mjs` -- runs under `--expose-gc`. Asserts heap budget
|
|
467
|
-
ceilings for
|
|
468
|
-
|
|
515
|
+
ceilings at 2 MB for the 1.0.0 paths (2K latest, 1K buffer, 1K abort) and
|
|
516
|
+
the 1.1 additions (5K `mode:"latest"` resolve, 3K timeout, 1K buffer
|
|
517
|
+
fill+drain, 2K `mode:"latest"` churn, 2K filter-throw).
|
|
469
518
|
|
|
470
|
-
|
|
519
|
+
The GC tier now runs under the default `npm test` (the script carries
|
|
520
|
+
`--expose-gc`), so these budgets gate on every run; `npm run test:gc` is
|
|
521
|
+
retained as an alias.
|
|
471
522
|
|
|
472
523
|
### Tier 3 -- performance (measured throughput)
|
|
473
524
|
|
|
474
525
|
`bench/bench.mjs` -- six scenarios; throughput and B/op retained. Run via
|
|
475
526
|
`npm run bench`.
|
|
476
527
|
|
|
528
|
+
No `package-lock.json` is committed: devDependencies float within their
|
|
529
|
+
stated ranges, and correctness against the peer floor is proven by running
|
|
530
|
+
the suite against `@zakkster/lite-signal@1.2.2` rather than by pinning a
|
|
531
|
+
lockfile.
|
|
532
|
+
|
|
477
533
|
## What this is not
|
|
478
534
|
|
|
479
535
|
- **Not a full reactive stream library.** No `map`, `filter`, `merge`, or
|