@zakkster/lite-signal 1.2.0 → 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 +331 -68
- package/README.md +244 -155
- package/Signal.d.ts +74 -20
- package/Signal.js +191 -85
- package/llms.txt +189 -66
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,14 +4,277 @@ 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
|
|
152
|
+
|
|
153
|
+
A correctness-and-pauses patch in two halves: the pool allocator stops paying
|
|
154
|
+
for growth in unbounded bursts, and the introspection surface stops lying about
|
|
155
|
+
handles the 1.2.0 owner tree disposed behind your back. Plus the graph-mutation
|
|
156
|
+
hook -- the keystone that lets lite-devtools 1.1 / lite-studio 1.1 go push-based.
|
|
157
|
+
Drop-in over 1.2.0: 404-test suite green, 177/178 on
|
|
158
|
+
johnsoncodehk/reactive-framework-test-suite (same single open cell, Inner
|
|
159
|
+
Write #179), hot-path regression gate flat on two hosts.
|
|
160
|
+
|
|
161
|
+
### Fixed -- bounded pool growth (no more construction bursts)
|
|
162
|
+
- Under `onCapacityExceeded: "grow"`, exhausting a pool used to double it by
|
|
163
|
+
synchronously constructing `currentCapacity` fresh nodes/links -- at a
|
|
164
|
+
524,288-node pool that is a quarter-million 25-field allocations in one
|
|
165
|
+
pause, in whatever frame triggered it. Growth is now incremental: **one**
|
|
166
|
+
node/link constructed per free-list miss, pushed into the pool, recycled
|
|
167
|
+
forever after. The capacity **ledger** still doubles, so `stats()`
|
|
168
|
+
(`nodePoolCapacity` / `linkPoolCapacity` / `pooledLinks`), the
|
|
169
|
+
`maxLinks × 16` ceiling, and every `CapacityError` are bit-identical to
|
|
170
|
+
1.2.0 -- only the construction schedule changed. Locked by the existing
|
|
171
|
+
`test/03-pool` capacity/ceiling/recycle contracts.
|
|
172
|
+
- Benchmark effect (volynetstyle/js-reactivity-benchmark, same host as the
|
|
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).
|
|
175
|
+
Honest redistribution note: rows that previously *fit inside the doubling
|
|
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
|
|
178
|
+
accidental prefetch, and the same mechanism produced the pathological
|
|
179
|
+
bursts. Bounded pauses are the right trade for real applications; the
|
|
180
|
+
group total still improves.
|
|
181
|
+
- Steady-state hot paths are untouched (update / dynamic-retracking /
|
|
182
|
+
effect-recycle measured flat on both benchmark hosts).
|
|
183
|
+
|
|
184
|
+
### Fixed -- effect queues / mark stack stay PACKED
|
|
185
|
+
- Pool growth used to pre-size `effectQueueA/B` and the mark stack with
|
|
186
|
+
`arr.length = newCap` -- which permanently converts a PACKED V8 array to
|
|
187
|
+
HOLEY elements, a silent tax on every subsequent flush read. The queues now
|
|
188
|
+
grow by sequential append (packed-preserving, auto-amortised) and
|
|
189
|
+
`destroy()` truncates instead of null-filling to capacity.
|
|
190
|
+
|
|
191
|
+
### Fixed -- `destroy()` iterates physical pools
|
|
192
|
+
- `destroy()` walked `currentNodesCapacity` slots by index; with incremental
|
|
193
|
+
growth (and any future lazy population) the ledger can exceed the physical
|
|
194
|
+
pool. It now walks `nodePool.length` / `linkPool.length` and is safe on an
|
|
195
|
+
empty pool.
|
|
196
|
+
|
|
197
|
+
### Fixed -- stale-handle introspection (the owner-tree follow-up)
|
|
198
|
+
- 1.2.0's owner tree made the engine recycle pool slots **autonomously**: an
|
|
199
|
+
owner re-run cascade-disposes its owned observers, so holding a stale handle
|
|
200
|
+
stopped being a user error and became a routine occurrence. The
|
|
201
|
+
introspection surface -- `nodeId` / `describe` / `forEachObserver` /
|
|
202
|
+
`forEachSource` / `hasObservers` / `observeObservers` -- still resolved
|
|
203
|
+
`NODE_PTR` without a generation check and would happily report the
|
|
204
|
+
**recycled slot's new resident** (wrong id, wrong value, wrong edges)
|
|
205
|
+
through an old handle. All six entry points now resolve through a
|
|
206
|
+
gen-guarded `liveNode()` and report stale handles as `undefined` (or throw
|
|
207
|
+
the existing `TypeError`, for `observeObservers`) -- the same ABA discipline
|
|
208
|
+
`read()` / `set()` / `dispose()` already had.
|
|
209
|
+
- `describe()` descriptors are now **gen-stamped** alongside the node
|
|
210
|
+
reference, so the documented "descriptors are re-walkable handles" contract
|
|
211
|
+
survives the guard: a fresh descriptor walks; one held across a recycle
|
|
212
|
+
correctly goes stale. Pinned by the existing
|
|
213
|
+
"forEach* descriptors carry id and are re-walkable" test.
|
|
214
|
+
- **Effect dispose handles are now first-class introspection handles.** On
|
|
215
|
+
every prior version, `effect()` returned a bare closure carrying neither
|
|
216
|
+
`NODE_PTR` nor `NODE_GEN` -- so `describe` / `nodeId` / `forEachSource`
|
|
217
|
+
returned `undefined`/empty for a **live** effect handle, and
|
|
218
|
+
`observeObservers(effectHandle)` threw. The dispose function is now stamped
|
|
219
|
+
with the same symbol pair as signal/computed handles (`NODE_GEN` mirrors the
|
|
220
|
+
disposer's own `birthGen`, so introspection validity agrees exactly with its
|
|
221
|
+
stale-guard). After explicit dispose, slot recycle, or owner-cascade the
|
|
222
|
+
handle correctly reads stale. Measured cost: two property stores per effect
|
|
223
|
+
creation (~50 ns on a create/dispose churn microbench) -- symmetric with
|
|
224
|
+
what signal/computed handles already pay, create-path only. Found by the
|
|
225
|
+
lite-devtools 1.1 cross-probe campaign (`track(effectHandle)` threw).
|
|
226
|
+
- `peek()` had the same hole: `sharedSignalPeek` / `sharedComputedPeek`
|
|
227
|
+
resolved the slot ungated, so a stale handle's `peek()` returned the new
|
|
228
|
+
resident's value. Both now gen-check first and return `undefined` when
|
|
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).
|
|
231
|
+
|
|
232
|
+
### Added -- `onGraphMutation(fn)`: the graph-mutation hook
|
|
233
|
+
- Registry-level (and default-registry module export) debug hook, the
|
|
234
|
+
connection point for push-based tooling. Single nullable listener; every
|
|
235
|
+
fire point is one `if (mutationHook !== null)` branch and the dispatch is
|
|
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
|
|
242
|
+
- Cost: **zero when unregistered** (hot-path gate flat); registered, the
|
|
243
|
+
worst case measured is +29% on a dynamic-retracking torture loop (11.4M
|
|
244
|
+
events for 400K writes) -- a debug-mode tax paid only while a consumer is
|
|
245
|
+
attached, proportional to event volume.
|
|
246
|
+
- **Listener contract: observe only -- never throw, never mutate the graph.**
|
|
247
|
+
The hook fires synchronously inside mutation points; lite-devtools 1.1
|
|
248
|
+
multiplexes all of its consumers behind one registration, isolates their
|
|
249
|
+
exceptions, and unregisters when the last consumer stops (returning the
|
|
250
|
+
engine to the zero-cost state). `onGraphMutation` returns an unsubscribe
|
|
251
|
+
that restores the previously registered listener.
|
|
252
|
+
|
|
253
|
+
### Added -- owner-tree introspection: `forEachOwned` / `ownerOf`
|
|
254
|
+
- The 1.2.0 owner tree finally gets a (read-only, gen-guarded) window:
|
|
255
|
+
`forEachOwned(handle, fn)` iterates a node's owned children as standard
|
|
256
|
+
re-walkable descriptors; `ownerOf(handle)` returns the owner's descriptor
|
|
257
|
+
or `undefined` (top-level or stale). Same descriptor conventions as
|
|
258
|
+
`forEachObserver` / `forEachSource`; garbage input is a no-op /
|
|
259
|
+
`undefined`. This is what lite-devtools 1.1 builds `ownerTree()` and the
|
|
260
|
+
`graph({owners: true})` ownership edges on
|
|
261
|
+
(`capabilities().owners === true` from this release).
|
|
262
|
+
|
|
263
|
+
### Compatibility
|
|
264
|
+
- No behavioural change for live handles; stale handles now read as stale
|
|
265
|
+
everywhere instead of as the slot's next tenant. Allocation strategy is
|
|
266
|
+
unobservable through the public API. Tooling floor: lite-devtools >= 1.1.0
|
|
267
|
+
detects `onGraphMutation` / `forEachOwned` at load and degrades to its
|
|
268
|
+
1.0 polling behaviour on older engines.
|
|
269
|
+
|
|
270
|
+
## [1.2.0] -- 2026-06-11
|
|
8
271
|
|
|
9
272
|
A structural refactor that internally splits the engine into three named layers
|
|
10
273
|
(graph topology / ownership-lifecycle / propagation-execution) with a strict
|
|
11
274
|
dependency direction, plus a small set of additive features built on top of
|
|
12
|
-
that split. No behavioural changes for existing code
|
|
275
|
+
that split. No behavioural changes for existing code -- drop-in over 1.1.5.
|
|
13
276
|
|
|
14
|
-
### Added
|
|
277
|
+
### Added -- auto-disposal of nested observers (owner tree)
|
|
15
278
|
- An effect or computed that creates **observers** (nested `effect`/`computed`)
|
|
16
279
|
now owns them via an internal owner tree. When the owner re-runs or is
|
|
17
280
|
disposed, all owned observers are cascade-disposed before the new run starts.
|
|
@@ -25,7 +288,7 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
25
288
|
`test/15-owner-lazy-alloc.test.mjs` (the lite-store cross-wire shape) and
|
|
26
289
|
the new `test/19-v12-additions.test.mjs`.
|
|
27
290
|
|
|
28
|
-
### Added
|
|
291
|
+
### Added -- pre-batch revert (the "set X, set X back" optimisation)
|
|
29
292
|
- Inside a `batch(...)`, if a signal is set and then set back to its
|
|
30
293
|
pre-batch value (under the signal's own `equals`), the version bump is
|
|
31
294
|
reverted and downstream effects/computeds do **not** fire. Eliminates a
|
|
@@ -33,15 +296,15 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
33
296
|
in form state and undo/redo. Verified end-to-end (signal, computed, effect)
|
|
34
297
|
in `test/19-v12-additions.test.mjs`.
|
|
35
298
|
|
|
36
|
-
### Added
|
|
299
|
+
### Added -- multi-effect throws aggregate to `AggregateError`
|
|
37
300
|
- When two or more effects throw in the **same flush pass**, the engine
|
|
38
301
|
collects all errors and rethrows a native `AggregateError` at the
|
|
39
302
|
triggering `set()` / batch boundary. A single thrown error is rethrown
|
|
40
303
|
unwrapped (no change). Effects that don't throw still run. Cycle detection
|
|
41
|
-
unchanged
|
|
304
|
+
unchanged -- a flush exceeding `maxFlushPasses` (default 100) throws an
|
|
42
305
|
`Error` prefixed `"CycleError:"`.
|
|
43
306
|
|
|
44
|
-
### Added
|
|
307
|
+
### Added -- scheduler thunk caching with gen-bound ABA guard
|
|
45
308
|
- `effect(fn, { scheduler })` now caches the scheduler thunk on the node
|
|
46
309
|
itself (`node.schedulerThunk`) so repeated re-schedules reuse the same
|
|
47
310
|
closure (no allocation per re-schedule). The thunk holds a generation snapshot
|
|
@@ -49,14 +312,14 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
49
312
|
generation, so a stale thunk fired by an async scheduler against a recycled
|
|
50
313
|
pool slot is a guaranteed no-op (ABA safe).
|
|
51
314
|
|
|
52
|
-
### Changed
|
|
315
|
+
### Changed -- internal refactor, no behavioural difference
|
|
53
316
|
- The engine is reorganised into three explicit layers with documented
|
|
54
317
|
invariants (see the file header in `Signal.js`):
|
|
55
|
-
- **L1 Graph topology**
|
|
318
|
+
- **L1 Graph topology** -- `allocateLink` / `freeLink` / `severTail`. Pure
|
|
56
319
|
edge mechanics. Never touches `owner` / `firstOwned`.
|
|
57
|
-
- **L2 Ownership/lifecycle**
|
|
320
|
+
- **L2 Ownership/lifecycle** -- `createNode` / `disposeNode` / `runCleanup`.
|
|
58
321
|
Owns the owner tree and user cleanup. Never touches the tracking cursor.
|
|
59
|
-
- **L3 Propagation/execution**
|
|
322
|
+
- **L3 Propagation/execution** -- `markDownstream` (cursor-free), and the
|
|
60
323
|
orchestrators `executeEffect` / `pullComputed` that drive the cursor
|
|
61
324
|
(L1) and call `runCleanup` (L2) before a re-run.
|
|
62
325
|
- `currentObserver` and `currentOwner` are now distinct pointers. Today they
|
|
@@ -65,31 +328,31 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
65
328
|
- **Shared `peek` (perf).** `signal()` and `computed()` now reuse a single
|
|
66
329
|
`peek` function per registry instead of allocating a fresh closure per
|
|
67
330
|
primitive. Equivalent across registries (each registry has its own pair).
|
|
68
|
-
~10
|
|
331
|
+
~10-14% faster signal/computed creation on the `S:create*` micros, no
|
|
69
332
|
hot-path or behavioural change. Verified by 5 dedicated tests + the full
|
|
70
333
|
309-strong existing suite + 30,000-write differential retracking fuzz vs
|
|
71
334
|
the published 1.1.5.
|
|
72
335
|
|
|
73
|
-
### Changed
|
|
74
|
-
- `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
|
|
75
338
|
a computed after an unrelated source changed is O(1).
|
|
76
|
-
- `allocateLink` retains the **O(1) `tailSub` dedup**
|
|
77
|
-
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
|
|
78
341
|
re-read of the same source after an intervening observer can retain one
|
|
79
342
|
duplicate link per intervening edge, bounded by the loop count and
|
|
80
343
|
dispose-reclaimed.
|
|
81
344
|
|
|
82
|
-
### Fixed
|
|
345
|
+
### Fixed -- conformance regressions surfaced during release prep
|
|
83
346
|
- **#141 (`dispose during execution then continue: no re-run`)**: an effect that
|
|
84
347
|
called its own dispose handle mid-run and then continued to read another
|
|
85
348
|
signal would corrupt the link-list bookkeeping in `severTail` (latent crash
|
|
86
|
-
present in 1.1.5 too
|
|
349
|
+
present in 1.1.5 too -- the v1.2 owner tree exercised the path more
|
|
87
350
|
aggressively and made it visible). Fixed by nulling the tracking cursor in
|
|
88
351
|
`disposeNode` when the disposed node is the active observer, plus a
|
|
89
352
|
gen-snapshot guard in `executeEffect` / `pullComputed` so a post-body
|
|
90
353
|
`severTail` on a recycled slot is skipped.
|
|
91
354
|
- **#238 / #241 / #243 (cleanup ordering)**: nested effect cleanups must fire
|
|
92
|
-
inside-out on owner-tree disposal
|
|
355
|
+
inside-out on owner-tree disposal -- grandchild before child before outer.
|
|
93
356
|
The previous `runCleanup` ran the node's OWN cleanup before cascading, which
|
|
94
357
|
surfaced on cascade-dispose, on owner re-run, AND on the regression path
|
|
95
358
|
where an inner-only re-run had fired first. Fixed by swapping the order:
|
|
@@ -104,18 +367,18 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
104
367
|
- 363 tests / 133 suites total, all passing under `node --expose-gc --test`.
|
|
105
368
|
- **100% line coverage** and **98.62% branch coverage** on `Signal.js`
|
|
106
369
|
+ `Watch.js` (the few uncovered branches are defensive guards: cycle
|
|
107
|
-
detection, batchEpoch wraparound after 2
|
|
108
|
-
`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
|
|
109
372
|
conformance + existing user code).
|
|
110
373
|
- New file `test/19-v12-additions.test.mjs` (24 tests) locks in shared peek,
|
|
111
374
|
owner adoption rule, pre-batch revert, AggregateError aggregation,
|
|
112
375
|
CycleError detection, the `maxLinks` config branch, the disposed-signal
|
|
113
376
|
read/set behaviour, and the stop-fn ABA guard.
|
|
114
|
-
- New file `test/20-axis-stress.test.mjs` (23 tests)
|
|
377
|
+
- New file `test/20-axis-stress.test.mjs` (23 tests) -- eight orthogonal
|
|
115
378
|
engine-invariant "axes" plus the permanent conformance pins for #141,
|
|
116
379
|
#238, #241, #243.
|
|
117
380
|
- Existing `test/15-owner-lazy-alloc.test.mjs` skips ("scheduler-thunk
|
|
118
|
-
caching lands in v1.2.0") are removed
|
|
381
|
+
caching lands in v1.2.0") are removed -- the owner tree exists, the
|
|
119
382
|
tests pass.
|
|
120
383
|
- Differential retracking fuzz against the published 1.1.5: 30,000 writes,
|
|
121
384
|
**0 disagreements** (`bench/retracking.difftest.mjs`).
|
|
@@ -130,76 +393,76 @@ that split. No behavioural changes for existing code — drop-in over 1.1.5.
|
|
|
130
393
|
- The "scheduler-thunk caching" hint that referenced an older internal
|
|
131
394
|
staging name (Signal-1.3.0-rc) is gone; the file is the public 1.2.0.
|
|
132
395
|
|
|
133
|
-
## [1.1.5]
|
|
396
|
+
## [1.1.5] -- 2026-06-04
|
|
134
397
|
|
|
135
398
|
Additive release in service of `@zakkster/lite-devtools`: stable node identity on the
|
|
136
399
|
introspection surface, so a tool can dedupe and traverse the full reactive DAG. Drop-in
|
|
137
400
|
over 1.1.4, no breaking changes.
|
|
138
401
|
|
|
139
|
-
### Added
|
|
402
|
+
### Added -- node identity (top-level + per-registry)
|
|
140
403
|
- `nodeId(handle)` -> the node's stable per-allocation id (`number`), or `undefined` for a
|
|
141
404
|
non-handle. The dedupe key for graph walks.
|
|
142
405
|
- `describe(handle)` -> the handle's own `{ id, kind, value }` descriptor, or `undefined`
|
|
143
406
|
for a non-handle. **Re-walkable**: the descriptor may be passed back into
|
|
144
|
-
`forEachObserver`/`forEachSource`
|
|
407
|
+
`forEachObserver`/`forEachSource` -- the recursion primitive for full DAG discovery.
|
|
145
408
|
- `forEachObserver`/`forEachSource` descriptors now carry `id` (`{ id, kind, value }`).
|
|
146
409
|
- Every node gains a stable `id` assigned at allocation: one SMI write at creation, node
|
|
147
410
|
shape kept uniform (monomorphic). **Zero steady-state cost.**
|
|
148
411
|
|
|
149
412
|
### Test suite
|
|
150
|
-
- Added `test/15-identity_test.mjs`: 5 tests
|
|
413
|
+
- Added `test/15-identity_test.mjs`: 5 tests -- ids unique + stable, `nodeId`/`describe`
|
|
151
414
|
undefined on non-handles, descriptor shape `{ id, kind, value }`, descriptors re-walkable,
|
|
152
415
|
identity walks non-perturbing.
|
|
153
416
|
|
|
154
|
-
## [1.1.4]
|
|
417
|
+
## [1.1.4] -- 2026-05-31
|
|
155
418
|
|
|
156
419
|
Combined release: a retracking rewrite that closes the two documented chaotic
|
|
157
420
|
read-order limitations, plus an observer-lifecycle introspection surface. No
|
|
158
|
-
breaking changes, no public-API removals
|
|
421
|
+
breaking changes, no public-API removals -- drop-in over 1.1.3. (This release
|
|
159
422
|
folds in the work that was internally staged as 1.1.4 and 1.1.5; it ships as a
|
|
160
423
|
single 1.1.4.)
|
|
161
424
|
|
|
162
|
-
### Changed
|
|
425
|
+
### Changed -- performance (retracking, no semantic change)
|
|
163
426
|
- **Version-stamped O(1) reconciliation + clean-read short-circuit.** The cursor
|
|
164
427
|
reconciliation now stamps each source per evaluation and a `markEpoch` guard
|
|
165
428
|
short-circuits the pull when a subtree is already clean. This replaces the
|
|
166
429
|
prior strategy's O(N)-per-dep degradation under chaotic, high-fan-in, batched
|
|
167
430
|
read-after-write (every read re-validating its dependency subtree). Stable
|
|
168
|
-
read order is unchanged
|
|
431
|
+
read order is unchanged -- still O(1) per dep via cursor reuse, still zero-alloc.
|
|
169
432
|
- **Result.** The two rows that were the documented v1.1.x limitation flipped from
|
|
170
433
|
multiples-behind to ahead of `alien-signals`, and are now the fastest of the five
|
|
171
434
|
benchmarked frameworks:
|
|
172
|
-
- `dyn: large web app` 6194ms
|
|
173
|
-
- `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)
|
|
174
437
|
No regressions on the other rows (steady-state update, propagation, and creation
|
|
175
438
|
paths are within noise of 1.1.2). See `resultsReactive.txt`.
|
|
176
439
|
- **Correctness.** The new retracking is validated by `retracking.difftest.mjs`
|
|
177
440
|
against a reference reconciler: 20,000 direct writes and 10,000 batched writes,
|
|
178
441
|
**0 disagreements**.
|
|
179
442
|
|
|
180
|
-
### Added
|
|
443
|
+
### Added -- observer-lifecycle introspection (top-level + per-registry)
|
|
181
444
|
A small, zero-cost-when-unused surface for auto-pausing wrappers and devtools.
|
|
182
445
|
All accept a public `Signal`/`Computed` handle.
|
|
183
|
-
- **`hasObservers(handle)`
|
|
446
|
+
- **`hasObservers(handle)` -> `boolean`.** O(1) (`node.headSub !== null`). The
|
|
184
447
|
auto-pause predicate: is anything subscribed to this source right now? A `peek`
|
|
185
448
|
does not count.
|
|
186
|
-
- **`observeObservers(handle, { onConnect?, onDisconnect? })`
|
|
187
|
-
Fires `onConnect` on the 0
|
|
188
|
-
*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
|
|
189
452
|
already observed). Re-tracking a persistently-read source does **not** churn
|
|
190
453
|
connect/disconnect. This is the hook `lite-time` / `lite-raf` use to start a
|
|
191
454
|
ticker only while a derived value is being watched.
|
|
192
455
|
- **`forEachObserver(handle, fn)` / `forEachSource(handle, fn)`.** Walk the live
|
|
193
456
|
graph in either direction; `fn` receives a `{ kind, value }` descriptor where
|
|
194
457
|
`kind` is `"signal" | "computed" | "effect"`. For graph inspection (lite-devtools).
|
|
195
|
-
- **Cost.** The hooks sit behind an internal lifecycle counter
|
|
458
|
+
- **Cost.** The hooks sit behind an internal lifecycle counter -- when no handle is
|
|
196
459
|
being observed, the hot path adds a single branch-predicted `count !== 0` check
|
|
197
460
|
inside link alloc/free and nothing else. Zero steady-state cost when unused.
|
|
198
461
|
- **Error contract.** `hasObservers` / `forEachObserver` / `forEachSource` no-op
|
|
199
462
|
on a non-handle argument; `observeObservers` throws `TypeError`.
|
|
200
463
|
|
|
201
464
|
### Test suite
|
|
202
|
-
- 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 --
|
|
203
466
|
`hasObservers` (live observation reflects, peek doesn't count), `observeObservers`
|
|
204
467
|
auto-pause lifecycle (start-on-first/stop-on-last, no extra connect for a 2nd
|
|
205
468
|
observer, re-observe fires again, no churn on re-track, conditional reads toggle
|
|
@@ -211,14 +474,14 @@ All accept a public `Signal`/`Computed` handle.
|
|
|
211
474
|
None required. Drop-in upgrade. No existing surface or behavior changed; the
|
|
212
475
|
introspection functions are purely additive and the retracking change is internal.
|
|
213
476
|
|
|
214
|
-
## [1.1.3]
|
|
477
|
+
## [1.1.3] -- 2026-05-28
|
|
215
478
|
|
|
216
|
-
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
|
|
217
480
|
over 1.1.2.
|
|
218
481
|
|
|
219
482
|
### Added
|
|
220
483
|
- **`isTracking()`** (top-level + per-registry). Returns `true` iff a read RIGHT
|
|
221
|
-
NOW would record a dependency on this registry
|
|
484
|
+
NOW would record a dependency on this registry -- an observer body is on the
|
|
222
485
|
stack AND tracking is enabled. Returns `false` inside `untrack()`, inside the
|
|
223
486
|
callback of `signal.subscribe` (which inlines the same untracked-notify), inside
|
|
224
487
|
`onCleanup` bodies, inside the `watch` / `when` callback path, and outside any
|
|
@@ -236,14 +499,14 @@ allocation on whether the read will actually subscribe anything.
|
|
|
236
499
|
|
|
237
500
|
### API notes
|
|
238
501
|
- **Per-registry.** A wrapper operating against a non-default registry MUST call
|
|
239
|
-
THAT registry's `isTracking()`, not the top-level one
|
|
502
|
+
THAT registry's `isTracking()`, not the top-level one -- each registry has its
|
|
240
503
|
own tracking state. The top-level helper delegates to the default registry,
|
|
241
504
|
matching the existing pattern for `signal`/`computed`/`effect`/`untrack`.
|
|
242
505
|
- **Cost.** Two closure-variable loads, one AND, one return; V8 inlines it.
|
|
243
|
-
Roughly 1
|
|
506
|
+
Roughly 1-2 ns per call.
|
|
244
507
|
|
|
245
508
|
### Test suite
|
|
246
|
-
- 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 --
|
|
247
510
|
observer-bodies (effect + computed), untracked windows (`untrack`, `subscribe`
|
|
248
511
|
callback, `onCleanup`, `watch` callback), outside-observer (module scope,
|
|
249
512
|
call-site of unobserved computed read), robustness (state restored after
|
|
@@ -252,19 +515,19 @@ allocation on whether the read will actually subscribe anything.
|
|
|
252
515
|
### Migration from 1.1.2
|
|
253
516
|
None required. Drop-in upgrade. No existing surface or behavior changed.
|
|
254
517
|
|
|
255
|
-
## [1.1.2]
|
|
518
|
+
## [1.1.2] -- 2026-05-26
|
|
256
519
|
|
|
257
520
|
Patch release: hot-path micro-optimizations and a zero-allocation cleanup of
|
|
258
|
-
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.
|
|
259
522
|
|
|
260
|
-
### Changed
|
|
523
|
+
### Changed -- performance (no semantic change)
|
|
261
524
|
- **Inlined cursor fast-path in `signal()`/`computed()` reads.** On stable read
|
|
262
525
|
order the cursor match is now handled inline; only a cursor *miss* falls
|
|
263
526
|
through into the (large, non-inlinable) `allocateLink` frame. Removes a
|
|
264
527
|
function call from the steady-state read hot path.
|
|
265
528
|
- **Allocation-free creation.** `signal`/`computed`/`effect` now read their
|
|
266
529
|
`opts` argument defensively instead of defaulting the parameter to `{}`. The
|
|
267
|
-
`= {}` default allocated a throwaway object on every no-opts call
|
|
530
|
+
`= {}` default allocated a throwaway object on every no-opts call -- the common
|
|
268
531
|
path when mounting many cells. Creation is now zero-allocation on that path.
|
|
269
532
|
- **Single-closure `subscribe`.** The tracked read + untracked notify is inlined
|
|
270
533
|
(one closure instead of two), dropping a per-subscription closure and an
|
|
@@ -274,11 +537,11 @@ the creation path. No behavior changes, no API changes — drop-in over 1.1.1.
|
|
|
274
537
|
the `markEpoch` dedup guard on purpose (hoisting it would add work on the
|
|
275
538
|
already-marked revisit path that the guard exists to keep cheap).
|
|
276
539
|
|
|
277
|
-
### Changed
|
|
540
|
+
### Changed -- packaging
|
|
278
541
|
- Canonical single-engine layout: the implementation is `Signal.js` and the
|
|
279
542
|
watcher utilities are `Watch.js`, which imports `effect`/`untrack` from
|
|
280
543
|
`./Signal.js`. Both the public entry and `Watch.js` resolve to one engine
|
|
281
|
-
instance
|
|
544
|
+
instance -- eliminating any chance of a duplicate-module-instance split that
|
|
282
545
|
would silently break cross-module dependency tracking.
|
|
283
546
|
|
|
284
547
|
### Test suite
|
|
@@ -297,7 +560,7 @@ the creation path. No behavior changes, no API changes — drop-in over 1.1.1.
|
|
|
297
560
|
### Migration from 1.1.x
|
|
298
561
|
None required. Drop-in upgrade.
|
|
299
562
|
|
|
300
|
-
## [1.1.1]
|
|
563
|
+
## [1.1.1] -- 2026-05-22
|
|
301
564
|
|
|
302
565
|
Patch release: cleanup-semantics adapter integration, conformance fixes from
|
|
303
566
|
the `johnsoncodehk/reactive-framework-test-suite`, and one targeted
|
|
@@ -311,7 +574,7 @@ correctness bug in flush error reporting.
|
|
|
311
574
|
- `tailSub` field on `ReactiveNode`. Symmetric with the existing `tailDep`;
|
|
312
575
|
enables O(1) tail insertion into the subscriber list.
|
|
313
576
|
|
|
314
|
-
### Changed
|
|
577
|
+
### Changed -- conformance fixes
|
|
315
578
|
|
|
316
579
|
- **#216** Effects now fire in **creation order** on a shared signal.
|
|
317
580
|
Subscriber list insertion is tail-first instead of head-first; traversal
|
|
@@ -320,8 +583,8 @@ correctness bug in flush error reporting.
|
|
|
320
583
|
|
|
321
584
|
- **#178** `runCleanup` invokes registered cleanups in an **untracked
|
|
322
585
|
context** (`currentObserver = null`, `isTrackingDeps = false`). Reads
|
|
323
|
-
inside a cleanup body
|
|
324
|
-
`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
|
|
325
588
|
observer's dep set.
|
|
326
589
|
|
|
327
590
|
- **#111** `executeEffect` bails cleanly when a node is disposed by its own
|
|
@@ -354,7 +617,7 @@ correctness bug in flush error reporting.
|
|
|
354
617
|
|
|
355
618
|
### Performance
|
|
356
619
|
- No regressions observed in MUX, BROADCAST, DEEP CHAIN, KAIROS, or
|
|
357
|
-
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
|
|
358
621
|
to optimise the flush loop more aggressively now that the per-iteration
|
|
359
622
|
`try/catch` shape is stable. Out-of-batch `signal.set` is unchanged
|
|
360
623
|
(revert-detection guards short-circuit on `batchDepth === 0`).
|
|
@@ -373,22 +636,22 @@ correctness bug in flush error reporting.
|
|
|
373
636
|
(sibling-effect propagation under no-re-run, cycle precedence over
|
|
374
637
|
buffered errors, custom-equals revert, etc.).
|
|
375
638
|
|
|
376
|
-
## [1.1.0]
|
|
639
|
+
## [1.1.0] -- 2026-05-20
|
|
377
640
|
|
|
378
641
|
### Added
|
|
379
|
-
- `markDownstream` iterative DFS marker backed by preallocated `markStack`
|
|
380
|
-
- Double-buffered effect queue (`effectQueueA` / `effectQueueB`)
|
|
381
|
-
- 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.
|
|
382
645
|
- `CapacityError` with `kind` (`"nodes"` | `"links"`) and `capacity` fields, thrown when the `"throw"` policy is set and a pool is exhausted.
|
|
383
|
-
- `createRegistry({ onCapacityExceeded: "grow" })`
|
|
384
|
-
- `createRegistry({ maxFlushPasses })`
|
|
385
|
-
- `destroy()`
|
|
386
|
-
- `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).
|
|
387
650
|
|
|
388
651
|
### Changed
|
|
389
652
|
- 32-bit modular epoch arithmetic across `globalVersion`, `evalVersion`, `markEpoch`. Engine survives indefinite uptime without integer-overflow risk.
|
|
390
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.
|
|
391
|
-
- `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`.
|
|
392
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.
|
|
393
656
|
|
|
394
657
|
### Fixed
|
|
@@ -404,12 +667,12 @@ correctness bug in flush error reporting.
|
|
|
404
667
|
- Full methodology and reproducibility recipe in [`bench/README.md`](./bench/README.md).
|
|
405
668
|
|
|
406
669
|
### Known limitations
|
|
407
|
-
- 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).
|
|
408
671
|
- Computed resolution is recursive on the JS call stack; bounded by the engine stack limit (~10,000 frames).
|
|
409
672
|
- `whenAsync` allocates one Promise per call. Use `when` (callback form) for per-frame paths.
|
|
410
673
|
|
|
411
674
|
### Migration from 1.0.x
|
|
412
675
|
None required. Drop-in upgrade.
|
|
413
676
|
|
|
414
|
-
## [1.0.0]
|
|
677
|
+
## [1.0.0] -- 2026-05-12
|
|
415
678
|
Initial public release.
|