@zakkster/lite-signal 1.0.0
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/LICENSE.txt +21 -0
- package/README.md +585 -0
- package/Signal.d.ts +167 -0
- package/Signal.js +1007 -0
- package/llms.txt +192 -0
- package/package.json +58 -0
package/Signal.js
ADDED
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-signal
|
|
3
|
+
* --------------------
|
|
4
|
+
* Zero-GC reactive graph.
|
|
5
|
+
*
|
|
6
|
+
* Architecture: monomorphic object pool + versioned push-pull propagation
|
|
7
|
+
* + SMI modular arithmetic for 32-bit version-wrap safety.
|
|
8
|
+
*
|
|
9
|
+
* Performance characteristics:
|
|
10
|
+
* - Object pool: nodes and links are allocated from preallocated arrays. Steady-state
|
|
11
|
+
* operations (signal.set / computed.peek / effect re-run) perform zero allocations
|
|
12
|
+
* after warmup.
|
|
13
|
+
* - Stable read order: re-tracking dependencies in the same order yields O(1) link reuse
|
|
14
|
+
* via the `activeObserverCurrentDep` cursor.
|
|
15
|
+
* - Chaotic / randomized read order degrades to O(N) per dep (linear search of headDep
|
|
16
|
+
* list) — see {@link allocateLink}.
|
|
17
|
+
* - Computed resolution is recursive on the JS call stack. Maximum chain depth is bound
|
|
18
|
+
* by the engine stack limit (~10,000 frames).
|
|
19
|
+
* - 32-bit modular arithmetic for versioning: the engine is immune to integer-overflow
|
|
20
|
+
* crashes regardless of uptime.
|
|
21
|
+
*
|
|
22
|
+
* Public surface: {@link signal}, {@link computed}, {@link effect}, {@link batch},
|
|
23
|
+
* {@link untrack}, {@link onCleanup}, {@link stats}, {@link createRegistry},
|
|
24
|
+
* {@link setDefaultRegistry}, {@link CapacityError}.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ─── Node flag bits ────────────────────────────────────────────────────────────
|
|
28
|
+
const FLAG_COMPUTED = 1 << 0;
|
|
29
|
+
const FLAG_EFFECT = 1 << 1;
|
|
30
|
+
const FLAG_QUEUED = 1 << 2;
|
|
31
|
+
const FLAG_COMPUTING = 1 << 3;
|
|
32
|
+
const FLAG_HAS_ERROR = 1 << 4;
|
|
33
|
+
const FLAG_SIGNAL = 1 << 5; // Identifies signals for universal disposal
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Internal: a reactive node (signal, computed, or effect).
|
|
37
|
+
* Lives in a preallocated pool; never released to GC during normal use.
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
class ReactiveNode {
|
|
41
|
+
constructor() {
|
|
42
|
+
/** Bitmask: FLAG_COMPUTED | FLAG_EFFECT | FLAG_QUEUED | FLAG_COMPUTING | FLAG_HAS_ERROR */
|
|
43
|
+
this.flags = 0;
|
|
44
|
+
/** Current value (signal, computed) or error (when FLAG_HAS_ERROR is set). */
|
|
45
|
+
this.value = undefined;
|
|
46
|
+
/** Compute body (computed, effect). */
|
|
47
|
+
this.computeFn = undefined;
|
|
48
|
+
/** Single fn OR array of fns; cleared after invocation. */
|
|
49
|
+
this.cleanupFn = undefined;
|
|
50
|
+
/** Custom equality predicate. Defaults to Object.is. */
|
|
51
|
+
this.equals = undefined;
|
|
52
|
+
/** Optional effect scheduler. */
|
|
53
|
+
this.scheduler = undefined;
|
|
54
|
+
|
|
55
|
+
/** Bumped on every change that mutates value. 32-bit modular. */
|
|
56
|
+
this.version = 0;
|
|
57
|
+
/** Last globalVersion at which this node was re-evaluated. */
|
|
58
|
+
this.evalVersion = 0;
|
|
59
|
+
/** Last globalVersion at which this node was marked dirty (de-duplicates traversal). */
|
|
60
|
+
this.markEpoch = 0;
|
|
61
|
+
/** Recycle generation: bumped on dispose, used to invalidate stale scheduler closures. */
|
|
62
|
+
this.gen = 0;
|
|
63
|
+
|
|
64
|
+
// Doubly-linked dependency list (this node depends on these sources).
|
|
65
|
+
this.headDep = null;
|
|
66
|
+
this.tailDep = null;
|
|
67
|
+
/** Cursor pointing into headDep during re-tracking. */
|
|
68
|
+
this.currentDep = null;
|
|
69
|
+
// Doubly-linked subscriber list (these targets depend on this node).
|
|
70
|
+
this.headSub = null;
|
|
71
|
+
|
|
72
|
+
// Pool free-list pointer.
|
|
73
|
+
this.nextFree = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Internal: a directed edge between a source node and a target node.
|
|
79
|
+
* Pool-allocated, never GC'd.
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
class ReactiveLink {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.source = null;
|
|
85
|
+
this.target = null;
|
|
86
|
+
|
|
87
|
+
this.prevDep = null;
|
|
88
|
+
this.nextDep = null;
|
|
89
|
+
this.prevSub = null;
|
|
90
|
+
this.nextSub = null;
|
|
91
|
+
|
|
92
|
+
this.nextFree = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Thrown when the registry would need to grow beyond its hard ceiling
|
|
98
|
+
* (or {@link RegistryConfig.onCapacityExceeded} is `"throw"` and the pool is full).
|
|
99
|
+
*/
|
|
100
|
+
export class CapacityError extends Error {
|
|
101
|
+
/**
|
|
102
|
+
* @param {"nodes"|"links"} kind Which pool was exhausted.
|
|
103
|
+
* @param {number} capacity Capacity at the time of the error.
|
|
104
|
+
*/
|
|
105
|
+
constructor(kind, capacity) {
|
|
106
|
+
super(`CapacityError: ${kind} capacity (${capacity}) exceeded.`);
|
|
107
|
+
this.name = "CapacityError";
|
|
108
|
+
/** @type {"nodes"|"links"} */
|
|
109
|
+
this.kind = kind;
|
|
110
|
+
/** @type {number} */
|
|
111
|
+
this.capacity = capacity;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create an isolated reactive registry.
|
|
117
|
+
*
|
|
118
|
+
* Use this when you need multiple independent reactive graphs (e.g. one per
|
|
119
|
+
* Twitch Extension viewer, one per worker, one per test).
|
|
120
|
+
*
|
|
121
|
+
* @param {object} [config]
|
|
122
|
+
* @param {number} [config.maxNodes=1024] Initial node-pool capacity.
|
|
123
|
+
* @param {number} [config.maxLinks=maxNodes*4] Initial link-pool capacity.
|
|
124
|
+
* @param {"throw"|"grow"} [config.onCapacityExceeded="throw"]
|
|
125
|
+
* `"throw"` fails fast when pools are full.
|
|
126
|
+
* `"grow"` doubles the pool (bounded by `maxLinks * 16` for links).
|
|
127
|
+
* @param {number} [config.maxFlushPasses=100] Cycle-protection: max effect-queue
|
|
128
|
+
* drain passes before throwing CycleError.
|
|
129
|
+
* @returns {Registry}
|
|
130
|
+
*/
|
|
131
|
+
export function createRegistry(config = {}) {
|
|
132
|
+
// Per-registry symbols. NODE_PTR carries a direct pool-slot reference;
|
|
133
|
+
// NODE_GEN stamps the slot's generation at the moment of API creation
|
|
134
|
+
// so dispose() can detect stale handles after the slot has been recycled.
|
|
135
|
+
const NODE_PTR = Symbol("node_ptr");
|
|
136
|
+
const NODE_GEN = Symbol("node_gen");
|
|
137
|
+
|
|
138
|
+
let currentNodesCapacity = config.maxNodes || 1024;
|
|
139
|
+
let currentLinkCapacity = config.maxLinks || currentNodesCapacity * 4;
|
|
140
|
+
const policy = config.onCapacityExceeded || "throw";
|
|
141
|
+
const maxFlushPasses = config.maxFlushPasses || 100;
|
|
142
|
+
const maxLinkLimit = currentLinkCapacity * 16;
|
|
143
|
+
|
|
144
|
+
// --- ZERO-GC OBJECT POOLS ---
|
|
145
|
+
const nodePool = [];
|
|
146
|
+
for (let i = 0; i < currentNodesCapacity; i++) nodePool[i] = new ReactiveNode();
|
|
147
|
+
let freeNodeHead = nodePool[0];
|
|
148
|
+
for (let i = 0; i < currentNodesCapacity - 1; i++) nodePool[i].nextFree = nodePool[i + 1];
|
|
149
|
+
|
|
150
|
+
const linkPool = [];
|
|
151
|
+
for (let i = 0; i < currentLinkCapacity; i++) linkPool[i] = new ReactiveLink();
|
|
152
|
+
let freeLinkHead = linkPool[0];
|
|
153
|
+
for (let i = 0; i < currentLinkCapacity - 1; i++) linkPool[i].nextFree = linkPool[i + 1];
|
|
154
|
+
|
|
155
|
+
let activeNodes = 0 | 0;
|
|
156
|
+
let activeLinks = 0 | 0;
|
|
157
|
+
let statSignals = 0 | 0;
|
|
158
|
+
let statComputeds = 0 | 0;
|
|
159
|
+
let statEffects = 0 | 0;
|
|
160
|
+
|
|
161
|
+
// --- QUEUES & STACKS (Monomorphic arrays) ---
|
|
162
|
+
const effectQueueA = [];
|
|
163
|
+
const effectQueueB = [];
|
|
164
|
+
const markStack = [];
|
|
165
|
+
for (let i = 0; i < currentNodesCapacity; i++) {
|
|
166
|
+
effectQueueA[i] = null;
|
|
167
|
+
effectQueueB[i] = null;
|
|
168
|
+
markStack[i] = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let activeQueue = effectQueueA;
|
|
172
|
+
let activeQueueLen = 0 | 0;
|
|
173
|
+
let isQueueA = true;
|
|
174
|
+
|
|
175
|
+
// --- GLOBAL STATE ---
|
|
176
|
+
let globalVersion = 1 | 0; // Forced 32-bit SMI
|
|
177
|
+
let currentObserver = null;
|
|
178
|
+
let activeObserverCurrentDep = null;
|
|
179
|
+
let batchDepth = 0 | 0;
|
|
180
|
+
let isTrackingDeps = false;
|
|
181
|
+
let isFlushing = false;
|
|
182
|
+
|
|
183
|
+
// --- ALLOCATORS ---
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Establish (or reuse) a dependency link from `source` → `target`.
|
|
187
|
+
*
|
|
188
|
+
* Fast path: cursor match (re-tracking same dep at same position) — O(1), no allocation.
|
|
189
|
+
* Slow path: linear search of target.headDep for an existing link — O(N) in N deps.
|
|
190
|
+
* Cold path: pool exhausted → grow or throw per policy.
|
|
191
|
+
*
|
|
192
|
+
* @private
|
|
193
|
+
*/
|
|
194
|
+
function allocateLink(source, target) {
|
|
195
|
+
let expected = activeObserverCurrentDep;
|
|
196
|
+
if (expected !== null && expected.source === source) {
|
|
197
|
+
activeObserverCurrentDep = expected.nextDep;
|
|
198
|
+
return expected;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let existing = target.headDep;
|
|
202
|
+
let found = null;
|
|
203
|
+
while (existing !== null) {
|
|
204
|
+
if (existing.source === source) {
|
|
205
|
+
found = existing;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
existing = existing.nextDep;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let link;
|
|
212
|
+
if (found !== null) {
|
|
213
|
+
link = found;
|
|
214
|
+
let p = link.prevDep;
|
|
215
|
+
let n = link.nextDep;
|
|
216
|
+
if (p !== null) p.nextDep = n; else target.headDep = n;
|
|
217
|
+
if (n !== null) n.prevDep = p; else target.tailDep = p;
|
|
218
|
+
} else {
|
|
219
|
+
if (freeLinkHead === null) {
|
|
220
|
+
if (policy === "throw") throw new CapacityError("links", currentLinkCapacity);
|
|
221
|
+
const newCap = currentLinkCapacity * 2;
|
|
222
|
+
if (newCap > maxLinkLimit) throw new CapacityError("links", maxLinkLimit);
|
|
223
|
+
|
|
224
|
+
const newLinks = new Array(newCap - currentLinkCapacity);
|
|
225
|
+
for (let i = 0; i < newLinks.length; i++) newLinks[i] = new ReactiveLink();
|
|
226
|
+
for (let i = 0; i < newLinks.length - 1; i++) newLinks[i].nextFree = newLinks[i + 1];
|
|
227
|
+
|
|
228
|
+
const startIdx = linkPool.length;
|
|
229
|
+
linkPool.length = newCap;
|
|
230
|
+
|
|
231
|
+
for (let i = 0; i < newLinks.length; i++) {
|
|
232
|
+
linkPool[startIdx + i] = newLinks[i];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
freeLinkHead = newLinks[0];
|
|
236
|
+
currentLinkCapacity = newCap;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
link = freeLinkHead;
|
|
240
|
+
freeLinkHead = link.nextFree;
|
|
241
|
+
link.nextFree = null;
|
|
242
|
+
activeLinks = (activeLinks + 1) | 0;
|
|
243
|
+
|
|
244
|
+
link.source = source;
|
|
245
|
+
link.target = target;
|
|
246
|
+
|
|
247
|
+
link.prevSub = null;
|
|
248
|
+
link.nextSub = source.headSub;
|
|
249
|
+
if (source.headSub !== null) source.headSub.prevSub = link;
|
|
250
|
+
source.headSub = link;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
link.nextDep = expected;
|
|
254
|
+
if (expected !== null) {
|
|
255
|
+
let p = expected.prevDep;
|
|
256
|
+
link.prevDep = p;
|
|
257
|
+
expected.prevDep = link;
|
|
258
|
+
if (p !== null) p.nextDep = link; else target.headDep = link;
|
|
259
|
+
} else {
|
|
260
|
+
let tail = target.tailDep;
|
|
261
|
+
link.prevDep = tail;
|
|
262
|
+
if (tail !== null) tail.nextDep = link; else target.headDep = link;
|
|
263
|
+
target.tailDep = link;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return link;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Return a link to the free pool and unlink it from the source's sub list. @private */
|
|
270
|
+
function freeLink(link, target, source) {
|
|
271
|
+
const pSub = link.prevSub;
|
|
272
|
+
const nSub = link.nextSub;
|
|
273
|
+
if (pSub !== null) pSub.nextSub = nSub; else source.headSub = nSub;
|
|
274
|
+
if (nSub !== null) nSub.prevSub = pSub;
|
|
275
|
+
|
|
276
|
+
link.source = null;
|
|
277
|
+
link.target = null;
|
|
278
|
+
link.prevDep = null;
|
|
279
|
+
link.nextDep = null;
|
|
280
|
+
link.prevSub = null;
|
|
281
|
+
link.nextSub = null;
|
|
282
|
+
|
|
283
|
+
link.nextFree = freeLinkHead;
|
|
284
|
+
freeLinkHead = link;
|
|
285
|
+
activeLinks = (activeLinks - 1) | 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- MANUAL MEMORY MANAGEMENT ---
|
|
289
|
+
|
|
290
|
+
function disposeNode(node) {
|
|
291
|
+
if (node.flags === 0) return; // Already freed
|
|
292
|
+
|
|
293
|
+
runCleanup(node);
|
|
294
|
+
|
|
295
|
+
// 1. Unlink from dependencies
|
|
296
|
+
let dLink = node.headDep;
|
|
297
|
+
while (dLink !== null) {
|
|
298
|
+
const next = dLink.nextDep;
|
|
299
|
+
freeLink(dLink, node, dLink.source);
|
|
300
|
+
dLink = next;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 2. Unlink from subscribers
|
|
304
|
+
let sLink = node.headSub;
|
|
305
|
+
while (sLink !== null) {
|
|
306
|
+
const target = sLink.target;
|
|
307
|
+
const next = sLink.nextSub;
|
|
308
|
+
|
|
309
|
+
const pDep = sLink.prevDep;
|
|
310
|
+
const nDep = sLink.nextDep;
|
|
311
|
+
if (pDep !== null) pDep.nextDep = nDep; else target.headDep = nDep;
|
|
312
|
+
if (nDep !== null) nDep.prevDep = pDep; else target.tailDep = pDep;
|
|
313
|
+
|
|
314
|
+
sLink.source = null;
|
|
315
|
+
sLink.target = null;
|
|
316
|
+
sLink.prevDep = null;
|
|
317
|
+
sLink.nextDep = null;
|
|
318
|
+
sLink.prevSub = null;
|
|
319
|
+
sLink.nextSub = null;
|
|
320
|
+
sLink.nextFree = freeLinkHead;
|
|
321
|
+
freeLinkHead = sLink;
|
|
322
|
+
activeLinks = (activeLinks - 1) | 0;
|
|
323
|
+
|
|
324
|
+
sLink = next;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 3. Clear node state and return to pool
|
|
328
|
+
node.computeFn = undefined;
|
|
329
|
+
node.cleanupFn = undefined;
|
|
330
|
+
node.scheduler = undefined;
|
|
331
|
+
node.value = undefined;
|
|
332
|
+
node.equals = undefined;
|
|
333
|
+
node.flags = 0;
|
|
334
|
+
node.headDep = null;
|
|
335
|
+
node.tailDep = null;
|
|
336
|
+
node.currentDep = null;
|
|
337
|
+
node.headSub = null;
|
|
338
|
+
|
|
339
|
+
node.gen = (node.gen + 1) | 0;
|
|
340
|
+
node.nextFree = freeNodeHead;
|
|
341
|
+
freeNodeHead = node;
|
|
342
|
+
activeNodes = (activeNodes - 1) | 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Claim a node from the free pool, reinitialise, and return it.
|
|
347
|
+
* Grows pool per `policy` if exhausted.
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
function createNode(value, flags) {
|
|
351
|
+
if (freeNodeHead === null) {
|
|
352
|
+
if (policy === "throw") throw new CapacityError("nodes", currentNodesCapacity);
|
|
353
|
+
const newCap = currentNodesCapacity * 2;
|
|
354
|
+
|
|
355
|
+
const newNodes = new Array(newCap - currentNodesCapacity);
|
|
356
|
+
for (let i = 0; i < newNodes.length; i++) newNodes[i] = new ReactiveNode();
|
|
357
|
+
for (let i = 0; i < newNodes.length - 1; i++) newNodes[i].nextFree = newNodes[i + 1];
|
|
358
|
+
|
|
359
|
+
const startIdx = nodePool.length;
|
|
360
|
+
nodePool.length = newCap; // Pre-allocate the new length
|
|
361
|
+
for (let i = 0; i < newNodes.length; i++) {
|
|
362
|
+
nodePool[startIdx + i] = newNodes[i];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
freeNodeHead = newNodes[0];
|
|
366
|
+
|
|
367
|
+
effectQueueA.length = newCap;
|
|
368
|
+
effectQueueB.length = newCap;
|
|
369
|
+
markStack.length = newCap;
|
|
370
|
+
|
|
371
|
+
currentNodesCapacity = newCap;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const node = freeNodeHead;
|
|
375
|
+
freeNodeHead = node.nextFree;
|
|
376
|
+
node.nextFree = null;
|
|
377
|
+
activeNodes = (activeNodes + 1) | 0;
|
|
378
|
+
|
|
379
|
+
node.value = value;
|
|
380
|
+
node.flags = flags | 0;
|
|
381
|
+
node.headDep = null;
|
|
382
|
+
node.tailDep = null;
|
|
383
|
+
node.currentDep = null;
|
|
384
|
+
node.headSub = null;
|
|
385
|
+
node.version = 0;
|
|
386
|
+
node.evalVersion = 0;
|
|
387
|
+
node.markEpoch = 0;
|
|
388
|
+
return node;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Invoke registered cleanup function(s) on `node` and clear. @private */
|
|
392
|
+
function runCleanup(node) {
|
|
393
|
+
const cleanup = node.cleanupFn;
|
|
394
|
+
if (cleanup) {
|
|
395
|
+
if (typeof cleanup === "function") cleanup();
|
|
396
|
+
else for (let i = 0; i < cleanup.length; i++) cleanup[i]();
|
|
397
|
+
node.cleanupFn = undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Mark all transitive subscribers of `startNode` dirty.
|
|
403
|
+
* Iterative DFS via the markStack to avoid call-stack growth.
|
|
404
|
+
* Effects are enqueued for the flush phase; computeds are merely marked.
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
function markDownstream(startNode) {
|
|
408
|
+
let stackLen = 0 | 0;
|
|
409
|
+
markStack[stackLen] = startNode;
|
|
410
|
+
stackLen = (stackLen + 1) | 0;
|
|
411
|
+
|
|
412
|
+
while (stackLen > 0) {
|
|
413
|
+
stackLen = (stackLen - 1) | 0;
|
|
414
|
+
const n = markStack[stackLen];
|
|
415
|
+
|
|
416
|
+
let link = n.headSub;
|
|
417
|
+
while (link !== null) {
|
|
418
|
+
const t = link.target;
|
|
419
|
+
if ((t.markEpoch | 0) !== (globalVersion | 0)) {
|
|
420
|
+
t.markEpoch = globalVersion | 0;
|
|
421
|
+
const flags = t.flags | 0;
|
|
422
|
+
|
|
423
|
+
if ((flags & FLAG_EFFECT) !== 0) {
|
|
424
|
+
if ((flags & FLAG_QUEUED) === 0) {
|
|
425
|
+
t.flags = flags | FLAG_QUEUED;
|
|
426
|
+
activeQueue[activeQueueLen] = t;
|
|
427
|
+
activeQueueLen = (activeQueueLen + 1) | 0;
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
markStack[stackLen] = t;
|
|
431
|
+
stackLen = (stackLen + 1) | 0;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
link = link.nextSub;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Execute an effect through a scheduler trampoline. Guards against running
|
|
441
|
+
* a stale node (disposed and recycled before the scheduler fired).
|
|
442
|
+
* @private
|
|
443
|
+
*/
|
|
444
|
+
function safeExecute(node, gen) {
|
|
445
|
+
if ((node.gen | 0) !== (gen | 0)) return;
|
|
446
|
+
if ((node.flags & FLAG_EFFECT) === 0) return;
|
|
447
|
+
executeEffect(node);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Drain the effect queue. Double-buffered so new effects scheduled mid-flush
|
|
452
|
+
* end up in the next pass.
|
|
453
|
+
* @private
|
|
454
|
+
*/
|
|
455
|
+
function flushEffects() {
|
|
456
|
+
if (isFlushing) return;
|
|
457
|
+
isFlushing = true;
|
|
458
|
+
let passes = 0 | 0;
|
|
459
|
+
|
|
460
|
+
while (activeQueueLen > 0) {
|
|
461
|
+
passes = (passes + 1) | 0;
|
|
462
|
+
if (passes > maxFlushPasses) {
|
|
463
|
+
isFlushing = false;
|
|
464
|
+
throw new Error("CycleError: flush passes exceeded");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const toRun = activeQueueLen | 0;
|
|
468
|
+
const currentQueue = activeQueue;
|
|
469
|
+
|
|
470
|
+
isQueueA = !isQueueA;
|
|
471
|
+
activeQueue = isQueueA ? effectQueueA : effectQueueB;
|
|
472
|
+
activeQueueLen = 0 | 0;
|
|
473
|
+
|
|
474
|
+
for (let i = 0; i < toRun; i++) {
|
|
475
|
+
const node = currentQueue[i];
|
|
476
|
+
const scheduler = node.scheduler;
|
|
477
|
+
if (scheduler) {
|
|
478
|
+
const gen = node.gen | 0;
|
|
479
|
+
scheduler(() => safeExecute(node, gen));
|
|
480
|
+
} else {
|
|
481
|
+
executeEffect(node);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
isFlushing = false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Free any tail links not visited during the current re-tracking pass.
|
|
490
|
+
* Called after computeFn returns: anything still hanging off the cursor is stale.
|
|
491
|
+
* @private
|
|
492
|
+
*/
|
|
493
|
+
function severTail(node) {
|
|
494
|
+
let stale = activeObserverCurrentDep;
|
|
495
|
+
if (stale !== null) {
|
|
496
|
+
let prev = stale.prevDep;
|
|
497
|
+
if (prev !== null) prev.nextDep = null; else node.headDep = null;
|
|
498
|
+
node.tailDep = prev;
|
|
499
|
+
|
|
500
|
+
while (stale !== null) {
|
|
501
|
+
let next = stale.nextDep;
|
|
502
|
+
freeLink(stale, node, stale.source);
|
|
503
|
+
stale = next;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Run an effect's compute body, re-tracking dependencies.
|
|
510
|
+
* Short-circuits if no dependency has bumped its version since last eval.
|
|
511
|
+
* @private
|
|
512
|
+
*/
|
|
513
|
+
function executeEffect(node) {
|
|
514
|
+
if ((node.flags & FLAG_COMPUTING) !== 0) throw new Error("CycleError: Infinite effect loop detected.");
|
|
515
|
+
|
|
516
|
+
const isFirst = node.evalVersion === 0;
|
|
517
|
+
|
|
518
|
+
if (!isFirst) {
|
|
519
|
+
let link = node.headDep;
|
|
520
|
+
const evalVer = node.evalVersion | 0;
|
|
521
|
+
let needsRun = false;
|
|
522
|
+
while (link !== null) {
|
|
523
|
+
const dep = link.source;
|
|
524
|
+
if ((dep.flags & FLAG_COMPUTED) !== 0) pullComputed(dep);
|
|
525
|
+
|
|
526
|
+
// Overflow-safe modular arithmetic version check
|
|
527
|
+
if (((dep.version - evalVer) | 0) > 0) {
|
|
528
|
+
needsRun = true;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
link = link.nextDep;
|
|
532
|
+
}
|
|
533
|
+
if (!needsRun) {
|
|
534
|
+
node.flags = node.flags & ~FLAG_QUEUED;
|
|
535
|
+
node.evalVersion = globalVersion | 0;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
node.flags = (node.flags & ~FLAG_QUEUED) | FLAG_COMPUTING;
|
|
541
|
+
|
|
542
|
+
runCleanup(node);
|
|
543
|
+
|
|
544
|
+
const prevObserver = currentObserver;
|
|
545
|
+
const prevActiveDep = activeObserverCurrentDep;
|
|
546
|
+
const prevTracking = isTrackingDeps;
|
|
547
|
+
|
|
548
|
+
currentObserver = node;
|
|
549
|
+
activeObserverCurrentDep = node.headDep;
|
|
550
|
+
isTrackingDeps = true;
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
node.computeFn();
|
|
554
|
+
} finally {
|
|
555
|
+
severTail(node);
|
|
556
|
+
node.currentDep = activeObserverCurrentDep;
|
|
557
|
+
|
|
558
|
+
currentObserver = prevObserver;
|
|
559
|
+
activeObserverCurrentDep = prevActiveDep;
|
|
560
|
+
isTrackingDeps = prevTracking;
|
|
561
|
+
|
|
562
|
+
node.flags = node.flags & ~FLAG_COMPUTING;
|
|
563
|
+
node.evalVersion = globalVersion | 0;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Resolve a computed node's current value: re-run if a dependency has
|
|
569
|
+
* changed since last evaluation, else return cached value.
|
|
570
|
+
*
|
|
571
|
+
* Errors thrown by computeFn are captured in `node.value` with FLAG_HAS_ERROR;
|
|
572
|
+
* subsequent reads re-throw until a dependency change re-runs computeFn.
|
|
573
|
+
*
|
|
574
|
+
* @private
|
|
575
|
+
*/
|
|
576
|
+
function pullComputed(node) {
|
|
577
|
+
if ((node.evalVersion | 0) === (globalVersion | 0)) {
|
|
578
|
+
if ((node.flags & FLAG_HAS_ERROR) !== 0) throw node.value;
|
|
579
|
+
return node.value;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let shouldRun = node.evalVersion === 0;
|
|
583
|
+
if (!shouldRun) {
|
|
584
|
+
let link = node.headDep;
|
|
585
|
+
const evalVer = node.evalVersion | 0;
|
|
586
|
+
while (link !== null) {
|
|
587
|
+
const dep = link.source;
|
|
588
|
+
if ((dep.flags & FLAG_COMPUTED) !== 0) pullComputed(dep);
|
|
589
|
+
// Modular Arithmetic 32-bit Wrap Check
|
|
590
|
+
if (((dep.version - evalVer) | 0) > 0) {
|
|
591
|
+
shouldRun = true;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
link = link.nextDep;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (shouldRun) {
|
|
599
|
+
if ((node.flags & FLAG_COMPUTING) !== 0) throw new Error("CycleError: Circular dependency detected.");
|
|
600
|
+
node.flags = node.flags | FLAG_COMPUTING;
|
|
601
|
+
|
|
602
|
+
// Run cleanups registered during the previous compute pass before re-tracking.
|
|
603
|
+
// Mirrors effect semantics so `onCleanup` works in both kinds of observer.
|
|
604
|
+
runCleanup(node);
|
|
605
|
+
|
|
606
|
+
const prevObserver = currentObserver;
|
|
607
|
+
const prevActiveDep = activeObserverCurrentDep;
|
|
608
|
+
const prevTracking = isTrackingDeps;
|
|
609
|
+
|
|
610
|
+
currentObserver = node;
|
|
611
|
+
activeObserverCurrentDep = node.headDep;
|
|
612
|
+
isTrackingDeps = true;
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const newValue = node.computeFn();
|
|
616
|
+
const eq = node.equals;
|
|
617
|
+
if (node.evalVersion === 0 || !eq || !eq(node.value, newValue)) {
|
|
618
|
+
node.value = newValue;
|
|
619
|
+
node.version = globalVersion | 0;
|
|
620
|
+
}
|
|
621
|
+
node.flags = node.flags & ~FLAG_HAS_ERROR;
|
|
622
|
+
} catch (err) {
|
|
623
|
+
node.value = err;
|
|
624
|
+
node.flags = node.flags | FLAG_HAS_ERROR;
|
|
625
|
+
node.version = globalVersion | 0;
|
|
626
|
+
} finally {
|
|
627
|
+
severTail(node);
|
|
628
|
+
node.currentDep = activeObserverCurrentDep;
|
|
629
|
+
|
|
630
|
+
currentObserver = prevObserver;
|
|
631
|
+
activeObserverCurrentDep = prevActiveDep;
|
|
632
|
+
isTrackingDeps = prevTracking;
|
|
633
|
+
|
|
634
|
+
node.flags = node.flags & ~FLAG_COMPUTING;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
node.evalVersion = globalVersion | 0;
|
|
639
|
+
if ((node.flags & FLAG_HAS_ERROR) !== 0) throw node.value;
|
|
640
|
+
return node.value;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// --- PUBLIC API SURFACE ---
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Create a reactive signal.
|
|
647
|
+
*
|
|
648
|
+
* @template T
|
|
649
|
+
* @param {T} initial Initial value.
|
|
650
|
+
* @param {object} [opts]
|
|
651
|
+
* @param {(a:T,b:T)=>boolean} [opts.equals=Object.is]
|
|
652
|
+
* Equality predicate. Returning true short-circuits notification.
|
|
653
|
+
* @returns {Signal<T>}
|
|
654
|
+
*/
|
|
655
|
+
function signal(initial, opts = {}) {
|
|
656
|
+
const node = createNode(initial, FLAG_SIGNAL);
|
|
657
|
+
node.equals = opts.equals !== undefined ? opts.equals : Object.is;
|
|
658
|
+
node.version = globalVersion | 0;
|
|
659
|
+
statSignals = (statSignals + 1) | 0;
|
|
660
|
+
|
|
661
|
+
const read = () => {
|
|
662
|
+
if (isTrackingDeps && currentObserver !== null) allocateLink(node, currentObserver);
|
|
663
|
+
return node.value;
|
|
664
|
+
};
|
|
665
|
+
read.peek = () => node.value;
|
|
666
|
+
read.set = (value) => {
|
|
667
|
+
const eq = node.equals;
|
|
668
|
+
if (eq && eq(node.value, value)) return;
|
|
669
|
+
node.value = value;
|
|
670
|
+
globalVersion = (globalVersion + 1) | 0;
|
|
671
|
+
node.version = globalVersion | 0;
|
|
672
|
+
markDownstream(node);
|
|
673
|
+
if (batchDepth === 0) flushEffects();
|
|
674
|
+
};
|
|
675
|
+
read.update = (fn) => read.set(fn(node.value));
|
|
676
|
+
|
|
677
|
+
read.subscribe = (fn) => {
|
|
678
|
+
let captured;
|
|
679
|
+
const invokeSub = () => fn(captured);
|
|
680
|
+
return effect(() => {
|
|
681
|
+
captured = read();
|
|
682
|
+
untrack(invokeSub);
|
|
683
|
+
});
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Secret pointer for safe, isolated disposal without allocating closures
|
|
687
|
+
read[NODE_PTR] = node;
|
|
688
|
+
read[NODE_GEN] = node.gen | 0;
|
|
689
|
+
return read;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Create a memoised, lazy derived value. The compute body only runs when a
|
|
694
|
+
* downstream observer reads it AND a dependency has changed since the last
|
|
695
|
+
* read.
|
|
696
|
+
*
|
|
697
|
+
* @template T
|
|
698
|
+
* @param {() => T} fn Compute body.
|
|
699
|
+
* @param {object} [opts]
|
|
700
|
+
* @param {(a:T,b:T)=>boolean} [opts.equals=Object.is]
|
|
701
|
+
* Equality predicate. Returning true blocks propagation downstream.
|
|
702
|
+
* @returns {Computed<T>}
|
|
703
|
+
*/
|
|
704
|
+
function computed(fn, opts = {}) {
|
|
705
|
+
const node = createNode(undefined, FLAG_COMPUTED);
|
|
706
|
+
node.computeFn = fn;
|
|
707
|
+
node.equals = opts.equals !== undefined ? opts.equals : Object.is;
|
|
708
|
+
statComputeds = (statComputeds + 1) | 0;
|
|
709
|
+
|
|
710
|
+
const read = () => {
|
|
711
|
+
if (isTrackingDeps && currentObserver !== null) allocateLink(node, currentObserver);
|
|
712
|
+
return pullComputed(node);
|
|
713
|
+
};
|
|
714
|
+
read.peek = () => pullComputed(node);
|
|
715
|
+
|
|
716
|
+
read.subscribe = (fn) => {
|
|
717
|
+
let captured;
|
|
718
|
+
const invokeSub = () => fn(captured);
|
|
719
|
+
return effect(() => {
|
|
720
|
+
captured = read();
|
|
721
|
+
untrack(invokeSub);
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
read[NODE_PTR] = node;
|
|
726
|
+
read[NODE_GEN] = node.gen | 0;
|
|
727
|
+
return read;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Create an eagerly-run side effect that re-executes whenever its tracked
|
|
732
|
+
* dependencies change.
|
|
733
|
+
*
|
|
734
|
+
* Errors thrown by the effect body propagate to the caller of `set()` (or
|
|
735
|
+
* to the scheduler trampoline). The effect's dependency state is fully
|
|
736
|
+
* restored before the error propagates.
|
|
737
|
+
*
|
|
738
|
+
* @param {() => void} fn Effect body.
|
|
739
|
+
* @param {object} [opts]
|
|
740
|
+
* @param {(run:()=>void)=>void} [opts.scheduler]
|
|
741
|
+
* Optional trampoline (e.g. queueMicrotask, requestAnimationFrame).
|
|
742
|
+
* Receives a `run` callback that the scheduler must eventually invoke.
|
|
743
|
+
* @returns {() => void} Dispose function. Idempotent. Safe to call
|
|
744
|
+
* after registry.destroy().
|
|
745
|
+
*/
|
|
746
|
+
function effect(fn, opts = {}) {
|
|
747
|
+
const node = createNode(undefined, FLAG_EFFECT);
|
|
748
|
+
node.computeFn = fn;
|
|
749
|
+
node.scheduler = opts.scheduler;
|
|
750
|
+
statEffects = (statEffects + 1) | 0;
|
|
751
|
+
|
|
752
|
+
const scheduler = opts.scheduler;
|
|
753
|
+
let firstRunError = null;
|
|
754
|
+
if (scheduler) {
|
|
755
|
+
const gen = node.gen | 0;
|
|
756
|
+
scheduler(() => safeExecute(node, gen));
|
|
757
|
+
} else {
|
|
758
|
+
try {
|
|
759
|
+
executeEffect(node);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
// First-run failure: dispose the node so we don't leak a half-initialised
|
|
762
|
+
// effect in the registry. Propagate the error to the caller.
|
|
763
|
+
firstRunError = err;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let disposed = false;
|
|
768
|
+
const birthGen = node.gen | 0;
|
|
769
|
+
const disposeFn = function dispose() {
|
|
770
|
+
if (disposed) return;
|
|
771
|
+
disposed = true;
|
|
772
|
+
// Generation guard: if destroy() (or a future direct disposal)
|
|
773
|
+
// recycled this slot to a different node, the stale closure must
|
|
774
|
+
// NOT operate on it — without this check, the closure would call
|
|
775
|
+
// disposeNode() on whatever now lives in the slot and desync
|
|
776
|
+
// statEffects (it would decrement even though the node is no
|
|
777
|
+
// longer an effect). Mirrors the NODE_GEN stamp on signals/computeds.
|
|
778
|
+
if ((node.gen | 0) !== birthGen) return;
|
|
779
|
+
if (node.flags !== 0) {
|
|
780
|
+
disposeNode(node);
|
|
781
|
+
statEffects = (statEffects - 1) | 0;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
if (firstRunError !== null) {
|
|
786
|
+
disposeFn();
|
|
787
|
+
throw firstRunError;
|
|
788
|
+
}
|
|
789
|
+
return disposeFn;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function dispose(api) {
|
|
793
|
+
// Safe read: foreign APIs or effects return undefined for NODE_PTR.
|
|
794
|
+
const node = api?.[NODE_PTR];
|
|
795
|
+
|
|
796
|
+
if (!node) {
|
|
797
|
+
// Plain functions self-execute (effect dispose handles and the
|
|
798
|
+
// dispose returned by .subscribe()). BUT we must not invoke a
|
|
799
|
+
// FOREIGN signal/computed here — they are also functions, and
|
|
800
|
+
// calling one would (1) read the value if untracked, or worse,
|
|
801
|
+
// (2) cross-link the foreign node into our observer if called
|
|
802
|
+
// inside a tracking context. Duck-type on `.peek`: every reactive
|
|
803
|
+
// primitive carries it; plain dispose handles do not.
|
|
804
|
+
if (typeof api === "function" && typeof api.peek !== "function") api();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Generation guard: if the slot has been recycled since this handle
|
|
809
|
+
// was issued, the handle is stale — silently no-op. Without this,
|
|
810
|
+
// a second dispose() call after the slot has been reallocated to a
|
|
811
|
+
// different signal/computed would free the new occupant.
|
|
812
|
+
const stamp = api[NODE_GEN] | 0;
|
|
813
|
+
if (stamp !== (node.gen | 0)) return;
|
|
814
|
+
|
|
815
|
+
if (node.flags !== 0) {
|
|
816
|
+
const isSig = (node.flags & FLAG_SIGNAL) !== 0;
|
|
817
|
+
const isComp = (node.flags & FLAG_COMPUTED) !== 0;
|
|
818
|
+
|
|
819
|
+
disposeNode(node);
|
|
820
|
+
|
|
821
|
+
if (isSig) statSignals = (statSignals - 1) | 0;
|
|
822
|
+
if (isComp) statComputeds = (statComputeds - 1) | 0;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Coalesce multiple synchronous writes into a single effect-flush pass.
|
|
828
|
+
* Nested batches are merged.
|
|
829
|
+
*
|
|
830
|
+
* @template T
|
|
831
|
+
* @param {() => T} fn
|
|
832
|
+
* @returns {T}
|
|
833
|
+
*/
|
|
834
|
+
function batch(fn) {
|
|
835
|
+
batchDepth = (batchDepth + 1) | 0;
|
|
836
|
+
try {
|
|
837
|
+
return fn();
|
|
838
|
+
} finally {
|
|
839
|
+
batchDepth = (batchDepth - 1) | 0;
|
|
840
|
+
if (batchDepth === 0) flushEffects();
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Run `fn` without recording any signal/computed reads as dependencies.
|
|
846
|
+
* Useful inside effects to peek at signals you don't want to react to.
|
|
847
|
+
*
|
|
848
|
+
* @template T
|
|
849
|
+
* @param {() => T} fn
|
|
850
|
+
* @returns {T}
|
|
851
|
+
*/
|
|
852
|
+
function untrack(fn) {
|
|
853
|
+
const prev = isTrackingDeps;
|
|
854
|
+
isTrackingDeps = false;
|
|
855
|
+
try {
|
|
856
|
+
return fn();
|
|
857
|
+
} finally {
|
|
858
|
+
isTrackingDeps = prev;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Register a function to run when the enclosing effect re-runs or is disposed.
|
|
864
|
+
*
|
|
865
|
+
* No-op if called outside an effect / computed body.
|
|
866
|
+
*
|
|
867
|
+
* @param {() => void} fn
|
|
868
|
+
*/
|
|
869
|
+
function onCleanup(fn) {
|
|
870
|
+
if (currentObserver !== null) {
|
|
871
|
+
const existing = currentObserver.cleanupFn;
|
|
872
|
+
if (existing === undefined) currentObserver.cleanupFn = fn;
|
|
873
|
+
else if (typeof existing === "function") currentObserver.cleanupFn = [existing, fn];
|
|
874
|
+
else existing.push(fn);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Snapshot of registry counters. Useful for diagnostics and tests.
|
|
880
|
+
* @returns {RegistryStats}
|
|
881
|
+
*/
|
|
882
|
+
function stats() {
|
|
883
|
+
return {
|
|
884
|
+
signals: statSignals,
|
|
885
|
+
computeds: statComputeds,
|
|
886
|
+
effects: statEffects,
|
|
887
|
+
activeLinks,
|
|
888
|
+
pooledLinks: currentLinkCapacity - activeLinks,
|
|
889
|
+
linkPoolCapacity: currentLinkCapacity,
|
|
890
|
+
nodePoolCapacity: currentNodesCapacity,
|
|
891
|
+
activeNodes
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Reset the entire registry: clear every node, every link, every queue, the
|
|
897
|
+
* global clock. All previously-issued read/set/dispose closures become
|
|
898
|
+
* no-ops (they're guarded internally — they will not corrupt the pool).
|
|
899
|
+
*/
|
|
900
|
+
function destroy() {
|
|
901
|
+
for (let i = 0; i < currentNodesCapacity; i++) {
|
|
902
|
+
const n = nodePool[i];
|
|
903
|
+
n.value = undefined;
|
|
904
|
+
n.computeFn = undefined;
|
|
905
|
+
n.cleanupFn = undefined;
|
|
906
|
+
n.equals = undefined;
|
|
907
|
+
n.scheduler = undefined;
|
|
908
|
+
n.flags = 0;
|
|
909
|
+
n.headDep = null;
|
|
910
|
+
n.tailDep = null;
|
|
911
|
+
n.currentDep = null;
|
|
912
|
+
n.headSub = null;
|
|
913
|
+
n.version = 0;
|
|
914
|
+
n.evalVersion = 0;
|
|
915
|
+
n.markEpoch = 0;
|
|
916
|
+
// Bump gen so any scheduler trampolines holding a stale node ref bail.
|
|
917
|
+
n.gen = (n.gen + 1) | 0;
|
|
918
|
+
if (i < currentNodesCapacity - 1) n.nextFree = nodePool[i + 1];
|
|
919
|
+
}
|
|
920
|
+
nodePool[currentNodesCapacity - 1].nextFree = null;
|
|
921
|
+
freeNodeHead = nodePool[0];
|
|
922
|
+
|
|
923
|
+
for (let i = 0; i < currentLinkCapacity; i++) {
|
|
924
|
+
const l = linkPool[i];
|
|
925
|
+
l.source = null;
|
|
926
|
+
l.target = null;
|
|
927
|
+
l.prevDep = null;
|
|
928
|
+
l.nextDep = null;
|
|
929
|
+
l.prevSub = null;
|
|
930
|
+
l.nextSub = null;
|
|
931
|
+
if (i < currentLinkCapacity - 1) l.nextFree = linkPool[i + 1];
|
|
932
|
+
}
|
|
933
|
+
linkPool[currentLinkCapacity - 1].nextFree = null;
|
|
934
|
+
freeLinkHead = linkPool[0];
|
|
935
|
+
|
|
936
|
+
activeNodes = 0 | 0;
|
|
937
|
+
activeLinks = 0 | 0;
|
|
938
|
+
activeQueueLen = 0 | 0;
|
|
939
|
+
isFlushing = false;
|
|
940
|
+
batchDepth = 0 | 0;
|
|
941
|
+
currentObserver = null;
|
|
942
|
+
activeObserverCurrentDep = null;
|
|
943
|
+
isTrackingDeps = false;
|
|
944
|
+
globalVersion = 1 | 0;
|
|
945
|
+
statSignals = 0 | 0;
|
|
946
|
+
statComputeds = 0 | 0;
|
|
947
|
+
statEffects = 0 | 0;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return {signal, computed, effect, dispose, batch, untrack, onCleanup, stats, destroy};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ─────────────────────────────────────────────────────────────────
|
|
954
|
+
// GLOBAL BINDINGS
|
|
955
|
+
// ─────────────────────────────────────────────────────────────────
|
|
956
|
+
|
|
957
|
+
let defaultRegistry = createRegistry();
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Replace the registry backing the top-level {@link signal} / {@link computed} /
|
|
961
|
+
* {@link effect} / {@link batch} / {@link untrack} / {@link onCleanup} / {@link stats}
|
|
962
|
+
* exports.
|
|
963
|
+
*
|
|
964
|
+
* @param {Registry} registry
|
|
965
|
+
*/
|
|
966
|
+
export function setDefaultRegistry(registry) {
|
|
967
|
+
defaultRegistry = registry;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** @type {Registry["signal"]} */
|
|
971
|
+
export function signal(initial, opts) {
|
|
972
|
+
return defaultRegistry.signal(initial, opts);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** @type {Registry["computed"]} */
|
|
976
|
+
export function computed(fn, opts) {
|
|
977
|
+
return defaultRegistry.computed(fn, opts);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/** @type {Registry["effect"]} */
|
|
981
|
+
export function effect(fn, opts) {
|
|
982
|
+
return defaultRegistry.effect(fn, opts);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export function dispose(api) {
|
|
986
|
+
return defaultRegistry.dispose(api);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** @type {Registry["batch"]} */
|
|
990
|
+
export function batch(fn) {
|
|
991
|
+
return defaultRegistry.batch(fn);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** @type {Registry["untrack"]} */
|
|
995
|
+
export function untrack(fn) {
|
|
996
|
+
return defaultRegistry.untrack(fn);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** @type {Registry["onCleanup"]} */
|
|
1000
|
+
export function onCleanup(fn) {
|
|
1001
|
+
return defaultRegistry.onCleanup(fn);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** @type {Registry["stats"]} */
|
|
1005
|
+
export function stats() {
|
|
1006
|
+
return defaultRegistry.stats();
|
|
1007
|
+
}
|