@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 +24 -0
- package/Signal.d.ts +6 -0
- package/Signal.js +36 -1
- package/llms.txt +12 -0
- package/package.json +1 -1
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.
|
|
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",
|