@zakkster/lite-signal 1.1.2 → 1.1.3

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/README.md CHANGED
@@ -90,6 +90,7 @@ No microtask between `B` and `I`. No promise, no `queueMicrotask`. Just call sta
90
90
  - **`dispose(api)`** — universal disposal for signals, computeds, and effect handles. Cross-registry calls are silent no-ops.
91
91
  - **`batch(fn)`** — defer effect flush until the outermost batch closes.
92
92
  - **`untrack(fn)`** — read without subscribing.
93
+ - **`isTracking()`** — `true` iff a read right now would subscribe (for lazy-allocation wrappers).
93
94
  - **`onCleanup(fn)`** — register teardown for the current computation. Works in effects *and* computeds.
94
95
  - **`createRegistry(config)`** — isolated pool for tests, plugins, sandboxing.
95
96
  - **`stats()`** — pool occupancy snapshot. Used by the demo and easy to wire into perf overlays.
@@ -273,6 +274,28 @@ const value = untrack(() => s()); // read without subscribing
273
274
 
274
275
  Useful inside computeds/effects when you need a current value but don't want it as a dependency.
275
276
 
277
+ ### isTracking
278
+
279
+ ```ts
280
+ function makeLazyField(initial) {
281
+ let s = null, value = initial;
282
+ return {
283
+ get() {
284
+ if (isTracking()) {
285
+ if (s === null) s = signal(value); // allocate only when subscribed
286
+ return s();
287
+ }
288
+ return value;
289
+ },
290
+ set(v) { value = v; if (s !== null) s.set(v); }
291
+ };
292
+ }
293
+ ```
294
+
295
+ Returns `true` iff a read right now would record a dependency on the current registry — an observer body is on the stack AND tracking is enabled. Mirrors the engine's own read-trap check (both flags), so it correctly returns `false` inside `untrack`, inside `subscribe` callbacks, inside `onCleanup` bodies, inside `watch` / `when` callbacks, and outside any observer.
296
+
297
+ For wrapper libraries (lite-store, lite-query, lite-form) gating lazy allocation on the read path. Per-registry — call `registry.isTracking()` if your signals live in a non-default registry.
298
+
276
299
  ### onCleanup
277
300
 
278
301
  ```ts
@@ -547,6 +570,7 @@ Three tiers, all reproducible.
547
570
  - **`07-dispose.test.mjs`** — unified `dispose(api)` across signals, computeds and effect handles, idempotency, cross-registry isolation (per-registry Symbol prevents pool corruption), foreign-value safety, top-level helper routing, 500-cycle balanced churn leaving pool and stats stable.
548
571
  - **`08-watch.test.mjs`** — Validates the user-land observer utilities (watch, when, whenAsync). Covers lifecycle teardown, old/new value tracking, and Promise-based asynchronous state resolution.
549
572
  - **`09-conformance.test.mjs`** — Industry-standard conformance tests. Validates the engine against extreme edge cases from the johnsoncodehk reactive test suite, ensuring strict zero-GC invariants, correct cleanup isolation, and re-entrant stability.
573
+ - **`10-is-tracking.test.mjs`** — The `isTracking()` observer-context predicate. 11 tests across 5 describe blocks: true inside effect/computed bodies; false inside `untrack`, `subscribe` callbacks, `onCleanup` bodies, and `watch` callbacks (the untracked-window cases that catch an observer-only misimplementation); false outside any observer including at the call site of an unobserved computed read; state-restoration after a thrown body; per-registry isolation; top-level binding.
550
574
 
551
575
  ```bash
552
576
  npm test
