@zakkster/lite-stream 1.1.1 → 1.3.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/llms.txt CHANGED
@@ -5,7 +5,7 @@ multi-shot dual of lite-await's `fromPromise`. Project async streams
5
5
  (paginated APIs, SSE, network frames, pubsub topics) into signals with
6
6
  bounded buffering and structural cleanup.
7
7
 
8
- ESM-only. Node >= 18. ~5 KB minified. Single file `Stream.js`.
8
+ ESM-only. Node >= 18. Zero runtime dependencies. Single file `Stream.js`.
9
9
  Peer dep: `@zakkster/lite-signal ^1.2.0`.
10
10
 
11
11
  ## Install
@@ -14,7 +14,7 @@ Peer dep: `@zakkster/lite-signal ^1.2.0`.
14
14
 
15
15
  ## Imports
16
16
 
17
- import { fromAsyncIterable, pipeToSignal, toAsyncIterable, TimeoutError } from "@zakkster/lite-stream";
17
+ import { fromAsyncIterable, pipeToSignal, toAsyncIterable, TimeoutError, VERSION } from "@zakkster/lite-stream";
18
18
 
19
19
  ## Exports
20
20
 
@@ -49,22 +49,72 @@ Throws `TypeError` on null/undefined/wrong-shape source. Throws
49
49
 
50
50
  ### `pipeToSignal(source, target, opts?) -> stop`
51
51
 
52
- Pump an existing writable signal from an async iterator. The signal's
53
- value is replaced directly (no wrapper). Returns an idempotent stop fn.
52
+ Pump an existing writable signal from an async iterator. In the default
53
+ `"latest"` mode the signal's value is replaced directly (no wrapper). Returns
54
+ an idempotent stop fn that also carries `droppedCount` / `overflowCount`
55
+ getters.
54
56
 
55
57
  ```js
56
58
  const stop = pipeToSignal(source, targetSig, {
57
59
  signal: abortCtrl.signal,
60
+ mode: "latest" | "buffer", // default "latest"
61
+ maxBuffer: 64, // REQUIRED if mode = "buffer"
62
+ transform: (v) => transform(v), // per-value map, runs first
63
+ onValue: (v) => {}, // tap: fires ONCE per value BEFORE set
64
+ onAbort: (reason) => {}, // aborts route HERE instead of onError
58
65
  onError: (e) => {},
59
- onDone: () => {},
60
- transform: (v) => transform(v)
66
+ onDone: () => {}
61
67
  });
68
+
69
+ stop(); // idempotent -- calling again is a no-op
70
+ stop.droppedCount; // buffer-mode overflow count (0 in latest mode)
71
+ stop.overflowCount; // alias of droppedCount
62
72
  ```
63
73
 
74
+ Enrichment (1.3.0, all ADDITIVE -- the un-optioned path is byte-identical to
75
+ 1.1.0):
76
+
77
+ - `mode: "buffer"` drives the target with a bounded newest-last snapshot array
78
+ (a fresh array per set) instead of the raw value. REQUIRES `maxBuffer` (a
79
+ positive integer; missing/invalid throws `RangeError` -- same
80
+ "unbounded buffering is a memory bug pretending to be a feature" voice as
81
+ `fromAsyncIterable`). Overflow drops the oldest and increments the stop fn's
82
+ `droppedCount`. `mode: "latest"` + `maxBuffer` throws `TypeError`; an unknown
83
+ mode throws `TypeError`. All validated ONCE at construction.
84
+ - `onValue(v)` is a per-value tap that fires exactly once per value BEFORE the
85
+ `set` (and after `transform`). A THROWING `onValue` routes through the same
86
+ leg as a throwing transform -- `stop()` then `onError` (precedent: test/04
87
+ "transform throwing stops the pump cleanly").
88
+ - `onAbort(reason)`, when present, receives aborts INSTEAD of `onError` (both at
89
+ the abort listener and the pre-aborted early return), so consumers stop
90
+ filtering intentional aborts out of their error handler. Absent -> 1.1.0
91
+ behavior byte-identical (aborts flow through `onError`). See the abort
92
+ vocabulary contract table below and `decisions/0001-abort-vocabulary.md`.
93
+ - `stop.droppedCount` and `stop.overflowCount` are getters ON the returned stop
94
+ fn (aliases of one overflow counter); the stop fn stays callable and
95
+ idempotent exactly as 1.1.0.
96
+
64
97
  Does NOT dispose the target signal. The caller owns its lifetime. Disposing
65
98
  the target signal does NOT stop the pump: call the returned stop fn or abort
66
99
  opts.signal.
67
100
 
101
+ #### Abort vocabulary contract
102
+
103
+ The two directions frame an AbortSignal abort differently, by design. See
104
+ `decisions/0001-abort-vocabulary.md` (a repo-only doc, not shipped in the
105
+ tarball).
106
+
107
+ | Direction | On abort | Framing | Escape hatch |
108
+ | --- | --- | --- | --- |
109
+ | `fromAsyncIterable` (from-side) | interrupt production; `iter.return()` best-effort; terminal error state | error (`onError`, `state.error`) | -- |
110
+ | `pipeToSignal` (from-side) | interrupt production; `iter.return()` best-effort | error (`onError`) by default | `onAbort` routes aborts away from `onError` |
111
+ | `toAsyncIterable` (to-side) | stop listening; pending `next()` resolves `{ done: true }` | graceful done | already graceful; nothing to filter |
112
+
113
+ Aborting a producer is an interruption of work in flight (error framing);
114
+ aborting a consumer is a subscriber leaving (graceful done). `onAbort` is added
115
+ to `pipeToSignal` ONLY -- the sole direction that carried a shipped consumer
116
+ workaround.
117
+
68
118
  ### `toAsyncIterable(sig, opts?) -> AsyncIterable<T> & { droppedCount, overflowCount }`
