@zakkster/lite-signal 1.2.0 → 1.2.2

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.
Files changed (6) hide show
  1. package/CHANGELOG.md +331 -68
  2. package/README.md +244 -155
  3. package/Signal.d.ts +74 -20
  4. package/Signal.js +191 -85
  5. package/llms.txt +189 -66
  6. package/package.json +7 -3
package/Signal.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * @zakkster/lite-signal zero-GC reactive graph.
2
+ * @zakkster/lite-signal -- zero-GC reactive graph.
3
3
  *
4
4
  * Public type surface for the JavaScript implementation in `Signal.js`.
5
5
  */
6
6
 
7
- // ─── Options ──────────────────────────────────────────────────────────────────
7
+ // --- Options ------------------------------------------------------------------
8
8
 
9
9
  /** Equality predicate. Returning `true` halts propagation. */
10
10
  export type EqualsFn<T> = (a: T, b: T) => boolean;
@@ -45,7 +45,7 @@ export type Dispose = () => void;
45
45
  */
46
46
  export type Disposable<T = unknown> = Signal<T> | Computed<T> | Dispose;
47
47
 
48
- // ─── Reactive primitive shapes ────────────────────────────────────────────────
48
+ // --- Reactive primitive shapes ------------------------------------------------
49
49
 
50
50
  /** Reactive source of truth. */