package/Signal.d.ts CHANGED
@@ -133,6 +133,10 @@ export interface Registry {
133
133
  dispose(api: Disposable): void;
134
134
  batch<T>(fn: () => T): T;
135
135
  untrack<T>(fn: () => T): T;
136
+ /** True iff a read RIGHT NOW would record a dependency on this registry.
137
+ * False inside `untrack`, `subscribe` callbacks, `onCleanup` bodies, and
138
+ * outside any observer. Use for lazy-allocation wrappers like lite-store. */
139
+ isTracking(): boolean;
136
140
  onCleanup(fn: () => void): void;
137
141
  stats(): RegistryStats;
138
142
  /** Reset everything: nodes, links, queues, global clock. Outstanding dispose
@@ -163,6 +167,8 @@ export function effect(fn: () => void, opts?: EffectOptions): Dispose;
163
167
  export function dispose(api: Disposable): void;
164
168
  export function batch<T>(fn: () => T): T;
165
169
  export function untrack<T>(fn: () => T): T;
170
+ /** Top-level binding of {@link Registry.isTracking} against the default registry. */
171
+ export function isTracking(): boolean;
166
172
  export function onCleanup(fn: () => void): void;
167
173
  export function stats(): RegistryStats;
168
174
  export declare function destroy(): void;
package/Signal.js CHANGED
@@ -993,6 +993,32 @@ export function createRegistry(config = {}) {
993
993
  * @param {() => T} fn
994
994
  * @returns {T}
995
995
  */
996
+ /**
997
+ * Returns true iff a read RIGHT NOW would record a dependency in the current
998
+ * call stack: an observer body is on the stack AND tracking is enabled.
999
+ *
1000
+ * Mirrors the engine's own read-trap predicate (both flags checked). Returns
1001
+ * false inside `untrack()`, inside `signal.subscribe`'s callback (which
1002
+ * inlines the same untracked-notify pattern), inside `runCleanup` bodies,
1003
+ * and outside any observer.
1004
+ *
1005
+ * Use case: wrapper libraries (lite-store, lite-query, lite-form) that
1006
+ * lazily allocate reactive primitives on property reads — gating on
1007
+ * isTracking() lets them skip allocation when reads happen outside a
1008
+ * reactive context, preserving the zero-GC contract.
1009
+ *
1010
+ * Cost: two closure-variable loads, one AND, one return. ~1–2 ns.
1011
+ *
1012
+ * Note: per-registry. Wrappers operating against a non-default registry
1013
+ * MUST call that registry's `isTracking()`, not the top-level one — each
1014
+ * registry has its own tracking state.
1015
+ *
1016
+ * @returns {boolean}
1017
+ */
1018
+ function isTracking() {
1019
+ return isTrackingDeps && currentObserver !== null;
1020
+ }
1021
+
996
1022
  function untrack(fn) {
997
1023
  const prev = isTrackingDeps;
998
1024
  isTrackingDeps = false;
@@ -1101,7 +1127,7 @@ export function createRegistry(config = {}) {
1101
1127
  flushErrorBuffer.length = 0; // release the backing array
1102
1128
  }
1103
1129
 
1104
- return {signal, computed, effect, dispose, batch, untrack, onCleanup, stats, destroy};
1130
+ return {signal, computed, effect, dispose, batch, untrack, onCleanup, stats, destroy, isTracking};
1105
1131
  }
1106
1132
 
1107
1133
  // ─────────────────────────────────────────────────────────────────
@@ -1150,6 +1176,15 @@ export function untrack(fn) {
1150
1176
  return defaultRegistry.untrack(fn);
1151
1177
  }
1152
1178
 
1179
+ /**
1180
+ * True iff a read RIGHT NOW would record a dependency on the default registry.
1181
+ * See {@link createRegistry} for the per-registry version. Wrappers operating
1182
+ * against a non-default registry MUST call THAT registry's `isTracking()`.
1183
+ */
1184
+ export function isTracking() {
1185
+ return defaultRegistry.isTracking();
1186
+ }
1187
+
1153
1188
  /** @type {Registry["onCleanup"]} */
1154
1189
  export function onCleanup(fn) {
1155
1190
  return defaultRegistry.onCleanup(fn);
package/llms.txt CHANGED
@@ -19,6 +19,7 @@ triggering write — no `queueMicrotask`, no promises, no scheduler ticks.
19
19
  - **dispose(api)**: universal disposal. Accepts a signal, computed, or effect dispose handle; idempotent; cross-registry calls are silent no-ops (per-registry `Symbol("node_ptr")` keys the node-identity slot on the returned API function, foreign signals fail the lookup and fall through). Passing an unrelated value is also safe; passing an arbitrary function invokes it (effect-handle contract).
20
20
  - **Batch**: `batch(fn)` defers effect flush until the outermost batch closes. Nestable.
21
21
  - **Untrack**: `untrack(fn)` reads without subscribing.
22
+ - **isTracking**: `isTracking()` returns true iff a read right now would record a dependency on this registry (for wrappers that lazily allocate signals).
22
23
  - **onCleanup**: registers teardown for the current computation; fires before each re-run and once on dispose. Works in effects and computeds.
23
24
  - **Registry**: `createRegistry({ maxNodes, maxLinks, onCapacityExceeded, maxFlushPasses })` creates an isolated reactive world with its own pools. Useful for tests, plugins, multi-tenant sandboxes. Top-level helpers use a default registry created at module load.
24
25
  - **CapacityError**: thrown when a fixed-size pool is exhausted under the `"throw"` policy, or when a `"grow"` policy hits the 16× starting-capacity ceiling on links.
@@ -48,6 +49,16 @@ triggering write — no `queueMicrotask`, no promises, no scheduler ticks.
48
49
 
49
50
  ## Version notes
50
51
 
52
+ - **1.1.3**: adds `isTracking()` (top-level + per-registry). Returns true iff a
53
+ read right now would record a dependency (`isTrackingDeps && currentObserver !== null`).
54
+ False inside `untrack`, `subscribe` callbacks, `onCleanup` bodies, `watch` /
55
+ `when` callbacks, and outside any observer. ~1–2 ns. For wrapper libraries
56
+ (lite-store, lite-query, lite-form) that allocate reactive primitives lazily
57
+ on property reads. Per-registry: wrappers operating against a non-default
58
+ registry must call THAT registry's `isTracking()`, not the top-level one.
59
+ No behavior or engine changes.
60
+
61
+
51
62
  - **1.1.2** (current): hot-path micro-optimizations, no behavior/API change.
52
63
  Inlined cursor fast-path in `signal`/`computed` reads (stable-order reads skip
53
64
  the `allocateLink` call); zero-allocation creation path (`opts` read defensively
@@ -79,6 +90,7 @@ function effect(fn: () => void, opts?: { scheduler?: (run: () => void) => void }
79
90
  function dispose(api: Signal<any> | Computed<any> | Dispose): void;
80
91
  function batch<T>(fn: () => T): T;
81
92
  function untrack<T>(fn: () => T): T;
93
+ function isTracking(): boolean;
82
94
  function onCleanup(fn: () => void): void;
83
95
  function stats(): RegistryStats;
84
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-signal",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Zero-GC reactive graph. Monomorphic object pool, versioned push-pull propagation, 32-bit modular versioning. Built for hot paths and long-running processes.",
5
5
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
6
6
  "license": "MIT",