69
119
 
70
120
  Reverse direction: yield signal changes as an async iterable.
@@ -120,12 +170,25 @@ Structurally identical to `@zakkster/lite-await`'s TimeoutError so
120
170
  `e.name === "TimeoutError"` duck-checks work across both packages. Not
121
171
  imported from lite-await -- lite-stream stays zero-dep.
122
172
 
173
+ ### `VERSION`
174
+
175
+ String, always equals the installed package.json version. Not re-exported
176
+ by consumers (suite ruling); the surface-guard asserts the equality on
177
+ every test run.
178
+
123
179
  ## Cleanup termination triplet
124
180
 
125
181
  `fromAsyncIterable` ends on exactly three paths, all structural:
126
182
 
127
183
  1. Iterator natural completion (`{ done: true }`) -> done state, onDone fires.
128
- 2. Iterator throws -> error state, onError fires.
184
+ 2. Iterator throws -> error state, onError fires. As of 1.3.0 (LS-13) this leg
185
+ also covers a `next()` result whose `done` or `value` is a THROWING getter:
186
+ the thrown value routes to the error state with onError fired exactly once,
187
+ instead of escaping as an unhandledRejection with the signal never settling.
188
+ `pipeToSignal` gets the same fix (stop + onError). Both pumps read the two
189
+ result properties once, inside a dedicated try/catch that routes to the
190
+ existing "iterator throws" leg; the peer-defense catch around `set()` (LS-01)
191
+ is separate and untouched.
129
192
  3. AbortSignal aborts -> `iter.return()` called, error state, onError fires.
130
193
 
131
194
  The abort listener is always removed on any of the three paths -- no
@@ -278,16 +341,15 @@ Per-yield, `fromAsyncIterable` latest mode allocates one wrapper state
278
341
  object; buffer mode adds one snapshot array. `toAsyncIterable` `mode:
279
342
  "latest"` allocates zero on the steady-state hot path when the consumer
280
343
  is caught up; `mode: "buffer"` also zero when no waiter is pending
281
- (structural claim; gated by the alloc tier in 1.2.0).
344
+ (gated: test/torture.mjs s5, per-scenario budgets).
282
345
 
283
346
  ## Files
284
347
 
285
- - `Stream.js` -- single-file ESM implementation (~875 lines)
348
+ - `Stream.js` -- single-file ESM implementation (~995 lines)
286
349
  - `Stream.d.ts` -- TypeScript types with discriminated state union
287
350
  - `README.md` -- full docs with integration recipes
288
351
  - `llms.txt` -- this file
289
352
  - `CHANGELOG.md` -- version history
290
- - `ROADMAP.md` -- deferred features
291
353
 
292
354
  ## Author
293
355
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-stream",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
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",
@@ -22,14 +22,16 @@
22
22
  "README.md",
23
23
  "llms.txt",
24
24
  "LICENSE.txt",
25
- "CHANGELOG.md",
26
- "ROADMAP.md"
25
+ "CHANGELOG.md"
27
26
  ],
28
27
  "scripts": {
29
- "test": "node --expose-gc --test --test-reporter=spec",
30
- "test:gc": "node --expose-gc --test --test-reporter=spec",
28
+ "test": "node --expose-gc --test --test-reporter=spec test/*.test.mjs",
29
+ "test:gc": "node --expose-gc --test --test-reporter=spec test/*.test.mjs",
31
30
  "bench": "node --expose-gc bench/bench.mjs",
32
- "verify": "npm test && npm run test:gc && npm run bench"
31
+ "torture": "node --expose-gc test/torture.mjs",
32
+ "torture:control": "for t in s0 s1 s2 s3 s4 s5 s6 s7 s7e s8; do if STREAM_TORTURE_BREAK=$t node --expose-gc test/torture.mjs >/dev/null 2>&1; then echo \"control $t FAILED-TO-FAIL\"; exit 1; else echo \"control $t failed-as-expected\"; fi; done",
33
+ "test:floor": "echo '== floor lane: pin @zakkster/lite-signal@1.2.2 (torture NOT run here -- lite-leak peers lite-signal>=1.5.0, LS-09 amendment) ==' && npm i --no-save --no-package-lock @zakkster/lite-signal@1.2.2 --legacy-peer-deps && npm test && echo 'FLOOR-PASS: npm test green @ lite-signal@1.2.2' && echo '== latest lane: restore @zakkster/lite-signal@^1.2.0 ==' && npm i --no-save --no-package-lock @zakkster/lite-signal@^1.2.0 && npm test && npm run torture && echo 'LATEST-PASS: npm test + torture green @ lite-signal latest'",
34
+ "verify": "npm test && npm run test:gc && npm run bench && npm run torture"
33
35
  },
34
36
  "keywords": [
35
37
  "stream",
@@ -66,6 +68,8 @@
66
68
  "@zakkster/lite-signal": "^1.2.0"
67
69
  },
68
70
  "devDependencies": {
71
+ "@zakkster/lite-gc-profiler": "^1.16.0",
72
+ "@zakkster/lite-leak": "^1.10.0",
69
73
  "@zakkster/lite-signal": "^1.2.2"
70
74
  }
71
75
  }