@zakkster/lite-signal 1.2.1 → 1.2.2
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 +243 -99
- package/README.md +197 -165
- package/Signal.d.ts +21 -21
- package/Signal.js +87 -80
- package/llms.txt +130 -74
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,63 +4,207 @@ All notable changes to `@zakkster/lite-signal` are documented here.
|
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
-
## [1.2.
|
|
7
|
+
## [1.2.2] -- 2026-06-16
|
|
8
|
+
|
|
9
|
+
A code-deletion ship: a `createNode` audit removes ten redundant field-writes
|
|
10
|
+
that defended against a state the engine cannot produce on a clean free-list.
|
|
11
|
+
No public surface change, no semantic change, no new tests required for new
|
|
12
|
+
behavior (because there is none) -- only an added invariant suite that pins
|
|
13
|
+
the cleanliness claim the audit relies on. Drop-in over 1.2.1.
|
|
14
|
+
|
|
15
|
+
**Version lineage note.** This is the engine previously labeled `1.2.3` in dev.
|
|
16
|
+
Renumbered to `1.2.2` to keep semver tidy: the deletion is small, isolated, and
|
|
17
|
+
intentionally non-behavioral; bumping the patch rather than the minor reflects
|
|
18
|
+
that. The upcoming `1.3.0` (lazy/chunked pool with `prealloc:"eager"` default
|
|
19
|
+
and intrusive mark stack) carries the next minor bump.
|
|
20
|
+
|
|
21
|
+
### Changed -- clean free-list invariant audit in `createNode`
|
|
22
|
+
|
|
23
|
+
Two clusters of redundant writes removed. Both rely on a single invariant:
|
|
24
|
+
**every node leaving the pool has the listed fields at their fresh-construct
|
|
25
|
+
default values** because `disposeNode` and `runCleanup` already null them and
|
|
26
|
+
the `ReactiveNode` constructor initializes them to the same values on
|
|
27
|
+
fresh-pool-growth allocation.
|
|
28
|
+
|
|
29
|
+
- **Seven graph/batch fields** no longer rewritten on every allocate:
|
|
30
|
+
`headDep`, `tailDep`, `headSub`, `tailSub`, `revertEpoch`, `preBatchValue`,
|
|
31
|
+
`preBatchVersion`. Paired-checked: `disposeNode` clears all seven on the
|
|
32
|
+
recycle path; the `ReactiveNode` constructor inits all seven to the same
|
|
33
|
+
values on the fresh-allocation path used by pool growth.
|
|
34
|
+
- **Three owner-tree fields** in the non-adoption path no longer rewritten:
|
|
35
|
+
the `firstOwned = null`, the adoption-path `prevOwned = null`, and the
|
|
36
|
+
else-branch `owner = null`. The `disposeNode` direct path nulls
|
|
37
|
+
`owner / prevOwned / nextOwned`; the `runCleanup` cascade path nulls them
|
|
38
|
+
on every disposed child and sets the parent's `firstOwned = null` at exit.
|
|
39
|
+
The constructor inits all four owner-tree fields to `null` on
|
|
40
|
+
fresh-allocation.
|
|
41
|
+
|
|
42
|
+
What `createNode` still writes are the *lifetime* writes for the new resident:
|
|
43
|
+
`value`, `flags`, `id`, and the three fields `disposeNode` does NOT touch
|
|
44
|
+
(`version`, `evalVersion`, `markEpoch` -- propagation state that must reset for
|
|
45
|
+
the new lifetime), plus the conditional owner-adoption splice
|
|
46
|
+
(`owner`, `nextOwned`, parent chain link).
|
|
47
|
+
|
|
48
|
+
### Added -- `test/10-free-list-invariant_test.mjs`
|
|
49
|
+
|
|
50
|
+
A three-test invariant suite that asserts the audit's claim by inspecting
|
|
51
|
+
freshly-allocated nodes' underlying field state (via the documented
|
|
52
|
+
`describe()` -> `NODE_PTR` surface, the same protocol devtools uses). Tests:
|
|
53
|
+
|
|
54
|
+
- Recycled slot reports null `headDep/tailDep/headSub/tailSub` and zero
|
|
55
|
+
`revertEpoch/preBatchVersion`, undefined `preBatchValue`, after disposing
|
|
56
|
+
a real signal->computed->effect graph.
|
|
57
|
+
- Recycled slot reports null `owner/prevOwned/nextOwned/firstOwned` after an
|
|
58
|
+
owner-cascade tears down a nested observer tree.
|
|
59
|
+
- Mixed-pattern churn (simple, batched-write, error-flush) leaves no dirty
|
|
60
|
+
state on the free list across 32 follow-up allocations.
|
|
61
|
+
|
|
62
|
+
If any future change reintroduces a write to a clean-state field on the
|
|
63
|
+
dispose path or removes a write that turns out NOT to be redundant, this
|
|
64
|
+
suite catches it.
|
|
65
|
+
|
|
66
|
+
### Added -- `test/11-devtools-contract_test.mjs`
|
|
67
|
+
|
|
68
|
+
A 12-test smoke probe of the introspection surface that lite-devtools 1.1 /
|
|
69
|
+
lite-studio 1.1 consume. Verifies handle resolution + walkers, owner-tree
|
|
70
|
+
walkers, the `onGraphMutation` push hook, the `observeObservers` ghost
|
|
71
|
+
contract (zero added nodes under heavy introspection), and pins the
|
|
72
|
+
authoritative 1.2.x `stats()` shape (exactly 8 keys: `signals`, `computeds`,
|
|
73
|
+
`effects`, `activeNodes`, `activeLinks`, `pooledLinks`, `nodePoolCapacity`,
|
|
74
|
+
`linkPoolCapacity`). Also pins the absence of `totalAllocations` /
|
|
75
|
+
`totalDisposals` / `poolGrowths` on 1.2.x -- those are reserved for 1.4.0 and
|
|
76
|
+
the test fails if they appear early.
|
|
77
|
+
|
|
78
|
+
### Verified
|
|
79
|
+
|
|
80
|
+
- **408 tests total: 398 pass, 10 skip, 0 fail** across the 23 active
|
|
81
|
+
suites (01-09 baseline + 11-23 introspection/ownership/perf-pin + my new
|
|
82
|
+
25 devtools-real-boot + 26 free-list-invariant). The 10 skips are 9
|
|
83
|
+
signalBox-staged-for-1.5.0 in `24-signalbox` and 1 architecturally-N/A
|
|
84
|
+
SSR case in `17-reactivity`.
|
|
85
|
+
- **Coverage on `Signal.js`: 100% statements, 98.43% branches, 100%
|
|
86
|
+
functions, 100% lines.** `Watch.js`: 100% across all four. (c8@11,
|
|
87
|
+
Node 22.) Better branch coverage than the 1.2.1 baseline documented in
|
|
88
|
+
`llms.txt` (was 98.07%); the engine path that closed the gap was a
|
|
89
|
+
targeted test of the swallow-on-self-dispose-then-throw branch in
|
|
90
|
+
`pullComputed`. The remaining ~5 unreached branches are exactly the
|
|
91
|
+
unreachable-by-construction cases already catalogued as `/* c8 ignore */`
|
|
92
|
+
candidates in `COVERAGE-NOTES.md` (cursor fast path, batch wraparound
|
|
93
|
+
sentinel, etc.).
|
|
94
|
+
- **Devtools 1.1.0 + Studio 1.1.0 contract: green.** Test
|
|
95
|
+
`25-devtools-real-boot` boots the actual `Devtools.js` against the
|
|
96
|
+
1.2.2 engine and exercises all 19 exports + the 10 symbols Studio
|
|
97
|
+
imports from Devtools. The ghost contract holds (heavy introspection
|
|
98
|
+
adds zero nodes). One real test-rig finding surfaced during this work:
|
|
99
|
+
if the engine is developed in a repo whose own `package.json` declares
|
|
100
|
+
`name: "@zakkster/lite-signal"`, importing the package by name from the
|
|
101
|
+
project root can resolve to a different module instance than imports
|
|
102
|
+
from inside a sibling `node_modules/@zakkster/lite-devtools/`,
|
|
103
|
+
fragmenting the `defaultRegistry`. This is purely a dev-loop / test-rig
|
|
104
|
+
matter (not an engine, devtools, or studio bug) -- in a real consumer
|
|
105
|
+
installation both packages live in `node_modules` and resolve once.
|
|
106
|
+
|
|
107
|
+
### Not changed
|
|
108
|
+
|
|
109
|
+
- Public API: no additions, no removals, no signature changes.
|
|
110
|
+
- Type surface: `Signal.d.ts` unchanged.
|
|
111
|
+
- Behavior: every existing test case in 01-09 passes unmodified.
|
|
112
|
+
|
|
113
|
+
### Honest notes
|
|
114
|
+
|
|
115
|
+
- **Perf**: no microbench numbers cited. Ten removed field writes per
|
|
116
|
+
`createNode` is a real saving on creation-heavy workloads, but creation
|
|
117
|
+
cost is dominated by the owner-adoption splice and the optional mutation
|
|
118
|
+
hook, not by these writes. Any "X% faster on creation" claim would need
|
|
119
|
+
to come from a per-run lite-vs-alien measurement on the project's
|
|
120
|
+
standard benchmark harness; this ship does not include one because the
|
|
121
|
+
audit is justified by correctness (clean invariant beats defensive
|
|
122
|
+
writes) rather than measured speedup.
|
|
123
|
+
- **Differential testing**: the retracking difftest harness expects two
|
|
124
|
+
engine builds (REF = prior shipped, CANDIDATE = under review). This ship
|
|
125
|
+
was validated against itself (30,000 writes, 0 disagreements), which
|
|
126
|
+
proves determinism but not 1.2.2-vs-1.2.1 behavioral equivalence --
|
|
127
|
+
equivalence is argued instead from the audit being a code-deletion of
|
|
128
|
+
writes provably-redundant under the existing pre/post-invariants, and
|
|
129
|
+
from the full 01-09 suite passing unmodified.
|
|
130
|
+
|
|
131
|
+
### Benchmarks
|
|
132
|
+
|
|
133
|
+
The audit ship does not move the curve on any benchmark -- by design;
|
|
134
|
+
the steady-state hot paths are byte-identical to 1.2.1. The bench
|
|
135
|
+
results were re-measured against 1.2.2 on the project's reference host
|
|
136
|
+
(2016 MacBook Pro, Intel, Node 22) and are published as the baseline
|
|
137
|
+
for the next version. See [`bench/results.txt`](./bench/results.txt)
|
|
138
|
+
(in-house anti-DCE harness, median-of-3 cold-process runs from
|
|
139
|
+
`bench/run-all.sh`), [`bench/resultsReactive.txt`](./bench/resultsReactive.txt)
|
|
140
|
+
(community reactive suite, 10 raw runs), and the third-party
|
|
141
|
+
[js-reactivity-benchmark](https://github.com/volynetstyle/js-reactivity-benchmark)
|
|
142
|
+
results (16 frameworks). Position on the third-party suite: **#4 of 16
|
|
143
|
+
by geomean** (2.05× vs alien-signals 1.00×), behind alien-signals,
|
|
144
|
+
reflex, and @reactively; ahead of Preact Signals (2.09×), uSignal,
|
|
145
|
+
$mol_wire, and 9 others. Outright wins on `manyEffectsFromOneSource`
|
|
146
|
+
and `manySourcesIntoOneComputedEffectWithDirect`; top-3 finishes on
|
|
147
|
+
18 of 47 tests. The version dependencies used for these numbers are
|
|
148
|
+
pinned in [`bench/package.json`](./bench/package.json) at
|
|
149
|
+
`lite-signal-bench@2.2.0`.
|
|
150
|
+
|
|
151
|
+
## [1.2.1] -- 2026-06-12
|
|
8
152
|
|
|
9
153
|
A correctness-and-pauses patch in two halves: the pool allocator stops paying
|
|
10
154
|
for growth in unbounded bursts, and the introspection surface stops lying about
|
|
11
155
|
handles the 1.2.0 owner tree disposed behind your back. Plus the graph-mutation
|
|
12
|
-
hook
|
|
156
|
+
hook -- the keystone that lets lite-devtools 1.1 / lite-studio 1.1 go push-based.
|
|
13
157
|
Drop-in over 1.2.0: 404-test suite green, 177/178 on
|
|
14
158
|
johnsoncodehk/reactive-framework-test-suite (same single open cell, Inner
|
|
15
159
|
Write #179), hot-path regression gate flat on two hosts.
|
|
16
160
|
|
|
17
|
-
### Fixed
|
|
161
|
+
### Fixed -- bounded pool growth (no more construction bursts)
|
|
18
162
|
- Under `onCapacityExceeded: "grow"`, exhausting a pool used to double it by
|
|
19
|
-
synchronously constructing `currentCapacity` fresh nodes/links
|
|
163
|
+
synchronously constructing `currentCapacity` fresh nodes/links -- at a
|
|
20
164
|
524,288-node pool that is a quarter-million 25-field allocations in one
|
|
21
165
|
pause, in whatever frame triggered it. Growth is now incremental: **one**
|
|
22
166
|
node/link constructed per free-list miss, pushed into the pool, recycled
|
|
23
167
|
forever after. The capacity **ledger** still doubles, so `stats()`
|
|
24
168
|
(`nodePoolCapacity` / `linkPoolCapacity` / `pooledLinks`), the
|
|
25
169
|
`maxLinks × 16` ceiling, and every `CapacityError` are bit-identical to
|
|
26
|
-
1.2.0
|
|
170
|
+
1.2.0 -- only the construction schedule changed. Locked by the existing
|
|
27
171
|
`test/03-pool` capacity/ceiling/recycle contracts.
|
|
28
172
|
- Benchmark effect (volynetstyle/js-reactivity-benchmark, same host as the
|
|
29
|
-
1.2.0 baseline run): creation group 489
|
|
30
|
-
cases roughly halved (`1to2` 112
|
|
173
|
+
1.2.0 baseline run): creation group 489 -> 423 ms (-13.5%), with the burst
|
|
174
|
+
cases roughly halved (`1to2` 112 -> 58, `1to8` 113 -> 55, `1to4` 81 -> 54).
|
|
31
175
|
Honest redistribution note: rows that previously *fit inside the doubling
|
|
32
|
-
overshoot* (`createDataSignals` 12.8
|
|
33
|
-
their construction inside the measured window
|
|
176
|
+
overshoot* (`createDataSignals` 12.8 -> 71.9, `1to1` 17.8 -> 43.2) now pay
|
|
177
|
+
their construction inside the measured window -- 1.2.0's overshoot was an
|
|
34
178
|
accidental prefetch, and the same mechanism produced the pathological
|
|
35
179
|
bursts. Bounded pauses are the right trade for real applications; the
|
|
36
180
|
group total still improves.
|
|
37
181
|
- Steady-state hot paths are untouched (update / dynamic-retracking /
|
|
38
182
|
effect-recycle measured flat on both benchmark hosts).
|
|
39
183
|
|
|
40
|
-
### Fixed
|
|
184
|
+
### Fixed -- effect queues / mark stack stay PACKED
|
|
41
185
|
- Pool growth used to pre-size `effectQueueA/B` and the mark stack with
|
|
42
|
-
`arr.length = newCap`
|
|
186
|
+
`arr.length = newCap` -- which permanently converts a PACKED V8 array to
|
|
43
187
|
HOLEY elements, a silent tax on every subsequent flush read. The queues now
|
|
44
188
|
grow by sequential append (packed-preserving, auto-amortised) and
|
|
45
189
|
`destroy()` truncates instead of null-filling to capacity.
|
|
46
190
|
|
|
47
|
-
### Fixed
|
|
191
|
+
### Fixed -- `destroy()` iterates physical pools
|
|
48
192
|
- `destroy()` walked `currentNodesCapacity` slots by index; with incremental
|
|
49
193
|
growth (and any future lazy population) the ledger can exceed the physical
|
|
50
194
|
pool. It now walks `nodePool.length` / `linkPool.length` and is safe on an
|
|
51
195
|
empty pool.
|
|
52
196
|
|
|
53
|
-
### Fixed
|
|
197
|
+
### Fixed -- stale-handle introspection (the owner-tree follow-up)
|
|
54
198
|
- 1.2.0's owner tree made the engine recycle pool slots **autonomously**: an
|
|
55
199
|
owner re-run cascade-disposes its owned observers, so holding a stale handle
|
|
56
200
|
stopped being a user error and became a routine occurrence. The
|
|
57
|
-
introspection surface
|
|
58
|
-
`forEachSource` / `hasObservers` / `observeObservers`
|
|
201
|
+
introspection surface -- `nodeId` / `describe` / `forEachObserver` /
|
|
202
|
+
`forEachSource` / `hasObservers` / `observeObservers` -- still resolved
|
|
59
203
|
`NODE_PTR` without a generation check and would happily report the
|
|
60
204
|
**recycled slot's new resident** (wrong id, wrong value, wrong edges)
|
|
61
205
|
through an old handle. All six entry points now resolve through a
|
|
62
206
|
gen-guarded `liveNode()` and report stale handles as `undefined` (or throw
|
|
63
|
-
the existing `TypeError`, for `observeObservers`)
|
|
207
|
+
the existing `TypeError`, for `observeObservers`) -- the same ABA discipline
|
|
64
208
|
`read()` / `set()` / `dispose()` already had.
|
|
65
209
|
- `describe()` descriptors are now **gen-stamped** alongside the node
|
|
66
210
|
reference, so the documented "descriptors are re-walkable handles" contract
|
|
@@ -69,44 +213,44 @@ Write #179), hot-path regression gate flat on two hosts.
|
|
|
69
213
|
"forEach* descriptors carry id and are re-walkable" test.
|
|
70
214
|
- **Effect dispose handles are now first-class introspection handles.** On
|
|
71
215
|
every prior version, `effect()` returned a bare closure carrying neither
|
|
72
|
-
`NODE_PTR` nor `NODE_GEN`
|
|
216
|
+
`NODE_PTR` nor `NODE_GEN` -- so `describe` / `nodeId` / `forEachSource`
|
|
73
217
|
returned `undefined`/empty for a **live** effect handle, and
|
|
74
218
|
`observeObservers(effectHandle)` threw. The dispose function is now stamped
|
|
75
219
|
with the same symbol pair as signal/computed handles (`NODE_GEN` mirrors the
|
|
76
220
|
disposer's own `birthGen`, so introspection validity agrees exactly with its
|
|
77
221
|
stale-guard). After explicit dispose, slot recycle, or owner-cascade the
|
|
78
222
|
handle correctly reads stale. Measured cost: two property stores per effect
|
|
79
|
-
creation (~50 ns on a create/dispose churn microbench)
|
|
223
|
+
creation (~50 ns on a create/dispose churn microbench) -- symmetric with
|
|
80
224
|
what signal/computed handles already pay, create-path only. Found by the
|
|
81
225
|
lite-devtools 1.1 cross-probe campaign (`track(effectHandle)` threw).
|
|
82
226
|
- `peek()` had the same hole: `sharedSignalPeek` / `sharedComputedPeek`
|
|
83
227
|
resolved the slot ungated, so a stale handle's `peek()` returned the new
|
|
84
228
|
resident's value. Both now gen-check first and return `undefined` when
|
|
85
|
-
stale
|
|
86
|
-
Measured cost: 4M peeks 7.1
|
|
229
|
+
stale -- closing the last unguarded entry point in the probe-c1 ABA family.
|
|
230
|
+
Measured cost: 4M peeks 7.1 -> 7.4 ms (~0.08 ns/op).
|
|
87
231
|
|
|
88
|
-
### Added
|
|
232
|
+
### Added -- `onGraphMutation(fn)`: the graph-mutation hook
|
|
89
233
|
- Registry-level (and default-registry module export) debug hook, the
|
|
90
234
|
connection point for push-based tooling. Single nullable listener; every
|
|
91
235
|
fire point is one `if (mutationHook !== null)` branch and the dispatch is
|
|
92
|
-
allocation-free
|
|
93
|
-
- `1` node create
|
|
94
|
-
- `2` node dispose
|
|
95
|
-
- `3` link add
|
|
96
|
-
- `4` link remove
|
|
97
|
-
- `5` recompute
|
|
236
|
+
allocation-free -- `(opcode, intA, intB)`:
|
|
237
|
+
- `1` node create -- `(id, flags)`, end of `createNode`
|
|
238
|
+
- `2` node dispose -- `(id, flags)`, top of `disposeNode` (cascades included)
|
|
239
|
+
- `3` link add -- `(source.id, target.id)`
|
|
240
|
+
- `4` link remove -- `(source.id, target.id)`
|
|
241
|
+
- `5` recompute -- `(id, 0)`, before an effect re-run / computed re-eval
|
|
98
242
|
- Cost: **zero when unregistered** (hot-path gate flat); registered, the
|
|
99
243
|
worst case measured is +29% on a dynamic-retracking torture loop (11.4M
|
|
100
|
-
events for 400K writes)
|
|
244
|
+
events for 400K writes) -- a debug-mode tax paid only while a consumer is
|
|
101
245
|
attached, proportional to event volume.
|
|
102
|
-
- **Listener contract: observe only
|
|
246
|
+
- **Listener contract: observe only -- never throw, never mutate the graph.**
|
|
103
247
|
The hook fires synchronously inside mutation points; lite-devtools 1.1
|
|
104
248
|
multiplexes all of its consumers behind one registration, isolates their
|
|
105
249
|
exceptions, and unregisters when the last consumer stops (returning the
|
|
106
250
|
engine to the zero-cost state). `onGraphMutation` returns an unsubscribe
|
|
107
251
|
that restores the previously registered listener.
|
|
108
252
|
|
|
109
|
-
### Added
|
|
253
|
+
### Added -- owner-tree introspection: `forEachOwned` / `ownerOf`
|
|
110
254
|
- The 1.2.0 owner tree finally gets a (read-only, gen-guarded) window:
|
|
111
255
|
`forEachOwned(handle, fn)` iterates a node's owned children as standard
|
|
112
256
|
re-walkable descriptors; `ownerOf(handle)` returns the owner's descriptor
|
|
@@ -119,18 +263,18 @@ Write #179), hot-path regression gate flat on two hosts.
|
|
|
119
263
|
### Compatibility
|
|
120
264
|
- No behavioural change for live handles; stale handles now read as stale
|
|
121
265
|
everywhere instead of as the slot's next tenant. Allocation strategy is
|
|
122
|
-
unobservable through the public API. Tooling floor: lite-devtools
|
|
266
|
+
unobservable through the public API. Tooling floor: lite-devtools >= 1.1.0
|
|
123
267
|
detects `onGraphMutation` / `forEachOwned` at load and degrades to its
|
|
124
268
|
1.0 polling behaviour on older engines.
|
|
125
269
|
|
|
126
|
-
## [1.2.0]
|
|
270
|
+
## [1.2.0] -- 2026-06-11
|
|
127
271
|
|
|
128
272
|
A structural refactor that internally splits the engine into three named layers
|
|
129
273
|
(graph topology / ownership-lifecycle / propagation-execution) with a strict
|
|
130
274
|
dependency direction, plus a small set of additive features built on top of
|
|
131
|
-
that split. No behavioural changes for existing code
|
|
275
|
+
that split. No behavioural changes for existing code -- drop-in over 1.1.5.
|
|
132
276
|
|
|
133
|
-
### Added
|
|
277
|
+
### Added -- auto-disposal of nested observers (owner tree)
|
|
134
278
|
- An effect or computed that creates **observers** (nested `effect`/`computed`)
|
|
135
279
|
now owns them via an internal owner tree. When the owner re-runs or is
|
|
136
280
|
disposed, all owned observers are cascade-disposed before the new run starts.
|
|
@@ -144,7 +288,7 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
144
288
|
`test/15-owner-lazy-alloc.test.mjs` (the lite-store cross-wire shape) and
|
|
145
289
|
the new `test/19-v12-additions.test.mjs`.
|
|
146
290
|
|
|
147
|
-
### Added
|
|
291
|
+
### Added -- pre-batch revert (the "set X, set X back" optimisation)
|
|
148
292
|
- Inside a `batch(...)`, if a signal is set and then set back to its
|
|
149
293
|
pre-batch value (under the signal's own `equals`), the version bump is
|
|
150
294
|
reverted and downstream effects/computeds do **not** fire. Eliminates a
|
|
@@ -152,15 +296,15 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
152
296
|
in form state and undo/redo. Verified end-to-end (signal, computed, effect)
|
|
153
297
|
in `test/19-v12-additions.test.mjs`.
|
|
154
298
|
|
|
155
|
-
### Added
|
|
299
|
+
### Added -- multi-effect throws aggregate to `AggregateError`
|
|
156
300
|
- When two or more effects throw in the **same flush pass**, the engine
|
|
157
301
|
collects all errors and rethrows a native `AggregateError` at the
|
|
158
302
|
triggering `set()` / batch boundary. A single thrown error is rethrown
|
|
159
303
|
unwrapped (no change). Effects that don't throw still run. Cycle detection
|
|
160
|
-
unchanged
|
|
304
|
+
unchanged -- a flush exceeding `maxFlushPasses` (default 100) throws an
|
|
161
305
|
`Error` prefixed `"CycleError:"`.
|
|
162
306
|
|
|
163
|
-
### Added
|
|
307
|
+
### Added -- scheduler thunk caching with gen-bound ABA guard
|
|
164
308
|
- `effect(fn, { scheduler })` now caches the scheduler thunk on the node
|
|
165
309
|
itself (`node.schedulerThunk`) so repeated re-schedules reuse the same
|
|
166
310
|
closure (no allocation per re-schedule). The thunk holds a generation snapshot
|
|
@@ -168,14 +312,14 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
168
312
|
generation, so a stale thunk fired by an async scheduler against a recycled
|
|
169
313
|
pool slot is a guaranteed no-op (ABA safe).
|
|
170
314
|
|
|
171
|
-
### Changed
|
|
315
|
+
### Changed -- internal refactor, no behavioural difference
|
|
172
316
|
- The engine is reorganised into three explicit layers with documented
|
|
173
317
|
invariants (see the file header in `Signal.js`):
|
|
174
|
-
- **L1 Graph topology**
|
|
318
|
+
- **L1 Graph topology** -- `allocateLink` / `freeLink` / `severTail`. Pure
|
|
175
319
|
edge mechanics. Never touches `owner` / `firstOwned`.
|
|
176
|
-
- **L2 Ownership/lifecycle**
|
|
320
|
+
- **L2 Ownership/lifecycle** -- `createNode` / `disposeNode` / `runCleanup`.
|
|
177
321
|
Owns the owner tree and user cleanup. Never touches the tracking cursor.
|
|
178
|
-
- **L3 Propagation/execution**
|
|
322
|
+
- **L3 Propagation/execution** -- `markDownstream` (cursor-free), and the
|
|
179
323
|
orchestrators `executeEffect` / `pullComputed` that drive the cursor
|
|
180
324
|
(L1) and call `runCleanup` (L2) before a re-run.
|
|
181
325
|
- `currentObserver` and `currentOwner` are now distinct pointers. Today they
|
|
@@ -184,31 +328,31 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
184
328
|
- **Shared `peek` (perf).** `signal()` and `computed()` now reuse a single
|
|
185
329
|
`peek` function per registry instead of allocating a fresh closure per
|
|
186
330
|
primitive. Equivalent across registries (each registry has its own pair).
|
|
187
|
-
~10
|
|
331
|
+
~10-14% faster signal/computed creation on the `S:create*` micros, no
|
|
188
332
|
hot-path or behavioural change. Verified by 5 dedicated tests + the full
|
|
189
333
|
309-strong existing suite + 30,000-write differential retracking fuzz vs
|
|
190
334
|
the published 1.1.5.
|
|
191
335
|
|
|
192
|
-
### Changed
|
|
193
|
-
- `pullComputed` retains the **`markEpoch` clean short-circuit**
|
|
336
|
+
### Changed -- port-forward of the 1.1.3/1.1.4 perf fixes
|
|
337
|
+
- `pullComputed` retains the **`markEpoch` clean short-circuit** -- re-reading
|
|
194
338
|
a computed after an unrelated source changed is O(1).
|
|
195
|
-
- `allocateLink` retains the **O(1) `tailSub` dedup**
|
|
196
|
-
remains O(N), not O(N
|
|
339
|
+
- `allocateLink` retains the **O(1) `tailSub` dedup** -- divergent re-tracking
|
|
340
|
+
remains O(N), not O(N^2). The same documented edge note applies: a nested
|
|
197
341
|
re-read of the same source after an intervening observer can retain one
|
|
198
342
|
duplicate link per intervening edge, bounded by the loop count and
|
|
199
343
|
dispose-reclaimed.
|
|
200
344
|
|
|
201
|
-
### Fixed
|
|
345
|
+
### Fixed -- conformance regressions surfaced during release prep
|
|
202
346
|
- **#141 (`dispose during execution then continue: no re-run`)**: an effect that
|
|
203
347
|
called its own dispose handle mid-run and then continued to read another
|
|
204
348
|
signal would corrupt the link-list bookkeeping in `severTail` (latent crash
|
|
205
|
-
present in 1.1.5 too
|
|
349
|
+
present in 1.1.5 too -- the v1.2 owner tree exercised the path more
|
|
206
350
|
aggressively and made it visible). Fixed by nulling the tracking cursor in
|
|
207
351
|
`disposeNode` when the disposed node is the active observer, plus a
|
|
208
352
|
gen-snapshot guard in `executeEffect` / `pullComputed` so a post-body
|
|
209
353
|
`severTail` on a recycled slot is skipped.
|
|
210
354
|
- **#238 / #241 / #243 (cleanup ordering)**: nested effect cleanups must fire
|
|
211
|
-
inside-out on owner-tree disposal
|
|
355
|
+
inside-out on owner-tree disposal -- grandchild before child before outer.
|
|
212
356
|
The previous `runCleanup` ran the node's OWN cleanup before cascading, which
|
|
213
357
|
surfaced on cascade-dispose, on owner re-run, AND on the regression path
|
|
214
358
|
where an inner-only re-run had fired first. Fixed by swapping the order:
|
|
@@ -223,18 +367,18 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
223
367
|
- 363 tests / 133 suites total, all passing under `node --expose-gc --test`.
|
|
224
368
|
- **100% line coverage** and **98.62% branch coverage** on `Signal.js`
|
|
225
369
|
+ `Watch.js` (the few uncovered branches are defensive guards: cycle
|
|
226
|
-
detection, batchEpoch wraparound after 2
|
|
227
|
-
`gen` branches added by the conformance fixes
|
|
370
|
+
detection, batchEpoch wraparound after 2^3^2 batches, and the self-dispose
|
|
371
|
+
`gen` branches added by the conformance fixes -- unreachable from
|
|
228
372
|
conformance + existing user code).
|
|
229
373
|
- New file `test/19-v12-additions.test.mjs` (24 tests) locks in shared peek,
|
|
230
374
|
owner adoption rule, pre-batch revert, AggregateError aggregation,
|
|
231
375
|
CycleError detection, the `maxLinks` config branch, the disposed-signal
|
|
232
376
|
read/set behaviour, and the stop-fn ABA guard.
|
|
233
|
-
- New file `test/20-axis-stress.test.mjs` (23 tests)
|
|
377
|
+
- New file `test/20-axis-stress.test.mjs` (23 tests) -- eight orthogonal
|
|
234
378
|
engine-invariant "axes" plus the permanent conformance pins for #141,
|
|
235
379
|
#238, #241, #243.
|
|
236
380
|
- Existing `test/15-owner-lazy-alloc.test.mjs` skips ("scheduler-thunk
|
|
237
|
-
caching lands in v1.2.0") are removed
|
|
381
|
+
caching lands in v1.2.0") are removed -- the owner tree exists, the
|
|
238
382
|
tests pass.
|
|
239
383
|
- Differential retracking fuzz against the published 1.1.5: 30,000 writes,
|
|
240
384
|
**0 disagreements** (`bench/retracking.difftest.mjs`).
|
|
@@ -249,76 +393,76 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
249
393
|
- The "scheduler-thunk caching" hint that referenced an older internal
|
|
250
394
|
staging name (Signal-1.3.0-rc) is gone; the file is the public 1.2.0.
|
|
251
395
|
|
|
252
|
-
## [1.1.5]
|
|
396
|
+
## [1.1.5] -- 2026-06-04
|
|
253
397
|
|
|
254
398
|
Additive release in service of `@zakkster/lite-devtools`: stable node identity on the
|
|
255
399
|
introspection surface, so a tool can dedupe and traverse the full reactive DAG. Drop-in
|
|
256
400
|
over 1.1.4, no breaking changes.
|
|
257
401
|
|
|
258
|
-
### Added
|
|
402
|
+
### Added -- node identity (top-level + per-registry)
|
|
259
403
|
- `nodeId(handle)` -> the node's stable per-allocation id (`number`), or `undefined` for a
|
|
260
404
|
non-handle. The dedupe key for graph walks.
|
|
261
405
|
- `describe(handle)` -> the handle's own `{ id, kind, value }` descriptor, or `undefined`
|
|
262
406
|
for a non-handle. **Re-walkable**: the descriptor may be passed back into
|
|
263
|
-
`forEachObserver`/`forEachSource`
|
|
407
|
+
`forEachObserver`/`forEachSource` -- the recursion primitive for full DAG discovery.
|
|
264
408
|
- `forEachObserver`/`forEachSource` descriptors now carry `id` (`{ id, kind, value }`).
|
|
265
409
|
- Every node gains a stable `id` assigned at allocation: one SMI write at creation, node
|
|
266
410
|
shape kept uniform (monomorphic). **Zero steady-state cost.**
|
|
267
411
|
|
|
268
412
|
### Test suite
|
|
269
|
-
- Added `test/15-identity_test.mjs`: 5 tests
|
|
413
|
+
- Added `test/15-identity_test.mjs`: 5 tests -- ids unique + stable, `nodeId`/`describe`
|
|
270
414
|
undefined on non-handles, descriptor shape `{ id, kind, value }`, descriptors re-walkable,
|
|
271
415
|
identity walks non-perturbing.
|
|
272
416
|
|
|
273
|
-
## [1.1.4]
|
|
417
|
+
## [1.1.4] -- 2026-05-31
|
|
274
418
|
|
|
275
419
|
Combined release: a retracking rewrite that closes the two documented chaotic
|
|
276
420
|
read-order limitations, plus an observer-lifecycle introspection surface. No
|
|
277
|
-
breaking changes, no public-API removals
|
|
421
|
+
breaking changes, no public-API removals -- drop-in over 1.1.3. (This release
|
|
278
422
|
folds in the work that was internally staged as 1.1.4 and 1.1.5; it ships as a
|
|
279
423
|
single 1.1.4.)
|
|
280
424
|
|
|
281
|
-
### Changed
|
|
425
|
+
### Changed -- performance (retracking, no semantic change)
|
|
282
426
|
- **Version-stamped O(1) reconciliation + clean-read short-circuit.** The cursor
|
|
283
427
|
reconciliation now stamps each source per evaluation and a `markEpoch` guard
|
|
284
428
|
short-circuits the pull when a subtree is already clean. This replaces the
|
|
285
429
|
prior strategy's O(N)-per-dep degradation under chaotic, high-fan-in, batched
|
|
286
430
|
read-after-write (every read re-validating its dependency subtree). Stable
|
|
287
|
-
read order is unchanged
|
|
431
|
+
read order is unchanged -- still O(1) per dep via cursor reuse, still zero-alloc.
|
|
288
432
|
- **Result.** The two rows that were the documented v1.1.x limitation flipped from
|
|
289
433
|
multiples-behind to ahead of `alien-signals`, and are now the fastest of the five
|
|
290
434
|
benchmarked frameworks:
|
|
291
|
-
- `dyn: large web app` 6194ms
|
|
292
|
-
- `dyn: wide dense` 5115ms
|
|
435
|
+
- `dyn: large web app` 6194ms -> **571ms** (~10.9× faster; +9% vs alien)
|
|
436
|
+
- `dyn: wide dense` 5115ms -> **912ms** (~5.6× faster; +10% vs alien)
|
|
293
437
|
No regressions on the other rows (steady-state update, propagation, and creation
|
|
294
438
|
paths are within noise of 1.1.2). See `resultsReactive.txt`.
|
|
295
439
|
- **Correctness.** The new retracking is validated by `retracking.difftest.mjs`
|
|
296
440
|
against a reference reconciler: 20,000 direct writes and 10,000 batched writes,
|
|
297
441
|
**0 disagreements**.
|
|
298
442
|
|
|
299
|
-
### Added
|
|
443
|
+
### Added -- observer-lifecycle introspection (top-level + per-registry)
|
|
300
444
|
A small, zero-cost-when-unused surface for auto-pausing wrappers and devtools.
|
|
301
445
|
All accept a public `Signal`/`Computed` handle.
|
|
302
|
-
- **`hasObservers(handle)`
|
|
446
|
+
- **`hasObservers(handle)` -> `boolean`.** O(1) (`node.headSub !== null`). The
|
|
303
447
|
auto-pause predicate: is anything subscribed to this source right now? A `peek`
|
|
304
448
|
does not count.
|
|
305
|
-
- **`observeObservers(handle, { onConnect?, onDisconnect? })`
|
|
306
|
-
Fires `onConnect` on the 0
|
|
307
|
-
*after* registration (transition-only
|
|
449
|
+
- **`observeObservers(handle, { onConnect?, onDisconnect? })` -> `unobserve`.**
|
|
450
|
+
Fires `onConnect` on the 0->1 observer transition and `onDisconnect` on 1->0,
|
|
451
|
+
*after* registration (transition-only -- no immediate fire if the handle is
|
|
308
452
|
already observed). Re-tracking a persistently-read source does **not** churn
|
|
309
453
|
connect/disconnect. This is the hook `lite-time` / `lite-raf` use to start a
|
|
310
454
|
ticker only while a derived value is being watched.
|
|
311
455
|
- **`forEachObserver(handle, fn)` / `forEachSource(handle, fn)`.** Walk the live
|
|
312
456
|
graph in either direction; `fn` receives a `{ kind, value }` descriptor where
|
|
313
457
|
`kind` is `"signal" | "computed" | "effect"`. For graph inspection (lite-devtools).
|
|
314
|
-
- **Cost.** The hooks sit behind an internal lifecycle counter
|
|
458
|
+
- **Cost.** The hooks sit behind an internal lifecycle counter -- when no handle is
|
|
315
459
|
being observed, the hot path adds a single branch-predicted `count !== 0` check
|
|
316
460
|
inside link alloc/free and nothing else. Zero steady-state cost when unused.
|
|
317
461
|
- **Error contract.** `hasObservers` / `forEachObserver` / `forEachSource` no-op
|
|
318
462
|
on a non-handle argument; `observeObservers` throws `TypeError`.
|
|
319
463
|
|
|
320
464
|
### Test suite
|
|
321
|
-
- Added `test/13-introspection_test.mjs`: 10 tests across 3 describe blocks
|
|
465
|
+
- Added `test/13-introspection_test.mjs`: 10 tests across 3 describe blocks --
|
|
322
466
|
`hasObservers` (live observation reflects, peek doesn't count), `observeObservers`
|
|
323
467
|
auto-pause lifecycle (start-on-first/stop-on-last, no extra connect for a 2nd
|
|
324
468
|
observer, re-observe fires again, no churn on re-track, conditional reads toggle
|
|
@@ -330,14 +474,14 @@ All accept a public `Signal`/`Computed` handle.
|
|
|
330
474
|
None required. Drop-in upgrade. No existing surface or behavior changed; the
|
|
331
475
|
introspection functions are purely additive and the retracking change is internal.
|
|
332
476
|
|
|
333
|
-
## [1.1.3]
|
|
477
|
+
## [1.1.3] -- 2026-05-28
|
|
334
478
|
|
|
335
|
-
Patch release: one new export, no behavior changes, no engine changes
|
|
479
|
+
Patch release: one new export, no behavior changes, no engine changes -- drop-in
|
|
336
480
|
over 1.1.2.
|
|
337
481
|
|
|
338
482
|
### Added
|
|
339
483
|
- **`isTracking()`** (top-level + per-registry). Returns `true` iff a read RIGHT
|
|
340
|
-
NOW would record a dependency on this registry
|
|
484
|
+
NOW would record a dependency on this registry -- an observer body is on the
|
|
341
485
|
stack AND tracking is enabled. Returns `false` inside `untrack()`, inside the
|
|
342
486
|
callback of `signal.subscribe` (which inlines the same untracked-notify), inside
|
|
343
487
|
`onCleanup` bodies, inside the `watch` / `when` callback path, and outside any
|
|
@@ -355,14 +499,14 @@ allocation on whether the read will actually subscribe anything.
|
|
|
355
499
|
|
|
356
500
|
### API notes
|
|
357
501
|
- **Per-registry.** A wrapper operating against a non-default registry MUST call
|
|
358
|
-
THAT registry's `isTracking()`, not the top-level one
|
|
502
|
+
THAT registry's `isTracking()`, not the top-level one -- each registry has its
|
|
359
503
|
own tracking state. The top-level helper delegates to the default registry,
|
|
360
504
|
matching the existing pattern for `signal`/`computed`/`effect`/`untrack`.
|
|
361
505
|
- **Cost.** Two closure-variable loads, one AND, one return; V8 inlines it.
|
|
362
|
-
Roughly 1
|
|
506
|
+
Roughly 1-2 ns per call.
|
|
363
507
|
|
|
364
508
|
### Test suite
|
|
365
|
-
- Added `test/10-is-tracking_test.mjs`: 11 tests across 5 describe blocks
|
|
509
|
+
- Added `test/10-is-tracking_test.mjs`: 11 tests across 5 describe blocks --
|
|
366
510
|
observer-bodies (effect + computed), untracked windows (`untrack`, `subscribe`
|
|
367
511
|
callback, `onCleanup`, `watch` callback), outside-observer (module scope,
|
|
368
512
|
call-site of unobserved computed read), robustness (state restored after
|
|
@@ -371,19 +515,19 @@ allocation on whether the read will actually subscribe anything.
|
|
|
371
515
|
### Migration from 1.1.2
|
|
372
516
|
None required. Drop-in upgrade. No existing surface or behavior changed.
|
|
373
517
|
|
|
374
|
-
## [1.1.2]
|
|
518
|
+
## [1.1.2] -- 2026-05-26
|
|
375
519
|
|
|
376
520
|
Patch release: hot-path micro-optimizations and a zero-allocation cleanup of
|
|
377
|
-
the creation path. No behavior changes, no API changes
|
|
521
|
+
the creation path. No behavior changes, no API changes -- drop-in over 1.1.1.
|
|
378
522
|
|
|
379
|
-
### Changed
|
|
523
|
+
### Changed -- performance (no semantic change)
|
|
380
524
|
- **Inlined cursor fast-path in `signal()`/`computed()` reads.** On stable read
|
|
381
525
|
order the cursor match is now handled inline; only a cursor *miss* falls
|
|
382
526
|
through into the (large, non-inlinable) `allocateLink` frame. Removes a
|
|
383
527
|
function call from the steady-state read hot path.
|
|
384
528
|
- **Allocation-free creation.** `signal`/`computed`/`effect` now read their
|
|
385
529
|
`opts` argument defensively instead of defaulting the parameter to `{}`. The
|
|
386
|
-
`= {}` default allocated a throwaway object on every no-opts call
|
|
530
|
+
`= {}` default allocated a throwaway object on every no-opts call -- the common
|
|
387
531
|
path when mounting many cells. Creation is now zero-allocation on that path.
|
|
388
532
|
- **Single-closure `subscribe`.** The tracked read + untracked notify is inlined
|
|
389
533
|
(one closure instead of two), dropping a per-subscription closure and an
|
|
@@ -393,11 +537,11 @@ the creation path. No behavior changes, no API changes — drop-in over 1.1.1.
|
|
|
393
537
|
the `markEpoch` dedup guard on purpose (hoisting it would add work on the
|
|
394
538
|
already-marked revisit path that the guard exists to keep cheap).
|
|
395
539
|
|
|
396
|
-
### Changed
|
|
540
|
+
### Changed -- packaging
|
|
397
541
|
- Canonical single-engine layout: the implementation is `Signal.js` and the
|
|
398
542
|
watcher utilities are `Watch.js`, which imports `effect`/`untrack` from
|
|
399
543
|
`./Signal.js`. Both the public entry and `Watch.js` resolve to one engine
|
|
400
|
-
instance
|
|
544
|
+
instance -- eliminating any chance of a duplicate-module-instance split that
|
|
401
545
|
would silently break cross-module dependency tracking.
|
|
402
546
|
|
|
403
547
|
### Test suite
|
|
@@ -416,7 +560,7 @@ the creation path. No behavior changes, no API changes — drop-in over 1.1.1.
|
|
|
416
560
|
### Migration from 1.1.x
|
|
417
561
|
None required. Drop-in upgrade.
|
|
418
562
|
|
|
419
|
-
## [1.1.1]
|
|
563
|
+
## [1.1.1] -- 2026-05-22
|
|
420
564
|
|
|
421
565
|
Patch release: cleanup-semantics adapter integration, conformance fixes from
|
|
422
566
|
the `johnsoncodehk/reactive-framework-test-suite`, and one targeted
|
|
@@ -430,7 +574,7 @@ correctness bug in flush error reporting.
|
|
|
430
574
|
- `tailSub` field on `ReactiveNode`. Symmetric with the existing `tailDep`;
|
|
431
575
|
enables O(1) tail insertion into the subscriber list.
|
|
432
576
|
|
|
433
|
-
### Changed
|
|
577
|
+
### Changed -- conformance fixes
|
|
434
578
|
|
|
435
579
|
- **#216** Effects now fire in **creation order** on a shared signal.
|
|
436
580
|
Subscriber list insertion is tail-first instead of head-first; traversal
|
|
@@ -439,8 +583,8 @@ correctness bug in flush error reporting.
|
|
|
439
583
|
|
|
440
584
|
- **#178** `runCleanup` invokes registered cleanups in an **untracked
|
|
441
585
|
context** (`currentObserver = null`, `isTrackingDeps = false`). Reads
|
|
442
|
-
inside a cleanup body
|
|
443
|
-
`dispose()` from a containing effect
|
|
586
|
+
inside a cleanup body -- including reads triggered by a synchronous
|
|
587
|
+
`dispose()` from a containing effect -- no longer leak into the parent
|
|
444
588
|
observer's dep set.
|
|
445
589
|
|
|
446
590
|
- **#111** `executeEffect` bails cleanly when a node is disposed by its own
|
|
@@ -473,7 +617,7 @@ correctness bug in flush error reporting.
|
|
|
473
617
|
|
|
474
618
|
### Performance
|
|
475
619
|
- No regressions observed in MUX, BROADCAST, DEEP CHAIN, KAIROS, or
|
|
476
|
-
SELECTIVE DAG benchmarks. MUX moved from 156K to 226K ops/s
|
|
620
|
+
SELECTIVE DAG benchmarks. MUX moved from 156K to 226K ops/s -- V8 appears
|
|
477
621
|
to optimise the flush loop more aggressively now that the per-iteration
|
|
478
622
|
`try/catch` shape is stable. Out-of-batch `signal.set` is unchanged
|
|
479
623
|
(revert-detection guards short-circuit on `batchDepth === 0`).
|
|
@@ -492,22 +636,22 @@ correctness bug in flush error reporting.
|
|
|
492
636
|
(sibling-effect propagation under no-re-run, cycle precedence over
|
|
493
637
|
buffered errors, custom-equals revert, etc.).
|
|
494
638
|
|
|
495
|
-
## [1.1.0]
|
|
639
|
+
## [1.1.0] -- 2026-05-20
|
|
496
640
|
|
|
497
641
|
### Added
|
|
498
|
-
- `markDownstream` iterative DFS marker backed by preallocated `markStack`
|
|
499
|
-
- Double-buffered effect queue (`effectQueueA` / `effectQueueB`)
|
|
500
|
-
- Generation counter (`gen`) per node
|
|
642
|
+
- `markDownstream` iterative DFS marker backed by preallocated `markStack` -- propagation no longer grows the JS call stack regardless of graph depth.
|
|
643
|
+
- Double-buffered effect queue (`effectQueueA` / `effectQueueB`) -- effects scheduled mid-flush land in the next pass, no recursive flush.
|
|
644
|
+
- Generation counter (`gen`) per node -- stale handles after dispose+recycle silently no-op instead of corrupting the pool.
|
|
501
645
|
- `CapacityError` with `kind` (`"nodes"` | `"links"`) and `capacity` fields, thrown when the `"throw"` policy is set and a pool is exhausted.
|
|
502
|
-
- `createRegistry({ onCapacityExceeded: "grow" })`
|
|
503
|
-
- `createRegistry({ maxFlushPasses })`
|
|
504
|
-
- `destroy()`
|
|
505
|
-
- `watch(source, callback, { immediate? })`, `when(predicate, callback)`, `whenAsync(predicate)`
|
|
646
|
+
- `createRegistry({ onCapacityExceeded: "grow" })` -- opt-in unbounded pool growth, bounded by `maxLinks * 16` ceiling.
|
|
647
|
+
- `createRegistry({ maxFlushPasses })` -- configurable cycle-protection limit (default `100`).
|
|
648
|
+
- `destroy()` -- full registry reset; all prior handles silently no-op afterward.
|
|
649
|
+
- `watch(source, callback, { immediate? })`, `when(predicate, callback)`, `whenAsync(predicate)` -- re-exported from `Watch.js`. Zero-allocation hot paths in `watch` and `when`; `whenAsync` allocates one Promise per call (documented; not for per-frame use).
|
|
506
650
|
|
|
507
651
|
### Changed
|
|
508
652
|
- 32-bit modular epoch arithmetic across `globalVersion`, `evalVersion`, `markEpoch`. Engine survives indefinite uptime without integer-overflow risk.
|
|
509
653
|
- `dispose(api)` is now universal across signals, computeds, effect handles, and `.subscribe()` return values. Cross-registry calls are silent no-ops. Foreign reactive primitives are duck-typed (on `.peek`) and not invoked.
|
|
510
|
-
- `untrack(fn)` restores prior tracking state via `try / finally`
|
|
654
|
+
- `untrack(fn)` restores prior tracking state via `try / finally` -- safe under thrown errors inside `fn`.
|
|
511
655
|
- `onCleanup(fn)` now accepts multiple registrations per scope and works in computeds, not just effects. Stored as a single function or upgraded to an array.
|
|
512
656
|
|
|
513
657
|
### Fixed
|
|
@@ -523,12 +667,12 @@ correctness bug in flush error reporting.
|
|
|
523
667
|
- Full methodology and reproducibility recipe in [`bench/README.md`](./bench/README.md).
|
|
524
668
|
|
|
525
669
|
### Known limitations
|
|
526
|
-
- Dependency reconciliation is O(1) per read on stable read order; degrades to O(N) under chaotic read order. v1.2 (in benchmark validation) replaces the cursor-based retracking with per-source version-stamped reconciliation
|
|
670
|
+
- Dependency reconciliation is O(1) per read on stable read order; degrades to O(N) under chaotic read order. v1.2 (in benchmark validation) replaces the cursor-based retracking with per-source version-stamped reconciliation -- see [RFC #N1](https://github.com/PeshoVurtoleta/lite-signal/issues/1).
|
|
527
671
|
- Computed resolution is recursive on the JS call stack; bounded by the engine stack limit (~10,000 frames).
|
|
528
672
|
- `whenAsync` allocates one Promise per call. Use `when` (callback form) for per-frame paths.
|
|
529
673
|
|
|
530
674
|
### Migration from 1.0.x
|
|
531
675
|
None required. Drop-in upgrade.
|
|
532
676
|
|
|
533
|
-
## [1.0.0]
|
|
677
|
+
## [1.0.0] -- 2026-05-12
|
|
534
678
|
Initial public release.
|