@solidjs/signals 2.0.0-beta.20 → 2.0.0-beta.22

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,12 +1,12 @@
1
- import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_MANUAL_WRITE } from "./constants.js";
1
+ import { NOT_PENDING, REACTIVE_DISPOSED, REACTIVE_DIRTY, REACTIVE_CHECK, unwrapOverride, REACTIVE_ZOMBIE, STATUS_UNINITIALIZED, STATUS_PENDING, REACTIVE_MANUAL_WRITE, STATUS_ERROR, REACTIVE_RECOMPUTING_DEPS } from "./constants.js";
2
2
 
3
- import { setSignal, read, context, stale, currentOptimisticLane, prepareComputed, tracking, setLatestReadActive, setContextInternal, optimisticComputed, setPendingCheckActive, latestReadActive, optimisticSignal, pendingCheckActive } from "./core.js";
3
+ import { setSignal, prepareComputed, read, context, stale, currentOptimisticLane, tracking, setLatestReadActive, setContextInternal, optimisticComputed, setPendingCheckActive, latestReadActive, optimisticSignal, pendingCheckActive } from "./core.js";
4
4
 
5
5
  import { NotReadyError } from "./error.js";
6
6
 
7
7
  import { link } from "./graph.js";
8
8
 
9
- import { insertIntoHeap, queueFor } from "./heap.js";
9
+ import { insertIntoHeap, queueFor, markHeap } from "./heap.js";
10
10
 
11
11
  import "./invariants.js";
12
12
 
@@ -14,7 +14,7 @@ import { findLane, hasActiveOverride } from "./lanes.js";
14
14
 
15
15
  import { installOptimisticEngine } from "./optimistic.js";
16
16
 
17
- import { GlobalQueue, clock, insertSubs, schedule } from "./scheduler.js";
17
+ import { GlobalQueue, clock, insertSubs, schedule, activeTransition, activeAffectsMarks } from "./scheduler.js";
18
18
 
