@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 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
- If the consumer disposes the result signal via `lite-signal`'s `dispose()`,
120
- the pump detects the disposal on its next attempted `sig.set()`, tears down
121
- the iterator silently (no `onError` fired -- this is consumer-initiated
122
- cleanup, not a stream error), and exits.
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. Measured retention is sub-byte per op
309
- across all scenarios:
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 detects on next
475
- `sig.set()`, tears down the iterator silently. No `onError` fired.
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
- 74 tests across `test/01-*` through `test/07-*`:
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
- Run via `npm run test:gc`.
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` -- ten scenarios (six 1.0.0 + four 1.1); throughput and
570
- B/op retained. Run via `npm run bench`.
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