@solidjs/signals 2.0.0-beta.31 → 2.0.0-beta.33

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, CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE, REACTIVE_DIRTY, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING } from "./constants.js";
1
+ import { STATUS_UNINITIALIZED, STATUS_ERROR, STATUS_PENDING, REACTIVE_DIRTY, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, CONFIG_AUTO_DISPOSE, REACTIVE_ZOMBIE } 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 { unobserved, trimStaleDeps } from "./graph.js";
9
+ import { trimStaleDeps, unobserved } 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, currentTransition, schedule, flush, queuePendingNode, insertSubs } from "./scheduler.js";
17
+ import { globalQueue, GlobalQueue, schedule, flush, queuePendingNode, insertSubs, clock, currentTransition } 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,6 +38,22 @@ function clearPendingSources(e) {
38
38
  e.oe = undefined;
39
39
  }
40
40
 
41
+ /**
42
+ * A loading-window node hit an unready source (sync throw in recompute, or a
43
+ * NotReadyError-rejected flight): register for the source's settle — the
44
+ * settlePendingSource walk runs off `_pendingSources` + `_blocked` alone —
45
+ * with NO read-visible pending status, no downstream propagation, no
46
+ * transition, no lane registration. Commit #0 keeps serving.
47
+ */ function parkLoadingWindow(e, n) {
48
+ e.ue = true;
49
+ if (n.source) addPendingSource(e, n.source);
50
+ // A settled error is the node's answer ("the error stays the answer until
51
+ // this retry can actually run") — the park must not replace it: reads
52
+ // throw `_error` while STATUS_ERROR is set, and overwriting it here leaks
53
+ // a pending-class NotReadyError from a read-invisible park (#2989).
54
+ if (!(e.S & STATUS_ERROR)) setPendingError(e, n.source, n);
55
+ }
56
+
41
57
  function setPendingError(e, n, t) {
42
58
  if (!n) {
43
59
  e._ = null;
@@ -54,10 +70,10 @@ function setPendingError(e, n, t) {
54
70
  }
55
71
 
56
72
  function forEachDependent(e, n) {
57
- for (let t = e.o; t !== null; t = t.ue) n(t.le, t);
73
+ for (let t = e.o; t !== null; t = t.le) n(t.fe, t);
58
74
  // `?? null`: affects() marks route plain signals (no `_child` slot) through here.
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);
75
+ for (let t = e.u ?? null; t !== null; t = t.ae) {
76
+ for (let e = t.o; e !== null; e = e.le) n(e.fe, e);
61
77
  }
62
78
  }
63
79
 
@@ -72,7 +88,7 @@ function forEachDependent(e, n) {
72
88
  // callbacks handle their own release (settleAutodispose in handleAsync); this
73
89
  // covers derivatively-pending dependents, which have no callbacks of their own.
74
90
  function releaseIfSettledUnobserved(e) {
75
- e.ae && e.T & CONFIG_AUTO_DISPOSE && !e.o && !(e.se & REACTIVE_ZOMBIE) && !(e.S & STATUS_PENDING) && unobserved(e);
91
+ e.ce && e.T & CONFIG_AUTO_DISPOSE && !e.o && !(e.se & REACTIVE_ZOMBIE) && !(e.S & STATUS_PENDING) && unobserved(e);
76
92
  }
77
93
 
78
94
  // Error-path sweep: notifyStatus(STATUS_ERROR) clears dependents' pending
@@ -128,24 +144,30 @@ function settlePendingSource(e) {
128
144
  let t;
129
145
  const r = new Set;
130
146
  // Companion updates no-op without the verdict layer (null hook).
131
- const o = GlobalQueue.ce;
147
+ const o = GlobalQueue.Se;
132
148
  const settle = u => {
133
149
  if (r.has(u) || !removePendingSource(u, e)) return;
134
150
  r.add(u);
135
- u.Se = clock;
136
- const l = u.oe?.values().next().value;
137
- if (l) {
138
- setPendingError(u, l);
151
+ u.de = clock;
152
+ const i = u.oe?.values().next().value;
153
+ // STATUS_ERROR + pending sources only coexist via an errored loading
154
+ // window's park (notifyStatus(STATUS_ERROR) clears pending sources
155
+ // otherwise): the settled error stays the answer through the settle —
156
+ // nulling it here would have reads throw `null` until the re-enqueued
157
+ // retry lands, or lose it entirely if that retry parks again (#2989).
158
+ const l = u.S & STATUS_ERROR;
159
+ if (i) {
160
+ if (!l) setPendingError(u, i);
139
161
  o !== null && o(u);
140
162
  } else {
141
163
  u.S &= ~STATUS_PENDING;
142
- setPendingError(u);
164
+ if (!l) setPendingError(u);
143
165
  o !== null && o(u);
144
- if (u.de) {
166
+ if (u.ue) {
145
167
  enqueueSub(u);
146
168
  n = true;
147
169
  }
148
- u.de = false;
170
+ u.ue = false;
149
171
  // Fully settled with nobody watching: release candidate (#2934). Checked
150
172
  // again at release time — deferred so unobserved() can't unlink subs
151
173
  // lists this walk is still iterating.
@@ -176,6 +198,8 @@ function handleAsync(e, n, t) {
176
198
  }
177
199
  if (!o && !r) {
178
200
  e.Te = null;
201
+ // A sync landing is the first real answer for a loadingValue node.
202
+ e.Ee = false;
179
203
  return n;
180
204
  }
181
205
  e.Te = n;
@@ -192,21 +216,31 @@ function handleAsync(e, n, t) {
192
216
  // are the transaction's reveal machinery and always re-enter.
193
217
  const settleTransition = () => {
194
218
  const n = resolveTransition(e);
195
- if (n && e.S & STATUS_UNINITIALIZED && !currentTransition(n).Ee.has(e)) {
219
+ if (n && e.S & STATUS_UNINITIALIZED && !currentTransition(n).Ie.has(e)) {
196
220
  // Drop the stale stamp too: the plain settle write (setSignal) and the
197
221
  // stash-path restamp both re-enter the transaction through it.
198
- e.Ie = null;
222
+ e.Ne = null;
199
223
  return;
200
224
  }
201
225
  globalQueue.initTransition(n);
202
226
  };
203
227
  const handleError = t => {
204
228
  if (e.Te !== n) return;
205
- settleTransition();
206
229
  // NotReadyError from rejected promises should be treated as pending, not error
207
- const r = t instanceof NotReadyError;
230
+ let r = t instanceof NotReadyError;
231
+ if (r && e.Ee) {
232
+ // Loading window: the flight died waiting on an unready source. Keep
233
+ // serving commit #0 — same parking as recompute's catch for sync
234
+ // dependency throws. The dead flight is released so the clock-gated
235
+ // error-retry pull (updateIfNecessary) can also re-ask.
236
+ e.Te = null;
237
+ parkLoadingWindow(e, t);
238
+ e.de = clock;
239
+ return;
240
+ }
241
+ settleTransition();
208
242
  notifyStatus(e, r ? STATUS_PENDING : STATUS_ERROR, t);
209
- e.Se = clock;
243
+ e.de = clock;
210
244
  // A real error settles derivatively-pending dependents (notifyStatus
211
245
  // cleared their pending sources), so stranded lazy ones release here —
212
246
  // the error twin of settlePendingSource's release (#2934).
@@ -222,12 +256,12 @@ function handleAsync(e, n, t) {
222
256
  const u = !!(e.S & STATUS_UNINITIALIZED);
223
257
  trimStaleDeps(e);
224
258
  clearStatus(e);
225
- const l = resolveLane(e);
226
- if (l) l.Ne.delete(e);
259
+ const i = resolveLane(e);
260
+ if (i) i.Ae.delete(e);
227
261
  if (t) {
228
262
  t(r);
229
263
  if (u) clearStatus(e, true);
230
- } else if (e.Ae !== undefined) {
264
+ } else if (e._e !== undefined) {
231
265
  // Optimistic node — resting OR covered by an active override — holds
232
266
  // through the shared pending-node path, exactly like a plain async memo,
233
267
  // so the commit clears STATUS_UNINITIALIZED (#2806) and elevation to
@@ -246,22 +280,22 @@ function handleAsync(e, n, t) {
246
280
  // only notified when the hold is visible to them: under an active
247
281
  // override every reader sees the override (A17), so waking subs would
248
282
  // re-show an unchanged view — the revert is the notification point.
249
- GlobalQueue._e !== null && GlobalQueue._e(e, r);
283
+ GlobalQueue.Pe !== null && GlobalQueue.Pe(e, r);
250
284
  if (!hasActiveOverride(e)) insertSubs(e);
251
- e.Se = clock;
252
- } else if (l) {
285
+ e.de = clock;
286
+ } else if (i) {
253
287
  // Route through lane's effect queue for independent flushing
254
- const n = e.Pe;
288
+ const n = e.Re;
255
289
  const t = e.Ue;
256
290
  const o = e.be;
257
291
  try {
258
292
  if (!n && u || !o || !o(r, t)) {
259
293
  e.Ue = r;
260
- e.Se = clock;
294
+ e.de = clock;
261
295
  // The latest() shadow write gives latest() effects independent lanes; the
262
296
  // _pendingSignal update is a no-op repeat of the clearStatus() call above
263
297
  // (computePendingState doesn't read _value).
264
- GlobalQueue._e !== null && GlobalQueue._e(e, r);
298
+ GlobalQueue.Pe !== null && GlobalQueue.Pe(e, r);
265
299
  insertSubs(e, true);
266
300
  }
267
301
  } catch (n) {
@@ -280,6 +314,13 @@ function handleAsync(e, n, t) {
280
314
  notifyStatus(e, STATUS_ERROR, n);
281
315
  }
282
316
  }
317
+ // First real answer landing: the window closes when the answer becomes
318
+ // OBSERVABLE. A direct commit is observable now; a transition-held write
319
+ // (`_pendingValue` set above or inside setSignal) is not — the verdict's
320
+ // held-value branch is window-gated, and commitPendingNode closes the
321
+ // window when the hold commits, so no one-frame isPending pulse can leak
322
+ // to live observers between the landing and its commit (#2990).
323
+ if (e.De === NOT_PENDING) e.Ee = false;
283
324
  settlePendingSource(e);
284
325
  schedule();
285
326
  flush();
@@ -300,9 +341,9 @@ function handleAsync(e, n, t) {
300
341
  return false;
301
342
  };
302
343
  if (o) {
303
- let t = false, r = false, o, l = true;
344
+ let t = false, r = false, o, i = true;
304
345
  n.then(e => {
305
- if (l) {
346
+ if (i) {
306
347
  u = e;
307
348
  t = true;
308
349
  } else {
@@ -310,7 +351,7 @@ function handleAsync(e, n, t) {
310
351
  settleAutodispose();
311
352
  }
312
353
  }, e => {
313
- if (l) {
354
+ if (i) {
314
355
  o = e;
315
356
  r = true;
316
357
  } else {
@@ -318,7 +359,7 @@ function handleAsync(e, n, t) {
318
359
  settleAutodispose();
319
360
  }
320
361
  });
321
- l = false;
362
+ i = false;
322
363
  if (r) {
323
364
  // Settle through the same status path an async rejection uses, then
324
365
  // unwind the in-progress synchronous read so the errored node isn't
@@ -326,15 +367,23 @@ function handleAsync(e, n, t) {
326
367
  handleError(o);
327
368
  throw o;
328
369
  } else if (!t) {
370
+ // Loading window: serve commit #0 instead of suspending. No transition
371
+ // is opened — first-flight work on a loadingValue node is loading-class
372
+ // (invisible to boundaries and transitions); the flight itself is
373
+ // already registered in _inFlight and lands through asyncWrite.
374
+ if (e.Ee) return e.Ue;
329
375
  globalQueue.initTransition(resolveTransition(e));
330
376
  throw new NotReadyError(context);
377
+ } else {
378
+ // Synchronously-resolved promise: the first real answer landed.
379
+ e.Ee = false;
331
380
  }
332
381
  }
333
382
  if (r) {
334
383
  const t = n[Symbol.asyncIterator]();
335
384
  let r = false;
336
385
  let o = false;
337
- let l = true;
386
+ let i = true;
338
387
  cleanup(() => {
339
388
  if (o) return;
340
389
  o = true;
@@ -350,10 +399,25 @@ function handleAsync(e, n, t) {
350
399
  if (!settleAutodispose()) iterate();
351
400
  };
352
401
  const iterate = () => {
353
- let s, i, f = false, a = false, c = true;
354
- t.next().then(t => {
355
- if (c) {
356
- s = t;
402
+ let l, s, f = false, a = false, c = true;
403
+ // Protocol tolerance, matching `for await`: `await` unwraps whatever
404
+ // next() returns — a thenable OR a bare IteratorResult. Real producers
405
+ // use the bare form as a promise-free fast path when a value is already
406
+ // buffered (seroval's deserialized streams do), so a bare result is
407
+ // assimilated as an already-settled step instead of crashing on `.then`.
408
+ const S = t.next();
409
+ const d = isThenable(S) ? S : {
410
+ then: e => void e(S)
411
+ };
412
+ d.then(t => {
413
+ // The sync stash only serves the INITIAL drain (handleAsync's caller
414
+ // consumes syncValue / throws NotReady from it). A sync-settled step
415
+ // after an async gap — seroval buffering values between pulls, a
416
+ // sync-thenable producer mid-stream — has no caller reading the
417
+ // stash: it must write through the async path or the value is
418
+ // silently dropped.
419
+ if (c && i) {
420
+ l = t;
357
421
  f = true;
358
422
  if (t.done) o = true;
359
423
  } else if (e.Te !== n) {
@@ -374,7 +438,7 @@ function handleAsync(e, n, t) {
374
438
  }
375
439
  }, t => {
376
440
  if (c) {
377
- i = t;
441
+ s = t;
378
442
  a = true;
379
443
  } else if (e.Te === n) {
380
444
  o = true;
@@ -386,41 +450,46 @@ function handleAsync(e, n, t) {
386
450
  if (a) {
387
451
  // Match the promise branch, but only rethrow during the initial read.
388
452
  o = true;
389
- handleError(i);
390
- if (l) throw i;
453
+ handleError(s);
454
+ if (i) throw s;
391
455
  return true;
392
456
  }
393
- if (f && !s.done) {
394
- u = s.value;
457
+ if (f && !l.done) {
458
+ u = l.value;
395
459
  r = true;
396
460
  return iterate();
397
461
  }
398
- return f && s.done;
462
+ return f && l.done;
399
463
  };
400
- const s = iterate();
464
+ const l = iterate();
401
465
  // Later iterate() calls run from asyncWrite, where rethrowing would be unhandled.
402
- l = false;
403
- if (!r && !s) {
466
+ i = false;
467
+ if (!r && !l) {
468
+ // Loading window: serve commit #0 (see the promise branch above).
469
+ if (e.Ee) return e.Ue;
404
470
  globalQueue.initTransition(resolveTransition(e));
405
471
  throw new NotReadyError(context);
406
472
  }
473
+ // A sync first yield (or immediate empty completion) is the first real
474
+ // answer; async yields clear inside asyncWrite.
475
+ e.Ee = false;
407
476
  }
408
477
  return u;
409
478
  }
410
479
 
411
480
  function clearStatus(e, n = false) {
412
481
  if (e.oe) clearPendingSources(e);
413
- if (e.de) e.de = false;
482
+ if (e.ue) e.ue = false;
414
483
  // The pending window is over; its quiet classification dies with it.
415
484
  // (Unconditional: _reask is baked into the node literals, so this is a
416
485
  // plain store to an existing slot — no shape change.)
417
- e.Re = false;
486
+ e.he = false;
418
487
  e.S = n ? 0 : e.S & STATUS_UNINITIALIZED;
419
488
  if (e._) setPendingError(e);
420
489
  // Update pending signal for isPending() reactivity (companions only exist
421
490
  // once the verdict layer created them, which installs the hooks).
422
- if (e.he || e.pe) GlobalQueue.ce(e);
423
- if (e.u && GlobalQueue.Ge !== null) GlobalQueue.Ge(e);
491
+ if (e.ge || e.pe) GlobalQueue.Se(e);
492
+ if (e.u && GlobalQueue.Oe !== null) GlobalQueue.Oe(e);
424
493
  if (e.i) e.i();
425
494
  }
426
495
 
@@ -428,9 +497,9 @@ function notifyStatus(e, n, t, r, o) {
428
497
  // Wrap regular errors to track source node
429
498
  if (n === STATUS_ERROR && !(t instanceof StatusError) && !(t instanceof NotReadyError)) t = new StatusError(e, t);
430
499
  const u = n === STATUS_PENDING && t instanceof NotReadyError ? t.source : undefined;
431
- const l = u === e;
432
- const s = n === STATUS_PENDING && e.Ae !== undefined && !l;
433
- const i = s && hasActiveOverride(e);
500
+ const i = u === e;
501
+ const l = n === STATUS_PENDING && e._e !== undefined && !i;
502
+ const s = l && hasActiveOverride(e);
434
503
  if (!r) {
435
504
  if (n === STATUS_PENDING && u) {
436
505
  addPendingSource(e, u);
@@ -443,14 +512,14 @@ function notifyStatus(e, n, t, r, o) {
443
512
  e.S = n | (n !== STATUS_ERROR ? e.S & STATUS_UNINITIALIZED : 0);
444
513
  e._ = t;
445
514
  }
446
- GlobalQueue.ce !== null && GlobalQueue.ce(e);
447
- if (e.u && GlobalQueue.Ge !== null) GlobalQueue.Ge(e);
515
+ GlobalQueue.Se !== null && GlobalQueue.Se(e);
516
+ if (e.u && GlobalQueue.Oe !== null) GlobalQueue.Oe(e);
448
517
  }
449
518
  if (o && !r) {
450
519
  assignOrMergeLane(e, o);
451
520
  }
452
- const f = r || i;
453
- const a = r || s ? undefined : o;
521
+ const f = r || s;
522
+ const a = r || l ? undefined : o;
454
523
  if (e.i) {
455
524
  if (r && n === STATUS_PENDING) {
456
525
  return;
@@ -463,22 +532,22 @@ function notifyStatus(e, n, t, r, o) {
463
532
  return;
464
533
  }
465
534
  forEachDependent(e, (e, r) => {
466
- e.Se = clock;
535
+ e.de = clock;
467
536
  if (n === STATUS_PENDING && u && !e.oe?.has(u) || n !== STATUS_PENDING && (e._ !== t || e.oe)) {
468
537
  // A pending-observer link is the subscription an `isPending` read created.
469
538
  // It exists so the observer re-runs when the source settles, but it must
470
539
  // not carry a real (non-NotReadyError) error — the synchronous `isPending`
471
540
  // read swallows those, and the async path must match. Re-run the observer
472
541
  // so `isPending` re-evaluates (to not-pending) instead of forwarding.
473
- if (r.Oe && n !== STATUS_PENDING && !(t instanceof NotReadyError)) {
542
+ if (r.Ge && n !== STATUS_PENDING && !(t instanceof NotReadyError)) {
474
543
  enqueueSub(e);
475
544
  schedule();
476
545
  return;
477
546
  }
478
- if (!f && !e.Ie) queuePendingNode(e);
547
+ if (!f && !e.Ne) queuePendingNode(e);
479
548
  notifyStatus(e, n, t, f, a);
480
549
  }
481
550
  });
482
551
  }
483
552
 
484
- export { addPendingSource, clearStatus, forEachDependent, handleAsync, isThenable, notifyStatus, releaseSettledDependents, setPendingError, settleErroredDependents, settlePendingSource };
553
+ export { addPendingSource, clearStatus, forEachDependent, handleAsync, isThenable, notifyStatus, parkLoadingWindow, releaseSettledDependents, setPendingError, settleErroredDependents, settlePendingSource };