@zakkster/lite-signal-decorators 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/README.md +107 -15
- package/SignalDecorators.d.ts +1 -1
- package/SignalDecorators.js +2 -2
- package/llms.txt +22 -10
- package/package.json +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,88 @@ All notable changes to `@zakkster/lite-signal-decorators` are documented here.
|
|
|
4
4
|
The format follows Keep a Changelog; this project adheres to Semantic
|
|
5
5
|
Versioning.
|
|
6
6
|
|
|
7
|
+
## [1.0.0] - 2026-08-29
|
|
8
|
+
|
|
9
|
+
The 1.0 release: docs freeze, the fleet playground, and the standing pre-publish
|
|
10
|
+
gate. Zero runtime changes -- the 16-export surface ships byte-identical to
|
|
11
|
+
0.4.0 (the hot accessor canon is review-diffed to a zero-line diff), and this
|
|
12
|
+
release freezes that surface under semver: any later signature change is a major
|
|
13
|
+
recorded in a decision file. Dist-tag `latest`. Stage gate, measured at
|
|
14
|
+
closeout by the full section-10 chain (archived verbatim below): 214/214 tests
|
|
15
|
+
on both lanes; torture 15 scenarios (13 pass + the two forward-compat scenarios
|
|
16
|
+
legitimately skipping under the installed 1.5.0 peer) with 15/15 sabotage
|
|
17
|
+
controls breaking as required; the peer-preview lane SUITE-GREEN per tag
|
|
18
|
+
(15/15, forward scenarios RUNNING) against lite-signal 1.9.0-preview.6 AND
|
|
19
|
+
1.9.0-canary.1; the bench sink self-test catching its sabotaged adapter; pack 7
|
|
20
|
+
files, no `demo/` or `Publications/`.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- The fleet-playground demo (`demo/`, dev-only, never in `files[]`): a
|
|
25
|
+
single-file instrument console over a two-plane architecture -- Plane A a
|
|
26
|
+
decorated entity-VM fleet in a `capacityFor`-sized custom registry with
|
|
27
|
+
enforced ceilings, Plane B the telemetry signals in the default registry
|
|
28
|
+
driving five `@zakkster/lite-watch-ex` watchers (`watchUntil`,
|
|
29
|
+
`pausableWatch`, `watchChanged`, `watchMany`, `watchPrevious`). The DOM-free
|
|
30
|
+
core runs headless under the same GC-budget and dispose-storm gates the
|
|
31
|
+
library uses; the PD-29 registry wall is proven by a stats-delta (no watcher
|
|
32
|
+
ever forms an edge into the custom-registry fleet), not by assertion.
|
|
33
|
+
- README migration section: MobX 7 and signal-utils translation tables,
|
|
34
|
+
including the closing row MobX cannot write -- `disposeReactive(vm)` is one
|
|
35
|
+
idempotent, node-exact call after which every later touch throws by name.
|
|
36
|
+
- README emit-support matrix, generated from `test/fixtures/hashes.json` (9
|
|
37
|
+
entries) under drift-proof `EMIT-MATRIX` markers; `04-fixture-freshness`
|
|
38
|
+
asserts the README block equals the generator, so a re-emit that changes a
|
|
39
|
+
byte is loud, not silent.
|
|
40
|
+
- `llms.txt` per-feature forward floors documented alongside the peer range:
|
|
41
|
+
1.6.0 for `createScope`, 1.9.0 for `Symbol.dispose`; the peer range floor
|
|
42
|
+
stays `>=1.5.0 <2.0.0`.
|
|
43
|
+
- `gate` script (`test/gate.mjs`): the section-10 pre-publish chain as captured
|
|
44
|
+
child processes -- fixtures, test, test:gc, torture (semantic + soak),
|
|
45
|
+
the TORTURE_BREAK control sweep, the non-blocking peer-preview lane,
|
|
46
|
+
the bench sink self-test, and `npm pack --dry-run` asserting exactly 7 files.
|
|
47
|
+
Every blocking step exits 0 or the gate exits non-zero; peer-preview is
|
|
48
|
+
reported, never gated.
|
|
49
|
+
- `Publications/` (dev-only, never shipped): per-channel release drafts and the
|
|
50
|
+
GitHub release notes, centered on the class-reactivity benchmark methodology
|
|
51
|
+
with an explicit invitation for competitor adapter PRs.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- Docs re-stamped to reality: Testing sections now read 214 tests across
|
|
56
|
+
fourteen files and 15 torture scenarios (13 run + 2 floor-gated skips), and
|
|
57
|
+
every numeric claim traces to a live stamp (`bench/results.txt`,
|
|
58
|
+
`decisions/0006`, or a gate tail). Version references across README and
|
|
59
|
+
llms.txt move to 1.0.0.
|
|
60
|
+
- The 16-export surface is frozen under semver at 1.0.0. The 0.x "frozen for
|
|
61
|
+
0.x" note is retired in favor of the semver promise.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- (Found in review, pass 1 charge C4) An inline-style violation in the demo
|
|
66
|
+
HTML -- a non-custom-property `style=` attribute against the demo CSS law --
|
|
67
|
+
was hoisted into the stylesheet. Custom-property `style="--var: value"` hooks
|
|
68
|
+
remain, per the law.
|
|
69
|
+
- (Found in the planner audit) The Testing and gates sections carried
|
|
70
|
+
0.2.0-era numbers (171 tests, "12/12 scenarios", an 11-row file table) two
|
|
71
|
+
stages stale, in violation of the no-claim-without-a-stamp rule. Re-stamped
|
|
72
|
+
against the 1.0.0 tree.
|
|
73
|
+
|
|
74
|
+
### Gate output (section-10 chain, archived verbatim)
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
fixtures OK exit 0 -- emit fixtures regenerated
|
|
78
|
+
test OK exit 0 -- 214 pass / 0 fail
|
|
79
|
+
test:gc OK exit 0 -- 214 pass / 0 fail
|
|
80
|
+
torture OK exit 0 -- 13 passed, 2 skipped, 0 warned, 0 failed in 32.7s
|
|
81
|
+
torture:controls OK exit 0 -- 15 passed, 0 skipped, 0 warned, 0 failed in 1.9s
|
|
82
|
+
torture:peer-preview REPORTED NON-BLOCKING -- lane completed (exit 0) [preview 1.9.0-preview.6 SUITE-GREEN 15 passed, 0 skipped, 0 warned, 0 failed; canary 1.9.0-canary.1 SUITE-GREEN 15 passed, 0 skipped, 0 warned, 0 failed]
|
|
83
|
+
bench:selftest OK exit 0 -- ALL PASS -- 22 passed, 0 failed
|
|
84
|
+
pack OK exit 0 -- 7/7 files, no demo/ no Publications/
|
|
85
|
+
----------------------------------------------------------------------
|
|
86
|
+
GATE PASS -- 7 blocking steps + 1 non-blocking (peer-preview)
|
|
87
|
+
```
|
|
88
|
+
|
|
7
89
|
## [0.4.0] - 2026-08-26
|
|
8
90
|
|
|
9
91
|
The introspection release: the surface grows 11 -> 16, every addition cold-path
|
|
@@ -276,6 +358,7 @@ Initial release -- the decorator core.
|
|
|
276
358
|
- Torture skeleton (`@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`):
|
|
277
359
|
retention, conservation, lifecycle, and zero-GC lanes.
|
|
278
360
|
|
|
361
|
+
[1.0.0]: https://github.com/PeshoVurtoleta/lite-signal-decorators/releases/tag/v1.0.0
|
|
279
362
|
[0.4.0]: https://github.com/PeshoVurtoleta/lite-signal-decorators/releases/tag/v0.4.0
|
|
280
363
|
[0.3.0]: https://github.com/PeshoVurtoleta/lite-signal-decorators/releases/tag/v0.3.0
|
|
281
364
|
[0.2.0]: https://github.com/PeshoVurtoleta/lite-signal-decorators/releases/tag/v0.2.0
|
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ No base class to extend. No `makeObservable(this, {...})` mirror object to keep
|
|
|
49
49
|
- [Design decisions worth knowing](#design-decisions-worth-knowing)
|
|
50
50
|
- [Testing (for clients & QA)](#testing-for-clients--qa)
|
|
51
51
|
- [Compatibility](#compatibility)
|
|
52
|
+
- [Migrating from MobX 7 & signal-utils](#migrating-from-mobx-7--signal-utils)
|
|
52
53
|
- [What this is not](#what-this-is-not)
|
|
53
54
|
- [Ecosystem](#ecosystem)
|
|
54
55
|
- [FAQ](#faq) - [License](#license)
|
|
@@ -240,7 +241,7 @@ Symbol keys work (`Reflect.ownKeys`). A spec key colliding with an own property
|
|
|
240
241
|
| `boxOf` | `(vm, key) => SignalBox \| ComputedBox` | The live engine box behind a `@reactive`/`@derived` member -- `.peek()`, `.subscribe()`, raw interop. Unknown key: named throw with a did-you-mean. After dispose: `ReactiveDisposedError`. |
|
|
241
242
|
| `rootOf` | `(vm) => NodeDescriptor` | The instance's anchor descriptor -- feeds `forEachOwned` and lite-devtools. Throws `ReactiveDisposedError` after dispose. |
|
|
242
243
|
|
|
243
|
-
### Introspection & audit (0.
|
|
244
|
+
### Introspection & audit (1.0.0)
|
|
244
245
|
|
|
245
246
|
| Export | Signature | Behavior |
|
|
246
247
|
|---|---|---|
|
|
@@ -256,7 +257,7 @@ With labels and audit off, the zero-GC budgets are byte-identical to 0.3.0 -- th
|
|
|
256
257
|
| Export | Value |
|
|
257
258
|
|---|---|
|
|
258
259
|
| `ReactiveDisposedError` | `extends Error`; `name: "ReactiveDisposedError"`; fields `className`, `key`. Thrown on ANY touch of a disposed instance's surface. |
|
|
259
|
-
| `VERSION` | `"0.
|
|
260
|
+
| `VERSION` | `"1.0.0"` |
|
|
260
261
|
|
|
261
262
|
### The rejection matrix
|
|
262
263
|
|
|
@@ -305,6 +306,27 @@ console.log(world.stats().activeNodes); // 0 -- every node returned to the pool
|
|
|
305
306
|
|
|
306
307
|
The default registry never notices any of it: bound-registry churn leaves outside `stats()` frozen (torture-pinned). And because `boxOf` returns the *engine's* box, everything lite-signal composes with -- subscriptions, `peek`, batch, untrack, lite-raf frame effects -- composes with decorated members too.
|
|
307
308
|
|
|
309
|
+
### Predicate-gated watchers (`@zakkster/lite-watch-ex`)
|
|
310
|
+
|
|
311
|
+
`lite-watch-ex` adds one-shot, pausable, and change-gated watchers over the same engine. Its sources are plain **thunks** (`() => vm.hp`, never a box handle), and every watcher creates its effect node in the **default registry** -- so wire one only to a **default-registry** instance (one with no `host.registry`), never across a custom-registry fleet, where the edge would cross a boundary the engine's default `dispose` cannot see:
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
import { watchUntil } from "@zakkster/lite-watch-ex";
|
|
315
|
+
import { defineReactive } from "@zakkster/lite-signal-decorators";
|
|
316
|
+
|
|
317
|
+
// Default-registry instance -- no host.registry, so it lives in the default graph.
|
|
318
|
+
const ReactivePlayer = defineReactive(class Player {}, { signals: { hp: 100 } });
|
|
319
|
+
const vm = new ReactivePlayer();
|
|
320
|
+
|
|
321
|
+
// Fires ONCE when hp crosses the threshold, then self-disposes:
|
|
322
|
+
watchUntil(() => vm.hp, (h) => h <= 25, (h) => console.log("low hp:", h));
|
|
323
|
+
|
|
324
|
+
vm.hp = 40; // predicate false -> no fire
|
|
325
|
+
vm.hp = 20; // predicate true -> "low hp: 20", watcher disposes itself
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The [`fleet-playground` demo](demo/fleet-playground.html) shows the safe split at scale: decorated entity VMs in an enforced custom registry, all watchers on a separate default-registry telemetry plane.
|
|
329
|
+
|
|
308
330
|
---
|
|
309
331
|
|
|
310
332
|
## The numbers
|
|
@@ -352,12 +374,12 @@ The ~7 ns over raw batch is the guarded thunk + rest-array the decorator allocat
|
|
|
352
374
|
| `disposeReactive(vm)` | none | allocation-free success path; poison handles are prebuilt per member at decoration time |
|
|
353
375
|
| `boxOf` / `rootOf` / any throw | cold path | introspection and failure paths may allocate; never on the hot path |
|
|
354
376
|
|
|
355
|
-
The gates that hold it (run on every change, all green at 0.
|
|
377
|
+
The gates that hold it (run on every change, all green at 1.0.0):
|
|
356
378
|
|
|
357
|
-
- `npm test` / `npm run test:gc` -- **
|
|
379
|
+
- `npm test` / `npm run test:gc` -- **214/214** on both lanes.
|
|
358
380
|
- Suite gate (lite-leak + lite-gc-profiler): `leak=size 0/0 findings=0 warnings=0 | gc major=0 minor=0 maxMs=0.00 | ok`.
|
|
359
|
-
- Torture: **
|
|
360
|
-
- `churn-soak`: sustained construct/use/dispose
|
|
381
|
+
- Torture: **15 scenarios** (zero-GC read/write lanes at `maxMajor 0, maxPauseMs 4`; 4096-cycle leak gate at 0 live / 0 findings / 0 warnings; capacity atomicity at every overflow point; a 300-seed x 20k-op oracle with zero divergences) -- **13 run + 2 that skip correctly below their peer floors** (`scope-adoption` needs 1.6.0, `using-dispose` needs 1.9.0; the installed peer is 1.5.0). A skip *below* a floor is the forward-compat design working; a skip *at or above* it is a FAIL (run.mjs enforces floor-escalation). Every scenario carries a `TORTURE_BREAK` sabotage control that must exit non-zero -- **15/15 controls** prove each gate can actually fail.
|
|
382
|
+
- `churn-soak` + `fleet-soak`: sustained construct/use/dispose and a 10s 2k-VM fleet tick; pools at floor and retained heap flat at every sample.
|
|
361
383
|
|
|
362
384
|
The cross-framework matrix lives in `bench/` (private, never shipped): six engines -- both our tiers, the hand-written `lite-raw-boxes` baseline, MobX 7, signal-utils/signal-polyfill, and a hand-rolled alien-signals class -- across seven class-shaped scenarios, checksum-verified for identical work, stamped into `bench/results.txt`. The formal verdicts are in [`decisions/0006-kill-criteria.md`](decisions/0006-kill-criteria.md): the decorated path measured **0.94x** the hand-written baseline on vm-write and **1.10x** on a 10k-instance fleet read (the 2.0x kill line cleared with margin), and **0 major + 0 minor GC over 4096 construct/use/dispose cycles** with pools at floor -- while emitting ~12.6x less transient garbage per churn run than the hand-rolled class it replaces.
|
|
363
385
|
|
|
@@ -382,18 +404,19 @@ Full rationale lives in [`decisions/`](decisions/) -- each is a numbered, dated
|
|
|
382
404
|
## Testing (for clients & QA)
|
|
383
405
|
|
|
384
406
|
```bash
|
|
385
|
-
npm test # node --test,
|
|
386
|
-
npm run test:gc # the same
|
|
407
|
+
npm test # node --test, 214 tests
|
|
408
|
+
npm run test:gc # the same 214 with --expose-gc (enables the allocation assertions)
|
|
409
|
+
npm run gate # the full pre-publish chain (section 10): fixtures -> test -> test:gc -> torture -> controls -> peer-preview (non-blocking) -> bench selftest -> pack
|
|
387
410
|
```
|
|
388
411
|
|
|
389
|
-
**
|
|
412
|
+
**214 tests** across fourteen files, all green at 1.0.0. The decorator protocol is tested three times over: against a mock Stage-3 emitter *and* against committed real TypeScript 5 and Babel `2023-11` emits, so both toolchains' codegen is pinned, not assumed.
|
|
390
413
|
|
|
391
414
|
| File | Tests | Covers |
|
|
392
415
|
|---|---:|---|
|
|
393
416
|
| `01-protocol-mock` | 30 | Decorator protocol on the mock Stage-3 emitter: wiring, values, options, rejection matrix |
|
|
394
417
|
| `02-fixtures-ts` | 19 | The same laws on real TypeScript 5 emit (committed fixtures) |
|
|
395
418
|
| `03-fixtures-babel` | 19 | The same laws on real Babel `2023-11` emit |
|
|
396
|
-
| `04-fixture-freshness` |
|
|
419
|
+
| `04-fixture-freshness` | 2 | Fixture hashes match the sources (stale-emit guard) + the README emit-matrix block matches its generator |
|
|
397
420
|
| `05-wiring` | 5 | Anchor creation, wiring order, leaf-wires-once |
|
|
398
421
|
| `06-dispose` | 6 | Cascade, idempotency, poison, `using` |
|
|
399
422
|
| `07-qa-boundary` | 13 | S1 adversarial boundary pins |
|
|
@@ -401,19 +424,54 @@ npm run test:gc # the same 171 with --expose-gc (enables the allocation asse
|
|
|
401
424
|
| `09-buildless` | 16 | `defineReactive` parity + the spec rejection matrix |
|
|
402
425
|
| `10-qa-s2a-boundary` | 34 | Adversarial pins: identity guard, frozen dispose, registry heterogeneity, stacking |
|
|
403
426
|
| `11-qa-s2b-boundary` | 8 | Construction-throw boundaries: init-phase drain, chain-base throws, overflow storms |
|
|
427
|
+
| `12-accounting` | 11 | `costOf` node/link/shape grid (double-probe, frozen + cached, fail-closed) + `capacityFor` budget sizing |
|
|
428
|
+
| `13-labels-audit` | 10 | `enableLabels`/`labelOf` per-registry identity + `auditReactive` leak reporting, both opt-in and default-OFF |
|
|
429
|
+
| `14-qa-s4-boundary` | 21 | S4 adversarial edges: stats-less facade closure, signals-only capacity floor, label/audit boundary matrix |
|
|
430
|
+
|
|
431
|
+
### Emit-support matrix
|
|
432
|
+
|
|
433
|
+
Three fixture sources, two Stage-3 emitters, both emit lanes -- every cell below is a committed, hash-pinned fixture (the `04-fixture-freshness` guard above). The table is generated from the fixture manifest, so a re-emit that changes a byte is loud, not silent:
|
|
434
|
+
|
|
435
|
+
<!-- EMIT-MATRIX:START -->
|
|
436
|
+
Generated by `node test/fixtures/emit-matrix.mjs` from `test/fixtures/hashes.json` -- do not hand-edit. Toolchain pinned by the committed fixtures: **TypeScript 5.9.3**, **@babel/core 7.29.7** + **@babel/plugin-proposal-decorators 7.29.7** (`version: 2023-11`). Each `sha256` is the first 12 hex of the committed emit; `npm run fixtures` regenerates and `test/04-fixture-freshness` fails loudly on any drift.
|
|
437
|
+
|
|
438
|
+
| Source | Emitter | Emit lane | Compiled output | sha256 | At decoration time |
|
|
439
|
+
|---|---|---|---|---|---|
|
|
440
|
+
| `fixture.src.ts` | TypeScript 5 | standard 2023-11 | `ts-out/fixture.src.js` | `1a3fc0f943bf` | accepted -- full decorator surface wired + pinned green |
|
|
441
|
+
| `fixture.src.ts` | Babel | standard 2023-11 | `babel-out/fixture.src.js` | `eb9dfb5939b1` | accepted -- full decorator surface wired + pinned green |
|
|
442
|
+
| `static.src.ts` | TypeScript 5 | standard 2023-11 | `ts-out/static.src.js` | `d2a03e3d5f70` | rejected -- static member is a named throw at decoration time |
|
|
443
|
+
| `static.src.ts` | Babel | standard 2023-11 | `babel-out/static.src.js` | `dc936c5aa235` | rejected -- static member is a named throw at decoration time |
|
|
444
|
+
| `legacy.src.ts` | TypeScript 5 | legacy (experimental) | `ts-legacy-out/legacy.src.js` | `c1059b1d37b1` | rejected -- legacy emit -> named rejection at decoration time |
|
|
445
|
+
| `legacy.src.ts` | Babel | legacy (experimental) | `babel-legacy-out/legacy.src.js` | `1d35a02c57ce` | rejected -- legacy emit -> named rejection at decoration time |
|
|
446
|
+
|
|
447
|
+
Source hashes: `fixture.src.ts` `fb492a396340`, `static.src.ts` `81fb649965e6`, `legacy.src.ts` `30ac3dabaf7c`.
|
|
448
|
+
<!-- EMIT-MATRIX:END -->
|
|
404
449
|
|
|
405
450
|
### The torture suite (dev-side, never shipped)
|
|
406
451
|
|
|
407
452
|
Process-isolated stress scenarios built on `@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`:
|
|
408
453
|
|
|
409
454
|
```bash
|
|
410
|
-
npm run torture # all
|
|
455
|
+
npm run torture # all 15 scenarios (13 run + 2 floor-gated skips)
|
|
411
456
|
npm run torture:semantic # the correctness lane (CI)
|
|
412
|
-
npm run torture:soak # the wall-clock churn
|
|
457
|
+
npm run torture:soak # the wall-clock churn + fleet soaks
|
|
413
458
|
npm run torture:controls # sabotage self-test: every scenario must FAIL when broken
|
|
414
459
|
```
|
|
415
460
|
|
|
416
|
-
|
|
461
|
+
Fifteen scenarios: emit-matrix, ordering, lifecycle, pool-conservation, zero-GC lanes, capacity atomicity (every overflow point x both construction paths), the full disposed-poison surface + resurrection storms, a 4096-cycle lite-leak gate, a **300-seed x 20k-op oracle fuzzer** (decorated vs hand-wired raw twin in lockstep: every derived value, every effect fire count, every graph opcode tally), raw/decorated interop + cross-registry + `registry.destroy()` contracts, batch/untrack semantics, the wall-clock churn soak, and a 10s 2k-VM fleet soak -- plus two forward-compat scenarios (`scope-adoption`, `using-dispose`) that **skip correctly** while the installed peer sits below their per-feature floors (1.6.0 `createScope`, 1.9.0 `Symbol.dispose`). A skip below a floor is the design working; a skip at or above it is a FAIL. On the installed 1.5.0 peer: 13 pass, 2 skip. Every scenario carries a `TORTURE_BREAK` sabotage control that must exit non-zero -- a gate that cannot fail is not a gate. Seeded lanes replay exactly via `TORTURE_SEED`.
|
|
462
|
+
|
|
463
|
+
### The fleet demo (dev-side, never shipped)
|
|
464
|
+
|
|
465
|
+
A single-file instrument console -- [`demo/fleet-playground.html`](demo/fleet-playground.html) -- drives a two-plane capacity fleet: decorated entity VMs in an enforced custom registry, telemetry watchers in the default registry. Its DOM-free core runs headless under the same gates the library uses:
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
npm run demo:build # esbuild bundle -> demo/bundle.js + rewrite demo/bundle.sha256
|
|
469
|
+
npm run demo:check # verify the committed bundle matches its recorded hash
|
|
470
|
+
npm run demo:gc # headless GC-budget lane over the fleet core (maxMajor 0)
|
|
471
|
+
npm run demo:storm # headless dispose-storm retention lane (lite-leak, size 0)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
The `demo/` directory is dev-only -- it never enters `package.json` `files[]` and never ships to consumers.
|
|
417
475
|
|
|
418
476
|
---
|
|
419
477
|
|
|
@@ -430,12 +488,45 @@ Native (untranspiled) `@` decorators in engines: not shipped anywhere yet -- unt
|
|
|
430
488
|
|
|
431
489
|
---
|
|
432
490
|
|
|
491
|
+
## Migrating from MobX 7 & signal-utils
|
|
492
|
+
|
|
493
|
+
The decorator vocabulary maps almost one-to-one; what changes is the lifetime story. Both libraries below leave teardown to the garbage collector -- this package makes it a single deterministic call.
|
|
494
|
+
|
|
495
|
+
### From MobX 7
|
|
496
|
+
|
|
497
|
+
| MobX 7 | lite-signal-decorators |
|
|
498
|
+
|---|---|
|
|
499
|
+
| `@observable accessor x` | `@reactive accessor x` |
|
|
500
|
+
| `@computed get y()` | `@derived get y()` |
|
|
501
|
+
| `@action m()` | `@batched m()` |
|
|
502
|
+
| `makeObservable(this, {...})` | `@reactiveHost` -- one wiring site, no mirror object to keep in sync |
|
|
503
|
+
| `reaction(...)` / `autorun(...)` | `@reactiveEffect m()` |
|
|
504
|
+
| reaction disposers only; the instance itself is never disposable | **`disposeReactive(vm)` -- one call, idempotent, node-exact, and every later touch throws by name. MobX has no equivalent; its per-instance graph ends when the collector decides.** |
|
|
505
|
+
|
|
506
|
+
### From signal-utils
|
|
507
|
+
|
|
508
|
+
Verified against the installed `signal-utils@0.21.1`: `@signal` (on accessors or getters) and `@cached` (on getters) are the two decorators in its surface.
|
|
509
|
+
|
|
510
|
+
| signal-utils 0.21 | lite-signal-decorators |
|
|
511
|
+
|---|---|
|
|
512
|
+
| `@signal accessor x` (or `@signal get x`) | `@reactive accessor x` |
|
|
513
|
+
| `@cached get y()` | `@derived get y()` |
|
|
514
|
+
| no disposal API at all | `disposeReactive(vm)` -- **and it disposes**: cascade teardown, poison swap, node-exact conservation |
|
|
515
|
+
| Stage-3 build required | `defineReactive(Class, spec)` -- the buildless door signal-utils has no equivalent for |
|
|
516
|
+
|
|
517
|
+
The cross-framework numbers behind this table are stamped in [`decisions/0006-kill-criteria.md`](decisions/0006-kill-criteria.md) (both engines measured through their documented class APIs at checksum-identical work).
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
433
521
|
## What this is not
|
|
434
522
|
|
|
435
523
|
- **Not a home for module-level or global signals.** That is raw `lite-signal` territory; `static` members are rejected by design.
|
|
524
|
+
- **Not a deep/proxy observation layer.** No `observable.deep`, no wrapped Arrays/Maps/Sets, no proxy magic -- the reactive unit is a declared member, not a traversed object graph. Collections are `@zakkster/lite-project` territory.
|
|
436
525
|
- **Not a per-frame action system.** `@batched` costs a measured thunk per call -- fine for "one call per user intent", wrong inside a render loop. Per-frame hot lanes stay on plain accessor writes (and frame *scheduling* belongs to `lite-raf`).
|
|
437
526
|
- **Not a framework, renderer, or component model.** It ends at the reactive view-model; DOM binding is `lite-signal-dom`'s job.
|
|
438
|
-
- **Not a
|
|
527
|
+
- **Not a general meta-programming kit.** Five decorators, one wiring law -- not an open decorator toolbox. It does one thing: turn a class into a reactive view-model with a provable lifetime.
|
|
528
|
+
- **Not a MobX API shim.** No `makeObservable`, no administration objects -- and no GC-based cleanup: disposal is explicit, deterministic, and verified, because "the collector will get it eventually" is not a lifecycle.
|
|
529
|
+
- **Not a legacy-decorators consumer.** TypeScript `experimentalDecorators` emit is detected by call shape at decoration time and rejected with a named error -- never "works differently under legacy".
|
|
439
530
|
- **Not usable as `@` syntax without a toolchain** -- that is exactly what `defineReactive` exists for.
|
|
440
531
|
|
|
441
532
|
---
|
|
@@ -448,6 +539,7 @@ Native (untranspiled) `@` decorators in engines: not shipped anywhere yet -- unt
|
|
|
448
539
|
| `@zakkster/lite-signal-dom` | DOM bindings for the same engine -- where a view-model meets actual elements. |
|
|
449
540
|
| `@zakkster/lite-raf` | Frame-rate scheduling for the same graph; the frame-coalescing pattern the `scheduler` option on `@reactiveEffect` exists to plug into. |
|
|
450
541
|
| `@zakkster/lite-devtools` | Graph inspection; `rootOf(vm)` + `forEachOwned` is the hook it walks. |
|
|
542
|
+
| [`@zakkster/lite-watch-ex`](https://www.npmjs.com/package/@zakkster/lite-watch-ex) | One-shot / predicate-gated / pausable watchers over the same engine; thunk sources, default-registry effects -- see the [registry note](#composability) before pointing one at a decorated member. |
|
|
451
543
|
| `@zakkster/lite-leak` + `@zakkster/lite-gc-profiler` | The dev-side harness that proves every retention and allocation claim in this README. Never shipped to consumers. |
|
|
452
544
|
|
|
453
545
|
---
|
|
@@ -484,7 +576,7 @@ Because an instance whose base-class boxes live in one pool and whose subclass b
|
|
|
484
576
|
No, and the README says so with numbers: 22.12 ns/op vs 15.25 raw vs 11.67 unbatched on the reference rig -- a thunk + rest-array per call. Use it for actions; keep per-frame writes on plain accessors.
|
|
485
577
|
|
|
486
578
|
**Where are `costOf`, labels, the audit hook, private members?**
|
|
487
|
-
|
|
579
|
+
All three are part of the frozen 1.0.0 surface -- `costOf`/`capacityFor` (measured capacity accounting), `enableLabels`/`labelOf` (devtools identity), and `auditReactive` (leak audit), all cold-path or opt-in with the hot canon untouched. Private-member support remains out. The `llms.txt` scope note tracks exactly what is and isn't included.
|
|
488
580
|
|
|
489
581
|
---
|
|
490
582
|
|
package/SignalDecorators.d.ts
CHANGED
|
@@ -366,4 +366,4 @@ export class ReactiveDisposedError extends Error {
|
|
|
366
366
|
// --- Version ------------------------------------------------------------------
|
|
367
367
|
|
|
368
368
|
/** Package version. Kept in lockstep with package.json and llms.txt. */
|
|
369
|
-
export const VERSION: "0.
|
|
369
|
+
export const VERSION: "1.0.0";
|
package/SignalDecorators.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-signal-decorators
|
|
2
|
+
* @zakkster/lite-signal-decorators v1.0.0
|
|
3
3
|
* --------------------
|
|
4
4
|
* Stage-3 decorator layer over @zakkster/lite-signal. Turns a plain class into
|
|
5
5
|
* a reactive view-model with measured per-instance cost and deterministic
|
|
@@ -1661,4 +1661,4 @@ export function auditReactive(on) {
|
|
|
1661
1661
|
// --- Version ------------------------------------------------------------------
|
|
1662
1662
|
|
|
1663
1663
|
/** Package version. Kept in lockstep with package.json and llms.txt. */
|
|
1664
|
-
export const VERSION = "0.
|
|
1664
|
+
export const VERSION = "1.0.0";
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-signal-decorators
|
|
2
2
|
|
|
3
|
-
VERSION 0.
|
|
3
|
+
VERSION 1.0.0
|
|
4
4
|
|
|
5
5
|
> Stage-3 decorator layer over @zakkster/lite-signal. Turns a plain class into a
|
|
6
6
|
> reactive view-model where each instance has a measured per-property cost, a
|
|
@@ -68,7 +68,7 @@ slot for a poison handle, so any later read/write throws a named
|
|
|
68
68
|
`FinalizationRegistry` reports any instance GC'd without `disposeReactive`.
|
|
69
69
|
- `ReactiveDisposedError` -- `extends Error`, `name` `"ReactiveDisposedError"`,
|
|
70
70
|
fields `className` and `key`.
|
|
71
|
-
- `VERSION` -- `"0.
|
|
71
|
+
- `VERSION` -- `"1.0.0"`.
|
|
72
72
|
|
|
73
73
|
## Registry law (one registry per host chain)
|
|
74
74
|
|
|
@@ -185,14 +185,26 @@ throw a named error.
|
|
|
185
185
|
Requires `@zakkster/lite-signal` `>=1.5.0 <2.0.0` (peer dependency). Uses
|
|
186
186
|
`signalBox`, `computedBox`, `effect`, `createRoot`, `getOwner`, `runWithOwner`,
|
|
187
187
|
`dispose`, `nodeId`, `isTracking`, `batch`, and `untrack` from it (all 11 also
|
|
188
|
-
routed per-registry via `createRegistry()`).
|
|
188
|
+
routed per-registry via `createRegistry()`). The RANGE floor is 1.5.0 and stays
|
|
189
|
+
1.5.0 -- everything the runtime surface needs shipped in 1.5.0 stable. Two
|
|
190
|
+
forward-compat torture scenarios are feature-detected above that floor and SKIP
|
|
191
|
+
below it (a skip below the floor is correct; at or above it is a failure): the
|
|
192
|
+
`createScope` adoption scenario needs peer 1.6.0, and the engine
|
|
193
|
+
`[Symbol.dispose]`/`using` interop scenario needs peer 1.9.0. Neither is a
|
|
194
|
+
runtime requirement -- the shipped surface runs on 1.5.0.
|
|
189
195
|
|
|
190
196
|
## Scope note
|
|
191
197
|
|
|
192
|
-
0.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
1.0.0 FREEZES the export surface -- 16 exports: the 11 runtime exports plus
|
|
199
|
+
`costOf`, `capacityFor`, `enableLabels`, `labelOf`, and `auditReactive` (all
|
|
200
|
+
cold / opt-in; the hot accessor canon is byte-identical to 0.3.0). The semver
|
|
201
|
+
promise from here: any change to an existing export's signature or behavior is a
|
|
202
|
+
MAJOR, recorded in a decision file; new exports are minors; the hot accessor
|
|
203
|
+
canon (`makeGet`/`makeSet`) does not move without a major. Also present since
|
|
204
|
+
0.3.0: the dev-side class-reactivity benchmark (`bench/`, private, never
|
|
205
|
+
shipped) and the fleet-soak torture scenario; the formal kill-criteria verdicts
|
|
206
|
+
live in decisions/0006-kill-criteria.md. Still OUT of the 1.0 surface (each a
|
|
207
|
+
deliberate, documented exclusion, not an oversight): private (`#`) members,
|
|
208
|
+
static members, module/global signals (raw lite-signal territory), and any
|
|
209
|
+
instance reinitialization / pooling API -- an instance is single-lifetime,
|
|
210
|
+
disposed once, never revived.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-signal-decorators",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Stage-3 decorator layer over @zakkster/lite-signal. The reactive class layer where an instance has a measured cost, deterministic teardown, and a churn benchmark.",
|
|
5
5
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"torture:soak": "node test/torture/run.mjs --group soak",
|
|
68
68
|
"torture:controls": "node test/torture/run.mjs --controls",
|
|
69
69
|
"torture:peer-preview": "node test/torture/peer-preview.mjs",
|
|
70
|
+
"gate": "node test/gate.mjs",
|
|
70
71
|
"spikes": "for f in spikes/*.mjs; do echo \"== $f ==\"; node --expose-gc \"$f\" || exit 1; done",
|
|
71
72
|
"spike:emit": "node --expose-gc spikes/emit/probe.mjs",
|
|
72
73
|
"spike:ownership": "node --expose-gc spikes/ownership.mjs",
|
|
@@ -74,7 +75,11 @@
|
|
|
74
75
|
"spike:manual": "node --expose-gc spikes/manual-call.mjs",
|
|
75
76
|
"spike:poison": "node --expose-gc spikes/poison.mjs",
|
|
76
77
|
"spike:buildless": "node --expose-gc spikes/buildless.mjs",
|
|
77
|
-
"fixtures:regen": "node spikes/emit/regen.mjs"
|
|
78
|
+
"fixtures:regen": "node spikes/emit/regen.mjs",
|
|
79
|
+
"demo:build": "node demo/build.mjs",
|
|
80
|
+
"demo:check": "node demo/build.mjs --check",
|
|
81
|
+
"demo:gc": "node --expose-gc demo/gc-lane.mjs",
|
|
82
|
+
"demo:storm": "node --expose-gc demo/storm-lane.mjs"
|
|
78
83
|
},
|
|
79
84
|
"peerDependencies": {
|
|
80
85
|
"@zakkster/lite-signal": ">=1.5.0 <2.0.0"
|
|
@@ -87,6 +92,8 @@
|
|
|
87
92
|
"@zakkster/lite-gc-profiler": "^1.16.0",
|
|
88
93
|
"@zakkster/lite-leak": "^1.10.0",
|
|
89
94
|
"@zakkster/lite-signal": "1.5.0",
|
|
95
|
+
"@zakkster/lite-watch-ex": "^1.1.0",
|
|
96
|
+
"esbuild": "0.28.2",
|
|
90
97
|
"typescript": "^5.6.0"
|
|
91
98
|
}
|
|
92
99
|
}
|