@zakkster/lite-signal-decorators 0.1.0 → 0.2.0-preview.1

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 CHANGED
@@ -4,6 +4,73 @@ 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
+ ## [0.2.0-preview.1] - 2026-08-26
8
+
9
+ The full runtime surface. Exports grow from 8 to 11. Published on the `preview`
10
+ tag; a stable 0.2.0 remains user-gated.
11
+
12
+ ### Added
13
+
14
+ - `reactiveEffect` -- `@reactiveEffect m()` (bare) / `@reactiveEffect({ scheduler })`
15
+ (factory): a method that auto-runs as an effect once the instance is wired
16
+ (after every field and every derived). The auto-effect tracks its reads; the
17
+ public method is a leak-guarded manual entry -- a call inside a foreign tracking
18
+ scope is untracked, so it records zero stray dependencies (0004 D-4b). Self-
19
+ dispose from inside an owned effect is allowed with pinned semantics (0004
20
+ D-4d): the current run completes, later decorated-member touches throw
21
+ `ReactiveDisposedError`, no re-runs follow, conservation is exact.
22
+ - `batched` -- `@batched m()` (bare) / `@batched()` (factory): runs the method
23
+ body inside one engine batch, coalescing its writes into a single flush.
24
+ Action-grade (one call per user intent), not a per-frame path: it allocates a
25
+ thunk + rest-array per call by design and is excluded from the zero-GC hot-path
26
+ gates.
27
+ - `defineReactive(Class, spec)` -- the buildless twin of the decorators (0005).
28
+ `spec` is `{ signals, deriveds, effects, host }`; it installs the members on
29
+ `Class.prototype` and wraps the class through the SAME wiring core the
30
+ decorators use (shared by function identity, not merely behaviorally alike).
31
+ Consumers without a Stage-3 transpiler get the complete feature set.
32
+ - `@reactiveHost({ registry })` -- an optional `registry` from lite-signal
33
+ `createRegistry()` isolates the whole host chain. Every engine call routes
34
+ through the bound registry, closing the cross-registry dispose trap (the
35
+ default `dispose` is a silent no-op across registries). One registry per chain:
36
+ a heterogeneous chain or an invalid (duck-check-failing) registry is a named
37
+ throw.
38
+
39
+ ### Changed
40
+
41
+ - `@reactiveHost` unknown options now report the standard unknown-option
42
+ did-you-mean over `["registry"]` (the 0.1.0 "takes no options" message is
43
+ retired) -- `registry` is now a valid option.
44
+ - The duplicate-key error wording now names both causes: subclass redeclaration
45
+ OR two stacked package decorators on one member.
46
+ - Internal error-string refactor (housekeeping): the package prefix is hoisted to
47
+ one `ERR` const and cold message building uses template literals; prewired
48
+ handles build their message once and share it. Message text is byte-identical
49
+ to 0.1.0 except the two changes above. Hot accessor bodies are untouched.
50
+ - `VERSION` is `"0.2.0-preview.1"`.
51
+
52
+ ### Fixed
53
+
54
+ - Manual-call identity guard (D-4e): calling a `@reactiveEffect` or `@batched`
55
+ method on a foreign, primitive, null, or cross-class receiver (e.g. via
56
+ `Class.prototype.m.call(...)`) now throws a named error instead of running the
57
+ body against the wrong instance. Uses byKey identity, so a subclass instance
58
+ calling a base-declared method still passes. Cold manual-call path only.
59
+ - Frozen-instance dispose refusal (D-2g): `disposeReactive` (and `using`) now
60
+ refuses a frozen instance up front with a named error, atomically -- previously
61
+ it half-disposed (tore down boxes) then threw a raw `TypeError`, leaving
62
+ disposed boxes behind live-looking slots. `Object.seal`/`preventExtensions`
63
+ remain fine.
64
+
65
+ ### Notes
66
+
67
+ - `@batched` per-call cost, measured by the committed cold-process probe
68
+ (`spikes/batched-cost.mjs`, K=10 cold processes, node 26.3.1, Apple M4 Pro,
69
+ lite-signal 1.5.0): the decorated method 22.12 ns/op vs raw `batch(fn)`
70
+ 15.25 vs the plain unbatched method 11.67 (median-of-medians; 0 major GC in
71
+ every lane). The ~7 ns over raw is the guarded rest-array + thunk (risk R8).
72
+ Action-grade, not a per-frame path; never in the zero-GC torture gates.
73
+
7
74
  ## [0.1.0] - 2026-08-26
