@pithyjs/signals 0.1.0-beta.0 → 0.1.0-beta.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/LICENSE +27 -0
- package/README.md +93 -4
- package/dist/internals.d.ts +29 -0
- package/dist/internals.d.ts.map +1 -0
- package/dist/internals.js +15 -0
- package/dist/internals.js.map +1 -0
- package/dist/signal.d.ts +40 -14
- package/dist/signal.d.ts.map +1 -1
- package/dist/signal.js +1030 -120
- package/dist/signal.js.map +1 -1
- package/dist/testing.d.ts +26 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +125 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +10 -3
package/dist/signal.js
CHANGED
|
@@ -5,25 +5,529 @@
|
|
|
5
5
|
"category": "feature"
|
|
6
6
|
}
|
|
7
7
|
*/
|
|
8
|
+
// The dependency graph (link, unlink, propagate, checkDirty) is adapted from alien-signals, MIT, Copyright (c) 2024-present Johnson Chu.
|
|
8
9
|
import { SIGNAL_SYMBOL } from './types';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
import { disposerCalled, effectWrappers, installIntrospection, nodesOf, rootOwners, } from './internals';
|
|
11
|
+
const DEV = !!import.meta.env?.DEV;
|
|
12
|
+
const MUTABLE = 1;
|
|
13
|
+
const RUNNING = 2;
|
|
14
|
+
const DIRTY = 8;
|
|
15
|
+
const PENDING = 16;
|
|
16
|
+
const QUEUED = 32;
|
|
17
|
+
const DISPOSED = 64;
|
|
18
|
+
const DRAINING = 128;
|
|
19
|
+
const DEFERRED = 256;
|
|
20
|
+
const FAILED = 512;
|
|
21
|
+
const CHECKING = 1024;
|
|
22
|
+
const OWNS = 2048;
|
|
23
|
+
let activeSub = null;
|
|
24
|
+
let currentOwner = null;
|
|
25
|
+
let computing = null;
|
|
26
|
+
let version = 0;
|
|
27
|
+
let markEpoch = 0;
|
|
28
|
+
let batchEpoch = 0;
|
|
29
|
+
const effectNodes = new WeakSet();
|
|
30
|
+
const noop = () => { };
|
|
18
31
|
const cleanupMap = new WeakMap();
|
|
32
|
+
const selfRegistered = new WeakMap();
|
|
33
|
+
const warnedImpure = new WeakSet();
|
|
34
|
+
function registerCleanup(owner, fn) {
|
|
35
|
+
if (!cleanupMap.has(owner))
|
|
36
|
+
return;
|
|
37
|
+
let stack = cleanupMap.get(owner);
|
|
38
|
+
if (!stack) {
|
|
39
|
+
stack = [];
|
|
40
|
+
cleanupMap.set(owner, stack);
|
|
41
|
+
}
|
|
42
|
+
stack.push(fn);
|
|
43
|
+
}
|
|
19
44
|
/**
|
|
20
|
-
*
|
|
45
|
+
* Runs every callback even if one throws, then rethrows the first failure.
|
|
46
|
+
* The caller has already dropped its reference to the stack, so a callback
|
|
47
|
+
* skipped here is unreachable forever — a throwing cleanup would otherwise
|
|
48
|
+
* leave the rest of an owner's children subscribed with no way to dispose them.
|
|
49
|
+
*
|
|
50
|
+
* Tracking and ownership are neutralised for the duration: a cleanup is
|
|
51
|
+
* teardown, not reactive work, so a signal it reads must not subscribe whatever
|
|
52
|
+
* effect happens to be mid-run, and an `onCleanup` it registers must not land
|
|
53
|
+
* on an owner that outlives it. Every disposal path shares this, not just the
|
|
54
|
+
* re-run one — a disposer called from inside another effect reaches here too.
|
|
21
55
|
*/
|
|
22
|
-
|
|
56
|
+
function drain(stack) {
|
|
57
|
+
const callerSub = activeSub;
|
|
58
|
+
const callerOwner = currentOwner;
|
|
59
|
+
activeSub = null;
|
|
60
|
+
currentOwner = null;
|
|
61
|
+
let failure;
|
|
62
|
+
let failed = false;
|
|
63
|
+
try {
|
|
64
|
+
for (const cleanup of stack) {
|
|
65
|
+
try {
|
|
66
|
+
cleanup();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!failed) {
|
|
70
|
+
failed = true;
|
|
71
|
+
failure = error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
activeSub = callerSub;
|
|
78
|
+
currentOwner = callerOwner;
|
|
79
|
+
}
|
|
80
|
+
if (failed)
|
|
81
|
+
throw failure;
|
|
82
|
+
}
|
|
83
|
+
function link(dep, sub) {
|
|
84
|
+
const prevDep = sub.depsTail;
|
|
85
|
+
if (prevDep !== undefined && prevDep.dep === dep)
|
|
86
|
+
return;
|
|
87
|
+
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps;
|
|
88
|
+
if (nextDep !== undefined && nextDep.dep === dep) {
|
|
89
|
+
nextDep.version = sub.version;
|
|
90
|
+
sub.depsTail = nextDep;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const prevSub = dep.subsTail;
|
|
94
|
+
if (prevSub !== undefined &&
|
|
95
|
+
prevSub.version === sub.version &&
|
|
96
|
+
prevSub.sub === sub) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const newLink = {
|
|
100
|
+
version: sub.version,
|
|
101
|
+
dep,
|
|
102
|
+
sub,
|
|
103
|
+
prevDep,
|
|
104
|
+
nextDep,
|
|
105
|
+
prevSub,
|
|
106
|
+
nextSub: undefined,
|
|
107
|
+
};
|
|
108
|
+
sub.depsTail = newLink;
|
|
109
|
+
dep.subsTail = newLink;
|
|
110
|
+
if (nextDep !== undefined)
|
|
111
|
+
nextDep.prevDep = newLink;
|
|
112
|
+
if (prevDep !== undefined)
|
|
113
|
+
prevDep.nextDep = newLink;
|
|
114
|
+
else
|
|
115
|
+
sub.deps = newLink;
|
|
116
|
+
if (prevSub !== undefined)
|
|
117
|
+
prevSub.nextSub = newLink;
|
|
118
|
+
else
|
|
119
|
+
dep.subs = newLink;
|
|
120
|
+
}
|
|
121
|
+
function unlink(target, sub) {
|
|
122
|
+
const { dep, prevDep, nextDep, nextSub, prevSub } = target;
|
|
123
|
+
if (nextDep !== undefined)
|
|
124
|
+
nextDep.prevDep = prevDep;
|
|
125
|
+
else
|
|
126
|
+
sub.depsTail = prevDep;
|
|
127
|
+
if (prevDep !== undefined)
|
|
128
|
+
prevDep.nextDep = nextDep;
|
|
129
|
+
else
|
|
130
|
+
sub.deps = nextDep;
|
|
131
|
+
if (nextSub !== undefined)
|
|
132
|
+
nextSub.prevSub = prevSub;
|
|
133
|
+
else
|
|
134
|
+
dep.subsTail = prevSub;
|
|
135
|
+
if (prevSub !== undefined)
|
|
136
|
+
prevSub.nextSub = nextSub;
|
|
137
|
+
else
|
|
138
|
+
dep.subs = nextSub;
|
|
139
|
+
return nextDep;
|
|
140
|
+
}
|
|
141
|
+
function unlinkAll(sub) {
|
|
142
|
+
let dep = sub.deps;
|
|
143
|
+
while (dep !== undefined)
|
|
144
|
+
dep = unlink(dep, sub);
|
|
145
|
+
}
|
|
146
|
+
function purgeDeps(sub) {
|
|
147
|
+
const tail = sub.depsTail;
|
|
148
|
+
let dep = tail !== undefined ? tail.nextDep : sub.deps;
|
|
149
|
+
while (dep !== undefined)
|
|
150
|
+
dep = unlink(dep, sub);
|
|
151
|
+
}
|
|
152
|
+
function settleDeps(sub, kept) {
|
|
153
|
+
if (sub.flags & DISPOSED) {
|
|
154
|
+
unlinkAll(sub);
|
|
155
|
+
}
|
|
156
|
+
else if (kept || sub.depsTail === undefined) {
|
|
157
|
+
let tail = sub.deps;
|
|
158
|
+
while (tail?.nextDep !== undefined)
|
|
159
|
+
tail = tail.nextDep;
|
|
160
|
+
sub.depsTail = tail;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
purgeDeps(sub);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function isValidLink(check, sub) {
|
|
167
|
+
let candidate = sub.depsTail;
|
|
168
|
+
while (candidate !== undefined) {
|
|
169
|
+
if (candidate === check)
|
|
170
|
+
return true;
|
|
171
|
+
candidate = candidate.prevDep;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
function isComputed(node) {
|
|
176
|
+
return (node.flags & MUTABLE) !== 0 && 'getter' in node;
|
|
177
|
+
}
|
|
178
|
+
let queue = [];
|
|
179
|
+
let queuedLength = 0;
|
|
180
|
+
let flushLevel = 0;
|
|
181
|
+
let segment = 0;
|
|
182
|
+
function isQueuedHere(node) {
|
|
183
|
+
return (node.flags & QUEUED) !== 0 && node.slot >= segment;
|
|
184
|
+
}
|
|
185
|
+
let ownersQueued = false;
|
|
186
|
+
function enqueue(node) {
|
|
187
|
+
node.flags |= QUEUED;
|
|
188
|
+
node.slot = queuedLength;
|
|
189
|
+
queue[queuedLength++] = node;
|
|
190
|
+
if (node.flags & OWNS)
|
|
191
|
+
ownersQueued = true;
|
|
192
|
+
}
|
|
193
|
+
function runQueuedAncestors(node) {
|
|
194
|
+
const chain = [];
|
|
195
|
+
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
196
|
+
if ((ancestor.flags & (QUEUED | DISPOSED)) === QUEUED)
|
|
197
|
+
chain.push(ancestor);
|
|
198
|
+
}
|
|
199
|
+
for (let at = chain.length - 1; at >= 0; at--)
|
|
200
|
+
run(chain[at]);
|
|
201
|
+
}
|
|
202
|
+
function notify(effectNode, direct) {
|
|
203
|
+
const flags = effectNode.flags;
|
|
204
|
+
if (flags & DISPOSED)
|
|
205
|
+
return;
|
|
206
|
+
effectNode.flags = flags | (direct ? DIRTY : PENDING);
|
|
207
|
+
if (!isQueuedHere(effectNode))
|
|
208
|
+
enqueue(effectNode);
|
|
209
|
+
}
|
|
210
|
+
function propagate(first) {
|
|
211
|
+
const epoch = ++markEpoch;
|
|
212
|
+
const stack = [];
|
|
213
|
+
let current = first;
|
|
214
|
+
do {
|
|
215
|
+
while (current !== undefined) {
|
|
216
|
+
const sub = current.sub;
|
|
217
|
+
const next = current.nextSub;
|
|
218
|
+
const flags = sub.flags;
|
|
219
|
+
const direct = stack.length === 0;
|
|
220
|
+
if (flags & DISPOSED) {
|
|
221
|
+
current = next;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!isComputed(sub)) {
|
|
225
|
+
notify(sub, direct);
|
|
226
|
+
current = next;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const node = sub;
|
|
230
|
+
if (flags & DRAINING) {
|
|
231
|
+
current = next;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (flags & RUNNING) {
|
|
235
|
+
if (isValidLink(current, node)) {
|
|
236
|
+
node.flags = flags | (direct ? DIRTY : PENDING);
|
|
237
|
+
}
|
|
238
|
+
current = next;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
node.flags = flags | (direct ? DIRTY : PENDING);
|
|
242
|
+
const settled = batchDepth > 0 && node.marked > batchEpoch && flags & (DIRTY | PENDING);
|
|
243
|
+
if (node.marked !== epoch && !settled) {
|
|
244
|
+
node.marked = epoch;
|
|
245
|
+
if (node.subs !== undefined) {
|
|
246
|
+
stack.push(next);
|
|
247
|
+
current = node.subs;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
current = next;
|
|
252
|
+
}
|
|
253
|
+
current = stack.pop();
|
|
254
|
+
} while (current !== undefined || stack.length > 0);
|
|
255
|
+
}
|
|
256
|
+
function shallowPropagate(first) {
|
|
257
|
+
let current = first;
|
|
258
|
+
while (current !== undefined) {
|
|
259
|
+
const sub = current.sub;
|
|
260
|
+
const flags = sub.flags;
|
|
261
|
+
if ((flags & (PENDING | DIRTY)) === PENDING)
|
|
262
|
+
sub.flags = flags | DIRTY;
|
|
263
|
+
current = current.nextSub;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function checkDirty(start, origin) {
|
|
267
|
+
const stack = [];
|
|
268
|
+
origin.flags |= CHECKING;
|
|
269
|
+
try {
|
|
270
|
+
return walkDirty(start, origin, stack);
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
origin.flags &= ~CHECKING;
|
|
274
|
+
for (const link of stack)
|
|
275
|
+
link.dep.flags &= ~CHECKING;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function walkDirty(start, origin, stack) {
|
|
279
|
+
let current = start;
|
|
280
|
+
let sub = origin;
|
|
281
|
+
let dirty = false;
|
|
282
|
+
for (;;) {
|
|
283
|
+
const dep = current.dep;
|
|
284
|
+
const flags = dep.flags;
|
|
285
|
+
if (sub.flags & DIRTY) {
|
|
286
|
+
dirty = true;
|
|
287
|
+
}
|
|
288
|
+
else if (isComputed(dep) && !(flags & (DISPOSED | RUNNING | CHECKING))) {
|
|
289
|
+
if (flags & DIRTY) {
|
|
290
|
+
if (updateComputed(dep)) {
|
|
291
|
+
if (dep.subs?.nextSub !== undefined)
|
|
292
|
+
shallowPropagate(dep.subs);
|
|
293
|
+
dirty = true;
|
|
294
|
+
}
|
|
295
|
+
else if (dep.flags & DIRTY) {
|
|
296
|
+
dirty = true;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (flags & PENDING && dep.deps !== undefined) {
|
|
300
|
+
dep.flags |= CHECKING;
|
|
301
|
+
stack.push(current);
|
|
302
|
+
current = dep.deps;
|
|
303
|
+
sub = dep;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!dirty && current.nextDep !== undefined) {
|
|
308
|
+
current = current.nextDep;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
let resumed = false;
|
|
312
|
+
let parent;
|
|
313
|
+
while ((parent = stack.pop()) !== undefined) {
|
|
314
|
+
const node = sub;
|
|
315
|
+
node.flags &= ~CHECKING;
|
|
316
|
+
if (dirty) {
|
|
317
|
+
const changed = updateComputed(node);
|
|
318
|
+
if (changed && node.subs?.nextSub !== undefined) {
|
|
319
|
+
shallowPropagate(node.subs);
|
|
320
|
+
}
|
|
321
|
+
if (changed || node.flags & DIRTY) {
|
|
322
|
+
sub = parent.sub;
|
|
323
|
+
current = parent;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
dirty = false;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
node.flags &= ~PENDING;
|
|
330
|
+
}
|
|
331
|
+
sub = parent.sub;
|
|
332
|
+
if (parent.nextDep !== undefined) {
|
|
333
|
+
current = parent.nextDep;
|
|
334
|
+
resumed = true;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
current = parent;
|
|
338
|
+
}
|
|
339
|
+
if (resumed)
|
|
340
|
+
continue;
|
|
341
|
+
return dirty && !(sub.flags & DISPOSED);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
let errorScopes = 0;
|
|
345
|
+
let recordedError;
|
|
346
|
+
let inBatchBody = false;
|
|
347
|
+
function record(error, node, compute) {
|
|
348
|
+
if (errorScopes === 0) {
|
|
349
|
+
node.flags |= DIRTY;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
if (compute && inBatchBody) {
|
|
353
|
+
node.flags |= DIRTY;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
recordedError ?? (recordedError = [error]);
|
|
357
|
+
}
|
|
358
|
+
function warnImpure(what) {
|
|
359
|
+
const node = computing;
|
|
360
|
+
if (!node || warnedImpure.has(node))
|
|
361
|
+
return;
|
|
362
|
+
warnedImpure.add(node);
|
|
363
|
+
console.warn(`[PithyJS] A computed ${what} while computing. A computed must be pure: move the work into an effect() that reads the computed.`);
|
|
364
|
+
}
|
|
365
|
+
function updateComputed(node) {
|
|
366
|
+
const getter = node.getter;
|
|
367
|
+
if (!getter)
|
|
368
|
+
return false;
|
|
369
|
+
const previousComputing = computing;
|
|
370
|
+
const stale = cleanupMap.get(node);
|
|
371
|
+
cleanupMap.set(node, null);
|
|
372
|
+
node.flags = (node.flags & ~(DIRTY | PENDING)) | RUNNING;
|
|
373
|
+
computing = node;
|
|
374
|
+
if (stale && stale.length > 0) {
|
|
375
|
+
node.flags |= DRAINING;
|
|
376
|
+
try {
|
|
377
|
+
drain(stale);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
node.flags &= ~(RUNNING | DRAINING);
|
|
381
|
+
computing = previousComputing;
|
|
382
|
+
record(error, node, false);
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
node.flags &= ~DRAINING;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (node.flags & DISPOSED) {
|
|
390
|
+
node.flags &= ~RUNNING;
|
|
391
|
+
computing = previousComputing;
|
|
392
|
+
unlinkAll(node);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
const previousSub = activeSub;
|
|
396
|
+
const previousOwner = currentOwner;
|
|
397
|
+
const previousTracking = isTrackingContext;
|
|
398
|
+
node.depsTail = undefined;
|
|
399
|
+
node.version = ++version;
|
|
400
|
+
activeSub = node;
|
|
401
|
+
currentOwner = node;
|
|
402
|
+
isTrackingContext = true;
|
|
403
|
+
let next;
|
|
404
|
+
let ok = false;
|
|
405
|
+
try {
|
|
406
|
+
next = getter();
|
|
407
|
+
ok = true;
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
record(error, node, true);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
activeSub = previousSub;
|
|
415
|
+
currentOwner = previousOwner;
|
|
416
|
+
isTrackingContext = previousTracking;
|
|
417
|
+
computing = previousComputing;
|
|
418
|
+
node.flags &= ~RUNNING;
|
|
419
|
+
settleDeps(node, !ok);
|
|
420
|
+
}
|
|
421
|
+
const changed = node.value !== next;
|
|
422
|
+
node.value = next;
|
|
423
|
+
return changed;
|
|
424
|
+
}
|
|
425
|
+
function refresh(node) {
|
|
426
|
+
const flags = node.flags;
|
|
427
|
+
if (flags & (DISPOSED | RUNNING | CHECKING) || !(flags & (DIRTY | PENDING))) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (flags & DIRTY ||
|
|
431
|
+
(node.deps !== undefined && checkDirty(node.deps, node))) {
|
|
432
|
+
if (updateComputed(node))
|
|
433
|
+
shallowPropagate(node.subs);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
node.flags &= ~PENDING;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function readComputed(node) {
|
|
440
|
+
refresh(node);
|
|
441
|
+
const sub = activeSub;
|
|
442
|
+
if (sub !== null &&
|
|
443
|
+
sub !== node &&
|
|
444
|
+
isComputed(sub) &&
|
|
445
|
+
(node.flags & (DIRTY | RUNNING | CHECKING | DISPOSED)) === DIRTY) {
|
|
446
|
+
sub.flags |= DIRTY;
|
|
447
|
+
}
|
|
448
|
+
if (sub !== null &&
|
|
449
|
+
sub !== node &&
|
|
450
|
+
!(node.flags & DISPOSED) &&
|
|
451
|
+
!(sub.flags & DISPOSED)) {
|
|
452
|
+
link(node, sub);
|
|
453
|
+
}
|
|
454
|
+
return node.value;
|
|
455
|
+
}
|
|
456
|
+
let notifyDepth = 0;
|
|
457
|
+
let batchFlushes = 0;
|
|
458
|
+
let reentries = 0;
|
|
459
|
+
function flushFrom(start, depth, reentered) {
|
|
460
|
+
let failure;
|
|
461
|
+
const outermost = flushLevel++ === 0;
|
|
462
|
+
let index = start;
|
|
463
|
+
try {
|
|
464
|
+
while (index < queuedLength) {
|
|
465
|
+
const node = queue[index];
|
|
466
|
+
queue[index++] = undefined;
|
|
467
|
+
if (node === undefined)
|
|
468
|
+
continue;
|
|
469
|
+
const stale = node.slot !== index - 1;
|
|
470
|
+
if (stale && node.flags & DISPOSED)
|
|
471
|
+
continue;
|
|
472
|
+
if (!stale &&
|
|
473
|
+
(node.flags & (RUNNING | DEFERRED)) === RUNNING &&
|
|
474
|
+
index < queuedLength) {
|
|
475
|
+
node.flags |= DEFERRED;
|
|
476
|
+
node.slot = queuedLength;
|
|
477
|
+
queue[queuedLength++] = node;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
node.flags &= ~DEFERRED;
|
|
481
|
+
const prior = reentries;
|
|
482
|
+
try {
|
|
483
|
+
if (ownersQueued && node.parent !== null)
|
|
484
|
+
runQueuedAncestors(node);
|
|
485
|
+
if (stale && node.flags & FAILED)
|
|
486
|
+
runEffect(node);
|
|
487
|
+
else
|
|
488
|
+
run(node);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
failure ?? (failure = [error]);
|
|
492
|
+
if (depth && (reentered || reentries !== prior))
|
|
493
|
+
throw failure[0];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
flushLevel--;
|
|
499
|
+
while (index < queuedLength) {
|
|
500
|
+
const node = queue[index];
|
|
501
|
+
queue[index++] = undefined;
|
|
502
|
+
if (node !== undefined)
|
|
503
|
+
node.flags &= ~QUEUED;
|
|
504
|
+
}
|
|
505
|
+
queuedLength = outermost ? 0 : start;
|
|
506
|
+
if (outermost) {
|
|
507
|
+
ownersQueued = false;
|
|
508
|
+
if (queue.length > 1024)
|
|
509
|
+
queue = [];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return failure;
|
|
513
|
+
}
|
|
514
|
+
function run(node) {
|
|
515
|
+
const flags = node.flags;
|
|
516
|
+
node.flags = flags & ~QUEUED;
|
|
517
|
+
if (flags & DISPOSED)
|
|
518
|
+
return;
|
|
519
|
+
if (flags & DIRTY ||
|
|
520
|
+
(flags & PENDING && node.deps !== undefined && checkDirty(node.deps, node))) {
|
|
521
|
+
runEffect(node);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
node.flags &= ~PENDING;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
23
527
|
/**
|
|
24
|
-
*
|
|
528
|
+
* How many `batch()` bodies are running; only the outermost one flushes.
|
|
25
529
|
*/
|
|
26
|
-
|
|
530
|
+
let batchDepth = 0;
|
|
27
531
|
/**
|
|
28
532
|
* Indicates whether we are currently inside a tracking context (effect or computed).
|
|
29
533
|
* Used to suppress signal read warnings in appropriate scopes.
|
|
@@ -34,11 +538,11 @@ let isTrackingContext = false;
|
|
|
34
538
|
*/
|
|
35
539
|
let suppressionDepth = 0;
|
|
36
540
|
/**
|
|
37
|
-
* Checks if a value is a
|
|
541
|
+
* Checks if a value is a signal or a computed at runtime.
|
|
38
542
|
* @param value - The value to check.
|
|
39
|
-
* @returns True if the value is a
|
|
543
|
+
* @returns True if the value is a signal or a computed.
|
|
40
544
|
*/
|
|
41
|
-
/** @codexApi {"parent":"pithy.feature.signals","name":"isSignal","stability":"stable","signature":"(value: unknown) => value is
|
|
545
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"isSignal","stability":"stable","signature":"(value: unknown) => value is ReadableSignal<unknown>"} */
|
|
42
546
|
export function isSignal(value) {
|
|
43
547
|
return (typeof value === 'function' &&
|
|
44
548
|
value[SIGNAL_SYMBOL] === true);
|
|
@@ -59,18 +563,21 @@ function suppressWarnings(fn) {
|
|
|
59
563
|
}
|
|
60
564
|
}
|
|
61
565
|
/**
|
|
62
|
-
* Registers a cleanup callback
|
|
63
|
-
*
|
|
566
|
+
* Registers a cleanup callback, executed once — before the current effect's
|
|
567
|
+
* next re-run, or when it is disposed, whichever comes first. Effects and
|
|
568
|
+
* computeds created during a run are
|
|
569
|
+
* owned by that run and torn down on the same schedule; use `createRoot` for
|
|
570
|
+
* work that must outlive the run that built it.
|
|
64
571
|
*
|
|
65
572
|
* @param fn - The cleanup function.
|
|
66
573
|
*/
|
|
67
574
|
/** @codexApi {"parent":"pithy.feature.signals","name":"onCleanup","stability":"stable","signature":"(fn: () => void) => void"} */
|
|
68
575
|
export function onCleanup(fn) {
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
576
|
+
if (!currentOwner)
|
|
577
|
+
return;
|
|
578
|
+
if (selfRegistered.get(fn) === currentOwner)
|
|
579
|
+
return;
|
|
580
|
+
registerCleanup(currentOwner, fn);
|
|
74
581
|
}
|
|
75
582
|
/**
|
|
76
583
|
* Creates a reactive effect that automatically tracks
|
|
@@ -81,36 +588,297 @@ export function onCleanup(fn) {
|
|
|
81
588
|
*/
|
|
82
589
|
/** @codexApi {"parent":"pithy.feature.signals","name":"effect","stability":"stable","signature":"(fn: () => void) => () => void"} */
|
|
83
590
|
export function effect(fn) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
591
|
+
if (DEV && computing)
|
|
592
|
+
warnImpure('created an effect');
|
|
593
|
+
const owner = currentOwner;
|
|
594
|
+
const node = {
|
|
595
|
+
fn,
|
|
596
|
+
deps: undefined,
|
|
597
|
+
depsTail: undefined,
|
|
598
|
+
flags: DIRTY,
|
|
599
|
+
version: 0,
|
|
600
|
+
parent: owner !== null && effectNodes.has(owner) ? owner : null,
|
|
601
|
+
slot: 0,
|
|
602
|
+
};
|
|
603
|
+
if (node.parent !== null) {
|
|
604
|
+
node.parent.flags |= OWNS;
|
|
605
|
+
if (node.parent.flags & QUEUED)
|
|
606
|
+
ownersQueued = true;
|
|
607
|
+
}
|
|
608
|
+
effectNodes.add(node);
|
|
609
|
+
cleanupMap.set(node, null);
|
|
88
610
|
const disposer = () => {
|
|
89
|
-
|
|
90
|
-
|
|
611
|
+
const flags = node.flags;
|
|
612
|
+
if (typeof __PITHY_TEST__ !== 'undefined' && __PITHY_TEST__)
|
|
613
|
+
disposerCalled.add(node);
|
|
614
|
+
if (flags & DISPOSED)
|
|
615
|
+
return;
|
|
616
|
+
node.flags |= DISPOSED;
|
|
617
|
+
node.parent = null;
|
|
618
|
+
try {
|
|
619
|
+
const stack = cleanupMap.get(node);
|
|
620
|
+
if (stack)
|
|
621
|
+
drain(stack);
|
|
622
|
+
}
|
|
623
|
+
finally {
|
|
624
|
+
cleanupMap.delete(node);
|
|
625
|
+
selfRegistered.delete(disposer);
|
|
626
|
+
node.fn = noop;
|
|
627
|
+
if (!(node.flags & RUNNING))
|
|
628
|
+
unlinkAll(node);
|
|
629
|
+
}
|
|
91
630
|
};
|
|
631
|
+
if (typeof __PITHY_TEST__ !== 'undefined' && __PITHY_TEST__)
|
|
632
|
+
effectWrappers.set(disposer, node);
|
|
633
|
+
if (owner) {
|
|
634
|
+
selfRegistered.set(disposer, owner);
|
|
635
|
+
registerCleanup(owner, disposer);
|
|
636
|
+
}
|
|
637
|
+
if (batchDepth > 0) {
|
|
638
|
+
enqueue(node);
|
|
639
|
+
return disposer;
|
|
640
|
+
}
|
|
641
|
+
const owns = errorScopes === 0;
|
|
642
|
+
const outer = recordedError;
|
|
643
|
+
let recorded;
|
|
644
|
+
if (owns) {
|
|
645
|
+
recordedError = undefined;
|
|
646
|
+
errorScopes++;
|
|
647
|
+
}
|
|
648
|
+
let thrown;
|
|
649
|
+
try {
|
|
650
|
+
runEffect(node);
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
thrown = [error];
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
if (owns) {
|
|
657
|
+
errorScopes--;
|
|
658
|
+
recorded = recordedError;
|
|
659
|
+
recordedError = outer;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const failure = recorded ?? thrown;
|
|
663
|
+
if (failure) {
|
|
664
|
+
if (!owner) {
|
|
665
|
+
try {
|
|
666
|
+
disposer();
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// The run's error is the one worth reporting.
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
throw failure[0];
|
|
673
|
+
}
|
|
92
674
|
return disposer;
|
|
93
675
|
}
|
|
676
|
+
function runEffect(node) {
|
|
677
|
+
if (node.flags & (DISPOSED | DRAINING))
|
|
678
|
+
return;
|
|
679
|
+
const stale = cleanupMap.get(node);
|
|
680
|
+
cleanupMap.set(node, null);
|
|
681
|
+
if (stale && stale.length > 0) {
|
|
682
|
+
node.flags |= DRAINING;
|
|
683
|
+
try {
|
|
684
|
+
drain(stale);
|
|
685
|
+
}
|
|
686
|
+
finally {
|
|
687
|
+
node.flags &= ~DRAINING;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (node.flags & DISPOSED)
|
|
691
|
+
return;
|
|
692
|
+
const running = node.flags & RUNNING;
|
|
693
|
+
node.flags = (node.flags & ~(DIRTY | PENDING | FAILED)) | RUNNING;
|
|
694
|
+
node.depsTail = undefined;
|
|
695
|
+
node.version = ++version;
|
|
696
|
+
const parentSub = activeSub;
|
|
697
|
+
const parentOwner = currentOwner;
|
|
698
|
+
const parentTracking = isTrackingContext;
|
|
699
|
+
const parentComputing = computing;
|
|
700
|
+
activeSub = node;
|
|
701
|
+
currentOwner = node;
|
|
702
|
+
isTrackingContext = true;
|
|
703
|
+
computing = null;
|
|
704
|
+
let ok = false;
|
|
705
|
+
try {
|
|
706
|
+
node.fn();
|
|
707
|
+
ok = true;
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
activeSub = parentSub;
|
|
711
|
+
currentOwner = parentOwner;
|
|
712
|
+
isTrackingContext = parentTracking;
|
|
713
|
+
computing = parentComputing;
|
|
714
|
+
if (!running)
|
|
715
|
+
node.flags &= ~RUNNING;
|
|
716
|
+
if (!ok)
|
|
717
|
+
node.flags |= FAILED;
|
|
718
|
+
settleDeps(node, !ok);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
94
721
|
/**
|
|
95
722
|
* Batches multiple signal updates together.
|
|
96
|
-
*
|
|
723
|
+
* Batches nest: effects triggered inside are deferred and run once, after the outermost batch.
|
|
97
724
|
*
|
|
98
725
|
* @param fn - The function performing multiple signal updates.
|
|
99
726
|
*/
|
|
100
727
|
/** @codexApi {"parent":"pithy.feature.signals","name":"batch","stability":"stable","signature":"(fn: () => void) => void"} */
|
|
101
728
|
export function batch(fn) {
|
|
102
|
-
|
|
729
|
+
let failure;
|
|
730
|
+
const outermost = batchDepth === 0;
|
|
731
|
+
const start = queuedLength;
|
|
732
|
+
const outerEpoch = batchEpoch;
|
|
733
|
+
const outerSegment = segment;
|
|
734
|
+
const outer = recordedError;
|
|
735
|
+
const outerBody = inBatchBody;
|
|
736
|
+
if (outermost) {
|
|
737
|
+
batchEpoch = markEpoch;
|
|
738
|
+
segment = start;
|
|
739
|
+
recordedError = undefined;
|
|
740
|
+
inBatchBody = true;
|
|
741
|
+
errorScopes++;
|
|
742
|
+
}
|
|
743
|
+
batchDepth++;
|
|
103
744
|
try {
|
|
104
745
|
fn();
|
|
105
746
|
}
|
|
106
747
|
finally {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
748
|
+
if (--batchDepth === 0) {
|
|
749
|
+
batchEpoch = outerEpoch;
|
|
750
|
+
segment = outerSegment;
|
|
751
|
+
inBatchBody = outerBody;
|
|
752
|
+
failure = recordedError;
|
|
753
|
+
recordedError = undefined;
|
|
754
|
+
const depth = notifyDepth++;
|
|
755
|
+
const nested = batchFlushes++ > 0;
|
|
756
|
+
if (nested)
|
|
757
|
+
reentries++;
|
|
758
|
+
try {
|
|
759
|
+
const flushed = flushFrom(start, depth, nested);
|
|
760
|
+
failure ?? (failure = flushed);
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
failure ?? (failure = [error]);
|
|
764
|
+
while (queuedLength > start) {
|
|
765
|
+
const node = queue[--queuedLength];
|
|
766
|
+
queue[queuedLength] = undefined;
|
|
767
|
+
if (node !== undefined)
|
|
768
|
+
node.flags &= ~QUEUED;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
finally {
|
|
772
|
+
notifyDepth = depth;
|
|
773
|
+
batchFlushes--;
|
|
774
|
+
inBatchBody = outerBody;
|
|
775
|
+
errorScopes--;
|
|
776
|
+
failure ?? (failure = recordedError);
|
|
777
|
+
recordedError = outer;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (failure)
|
|
782
|
+
throw failure[0];
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Reads signals without subscribing the enclosing effect to them.
|
|
786
|
+
*
|
|
787
|
+
* @param fn - The function to run untracked.
|
|
788
|
+
* @returns Whatever `fn` returns.
|
|
789
|
+
*/
|
|
790
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"untrack","stability":"stable","signature":"<T>(fn: () => T) => T"} */
|
|
791
|
+
export function untrack(fn) {
|
|
792
|
+
const previous = activeSub;
|
|
793
|
+
activeSub = null;
|
|
794
|
+
try {
|
|
795
|
+
return suppressWarnings(fn);
|
|
796
|
+
}
|
|
797
|
+
finally {
|
|
798
|
+
activeSub = previous;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Creates an ownership root: effects and computeds created inside `fn` are owned
|
|
803
|
+
* by the root rather than by any enclosing effect, so an enclosing re-run does
|
|
804
|
+
* not dispose them. Disposing the root disposes everything it owns — but a root
|
|
805
|
+
* created inside another root is independent: hold its disposer, because the
|
|
806
|
+
* outer root will not reach it.
|
|
807
|
+
*
|
|
808
|
+
* Tracking is untouched — a signal read inside `fn` still subscribes whatever
|
|
809
|
+
* effect is current.
|
|
810
|
+
*
|
|
811
|
+
* @param fn - Receives the root's disposer.
|
|
812
|
+
* @returns Whatever `fn` returns.
|
|
813
|
+
*/
|
|
814
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"createRoot","stability":"stable","signature":"<T>(fn: (dispose: () => void) => T) => T"} */
|
|
815
|
+
export function createRoot(fn) {
|
|
816
|
+
const owner = {};
|
|
817
|
+
cleanupMap.set(owner, null);
|
|
818
|
+
const dispose = () => {
|
|
819
|
+
const stack = cleanupMap.get(owner);
|
|
820
|
+
cleanupMap.delete(owner);
|
|
821
|
+
if (stack)
|
|
822
|
+
drain(stack);
|
|
823
|
+
};
|
|
824
|
+
if (typeof __PITHY_TEST__ !== 'undefined' && __PITHY_TEST__)
|
|
825
|
+
rootOwners.set(dispose, owner);
|
|
826
|
+
const parentOwner = currentOwner;
|
|
827
|
+
currentOwner = owner;
|
|
828
|
+
try {
|
|
829
|
+
return fn(dispose);
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
try {
|
|
833
|
+
dispose();
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// `fn`'s error is the one worth reporting.
|
|
837
|
+
}
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
currentOwner = parentOwner;
|
|
112
842
|
}
|
|
113
843
|
}
|
|
844
|
+
function callListeners(listeners, value, depth, reentered, failure) {
|
|
845
|
+
const previousSub = activeSub;
|
|
846
|
+
const previousComputing = computing;
|
|
847
|
+
activeSub = null;
|
|
848
|
+
computing = null;
|
|
849
|
+
try {
|
|
850
|
+
for (const fn of listeners) {
|
|
851
|
+
const prior = reentries;
|
|
852
|
+
try {
|
|
853
|
+
fn(value);
|
|
854
|
+
}
|
|
855
|
+
catch (error) {
|
|
856
|
+
failure ?? (failure = [error]);
|
|
857
|
+
if (depth && (reentered || reentries !== prior))
|
|
858
|
+
throw failure[0];
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
finally {
|
|
863
|
+
activeSub = previousSub;
|
|
864
|
+
computing = previousComputing;
|
|
865
|
+
}
|
|
866
|
+
return failure;
|
|
867
|
+
}
|
|
868
|
+
function subscribeWith(listeners, fn, current) {
|
|
869
|
+
const added = !listeners.has(fn);
|
|
870
|
+
listeners.add(fn);
|
|
871
|
+
let called = false;
|
|
872
|
+
try {
|
|
873
|
+
fn(current());
|
|
874
|
+
called = true;
|
|
875
|
+
}
|
|
876
|
+
finally {
|
|
877
|
+
if (!called && added)
|
|
878
|
+
listeners.delete(fn);
|
|
879
|
+
}
|
|
880
|
+
return called;
|
|
881
|
+
}
|
|
114
882
|
/**
|
|
115
883
|
* Creates a signal: a reactive function holding a value and supporting subscriptions.
|
|
116
884
|
*
|
|
@@ -119,112 +887,254 @@ export function batch(fn) {
|
|
|
119
887
|
*/
|
|
120
888
|
/** @codexApi {"parent":"pithy.feature.signals","name":"signal","stability":"stable","signature":"<T>(initial: T) => Signal<T>"} */
|
|
121
889
|
export function signal(initial) {
|
|
122
|
-
|
|
123
|
-
|
|
890
|
+
const node = {
|
|
891
|
+
value: initial,
|
|
892
|
+
subs: undefined,
|
|
893
|
+
subsTail: undefined,
|
|
894
|
+
flags: 0,
|
|
895
|
+
};
|
|
896
|
+
let active = 0;
|
|
124
897
|
const listeners = new Set();
|
|
125
|
-
const read = () =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// we'd delete the wrong effect from subscribers.
|
|
132
|
-
const effectToRemove = currentEffect;
|
|
133
|
-
onCleanup(() => {
|
|
134
|
-
subscribers.delete(effectToRemove);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
// Warn if a signal is read outside of reactive context (in dev mode)
|
|
139
|
-
if (!isTrackingContext &&
|
|
140
|
-
suppressionDepth === 0 &&
|
|
141
|
-
import.meta.env?.DEV &&
|
|
142
|
-
!import.meta.env?.TEST) {
|
|
143
|
-
const error = new Error();
|
|
144
|
-
const stack = error.stack ?? '';
|
|
145
|
-
console.warn(`[PithyJS] ❗ Signal (value: ${value}) was read outside of a reactive context.\n` +
|
|
146
|
-
` • This might lead to missed updates in the UI.\n\n` +
|
|
147
|
-
`🔎 Where it happened:\n${stack.split('\n').slice(2).join('\n')}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return value;
|
|
151
|
-
});
|
|
898
|
+
const read = () => {
|
|
899
|
+
const sub = activeSub;
|
|
900
|
+
if (sub !== null && !(sub.flags & DISPOSED))
|
|
901
|
+
link(node, sub);
|
|
902
|
+
return node.value;
|
|
903
|
+
};
|
|
152
904
|
read.set = (next) => {
|
|
153
|
-
const newValue = suppressWarnings(() => typeof next === 'function' ? next(value) : next);
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
905
|
+
const newValue = suppressWarnings(() => typeof next === 'function' ? next(node.value) : next);
|
|
906
|
+
if (DEV && computing)
|
|
907
|
+
warnImpure('wrote a signal');
|
|
908
|
+
if (node.value === newValue)
|
|
909
|
+
return;
|
|
910
|
+
node.value = newValue;
|
|
911
|
+
let failure;
|
|
912
|
+
const start = queuedLength;
|
|
913
|
+
const outerSegment = segment;
|
|
914
|
+
const outer = recordedError;
|
|
915
|
+
const outerBody = inBatchBody;
|
|
916
|
+
const depth = notifyDepth++;
|
|
917
|
+
const entered = active++ > 0;
|
|
918
|
+
if (entered)
|
|
919
|
+
reentries++;
|
|
920
|
+
if (batchDepth === 0)
|
|
921
|
+
segment = start;
|
|
922
|
+
recordedError = undefined;
|
|
923
|
+
inBatchBody = false;
|
|
924
|
+
errorScopes++;
|
|
925
|
+
try {
|
|
926
|
+
if (node.subs !== undefined)
|
|
927
|
+
propagate(node.subs);
|
|
928
|
+
if (batchDepth === 0)
|
|
929
|
+
failure = flushFrom(start, depth, entered);
|
|
930
|
+
if (listeners.size > 0) {
|
|
931
|
+
failure = callListeners(listeners, node.value, depth, entered, failure);
|
|
932
|
+
}
|
|
933
|
+
failure ?? (failure = recordedError);
|
|
934
|
+
}
|
|
935
|
+
finally {
|
|
936
|
+
notifyDepth = depth;
|
|
937
|
+
active--;
|
|
938
|
+
segment = outerSegment;
|
|
939
|
+
errorScopes--;
|
|
940
|
+
recordedError = outer;
|
|
941
|
+
inBatchBody = outerBody;
|
|
167
942
|
}
|
|
943
|
+
if (failure)
|
|
944
|
+
throw failure[0];
|
|
168
945
|
};
|
|
169
946
|
read.subscribe = (fn) => {
|
|
170
|
-
listeners
|
|
171
|
-
fn(value); // Immediately call with current value
|
|
947
|
+
subscribeWith(listeners, fn, () => node.value);
|
|
172
948
|
return () => listeners.delete(fn);
|
|
173
949
|
};
|
|
174
950
|
read[SIGNAL_SYMBOL] =
|
|
175
951
|
true;
|
|
952
|
+
if (typeof __PITHY_TEST__ !== 'undefined' && __PITHY_TEST__)
|
|
953
|
+
nodesOf.set(read, node);
|
|
176
954
|
return read;
|
|
177
955
|
}
|
|
178
956
|
/**
|
|
179
|
-
* Creates a derived signal (computed value)
|
|
180
|
-
*
|
|
957
|
+
* Creates a derived signal (computed value). It computes once when created,
|
|
958
|
+
* then recomputes when it is read after one of its sources changed.
|
|
181
959
|
*
|
|
182
|
-
* @param compute - A function returning the computed value.
|
|
183
|
-
* @returns A
|
|
960
|
+
* @param compute - A function returning the computed value. It must not write signals or create effects.
|
|
961
|
+
* @returns A read-only signal returning the latest computed value.
|
|
184
962
|
*/
|
|
185
|
-
/** @codexApi {"parent":"pithy.feature.signals","name":"computed","stability":"stable","signature":"<T>(compute: () => T) =>
|
|
963
|
+
/** @codexApi {"parent":"pithy.feature.signals","name":"computed","stability":"stable","signature":"<T>(compute: () => T) => ReadableSignal<T>"} */
|
|
186
964
|
export function computed(compute) {
|
|
187
|
-
|
|
188
|
-
|
|
965
|
+
const node = {
|
|
966
|
+
value: undefined,
|
|
967
|
+
getter: compute,
|
|
968
|
+
subs: undefined,
|
|
969
|
+
subsTail: undefined,
|
|
970
|
+
deps: undefined,
|
|
971
|
+
depsTail: undefined,
|
|
972
|
+
flags: MUTABLE,
|
|
973
|
+
version: 0,
|
|
974
|
+
marked: 0,
|
|
975
|
+
watcher: null,
|
|
976
|
+
};
|
|
977
|
+
const listeners = new Set();
|
|
978
|
+
cleanupMap.set(node, null);
|
|
979
|
+
const dispose = () => {
|
|
980
|
+
if (node.flags & DISPOSED)
|
|
981
|
+
return;
|
|
982
|
+
node.flags |= DISPOSED;
|
|
983
|
+
const watcher = node.watcher;
|
|
984
|
+
node.watcher = null;
|
|
985
|
+
try {
|
|
986
|
+
watcher?.();
|
|
987
|
+
const stack = cleanupMap.get(node);
|
|
988
|
+
if (stack)
|
|
989
|
+
drain(stack);
|
|
990
|
+
}
|
|
991
|
+
finally {
|
|
992
|
+
cleanupMap.delete(node);
|
|
993
|
+
if (!(node.flags & RUNNING))
|
|
994
|
+
unlinkAll(node);
|
|
995
|
+
node.getter = null;
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
const owner = currentOwner;
|
|
999
|
+
if (owner)
|
|
1000
|
+
registerCleanup(owner, dispose);
|
|
1001
|
+
const previousSub = activeSub;
|
|
1002
|
+
const previousOwner = currentOwner;
|
|
1003
|
+
const previousTracking = isTrackingContext;
|
|
1004
|
+
const previousComputing = computing;
|
|
1005
|
+
node.flags |= RUNNING;
|
|
1006
|
+
node.version = ++version;
|
|
1007
|
+
activeSub = node;
|
|
1008
|
+
currentOwner = node;
|
|
1009
|
+
isTrackingContext = true;
|
|
1010
|
+
computing = node;
|
|
1011
|
+
let recorded;
|
|
1012
|
+
let failed;
|
|
1013
|
+
const owns = errorScopes === 0;
|
|
1014
|
+
const outer = recordedError;
|
|
1015
|
+
if (owns) {
|
|
1016
|
+
recordedError = undefined;
|
|
1017
|
+
errorScopes++;
|
|
1018
|
+
}
|
|
189
1019
|
try {
|
|
190
|
-
|
|
1020
|
+
node.value = compute();
|
|
1021
|
+
}
|
|
1022
|
+
catch (error) {
|
|
1023
|
+
failed = [error];
|
|
191
1024
|
}
|
|
192
1025
|
finally {
|
|
193
|
-
|
|
1026
|
+
if (owns) {
|
|
1027
|
+
errorScopes--;
|
|
1028
|
+
recorded = recordedError;
|
|
1029
|
+
recordedError = outer;
|
|
1030
|
+
}
|
|
1031
|
+
activeSub = previousSub;
|
|
1032
|
+
currentOwner = previousOwner;
|
|
1033
|
+
isTrackingContext = previousTracking;
|
|
1034
|
+
computing = previousComputing;
|
|
1035
|
+
node.flags &= ~RUNNING;
|
|
194
1036
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
});
|
|
200
|
-
if (owner) {
|
|
201
|
-
cleanupMap.get(owner)?.push(dispose);
|
|
1037
|
+
failed ?? (failed = recorded);
|
|
1038
|
+
if (failed) {
|
|
1039
|
+
dispose();
|
|
1040
|
+
throw failed[0];
|
|
202
1041
|
}
|
|
203
|
-
|
|
1042
|
+
if (node.flags & DISPOSED)
|
|
1043
|
+
unlinkAll(node);
|
|
1044
|
+
const read = () => readComputed(node);
|
|
1045
|
+
read.subscribe = (fn) => {
|
|
1046
|
+
let delivered = untrack(read);
|
|
1047
|
+
if (!subscribeWith(listeners, fn, () => delivered))
|
|
1048
|
+
return () => { };
|
|
1049
|
+
if (!node.watcher && !(node.flags & DISPOSED)) {
|
|
1050
|
+
const outerOwner = currentOwner;
|
|
1051
|
+
const outerComputing = computing;
|
|
1052
|
+
currentOwner = null;
|
|
1053
|
+
computing = null;
|
|
1054
|
+
try {
|
|
1055
|
+
node.watcher = effect(() => {
|
|
1056
|
+
const value = read();
|
|
1057
|
+
if (value === delivered)
|
|
1058
|
+
return;
|
|
1059
|
+
delivered = value;
|
|
1060
|
+
const failure = callListeners(listeners, value, 0, false, undefined);
|
|
1061
|
+
if (failure)
|
|
1062
|
+
throw failure[0];
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
finally {
|
|
1066
|
+
currentOwner = outerOwner;
|
|
1067
|
+
computing = outerComputing;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return () => {
|
|
1071
|
+
listeners.delete(fn);
|
|
1072
|
+
if (listeners.size === 0 && node.watcher) {
|
|
1073
|
+
const stop = node.watcher;
|
|
1074
|
+
node.watcher = null;
|
|
1075
|
+
stop();
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
};
|
|
1079
|
+
read[SIGNAL_SYMBOL] =
|
|
1080
|
+
true;
|
|
1081
|
+
if (typeof __PITHY_TEST__ !== 'undefined' && __PITHY_TEST__)
|
|
1082
|
+
nodesOf.set(read, node);
|
|
1083
|
+
return read;
|
|
204
1084
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
function runEffect(wrapped, fn) {
|
|
213
|
-
isTrackingContext = true;
|
|
214
|
-
if (isBatching) {
|
|
215
|
-
// Defer running effect until after batch completes
|
|
216
|
-
pendingEffects.add(() => runEffect(wrapped, fn));
|
|
217
|
-
return;
|
|
1085
|
+
function subscribersOfNode(node) {
|
|
1086
|
+
const result = [];
|
|
1087
|
+
let current = node.subs;
|
|
1088
|
+
while (current !== undefined) {
|
|
1089
|
+
if (!result.includes(current.sub))
|
|
1090
|
+
result.push(current.sub);
|
|
1091
|
+
current = current.nextSub;
|
|
218
1092
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
1093
|
+
return result;
|
|
1094
|
+
}
|
|
1095
|
+
function dependenciesOfNode(node) {
|
|
1096
|
+
const result = [];
|
|
1097
|
+
let current = node.deps;
|
|
1098
|
+
while (current !== undefined) {
|
|
1099
|
+
if (!result.includes(current.dep))
|
|
1100
|
+
result.push(current.dep);
|
|
1101
|
+
current = current.nextDep;
|
|
228
1102
|
}
|
|
1103
|
+
return result;
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof __PITHY_TEST__ !== 'undefined' && __PITHY_TEST__) {
|
|
1106
|
+
installIntrospection({
|
|
1107
|
+
context: () => {
|
|
1108
|
+
let pending = 0;
|
|
1109
|
+
for (let index = 0; index < queuedLength; index++) {
|
|
1110
|
+
const node = queue[index];
|
|
1111
|
+
if (node !== undefined && !(node.flags & DISPOSED))
|
|
1112
|
+
pending++;
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
effect: activeSub,
|
|
1116
|
+
owner: currentOwner,
|
|
1117
|
+
tracking: isTrackingContext,
|
|
1118
|
+
suppression: suppressionDepth,
|
|
1119
|
+
batching: batchDepth > 0,
|
|
1120
|
+
pending,
|
|
1121
|
+
notifying: notifyDepth,
|
|
1122
|
+
flushing: flushLevel,
|
|
1123
|
+
};
|
|
1124
|
+
},
|
|
1125
|
+
cleanupStack: owner => cleanupMap.get(owner),
|
|
1126
|
+
subscribers: subscribersOfNode,
|
|
1127
|
+
links: node => {
|
|
1128
|
+
let count = 0;
|
|
1129
|
+
let current = node.subs;
|
|
1130
|
+
while (current !== undefined) {
|
|
1131
|
+
count++;
|
|
1132
|
+
current = current.nextSub;
|
|
1133
|
+
}
|
|
1134
|
+
return count;
|
|
1135
|
+
},
|
|
1136
|
+
dependencies: dependenciesOfNode,
|
|
1137
|
+
disposed: node => (node.flags & DISPOSED) !== 0,
|
|
1138
|
+
});
|
|
229
1139
|
}
|
|
230
1140
|
//# sourceMappingURL=signal.js.map
|