@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/ROADMAP.md
CHANGED
|
@@ -1,239 +1,711 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lite-stream -- enriched roadmap (post-1.1.0)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Blueprint: `../BLUEPRINT_ROADMAP.md`. Sibling roadmaps this one is wired to:
|
|
4
|
+
`../LiteQuery/ROADMAP.md` (the flagship consumer), `../LiteBakeStream/ROADMAP.md`
|
|
5
|
+
(the torture-spec model), `../LiteAwait/AWAIT_ROADMAP.md` (the TimeoutError
|
|
6
|
+
duck-pact). Ethos carried over unchanged: **reproduce by running, never infer.**
|
|
7
|
+
Every finding below was produced by executing code on 2026-09-01, not by
|
|
8
|
+
reading it. Reproductions live in `bench/findings-probes-2026-09-01.mjs`.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
This roadmap exists because of the same discipline that drove
|
|
10
|
-
`lite-signal`'s and `lite-statechart`'s development: ship the minimum that
|
|
11
|
-
solves the actual problem, defer everything else until a concrete consumer
|
|
12
|
-
demonstrates the need with a real code site. Nothing in this file is
|
|
13
|
-
"planned for 1.x" -- everything is "considered, deferred, waiting for a
|
|
14
|
-
triggering signal."
|
|
10
|
+
Sessions: LS1 (v1.1.1, truth patch) -> LS2 (v1.2.0, the law's gate) ->
|
|
11
|
+
LS3 (v1.3.0, the consumer contract) -> LS4 (v1.4.0, Q8 enablement, GATED).
|
|
15
12
|
|
|
16
13
|
---
|
|
17
14
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
15
|
+
## 0. Ground truth (verified 2026-09-01)
|
|
16
|
+
|
|
17
|
+
- Registry: `@zakkster/lite-stream` 1.0.0 and 1.1.0 published; `latest` =
|
|
18
|
+
1.1.0. Local HEAD `e9d2151 1.1.0;` IS the published state; tree clean.
|
|
19
|
+
- Surface: 4 named exports -- `fromAsyncIterable`, `pipeToSignal`,
|
|
20
|
+
`toAsyncIterable`, `TimeoutError`. NO `VERSION` export (LS-05). `Stream.d.ts`
|
|
21
|
+
and `llms.txt` agree with the export block (hand-checked; guard pending LS2).
|
|
22
|
+
- Tests: 79. Default `npm test` -> 71 pass, **8 skipped silently** (the entire
|
|
23
|
+
GC tier, LS-02). `npm run test:gc` -> 79 pass, 0 skip, 0 fail -- against
|
|
24
|
+
lite-signal **1.5.0** AND against the floor **1.2.2** (both run today; the
|
|
25
|
+
`^1.2.0` peer claim is TRUE, previously untested -- LS-09).
|
|
26
|
+
- Torture: none. No `test/torture.mjs`, no `@zakkster/lite-leak`, no
|
|
27
|
+
`@zakkster/lite-gc-profiler`, no seeded fuzz, no controls. The 8 GC tests
|
|
28
|
+
use coarse "retain < 2 MB" ceilings, not the law's `maxMajor: 0` gate (LS-03).
|
|
29
|
+
- Bench (node v26.3.1, this machine, 2026-09-01): from-latest 61,464 ops/s
|
|
30
|
+
(0.35 B/op retained), from-buffer 63,382 (0.46), abort-cycle 29,652 (1.49),
|
|
31
|
+
pipe-to-signal 42,382 (0.26), to-async-iterable 200,956 (negative retained),
|
|
32
|
+
overflow 204,774 (0.13). Shipped llms.txt claims different numbers with no
|
|
33
|
+
provenance on the 1.0 block (LS-08).
|
|
34
|
+
- Consumers in the suite: exactly one. `../LiteQuery/StreamQuery.js:36`
|
|
35
|
+
imports `pipeToSignal` (peer `^1.0.0`, optional). lite-query also vendors
|
|
36
|
+
a 1.0.0 copy for demos (`demo/vendor/lite-stream.js`).
|
|
37
|
+
- ASCII: **zero** non-ASCII bytes across Stream.js, Stream.d.ts, all docs,
|
|
38
|
+
test/, bench/, demo/. Already clean; LS2's guard keeps it that way.
|
|
39
|
+
- Hygiene: no `.gitignore` (added at roadmap-pass time: `node_modules/`,
|
|
40
|
+
`package-lock.json` -- same operator ruling as lite-query Q1), no lockfile
|
|
41
|
+
(deliberate, now recorded). `files[]` ships ROADMAP.md (LS-11).
|
|
42
|
+
- Runtime facts: engines `>=18`; `Symbol.asyncDispose` feature-detected
|
|
43
|
+
(present on node 26, its conditional test runs). devDep `^1.2.2` resolves
|
|
44
|
+
to lite-signal 1.5.0 at install time.
|
|
45
|
+
|
|
46
|
+
## 1. Shared law (holds in every session)
|
|
47
|
+
|
|
48
|
+
1. Zero runtime deps; `@zakkster/lite-signal` is the single peer. node:test
|
|
49
|
+
only. ASCII-only source and docs. Single main file `Stream.js`.
|
|
50
|
+
2. Bounded buffering is this package's own law: "unbounded buffering is a
|
|
51
|
+
memory bug pretending to be a feature." Every buffer has a required,
|
|
52
|
+
validated ceiling; every drop is counted and observable.
|
|
53
|
+
3. Fail closed on every unverified state. The LS-01 lesson is the local
|
|
54
|
+
phrasing: a cleanup path that depends on a peer behavior nobody ever
|
|
55
|
+
executed is not a cleanup path, it is a comment.
|
|
56
|
+
4. **AbortSignal is the only cleanup contract.** Dispose-based teardown is
|
|
57
|
+
not promised anywhere until a lite-signal disposed-oracle exists (parked).
|
|
58
|
+
Docs, tests, and torture pin this in every direction.
|
|
59
|
+
5. No silent skips (the Q-02/AW-14/LS-02 class): the default `npm test` runs
|
|
60
|
+
every test the repo owns. Environment-conditional skips (asyncDispose on
|
|
61
|
+
node < 20) must name their condition in the skip message and have a
|
|
62
|
+
counterpart that runs.
|
|
63
|
+
6. Every module change is proven by `node --expose-gc test/torture.mjs`
|
|
64
|
+
(lite-leak + lite-gc-profiler) once LS2 lands. No gate output is a FAIL.
|
|
65
|
+
A gate that cannot fail is decorative -- controls are part of the gate.
|
|
66
|
+
7. Docs parity is release-blocking: exports == llms.txt == Stream.d.ts ==
|
|
67
|
+
README API section; peer floors in docs == package.json; perf numbers
|
|
68
|
+
carry provenance (node version, date, machine class) or do not ship.
|
|
69
|
+
8. Measured numbers are measured once, at the source of truth, and cited
|
|
70
|
+
everywhere else (the bake-stream MQ2 discipline).
|
|
71
|
+
|
|
72
|
+
## 2. Verified findings
|
|
73
|
+
|
|
74
|
+
Severity: S1 = a shipped claim is false / correctness; S2 = a gate or
|
|
75
|
+
guarantee is dark; S3 = drift, hygiene, or docs debt. Every row was
|
|
76
|
+
reproduced by running on 2026-09-01; probe bodies live in
|
|
77
|
+
`bench/findings-probes-2026-09-01.mjs` (P0..P4).
|
|
78
|
+
|
|
79
|
+
| ID | Sev | Finding | Reproduce |
|
|
80
|
+
| --- | --- | --- | --- |
|
|
81
|
+
| **LS-01** | S1 | **The shipped dispose-teardown claim is false, and it has never been true.** llms.txt (lines 132-134): "If the consumer disposes the result signal ... the pump detects on next `sig.set()` and tears down silently." Reality: lite-signal's `set()` after `dispose()` is a **silent no-op** in 1.5.0 AND 1.2.2 (P0), so the pump's catch-based detection (`Stream.js:343`) is dead code born dead -- it never fired against any published peer in the range. Observed: dispose mid-pump leaves the iterator **pulling forever** (P1: 14 pulls at dispose, 64 and climbing 60ms later, `return()` never called); same for `pipeToSignal` with a disposed target (P2, `onError` never fires). For an infinite source (SSE -- the package's own headline use case) this is an unbounded background fetch loop. The `Stream.js:148-152` docstring even offers dispose as a stop mechanism ("the iterator continues until natural completion" -- an infinite source has none). Sole suite consumer verified SAFE: StreamQuery always `ac.abort()`s via `streamStop` before entry teardown (`StreamQuery.js:156`). | `node bench/findings-probes-2026-09-01.mjs` (P0/P1/P2) |
|
|
82
|
+
| **LS-02** | S2 | **Default `npm test` silently skips the entire GC tier.** All 8 tests in `test/06-gc.test.mjs` are `{ skip: !hasGc }` and the `test` script lacks `--expose-gc`; 71/79 green LOOKS complete. The `verify` script runs `test:gc`, but the habitual gate is `npm test`. Third suite recurrence of the green-light-over-a-hole class (Q-02, AW-14). Fix pre-verified: the `test:gc` form runs 79/79 green in 21.5s. | `npm test 2>&1 \| grep skipped` -> 8; `npm run test:gc` -> 0 |
|
|
83
|
+
| **LS-03** | S2 | **The pipeline law's torture gate does not exist.** No `test/torture.mjs`, no lite-leak, no lite-gc-profiler, no seeded fuzz, no zero-alloc gate (llms.txt claims "allocates zero on the steady-state hot path" -- nothing gates it), no controls. The GC tests' "retain < 2 MB" ceilings would pass a 1.9 MB leak. | `ls test/torture.mjs` -> ENOENT; `grep -c lite-leak package.json` -> 0 |
|
|
84
|
+
| **LS-04** | S3 | **Disposing the SOURCE under `toAsyncIterable` hangs the pending `next()` forever** (P3: still PENDING 50ms after dispose; no resolve, no reject). llms.txt already anti-patterns this as "undefined behavior", so it is documented-away rather than false -- but the observed behavior is now on record, and `timeout` is the only escape hatch. Hardening parked on a lite-signal disposed-oracle (see parking ledger). | probes P3 |
|
|
85
|
+
| **LS-05** | S3 | **No `VERSION` const or export.** Suite convention (lite-query, lite-await) and the /release drill's site list expect one in the main file. Current version sites: `package.json`, `Stream.js:1` header comment, CHANGELOG headings. Additive export -> lands with LS2 (1.2.0), never re-exported by consumers (suite ruling, lite-await pass). | `grep -c VERSION Stream.js` -> 0 |
|
|
86
|
+
| **LS-06** | S3 | **Shipped source references a document that does not exist.** `Stream.js:25` and `:560` cite `CONTRACT-toAsyncIterable-1.1.md`; no such file in the repo or the tarball. The TimeoutError comment (`Stream.js:44-45`) still narrates its own dev sessions ("Session B's territory ... Session A"). | `ls CONTRACT-toAsyncIterable-1.1.md` -> ENOENT |
|
|
87
|
+
| **LS-07** | S3 | **The pump comment describes code that is not there.** `Stream.js:352-353`: "Defer the next pull through the microtask queue so back-to-back synchronous yields don't blow the stack" -- `pump()` is called directly; no deferral exists at that site. Behavior is stack-safe anyway (`.then` handlers unwind): P4 drains a 1,000,000-value sync source in ~80ms with no overflow. Fix is the comment, plus a torture case pinning the law. | probes P4 |
|
|
88
|
+
| **LS-08** | S3 | **Shipped perf numbers lack provenance and do not match a current run.** llms.txt's 1.0 block ("178K ops/s ... 235K ... 84K") names no node version, date, or machine; today's run on node v26.3.1 gives 61K/42K/29K for the same scenarios. Not a lie -- a rotting number. Re-measure once with provenance at LS1, cite everywhere (README Benchmarks included). | `npm run bench` vs llms.txt Performance section |
|
|
89
|
+
| **LS-09** | S3 | **The peer floor `^1.2.0` was a claim, not a fact -- now verified, must stay verified.** Tests had only ever run against the latest resolution (1.5.0). Run today against pinned 1.2.2: 79/79 green. The floor holds; nothing keeps it holding. LS2's floor matrix (scripted `test:floor`) institutionalizes the check. | `npm i --no-save --no-package-lock @zakkster/lite-signal@1.2.2 && npm run test:gc` |
|
|
90
|
+
| **LS-10** | S3 | **No repo hygiene layer.** No `.gitignore` -- a plain `npm install` leaves `node_modules/` (and would leave `package-lock.json`) as untracked noise poised to be committed; no recorded lockfile policy. Mitigated at roadmap-pass time (2-line `.gitignore`, same ruling as lite-query Q1); policy recorded here: no lockfile, devDeps float within their stated ranges, the floor matrix pins what must stay true. | `git status` after `npm install` (pre-fix) |
|
|
91
|
+
| **LS-11** | S3 | **ROADMAP.md ships in the tarball, and two shipped docs point into its old shape.** `files[]` includes ROADMAP.md; llms.txt's Files section describes it as "deferred features"; CHANGELOG 1.1.0 cites "ROADMAP.md's 'Shipped in 1.1.0' section" (preserved below under History so the published 1.1.0 pointer stays honest). Ruling to execute at LS2: drop ROADMAP.md from `files[]` (align with lite-query/bake-stream -- the roadmap is a working doc), fix the llms.txt Files line and CHANGELOG forward-refs in the same release. | `npm pack --dry-run \| grep ROADMAP` |
|
|
92
|
+
| **LS-12** | S3 | **The two directions disagree about what abort means, and the flagship consumer pays for it.** `fromAsyncIterable`/`pipeToSignal` route abort through `onError` (error framing, `makeAbortError`); `toAsyncIterable` treats abort as graceful done. StreamQuery hand-filters the noise: "pipeToSignal funnels intentional aborts through onError too. Those are not failures" (`StreamQuery.js:139-142`). Each direction is documented locally; the asymmetry as a whole is undocumented and exports a workaround obligation to every consumer. LS3 owns the decision doc (pin the rationale or add an `onAbort` hook -- see the brief). | read `StreamQuery.js:139-142`; compare `Stream.js:261` vs `:815` |
|
|
93
|
+
|
|
94
|
+
### 2b. Cross-package facts (verified against the neighbors' code, not their docs)
|
|
95
|
+
|
|
96
|
+
- **lite-signal 1.5.0 has no public disposed-oracle.** Exports: signal,
|
|
97
|
+
computed, signalBox, computedBox, effect, dispose, batch, createRegistry,
|
|
98
|
+
setDefaultRegistry, CapacityError, getOwner, runWithOwner. Nothing answers
|
|
99
|
+
"is this signal disposed?" -- so LS-01's behavioral fix (pump detects
|
|
100
|
+
dispose) is IMPOSSIBLE in-library today. Tripwire: if lite-signal ships an
|
|
101
|
+
oracle, un-park "dispose-aware pumps" (parking ledger).
|
|
102
|
+
- **StreamQuery's exact usage shape** (the contract LS2's conformance tier
|
|
103
|
+
freezes): `pipeToSignal(source, entry.data, { signal, transform, onError,
|
|
104
|
+
onDone })` where `transform` doubles as a first-value status tap and a
|
|
105
|
+
hand-rolled buffer window (`ring.push/shift/slice`, `StreamQuery.js:119-137`),
|
|
106
|
+
and `onError` filters `signal.aborted`. Three shipped triggering signals for
|
|
107
|
+
LS3, cited by line.
|
|
108
|
+
- **TimeoutError duck-pact.** Three packages now carry the shape
|
|
109
|
+
(`e.name === "TimeoutError"`, `.timeout` field): lite-await (origin),
|
|
110
|
+
lite-stream (structural copy, deliberate zero-dep), lite-query's /await
|
|
111
|
+
entry (re-export of lite-await's). The duck-check IS suite vocabulary;
|
|
112
|
+
renaming it anywhere is a breaking change everywhere. Pinned by a shape
|
|
113
|
+
test in LS2's conformance tier.
|
|
114
|
+
- **lite-bake-stream is bytes-domain** (`ReadableStream<Uint8Array>` in,
|
|
115
|
+
LBK1 containers out) -- no structural integration with value-domain
|
|
116
|
+
lite-stream. One honest recipe candidate, docs-only: RangeReader shard
|
|
117
|
+
walk -> async generator -> `fromAsyncIterable` for a UI-reactive lazy
|
|
118
|
+
view (parking ledger; needs bake-stream >= MQ1 for abortability).
|
|
119
|
+
- **lite-query Q8 (2.0.0, shared streams)** is the design event LS4 waits
|
|
120
|
+
for: leader owns one iterator, followers receive frames over lite-channel
|
|
121
|
+
and need "push externally-delivered values into entry signals with the
|
|
122
|
+
same windowed-view discipline", plus "leader hung (frames stop, channel
|
|
123
|
+
alive)" stall detection. Whether those primitives land in lite-stream 1.4.0
|
|
124
|
+
or inside lite-query is Q8's session-start spike ruling. Their spike is
|
|
125
|
+
our tripwire; recorded on both sides.
|
|
126
|
+
|
|
127
|
+
## 3. The torture suite (`test/torture.mjs`) -- spec
|
|
128
|
+
|
|
129
|
+
Model: `../LiteBakeStream/ROADMAP.md` section 3 (itself ported from
|
|
130
|
+
`../LiteBvh/test/torture/`), tiers renamed to this package's threat model:
|
|
131
|
+
lifecycle leaks, protocol violations, scheduling races, and false zero-GC
|
|
132
|
+
claims. Gate: `node --expose-gc test/torture.mjs` prints exactly "ok",
|
|
133
|
+
exit 0/1. Control switch: `STREAM_TORTURE_BREAK=1` must exit non-zero.
|
|
79
134
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
```
|
|
136
|
+
test/
|
|
137
|
+
torture.mjs # entry: tiers in order, prints "ok", exit 0/1
|
|
138
|
+
torture/
|
|
139
|
+
harness.mjs # xorshift32 PRNG (TORTURE_SEED replay), check() with
|
|
140
|
+
# message thunks, runOpsGate (stabilize:'deep'),
|
|
141
|
+
# STREAM_TORTURE_BREAK switch, preflight: exit 2 with
|
|
142
|
+
# a clear message when devDeps are missing
|
|
143
|
+
s0-laws.mjs # metamorphic invariants
|
|
144
|
+
s1-degenerate.mjs # nasty values and nasty iterators
|
|
145
|
+
s2-protocol.mjs # async-iteration protocol conformance
|
|
146
|
+
s3-abuse.mjs # API misuse: every case has a decided policy
|
|
147
|
+
s4-fuzz.mjs # seeded schedule fuzz vs a naive oracle
|
|
148
|
+
s5-alloc.mjs # the zero/bounded-alloc gate (gc-profiler)
|
|
149
|
+
s6-soak.mjs # lite-leak retention + lifecycle churn
|
|
150
|
+
s7-conformance.mjs # consumer corpus + floor matrix + duck-pact
|
|
151
|
+
s8-controls.mjs # every tier, deliberately broken, exits non-zero
|
|
152
|
+
```
|
|
87
153
|
|
|
88
|
-
|
|
89
|
-
|
|
154
|
+
Harness rules (verbatim from the blueprint; all apply):
|
|
155
|
+
- All scratch allocated once, outside every loop; assertion messages built
|
|
156
|
+
only on failure.
|
|
157
|
+
- Seeded xorshift32; any failure prints seed + op index, replayable via
|
|
158
|
+
`TORTURE_SEED=... node --expose-gc test/torture.mjs`.
|
|
159
|
+
- lite-gc-profiler is one-measurement-at-a-time; tiers run sequentially;
|
|
160
|
+
read `../lite-gc-profiler/llms.txt` before wiring -- rule names from
|
|
161
|
+
memory are how gates rot. Unknown rule keys throw as of 1.10.0; floor the
|
|
162
|
+
devDep there.
|
|
163
|
+
- lite-leak held-value contract: neither `cleanup` nor `tag` closes over the
|
|
164
|
+
tracked target. Never resolve an unexpected `inconclusive` with
|
|
165
|
+
`allowInconclusive`.
|
|
166
|
+
- `test/` never enters `files[]`; `npm pack --dry-run` proves it.
|
|
167
|
+
|
|
168
|
+
### s0 -- metamorphic laws
|
|
169
|
+
|
|
170
|
+
For any seeded op sequence: `state.count` equals values yielded (both modes);
|
|
171
|
+
buffer `values.length <= maxBuffer` always; `droppedCount` equals exactly
|
|
172
|
+
`max(0, yields - maxBuffer)` when no consumer drains; latest-mode terminal
|
|
173
|
+
`value` equals the last yield; toAsyncIterable delivery order equals source
|
|
174
|
+
order (filtered) in buffer mode; **timing invariance**: the same logical
|
|
175
|
+
value sequence delivered via sync iterables, microtask-resolved promises,
|
|
176
|
+
and macrotask-resolved promises produces the same terminal state (count,
|
|
177
|
+
last value/values, done, error class) -- the scheduling analog of
|
|
178
|
+
bake-stream's re-chunk invariance. Terminal states are frozen: after
|
|
179
|
+
done/error, no further mutation ever (already unit-pinned; fuzz-scale it).
|
|
180
|
+
|
|
181
|
+
### s1 -- degenerate values and hostile iterators
|
|
182
|
+
|
|
183
|
+
Values: `undefined` yields (state.count is the oracle for "did a value
|
|
184
|
+
arrive", value alone is ambiguous -- pin it), null, NaN, -0, frozen objects,
|
|
185
|
+
functions, symbols. Sources: 1M-value sync iterable (stack law, P4 baseline
|
|
186
|
+
~80ms), empty iterable (immediate done), iterator with `.next` but no
|
|
187
|
+
`Symbol.asyncIterator`, thenable-returning next(), next() returning
|
|
188
|
+
non-object (pinned TypeError), result objects with throwing getters
|
|
189
|
+
(`{get done() {throw}}`), `return()` that throws (swallowed -- pin),
|
|
190
|
+
`return()` that never settles, an iterator that resolves a pull AFTER
|
|
191
|
+
stop/abort landed (the `if (stopped)` guards, fuzz-reachable). Aborts:
|
|
192
|
+
pre-aborted (pinned), abort inside the first `queueMicrotask` window before
|
|
193
|
+
pull one, abort reentrantly from inside `onError`/`onDone`, abort with a
|
|
194
|
+
custom `reason` (pinned), double abort.
|
|
195
|
+
|
|
196
|
+
### s2 -- protocol conformance (toAsyncIterable)
|
|
197
|
+
|
|
198
|
+
next() after done; `return(value)` passthrough vs pending waiters resolving
|
|
199
|
+
`{value: undefined, done: true}` (the documented split); `throw()` rejecting
|
|
200
|
+
both waiters and its own call site; multi-waiter FIFO under fuzz (N waiters
|
|
201
|
+
x M values, orders compared to a queue oracle); `Promise.all` batch pulls;
|
|
202
|
+
`for await` + break; `await using` on node >= 20; the emitInitial x filter x
|
|
203
|
+
initial-value matrix (8 cells, each named); filter throwing on the initial
|
|
204
|
+
synchronous fire (the late-binding unsubscribe path, `Stream.js:656-663`).
|
|
205
|
+
|
|
206
|
+
### s3 -- API abuse (throw / documented no-op / documented undefined -- pick one per case)
|
|
207
|
+
|
|
208
|
+
Mode typos ("Latest", "newest", 5); maxBuffer 0 / -1 / 1.5 / Infinity / NaN /
|
|
209
|
+
"5"; `mode: "latest"` + maxBuffer (pinned TypeError); timeout 0 (DECIDE and
|
|
210
|
+
pin: fires on the first turn -> first pending next() rejects TimeoutError),
|
|
211
|
+
timeout Infinity (non-finite -> RangeError, pin), timeout -1; filter
|
|
212
|
+
non-function; non-signal source/target (missing peek, missing subscribe,
|
|
213
|
+
missing set, frozen impostor objects); `stop()` twice (pinned) and `stop()`
|
|
214
|
+
racing a pending pull; dispose-then-X: every LS-01/LS-04 case lands here as
|
|
215
|
+
a NAMED test pinning the documented behavior ("dispose does not stop the
|
|
216
|
+
pump; abort does" / "dispose under a consuming iterable hangs -- documented
|
|
217
|
+
anti-pattern, timeout is the escape").
|
|
218
|
+
|
|
219
|
+
### s4 -- seeded schedule fuzz vs a naive oracle
|
|
220
|
+
|
|
221
|
+
A seeded generator emits op schedules over both directions: producer-set /
|
|
222
|
+
consumer-next / filter-toggle-outcome / abort / break / return / timeout-arm,
|
|
223
|
+
with micro/macro-task jitter. Oracle: a deliberately naive reference model
|
|
224
|
+
(unbounded array + hand bookkeeping of drops/overflow per the documented
|
|
225
|
+
semantics). Compare delivered sequences, terminal states, and counter values
|
|
226
|
+
cell-for-cell. Divergence prints seed + op index + a minimal replay line.
|
|
227
|
+
The multi-waiter FIFO and the ring head/wrap arithmetic are the two
|
|
228
|
+
mechanisms this tier exists to break.
|
|
229
|
+
|
|
230
|
+
### s5 -- the alloc gate (the llms.txt claims, gated at last)
|
|
231
|
+
|
|
232
|
+
`runOpsGate(fn, { ops })` with `RULES = { maxMajor: 0, maxPauseMs: 4 }`,
|
|
233
|
+
stabilize `'deep'`. Scenarios and their DOCUMENTED budgets (the M0 doctrine:
|
|
234
|
+
per-scenario budgets, never a widened global rule):
|
|
235
|
+
- toAsyncIterable `mode:"latest"`, consumer caught up: llms.txt claims ZERO
|
|
236
|
+
steady-state allocation -- gate exactly that (plus the per-next Promise
|
|
237
|
+
cost intrinsic to the protocol, measured and documented once).
|
|
238
|
+
- toAsyncIterable buffer, no waiter pending: same claim, same gate.
|
|
239
|
+
- pipeToSignal steady pump: budget from today's 0.26 B/op retained baseline.
|
|
240
|
+
- fromAsyncIterable latest: one state object per yield BY DESIGN -- assert
|
|
241
|
+
the documented envelope (today 0.35 B/op retained steady-state), assert
|
|
242
|
+
nothing HIDDEN grows (retained slope flat).
|
|
243
|
+
- fromAsyncIterable buffer: state object + snapshot array per yield BY
|
|
244
|
+
DESIGN -- envelope pinned per maxBuffer tier (8 / 64 / 1024).
|
|
245
|
+
- waiter-heavy path (consumer faster than producer): waiter objects are
|
|
246
|
+
intrinsic; retained slope must be flat.
|
|
247
|
+
|
|
248
|
+
### s6 -- soak and retention (lite-leak enters here)
|
|
249
|
+
|
|
250
|
+
4096 cycles per surface of: construct -> pump N values -> terminate (each of
|
|
251
|
+
the three paths in rotation) -> drop refs; tracker with the standard kernels;
|
|
252
|
+
`tracker.size()` returns to 0. Abort-listener accumulation on a SHARED
|
|
253
|
+
AbortSignal across 4096 streams (the removeEventListener discipline at
|
|
254
|
+
scale). Timer discipline: 4096 timeout-armed iterables that settle before
|
|
255
|
+
the deadline -- `clearTimeout` called every time (the llms.txt "negative
|
|
256
|
+
retained" bench hints this is right; prove it). The existing 4K-cycle unit
|
|
257
|
+
leak tests stay in `test/07`; s6 is the deeper per-commit version.
|
|
258
|
+
|
|
259
|
+
### s7 -- conformance: the consumer corpus, the floor, the pact
|
|
260
|
+
|
|
261
|
+
- **StreamQuery corpus**: the exact call shape from `StreamQuery.js:124-154`
|
|
262
|
+
(transform-as-tap + hand ring + abort-filtering onError), run against a
|
|
263
|
+
seeded source, asserting the semantics lite-query depends on: transform
|
|
264
|
+
called once per value BEFORE set, abort fires onError with
|
|
265
|
+
`signal.aborted === true` observable, onDone exactly once, stop idempotent.
|
|
266
|
+
lite-stream must never drift under its flagship consumer silently. When
|
|
267
|
+
LS3 ships the enriched pipeToSignal, the corpus gains the enriched shape
|
|
268
|
+
and KEEPS the 1.1.0 shape (both are public API).
|
|
269
|
+
- **Floor matrix**: `npm run test:floor` -- scripted: install the peer floor
|
|
270
|
+
(`@zakkster/lite-signal@1.2.2`) via `--no-save --no-package-lock`, full
|
|
271
|
+
`test:gc` + torture, restore latest, print both verdicts. Run per release
|
|
272
|
+
(release drill step), not per commit. LS-09 stays a fact, not a claim.
|
|
273
|
+
- **TimeoutError duck-pact**: shape assertions (`name`, `timeout` field,
|
|
274
|
+
`instanceof Error`, message prefix) pinning the cross-package duck-check
|
|
275
|
+
contract (2b). No lite-await devDep -- the pact is structural.
|
|
276
|
+
|
|
277
|
+
### s8 -- controls
|
|
278
|
+
|
|
279
|
+
`STREAM_TORTURE_BREAK=1 node --expose-gc test/torture.mjs` must exit
|
|
280
|
+
non-zero via injected breakage, one per tier minimum: an allocating closure
|
|
281
|
+
in the s5 caught-up loop; an oracle that ignores droppedCount in s4; a
|
|
282
|
+
skipped abort case in s1; a waiter-order swap in s2; a retained-stream ref
|
|
283
|
+
in s6; a corpus assertion inverted in s7. A gate that cannot fail is
|
|
284
|
+
decorative.
|
|
285
|
+
|
|
286
|
+
## 4. Session order
|
|
90
287
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
288
|
+
```
|
|
289
|
+
LS1 (1.1.1 truth patch)
|
|
290
|
+
|
|
|
291
|
+
v
|
|
292
|
+
LS2 (1.2.0 the law's gate + guards + VERSION + packaging)
|
|
293
|
+
|
|
|
294
|
+
v
|
|
295
|
+
LS3 (1.3.0 the consumer contract: pipeToSignal enrichment + abort decision)
|
|
296
|
+
|
|
|
297
|
+
v [tripwire: lite-query Q8 session-start spike rules the split]
|
|
298
|
+
LS4 (1.4.0 Q8 enablement: stall detection / push-writer / share primitives)
|
|
299
|
+
```
|
|
95
300
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
301
|
+
Serial on purpose: LS1's honest docs are the floor LS2's gates stand on;
|
|
302
|
+
LS2's harness is what makes LS3's surface growth safe to ship; LS4 is
|
|
303
|
+
design-gated on lite-query Q8 and MUST NOT jump the queue (building shared-
|
|
304
|
+
stream primitives before the consumer's failover matrix exists would be
|
|
305
|
+
speculative API -- the exact thing the 1.0 deferral discipline refused).
|
|
100
306
|
|
|
101
|
-
|
|
102
|
-
in the roadmap actually wants. Adding alternatives speculatively grows the
|
|
103
|
-
API surface for no measurable benefit.
|
|
307
|
+
## 5. The briefs
|
|
104
308
|
|
|
105
|
-
|
|
106
|
-
|
|
309
|
+
===============================================================================
|
|
310
|
+
# LS1 -- lite-stream v1.1.1 -- the docs tell the truth and the gates run
|
|
311
|
+
===============================================================================
|
|
107
312
|
|
|
313
|
+
```markdown
|
|
108
314
|
---
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
**Why deferred from 1.0:** Doable today by composing iterators externally
|
|
117
|
-
(an async generator that races multiple sources). Built-in support cleans
|
|
118
|
-
up the cancellation story (one AbortSignal cancels all sources) but adds
|
|
119
|
-
complexity.
|
|
120
|
-
|
|
121
|
-
**Triggering signal:** First Twitch consumer that needs to merge >= 2
|
|
122
|
-
async sources into a single reactive surface with shared cancellation.
|
|
123
|
-
|
|
315
|
+
package: "@zakkster/lite-stream"
|
|
316
|
+
version_target: 1.1.1
|
|
317
|
+
status: planned
|
|
318
|
+
tests_min: 85
|
|
319
|
+
skip_max: 1 # only the named asyncDispose environment skip, node < 20
|
|
320
|
+
findings: [LS-01, LS-02, LS-04, LS-06, LS-07, LS-08, LS-10]
|
|
321
|
+
depends_on: []
|
|
124
322
|
---
|
|
125
323
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
324
|
+
# lite-stream -- no shipped sentence the probes can refute
|
|
325
|
+
|
|
326
|
+
PURPOSE
|
|
327
|
+
A patch release with zero API change that makes every shipped claim true
|
|
328
|
+
and every existing test actually run. The package's core positioning is
|
|
329
|
+
"structural cleanup on every termination path"; today one documented path
|
|
330
|
+
is a leak generator. Fix the words now, the machinery in LS2/LS3.
|
|
331
|
+
|
|
332
|
+
TASKS
|
|
333
|
+
- LS-01 docs surgery, all three shipped docs: llms.txt lines 132-134
|
|
334
|
+
replaced with the truth ("disposing the result signal does NOT stop the
|
|
335
|
+
pump; the iterator keeps pulling. AbortSignal or natural completion are
|
|
336
|
+
the only stop mechanisms. See ROADMAP LS-01."); the Stream.js:148-152
|
|
337
|
+
docstring stops offering dispose as a stop mechanism; README cleanup
|
|
338
|
+
section aligned. The dead catch at Stream.js:343-351 STAYS (belt against
|
|
339
|
+
a future lite-signal that throws) but its comment stops claiming it is
|
|
340
|
+
the dispose path and starts naming what it actually is: defensive.
|
|
341
|
+
- LS-01/LS-04 pinned tests (new test/08-dispose-behavior.test.mjs, ~6
|
|
342
|
+
tests): dispose-mid-pump -> pump continues, return() not called (pin the
|
|
343
|
+
TRUE behavior so any future change is a conscious one); same for
|
|
344
|
+
pipeToSignal; dispose-source under toAsyncIterable -> pending next()
|
|
345
|
+
hangs (pin via a bounded race against a sentinel timer); each test cites
|
|
346
|
+
LS-01/LS-04 in its name.
|
|
347
|
+
- LS-02: default gate becomes `"test": "node --expose-gc --test
|
|
348
|
+
--test-reporter=spec"`; `test:gc` kept as an alias for muscle memory;
|
|
349
|
+
`verify` unchanged. Assert in-session: 79 -> 79 pass 0 skip (pre-verified
|
|
350
|
+
2026-09-01: 21.5s).
|
|
351
|
+
- LS-06: delete the two `CONTRACT-toAsyncIterable-1.1.md` references (the
|
|
352
|
+
locked semantics live in llms.txt + this roadmap's s2/s3 tiers now);
|
|
353
|
+
rewrite the TimeoutError comment without the Session A/B narrative.
|
|
354
|
+
- LS-07: fix the pump comment to state the actual mechanism (".then
|
|
355
|
+
handlers unwind the stack; no explicit deferral needed") and cite the 1M
|
|
356
|
+
sync-source probe number.
|
|
357
|
+
- LS-08: re-run `npm run bench`, stamp llms.txt Performance and README
|
|
358
|
+
Benchmarks with node version + date + "your numbers will differ", one
|
|
359
|
+
source of truth (llms.txt), README cites it.
|
|
360
|
+
- LS-10 (partially done at roadmap-pass time): .gitignore landed; record
|
|
361
|
+
the no-lockfile policy line in README's Testing section or CONTRIBUTING
|
|
362
|
+
stub -- one sentence, not a document.
|
|
363
|
+
- CHANGELOG [1.1.1]: Fixed (docs corrections, each naming its finding),
|
|
364
|
+
Added (dispose-behavior pins, gate flag). Facts only.
|
|
365
|
+
|
|
366
|
+
HOT PATH
|
|
367
|
+
Untouched. `git diff -- Stream.js` shows comment-only hunks; reviewer
|
|
368
|
+
proves token-stream identity (the Q2 discipline).
|
|
369
|
+
|
|
370
|
+
ASSERTIONS
|
|
371
|
+
- npm test -> >= 85 pass, 0 fail, skip <= 1 (asyncDispose env skip only,
|
|
372
|
+
named).
|
|
373
|
+
- bench/findings-probes-2026-09-01.mjs output UNCHANGED (the probes pin
|
|
374
|
+
behavior; this release changes words).
|
|
375
|
+
- grep proves no shipped doc contains "tears down" dispose claims; grep
|
|
376
|
+
finds the AbortSignal-only sentence in llms.txt AND README.
|
|
377
|
+
- npm pack --dry-run: no test/, no bench/, no demo/.
|
|
378
|
+
|
|
379
|
+
NON-GOALS
|
|
380
|
+
No API change. No VERSION export (that is additive -> LS2). No torture
|
|
381
|
+
harness (LS2). No behavior change to the pump.
|
|
382
|
+
|
|
383
|
+
DONE WHEN
|
|
384
|
+
Every sentence the probes can check is true, the default gate runs
|
|
385
|
+
everything, and 1.1.1 is published with the corrected llms.txt.
|
|
386
|
+
```
|
|
133
387
|
|
|
134
|
-
|
|
135
|
-
|
|
388
|
+
===============================================================================
|
|
389
|
+
# LS2 -- lite-stream v1.2.0 -- the law's gate, able to fail
|
|
390
|
+
===============================================================================
|
|
136
391
|
|
|
392
|
+
```markdown
|
|
137
393
|
---
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- **`backpressure: "block"` for pipeToSignal** -- await the target
|
|
148
|
-
signal's effects before pulling the next value. Requires effect-
|
|
149
|
-
completion semantics from lite-signal, which don't exist today.
|
|
150
|
-
|
|
151
|
-
- **`opts.equals` for diff-mode signals** -- by default the underlying
|
|
152
|
-
signal uses lite-signal's default `Object.is`. Allow opt-in to a custom
|
|
153
|
-
equality predicate for buffer-mode snapshot arrays (where Object.is
|
|
154
|
-
always reports them as distinct).
|
|
155
|
-
|
|
156
|
-
- **Diagnostic surface** -- `live.stats() -> { yields, drops, errors,
|
|
157
|
-
startedAt, settledAt }` for telemetry. Useful but no consumer asking
|
|
158
|
-
for it yet.
|
|
159
|
-
|
|
394
|
+
package: "@zakkster/lite-stream"
|
|
395
|
+
version_target: 1.2.0
|
|
396
|
+
status: planned
|
|
397
|
+
tests_min: 90
|
|
398
|
+
skip_max: 1
|
|
399
|
+
torture: "s0-s8 ok; STREAM_TORTURE_BREAK=1 fails every tier; floor matrix green"
|
|
400
|
+
findings: [LS-03, LS-05, LS-09, LS-11]
|
|
401
|
+
depends_on: [LS1]
|
|
160
402
|
---
|
|
161
403
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
404
|
+
# lite-stream -- prove the claims, then keep them proven
|
|
405
|
+
|
|
406
|
+
PURPOSE
|
|
407
|
+
Port the suite-law torture harness (bake-stream M0 discipline, tiers per
|
|
408
|
+
section 3), add the drift guards that keep docs/exports/ASCII honest
|
|
409
|
+
(lite-query Q2 discipline), export VERSION, and execute the LS-11
|
|
410
|
+
packaging ruling. After this session the package can prove its own
|
|
411
|
+
headline claims -- including the zero-steady-state-alloc sentence that has
|
|
412
|
+
been unverified since 1.0.0.
|
|
413
|
+
|
|
414
|
+
TASKS
|
|
415
|
+
- test/torture.mjs + test/torture/ per section 3: harness (seeded PRNG,
|
|
416
|
+
check thunks, runOpsGate stabilize:'deep', STREAM_TORTURE_BREAK,
|
|
417
|
+
preflight exit 2 on missing devDeps), tiers s0-s8, controls for every
|
|
418
|
+
tier. devDeps: @zakkster/lite-leak (floor from its llms.txt),
|
|
419
|
+
@zakkster/lite-gc-profiler ^1.10.0 (unknown-rule-keys-throw relied on).
|
|
420
|
+
Read both llms.txt files before wiring; rule names from memory are how
|
|
421
|
+
gates rot.
|
|
422
|
+
- package.json: "torture": "node --expose-gc test/torture.mjs";
|
|
423
|
+
"torture:control": "STREAM_TORTURE_BREAK=1 node --expose-gc
|
|
424
|
+
test/torture.mjs || echo control-failed-as-expected"; "test:floor" per
|
|
425
|
+
s7; "verify" gains torture.
|
|
426
|
+
- Guards (Q2-modeled, ~5 tests): ascii-guard (walk = files[] parsed at
|
|
427
|
+
runtime + test/ + bench/ + demo/; printable ASCII + LF; empty allowlist;
|
|
428
|
+
inline failing control) and surface-guard (exports == llms.txt == d.ts
|
|
429
|
+
export names; peer names in llms.txt exist in peerDependencies; failing
|
|
430
|
+
fixture control). Pre-verified 2026-09-01: repo is already 0 non-ASCII.
|
|
431
|
+
- LS-05: `const VERSION = "1.2.0"` exported from Stream.js; string-equals
|
|
432
|
+
package.json (release-drill site). Never re-exported by consumers
|
|
433
|
+
(suite ruling). Surface-guard and d.ts updated in the same commit.
|
|
434
|
+
- LS-11 ruling executes: ROADMAP.md out of files[]; llms.txt Files section
|
|
435
|
+
updated; CHANGELOG forward-references fixed (the published 1.1.0 tarball
|
|
436
|
+
keeps its pointer -- the History section below preserves the target).
|
|
437
|
+
- LS-09: test:floor scripted and run in-session (pre-verified green
|
|
438
|
+
2026-09-01); result recorded in CHANGELOG with provenance. Peer floor
|
|
439
|
+
STAYS ^1.2.0 because it is now a tested fact.
|
|
440
|
+
- Expected s5 friction, decided in advance (M0 lesson): the per-next
|
|
441
|
+
Promise allocation in toAsyncIterable and the by-design per-yield state
|
|
442
|
+
objects in fromAsyncIterable get per-scenario documented budgets --
|
|
443
|
+
never a widened global rule. A budget that moves is not a gate.
|
|
444
|
+
|
|
445
|
+
ASSERTIONS
|
|
446
|
+
- npm test >= 90 pass, 0 fail, skip <= 1; torture prints "ok" exit 0;
|
|
447
|
+
torture:control exits non-zero FOR EVERY TIER (each control verified
|
|
448
|
+
individually in-session).
|
|
449
|
+
- The llms.txt zero-steady-state-alloc sentence now has a gate (s5) and
|
|
450
|
+
the gate has a control (s8).
|
|
451
|
+
- npm pack --dry-run: ROADMAP.md ABSENT, test/ bench/ demo/ absent,
|
|
452
|
+
llms.txt + CHANGELOG.md present; VERSION === package.json version.
|
|
453
|
+
- Fresh-clone drill: clone to scratch, npm install, npm test, npm run
|
|
454
|
+
torture -- all green (the lite-query G10 habit).
|
|
455
|
+
|
|
456
|
+
NON-GOALS
|
|
457
|
+
No surface growth beyond VERSION. No pipeToSignal enrichment (LS3). No
|
|
458
|
+
behavior changes -- if a tier finds a real bug, it lands as a NAMED
|
|
459
|
+
failing todo + ledger row and ships fixed in LS3, not silently here.
|
|
460
|
+
|
|
461
|
+
DONE WHEN
|
|
462
|
+
the law's command runs from the law's path, every control fails, the
|
|
463
|
+
floor matrix is scripted, and 1.2.0 is published with VERSION exported
|
|
190
464
|
```
|
|
191
465
|
|
|
192
|
-
|
|
193
|
-
|
|
466
|
+
===============================================================================
|
|
467
|
+
# LS3 -- lite-stream v1.3.0 -- the consumer contract session
|
|
468
|
+
===============================================================================
|
|
194
469
|
|
|
470
|
+
```markdown
|
|
195
471
|
---
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
practice; the wrapper state shape already guarantees per-yield
|
|
206
|
-
propagation.
|
|
207
|
-
|
|
472
|
+
package: "@zakkster/lite-stream"
|
|
473
|
+
version_target: 1.3.0
|
|
474
|
+
status: planned
|
|
475
|
+
tests_min: 105
|
|
476
|
+
skip_max: 1
|
|
477
|
+
torture: "ok + s7 corpus extended to the enriched shape"
|
|
478
|
+
findings: [LS-12]
|
|
479
|
+
depends_on: [LS2]
|
|
480
|
+
consumer_evidence: "StreamQuery.js:119-137 (hand ring), :127 (status tap), :139-142 (abort filter)"
|
|
208
481
|
---
|
|
209
482
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
483
|
+
# lite-stream -- what its flagship consumer already built by hand, offered properly
|
|
484
|
+
|
|
485
|
+
PURPOSE
|
|
486
|
+
Every addition in this session has a shipped consumer code-site as its
|
|
487
|
+
triggering signal -- the 1.0 deferral discipline, satisfied for real.
|
|
488
|
+
lite-query's StreamQuery hand-rolls three things around pipeToSignal:
|
|
489
|
+
a buffer window (push/shift/slice -- O(n) shift where our ring is O(1)),
|
|
490
|
+
a first-value status tap smuggled through `transform`, and an
|
|
491
|
+
abort-filtering onError. Offer all three natively; let the consumer
|
|
492
|
+
delete code.
|
|
493
|
+
|
|
494
|
+
TASKS
|
|
495
|
+
- pipeToSignal enrichment (design settled in-session by the planner,
|
|
496
|
+
candidates): `mode: "latest" | "buffer"` + `maxBuffer` + a
|
|
497
|
+
droppedCount observable (return-object upgrade or stats getter --
|
|
498
|
+
decide against the "returns stop fn" 1.x compat constraint: the stop
|
|
499
|
+
FUNCTION must remain callable as before; additive properties on it are
|
|
500
|
+
the compat-safe shape), and `onValue(v)` tap firing before set (the
|
|
501
|
+
status-tap consumer). All additive; 1.1.0 call shapes byte-compatible
|
|
502
|
+
(s7 corpus keeps both shapes green).
|
|
503
|
+
- LS-12 decision doc, decisions/0001-abort-vocabulary.md (the lite-await
|
|
504
|
+
decisions/ convention): EITHER pin the asymmetry as deliberate with the
|
|
505
|
+
producer/consumer rationale (from-side: abort interrupts production =
|
|
506
|
+
error framing; to-side: abort stops listening = graceful done) and
|
|
507
|
+
document it in llms.txt as a contract table -- OR add `onAbort`
|
|
508
|
+
callbacks so consumers stop filtering. Recommendation: PIN the
|
|
509
|
+
asymmetry + add `onAbort` to pipeToSignal only (the direction with the
|
|
510
|
+
shipped workaround); do not touch toAsyncIterable's graceful-done
|
|
511
|
+
contract (1.0.0-locked, correct for its side).
|
|
512
|
+
- s7 corpus: add the enriched-shape cases; keep the 1.1.0-shape cases
|
|
513
|
+
forever.
|
|
514
|
+
- Cross-repo rider recorded (not executed here): lite-query parking entry
|
|
515
|
+
exists pointing at this session -- after 1.3.0 ships, StreamQuery can
|
|
516
|
+
collapse its ring + tap + filter into the enriched call in a lite-query
|
|
517
|
+
minor, and refresh its vendored demo copy (still 1.0.0).
|
|
518
|
+
|
|
519
|
+
ASSERTIONS
|
|
520
|
+
- Old-shape s7 corpus green UNCHANGED against the new build (no drift
|
|
521
|
+
under the flagship consumer).
|
|
522
|
+
- A StreamQuery-shaped rewrite using the enriched surface (as a TEST, in
|
|
523
|
+
this repo) produces identical observable semantics to the hand-rolled
|
|
524
|
+
original, including droppedCount parity on overflow.
|
|
525
|
+
- Suite >= 105, 0 fail; torture ok; guards green; VERSION bumped.
|
|
526
|
+
|
|
527
|
+
NON-GOALS
|
|
528
|
+
No fromAsyncIterable changes. No toAsyncIterable changes. No shared/
|
|
529
|
+
multicast primitives (LS4, gated). No combinators (never -- History).
|
|
530
|
+
|
|
531
|
+
DONE WHEN
|
|
532
|
+
lite-query COULD delete StreamQuery.js:119-137 without behavior change,
|
|
533
|
+
and 1.3.0 is published with the decision doc on disk
|
|
534
|
+
```
|
|
213
535
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`withTimeout`, `withAbort`, and a plain `for` loop. Every Twitch SDK
|
|
218
|
-
endpoint has different retry semantics; a single policy would be
|
|
219
|
-
opinionated wrong for half of them.
|
|
220
|
-
- **WebSocket / EventSource adapters.** Those belong in the SDK that
|
|
221
|
-
consumes them (lite-twitch-ebs, lite-twitch-pubsub), not in lite-stream.
|
|
222
|
-
- **Concurrent multi-iterator orchestration beyond `mergeIterables`** --
|
|
223
|
-
use `lite-await`'s `allOf` / `anyOf` / `raceOf` for that.
|
|
536
|
+
===============================================================================
|
|
537
|
+
# LS4 -- lite-stream v1.4.0 -- Q8 enablement (GATED, do not start on a hunch)
|
|
538
|
+
===============================================================================
|
|
224
539
|
|
|
540
|
+
```markdown
|
|
541
|
+
---
|
|
542
|
+
package: "@zakkster/lite-stream"
|
|
543
|
+
version_target: 1.4.0
|
|
544
|
+
status: gated
|
|
545
|
+
gate: "lite-query Q8 session-start spike has ruled which primitives land here"
|
|
546
|
+
tests_min: 120
|
|
547
|
+
depends_on: [LS3]
|
|
225
548
|
---
|
|
226
549
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
550
|
+
# lite-stream -- the primitives a one-connection browser needs, if Q8 wants them here
|
|
551
|
+
|
|
552
|
+
PURPOSE
|
|
553
|
+
lite-query 2.0 (Q8) makes one tab own an upstream iterator while N tabs
|
|
554
|
+
consume frames. Three primitive-shaped needs are visible from Q8's brief
|
|
555
|
+
today; WHICH of them belong in lite-stream (vs inside lite-query, vs
|
|
556
|
+
nowhere) is Q8's session-start spike ruling. This brief is a menu, not a
|
|
557
|
+
commitment.
|
|
558
|
+
|
|
559
|
+
CANDIDATE MENU (each with its Q8 obligation)
|
|
560
|
+
- Idle/stall detection: an `idleTimeout` that resets per value (today's
|
|
561
|
+
`timeout` is an overall deadline -- wrong tool for "leader hung, frames
|
|
562
|
+
stopped, channel alive"). Smallest candidate, clearest fit.
|
|
563
|
+
- Push-writer surface: values that arrive by callback (channel frames),
|
|
564
|
+
not by iterator pull, entering a signal with the same mode/maxBuffer/
|
|
565
|
+
droppedCount discipline (the follower side's projection). Shape TBD --
|
|
566
|
+
possibly just documenting the enriched pipeToSignal over a hand-made
|
|
567
|
+
async queue, possibly a real `createSignalWriter`.
|
|
568
|
+
- Share/refcount: one source iterator, N signal consumers, last-consumer
|
|
569
|
+
teardown. The most API-heavy candidate and the most likely to belong
|
|
570
|
+
INSIDE lite-query next to its cache entries instead -- do not build
|
|
571
|
+
speculatively.
|
|
572
|
+
|
|
573
|
+
ASSERTIONS (floor, if the gate opens)
|
|
574
|
+
Whatever ships: torture tiers extended same-session (a primitive without
|
|
575
|
+
its soak + fuzz + controls does not ship); Q8's failover matrix consumes
|
|
576
|
+
the new surface in the same quarter, or the surface is reverted before
|
|
577
|
+
2.0 -- an enabler nobody enabled is speculative API with a version number.
|
|
578
|
+
|
|
579
|
+
DONE WHEN
|
|
580
|
+
the Q8 ruling is recorded here (menu -> commitments), and either 1.4.0
|
|
581
|
+
ships what Q8 consumes, or this session closes as "not needed in
|
|
582
|
+
lite-stream" with the reasoning -- both are wins
|
|
583
|
+
```
|
|
235
584
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
585
|
+
## 6. How to run it
|
|
586
|
+
|
|
587
|
+
In order: LS1 -> LS2 -> LS3 -> LS4 (gated). Pipeline per session: planner ->
|
|
588
|
+
coder -> reviewer -> qa; reviewer REJECTED goes back to coder. `status:
|
|
589
|
+
planned -> shipped` in the brief's frontmatter on publish, with the /release
|
|
590
|
+
drill (fail closed; the drill owns version bumps; VERSION site list from
|
|
591
|
+
LS2 on: package.json, Stream.js header, VERSION const, llms.txt if it ever
|
|
592
|
+
gains a version string).
|
|
593
|
+
|
|
594
|
+
Habits this roadmap is built around (inherited, locally re-proven):
|
|
595
|
+
1. **Reproduce by running, never infer.** Every ledger row above has a
|
|
596
|
+
command. LS-01 existed for two shipped versions because a comment was
|
|
597
|
+
trusted over a run.
|
|
598
|
+
2. **A green suite that skips its GC tier is a green light over a hole.**
|
|
599
|
+
Fourth suite recurrence (Q-02, AR-02, AW-14, LS-02). The fix is always
|
|
600
|
+
the same: the default gate runs everything.
|
|
601
|
+
3. **Claims about a peer are facts about a VERSION of a peer.** The dispose
|
|
602
|
+
assumption was never true of any version; the floor claim happened to be
|
|
603
|
+
true of all of them. Neither status was known until today. The floor
|
|
604
|
+
matrix and the conformance corpus exist so both stay known.
|
|
605
|
+
4. **Budgets, not widened rules.** By-design allocations (state objects,
|
|
606
|
+
snapshot arrays, protocol promises) get named per-scenario budgets in
|
|
607
|
+
s5; the global maxMajor: 0 rule never widens.
|
|
608
|
+
|
|
609
|
+
## Parking ledger (deferred, with reasons and tripwires)
|
|
610
|
+
|
|
611
|
+
- **Dispose-aware pumps** (the LS-01 behavioral fix): IMPOSSIBLE in-library
|
|
612
|
+
today -- lite-signal 1.5.0 exposes no disposed-oracle (2b). Tripwire:
|
|
613
|
+
lite-signal ships one -> un-park, wire into fromAsyncIterable/pipeToSignal
|
|
614
|
+
teardown, upgrade the LS1 pinned "pump continues" tests into "pump stops"
|
|
615
|
+
tests in the same change. Until then: docs + pins (LS1).
|
|
616
|
+
- **toAsyncIterable dispose-source hardening** (LS-04): same tripwire, same
|
|
617
|
+
fate (pending next() could reject instead of hang).
|
|
618
|
+
- **mapLimit / transform concurrency** on fromAsyncIterable (from the 1.0
|
|
619
|
+
ledger): trigger re-grounded -- two or more SUITE consumers writing the
|
|
620
|
+
same bounded-concurrency wrapper. None exist today (checked: only
|
|
621
|
+
StreamQuery consumes, no concurrency wrapper).
|
|
622
|
+
- **toAsyncIterable overflow policies** (drop-newest / throw / block):
|
|
623
|
+
trigger unchanged -- a consumer demonstrating drop-oldest causes
|
|
624
|
+
observable correctness issues. None known.
|
|
625
|
+
- **mergeIterables / splitSignal**: trigger re-grounded from "first Twitch
|
|
626
|
+
consumer" to "first suite consumer needing >= 2 async sources into one
|
|
627
|
+
reactive surface with shared cancellation". Q8's leader might be that
|
|
628
|
+
consumer -- if so it routes through the LS4 menu, not around it.
|
|
629
|
+
- **fromEventTarget(target, eventName)**: browser-facing sugar; trigger:
|
|
630
|
+
the pattern recurring across suite demos. demo/index.html is the place to
|
|
631
|
+
look first (unaudited against the demo-audit law -- run that skill next
|
|
632
|
+
time the demo is touched).
|
|
633
|
+
- **pipeToSignal backpressure "block"**: requires effect-completion
|
|
634
|
+
semantics lite-signal does not have (2b-class cross-package fact).
|
|
635
|
+
Tripwire: lite-signal ships them.
|
|
636
|
+
- **opts.equals for buffer-mode snapshot dedup**: trigger: a consumer
|
|
637
|
+
demonstrating redundant snapshot propagation mattering in a profile.
|
|
638
|
+
- **stats() diagnostic surface**: superseded in part by droppedCount/
|
|
639
|
+
overflowCount; remainder (yields, startedAt, settledAt) waits for the
|
|
640
|
+
lite-query Q7 devtools feed to define what a stream should report --
|
|
641
|
+
tripwire: Q7 ships and asks.
|
|
642
|
+
- **RangeReader recipe** (bake-stream): docs-only candidate -- lazy shard
|
|
643
|
+
walk -> async generator -> fromAsyncIterable; needs bake-stream >= MQ1
|
|
644
|
+
(abortable range reads) so the AbortSignal story is end-to-end. Tripwire:
|
|
645
|
+
MQ1 ships.
|
|
646
|
+
- **propagate: "always"** and **full combinators (map/filter/debounce/...)**:
|
|
647
|
+
never -- see History; the async-generator composition answer stands.
|
|
648
|
+
|
|
649
|
+
## History (preserved from the 1.0/1.1 roadmap; the shipped 1.1.0 CHANGELOG points here)
|
|
650
|
+
|
|
651
|
+
### Shipped in 1.1.0 -- toAsyncIterable enrichment
|
|
652
|
+
|
|
653
|
+
Six additions, each with the triggering signal that moved it out of
|
|
654
|
+
deferred status: `mode: "latest" | "buffer"` (vocabulary match with
|
|
655
|
+
lite-query's streamQuery + reactive-state consumers); `filter` (removes the
|
|
656
|
+
per-yield wrapper-generator allocation); `timeout` + `TimeoutError` export
|
|
657
|
+
(replaces external AbortController boilerplate; structurally identical to
|
|
658
|
+
lite-await's class, deliberately not imported -- the zero-dep story);
|
|
659
|
+
`Symbol.asyncDispose` on node >= 20 (`await using`); multi-waiter FIFO
|
|
660
|
+
queue (latent 1.0.0 bug: `Promise.all([iter.next(), iter.next()])` silently
|
|
661
|
+
lost the first resolver); `overflowCount` (mode-neutral alias for
|
|
662
|
+
`droppedCount`). Full details: CHANGELOG.md [1.1.0].
|
|
663
|
+
|
|
664
|
+
### Non-goals carried forward verbatim
|
|
665
|
+
|
|
666
|
+
No standalone scheduler; no retry/backoff policy (compose lite-await);
|
|
667
|
+
no WebSocket/EventSource adapters (they belong to the consuming SDK); no
|
|
668
|
+
ReactiveX combinator surface (async generators are the composition story --
|
|
669
|
+
inspectable, debuggable, cancellation-transparent); no multi-iterator
|
|
670
|
+
orchestration beyond a potential mergeIterables (lite-await's allOf/anyOf/
|
|
671
|
+
raceOf own that).
|
|
672
|
+
|
|
673
|
+
### Versioning policy (unchanged)
|
|
674
|
+
|
|
675
|
+
Strict semver. 1.x additive only (new exports, new opts fields, new
|
|
676
|
+
state-shape fields); breaking waits for 2.0; patches are fixes/docs only.
|
|
677
|
+
Every addition lands with: a concrete suite consumer, full-lifecycle tests
|
|
678
|
+
(happy + all termination paths), a bench with provenance, and a CHANGELOG
|
|
679
|
+
line with a hot-path-cost note.
|
|
680
|
+
|
|
681
|
+
## Progress log
|
|
682
|
+
|
|
683
|
+
- 2026-09-01 -- Roadmap enriched to blueprint form. Ground truth verified by
|
|
684
|
+
running: 79 tests (71+8 dark under default gate; 79/79 under test:gc,
|
|
685
|
+
against lite-signal 1.5.0 and 1.2.2 both), bench re-run with provenance,
|
|
686
|
+
five probes written and committed (bench/findings-probes-2026-09-01.mjs).
|
|
687
|
+
Ledger LS-01..LS-12 opened; LS-01 (dispose forever-pump, llms.txt claim
|
|
688
|
+
false since 1.0.0) is the headline. .gitignore added (LS-10 mitigation,
|
|
689
|
+
lite-query Q1 ruling). Cross-package wiring recorded on both sides:
|
|
690
|
+
lite-query Q8 spike <-> LS4 gate; LS3 <-> StreamQuery hand-ring rider.
|
|
691
|
+
Sessions LS1..LS4 defined. Nothing else changed; the tree still builds
|
|
692
|
+
and tests exactly as published 1.1.0.
|
|
693
|
+
|
|
694
|
+
- 2026-09-01 -- LS1 (v1.1.1 truth patch) landed in the working tree. LS-01:
|
|
695
|
+
the dispose-teardown claim corrected in llms.txt, README (two sites), the
|
|
696
|
+
Stream.js fromAsyncIterable + pipeToSignal docstrings, and the package.json
|
|
697
|
+
description; the defensive set() catch and its forward comment re-framed as
|
|
698
|
+
peer-defense (not disposal detection), and the LS-07 pump comment corrected
|
|
699
|
+
to "direct call, no deferral" -- the Stream.js diff is comment-only and the
|
|
700
|
+
code-line stream is byte-identical (md5 unchanged, exports unchanged). LS-06:
|
|
701
|
+
the three phantom CONTRACT-toAsyncIterable-1.1.md references and the
|
|
702
|
+
Session A/B narrative removed from Stream.js and test/07. LS-08: bench
|
|
703
|
+
re-measured with provenance (node v26.3.1, macOS arm64, 150ms warmup /
|
|
704
|
+
500ms runs) and consolidated as llms.txt Performance (the single source of
|
|
705
|
+
truth); README now cites it with no ops/s digits; the four unreproducible
|
|
706
|
+
1.1 bench scenarios deleted from the docs (no runnable source in the repo).
|
|
707
|
+
LS-02: --expose-gc added to the default npm test so the GC tier gates on
|
|
708
|
+
every run. Added test/08-dispose-behavior.test.mjs (6 pins, LS-01/LS-04);
|
|
709
|
+
LS-10 no-lockfile sentence added to the README. Measured: npm test 85 pass
|
|
710
|
+
/ 0 fail / 0 skipped; probes P0-P4 structurally unchanged. The LS1 brief's
|
|
711
|
+
`status:` is left at planned -- it flips at publish, not here.
|