8
75
 
9
76
  Initial release -- the decorator core.
@@ -36,4 +103,5 @@ Initial release -- the decorator core.
36
103
  - Torture skeleton (`@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`):
37
104
  retention, conservation, lifecycle, and zero-GC lanes.
38
105
 
106
+ [0.2.0-preview.1]: https://github.com/zakkster/lite-signal-decorators/releases/tag/v0.2.0-preview.1
39
107
  [0.1.0]: https://github.com/zakkster/lite-signal-decorators/releases/tag/v0.1.0
@@ -6,7 +6,13 @@
6
6
  * `SignalDecorators.js`.
7
7
  */
8
8
 
9
- import type { SignalBox, ComputedBox, NodeDescriptor } from "@zakkster/lite-signal";
9
+ import type {
10
+ SignalBox,
11
+ ComputedBox,
12
+ NodeDescriptor,
13
+ Registry,
14
+ EffectScheduler,
15
+ } from "@zakkster/lite-signal";
10
16
 
11
17
  // --- Options ------------------------------------------------------------------
12
18
 
@@ -22,6 +28,22 @@ export interface DerivedOptions<V> {
22
28
  equals?: (a: V, b: V) => boolean;
23
29
  }
24
30
 
31
+ /** Options for a `@reactiveEffect` method. */
32
+ export interface ReactiveEffectOptions {
33
+ /** Optional scheduler forwarded straight to the underlying `effect(fn, { scheduler })`. */
34
+ scheduler?: EffectScheduler;
35
+ }
36
+
37
+ /**
38
+ * Options for `@reactiveHost` / `defineReactive` `host`. A `registry` isolates
39
+ * the entire host chain on a custom lite-signal registry (one registry per
40
+ * chain -- a mixed chain is a named throw).
41
+ */
42
+ export interface ReactiveHostOptions {
43
+ /** A `Registry` from lite-signal `createRegistry()`. Omit for the default registry. */
44
+ registry?: Registry;
45
+ }
46
+
25
47
  // --- reactive -----------------------------------------------------------------
26
48
 
27
49
  /**
@@ -81,22 +103,128 @@ export function derived<V>(
81
103
 
82
104
  /**
83
105
  * `@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.
106
+ * constructor builds the reactive graph (anchor + every derived + every effect)
107
+ * exactly once, after all field initializers have run.
86
108
  *
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.
109
+ * Bare application decorates the class; the factory form
110
+ * (`@reactiveHost({ registry })`) validates its options eagerly and returns the
111
+ * same class decorator. A `registry` isolates the whole host chain.
90
112
  */
91
113
  export function reactiveHost<C extends abstract new (...args: any[]) => any>(
92
114
  target: C,
93
115
  ctx: ClassDecoratorContext<C>,
94
116
  ): C;
