@zakkster/lite-signal-decorators 0.1.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 ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@zakkster/lite-signal-decorators` are documented here.
4
+ The format follows Keep a Changelog; this project adheres to Semantic
5
+ Versioning.
6
+
7
+ ## [0.1.0] - 2026-08-26
8
+
9
+ Initial release -- the decorator core.
10
+
11
+ ### Added
12
+
13
+ - `reactive` -- `@reactive accessor x = v` (bare) / `@reactive({ equals })`
14
+ (factory): a per-instance signal box stored in a unique symbol slot with an
15
+ unbranched, allocation-free accessor body.
16
+ - `derived` -- `@derived get y()` (bare) / `@derived({ equals })` (factory): a
17
+ lazy computed owned by the instance's anchor.
18
+ - `reactiveHost` -- `@reactiveHost` (bare) / `@reactiveHost()` (factory): the
19
+ single wiring site; its most-derived constructor builds the anchor and every
20
+ derived exactly once, after all field initializers run.
21
+ - `disposeReactive(vm)` -- idempotent cascade + poison teardown; returns `true`
22
+ on the first call, `false` thereafter. Also wired to `Symbol.dispose` for
23
+ `using` blocks.
24
+ - `boxOf(vm, key)` / `rootOf(vm)` -- live box and anchor-descriptor lookups for
25
+ interop with raw lite-signal and lite-devtools.
26
+ - `ReactiveDisposedError` -- named error thrown on any post-dispose touch,
27
+ carrying `className` and `key`.
28
+ - Dispose re-entrancy guard (D-2f): calling `disposeReactive(this)` from inside
29
+ the instance's own `@derived` computation throws a named error instead of
30
+ silently dropping the derivation's value.
31
+ - `VERSION` constant (`"0.1.0"`).
32
+ - Fail-closed rejection matrix (all named, all at decoration time): legacy emit,
33
+ wrong kind, static members, private (#) members, unknown option keys (with a
34
+ nearest-key did-you-mean), non-function `equals`, double host, host options,
35
+ and orphaned members.
36
+ - Torture skeleton (`@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`):
37
+ retention, conservation, lifecycle, and zero-GC lanes.
38
+
39
+ [0.1.0]: https://github.com/zakkster/lite-signal-decorators/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zahary Shinikchiev <shinikchiev@yahoo.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @zakkster/lite-signal-decorators
2
+
3
+ > Stage-3 decorators that turn a plain class into a reactive view-model with a
4
+ > measured per-property cost, one deterministic teardown, and poison-on-dispose
5
+ > safety -- built on @zakkster/lite-signal.
6
+
7
+ ```js
8
+ import { reactive, derived, reactiveHost, disposeReactive } from "@zakkster/lite-signal-decorators";
9
+
10
+ @reactiveHost
11
+ class Vector {
12
+ @reactive accessor x = 3;
13
+ @reactive accessor y = 4;
14
+ @derived get len() { return Math.hypot(this.x, this.y); }
15
+ }
16
+
17
+ const v = new Vector();
18
+ v.len; // 5
19
+ v.x = 6;
20
+ v.len; // 7.211...
21
+ disposeReactive(v); // cascade teardown; later reads throw ReactiveDisposedError
22
+ ```
23
+
24
+ A `@reactive accessor` is a per-instance signal in a symbol slot; a `@derived
25
+ get` is a lazy computed owned by the instance's anchor; `@reactiveHost` is the
26
+ one place wiring happens. `disposeReactive` (or a `using` block) cascades the
27
+ anchor, disposes each signal box, and poisons every slot.
28
+
29
+ Requires `@zakkster/lite-signal` `>=1.5.0 <2.0.0` as a peer dependency, and a
30
+ Stage-3 decorator toolchain (TS 5 with `experimentalDecorators: false`, or Babel
31
+ `2023-11`).
32
+
33
+ The full README -- positioning, deep-dives, API reference, composability
34
+ pipeline, zero-GC design notes, and gated quality numbers -- lands at 1.0.0.
35
+
36
+ ## License
37
+
38
+ MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @zakkster/lite-signal-decorators -- Stage-3 decorator layer over
3
+ * @zakkster/lite-signal.
4
+ *
5
+ * Public type surface for the JavaScript implementation in
6
+ * `SignalDecorators.js`.
7
+ */
8
+
9
+ import type { SignalBox, ComputedBox, NodeDescriptor } from "@zakkster/lite-signal";
10
+
11
+ // --- Options ------------------------------------------------------------------
12
+
13
+ /** Custom equality predicate for a `@reactive` member. Returning `true` halts propagation. */
14
+ export interface ReactiveOptions<V> {
15
+ /** Custom equality predicate. Default: `Object.is`. */
16
+ equals?: (a: V, b: V) => boolean;
17
+ }
18
+
19
+ /** Custom equality predicate for a `@derived` member. Returning `true` halts propagation. */
20
+ export interface DerivedOptions<V> {
21
+ /** Custom equality predicate. Default: `Object.is`. */
22
+ equals?: (a: V, b: V) => boolean;
23
+ }
24
+
25
+ // --- reactive -----------------------------------------------------------------
26
+
27
+ /**
28
+ * `@reactive accessor x = v` -- declares a per-instance signal, stored in a
29
+ * unique symbol slot and read/written through an unbranched, allocation-free
30
+ * accessor body.
31
+ *
32
+ * Bare application decorates an `accessor`; the factory form validates its
33
+ * options eagerly and returns the same accessor decorator.
34
+ *
35
+ * @example
36
+ * class Counter {
37
+ * `@reactive` accessor count = 0;
38
+ * `@reactive`({ equals: (a, b) => a === b }) accessor label = "";
39
+ * }
40
+ */
41
+ export function reactive<This, V>(
42
+ target: ClassAccessorDecoratorTarget<This, V>,
43
+ ctx: ClassAccessorDecoratorContext<This, V>,
44
+ ): ClassAccessorDecoratorResult<This, V>;
45
+ export function reactive<V>(
46
+ opts?: ReactiveOptions<V>,
47
+ ): <This>(
48
+ target: ClassAccessorDecoratorTarget<This, V>,
49
+ ctx: ClassAccessorDecoratorContext<This, V>,
50
+ ) => ClassAccessorDecoratorResult<This, V>;
51
+
52
+ // --- derived ------------------------------------------------------------------
53
+
54
+ /**
55
+ * `@derived get y()` -- declares a lazy computed derived from other reactive
56
+ * members. The getter body becomes a `computedBox` owned by the instance's
57
+ * anchor and cascade-disposes with it.
58
+ *
59
+ * Bare application decorates a `getter`; the factory form validates its options
60
+ * eagerly and returns the same getter decorator.
61
+ *
62
+ * @example
63
+ * class Vector {
64
+ * `@reactive` accessor x = 3;
65
+ * `@reactive` accessor y = 4;
66
+ * `@derived` get len() { return Math.hypot(this.x, this.y); }
67
+ * }
68
+ */
69
+ export function derived<This, V>(
70
+ value: (this: This) => V,
71
+ ctx: ClassGetterDecoratorContext<This, V>,
72
+ ): (this: This) => V;
73
+ export function derived<V>(
74
+ opts?: DerivedOptions<V>,
75
+ ): <This>(
76
+ value: (this: This) => V,
77
+ ctx: ClassGetterDecoratorContext<This, V>,
78
+ ) => (this: This) => V;
79
+
80
+ // --- reactiveHost -------------------------------------------------------------
81
+
82
+ /**
83
+ * `@reactiveHost` -- the single wiring site. Wraps the class so its most-derived
84
+ * constructor builds the reactive graph (anchor + every derived) exactly once,
85
+ * after all field initializers have run.
86
+ *
87
+ * Bare application decorates the class; the zero-key factory form
88
+ * (`@reactiveHost()`) returns the same class decorator. No options are accepted
89
+ * in 0.1.0.
90
+ */
91
+ export function reactiveHost<C extends abstract new (...args: any[]) => any>(
92
+ target: C,
93
+ ctx: ClassDecoratorContext<C>,
94
+ ): C;
95
+ export function reactiveHost(): <C extends abstract new (...args: any[]) => any>(
96
+ target: C,
97
+ ctx: ClassDecoratorContext<C>,
98
+ ) => C;
99
+
100
+ // --- Lifecycle + lookups ------------------------------------------------------
101
+
102
+ /**
103
+ * Dispose a reactive instance: cascade its anchor (freeing every derived),
104
+ * dispose each signal box, and poison every slot so later touches throw
105
+ * {@link ReactiveDisposedError}. Idempotent -- a second call returns `false`
106
+ * and changes nothing. Returns `true` on the first successful dispose.
107
+ *
108
+ * @throws if `vm` is not a reactive instance, or is called before wiring.
109
+ */
110
+ export function disposeReactive(vm: object): boolean;
111
+
112
+ /**
113
+ * Return the live {@link SignalBox} / {@link ComputedBox} backing a reactive
114
+ * member -- for interop with raw lite-signal code and devtools.
115
+ *
116
+ * @throws {ReactiveDisposedError} if the instance was disposed.
117
+ * @throws if `key` is not a reactive member (with a did-you-mean hint), or `vm`
118
+ * is not a reactive instance.
119
+ */
120
+ export function boxOf<T = unknown>(vm: object, key: PropertyKey): SignalBox<T> | ComputedBox<T>;
121
+
122
+ /**
123
+ * Return the instance's anchor {@link NodeDescriptor} -- feeds
124
+ * `forEachOwned` / lite-devtools directly.
125
+ *
126
+ * @throws {ReactiveDisposedError} if the instance was disposed.
127
+ * @throws if `vm` is not wired yet, or is not a reactive instance.
128
+ */
129
+ export function rootOf(vm: object): NodeDescriptor;
130
+
131
+ // --- Errors -------------------------------------------------------------------
132
+
133
+ /**
134
+ * Thrown when a disposed reactive member (or root) is read or written. Carries
135
+ * the originating class name and the member key for actionable diagnostics.
136
+ */
137
+ export class ReactiveDisposedError extends Error {
138
+ constructor(className: string, key: PropertyKey);
139
+ /** The class whose instance was disposed. */
140
+ className: string;
141
+ /** The reactive member key that was touched after disposal, or `"<root>"`. */
142
+ key: PropertyKey;
143
+ }
144
+
145
+ // --- Version ------------------------------------------------------------------
146
+
147
+ /** Package version. Kept in lockstep with package.json and llms.txt. */
148
+ export const VERSION: string;
@@ -0,0 +1,737 @@
1
+ /**
2
+ * @zakkster/lite-signal-decorators v0.1.0
3
+ * --------------------
4
+ * Stage-3 decorator layer over @zakkster/lite-signal. Turns a plain class into
5
+ * a reactive view-model with measured per-instance cost and deterministic
6
+ * teardown:
7
+ * - `@reactive accessor x = v` -- a per-instance signal box, stored in a
8
+ * unique symbol slot (0003 S-A), read/written through an unbranched body.
9
+ * - `@derived get y()` -- a lazy computedBox, owned by the instance's
10
+ * anchor so it cascade-disposes with the instance.
11
+ * - `@reactiveHost` -- the single wiring site (0001 D-1b): its
12
+ * most-derived constructor builds the R-A anchor and every derived once.
13
+ * - `disposeReactive(vm)` -- idempotent cascade teardown + poison swap
14
+ * (0002 D-2d); a disposed slot throws ReactiveDisposedError on touch.
15
+ *
16
+ * Ownership model (0002): one detached anchor per instance owns all deriveds;
17
+ * signal boxes are created bare (not adopted) and disposed explicitly. The
18
+ * accessor get/set/derived-get bodies carry exactly one slot load + one
19
+ * monomorphic box call -- zero branches, zero allocation (0003 hot-body canon).
20
+ * The wiring/register/dispose core is decorator-agnostic so `defineReactive`
21
+ * (S2, 0005) can become its second caller without a second implementation.
22
+ *
23
+ * ESM-only. Zero runtime deps beyond the peer @zakkster/lite-signal.
24
+ *
25
+ * MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
26
+ */
27
+
28
+ import {
29
+ signalBox,
30
+ computedBox,
31
+ effect,
32
+ createRoot,
33
+ getOwner,
34
+ runWithOwner,
35
+ dispose,
36
+ nodeId,
37
+ isTracking,
38
+ } from "@zakkster/lite-signal";
39
+
40
+ // --- Module state -------------------------------------------------------------
41
+
42
+ // PENDING holds records pushed by member decorators (@reactive/@derived) until
43
+ // the class decorator (@reactiveHost) claims them (PD-1). Members and the class
44
+ // of one class definition evaluate in one synchronous sequence, so records from
45
+ // two classes never interleave; they only linger if a class had reactive
46
+ // members but no @reactiveHost -- caught by PD-2/PD-3.
47
+ const PENDING = [];
48
+
49
+ // The plan store (0001 D-1c): module WeakMap keyed by the wrapper constructor.
50
+ const PLANS = new WeakMap();
51
+
52
+ // One module symbol whose VALUE on a constructor is the wrapper itself. `new W`
53
+ // wires iff `new.target[HOST_MARK] === W`, so only the deepest host wires (PD-5).
54
+ const HOST_MARK = Symbol("lite-signal-decorators.host");
55
+
56
+ // The instance's anchor NodeDescriptor lives here; DISPOSED after teardown.
57
+ const ANCHOR = Symbol("lite-signal-decorators.anchor");
58
+
59
+ // Marks the poison/prewired handles so boxOf/rootOf recognize them without
60
+ // calling get() (PD-4): value is "disposed" or "prewired".
61
+ const NONLIVE = Symbol("lite-signal-decorators.nonlive");
62
+
63
+ // Frozen sentinel written to ANCHOR on dispose (idempotency signal, PD-7).
64
+ const DISPOSED = Object.freeze({ [NONLIVE]: "disposed" });
65
+
66
+ // Known option keys (PD-8 unknown-key did-you-mean set).
67
+ const KNOWN_OPTION_KEYS = ["equals"];
68
+
69
+ // --- ReactiveDisposedError ----------------------------------------------------
70
+
71
+ /**
72
+ * Thrown when a disposed reactive member (or root) is read or written. Carries
73
+ * the originating class name and the member key for actionable diagnostics.
74
+ */
75
+ export class ReactiveDisposedError extends Error {
76
+ constructor(className, key) {
77
+ super(
78
+ "@zakkster/lite-signal-decorators: " +
79
+ className +
80
+ "." +
81
+ String(key) +
82
+ " was used after disposeReactive() -- the reactive graph is gone",
83
+ );
84
+ this.name = "ReactiveDisposedError";
85
+ this.className = className;
86
+ this.key = key;
87
+ }
88
+ }
89
+
90
+ // --- Error-throw helpers (cold; message building allocates only here) ---------
91
+
92
+ function keyLabel(key) {
93
+ return String(key);
94
+ }
95
+
96
+ function nearestKey(bad, known) {
97
+ let best = null;
98
+ let bestDist = Infinity;
99
+ for (let i = 0; i < known.length; i++) {
100
+ const d = editDistance(bad, known[i]);
101
+ if (d < bestDist) {
102
+ bestDist = d;
103
+ best = known[i];
104
+ }
105
+ }
106
+ return best;
107
+ }
108
+
109
+ function editDistance(a, b) {
110
+ const al = a.length;
111
+ const bl = b.length;
112
+ if (al === 0) return bl;
113
+ if (bl === 0) return al;
114
+ let prev = new Array(bl + 1);
115
+ let cur = new Array(bl + 1);
116
+ for (let j = 0; j <= bl; j++) prev[j] = j;
117
+ for (let i = 1; i <= al; i++) {
118
+ cur[0] = i;
119
+ const ac = a.charCodeAt(i - 1);
120
+ for (let j = 1; j <= bl; j++) {
121
+ const cost = ac === b.charCodeAt(j - 1) ? 0 : 1;
122
+ let m = prev[j] + 1;
123
+ const ins = cur[j - 1] + 1;
124
+ if (ins < m) m = ins;
125
+ const sub = prev[j - 1] + cost;
126
+ if (sub < m) m = sub;
127
+ cur[j] = m;
128
+ }
129
+ const tmp = prev;
130
+ prev = cur;
131
+ cur = tmp;
132
+ }
133
+ return prev[bl];
134
+ }
135
+
136
+ function throwLegacyEmit(what) {
137
+ throw new TypeError(
138
+ "@zakkster/lite-signal-decorators: " +
139
+ what +
140
+ " received a legacy decorator call (2nd arg is a property key, not a" +
141
+ " standard context). Compile with standard decorators (TS 5" +
142
+ ' `experimentalDecorators: false` / Babel `2023-11`).',
143
+ );
144
+ }
145
+
146
+ function throwWrongKind(what, wantKind, gotKind, fix) {
147
+ throw new TypeError(
148
+ "@zakkster/lite-signal-decorators: " +
149
+ what +
150
+ ' expects to decorate a member of kind "' +
151
+ wantKind +
152
+ '", but got kind "' +
153
+ gotKind +
154
+ '". ' +
155
+ fix,
156
+ );
157
+ }
158
+
159
+ function throwStatic(what, name) {
160
+ throw new TypeError(
161
+ "@zakkster/lite-signal-decorators: " +
162
+ what +
163
+ " cannot decorate the static member " +
164
+ String(name) +
165
+ " -- module-level signals are raw lite-signal territory.",
166
+ );
167
+ }
168
+
169
+ function throwPrivate(what, name) {
170
+ throw new TypeError(
171
+ "@zakkster/lite-signal-decorators: " +
172
+ what +
173
+ " cannot decorate the private (#) member " +
174
+ String(name) +
175
+ " -- private (#) members are not supported in 0.1.0.",
176
+ );
177
+ }
178
+
179
+ function throwBadEquals(what) {
180
+ throw new TypeError(
181
+ "@zakkster/lite-signal-decorators: " +
182
+ what +
183
+ " option `equals` must be a function (a, b) -> boolean.",
184
+ );
185
+ }
186
+
187
+ function throwUnknownOption(what, key) {
188
+ const near = nearestKey(String(key), KNOWN_OPTION_KEYS);
189
+ throw new TypeError(
190
+ "@zakkster/lite-signal-decorators: " +
191
+ what +
192
+ " got unknown option `" +
193
+ String(key) +
194
+ "`" +
195
+ (near ? " -- did you mean `" + near + "`?" : "") +
196
+ " Known options: " +
197
+ KNOWN_OPTION_KEYS.join(", ") +
198
+ ".",
199
+ );
200
+ }
201
+
202
+ function throwHostOptions(key) {
203
+ throw new TypeError(
204
+ "@zakkster/lite-signal-decorators: reactiveHost takes no options in" +
205
+ " 0.1.0" +
206
+ (key !== undefined ? " (got `" + String(key) + "`)" : "") +
207
+ ".",
208
+ );
209
+ }
210
+
211
+ function throwUsage(what) {
212
+ throw new TypeError(
213
+ "@zakkster/lite-signal-decorators: " +
214
+ what +
215
+ " was called with an unrecognized argument shape. Apply it as a bare" +
216
+ " decorator (`@" +
217
+ what +
218
+ "`) or a factory (`@" +
219
+ what +
220
+ "({ ... })`).",
221
+ );
222
+ }
223
+
224
+ function throwOrphans(ctorName, keys) {
225
+ throw new Error(
226
+ "@zakkster/lite-signal-decorators: class " +
227
+ ctorName +
228
+ " claimed reactive members that were never installed on its" +
229
+ " prototype (keys: " +
230
+ keys.join(", ") +
231
+ "). An earlier class used @reactive/@derived without @reactiveHost.",
232
+ );
233
+ }
234
+
235
+ function throwDuplicateKey(ctorName, key) {
236
+ throw new Error(
237
+ "@zakkster/lite-signal-decorators: reactive member " +
238
+ keyLabel(key) +
239
+ " is declared twice across the prototype chain of " +
240
+ ctorName +
241
+ " -- subclass redeclaration of a reactive member is not supported.",
242
+ );
243
+ }
244
+
245
+ function throwDoubleHost(name) {
246
+ throw new Error(
247
+ "@zakkster/lite-signal-decorators: class " +
248
+ name +
249
+ " already has a @reactiveHost wrapper -- do not apply @reactiveHost" +
250
+ " twice.",
251
+ );
252
+ }
253
+
254
+ function throwMissingHost(rec) {
255
+ throw new Error(
256
+ "@zakkster/lite-signal-decorators: reactive member " +
257
+ keyLabel(rec.key) +
258
+ " was constructed without a @reactiveHost -- add @reactiveHost to" +
259
+ " the class that declares it.",
260
+ );
261
+ }
262
+
263
+ function throwNoPlan(what) {
264
+ throw new Error(
265
+ "@zakkster/lite-signal-decorators: " +
266
+ what +
267
+ " received a value that is not a reactive instance (no @reactiveHost" +
268
+ " plan on its constructor chain).",
269
+ );
270
+ }
271
+
272
+ function throwNotWired(what) {
273
+ throw new Error(
274
+ "@zakkster/lite-signal-decorators: " +
275
+ what +
276
+ " called on an instance that is not wired -- called during" +
277
+ " construction, or not a reactive instance.",
278
+ );
279
+ }
280
+
281
+ function throwSelfDisposeInDerived(ctorName, key) {
282
+ throw new Error(
283
+ "@zakkster/lite-signal-decorators: disposeReactive(" +
284
+ ctorName +
285
+ ") was called from inside its own @derived " +
286
+ keyLabel(key) +
287
+ " computation -- derived getters must be pure. Dispose from an" +
288
+ " effect, a subscription, or plain code instead.",
289
+ );
290
+ }
291
+
292
+ function throwUnknownMember(ctorName, key, plan) {
293
+ const avail = [];
294
+ const it = plan.byKey.keys();
295
+ for (let e = it.next(); !e.done; e = it.next()) avail.push(keyLabel(e.value));
296
+ const near = nearestKey(keyLabel(key), avail);
297
+ throw new Error(
298
+ "@zakkster/lite-signal-decorators: boxOf(" +
299
+ ctorName +
300
+ ", " +
301
+ keyLabel(key) +
302
+ ") -- no such reactive member" +
303
+ (near ? " -- did you mean `" + near + "`?" : "") +
304
+ " Available: " +
305
+ avail.join(", ") +
306
+ ".",
307
+ );
308
+ }
309
+
310
+ function throwPrewiredMember(ctorName, key) {
311
+ throw new Error(
312
+ "@zakkster/lite-signal-decorators: " +
313
+ ctorName +
314
+ "." +
315
+ keyLabel(key) +
316
+ " is not yet wired -- accessed before construction completed.",
317
+ );
318
+ }
319
+
320
+ // --- Hot-body factories (section-2 canon; reviewer diffs byte-for-byte) -------
321
+
322
+ function makeGet(slot) { return function () { return this[slot].get(); }; }
323
+ function makeSet(slot) { return function (v) { this[slot].set(v); }; }
324
+ function makeDerivedGet(slot) { return function () { return this[slot].get(); }; }
325
+
326
+ function makeInit(rec) {
327
+ return function (v) {
328
+ if (rec.plan === null) throwMissingHost(rec);
329
+ this[rec.slot] = signalBox(v, rec.opts);
330
+ return v; // emitter backing store, unused
331
+ };
332
+ }
333
+
334
+ // --- Option validation (cold) -------------------------------------------------
335
+
336
+ function isStandardContext(c) {
337
+ return typeof c === "object" && c !== null && typeof c.kind === "string";
338
+ }
339
+
340
+ function validateOptions(what, opts) {
341
+ // Returns a frozen { equals } copy, or undefined for the bare form. Never
342
+ // aliases the caller's object.
343
+ if (opts === undefined || opts === null) return undefined;
344
+ if (typeof opts !== "object") throwUsage(what);
345
+ const keys = Object.keys(opts);
346
+ for (let i = 0; i < keys.length; i++) {
347
+ if (KNOWN_OPTION_KEYS.indexOf(keys[i]) === -1) throwUnknownOption(what, keys[i]);
348
+ }
349
+ if ("equals" in opts && opts.equals !== undefined && typeof opts.equals !== "function") throwBadEquals(what);
350
+ if (opts.equals === undefined) return undefined;
351
+ return Object.freeze({ equals: opts.equals });
352
+ }
353
+
354
+ // --- reactive -----------------------------------------------------------------
355
+
356
+ function applyReactive(target, ctx, opts) {
357
+ if (!isStandardContext(ctx)) throwLegacyEmit("reactive");
358
+ if (ctx.kind !== "accessor") {
359
+ throwWrongKind("reactive", "accessor", ctx.kind, "write `@reactive accessor x = ...`.");
360
+ }
361
+ if (ctx.static === true) throwStatic("reactive", ctx.name);
362
+ if (ctx.private === true) throwPrivate("reactive", ctx.name);
363
+ const slot = Symbol(typeof ctx.name === "symbol" ? "reactive" : "reactive:" + String(ctx.name));
364
+ const rec = {
365
+ kind: "signal",
366
+ key: ctx.name,
367
+ slot,
368
+ get: makeGet(slot),
369
+ set: makeSet(slot),
370
+ fn: null,
371
+ opts,
372
+ plan: null,
373
+ poison: null,
374
+ prewired: null,
375
+ };
376
+ PENDING.push(rec);
377
+ return { get: rec.get, set: rec.set, init: makeInit(rec) };
378
+ }
379
+
380
+ /**
381
+ * `@reactive accessor x = v` -- declares a per-instance signal. Bare or as a
382
+ * factory `@reactive({ equals })`.
383
+ */
384
+ export function reactive(target, ctx) {
385
+ // A decorator APPLICATION always passes >= 2 args (target, context); a
386
+ // FACTORY call passes 0-1 (options or nothing). applyReactive re-checks the
387
+ // context, so a legacy 2-arg call (string context) throws the legacy error.
388
+ if (arguments.length >= 2) return applyReactive(target, ctx, undefined);
389
+ const opts = validateOptions("reactive", target);
390
+ return function (t, c) { return applyReactive(t, c, opts); };
391
+ }
392
+
393
+ // --- derived ------------------------------------------------------------------
394
+
395
+ function applyDerived(value, ctx, opts) {
396
+ if (!isStandardContext(ctx)) throwLegacyEmit("derived");
397
+ if (ctx.kind !== "getter") {
398
+ throwWrongKind("derived", "getter", ctx.kind, "write `@derived get y() { ... }`.");
399
+ }
400
+ if (ctx.static === true) throwStatic("derived", ctx.name);
401
+ if (ctx.private === true) throwPrivate("derived", ctx.name);
402
+ const slot = Symbol(typeof ctx.name === "symbol" ? "derived" : "derived:" + String(ctx.name));
403
+ const rec = {
404
+ kind: "derived",
405
+ key: ctx.name,
406
+ slot,
407
+ get: makeDerivedGet(slot),
408
+ set: undefined,
409
+ fn: value,
410
+ opts,
411
+ plan: null,
412
+ poison: null,
413
+ prewired: null,
414
+ };
415
+ PENDING.push(rec);
416
+ // PD-3: a derived getter has no init, so register one initializer whose body
417
+ // fails closed if the class was never hosted.
418
+ ctx.addInitializer(function () {
419
+ if (rec.plan === null) throwMissingHost(rec);
420
+ });
421
+ return rec.get;
422
+ }
423
+
424
+ /**
425
+ * `@derived get y()` -- declares a lazy computed derived from other reactive
426
+ * members. Bare or as a factory `@derived({ equals })`.
427
+ */
428
+ export function derived(value, ctx) {
429
+ if (arguments.length >= 2) return applyDerived(value, ctx, undefined);
430
+ const opts = validateOptions("derived", value);
431
+ return function (v, c) { return applyDerived(v, c, opts); };
432
+ }
433
+
434
+ // --- Claim + plan (PD-1/2/6) --------------------------------------------------
435
+
436
+ function buildHandles(rec, ctorName) {
437
+ // Poison: thrown after dispose. Prewired: thrown before wiring installs the
438
+ // own slot. Both frozen, both carrying NONLIVE for marker-tag recognition.
439
+ const key = rec.key;
440
+ rec.poison = Object.freeze({
441
+ [NONLIVE]: "disposed",
442
+ get() { throw new ReactiveDisposedError(ctorName, key); },
443
+ set(v) { throw new ReactiveDisposedError(ctorName, key); },
444
+ });
445
+ if (rec.kind === "signal") {
446
+ rec.prewired = Object.freeze({
447
+ [NONLIVE]: "prewired",
448
+ get() {
449
+ throw new TypeError(
450
+ "@zakkster/lite-signal-decorators: " + ctorName + "." + keyLabel(key) +
451
+ " read/write before its initializer ran (declaration order).",
452
+ );
453
+ },
454
+ set(v) {
455
+ throw new TypeError(
456
+ "@zakkster/lite-signal-decorators: " + ctorName + "." + keyLabel(key) +
457
+ " read/write before its initializer ran (declaration order).",
458
+ );
459
+ },
460
+ });
461
+ } else {
462
+ rec.prewired = Object.freeze({
463
+ [NONLIVE]: "prewired",
464
+ get() {
465
+ throw new TypeError(
466
+ "@zakkster/lite-signal-decorators: " + ctorName + "." + keyLabel(key) +
467
+ " read before construction completed (deriveds are available after wiring).",
468
+ );
469
+ },
470
+ set(v) {
471
+ throw new TypeError(
472
+ "@zakkster/lite-signal-decorators: " + ctorName + "." + keyLabel(key) +
473
+ " read before construction completed (deriveds are available after wiring).",
474
+ );
475
+ },
476
+ });
477
+ }
478
+ }
479
+
480
+ function nearestAncestorPlan(C) {
481
+ let p = Object.getPrototypeOf(C);
482
+ while (p !== null && p !== Function.prototype) {
483
+ const plan = PLANS.get(p);
484
+ if (plan !== undefined) return plan;
485
+ p = Object.getPrototypeOf(p);
486
+ }
487
+ return undefined;
488
+ }
489
+
490
+ function claimPlan(C, ctorName) {
491
+ // Drain PENDING wholly FIRST so a broken earlier class cannot poison every
492
+ // later claim forever (PD-2). Validate identity against C.prototype after.
493
+ const own = PENDING.splice(0, PENDING.length);
494
+ const proto = C.prototype;
495
+ const orphans = [];
496
+ for (let i = 0; i < own.length; i++) {
497
+ const rec = own[i];
498
+ const desc = Object.getOwnPropertyDescriptor(proto, rec.key);
499
+ if (desc === undefined || desc.get !== rec.get) orphans.push(keyLabel(rec.key));
500
+ }
501
+ if (orphans.length > 0) throwOrphans(ctorName, orphans);
502
+
503
+ const ancestor = nearestAncestorPlan(C);
504
+ const signals = [];
505
+ const deriveds = [];
506
+ const byKey = new Map();
507
+ if (ancestor !== undefined) {
508
+ for (let i = 0; i < ancestor.signals.length; i++) {
509
+ const r = ancestor.signals[i];
510
+ signals.push(r);
511
+ byKey.set(r.key, r);
512
+ }
513
+ for (let i = 0; i < ancestor.deriveds.length; i++) {
514
+ const r = ancestor.deriveds[i];
515
+ deriveds.push(r);
516
+ byKey.set(r.key, r);
517
+ }
518
+ }
519
+ for (let i = 0; i < own.length; i++) {
520
+ const rec = own[i];
521
+ if (byKey.has(rec.key)) throwDuplicateKey(ctorName, rec.key);
522
+ buildHandles(rec, ctorName);
523
+ if (rec.kind === "signal") signals.push(rec);
524
+ else deriveds.push(rec);
525
+ byKey.set(rec.key, rec);
526
+ }
527
+
528
+ const plan = {
529
+ ctorName,
530
+ signals: Object.freeze(signals),
531
+ deriveds: Object.freeze(deriveds),
532
+ byKey,
533
+ };
534
+ Object.freeze(plan);
535
+ // Records get their plan pointer (PD-3 missing-host check) then freeze.
536
+ for (let i = 0; i < own.length; i++) {
537
+ own[i].plan = plan;
538
+ Object.freeze(own[i]);
539
+ }
540
+ return plan;
541
+ }
542
+
543
+ // --- Wiring core (decorator-agnostic; 0005) -----------------------------------
544
+
545
+ function makeDerivedBody(inst, fn) { return function () { return fn.call(inst); }; }
546
+
547
+ function wireInstance(inst, plan) {
548
+ let a;
549
+ createRoot(() => { effect(() => { a = getOwner(); }); }); // R-A anchor
550
+ inst[ANCHOR] = a;
551
+ try {
552
+ runWithOwner(a, () => {
553
+ const ders = plan.deriveds;
554
+ for (let i = 0; i < ders.length; i++) {
555
+ const d = ders[i];
556
+ inst[d.slot] = computedBox(makeDerivedBody(inst, d.fn), d.opts);
557
+ }
558
+ });
559
+ } catch (e) {
560
+ disposeCore(inst, plan); // conservation intact
561
+ throw e; // CapacityError propagates as-is
562
+ }
563
+ }
564
+
565
+ function disposeCore(inst, plan) { // assumes not already disposed
566
+ const a = inst[ANCHOR];
567
+ if (a !== undefined && a !== DISPOSED) dispose(a); // cascades owned deriveds
568
+ const sigs = plan.signals;
569
+ for (let i = 0; i < sigs.length; i++) {
570
+ const r = sigs[i];
571
+ const box = inst[r.slot];
572
+ if (box !== undefined && box[NONLIVE] === undefined) dispose(box);
573
+ inst[r.slot] = r.poison;
574
+ }
575
+ const ders = plan.deriveds;
576
+ for (let i = 0; i < ders.length; i++) {
577
+ const r = ders[i];
578
+ inst[r.slot] = r.poison; // cboxes already cascaded
579
+ }
580
+ inst[ANCHOR] = DISPOSED;
581
+ }
582
+
583
+ // --- reactiveHost -------------------------------------------------------------
584
+
585
+ function applyReactiveHost(C, ctx) {
586
+ if (!isStandardContext(ctx)) throwLegacyEmit("reactiveHost");
587
+ if (ctx.kind !== "class") {
588
+ throwWrongKind("reactiveHost", "class", ctx.kind, "apply `@reactiveHost` to the class.");
589
+ }
590
+ if (Object.prototype.hasOwnProperty.call(C, HOST_MARK)) throwDoubleHost(ctx.name || C.name);
591
+
592
+ const ctorName = ctx.name || C.name;
593
+ const plan = claimPlan(C, ctorName); // PD-1/2/6
594
+
595
+ // PD-4: prewired proto slots -- named errors before wiring, shadowed after.
596
+ for (let i = 0; i < plan.signals.length; i++) {
597
+ const r = plan.signals[i];
598
+ Object.defineProperty(C.prototype, r.slot, {
599
+ value: r.prewired,
600
+ writable: true,
601
+ configurable: true,
602
+ enumerable: false,
603
+ });
604
+ }
605
+ for (let i = 0; i < plan.deriveds.length; i++) {
606
+ const r = plan.deriveds[i];
607
+ Object.defineProperty(C.prototype, r.slot, {
608
+ value: r.prewired,
609
+ writable: true,
610
+ configurable: true,
611
+ enumerable: false,
612
+ });
613
+ }
614
+
615
+ class W extends C {
616
+ constructor(...args) {
617
+ super(...args);
618
+ if (new.target[HOST_MARK] === W) wireInstance(this, plan);
619
+ }
620
+ }
621
+ Object.defineProperty(W, HOST_MARK, { value: W });
622
+ Object.defineProperty(W, "name", { value: ctorName });
623
+ // Symbol.dispose reached runtimes after Node 18 (the engines floor); without
624
+ // the guard, older nodes would define a stray "undefined" key and `using`
625
+ // would silently no-op instead of being observably absent.
626
+ if (typeof Symbol.dispose === "symbol") {
627
+ Object.defineProperty(W.prototype, Symbol.dispose, {
628
+ value: function () { disposeReactive(this); },
629
+ writable: true,
630
+ configurable: true,
631
+ });
632
+ }
633
+ PLANS.set(W, plan);
634
+ return W;
635
+ }
636
+
637
+ /**
638
+ * `@reactiveHost` -- the single wiring site. Wraps the class so its most-derived
639
+ * constructor builds the anchor and every derived exactly once. Bare or as a
640
+ * zero-key factory `@reactiveHost()`.
641
+ */
642
+ export function reactiveHost(C, ctx) {
643
+ // Standard class-decorator application passes 2 args (class, context).
644
+ if (arguments.length >= 2) return applyReactiveHost(C, ctx);
645
+ // A legacy class decorator is called with just the constructor (1 arg).
646
+ if (typeof C === "function") throwLegacyEmit("reactiveHost");
647
+ // Factory form: bare (), ({}), (undefined) only -- no keys in 0.1.0.
648
+ if (C !== undefined && C !== null) {
649
+ if (typeof C !== "object") throwUsage("reactiveHost");
650
+ const keys = Object.keys(C);
651
+ if (keys.length > 0) throwHostOptions(keys[0]);
652
+ }
653
+ return function (cls, c) { return applyReactiveHost(cls, c); };
654
+ }
655
+
656
+ // --- Lookups (PD-9) -----------------------------------------------------------
657
+
658
+ function planOf(vm) {
659
+ if (vm === null || vm === undefined) return undefined;
660
+ let c = vm.constructor;
661
+ while (c !== null && c !== undefined && c !== Function.prototype) {
662
+ const plan = PLANS.get(c);
663
+ if (plan !== undefined) return plan;
664
+ c = Object.getPrototypeOf(c);
665
+ }
666
+ return undefined;
667
+ }
668
+
669
+ /**
670
+ * Dispose a reactive instance: cascade its anchor, dispose each signal box, and
671
+ * poison every slot. Idempotent -- a second call returns `false` and changes
672
+ * nothing. Returns `true` on the first successful dispose.
673
+ */
674
+ export function disposeReactive(vm) {
675
+ const plan = planOf(vm);
676
+ if (plan === undefined) throwNoPlan("disposeReactive");
677
+ const a = vm[ANCHOR];
678
+ if (a === DISPOSED) return false; // idempotent no-op
679
+ if (a === undefined) throwNotWired("disposeReactive");
680
+ // Re-entrancy guard (D-2f): disposing this instance from inside one of its
681
+ // OWN @derived computations would cascade the very node being computed, and
682
+ // the engine would silently drop the freshly computed value (fail-open).
683
+ // The isTracking() gate keeps the plain-code dispose path byte-identical and
684
+ // zero-alloc; only under an active tracking context do we pay one getOwner()
685
+ // descriptor to check whether the current computation is one of our deriveds.
686
+ if (isTracking()) {
687
+ const cur = getOwner();
688
+ if (cur !== undefined) {
689
+ const ders = plan.deriveds;
690
+ for (let i = 0; i < ders.length; i++) {
691
+ const h = vm[ders[i].slot];
692
+ if (h !== undefined && h[NONLIVE] === undefined && nodeId(h) === cur.id) {
693
+ throwSelfDisposeInDerived(plan.ctorName, ders[i].key);
694
+ }
695
+ }
696
+ }
697
+ }
698
+ disposeCore(vm, plan);
699
+ return true;
700
+ }
701
+
702
+ /**
703
+ * Return the live SignalBox/ComputedBox backing a reactive member. Throws
704
+ * `ReactiveDisposedError` if the instance was disposed, and a named error for an
705
+ * unknown key or a non-reactive value.
706
+ */
707
+ export function boxOf(vm, key) {
708
+ const plan = planOf(vm);
709
+ if (plan === undefined) throwNoPlan("boxOf");
710
+ const rec = plan.byKey.get(key);
711
+ if (rec === undefined) throwUnknownMember(plan.ctorName, key, plan);
712
+ const h = vm[rec.slot];
713
+ if (h === undefined || h === null) throwNotWired("boxOf");
714
+ const nl = h[NONLIVE];
715
+ if (nl === "disposed") throw new ReactiveDisposedError(plan.ctorName, key);
716
+ if (nl === "prewired") throwPrewiredMember(plan.ctorName, key);
717
+ return h;
718
+ }
719
+
720
+ /**
721
+ * Return the instance's anchor NodeDescriptor -- feeds `forEachOwned`/devtools.
722
+ * Throws `ReactiveDisposedError` after dispose, and a named error before wiring
723
+ * or on a non-reactive value.
724
+ */
725
+ export function rootOf(vm) {
726
+ const plan = planOf(vm);
727
+ if (plan === undefined) throwNoPlan("rootOf");
728
+ const a = vm[ANCHOR];
729
+ if (a === undefined) throwNotWired("rootOf");
730
+ if (a === DISPOSED) throw new ReactiveDisposedError(plan.ctorName, "<root>");
731
+ return a;
732
+ }
733
+
734
+ // --- Version ------------------------------------------------------------------
735
+
736
+ /** Package version. Kept in lockstep with package.json and llms.txt. */
737
+ export const VERSION = "0.1.0";
package/llms.txt ADDED
@@ -0,0 +1,66 @@
1
+ # @zakkster/lite-signal-decorators
2
+
3
+ VERSION 0.1.0
4
+
5
+ > Stage-3 decorator layer over @zakkster/lite-signal. Turns a plain class into a
6
+ > reactive view-model where each instance has a measured per-property cost, a
7
+ > single deterministic teardown, and poison-on-dispose safety. ESM-only, zero
8
+ > runtime dependencies beyond the peer. The accessor read/write bodies carry one
9
+ > slot load + one monomorphic box call -- zero branches, zero allocation.
10
+
11
+ A `@reactive accessor` becomes a per-instance signal box stored in a unique
12
+ symbol slot. A `@derived get` becomes a lazy computed owned by the instance's
13
+ anchor. `@reactiveHost` is the one place wiring happens -- its most-derived
14
+ constructor builds the anchor and every derived exactly once, after all fields
15
+ initialize. `disposeReactive` cascades that anchor, disposes each signal box,
16
+ and swaps every slot for a poison handle, so any later read/write throws a named
17
+ `ReactiveDisposedError`.
18
+
19
+ ## Exports (8)
20
+
21
+ - `reactive` -- `@reactive accessor x = v` (bare) or `@reactive({ equals })`
22
+ (factory). Declares a per-instance signal.
23
+ - `derived` -- `@derived get y()` (bare) or `@derived({ equals })` (factory).
24
+ Declares a lazy computed derived from other reactive members.
25
+ - `reactiveHost` -- `@reactiveHost` (bare) or `@reactiveHost()` (factory). The
26
+ single wiring site; wraps the class. No options in 0.1.0.
27
+ - `disposeReactive(vm) -> boolean` -- cascade + poison teardown. Idempotent: a
28
+ second call returns `false` and changes nothing.
29
+ - `boxOf(vm, key) -> SignalBox | ComputedBox` -- the live box behind a member;
30
+ throws with a did-you-mean on an unknown key, `ReactiveDisposedError` after
31
+ dispose.
32
+ - `rootOf(vm) -> NodeDescriptor` -- the instance's anchor descriptor; feeds
33
+ `forEachOwned` / lite-devtools. Throws `ReactiveDisposedError` after dispose.
34
+ - `ReactiveDisposedError` -- `extends Error`, `name` `"ReactiveDisposedError"`,
35
+ fields `className` and `key`.
36
+ - `VERSION` -- `"0.1.0"`.
37
+
38
+ ## Disposal law
39
+
40
+ One anchor per instance owns all deriveds; signal boxes are created bare (not
41
+ adopted) and disposed explicitly. `disposeReactive` (or a `using` block via
42
+ `Symbol.dispose`) is the only lifecycle owner. Dispose is allocation-free on the
43
+ success path and idempotent. After dispose, every member touch, `boxOf`, and
44
+ `rootOf` throw `ReactiveDisposedError` naming `Class.key`. Calling
45
+ `disposeReactive(this)` from inside the instance's own `@derived` computation
46
+ throws a named error -- derived getters must be pure; dispose from an effect, a
47
+ subscription, or plain code instead.
48
+
49
+ ## Rejections (all named, all fail-closed, all at decoration time)
50
+
51
+ Legacy emit, wrong decorator kind, static members, private (#) members, unknown
52
+ option keys (with a nearest-key did-you-mean), non-function `equals`, double
53
+ `@reactiveHost`, host options, and orphaned members (reactive members whose
54
+ class never got `@reactiveHost`) each throw a named error.
55
+
56
+ ## Peer range
57
+
58
+ Requires `@zakkster/lite-signal` `>=1.5.0 <2.0.0` (peer dependency). Uses
59
+ `signalBox`, `computedBox`, `effect`, `createRoot`, `getOwner`, `runWithOwner`,
60
+ and `dispose` from it.
61
+
62
+ ## Scope note
63
+
64
+ 0.1.0 ships the decorator core. Not yet included: `@reactiveEffect`, `batched`,
65
+ `defineReactive` (buildless co-export), sized-registry host options, cost
66
+ introspection, labels, and private-member support.
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@zakkster/lite-signal-decorators",
3
+ "version": "0.1.0",
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
+ "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./SignalDecorators.js",
9
+ "module": "./SignalDecorators.js",
10
+ "types": "./SignalDecorators.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./SignalDecorators.d.ts",
14
+ "node": "./SignalDecorators.js",
15
+ "import": "./SignalDecorators.js",
16
+ "default": "./SignalDecorators.js"
17
+ }
18
+ },
19
+ "sideEffects": false,
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": [
24
+ "signal",
25
+ "signals",
26
+ "reactive",
27
+ "reactivity",
28
+ "decorators",
29
+ "stage-3",
30
+ "computed",
31
+ "derived",
32
+ "class",
33
+ "view-model",
34
+ "zero-gc",
35
+ "zero-allocation",
36
+ "dispose",
37
+ "lifecycle",
38
+ "lite-signal"
39
+ ],
40
+ "files": [
41
+ "SignalDecorators.js",
42
+ "SignalDecorators.d.ts",
43
+ "llms.txt",
44
+ "CHANGELOG.md",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "scripts": {
49
+ "test": "node --test --test-reporter=spec 'test/*.test.mjs'",
50
+ "test:gc": "node --expose-gc --test --test-reporter=spec 'test/*.test.mjs'",
51
+ "fixtures": "node test/fixtures/regen.mjs",
52
+ "torture": "node test/torture/run.mjs",
53
+ "torture:semantic": "node test/torture/run.mjs --group semantic",
54
+ "torture:soak": "node test/torture/run.mjs --group soak",
55
+ "torture:controls": "node test/torture/run.mjs --controls",
56
+ "spikes": "for f in spikes/*.mjs; do echo \"== $f ==\"; node --expose-gc \"$f\" || exit 1; done",
57
+ "spike:emit": "node --expose-gc spikes/emit/probe.mjs",
58
+ "spike:ownership": "node --expose-gc spikes/ownership.mjs",
59
+ "spike:storage": "node --expose-gc spikes/storage-bench.mjs",
60
+ "spike:manual": "node --expose-gc spikes/manual-call.mjs",
61
+ "spike:poison": "node --expose-gc spikes/poison.mjs",
62
+ "spike:buildless": "node --expose-gc spikes/buildless.mjs",
63
+ "fixtures:regen": "node spikes/emit/regen.mjs"
64
+ },
65
+ "peerDependencies": {
66
+ "@zakkster/lite-signal": ">=1.5.0 <2.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "@babel/core": "^7.25.0",
70
+ "@babel/plugin-proposal-decorators": "^7.25.0",
71
+ "@babel/preset-typescript": "^7.29.7",
72
+ "@zakkster/lite-gc-profiler": "^1.16.0",
73
+ "@zakkster/lite-leak": "^1.10.0",
74
+ "@zakkster/lite-signal": "1.5.0",
75
+ "typescript": "^5.6.0"
76
+ }
77
+ }