19
19
  /**
20
20
  * The isPending()/latest() verdict layer, moved out of core.ts. Importing this
@@ -32,21 +32,21 @@ let pendingProbe = null;
32
32
  * Get or create the pending signal for a node (lazy).
33
33
  * Used by isPending() to track pending state reactively.
34
34
  */ function getPendingSignal(e) {
35
- if (!e.ye) {
35
+ if (!e.ge) {
36
36
  // Start false, write true if pending - ensures reversion returns to false
37
- e.ye = optimisticSignal(false, {
37
+ e.ge = optimisticSignal(false, {
38
38
  ownedWrite: true
39
39
  });
40
- e.ye.en = e;
41
- if (computePendingState(e)) setSignal(e.ye, true);
40
+ e.ge.nn = e;
41
+ if (computePendingState(e)) setSignal(e.ge, true);
42
42
  }
43
- return e.ye;
43
+ return e.ge;
44
44
  }
45
45
 
46
46
  function collectPendingSources(e) {
47
47
  if (!pendingProbe) return;
48
48
  pendingProbe.sources.add(e);
49
- const t = e.rt || e;
49
+ const t = e.nt || e;
50
50
  if (t !== e) pendingProbe.sources.add(t);
51
51
  }
52
52
 
@@ -60,65 +60,116 @@ function collectPendingSources(e) {
60
60
  pendingProbe?.sources.add(e);
61
61
  }
62
62
 
63
+ /**
64
+ * The affects() coverage walk — the read half of the dedicated mark channel.
65
+ * A node is covered by a live mark iff it carries one (`_affectsCount`) or
66
+ * derives, through its CURRENT deps (hopping store firewalls), from a node
67
+ * that does. Pull-based coverage means graph rewires, mid-window recomputes,
68
+ * and probe-triggered recomputes can never strand or strip a mark — there is
69
+ * nothing stored downstream to corrupt. Probe-created links
70
+ * (`_pendingObserver`) are skipped so an `isPending` wrapper memo never
71
+ * inherits the coverage it reports on.
72
+ */ function markWalk(e, t) {
73
+ if (e.t) return true;
74
+ // A real error outranks an inherited mark (A16/A24c): an errored node
75
+ // answers probes with its error, not a coverage verdict, and coverage does
76
+ // not flow through it — matching the rails' behavior, where propagation
77
+ // stopped at errored nodes. A DIRECT mark on an errored node still reads
78
+ // pending (the count check above), also matching.
79
+ if (e.S & STATUS_ERROR) return false;
80
+ if (t.has(e)) return false;
81
+ t.add(e);
82
+ const n = e.nt;
83
+ if (n && markWalk(n, t)) return true;
84
+ // Mid-recompute (the clearStatus companion poke runs before
85
+ // trimStaleDeps), only the validated prefix [_deps.._depsTail] is this
86
+ // pass's dependency set — walking past it would read dropped deps and
87
+ // latch a stale verdict on the companion.
88
+ const i = e;
89
+ const r = i.se & REACTIVE_RECOMPUTING_DEPS ? i.qe : undefined;
90
+ if (r !== null) {
91
+ for (let e = i.Xe ?? null; e !== null; e = e.et) {
92
+ if (!e.De && markWalk(e.tt, t)) return true;
93
+ if (e === r) break;
94
+ }
95
+ }
96
+ return false;
97
+ }
98
+
99
+ /** Gated entry: apps with no live mark pay one integer compare. */ function markCovered(e) {
100
+ return activeAffectsMarks !== 0 && markWalk(e, new Set);
101
+ }
102
+
63
103
  function quietPending(e) {
64
- if (e.m) {
65
- for (const t of e.m) if (!t.A) return false;
104
+ if (e.oe) {
105
+ for (const t of e.oe) if (!t.be) return false;
66
106
  return true;
67
107
  }
68
- return e.A;
108
+ return e.be;
69
109
  }
70
110
 
71
111
  function newQuestionInFlight(e) {
72
- return !!(e.i & STATUS_PENDING) && !(e.i & STATUS_UNINITIALIZED) && !quietPending(e);
112
+ return !!(e.S & STATUS_PENDING) && !(e.S & STATUS_UNINITIALIZED) && !quietPending(e);
73
113
  }
74
114
 
75
115
  function computePendingState(e) {
76
116
  const t = e;
77
- if (t.u & REACTIVE_DISPOSED) return false;
78
- if (e.M) return true;
79
- const n = e.rt;
80
- if (e.en) {
81
- const t = e.en;
82
- if (t.M) return true;
83
- const n = t.rt || t;
117
+ if (t.se & REACTIVE_DISPOSED) return false;
118
+ // Mark coverage is transitive by dep-graph reachability: a latest() shadow
119
+ // reaches its owner (and a store leaf its firewall) through its own deps,
120
+ // so the one walk covers direct marks, derivation, and companion chains.
121
+ if (markCovered(e)) return true;
122
+ const n = e.nt;
123
+ if (e.nn) {
124
+ const t = e.nn;
125
+ const n = t.nt || t;
84
126
  return newQuestionInFlight(n);
85
127
  }
86
- if (n && e.ge !== NOT_PENDING && !hasActiveOverride(e)) {
87
- return !!(n.u & REACTIVE_MANUAL_WRITE) || !n.Ae && !(n.i & STATUS_PENDING) || !!(n.i & STATUS_PENDING) && quietPending(n);
128
+ if (n && e.Ne !== NOT_PENDING && !hasActiveOverride(e)) {
129
+ return !!(n.se & REACTIVE_MANUAL_WRITE) || !n.Te && !(n.S & STATUS_PENDING) || !!(n.S & STATUS_PENDING) && quietPending(n);
88
130
  }
89
- if (e.ge !== NOT_PENDING && !(t.i & STATUS_UNINITIALIZED)) {
90
- if (hasActiveOverride(e)) return !e.Ge || !e.Ge(e.ge, unwrapOverride(e.be));
131
+ if (e.Ne !== NOT_PENDING && !(t.S & STATUS_UNINITIALIZED)) {
132
+ if (hasActiveOverride(e)) return !e.Pe || !e.Pe(e.Ne, unwrapOverride(e.Ee));
91
133
  return true;
92
134
  }
93
135
  return newQuestionInFlight(t);
94
136
  }
95
137
 
96
138
  function syncCompanions(e, t) {
97
- if (e.ye) updatePendingSignal(e);
98
- if (e.pe) setSignal(e.pe, t);
139
+ if (e.ge) updatePendingSignal(e);
140
+ if (e.he) setSignal(e.he, t);
99
141
  }
100
142
 
101
143
  function updatePendingSignal(e) {
102
- if (e.ye) {
103
- setSignal(e.ye, computePendingState(e));
144
+ if (e.ge) {
145
+ setSignal(e.ge, computePendingState(e));
104
146
  }
105
- if (e.pe) updatePendingSignal(e.pe);
147
+ if (e.he) updatePendingSignal(e.he);
106
148
  }
107
149
 
108
150
  function updateChildCompanions(e) {
109
- for (let t = e.G; t !== null; t = t.Ne) {
110
- if (t.ye || t.pe) updatePendingSignal(t);
151
+ for (let t = e.u; t !== null; t = t.fe) {
152
+ if (t.ge || t.he) updatePendingSignal(t);
111
153
  }
112
154
  }
113
155
 
114
- function repollDownstreamVerdicts(e) {
115
- const t = new Set;
156
+ /**
157
+ * Re-derive every verdict companion downstream of `el` (subs + firewall
158
+ * children, dedup'd). The affects() channel's poke walk: registration and
159
+ * re-ask flips use the live write path (companion setSignal — its own lane
160
+ * lets the wake escape an incomplete transition's effect stash, #2887);
161
+ * mark release passes `snap` because it runs inside queue finalization,
162
+ * where companion writes must land committed (a setSignal there would open
163
+ * a fresh override window that nothing settles).
164
+ */ function repollDownstreamVerdicts(e, t = false) {
165
+ const n = t ? snapCompanionsToState : updatePendingSignal;
166
+ const i = new Set;
116
167
  const visit = e => {
117
- if (t.has(e)) return;
118
- t.add(e);
119
- if (e.ye || e.pe) updatePendingSignal(e);
120
- for (let t = e.p; t !== null; t = t.de) visit(t.Ee);
121
- for (let t = e.G ?? null; t !== null; t = t.Ne) {
168
+ if (i.has(e)) return;
169
+ i.add(e);
170
+ if (e.ge || e.he) n(e);
171
+ for (let t = e.o; t !== null; t = t.ue) visit(t.le);
172
+ for (let t = e.u ?? null; t !== null; t = t.fe) {
122
173
  visit(t);
123
174
  }
124
175
  };
@@ -126,21 +177,21 @@ function repollDownstreamVerdicts(e) {
126
177
  }
127
178
 
128
179
  function snapCompanionsToState(e) {
129
- const t = e.ye;
130
- if (t && (t.be === undefined || t.be === NOT_PENDING)) {
180
+ const t = e.ge;
181
+ if (t && (t.Ee === undefined || t.Ee === NOT_PENDING)) {
131
182
  const n = computePendingState(e);
132
- if (t.Ue !== n || t.ge !== NOT_PENDING) {
133
- t.Ue = n;
134
- t.ge = NOT_PENDING;
135
- t.Ie = clock;
183
+ if (t.Re !== n || t.Ne !== NOT_PENDING) {
184
+ t.Re = n;
185
+ t.Ne = NOT_PENDING;
186
+ t.ce = clock;
136
187
  insertSubs(t);
137
188
  schedule();
138
189
  }
139
190
  }
140
- const n = e.pe;
141
- if (n && !(n.u & REACTIVE_DISPOSED)) {
142
- if ((n.be === undefined || n.be === NOT_PENDING) && n.ge === NOT_PENDING && !Object.is(n.Ue, e.Ue) && !(n.u & (REACTIVE_DIRTY | REACTIVE_CHECK))) {
143
- n.u |= REACTIVE_DIRTY;
191
+ const n = e.he;
192
+ if (n && !(n.se & REACTIVE_DISPOSED)) {
193
+ if ((n.Ee === undefined || n.Ee === NOT_PENDING) && n.Ne === NOT_PENDING && !Object.is(n.Re, e.Re) && !(n.se & (REACTIVE_DIRTY | REACTIVE_CHECK))) {
194
+ n.se |= REACTIVE_DIRTY;
144
195
  insertIntoHeap(n, queueFor(n));
145
196
  insertSubs(n);
146
197
  schedule();
@@ -150,7 +201,7 @@ function snapCompanionsToState(e) {
150
201
  }
151
202
 
152
203
  function getLatestValueComputed(e) {
153
- if (!e.pe) {
204
+ if (!e.he) {
154
205
  const t = latestReadActive;
155
206
  setLatestReadActive(false);
156
207
  const n = pendingCheckActive;
@@ -158,49 +209,64 @@ function getLatestValueComputed(e) {
158
209
  const i = context;
159
210
  setContextInternal(null);
160
211
  // Detach from owner so it isn't disposed with effects
161
- e.pe = optimisticComputed(() => read(e));
162
- e.pe.en = e;
212
+ e.he = optimisticComputed(() => read(e));
213
+ e.he.nn = e;
163
214
  // Parent-child lane relationship
164
215
  setContextInternal(i);
165
216
  setPendingCheckActive(n);
166
217
  setLatestReadActive(t);
167
218
  }
168
- return e.pe;
219
+ return e.he;
169
220
  }
170
221
 
171
222
  /** The latest()-mode read path, installed as GlobalQueue._latestRead. */ function latestRead(e) {
172
223
  const t = getLatestValueComputed(e);
173
224
  const n = latestReadActive;
174
225
  setLatestReadActive(false);
175
- const i = e.be !== undefined && e.be !== NOT_PENDING ? unwrapOverride(e.be) : e.Ue;
226
+ const i = e.Ee !== undefined && e.Ee !== NOT_PENDING ? unwrapOverride(e.Ee) : e.Re;
176
227
  let r;
177
228
  try {
229
+ // An untracked latest() read has no reading context, so read() never
230
+ // performs its mid-tick pull — a plain write queued between two latest()
231
+ // calls left a still-subscribed shadow at its previous speculative value
232
+ // until the flush (#2922). Mirror the tracked-read pull here: mark the
233
+ // queued staleness through the graph, then bring the shadow up to date.
234
+ const e = queueFor(t);
235
+ if (t.He >= e.Fe && !(t.se & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
236
+ markHeap(e);
237
+ prepareComputed(t, true);
238
+ }
178
239
  r = read(t);
179
240
  } catch (t) {
180
- if (t instanceof NotReadyError && (!context || !(e.i & STATUS_UNINITIALIZED))) return i;
241
+ if (t instanceof NotReadyError && (!context || !(e.S & STATUS_UNINITIALIZED))) return i;
181
242
  throw t;
182
243
  } finally {
183
244
  setLatestReadActive(n);
184
245
  }
185
- if (t.i & STATUS_PENDING) return i;
186
- if (stale && currentOptimisticLane && t.Je) {
187
- const e = findLane(t.Je);
246
+ if (t.S & STATUS_PENDING) return i;
247
+ if (stale && currentOptimisticLane && t.je) {
248
+ const e = findLane(t.je);
188
249
  const n = findLane(currentOptimisticLane);
189
- if (e !== n && e.Pe.size > 0) {
250
+ if (e !== n && e.de.size > 0) {
190
251
  return i;
191
252
  }
192
253
  }
254
+ // A shadow recomputed by the pull above (not at creation) holds its fresh
255
+ // speculative value in _pendingValue; a contextless read() only surfaces
256
+ // _value. Overrides stay authoritative (A17), and stale readers keep the
257
+ // other transition's committed view, matching read()'s own selection.
258
+ if (t.Ne !== NOT_PENDING && !hasActiveOverride(t) && !(stale && t.Ue && activeTransition !== t.Ue)) return t.Ne;
193
259
  return r;
194
260
  }
195
261
 
196
262
  /** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */ function pendingCheckRead(e, t, n, i) {
197
263
  setPendingCheckActive(false);
198
- if (typeof e.xe === "function") prepareComputed(e, true);
199
- const r = n.i;
264
+ if (typeof e.ke === "function") prepareComputed(e, true);
265
+ const r = n.S;
200
266
  if (t && r & STATUS_PENDING && r & STATUS_UNINITIALIZED) {
201
267
  if (tracking && e !== t) link(e, t);
202
268
  setPendingCheckActive(true);
203
- throw n.k;
269
+ throw n._;
204
270
  }
205
271
  collectPendingSources(e);
206
272
  if (i) collectPendingSources(i);
@@ -208,14 +274,14 @@ function getLatestValueComputed(e) {
208
274
  }
209
275
 
210
276
  function recordFreshRead(e, t) {
211
- if (pendingProbe !== null && e.ge !== NOT_PENDING && t === e.ge) pendingProbe.freshReads.add(e);
277
+ if (pendingProbe !== null && e.Ne !== NOT_PENDING && t === e.Ne) pendingProbe.freshReads.add(e);
212
278
  }
213
279
 
214
280
  function applyReask(e, t) {
215
- const n = !!(e.i & STATUS_PENDING);
216
- const i = t && !(n && !e.A);
217
- const r = n && e.A !== i;
218
- e.A = i;
281
+ const n = !!(e.S & STATUS_PENDING);
282
+ const i = t && !(n && !e.be);
283
+ const r = n && e.be !== i;
284
+ e.be = i;
219
285
  return r;
220
286
  }
221
287
 
@@ -255,7 +321,7 @@ function isPending(e) {
255
321
  } catch (e) {
256
322
  collectPending();
257
323
  if (e instanceof NotReadyError) {
258
- const t = !!(e.source?.i & STATUS_UNINITIALIZED);
324
+ const t = !!(e.source?.S & STATUS_UNINITIALIZED);
259
325
  if (i.found && !t) return true;
260
326
  if (context && t) throw e;
261
327
  }
@@ -269,24 +335,24 @@ function isPending(e) {
269
335
  // Hook installation (same late-binding pattern as GlobalQueue._update /
270
336
  // _propagateAffects): core call sites fire these behind the same guards the
271
337
  // direct calls used, so behavior is identical once this module loads.
272
- GlobalQueue._e = syncCompanions;
338
+ GlobalQueue.Ie = syncCompanions;
273
339
 
274
- GlobalQueue.P = updatePendingSignal;
340
+ GlobalQueue.ae = updatePendingSignal;
275
341
 
276
- GlobalQueue.me = updateChildCompanions;
342
+ GlobalQueue._e = updateChildCompanions;
277
343
 
278
- GlobalQueue.R = snapCompanionsToState;
344
+ GlobalQueue.un = snapCompanionsToState;
279
345
 
280
- GlobalQueue.Gt = latestRead;
346
+ GlobalQueue.St = latestRead;
281
347
 
282
- GlobalQueue.Pt = pendingCheckRead;
348
+ GlobalQueue.dt = pendingCheckRead;
283
349
 
284
- GlobalQueue.kt = recordFreshRead;
350
+ GlobalQueue.Rt = recordFreshRead;
285
351
 
286
- GlobalQueue.tt = applyReask;
352
+ GlobalQueue.ze = applyReask;
287
353
 
288
- GlobalQueue.nt = repollDownstreamVerdicts;
354
+ GlobalQueue.k = repollDownstreamVerdicts;
289
355
 
290
- GlobalQueue.wt = witnessAffects;
356
+ GlobalQueue.Lt = witnessAffects;
291
357
 
292
358
  export { isPending, latest };