@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 +68 -0
- package/SignalDecorators.d.ts +136 -8
- package/SignalDecorators.js +711 -183
- package/llms.txt +98 -25
- package/package.json +1 -1
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
|
package/SignalDecorators.d.ts
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* `SignalDecorators.js`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
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
|
|
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
|
|
88
|
-
* (`@reactiveHost()`)
|
|
89
|
-
*
|
|
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(
|
|
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:
|
|
276
|
+
export const VERSION: "0.2.0-preview.1";
|