51
51
  export interface Signal<T> {
@@ -71,7 +71,7 @@ export interface Computed<T> {
71
71
  subscribe(fn: (value: T) => void): Dispose;
72
72
  }
73
73
 
74
- // ─── Diagnostics ──────────────────────────────────────────────────────────────
74
+ // --- Diagnostics --------------------------------------------------------------
75
75
 
76
76
  export interface RegistryStats {
77
77
  /** Number of signals created in this registry's lifetime. */
@@ -92,7 +92,7 @@ export interface RegistryStats {
92
92
  activeNodes: number;
93
93
  }
94
94
 
95
- // ─── Observer-lifecycle introspection (1.1.4) ─────────────────────────────────
95
+ // --- Observer-lifecycle introspection (1.1.4) ---------------------------------
96
96
 
97
97
  /** Whether a described node is a signal, a computed, or an effect. */
98
98
  export type NodeKind = "signal" | "computed" | "effect";
@@ -109,9 +109,9 @@ export interface NodeDescriptor {
109
109
 
110
110
  /** Transition callbacks for {@link Registry.observeObservers}. */
111
111
  export interface ObserveObserversHooks {
112
- /** Fired on the 01 observer transition (after registration). */
112
+ /** Fired on the 0->1 observer transition (after registration). */
113
113
  onConnect?: () => void;
114
- /** Fired on the 10 observer transition. */
114
+ /** Fired on the 1->0 observer transition. */
115
115
  onDisconnect?: () => void;
116
116
  }
117
117
 
@@ -121,7 +121,36 @@ export type Unobserve = () => void;
121
121
  /** Anything carrying a node identity that the introspection surface can read. */
122
122
  export type ReactiveHandle = Signal<any> | Computed<any>;
123
123
 
124
- // ─── Errors ───────────────────────────────────────────────────────────────────
124
+ // --- Graph-mutation hook (1.2.1) ----------------------------------------------
125
+
126
+ /**
127
+ * Opcode passed as the first argument to a {@link GraphMutationListener}.
128
+ *
129
+ * - `1` node create. `(intA, intB) = (node.id, node.flags)`.
130
+ * - `2` node dispose. `(intA, intB) = (node.id, node.flags)` -- fires for every node
131
+ * disposed, including cascaded owner-tree children.
132
+ * - `3` link add. `(intA, intB) = (source.id, target.id)`.
133
+ * - `4` link remove. `(intA, intB) = (source.id, target.id)` -- `-1` if the link
134
+ * was already nulled (defensive, rare).
135
+ * - `5` recompute. `(intA, intB) = (node.id, 0)` -- fires just before an effect
136
+ * re-run or a computed re-eval.
137
+ */
138
+ export type GraphMutationOpcode = 1 | 2 | 3 | 4 | 5;
139
+
140
+ /**
141
+ * Listener registered with {@link Registry.onGraphMutation}. Called synchronously
142
+ * inside each mutation point with three integers -- no objects allocated.
143
+ *
144
+ * **Contract: observe only.** Listeners MUST NOT throw and MUST NOT mutate the
145
+ * graph from inside the callback. Both will corrupt the engine's state. Wrap any
146
+ * downstream work in a microtask if it could touch the registry.
147
+ */
148
+ export type GraphMutationListener = (opcode: GraphMutationOpcode, intA: number, intB: number) => void;
149
+
150
+ /** Idempotent unsubscriber returned by {@link Registry.onGraphMutation}. */
151
+ export type GraphMutationUnsubscribe = () => void;
152
+
153
+ // --- Errors -------------------------------------------------------------------
125
154
 
126
155
  /** Thrown when a pool ceiling is hit. */
127
156
  export class CapacityError extends Error {
@@ -131,7 +160,7 @@ export class CapacityError extends Error {
131
160
  constructor(kind: "nodes" | "links", capacity: number);
132
161
  }
133
162
 
134
- // ─── Registry ─────────────────────────────────────────────────────────────────
163
+ // --- Registry -----------------------------------------------------------------
135
164
 
136
165
  export interface RegistryConfig {
137
166
  /** Initial node-pool capacity. Default: 1024. */
@@ -168,8 +197,8 @@ export interface Registry {
168
197
  isTracking(): boolean;
169
198
  /** O(1): does this source have at least one live observer right now? A `peek` does not count. */
170
199
  hasObservers(handle: ReactiveHandle): boolean;
171
- /** Auto-pause hook: fires `onConnect` on the 01 observer transition and `onDisconnect`
172
- * on 10, after registration (transition-only no immediate fire if already observed).
200
+ /** Auto-pause hook: fires `onConnect` on the 0->1 observer transition and `onDisconnect`
201
+ * on 1->0, after registration (transition-only -- no immediate fire if already observed).
173
202
  * Re-tracking a persistently-read source does not churn. Returns an idempotent unobserve.
174
203
  * @throws TypeError if `handle` is not a reactive handle. */
175
204
  observeObservers(handle: ReactiveHandle, hooks?: ObserveObserversHooks): Unobserve;
@@ -177,16 +206,35 @@ export interface Registry {
177
206
  forEachObserver(handle: ReactiveHandle, fn: (descriptor: NodeDescriptor) => void): void;
178
207
  /** Walk the sources (dependencies) of `handle`. No-op on a non-handle. */
179
208
  forEachSource(handle: ReactiveHandle, fn: (descriptor: NodeDescriptor) => void): void;
180
- /** Stable node id of `handle` (1.1.5+), or undefined for a non-handle. */
209
+ /** Walk the owned children of `handle` -- nodes whose lifetime is bound to this
210
+ * one by the 1.2 owner tree (1.2.1+). Top-level handles and signals have no
211
+ * owned children; effects/computeds may own nested observers created inside
212
+ * their bodies. No-op on a non-handle or stale handle. */
213
+ forEachOwned(handle: ReactiveHandle, fn: (descriptor: NodeDescriptor) => void): void;
214
+ /** Descriptor of `handle`'s owner, or `undefined` for top-level handles, stale
215
+ * handles, or non-handles (1.2.1+). The owner is the effect or computed inside
216
+ * whose body `handle` was created -- the node that will cascade-dispose it
217
+ * on re-run or explicit dispose. */
218
+ ownerOf(handle: ReactiveHandle): NodeDescriptor | undefined;
219
+ /** Stable node id of `handle` (1.1.5+), or undefined for a non-handle or stale handle. */
181
220
  nodeId(handle: ReactiveHandle): number | undefined;
182
- /** The own descriptor of `handle` (1.1.5+), or undefined for a non-handle. Re-walkable:
183
- * the returned descriptor may be passed back into forEachObserver/forEachSource. */
221
+ /** The own descriptor of `handle` (1.1.5+), or undefined for a non-handle or stale handle.
222
+ * Re-walkable: the returned descriptor may be passed back into forEachObserver/
223
+ * forEachSource/forEachOwned/ownerOf. Descriptors are gen-stamped (1.2.1+): a
224
+ * descriptor obtained pre-recycle goes stale and walks as undefined post-recycle. */
184
225
  describe(handle: ReactiveHandle): NodeDescriptor | undefined;
226
+ /** Register a single graph-mutation listener (1.2.1+). Replaces any existing
227
+ * listener and returns an unsubscribe that restores the previous one on call.
228
+ * Listener is invoked synchronously at each mutation point with three integers:
229
+ * `(opcode, intA, intB)` -- see {@link GraphMutationOpcode}. Cost when no
230
+ * listener is registered: one branch-predicted `null` check per mutation point.
231
+ * @throws TypeError if `fn` is not a function or null. */
232
+ onGraphMutation(fn: GraphMutationListener | null): GraphMutationUnsubscribe;
185
233
  onCleanup(fn: () => void): void;
186
234
  stats(): RegistryStats;
187
235
  /** Reset everything: nodes, links, queues, global clock. Outstanding dispose
188
236
  * closures become safe no-ops. Outstanding read/set closures still reference
189
- * pool slots they will silently misbehave; use a fresh registry afterwards. */
237
+ * pool slots -- they will silently misbehave; use a fresh registry afterwards. */
190
238
  destroy(): void;
191
239
  }
192
240
 
@@ -203,12 +251,12 @@ export function createRegistry(config?: RegistryConfig): Registry;
203
251
  /** Replace the default registry backing the top-level helpers. */
204
252
  export function setDefaultRegistry(registry: Registry): void;
205
253
 
206
- // ─── Top-level helpers (delegate to default registry) ────────────────────────
254
+ // --- Top-level helpers (delegate to default registry) ------------------------
207
255
 
208
256
  export function signal<T>(initial: T, opts?: SignalOptions<T>): Signal<T>;
209
257
  export function computed<T>(fn: () => T, opts?: ComputedOptions<T>): Computed<T>;
210
258
  export function effect(fn: () => void, opts?: EffectOptions): Dispose;
211
- /** Universal disposal see {@link Registry.dispose}. */
259
+ /** Universal disposal -- see {@link Registry.dispose}. */
212
260
  export function dispose(api: Disposable): void;
213
261
  export function batch<T>(fn: () => T): T;
214
262
  export function untrack<T>(fn: () => T): T;
@@ -222,10 +270,16 @@ export function observeObservers(handle: ReactiveHandle, hooks?: ObserveObserver
222
270
  export function forEachObserver(handle: ReactiveHandle, fn: (descriptor: NodeDescriptor) => void): void;
223
271
  /** Top-level binding of {@link Registry.forEachSource}. */
224
272
  export function forEachSource(handle: ReactiveHandle, fn: (descriptor: NodeDescriptor) => void): void;
273
+ /** Top-level binding of {@link Registry.forEachOwned} (1.2.1+). */
274
+ export function forEachOwned(handle: ReactiveHandle, fn: (descriptor: NodeDescriptor) => void): void;
275
+ /** Top-level binding of {@link Registry.ownerOf} (1.2.1+). */
276
+ export function ownerOf(handle: ReactiveHandle): NodeDescriptor | undefined;
225
277
  /** Top-level binding of {@link Registry.nodeId}. */
226
278
  export function nodeId(handle: ReactiveHandle): number | undefined;
227
279
  /** Top-level binding of {@link Registry.describe}. */
228
280
  export function describe(handle: ReactiveHandle): NodeDescriptor | undefined;
281
+ /** Top-level binding of {@link Registry.onGraphMutation} (1.2.1+). */
282
+ export function onGraphMutation(fn: GraphMutationListener | null): GraphMutationUnsubscribe;
229
283
  export function onCleanup(fn: () => void): void;
230
284
  export function stats(): RegistryStats;
231
285
  export declare function destroy(): void;
@@ -242,7 +296,7 @@ export interface WatchOptions {
242
296
 
243
297
  /**
244
298
  * Track a reactive source and run a callback whenever its projected value
245
- * changes. The callback receives `(newValue, oldValue, stop)` the third
299
+ * changes. The callback receives `(newValue, oldValue, stop)` -- the third
246
300
  * argument is a dispose function that can be called from inside the callback
247
301
  * to terminate the watcher.
248
302
  *
@@ -282,10 +336,10 @@ export function when(
282
336
  * Promise-returning variant of {@link when}. The returned promise resolves
283
337
  * when `predicate` first returns a truthy value.
284
338
  *
285
- * ⚠️ **HOT-PATH WARNING DO NOT USE PER FRAME.** This function calls
339
+ * ! **HOT-PATH WARNING -- DO NOT USE PER FRAME.** This function calls
286
340
  * `new Promise(...)`, which is a heap allocation (one Promise object plus
287
341
  * executor closure plus internal infrastructure per call). Promises require
288
- * heap allocation by the language spec this cost is unavoidable.
342
+ * heap allocation by the language spec -- this cost is unavoidable.
289
343
  *
290
344
  * **Use for:** high-level scene/UI orchestration, boot sequences, awaiting
291
345
  * user input or network state, level transitions. Anything that runs once