95
- export function reactiveHost(): <C extends abstract new (...args: any[]) => any>(
117
+ export function reactiveHost(
118
+ opts?: ReactiveHostOptions,
119
+ ): <C extends abstract new (...args: any[]) => any>(
96
120
  target: C,
97
121
  ctx: ClassDecoratorContext<C>,
98
122
  ) => C;
99
123
 
124
+ // --- reactiveEffect -----------------------------------------------------------
125
+
126
+ /**
127
+ * `@reactiveEffect m()` -- a method that auto-runs as an effect once the
128
+ * instance is wired (after every field and derived). The auto-effect tracks its
129
+ * reads; the public method is a leak-guarded manual entry (a call inside a
130
+ * foreign tracking scope is untracked, so it records no stray dependencies).
131
+ *
132
+ * Bare application decorates a `method`; the factory form
133
+ * (`@reactiveEffect({ scheduler })`) validates its options eagerly and returns
134
+ * the same method decorator.
135
+ */
136
+ export function reactiveEffect<This, Args extends any[], R>(
137
+ value: (this: This, ...args: Args) => R,
138
+ ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
139
+ ): (this: This, ...args: Args) => R;
140
+ export function reactiveEffect(
141
+ opts?: ReactiveEffectOptions,
142
+ ): <This, Args extends any[], R>(
143
+ value: (this: This, ...args: Args) => R,
144
+ ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
145
+ ) => (this: This, ...args: Args) => R;
146
+
147
+ // --- batched ------------------------------------------------------------------
148
+
149
+ /**
150
+ * `@batched m()` -- a method whose body runs inside one engine batch, so its
151
+ * writes flush together. Action-grade (one call per user intent), not a
152
+ * per-frame path.
153
+ *
154
+ * Bare application decorates a `method`; the zero-key factory form
155
+ * (`@batched()`) returns the same method decorator. No options in 0.2.0.
156
+ */
157
+ export function batched<This, Args extends any[], R>(
158
+ value: (this: This, ...args: Args) => R,
159
+ ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
160
+ ): (this: This, ...args: Args) => R;
161
+ export function batched(): <This, Args extends any[], R>(
162
+ value: (this: This, ...args: Args) => R,
163
+ ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
164
+ ) => (this: This, ...args: Args) => R;
165
+
166
+ // --- defineReactive (buildless twin) ------------------------------------------
167
+
168
+ /** Per-instance signal descriptor for a `defineReactive` `signals` map entry. */
169
+ export interface SignalSpec<This = unknown, V = unknown> {
170
+ /** The initial value, verbatim (mutually exclusive with `init`). */
171
+ initial?: V;
172
+ /** A per-instance factory `(self) => value` (mutually exclusive with `initial`). */
173
+ init?: (this: This, self: This) => V;
174
+ /** Custom equality predicate. Default: `Object.is`. */
175
+ equals?: (a: V, b: V) => boolean;
176
+ }
177
+
178
+ /** Derived descriptor for a `defineReactive` `deriveds` map entry. */
179
+ export interface DerivedSpec<This = unknown, V = unknown> {
180
+ /** The compute body `(self) => value`. */
181
+ get: (this: This, self: This) => V;
182
+ /** Custom equality predicate. Default: `Object.is`. */
183
+ equals?: (a: V, b: V) => boolean;
184
+ }
185
+
186
+ /** Effect descriptor for a `defineReactive` `effects` map entry. */
187
+ export interface EffectSpec<This = unknown> {
188
+ /** The effect body `(self) => void`, run under the instance's anchor. */
189
+ run: (this: This, self: This) => void;
190
+ /** Optional scheduler forwarded to the underlying effect. */
191
+ scheduler?: EffectScheduler;
192
+ }
193
+
194
+ /**
195
+ * The `defineReactive(Class, spec)` specification. Every section is optional;
196
+ * unknown sections and descriptor keys are named throws.
197
+ */
198
+ export interface DefineReactiveSpec<This = any> {
199
+ /**
200
+ * Reactive signals: an array of keys (each initialized to `undefined`) or a
201
+ * map of `key -> plain value | SignalSpec`. A bare function value is a named
202
+ * throw (ambiguous -- use `{ initial: fn }` or `{ init: (self) => value }`).
203
+ */
204
+ signals?: PropertyKey[] | Record<PropertyKey, unknown | SignalSpec<This>>;
205
+ /** Lazy deriveds: a map of `key -> (self) => value | DerivedSpec`. */
206
+ deriveds?: Record<PropertyKey, ((this: This, self: This) => unknown) | DerivedSpec<This>>;
207
+ /** Auto-effects: a map of `key -> (self) => void | EffectSpec`. */
208
+ effects?: Record<PropertyKey, ((this: This, self: This) => void) | EffectSpec<This>>;
209
+ /** Host options (registry isolation), shared with `@reactiveHost`. */
210
+ host?: ReactiveHostOptions;
211
+ }
212
+
213
+ /**
214
+ * `defineReactive(Class, spec)` -- the buildless twin of the decorators. Installs
215
+ * the accessors, deriveds, and effect methods declared in `spec` on
216
+ * `Class.prototype`, then wraps `Class` through the same host step the
217
+ * decorators use. Returns the wrapper class. Zero decorator syntax required.
218
+ *
219
+ * @throws for an unknown section/descriptor key, an ambiguous signal entry, an
220
+ * `initial`+`init` conflict, a spec key that collides with a hand-written
221
+ * member, or an invalid host registry.
222
+ */
223
+ export function defineReactive<C extends new (...args: any[]) => any>(
224
+ Class: C,
225
+ spec: DefineReactiveSpec<InstanceType<C>>,
226
+ ): C;
227
+
100
228
  // --- Lifecycle + lookups ------------------------------------------------------
101
229
 
102
230
  /**
@@ -145,4 +273,4 @@ export class ReactiveDisposedError extends Error {
145
273
  // --- Version ------------------------------------------------------------------
146
274
 
147
275
  /** Package version. Kept in lockstep with package.json and llms.txt. */
148
- export const VERSION: string;
276
+ export const VERSION: "0.2.0-preview.1";