@zakkster/lite-stream 1.1.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 +57 -0
- package/README.md +30 -69
- package/ROADMAP.md +680 -208
- package/Stream.js +32 -19
- package/llms.txt +33 -18
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@ 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
|
+
|
|
10
65
|
## 1.1.0 -- 2026-07-10
|
|
11
66
|
|
|
12
67
|
**Additive `toAsyncIterable` enrichment.** Six additions, no breaking
|
|
@@ -148,6 +203,8 @@ All three paths handled structurally; abort listeners always removed.
|
|
|
148
203
|
Additionally: if the consumer disposes the result signal externally, the
|
|
149
204
|
pump detects on next `sig.set()` and tears down silently (no `onError` --
|
|
150
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.)
|
|
151
208
|
|
|
152
209
|
### Performance (Node v22, --expose-gc, 500ms runs)
|
|
153
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
|
|
|
@@ -305,15 +308,8 @@ aborts on detach, `toAsyncIterable` catches the abort and calls
|
|
|
305
308
|
## Zero-GC hot paths
|
|
306
309
|
|
|
307
310
|
`lite-stream`'s hot paths -- per-yield state allocation and per-pull abort
|
|
308
|
-
checks -- minimize per-op allocation.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
```
|
|
312
|
-
from-latest-10 178K ops/s 0.07 B/op
|
|
313
|
-
from-buffer-20-drop 128K ops/s 0.13 B/op
|
|
314
|
-
abort-cycle 84K ops/s 0.71 B/op
|
|
315
|
-
pipe-to-signal 235K ops/s 0.10 B/op
|
|
316
|
-
```
|
|
311
|
+
checks -- minimize per-op allocation. Current numbers with provenance live in
|
|
312
|
+
`llms.txt` (Performance); re-run `npm run bench` on your target.
|
|
317
313
|
|
|
318
314
|
Per-yield, `"latest"` mode allocates exactly one wrapper state object. The
|
|
319
315
|
underlying signal node is reused via lite-signal's pool. `"buffer"` mode
|
|
@@ -471,8 +467,9 @@ const ws = new WebSocket("wss://logs.example.com/ingest");
|
|
|
471
467
|
- **Sync `Iterable<T>`**: accepted via `Symbol.iterator`. Pulls happen
|
|
472
468
|
microtask-asynchronously even for sync sources, so caller's `effect()`
|
|
473
469
|
can wire up before the first yield.
|
|
474
|
-
- **Consumer disposes the signal mid-stream**: pump
|
|
475
|
-
`
|
|
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.
|
|
476
473
|
- **Buffer mode snapshot array**: a fresh array per yield. Past observers
|
|
477
474
|
hold stable, non-mutated references.
|
|
478
475
|
- **Disposing the signal multiple times**: lite-signal's `dispose` is
|
|
@@ -481,62 +478,15 @@ const ws = new WebSocket("wss://logs.example.com/ingest");
|
|
|
481
478
|
|
|
482
479
|
## Benchmarks
|
|
483
480
|
|
|
484
|
-
Run via `npm run bench` (requires `--expose-gc`).
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
[1] fromAsyncIterable latest mode -- 10-item async gen
|
|
488
|
-
from-latest-10 178,554 ops/s retained: 0.07 B/op
|
|
489
|
-
|
|
490
|
-
[2] fromAsyncIterable buffer mode -- 20-item gen, maxBuffer 5
|
|
491
|
-
from-buffer-20-drop 128,630 ops/s retained: 0.13 B/op
|
|
492
|
-
|
|
493
|
-
[3] Abort cycle -- infinite gen, abort after first yield
|
|
494
|
-
abort-cycle 84,102 ops/s retained: 0.71 B/op
|
|
495
|
-
|
|
496
|
-
[4] pipeToSignal -- pump existing signal directly
|
|
497
|
-
pipe-to-signal 235,376 ops/s retained: 0.10 B/op
|
|
498
|
-
|
|
499
|
-
[5] toAsyncIterable -- 10 signal updates, consumer drains in order
|
|
500
|
-
to-async-iterable 42,480 ops/s retained: ~0 B/op
|
|
501
|
-
|
|
502
|
-
[6] toAsyncIterable -- producer overflow, drop-oldest
|
|
503
|
-
to-async-iterable-overflow 62,164 ops/s retained: ~25 B/op
|
|
504
|
-
|
|
505
|
-
--- 1.1 additions ---
|
|
506
|
-
|
|
507
|
-
[7] toAsyncIterable mode:latest -- 10 write/next pairs per cycle
|
|
508
|
-
to-async-iterable-latest-1to1-x10 119,278 ops/s retained: 0.12 B/op
|
|
509
|
-
|
|
510
|
-
[8] toAsyncIterable mode:latest -- 100 writes coalesced, single read
|
|
511
|
-
to-async-iterable-latest-churn-100 60,674 ops/s retained: 0.28 B/op
|
|
512
|
-
|
|
513
|
-
[9] toAsyncIterable filter + timeout -- consumer wins before timeout
|
|
514
|
-
to-async-iterable-filter-timeout 141,080 ops/s retained: -11.80 B/op
|
|
515
|
-
|
|
516
|
-
[10] toAsyncIterable multi-waiter -- 3 concurrent next() calls
|
|
517
|
-
to-async-iterable-multi-waiter-x3 120,020 ops/s retained: -0.01 B/op
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
(Node v22, 150ms warmup, 500ms runs.)
|
|
521
|
-
|
|
522
|
-
The full lifecycle (construct, pull, settle, dispose) of `fromAsyncIterable`
|
|
523
|
-
in latest mode runs at 178K ops/s with sub-byte/op retained heap. Per-pull
|
|
524
|
-
allocation is dominated by the iterator's own microtask cost, not by
|
|
525
|
-
lite-stream's wrapper state.
|
|
526
|
-
|
|
527
|
-
1.1's `toAsyncIterable mode:"latest"` amortizes to ~2.4M ops/s per operation
|
|
528
|
-
(divide the reported cycle rate by the inner loop count). The filter+timeout
|
|
529
|
-
scenario `[9]` runs at 141K ops/s with NEGATIVE retained heap -- the
|
|
530
|
-
timer-clear discipline on `iter.return()` lets V8's subsequent GC pass
|
|
531
|
-
reclaim more than the pre-run measurement. The multi-waiter scenario `[10]`
|
|
532
|
-
confirms the 1.0.0 latent-bug fix (single-slot `pendingResolve` overwriting
|
|
533
|
-
the first resolver) works cleanly under bench load.
|
|
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.
|
|
534
484
|
|
|
535
485
|
## Testing strategy
|
|
536
486
|
|
|
537
487
|
### Tier 1 -- behavior (unit tests, fast)
|
|
538
488
|
|
|
539
|
-
|
|
489
|
+
85 tests across `test/01-*` through `test/08-*`:
|
|
540
490
|
|
|
541
491
|
- `01-from-async-iterable-latest.test.mjs` -- state shape, lifecycle,
|
|
542
492
|
Iterable acceptance variants, pre-aborted, subscriber observability
|
|
@@ -552,6 +502,10 @@ the first resolver) works cleanly under bench load.
|
|
|
552
502
|
filter (including throwing-filter no-writer-surface discipline), timeout
|
|
553
503
|
+ TimeoutError, Symbol.asyncDispose (Node 20+ guarded), multi-waiter
|
|
554
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)
|
|
555
509
|
|
|
556
510
|
Run via `npm test`.
|
|
557
511
|
|
|
@@ -562,12 +516,19 @@ ceilings at 2 MB for the 1.0.0 paths (2K latest, 1K buffer, 1K abort) and
|
|
|
562
516
|
the 1.1 additions (5K `mode:"latest"` resolve, 3K timeout, 1K buffer
|
|
563
517
|
fill+drain, 2K `mode:"latest"` churn, 2K filter-throw).
|
|
564
518
|
|
|
565
|
-
|
|
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.
|
|
566
522
|
|
|
567
523
|
### Tier 3 -- performance (measured throughput)
|
|
568
524
|
|
|
569
|
-
`bench/bench.mjs` --
|
|
570
|
-
|
|
525
|
+
`bench/bench.mjs` -- six scenarios; throughput and B/op retained. Run via
|
|
526
|
+
`npm run bench`.
|
|
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.
|
|
571
532
|
|
|
572
533
|
## What this is not
|
|
573
534
|
|