@solidjs/signals 2.0.0-beta.24 → 2.0.0-beta.26

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.
@@ -1,4 +1,4 @@
1
- import { STATUS_UNINITIALIZED, STATUS_ERROR, STATUS_PENDING, REACTIVE_DIRTY, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, CONFIG_AUTO_DISPOSE } from "./constants.js";
1
+ import { STATUS_UNINITIALIZED, STATUS_ERROR, STATUS_PENDING, CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE, REACTIVE_DIRTY, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING } from "./constants.js";
2
2
 
3
3
  import { untrack, context, setSignal } from "./core.js";
4
4
 
@@ -6,7 +6,7 @@ import "./invariants.js";
6
6
 
7
7
  import { NotReadyError, StatusError } from "./error.js";
8
8
 
9
- import { trimStaleDeps, unobserved } from "./graph.js";
9
+ import { unobserved, trimStaleDeps } from "./graph.js";
10
10
 
11
11
  import { enqueueSub } from "./heap.js";
12
12
 
@@ -14,7 +14,7 @@ import { resolveTransition, hasActiveOverride, assignOrMergeLane, resolveLane }
14
14
 
15
15
  import { cleanup } from "./owner.js";
16
16
 
17
- import { globalQueue, GlobalQueue, clock, schedule, flush, queuePendingNode, insertSubs } from "./scheduler.js";
17
+ import { globalQueue, GlobalQueue, clock, currentTransition, schedule, flush, queuePendingNode, insertSubs } from "./scheduler.js";
18
18
 
19
19
  // The lazily-created Set is the ONE container for pending sources. Its
20
20
  // predecessor — a singular slot promoted to a Set on the second source —
@@ -38,58 +38,94 @@ function clearPendingSources(e) {
38
38
  e.oe = undefined;
39
39
  }
40
40
 
