@zakkster/lite-signal-decorators 1.4.0 → 1.5.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 +111 -0
- package/README.md +36 -7
- package/SignalDecorators.d.ts +66 -1
- package/SignalDecorators.js +219 -2
- package/llms.txt +34 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,117 @@ 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.5.0] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
The gamedev release -- decisions/0013 criterion (d), and the LADDER CLOSES. The
|
|
10
|
+
package already shipped the fleet primitives (`capacityFor` sizes a registry;
|
|
11
|
+
`createRegistry` builds it; `releaseReactive`/`reinitReactive` park and revive an
|
|
12
|
+
instance with zero engine nodes held while parked), and the demo hand-rolled a
|
|
13
|
+
pool over them. `createFleet(inventory, bind, opts?)` is that pool, extracted:
|
|
14
|
+
the flagship-audience helper that composes the shipped primitives into one
|
|
15
|
+
fixed-capacity fleet handle. The demo is the NAMED consumer (criterion (d): serve
|
|
16
|
+
the flagship audience on a shipped primitive with the demo as a live consumer
|
|
17
|
+
landing in the same release) -- its hand-rolled pool was DELETED for the helper,
|
|
18
|
+
and that diff went NET-NEGATIVE. Surface 22 -> 23, and the decisions/0013
|
|
19
|
+
strategic-admission ladder is closed at four rungs.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`createFleet(inventory, bind, opts?) -> Fleet`** (23rd export) -- a
|
|
24
|
+
fixed-capacity pool of reactive instances over the shipped primitives. COLD
|
|
25
|
+
construction: `capacityFor(inventory, opts)` sizes a registry, `createRegistry`
|
|
26
|
+
builds it, `bind(registry)` binds the caller's decorated class to that registry
|
|
27
|
+
and returns it (PD-76 -- the helper never wraps or redefines the class; the demo
|
|
28
|
+
keeps its real `@reactiveHost` cycle), then one member per inventory unit is
|
|
29
|
+
EAGER-constructed and PARKED (PD-75 -- `acquire` never constructs). The handle
|
|
30
|
+
is `{ registry, Class, capacity, acquire(initials?), release(vm), at(i),
|
|
31
|
+
size(), stats(), dispose() }`. HOT: `acquire` pops an `Int32Array` free-list and
|
|
32
|
+
`reinitReactive`s a parked member (`initials` override the reset values);
|
|
33
|
+
`release` validates a per-fleet symbol slot stamp (a plain symbol-keyed integer
|
|
34
|
+
field, NOT a WeakMap -- PD-77's forcing condition, resolved) and
|
|
35
|
+
`releaseReactive`s the member back to the pool. Both hot bodies allocate ZERO.
|
|
36
|
+
SIX named fail-closed misuses: `FleetExhaustedError` (acquire at capacity;
|
|
37
|
+
pre-checked, the registry's own `CapacityError` is unreachable because all N are
|
|
38
|
+
prealloc'd), `FleetForeignMemberError` (release of a vm this fleet never handed
|
|
39
|
+
out -- the slot stamp is the check), `FleetDoubleReleaseError` (release of an
|
|
40
|
+
already-parked vm -- `releaseReactive` returning false is the check),
|
|
41
|
+
`FleetDisposedError` (any call after `dispose()`), a `RangeError` (`at(i)`
|
|
42
|
+
outside `[0, capacity)`), and a `TypeError` (a `bind` that is not a function or
|
|
43
|
+
does not return a constructor). These six are internal by design -- ONE exported
|
|
44
|
+
error class remains the surface law (`ReactiveDisposedError`); the fleet names
|
|
45
|
+
are message/name-level and pinned by test/20. Construction is ATOMIC: any
|
|
46
|
+
mid-prefill throw disposes the already-built members and destroys the registry
|
|
47
|
+
before rethrowing. `dispose()` disposes every member LIVE AND PARKED (park ->
|
|
48
|
+
dispose lands DISPOSED) then destroys the fleet-owned registry -- parked members
|
|
49
|
+
are disposed too, never leaked.
|
|
50
|
+
- **`test/20-fleet.test.mjs`** (37 cases) -- the API surface + the six fail-closed
|
|
51
|
+
laws + both emit lanes where meaningful + a buildless (`defineReactive`) class +
|
|
52
|
+
`initials` pass-through, and the 22 -> 23 export-count assertion.
|
|
53
|
+
- **`test/torture/fleet-torture.mjs`** (a NEW lane, 19 scenarios / 19 controls) --
|
|
54
|
+
proves the HELPER's bookkeeping (the primitive pooled-cycle proof stays in
|
|
55
|
+
`reinit-torture`): A1 4096 acquire/release cycles at zero-alloc budgets with a
|
|
56
|
+
`TORTURE_BREAK` control that catches a deliberately leaky variant; A2 1000
|
|
57
|
+
fleet lifecycles at N=512 returning `activeNodes` to exact baseline with
|
|
58
|
+
`poolGrowths` 0.
|
|
59
|
+
- **The demo consumer** (never cut -- the admission ground): `loop.ts`'s
|
|
60
|
+
hand-rolled pool (the `EntityShape` wall plumbing, slots/count, spawn/kill,
|
|
61
|
+
dispose-storm bodies) is DELETED in favor of one `createFleet` call;
|
|
62
|
+
`step`/`readPositions` use `at(i)`; the drift wall + HUD keep working. The
|
|
63
|
+
`EntityShape` twin is kept ONLY for pre-existence `capacityFor` sizing.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **The export surface: 22 -> 23** -- an additive MINOR under the 1.0.0 semver
|
|
68
|
+
promise (new exports are minors). The 1.0.0 hot canon
|
|
69
|
+
(`makeGet`/`makeSet`/`makeDerivedGet`) stays byte-identical: `createFleet` is
|
|
70
|
+
cold construction over the shipped primitives and moves no accessor byte.
|
|
71
|
+
- Version sync to 1.5.0 across the FOUR sites: `package.json`, the `VERSION`
|
|
72
|
+
const, `llms.txt` line 3, and the `SignalDecorators.d.ts` VERSION literal.
|
|
73
|
+
- `test/15` surface-freeze recount 22 -> 23 (the CB-A2 sites); its four-place
|
|
74
|
+
VERSION leg reads live values and needed no edit.
|
|
75
|
+
- The demo's boot cost rises by ~7ms one-time for the 4096 eager constructions
|
|
76
|
+
(the eager prefill moves cost to load -- the honest number, reported not hidden;
|
|
77
|
+
it buys a zero-alloc steady state).
|
|
78
|
+
|
|
79
|
+
### Measured (rig: Node v26.3.1, arm64 Apple M4 Pro, lite-signal 1.5.0)
|
|
80
|
+
|
|
81
|
+
- A1 acquire/release, 4096 cycles at 8N: **1.649 B/cyc** vs the in-process
|
|
82
|
+
zero-alloc control **0.234 B/cyc** (+2 limit); `gc.major` **0**; minors **2**
|
|
83
|
+
vs a limit of 129; `maxPauseMs` **0.36**. The `TORTURE_BREAK` leaky variant is
|
|
84
|
+
caught at **42.881 B/cyc**.
|
|
85
|
+
- A2 1000 lifecycles x N=512 return `activeNodes` to the exact baseline;
|
|
86
|
+
`poolGrowths` delta **0**.
|
|
87
|
+
- Release frees exactly `-(P+L+D+E+1)` nodes -- on the demo `Entity` shape that
|
|
88
|
+
is **-8** node-delta per release.
|
|
89
|
+
|
|
90
|
+
### Records
|
|
91
|
+
|
|
92
|
+
- decisions/0009 (the fifth admission candidate) and decisions/0011 (the
|
|
93
|
+
fleet-helpers section) each gain an ADMITTED stamp.
|
|
94
|
+
- decisions/0013 gains its COMPLETION ADDENDUM: the strategic-admission ladder is
|
|
95
|
+
CLOSED -- four rungs shipped same-day (v1.2.0 `@localTo` (a); v1.3.0
|
|
96
|
+
`snapshotOf` + `forEachReactive` (c); v1.4.0 `costOfInstance` (b); v1.5.0
|
|
97
|
+
`createFleet` (d)). Surface 18 -> 23 across the ladder; candidates 1 (`bump`)
|
|
98
|
+
and 7 (`onObserved` sugar) remain deferred behind the original real-consumer
|
|
99
|
+
bar, which resumes as the only track for anything new. The planned
|
|
100
|
+
decisions/0015 was CUT -- its fleet-contract facts fold into the 0013 addendum.
|
|
101
|
+
|
|
102
|
+
### Gate output (section-10 chain, archived verbatim)
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
fixtures OK exit 0 -- emit fixtures regenerated
|
|
106
|
+
test OK exit 0 -- 377 pass / 0 fail
|
|
107
|
+
test:gc OK exit 0 -- 377 pass / 0 fail
|
|
108
|
+
torture OK exit 0 -- 17 passed, 2 skipped, 0 warned, 0 failed in 35.5s
|
|
109
|
+
torture:controls OK exit 0 -- 19 passed, 0 skipped, 0 warned, 0 failed in 3.7s
|
|
110
|
+
torture:peer-preview REPORTED NON-BLOCKING -- lane completed (exit 0) [preview 1.9.0-preview.6 SUITE-GREEN 19/0/0/0; canary 1.9.0-canary.1 SUITE-GREEN 19/0/0/0]
|
|
111
|
+
bench:selftest OK exit 0 -- ALL PASS -- 22 passed, 0 failed
|
|
112
|
+
cookbook OK exit 0/0 -- corpus 18/18 companions ok in 2.0s; controls 8/8 controls fail correctly in 4.9s
|
|
113
|
+
pack OK exit 0 -- 7/7 files, exact 7-name set, no demo/ no Publications/
|
|
114
|
+
----------------------------------------------------------------------
|
|
115
|
+
GATE PASS -- 8 blocking steps + 1 non-blocking (peer-preview)
|
|
116
|
+
```
|
|
117
|
+
|
|
7
118
|
## [1.4.0] - 2026-08-30
|
|
8
119
|
|
|
9
120
|
The measured-instance pillar -- decisions/0013 criterion (b). `costOf(Factory)`
|
package/README.md
CHANGED
|
@@ -148,6 +148,7 @@ const ReactivePlayer = defineReactive(Player, {
|
|
|
148
148
|
- **Interop that stays raw** -- `boxOf(vm, key)` hands you the live engine box; `rootOf(vm)` hands the anchor descriptor to `forEachOwned` / lite-devtools. Decorated and hand-written signals share one graph.
|
|
149
149
|
- **Introspection & migration (1.3.0)** -- `forEachReactive(vm, fn, arg)` walks every value-bearing member in plan order (`signal`/`local`/`derived`, effects excluded) with a zero-alloc `fn(key, box, kind, arg)` callback; `snapshotOf(vm)` returns a shallow plain-object copy read through the accessors under one `untrack` -- the native `toJS` this package now ships, safe to call inside an effect.
|
|
150
150
|
- **Live per-instance cost (1.4.0)** -- `costOfInstance(vm)` walks one wired instance's own graph and reports what it costs RIGHT NOW: `costOf(Factory)` answers "what will an instance of this class cost" (it forces every derived to the constructed ceiling), `costOfInstance` answers "what does THIS instance cost" -- an unforced lazy derived or an untaken branch shows fewer links until the graph is exercised, and reading every derived once makes the two agree exactly. Twin-free: it needs no stats() ledger, so it measures instances on hand-rolled registries where `costOf` fails closed.
|
|
151
|
+
- **One-call pooled fleets (1.5.0)** -- `createFleet(inventory, bind, opts?)` composes the shipped primitives (`capacityFor` sizes the registry, `createRegistry` builds it, `releaseReactive`/`reinitReactive` park and revive) into one fixed-capacity fleet handle `{ registry, Class, capacity, acquire, release, at, size, stats, dispose }`. It EAGER-prefills and parks every member at construction, so `acquire(initials?)` never constructs -- it pops an `Int32Array` free-list and revives a parked member with ZERO allocation, and `release(vm)` parks it back after a per-fleet slot-stamp check. Six named fail-closed misuses (exhausted, foreign vm, double release, use-after-dispose, out-of-range `at`, bad `bind`); `dispose()` tears every member LIVE and PARKED plus the fleet-owned registry. The gamedev release: spawn/kill worlds and respawn-heavy scenes on a zero-GC steady state (decisions/0013 criterion (d) -- the demo's hand-rolled pool was deleted for it).
|
|
151
152
|
|
|
152
153
|
---
|
|
153
154
|
|
|
@@ -294,6 +295,28 @@ With labels and audit off, the zero-GC budgets are byte-identical to 0.3.0 -- th
|
|
|
294
295
|
| `forEachReactive` | `(vm, fn, arg) => count` | Cold value-member walk. Calls `fn(key, box, kind, arg)` once per value-bearing member and returns the visit count. `kind` is `"signal" \| "local" \| "derived"`; `@reactiveEffect`/`@batched` are EXCLUDED (non-value-bearing). Order is PLAN order -- signals, then locals, then deriveds, each declaration-ordered and ancestor-first (never `Reflect.ownKeys`, so it is stable across reinit). Four scalar args, zero descriptor object, and the `arg` pass-through kills the caller's closure: the walk is a gated zero-alloc body. Symbol keys are visited. Fails closed on a non-reactive, unwired, parked, or disposed value with the same named errors as `rootOf`. |
|
|
295
296
|
| `snapshotOf` | `(vm) => object` | A shallow plain-object copy of every value-bearing member, keyed by member key. Values are read through the ACCESSOR `vm[key]`, NOT `box.get`, so a `@localTo` compare-on-read resets honestly and a `@derived` computes on read (PD-62: reading the box directly would show a stale local after an untracked upstream move -- the accessor is the documented read). The whole walk runs under ONE `untrack` when the caller is tracking, so `snapshotOf` inside an effect subscribes to nothing. SHALLOW by design: a nested VM is copied by reference, not recursed. Symbol keys included. Fails closed on parked/disposed (`ReactiveDisposedError`, parked vs disposed flavor) and non-reactive values. This export ALLOCATES the returned object by design (~96 B/op measured) -- reported, never gated; the walk under it stays zero-alloc. |
|
|
296
297
|
|
|
298
|
+
### Fleet (1.5.0)
|
|
299
|
+
|
|
300
|
+
| Export | Signature | Behavior |
|
|
301
|
+
|---|---|---|
|
|
302
|
+
| `createFleet` | `(inventory, bind, opts?) => Fleet` | A fixed-capacity pool of reactive instances over the shipped primitives. COLD construction: `capacityFor(inventory, opts)` sizes a registry, `createRegistry` builds it, `bind(registry)` binds the caller's decorated class to it and returns it (the helper never wraps or redefines the class), then one member per inventory unit is EAGER-constructed and PARKED. The handle is `{ registry, Class, capacity, acquire(initials?), release(vm), at(i), size(), stats(), dispose() }`. HOT: `acquire` pops an `Int32Array` free-list and revives a parked member (`initials` override the reset values); `release` validates a per-fleet symbol slot stamp (a plain symbol-keyed integer field, NOT a WeakMap) and parks the member back. Both hot bodies allocate ZERO. Six named fail-closed misuses: `FleetExhaustedError` (acquire at capacity, pre-checked), `FleetForeignMemberError` (release of a vm this fleet never handed out), `FleetDoubleReleaseError` (release of an already-parked vm), `FleetDisposedError` (any call after `dispose()`), a `RangeError` (`at(i)` out of `[0, capacity)`), and a `TypeError` (a `bind` that is not a function or does not return a constructor). Construction is ATOMIC (a mid-prefill throw disposes what was built + destroys the registry). `dispose()` disposes every member LIVE and PARKED then destroys the fleet-owned registry. |
|
|
303
|
+
|
|
304
|
+
The fleet is the demo's pool, extracted -- one call for a spawn/kill world:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
const fleet = createFleet([[Entity, 4096]], (reg) => {
|
|
308
|
+
@reactiveHost({ registry: reg })
|
|
309
|
+
class Bound extends Entity {}
|
|
310
|
+
return Bound;
|
|
311
|
+
});
|
|
312
|
+
const e = fleet.acquire({ x: 10, y: 20 }); // revive a parked member, zero alloc
|
|
313
|
+
fleet.at(0); // slot read (bounds-checked)
|
|
314
|
+
fleet.release(e); // park it back; nodes return to the pool
|
|
315
|
+
fleet.dispose(); // every member + the registry torn down
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`acquire` never constructs: all `capacity` members are built and parked at construction, so the steady state is allocation-free. That moves the construction cost to load (the demo pays ~7ms one-time to prefill 4096 members) in exchange for a zero-GC spawn/kill loop.
|
|
319
|
+
|
|
297
320
|
### Errors & constants
|
|
298
321
|
|
|
299
322
|
| Export | Value |
|
|
@@ -404,6 +427,8 @@ The ~7 ns over raw batch is the guarded thunk + rest-array the decorator allocat
|
|
|
404
427
|
|
|
405
428
|
`P + L + D + E + 1` pool nodes -- one per signal, per local, per derived, per effect, plus the anchor. All of them return to the pool on dispose: conservation is node-exact (`activeNodes` to baseline, zero pool growths, allocations minus disposals reconciled) over 4096-cycle churn and a wall-clock soak.
|
|
406
429
|
|
|
430
|
+
**`createFleet` acquire/release (1.5.0).** The fleet's hot loop measures **1.649 B/cyc** at 8N against a **0.234 B/cyc** in-process zero-alloc control (a +2 limit); `gc.major 0`, 2 minors against a limit of 129, `maxPauseMs 0.36`. A `release(vm)` frees exactly `P + L + D + E + 1` nodes -- on the demo `Entity` shape that is a **-8** `activeNodes` delta per release. 1000 fleet lifecycles at N=512 return `activeNodes` to the exact baseline with `poolGrowths` 0, and the lane's `TORTURE_BREAK` control catches a leaky variant at 42.881 B/cyc.
|
|
431
|
+
|
|
407
432
|
<details>
|
|
408
433
|
<summary><strong>Zero-GC design notes: the allocation table + the gates</strong></summary>
|
|
409
434
|
|
|
@@ -420,12 +445,13 @@ The ~7 ns over raw batch is the guarded thunk + rest-array the decorator allocat
|
|
|
420
445
|
| `forEachReactive` walk | none | gated: 1e6 hoisted-callback walks measure **0.002 B/walk** (vs the 0.000 B/op zero-alloc control -- within a +2-byte limit), `gc.major === 0`; the 4-scalar `fn(key, box, kind, arg)` carries no descriptor object and the `arg` pass-through kills the caller's closure |
|
|
421
446
|
| `snapshotOf(vm)` | 1 plain object | **by design** -- the returned copy allocates (**95.8 B/op measured**, 1e5 cycles); REPORTED in the torture summary line, never gated. The walk *under* it stays zero-alloc; cold, off any frame path |
|
|
422
447
|
| `costOfInstance(vm)` | 1 frozen object | **by design** -- the per-call frozen result allocates (**71.3 B/op measured**, 1e4 calls); the measurement itself is `gc.major === 0` over those 1e4 calls -- REPORTED, never gated. The graph walk *under* it allocates nothing (module-slot visitors, no per-call closure); cold, off any frame path |
|
|
448
|
+
| `fleet.acquire` / `fleet.release` | none | gated: the fleet's hot loop measures **1.649 B/cyc** at 8N (vs a 0.234 B/cyc zero-alloc control, +2 limit), `gc.major 0`, `maxPauseMs 0.36`; `acquire` pops an `Int32Array` free-list + revives a parked member, `release` parks it after a symbol-slot-stamp check -- no WeakMap, no per-call closure |
|
|
423
449
|
|
|
424
|
-
The gates that hold it (run on every change, all green at 1.
|
|
450
|
+
The gates that hold it (run on every change, all green at 1.5.0):
|
|
425
451
|
|
|
426
|
-
- `npm test` / `npm run test:gc` -- **
|
|
452
|
+
- `npm test` / `npm run test:gc` -- **372/372** on both lanes.
|
|
427
453
|
- Suite gate (lite-leak + lite-gc-profiler): `leak=size 0/0 findings=0 warnings=0 | gc major=0 minor=0 maxMs=0.00 | ok`.
|
|
428
|
-
- Torture: **
|
|
454
|
+
- Torture: **19 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; the `reinit-torture` acquire/release gate; the `localto-torture` zero-alloc read/write storm + ABA-stale interleave lattice + pooled park/reinit; the `introspection-torture` 1e6 hoisted-callback `forEachReactive` walk at `maxMajor 0` with the snapshot-allocates figure reported, never gated; the `fleet-torture` 4096 acquire/release cycles at zero-alloc budgets + 1000 lifecycles to exact baseline) -- **17 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 -- **19/19 controls** prove each gate can actually fail.
|
|
429
455
|
- `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.
|
|
430
456
|
|
|
431
457
|
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 eight class-shaped scenarios (including the `churn-reuse` acquire/release lane, where the lite tiers pool with zero retained growth and MobX/signal-utils/alien-class are structurally `unsupported` -- no disposable instance lifecycle to pool), 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.
|
|
@@ -457,12 +483,12 @@ Full rationale lives in [`decisions/`](decisions/) -- each is a numbered, dated
|
|
|
457
483
|
## Testing (for clients & QA)
|
|
458
484
|
|
|
459
485
|
```bash
|
|
460
|
-
npm test # node --test,
|
|
461
|
-
npm run test:gc # the same
|
|
486
|
+
npm test # node --test, 372 tests
|
|
487
|
+
npm run test:gc # the same 372 with --expose-gc (enables the allocation assertions)
|
|
462
488
|
npm run gate # the full pre-publish chain (section 10): fixtures -> test -> test:gc -> torture -> controls -> peer-preview (non-blocking) -> bench selftest -> cookbook -> pack
|
|
463
489
|
```
|
|
464
490
|
|
|
465
|
-
**
|
|
491
|
+
**372 tests** across twenty files, all green at 1.5.0. The decorator protocol is tested three times over: against a mock standard-decorators emitter *and* against committed real TypeScript 5 and Babel `2023-11` emits, so both toolchains' codegen is pinned, not assumed.
|
|
466
492
|
|
|
467
493
|
| File | Tests | Covers |
|
|
468
494
|
|---|---:|---|
|
|
@@ -480,11 +506,12 @@ npm run gate # the full pre-publish chain (section 10): fixtures -> test
|
|
|
480
506
|
| `12-accounting` | 11 | `costOf` node/link/shape grid (double-probe, frozen + cached, fail-closed) + `capacityFor` budget sizing |
|
|
481
507
|
| `13-labels-audit` | 10 | `enableLabels`/`labelOf` per-registry identity + `auditReactive` leak reporting, both opt-in and default-OFF |
|
|
482
508
|
| `14-qa-s4-boundary` | 21 | S4 adversarial edges: stats-less facade closure, signals-only capacity floor, label/audit boundary matrix |
|
|
483
|
-
| `15-cookbook` | 14 | [`COOKBOOK.md`](https://github.com/PeshoVurtoleta/lite-signal-decorators/blob/main/COOKBOOK.md) drift/parity: each fenced block byte-compared against its tagged companion `#region` (both directions + both-way coverage), surface freeze (exactly
|
|
509
|
+
| `15-cookbook` | 14 | [`COOKBOOK.md`](https://github.com/PeshoVurtoleta/lite-signal-decorators/blob/main/COOKBOOK.md) drift/parity: each fenced block byte-compared against its tagged companion `#region` (both directions + both-way coverage), surface freeze (exactly 23 exports), citation allowlist, link law, static-cost probe, and the four-place VERSION sync (module const === package.json === llms.txt === `SignalDecorators.d.ts` literal) |
|
|
484
510
|
| `16-reinit` | 29 | Pooled-reinit lattice on both emit lanes: park/reinit/dispose transitions, the five `reinitReactive` fail-closed states, parked-touch throws by name, `initials` boundary matrix (0..N+1 keys, null/undefined, NaN/-0 verbatim), `Symbol.dispose` on a parked instance, accessor descriptors byte-identical across reinit, self-release re-entrancy, ledger conservation |
|
|
485
511
|
| `17-localto` | 34 | `@localTo` on both emit lanes + buildless `locals`: the read/write/upstream-reset lattice, both initial flavors (follow-from-wiring vs reset-from-initial), the ABA stale-local contract, `equals` override survival, park/reinit box+seen reset, `costOf` = P+L+D+E+1, source-throw fail-closed, and the fail-closed option/source matrix |
|
|
486
512
|
| `18-introspection` | 22 | `forEachReactive`/`snapshotOf` on both emit lanes + buildless: plan-order walk (signals, locals, deriveds; ancestor-first), symbol keys, the `signal`/`local`/`derived` kind tags, effect/batched exclusion, count return + `arg` pass-through, the untracked-read law (snapshotOf inside an effect fires once), r7 `{name,hp,mp,alive}` parity, the PD-62 accessor-read reset honesty, and the fail-closed non-reactive/unwired/parked/disposed matrix |
|
|
487
513
|
| `19-cost-instance` | 22 | `costOfInstance` on both emit lanes + buildless: A1 parity-when-forced (=== `costOf`, nodes/links/every kind count), A2 delta-when-lazy (links strictly lower, then monotonic toward the forced number), `@localTo` counted in locals with zero graph links, the frozen `{nodes,links,signals,locals,deriveds,effects}` shape, A3 registry-untouched over 10000 calls, PD-72 bound-registry + stats-less-facade measurement (where `costOf` fails closed), PD-70 uncached/live across a branch flip, and the A6 fail-closed matrix (plain/unwired/parked/disposed each a NAMED throw, never a `{nodes:0}` report) |
|
|
514
|
+
| `20-fleet` | 37 | `createFleet` on both emit lanes + a buildless class: the `{registry,Class,capacity,acquire,release,at,size,stats,dispose}` handle surface, eager-prefill (all members parked at construction, `acquire` never constructs), `initials` pass-through, the six fail-closed misuses (exhausted/foreign/double-release/use-after-dispose/`at` out-of-range/bad `bind`), atomic mid-prefill cleanup, `dispose()` tearing live AND parked members plus the registry, and the 22 -> 23 export-count freeze |
|
|
488
515
|
|
|
489
516
|
### Emit-support matrix
|
|
490
517
|
|
|
@@ -543,6 +570,8 @@ npm run demo:storm # headless dispose-storm retention lane (lite-leak,
|
|
|
543
570
|
|
|
544
571
|
Since 1.4.0 the console is the named consumer of `costOfInstance`: its shape-drift wall measures a real live `Entity` (its node count === the `EntityShape` sizing twin's) and the HUD reports one live fleet member's `costOfInstance` per HUD tick, so the live-vs-`costOf` delta -- a forced ceiling against the lazy live cost -- is visible on screen. The `EntityShape` twin stays for `capacityFor` sizing only (the world must be sized before it exists).
|
|
545
572
|
|
|
573
|
+
Since 1.5.0 the demo's hand-rolled pool IS `createFleet`: the spawn/kill plumbing (the slot array, free-list, count, and dispose-storm bodies) was DELETED in favor of one fleet handle, and `step`/`readPositions` read members through `fleet.at(i)`. The diff went net-negative -- the demo is the named consumer that admitted the helper (decisions/0013 criterion (d)). The eager prefill moves construction to load (~7ms one-time for 4096 members), the honest cost of a zero-alloc steady state.
|
|
574
|
+
|
|
546
575
|
The `demo/` directory is dev-only -- it never enters `package.json` `files[]` and never ships to consumers.
|
|
547
576
|
|
|
548
577
|
---
|
package/SignalDecorators.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
NodeDescriptor,
|
|
13
13
|
Registry,
|
|
14
14
|
RegistryConfig,
|
|
15
|
+
RegistryStats,
|
|
15
16
|
ReactiveHandle,
|
|
16
17
|
EffectScheduler,
|
|
17
18
|
} from "@zakkster/lite-signal";
|
|
@@ -480,6 +481,70 @@ export function capacityFor(
|
|
|
480
481
|
options?: CapacityForOptions,
|
|
481
482
|
): RegistryConfig;
|
|
482
483
|
|
|
484
|
+
// --- createFleet --------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
/** Options for {@link createFleet}. Forwarded to {@link capacityFor}. */
|
|
487
|
+
export interface CreateFleetOptions {
|
|
488
|
+
/** Link-budget multiplier (`>= 1`, default `1` = exact). See {@link CapacityForOptions.headroom}. */
|
|
489
|
+
headroom?: number;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* A fixed-capacity pool of reactive instances built by {@link createFleet}. Owns
|
|
494
|
+
* its registry; `dispose()` tears both members and registry down.
|
|
495
|
+
*/
|
|
496
|
+
export interface Fleet<T extends object = object> {
|
|
497
|
+
/** The fleet-owned `Registry` the members are bound to. */
|
|
498
|
+
readonly registry: Registry;
|
|
499
|
+
/** The bound class `bind(registry)` returned; every member is an instance of it. */
|
|
500
|
+
readonly Class: new (...args: any[]) => T;
|
|
501
|
+
/** The fixed member ceiling (sum of the inventory counts), sized eagerly at construction. */
|
|
502
|
+
readonly capacity: number;
|
|
503
|
+
/**
|
|
504
|
+
* Revive a parked member and return it. `initials` overrides its reset values
|
|
505
|
+
* (undefined = the plan's initials). Zero allocation.
|
|
506
|
+
* @throws a named `FleetExhaustedError` when all `capacity` members are live.
|
|
507
|
+
* @throws a named `FleetDisposedError` after `dispose()`.
|
|
508
|
+
*/
|
|
509
|
+
acquire(initials?: Record<PropertyKey, unknown>): T;
|
|
510
|
+
/**
|
|
511
|
+
* Park a member back into the pool. Zero allocation.
|
|
512
|
+
* @throws a named `FleetForeignMemberError` if `vm` was not acquired from this fleet.
|
|
513
|
+
* @throws a named `FleetDoubleReleaseError` if `vm` is already parked.
|
|
514
|
+
* @throws a named `FleetDisposedError` after `dispose()`.
|
|
515
|
+
*/
|
|
516
|
+
release(vm: T): T;
|
|
517
|
+
/**
|
|
518
|
+
* The member in slot `i` (live or parked).
|
|
519
|
+
* @throws a `RangeError` if `i` is out of `[0, capacity)`.
|
|
520
|
+
* @throws a named `FleetDisposedError` after `dispose()`.
|
|
521
|
+
*/
|
|
522
|
+
at(i: number): T;
|
|
523
|
+
/** The live member count (`capacity` minus parked). */
|
|
524
|
+
size(): number;
|
|
525
|
+
/** The fleet-owned registry's stats ledger, passed through. */
|
|
526
|
+
stats(): RegistryStats;
|
|
527
|
+
/** Dispose every member (live and parked) and destroy the registry. Idempotent. */
|
|
528
|
+
dispose(): void;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Build a fixed-capacity {@link Fleet} of reactive instances over the shipped
|
|
533
|
+
* primitives: `capacityFor(inventory, opts)` sizes a registry, `createRegistry`
|
|
534
|
+
* builds it, `bind(registry)` binds the caller's decorated class to it and returns
|
|
535
|
+
* it, then one member per inventory unit is EAGER-constructed and parked (acquire
|
|
536
|
+
* never constructs). The returned handle acquires/releases members with zero
|
|
537
|
+
* allocation and fails closed (named throws) on every misuse.
|
|
538
|
+
*
|
|
539
|
+
* @throws {TypeError} if `bind` is not a function or does not return a constructor.
|
|
540
|
+
* @throws if `inventory`/`opts` are invalid (via {@link capacityFor}).
|
|
541
|
+
*/
|
|
542
|
+
export function createFleet<T extends object = object>(
|
|
543
|
+
inventory: InventoryEntry[],
|
|
544
|
+
bind: (registry: Registry) => new (...args: any[]) => T,
|
|
545
|
+
opts?: CreateFleetOptions,
|
|
546
|
+
): Fleet<T>;
|
|
547
|
+
|
|
483
548
|
/**
|
|
484
549
|
* Toggle devtools labels (default OFF). While ON, wiring registers a
|
|
485
550
|
* `nodeId -> label` for every node an instance creates (`"Class.prop"`,
|
|
@@ -531,4 +596,4 @@ export class ReactiveDisposedError extends Error {
|
|
|
531
596
|
// --- Version ------------------------------------------------------------------
|
|
532
597
|
|
|
533
598
|
/** Package version. Kept in lockstep with package.json and llms.txt. */
|
|
534
|
-
export const VERSION: "1.
|
|
599
|
+
export const VERSION: "1.5.0";
|
package/SignalDecorators.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-signal-decorators v1.
|
|
2
|
+
* @zakkster/lite-signal-decorators v1.5.0
|
|
3
3
|
* --------------------
|
|
4
4
|
* Standard-decorators layer over @zakkster/lite-signal. Turns a plain class into
|
|
5
5
|
* a reactive view-model with measured per-instance cost and deterministic
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
stats,
|
|
48
48
|
forEachOwned,
|
|
49
49
|
forEachSource,
|
|
50
|
+
createRegistry,
|
|
50
51
|
} from "@zakkster/lite-signal";
|
|
51
52
|
|
|
52
53
|
// --- Module state -------------------------------------------------------------
|
|
@@ -2343,6 +2344,222 @@ export function capacityFor(inventory, options) {
|
|
|
2343
2344
|
};
|
|
2344
2345
|
}
|
|
2345
2346
|
|
|
2347
|
+
// --- createFleet (S11; the fleet helper over the shipped primitives) ----------
|
|
2348
|
+
//
|
|
2349
|
+
// The flagship-audience helper (0013 (d)): capacityFor -> createRegistry -> bind
|
|
2350
|
+
// -> EAGER-prefill N parked members over a slot array + an Int32Array free-list.
|
|
2351
|
+
// The demo's hand-rolled pool (loop.ts) is the extracted spec, so acquire/release
|
|
2352
|
+
// are the extracted spawn/kill: reinitReactive/releaseReactive with zero-alloc
|
|
2353
|
+
// bookkeeping. Cold construction; the hot acquire/release/at bodies are prebuilt
|
|
2354
|
+
// closures over the fleet's arrays (zero allocation, fail-closed guards only).
|
|
2355
|
+
//
|
|
2356
|
+
// SLOT STAMP (PD-77 forcing condition resolved): ownership is proven by a
|
|
2357
|
+
// per-fleet Symbol stamped onto each vm at prefill carrying its slot index -- a
|
|
2358
|
+
// plain (symbol-keyed) integer field, NOT a WeakMap. A field read is one property
|
|
2359
|
+
// load (zero allocation, monomorphic); a WeakMap.get is a slower hash probe AND
|
|
2360
|
+
// retains a parallel table for the fleet's lifetime. The stamp is minted per
|
|
2361
|
+
// fleet, so a foreign vm (no stamp, or another fleet's stamp) reads `undefined`
|
|
2362
|
+
// for THIS fleet's symbol and fails closed. slots[i] === vm re-confirms identity;
|
|
2363
|
+
// releaseReactive() returning false (already parked) is the double-release check.
|
|
2364
|
+
|
|
2365
|
+
function throwFleetBind() {
|
|
2366
|
+
throw new TypeError(
|
|
2367
|
+
`${ERR}createFleet(inventory, bind, opts?) -- bind must be a function (registry) -> BoundClass.`,
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
function throwFleetBindReturn() {
|
|
2372
|
+
throw new TypeError(
|
|
2373
|
+
`${ERR}createFleet -- bind(registry) must return the bound class (a constructor); the fleet constructs its members from it.`,
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function throwFleetExhausted(ctorName, capacity) {
|
|
2378
|
+
// Named so a caller can catch the capacity ceiling distinctly. The registry's
|
|
2379
|
+
// own CapacityError is unreachable (all N are prealloc'd), so this pre-check
|
|
2380
|
+
// is the ONLY exhaustion signal.
|
|
2381
|
+
const e = new Error(
|
|
2382
|
+
`${ERR}fleet<${ctorName}> is exhausted -- all ${capacity} members are live. release() one before acquire(), or size the fleet larger; capacity is fixed at construction (eager prefill).`,
|
|
2383
|
+
);
|
|
2384
|
+
e.name = "FleetExhaustedError";
|
|
2385
|
+
throw e;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function throwFleetForeign(ctorName) {
|
|
2389
|
+
const e = new Error(
|
|
2390
|
+
`${ERR}release(vm) -- vm was not acquired from this fleet<${ctorName}> (no matching slot stamp). Release only members this fleet handed out.`,
|
|
2391
|
+
);
|
|
2392
|
+
e.name = "FleetForeignMemberError";
|
|
2393
|
+
throw e;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function throwFleetDoubleRelease(ctorName) {
|
|
2397
|
+
const e = new Error(
|
|
2398
|
+
`${ERR}release(vm) -- vm is already parked in fleet<${ctorName}> (double release). Each acquire() pairs with exactly one release().`,
|
|
2399
|
+
);
|
|
2400
|
+
e.name = "FleetDoubleReleaseError";
|
|
2401
|
+
throw e;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function throwFleetDead(ctorName, op) {
|
|
2405
|
+
const e = new Error(
|
|
2406
|
+
`${ERR}${op}() -- fleet<${ctorName}> was disposed; its members are torn down and its registry is destroyed. Construct a new fleet.`,
|
|
2407
|
+
);
|
|
2408
|
+
e.name = "FleetDisposedError";
|
|
2409
|
+
throw e;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function throwFleetRange(ctorName, i, capacity) {
|
|
2413
|
+
throw new RangeError(
|
|
2414
|
+
`${ERR}at(${String(i)}) -- index out of bounds for fleet<${ctorName}> [0, ${capacity}).`,
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* Build a fixed-capacity fleet of reactive instances over the shipped primitives.
|
|
2420
|
+
* COLD construction sizes a registry from `inventory` (capacityFor), builds it
|
|
2421
|
+
* (createRegistry), hands it to `bind(registry)` so the caller binds its decorated
|
|
2422
|
+
* class to that registry and returns it, then EAGER-constructs and PARKS one
|
|
2423
|
+
* member per inventory unit (PD-75: acquire never constructs). Returns a Fleet
|
|
2424
|
+
* handle `{ registry, Class, capacity, acquire, release, at, size, stats,
|
|
2425
|
+
* dispose }`.
|
|
2426
|
+
*
|
|
2427
|
+
* HOT: `acquire(initials?)` pops the free-list and reinitReactive()s a parked
|
|
2428
|
+
* member (throws `FleetExhaustedError` at capacity); `release(vm)` validates the
|
|
2429
|
+
* slot stamp, releaseReactive()s the member back to the pool (throws on a foreign
|
|
2430
|
+
* vm or a double release), and pushes its slot. `at(i)` is a bounds-checked
|
|
2431
|
+
* slot read; `size()` is the live count; `stats()` passes the registry ledger
|
|
2432
|
+
* through. Both hot bodies allocate nothing.
|
|
2433
|
+
*
|
|
2434
|
+
* `dispose()` disposes every member (live AND parked -> DISPOSED), destroys the
|
|
2435
|
+
* fleet-owned registry, and marks the fleet dead; every later call fails closed
|
|
2436
|
+
* with a named throw. Construction is atomic: any mid-prefill throw disposes the
|
|
2437
|
+
* already-built members and destroys the registry before rethrowing (fail closed).
|
|
2438
|
+
*
|
|
2439
|
+
* @throws {TypeError} if `bind` is not a function or does not return a constructor.
|
|
2440
|
+
* @throws if `inventory`/`opts` are invalid (via capacityFor's fail-closed checks).
|
|
2441
|
+
*/
|
|
2442
|
+
export function createFleet(inventory, bind, opts) {
|
|
2443
|
+
if (typeof bind !== "function") throwFleetBind();
|
|
2444
|
+
// capacityFor validates inventory + opts (unknown-key did-you-mean, headroom)
|
|
2445
|
+
// and returns the eager/throw config; reuse its fail-closed checks wholesale.
|
|
2446
|
+
const config = capacityFor(inventory, opts);
|
|
2447
|
+
// Total prefill count = the sum of the inventory units (each pair[1] is a
|
|
2448
|
+
// validated positive integer by the time capacityFor returned).
|
|
2449
|
+
let capacity = 0;
|
|
2450
|
+
for (let i = 0; i < inventory.length; i++) capacity += inventory[i][1];
|
|
2451
|
+
|
|
2452
|
+
// The fleet OWNS this registry (dispose() destroys it). Any throw from here
|
|
2453
|
+
// on routes through the atomic-cleanup catch below.
|
|
2454
|
+
const registry = createRegistry(config);
|
|
2455
|
+
const STAMP = Symbol("lite-signal-decorators.fleet");
|
|
2456
|
+
const slots = new Array(capacity);
|
|
2457
|
+
const free = new Int32Array(capacity); // free-list: free[0..freeTop) = idle slots
|
|
2458
|
+
let Class;
|
|
2459
|
+
let ctorName;
|
|
2460
|
+
let built = 0;
|
|
2461
|
+
try {
|
|
2462
|
+
Class = bind(registry);
|
|
2463
|
+
if (typeof Class !== "function") throwFleetBindReturn();
|
|
2464
|
+
ctorName = Class.name || "fleet";
|
|
2465
|
+
for (let i = 0; i < capacity; i++) {
|
|
2466
|
+
const vm = new Class(); // eager construct on the fleet's registry
|
|
2467
|
+
vm[STAMP] = i; // slot stamp: ownership + slot index
|
|
2468
|
+
releaseReactive(vm); // park it (PD-75); nodes return to pool
|
|
2469
|
+
slots[i] = vm;
|
|
2470
|
+
free[i] = i;
|
|
2471
|
+
built = i + 1;
|
|
2472
|
+
}
|
|
2473
|
+
} catch (e) {
|
|
2474
|
+
for (let j = 0; j < built; j++) disposeReactive(slots[j]);
|
|
2475
|
+
registry.destroy(); // tear the fleet-owned registry down
|
|
2476
|
+
throw e; // atomic: nothing half-built survives
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
let freeTop = capacity; // all slots idle (all parked)
|
|
2480
|
+
let dead = false;
|
|
2481
|
+
|
|
2482
|
+
// HOT: revive the head parked slot. reinitReactive resets its boxes to
|
|
2483
|
+
// `initials` (undefined = the plan's reset values) with zero closure alloc.
|
|
2484
|
+
// ORDERING (mirrors fleetRelease): PEEK the candidate via free[freeTop-1],
|
|
2485
|
+
// run the FALLIBLE reinit FIRST, and decrement freeTop only AFTER it succeeds.
|
|
2486
|
+
// reinit throws named on bad initials (e.g. acquire({typo:1})) and on an
|
|
2487
|
+
// out-of-band-disposed member; decrementing before it would strand the popped
|
|
2488
|
+
// slot above freeTop -- lost capacity, over-reported size(). Fail closed:
|
|
2489
|
+
// freeTop moves only on success, so a rejected acquire leaves the slot
|
|
2490
|
+
// acquirable. (a) The disposed case: a member disposed out of band via at()
|
|
2491
|
+
// wedges at the free-list head -- every acquire rethrows reinit's named
|
|
2492
|
+
// "disposed (terminal)" error, refusing loudly rather than eroding capacity
|
|
2493
|
+
// silently. Left as (a) not (b): reinit's disposed and bad-initials throws are
|
|
2494
|
+
// both plain Errors with no distinct code/class, so dropping only the disposed
|
|
2495
|
+
// slot would need brittle message-matching -- a fail-open hazard worse than an
|
|
2496
|
+
// honest, named refusal.
|
|
2497
|
+
function fleetAcquire(initials) {
|
|
2498
|
+
if (dead) throwFleetDead(ctorName, "acquire");
|
|
2499
|
+
if (freeTop === 0) throwFleetExhausted(ctorName, capacity);
|
|
2500
|
+
const i = free[freeTop - 1];
|
|
2501
|
+
const vm = slots[i];
|
|
2502
|
+
reinitReactive(vm, initials);
|
|
2503
|
+
freeTop = freeTop - 1;
|
|
2504
|
+
return vm;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// HOT: validate ownership by the slot stamp, then park. `vm[STAMP]` is
|
|
2508
|
+
// `undefined` for a foreign or non-object vm (fail closed); slots[i] === vm
|
|
2509
|
+
// re-confirms identity; releaseReactive() false is the double-release signal.
|
|
2510
|
+
function fleetRelease(vm) {
|
|
2511
|
+
if (dead) throwFleetDead(ctorName, "release");
|
|
2512
|
+
const i = vm !== null && typeof vm === "object" ? vm[STAMP] : undefined;
|
|
2513
|
+
if (i === undefined || slots[i] !== vm) throwFleetForeign(ctorName);
|
|
2514
|
+
if (!releaseReactive(vm)) throwFleetDoubleRelease(ctorName);
|
|
2515
|
+
free[freeTop] = i;
|
|
2516
|
+
freeTop = freeTop + 1;
|
|
2517
|
+
return vm;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// HOT: bounds-checked slot read (live OR parked). `i >>> 0` folds negative and
|
|
2521
|
+
// out-of-range into one unsigned compare.
|
|
2522
|
+
function fleetAt(i) {
|
|
2523
|
+
if (dead) throwFleetDead(ctorName, "at");
|
|
2524
|
+
if ((i >>> 0) >= capacity) throwFleetRange(ctorName, i, capacity);
|
|
2525
|
+
return slots[i];
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function fleetSize() {
|
|
2529
|
+
if (dead) throwFleetDead(ctorName, "size");
|
|
2530
|
+
return capacity - freeTop; // live = capacity - idle
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// The fleet-owned registry always comes from createRegistry(), which always
|
|
2534
|
+
// carries the stats ledger, so this is a straight pass-through.
|
|
2535
|
+
function fleetStats() {
|
|
2536
|
+
if (dead) throwFleetDead(ctorName, "stats");
|
|
2537
|
+
return registry.stats();
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// Dispose every member (live AND parked; disposeReactive on a parked member
|
|
2541
|
+
// lands it DISPOSED), then destroy the registry. Idempotent: a second call
|
|
2542
|
+
// no-ops. After dispose, every hot method fails closed via the `dead` guard.
|
|
2543
|
+
function fleetDispose() {
|
|
2544
|
+
if (dead) return;
|
|
2545
|
+
dead = true;
|
|
2546
|
+
for (let i = 0; i < capacity; i++) disposeReactive(slots[i]);
|
|
2547
|
+
registry.destroy();
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
return {
|
|
2551
|
+
registry,
|
|
2552
|
+
Class,
|
|
2553
|
+
capacity,
|
|
2554
|
+
acquire: fleetAcquire,
|
|
2555
|
+
release: fleetRelease,
|
|
2556
|
+
at: fleetAt,
|
|
2557
|
+
size: fleetSize,
|
|
2558
|
+
stats: fleetStats,
|
|
2559
|
+
dispose: fleetDispose,
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2346
2563
|
function throwFlagArg(what) {
|
|
2347
2564
|
throw new TypeError(`${ERR}${what}(on) -- on must be a boolean.`);
|
|
2348
2565
|
}
|
|
@@ -2485,4 +2702,4 @@ export function auditReactive(on) {
|
|
|
2485
2702
|
// --- Version ------------------------------------------------------------------
|
|
2486
2703
|
|
|
2487
2704
|
/** Package version. Kept in lockstep with package.json and llms.txt. */
|
|
2488
|
-
export const VERSION = "1.
|
|
2705
|
+
export const VERSION = "1.5.0";
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-signal-decorators
|
|
2
2
|
|
|
3
|
-
VERSION 1.
|
|
3
|
+
VERSION 1.5.0
|
|
4
4
|
|
|
5
5
|
> Standard-decorators layer over @zakkster/lite-signal, built on the TC39
|
|
6
6
|
> decorators proposal (Stage 2.7 since 2026-05; TS 5.x / Babel 2023-11 emit
|
|
@@ -22,7 +22,7 @@ zero decorator syntax, sharing the SAME core by function identity.
|
|
|
22
22
|
slot for a poison handle, so any later read/write throws a named
|
|
23
23
|
`ReactiveDisposedError`.
|
|
24
24
|
|
|
25
|
-
## Exports (
|
|
25
|
+
## Exports (23)
|
|
26
26
|
|
|
27
27
|
- `reactive` -- `@reactive accessor x = v` (bare) or `@reactive({ equals })`
|
|
28
28
|
(factory). Declares a per-instance signal.
|
|
@@ -135,6 +135,33 @@ slot for a poison handle, so any later read/write throws a named
|
|
|
135
135
|
- `capacityFor(inventory, { headroom }?) -> RegistryConfig` -- size a
|
|
136
136
|
`createRegistry` config from `[Factory, count]` pairs. Nodes exact, links x
|
|
137
137
|
`headroom` (default 1). Fail-closed inventory validation.
|
|
138
|
+
- `createFleet(inventory, bind, opts?) -> Fleet` -- the flagship-audience fleet
|
|
139
|
+
helper over the shipped primitives (`capacityFor` + `createRegistry` +
|
|
140
|
+
park/reinit; decisions/0013 criterion (d)). COLD construction sizes a registry
|
|
141
|
+
from `inventory`, builds it, hands it to `bind(registry)` (the caller binds its
|
|
142
|
+
decorated class to that registry and returns it -- the helper never wraps or
|
|
143
|
+
redefines the class), then EAGER-constructs and PARKS one member per inventory
|
|
144
|
+
unit. Returns a handle `{ registry, Class, capacity, acquire(initials?),
|
|
145
|
+
release(vm), at(i), size(), stats(), dispose() }`. Import in the exports block:
|
|
146
|
+
`import { createFleet } from "@zakkster/lite-signal-decorators"`. LAWS: eager
|
|
147
|
+
prefill is done at construction -- `acquire` never constructs, it pops a
|
|
148
|
+
free-list and `reinitReactive`s a parked member (`initials` override the reset
|
|
149
|
+
values); `release` validates a per-fleet slot stamp and `releaseReactive`s the
|
|
150
|
+
member back to the pool. Both hot bodies allocate ZERO (a slot array + an
|
|
151
|
+
`Int32Array` free-list + a per-vm symbol slot stamp -- never a WeakMap). SIX
|
|
152
|
+
named fail-closed misuses: `acquire` at capacity throws `FleetExhaustedError`
|
|
153
|
+
(pre-checked; the registry's own `CapacityError` is unreachable, all N are
|
|
154
|
+
prealloc'd); `release` of a foreign vm throws `FleetForeignMemberError` (the
|
|
155
|
+
slot stamp is the check); a double release throws `FleetDoubleReleaseError`;
|
|
156
|
+
any call after `dispose()` throws `FleetDisposedError`; `at(i)` out of
|
|
157
|
+
`[0, capacity)` throws a `RangeError`; a `bind` that is not a function or does
|
|
158
|
+
not return a constructor throws a `TypeError`. Construction is ATOMIC: any
|
|
159
|
+
mid-prefill throw disposes the already-built members and destroys the registry
|
|
160
|
+
before rethrowing. `dispose()` disposes every member LIVE AND PARKED (park ->
|
|
161
|
+
dispose lands DISPOSED), then destroys the fleet-owned registry -- parked
|
|
162
|
+
members are disposed too, not leaked. `null` is not zero at any gate. The demo's
|
|
163
|
+
hand-rolled pool is the extracted spec (its diff went net-negative when the
|
|
164
|
+
helper replaced it).
|
|
138
165
|
- `enableLabels(on)` / `labelOf(idOrHandle, registry?) -> string | undefined` --
|
|
139
166
|
opt-in devtools labels (default OFF); per-registry `nodeId -> "Class.prop" /
|
|
140
167
|
"Class#method" / "Class@anchor"`.
|
|
@@ -142,7 +169,7 @@ slot for a poison handle, so any later read/write throws a named
|
|
|
142
169
|
`FinalizationRegistry` reports any instance GC'd without `disposeReactive`.
|
|
143
170
|
- `ReactiveDisposedError` -- `extends Error`, `name` `"ReactiveDisposedError"`,
|
|
144
171
|
fields `className` and `key`.
|
|
145
|
-
- `VERSION` -- `"1.
|
|
172
|
+
- `VERSION` -- `"1.5.0"`.
|
|
146
173
|
|
|
147
174
|
## Registry law (one registry per host chain)
|
|
148
175
|
|
|
@@ -288,7 +315,10 @@ and pays its own measured cost. 1.3.0 adds `forEachReactive` + `snapshotOf`
|
|
|
288
315
|
named in-package consumer that admits `forEachReactive` under the 0009 bar, and
|
|
289
316
|
the 1.0.0 canon stays byte-identical. 1.4.0 adds `costOfInstance` (decisions/0013
|
|
290
317
|
criterion (b); the demo is the named consumer) as an additive MINOR -> 22
|
|
291
|
-
exports; the 1.0.0 canon stays byte-identical.
|
|
318
|
+
exports; the 1.0.0 canon stays byte-identical. 1.5.0 adds `createFleet`
|
|
319
|
+
(decisions/0013 criterion (d); the demo fleet is the named consumer, its
|
|
320
|
+
hand-rolled pool DELETED for the helper) as an additive MINOR -> 23 exports --
|
|
321
|
+
the 0013 ladder CLOSES at 23; the 1.0.0 canon stays byte-identical. The semver promise from here: any change to an existing export's
|
|
292
322
|
signature or behavior is a MAJOR, recorded in a decision file; new exports are
|
|
293
323
|
minors; the hot accessor canon (`makeGet`/`makeSet`) does not move without a
|
|
294
324
|
major. Also present since
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-signal-decorators",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Standard-decorators layer over @zakkster/lite-signal (TC39 decorators proposal, Stage 2.7 since 2026-05; TS 5.x / Babel 2023-11 emit unchanged). 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",
|