@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/CHANGELOG.md +166 -3
- package/README.md +83 -9
- package/Stream.d.ts +70 -4
- package/Stream.js +133 -12
- package/llms.txt +72 -10
- package/package.json +10 -6
- package/ROADMAP.md +0 -711
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.
|
|
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.
|
|
53
|
-
value is replaced directly (no wrapper). Returns
|
|
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
|
-
(
|
|
344
|
+
(gated: test/torture.mjs s5, per-scenario budgets).
|
|
282
345
|
|
|
283
346
|
## Files
|
|
284
347
|
|
|
285
|
-
- `Stream.js` -- single-file ESM implementation (~
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|