41
- function setPendingError(e, n, r) {
41
+ function setPendingError(e, n, t) {
42
42
  if (!n) {
43
43
  e._ = null;
44
44
  return;
45
45
  }
46
- if (r instanceof NotReadyError && r.source === n) {
47
- e._ = r;
46
+ if (t instanceof NotReadyError && t.source === n) {
47
+ e._ = t;
48
48
  return;
49
49
  }
50
- const t = e._;
51
- if (!(t instanceof NotReadyError) || t.source !== n) {
50
+ const r = e._;
51
+ if (!(r instanceof NotReadyError) || r.source !== n) {
52
52
  e._ = new NotReadyError(n);
53
53
  }
54
54
  }
55
55
 
56
56
  function forEachDependent(e, n) {
57
- for (let r = e.o; r !== null; r = r.ue) n(r.le, r);
57
+ for (let t = e.o; t !== null; t = t.ue) n(t.le, t);
58
58
  // `?? null`: affects() marks route plain signals (no `_child` slot) through here.
59
- for (let r = e.u ?? null; r !== null; r = r.fe) {
60
- for (let e = r.o; e !== null; e = e.ue) n(e.le, e);
59
+ for (let t = e.u ?? null; t !== null; t = t.fe) {
60
+ for (let e = t.o; e !== null; e = e.ue) n(e.le, e);
61
61
  }
62
62
  }
63
63
 
64
64
  // Queue a node to re-run on the next flush (used both when a pending source
65
65
  // settles and when an `isPending` observer must re-evaluate after a real error):
66
66
  // shared scheduling helper in heap.ts (tracked effects bypass the heap).
67
+ // Settle-time counterpart of unlinkSubs' last-one-out check. A lazy node that
68
+ // loses its last subscriber while STATUS_PENDING is exempt from autodispose
69
+ // (the in-flight work is an observer), so whatever CLEARS that pending state
70
+ // must run the release — otherwise the node stays linked and recomputes
71
+ // forever with zero subscribers (#2934). The node's own promise/iterator
72
+ // callbacks handle their own release (settleAutodispose in handleAsync); this
73
+ // covers derivatively-pending dependents, which have no callbacks of their own.
74
+ function releaseIfSettledUnobserved(e) {
75
+ e.ae && e.T & CONFIG_AUTO_DISPOSE && !e.o && !(e.se & REACTIVE_ZOMBIE) && !(e.S & STATUS_PENDING) && unobserved(e);
76
+ }
77
+
78
+ // Error-path sweep: notifyStatus(STATUS_ERROR) clears dependents' pending
79
+ // sources through its own recursion (no per-node settle callback), so after
80
+ // the propagation completes, walk the same graph for stranded lazy nodes.
81
+ // Collect-then-release so unobserved() never unlinks under the walk.
82
+ function releaseSettledDependents(e) {
83
+ let n;
84
+ const t = new Set;
85
+ const visit = e => {
86
+ if (t.has(e)) return;
87
+ t.add(e);
88
+ if (!e.o && e.T & CONFIG_AUTO_DISPOSE) (n ??= []).push(e);
89
+ forEachDependent(e, visit);
90
+ };
91
+ forEachDependent(e, visit);
92
+ if (n) for (const e of n) releaseIfSettledUnobserved(e);
93
+ }
94
+
67
95
  function settlePendingSource(e) {
68
96
  let n = false;
97
+ let t;
69
98
  const r = new Set;
70
99
  // Companion updates no-op without the verdict layer (null hook).
71
- const t = GlobalQueue.ae;
72
- const settle = o => {
73
- if (r.has(o) || !removePendingSource(o, e)) return;
74
- r.add(o);
75
- o.ce = clock;
76
- const u = o.oe?.values().next().value;
77
- if (u) {
78
- setPendingError(o, u);
79
- t !== null && t(o);
100
+ const o = GlobalQueue.ce;
101
+ const settle = u => {
102
+ if (r.has(u) || !removePendingSource(u, e)) return;
103
+ r.add(u);
104
+ u.Se = clock;
105
+ const l = u.oe?.values().next().value;
106
+ if (l) {
107
+ setPendingError(u, l);
108
+ o !== null && o(u);
80
109
  } else {
81
- o.S &= ~STATUS_PENDING;
82
- setPendingError(o);
83
- t !== null && t(o);
84
- if (o.Se) {
85
- enqueueSub(o);
110
+ u.S &= ~STATUS_PENDING;
111
+ setPendingError(u);
112
+ o !== null && o(u);
113
+ if (u.de) {
114
+ enqueueSub(u);
86
115
  n = true;
87
116
  }
88
- o.Se = false;
117
+ u.de = false;
118
+ // Fully settled with nobody watching: release candidate (#2934). Checked
119
+ // again at release time — deferred so unobserved() can't unlink subs
120
+ // lists this walk is still iterating.
121
+ if (!u.o && u.T & CONFIG_AUTO_DISPOSE) (t ??= []).push(u);
89
122
  }
90
- forEachDependent(o, settle);
123
+ forEachDependent(u, settle);
91
124
  };
92
125
  forEachDependent(e, settle);
126
+ // Release before the flush schedule below: unobserved() pulls the node back
127
+ // out of the heap, so the enqueueSub above never recomputes a released node.
128
+ if (t) for (const e of t) releaseIfSettledUnobserved(e);
93
129
  if (n) schedule();
94
130
  }
95
131
 
@@ -98,44 +134,69 @@ function isThenable(e) {
98
134
  return e != null && typeof e === "object" && typeof e.then === "function";
99
135
  }
100
136
 
101
- function handleAsync(e, n, r) {
102
- let t = false;
137
+ function handleAsync(e, n, t) {
138
+ let r = false;
103
139
  let o = false;
104
140
  if (typeof n === "object" && n !== null) {
105
141
  untrack(() => {
106
- t = n[Symbol.asyncIterator];
107
- o = !t && isThenable(n);
142
+ r = n[Symbol.asyncIterator];
143
+ o = !r && isThenable(n);
108
144
  });
109
145
  }
110
- if (!o && !t) {
146
+ if (!o && !r) {
111
147
  e.Te = null;
112
148
  return n;
113
149
  }
114
150
  e.Te = n;
115
151
  let u;
116
- const handleError = r => {
152
+ // Settle-time transition re-entry. The loading rail is invisible to
153
+ // transactions (#2933): a boundary-caught first load never registers as an
154
+ // async reporter, so its settle — the boundary's fallback -> content
155
+ // reveal — must flow ambiently. The node can still carry a `_transition`
156
+ // stamp (pending-node bookkeeping rides through the stamping sites), and
157
+ // blindly re-entering that stamped, still-incomplete transaction stashed
158
+ // the reveal with it — a deadlock when the transaction's completion
159
+ // depended on the reveal (#2937). An ESCAPED first load did register and
160
+ // keeps transition scheduling; initialized (value-holding) pending settles
161
+ // are the transaction's reveal machinery and always re-enter.
162
+ const settleTransition = () => {
163
+ const n = resolveTransition(e);
164
+ if (n && e.S & STATUS_UNINITIALIZED && !currentTransition(n).Ee.has(e)) {
165
+ // Drop the stale stamp too: the plain settle write (setSignal) and the
166
+ // stash-path restamp both re-enter the transaction through it.
167
+ e.Ie = null;
168
+ return;
169
+ }
170
+ globalQueue.initTransition(n);
171
+ };
172
+ const handleError = t => {
117
173
  if (e.Te !== n) return;
118
- globalQueue.initTransition(resolveTransition(e));
174
+ settleTransition();
119
175
  // NotReadyError from rejected promises should be treated as pending, not error
120
- notifyStatus(e, r instanceof NotReadyError ? STATUS_PENDING : STATUS_ERROR, r);
121
- e.ce = clock;
176
+ const r = t instanceof NotReadyError;
177
+ notifyStatus(e, r ? STATUS_PENDING : STATUS_ERROR, t);
178
+ e.Se = clock;
179
+ // A real error settles derivatively-pending dependents (notifyStatus
180
+ // cleared their pending sources), so stranded lazy ones release here —
181
+ // the error twin of settlePendingSource's release (#2934).
182
+ if (!r) releaseSettledDependents(e);
122
183
  };
123
- const asyncWrite = (t, o) => {
184
+ const asyncWrite = (r, o) => {
124
185
  if (e.Te !== n) return;
125
186
  // If the node was dirtied by a newer write (optimistic override or regular),
126
187
  // skip this stale async result — the upcoming flush will recompute the node
127
188
  // with the new value, creating a fresh Promise that supersedes this one.
128
189
  if (e.se & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
129
- globalQueue.initTransition(resolveTransition(e));
190
+ settleTransition();
130
191
  const u = !!(e.S & STATUS_UNINITIALIZED);
131
192
  trimStaleDeps(e);
132
193
  clearStatus(e);
133
194
  const l = resolveLane(e);
134
- if (l) l.de.delete(e);
135
- if (r) {
136
- r(t);
195
+ if (l) l.Ne.delete(e);
196
+ if (t) {
197
+ t(r);
137
198
  if (u) clearStatus(e, true);
138
- } else if (e.Ee !== undefined) {
199
+ } else if (e.Ae !== undefined) {
139
200
  // Optimistic node — resting OR covered by an active override — holds
140
201
  // through the shared pending-node path, exactly like a plain async memo,
141
202
  // so the commit clears STATUS_UNINITIALIZED (#2806) and elevation to
@@ -145,8 +206,8 @@ function handleAsync(e, n, r) {
145
206
  // (A17 — every reader sees the override); the revert reveals whatever
146
207
  // has committed by then, so corrections reveal atomically with their
147
208
  // transition rather than escaping it.
148
- if (e.Ne === NOT_PENDING) queuePendingNode(e);
149
- e.Ne = t;
209
+ if (e._e === NOT_PENDING) queuePendingNode(e);
210
+ e._e = r;
150
211
  // The hold is a companion-visible write like any other (A13/A19): the
151
212
  // clearStatus() above computed its verdict before the hold existed, so
152
213
  // isPending must re-derive (the value is not final until commit — V1)
@@ -154,22 +215,22 @@ function handleAsync(e, n, r) {
154
215
  // only notified when the hold is visible to them: under an active
155
216
  // override every reader sees the override (A17), so waking subs would
156
217
  // re-show an unchanged view — the revert is the notification point.
157
- GlobalQueue.Ie !== null && GlobalQueue.Ie(e, t);
218
+ GlobalQueue.De !== null && GlobalQueue.De(e, r);
158
219
  if (!hasActiveOverride(e)) insertSubs(e);
159
- e.ce = clock;
220
+ e.Se = clock;
160
221
  } else if (l) {
161
222
  // Route through lane's effect queue for independent flushing
162
- const n = e.Ae;
163
- const r = e.Pe;
223
+ const n = e.Pe;
224
+ const t = e.Ue;
164
225
  const o = e.Re;
165
226
  try {
166
- if (!n && u || !o || !o(t, r)) {
167
- e.Pe = t;
168
- e.ce = clock;
227
+ if (!n && u || !o || !o(r, t)) {
228
+ e.Ue = r;
229
+ e.Se = clock;
169
230
  // The latest() shadow write gives latest() effects independent lanes; the
170
231
  // _pendingSignal update is a no-op repeat of the clearStatus() call above
171
232
  // (computePendingState doesn't read _value).
172
- GlobalQueue.Ie !== null && GlobalQueue.Ie(e, t);
233
+ GlobalQueue.De !== null && GlobalQueue.De(e, r);
173
234
  insertSubs(e, true);
174
235
  }
175
236
  } catch (n) {
@@ -181,7 +242,7 @@ function handleAsync(e, n, r) {
181
242
  }
182
243
  } else {
183
244
  try {
184
- setSignal(e, () => t);
245
+ setSignal(e, () => r);
185
246
  } catch (n) {
186
247
  // Same containment as above: setSignal's comparator throw is the only
187
248
  // pre-commit failure here, and there is no user callsite to throw to.
@@ -198,17 +259,21 @@ function handleAsync(e, n, r) {
198
259
  // work (a lazy async memo would otherwise tear down and re-execute — one
199
260
  // fetch per suspended re-read). Settling is that observer's release, so
200
261
  // it runs the same last-one-out check the other release sites run.
262
+ // Returns whether the node released, so the iterator branch can stop
263
+ // pulling values instead of pumping an unobserved stream forever (#2935).
201
264
  const settleAutodispose = () => {
202
265
  if (e.T & CONFIG_AUTO_DISPOSE && !e.o && !(e.S & STATUS_PENDING)) {
203
266
  unobserved(e);
267
+ return true;
204
268
  }
269
+ return false;
205
270
  };
206
271
  if (o) {
207
- let r = false, t = false, o, l = true;
272
+ let t = false, r = false, o, l = true;
208
273
  n.then(e => {
209
274
  if (l) {
210
275
  u = e;
211
- r = true;
276
+ t = true;
212
277
  } else {
213
278
  asyncWrite(e);
214
279
  settleAutodispose();
@@ -216,87 +281,95 @@ function handleAsync(e, n, r) {
216
281
  }, e => {
217
282
  if (l) {
218
283
  o = e;
219
- t = true;
284
+ r = true;
220
285
  } else {
221
286
  handleError(e);
222
287
  settleAutodispose();
223
288
  }
224
289
  });
225
290
  l = false;
226
- if (t) {
291
+ if (r) {
227
292
  // Settle through the same status path an async rejection uses, then
228
293
  // unwind the in-progress synchronous read so the errored node isn't
229
294
  // momentarily read as `undefined`.
230
295
  handleError(o);
231
296
  throw o;
232
- } else if (!r) {
297
+ } else if (!t) {
233
298
  globalQueue.initTransition(resolveTransition(e));
234
299
  throw new NotReadyError(context);
235
300
  }
236
301
  }
237
- if (t) {
238
- const r = n[Symbol.asyncIterator]();
239
- let t = false;
302
+ if (r) {
303
+ const t = n[Symbol.asyncIterator]();
304
+ let r = false;
240
305
  let o = false;
241
306
  let l = true;
242
307
  cleanup(() => {
243
308
  if (o) return;
244
309
  o = true;
245
310
  try {
246
- const e = r.return?.();
311
+ const e = t.return?.();
247
312
  if (isThenable(e)) e.then(undefined, () => {});
248
313
  } catch {}
249
314
  });
315
+ // Release check before each next pull: an unobserved lazy node must tear
316
+ // down (its cleanup above closes the iterator) instead of pumping the
317
+ // stream forever with zero subscribers (#2935).
318
+ const iterateOrRelease = () => {
319
+ if (!settleAutodispose()) iterate();
320
+ };
250
321
  const iterate = () => {
251
- let i, s, f = false, a = false, c = true;
252
- r.next().then(r => {
322
+ let s, i, f = false, a = false, c = true;
323
+ t.next().then(t => {
253
324
  if (c) {
254
- i = r;
325
+ s = t;
255
326
  f = true;
256
- if (r.done) o = true;
327
+ if (t.done) o = true;
257
328
  } else if (e.Te !== n) {
258
329
  return;
259
- } else if (!r.done) {
260
- t = true;
261
- asyncWrite(r.value, iterate);
330
+ } else if (!t.done) {
331
+ r = true;
332
+ asyncWrite(t.value, iterateOrRelease);
262
333
  } else {
263
334
  o = true;
264
- if (t) {
335
+ if (r) {
265
336
  schedule();
266
337
  flush();
267
338
  } else {
268
339
  // Empty completion settles like the immediately-done sync path.
269
340
  asyncWrite(undefined);
270
341
  }
342
+ settleAutodispose();
271
343
  }
272
- }, r => {
344
+ }, t => {
273
345
  if (c) {
274
- s = r;
346
+ i = t;
275
347
  a = true;
276
348
  } else if (e.Te === n) {
277
349
  o = true;
278
- handleError(r);
350
+ handleError(t);
351
+ settleAutodispose();
279
352
  }
280
353
  });
281
354
  c = false;
282
355
  if (a) {
283
356
  // Match the promise branch, but only rethrow during the initial read.
284
357
  o = true;
285
- handleError(s);
286
- if (l) throw s;
358
+ handleError(i);
359
+ if (l) throw i;
287
360
  return true;
288
361
  }
289
- if (f && !i.done) {
290
- u = i.value;
291
- t = true;
362
+ if (f && !s.done) {
363
+ u = s.value;
364
+ r = true;
292
365
  return iterate();
293
366
  }
294
- return f && i.done;
367
+ return f && s.done;
295
368
  };
296
- const i = iterate();
369
+ const s = iterate();
297
370
  // Later iterate() calls run from asyncWrite, where rethrowing would be unhandled.
298
371
  l = false;
299
- if (!t && !i) {
372
+ if (!r && !s) {
300
373
  globalQueue.initTransition(resolveTransition(e));
301
374
  throw new NotReadyError(context);
302
375
  }
@@ -306,75 +379,75 @@ function handleAsync(e, n, r) {
306
379
 
307
380
  function clearStatus(e, n = false) {
308
381
  if (e.oe) clearPendingSources(e);
309
- if (e.Se) e.Se = false;
382
+ if (e.de) e.de = false;
310
383
  // The pending window is over; its quiet classification dies with it.
311
384
  // (Unconditional: _reask is baked into the node literals, so this is a
312
385
  // plain store to an existing slot — no shape change.)
313
- e._e = false;
386
+ e.be = false;
314
387
  e.S = n ? 0 : e.S & STATUS_UNINITIALIZED;
315
388
  if (e._) setPendingError(e);
316
389
  // Update pending signal for isPending() reactivity (companions only exist
317
390
  // once the verdict layer created them, which installs the hooks).
318
- if (e.be || e.ge) GlobalQueue.ae(e);
319
- if (e.u && GlobalQueue.he !== null) GlobalQueue.he(e);
391
+ if (e.he || e.Ge) GlobalQueue.ce(e);
392
+ if (e.u && GlobalQueue.Oe !== null) GlobalQueue.Oe(e);
320
393
  if (e.i) e.i();
321
394
  }
322
395
 
323
- function notifyStatus(e, n, r, t, o) {
396
+ function notifyStatus(e, n, t, r, o) {
324
397
  // Wrap regular errors to track source node
325
- if (n === STATUS_ERROR && !(r instanceof StatusError) && !(r instanceof NotReadyError)) r = new StatusError(e, r);
326
- const u = n === STATUS_PENDING && r instanceof NotReadyError ? r.source : undefined;
398
+ if (n === STATUS_ERROR && !(t instanceof StatusError) && !(t instanceof NotReadyError)) t = new StatusError(e, t);
399
+ const u = n === STATUS_PENDING && t instanceof NotReadyError ? t.source : undefined;
327
400
  const l = u === e;
328
- const i = n === STATUS_PENDING && e.Ee !== undefined && !l;
329
- const s = i && hasActiveOverride(e);
330
- if (!t) {
401
+ const s = n === STATUS_PENDING && e.Ae !== undefined && !l;
402
+ const i = s && hasActiveOverride(e);
403
+ if (!r) {
331
404
  if (n === STATUS_PENDING && u) {
332
405
  addPendingSource(e, u);
333
406
  e.S = STATUS_PENDING | e.S & STATUS_UNINITIALIZED;
334
407
  // Preserve the current source on this propagation so render-effect notification
335
408
  // can register every distinct pending source with the transition.
336
- setPendingError(e, u, r);
409
+ setPendingError(e, u, t);
337
410
  } else {
338
411
  clearPendingSources(e);
339
412
  e.S = n | (n !== STATUS_ERROR ? e.S & STATUS_UNINITIALIZED : 0);
340
- e._ = r;
413
+ e._ = t;
341
414
  }
342
- GlobalQueue.ae !== null && GlobalQueue.ae(e);
343
- if (e.u && GlobalQueue.he !== null) GlobalQueue.he(e);
415
+ GlobalQueue.ce !== null && GlobalQueue.ce(e);
416
+ if (e.u && GlobalQueue.Oe !== null) GlobalQueue.Oe(e);
344
417
  }
345
- if (o && !t) {
418
+ if (o && !r) {
346
419
  assignOrMergeLane(e, o);
347
420
  }
348
- const f = t || s;
349
- const a = t || i ? undefined : o;
421
+ const f = r || i;
422
+ const a = r || s ? undefined : o;
350
423
  if (e.i) {
351
- if (t && n === STATUS_PENDING) {
424
+ if (r && n === STATUS_PENDING) {
352
425
  return;
353
426
  }
354
427
  if (f) {
355
- e.i(n, r);
428
+ e.i(n, t);
356
429
  } else {
357
430
  e.i();
358
431
  }
359
432
  return;
360
433
  }
361
- forEachDependent(e, (e, t) => {
362
- e.ce = clock;
363
- if (n === STATUS_PENDING && u && !e.oe?.has(u) || n !== STATUS_PENDING && (e._ !== r || e.oe)) {
434
+ forEachDependent(e, (e, r) => {
435
+ e.Se = clock;
436
+ if (n === STATUS_PENDING && u && !e.oe?.has(u) || n !== STATUS_PENDING && (e._ !== t || e.oe)) {
364
437
  // A pending-observer link is the subscription an `isPending` read created.
365
438
  // It exists so the observer re-runs when the source settles, but it must
366
439
  // not carry a real (non-NotReadyError) error — the synchronous `isPending`
367
440
  // read swallows those, and the async path must match. Re-run the observer
368
441
  // so `isPending` re-evaluates (to not-pending) instead of forwarding.
369
- if (t.De && n !== STATUS_PENDING && !(r instanceof NotReadyError)) {
442
+ if (r.ge && n !== STATUS_PENDING && !(t instanceof NotReadyError)) {
370
443
  enqueueSub(e);
371
444
  schedule();
372
445
  return;
373
446
  }
374
- if (!f && !e.Ue) queuePendingNode(e);
375
- notifyStatus(e, n, r, f, a);
447
+ if (!f && !e.Ie) queuePendingNode(e);
448
+ notifyStatus(e, n, t, f, a);
376
449
  }
377
450
  });
378
451
  }
379
452
 
380
- export { addPendingSource, clearStatus, forEachDependent, handleAsync, isThenable, notifyStatus, setPendingError, settlePendingSource };
453
+ export { addPendingSource, clearStatus, forEachDependent, handleAsync, isThenable, notifyStatus, releaseSettledDependents, setPendingError, settlePendingSource };