@solidjs/signals 2.0.0-beta.19 → 2.0.0-beta.21

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.
Files changed (37) hide show
  1. package/README.md +5 -1
  2. package/dist/dev.js +753 -378
  3. package/dist/node.cjs +1947 -1607
  4. package/dist/prod/affects.js +17 -21
  5. package/dist/prod/core/action.js +66 -15
  6. package/dist/prod/core/async.js +57 -71
  7. package/dist/prod/core/constants.js +15 -1
  8. package/dist/prod/core/core.js +39 -39
  9. package/dist/prod/core/effect.js +14 -14
  10. package/dist/prod/core/graph.js +1 -1
  11. package/dist/prod/core/heap.js +11 -3
  12. package/dist/prod/core/lanes.js +23 -13
  13. package/dist/prod/core/optimistic.js +107 -99
  14. package/dist/prod/core/owner.js +23 -23
  15. package/dist/prod/core/scheduler.js +188 -181
  16. package/dist/prod/core/verdict.js +29 -15
  17. package/dist/prod/map.js +196 -167
  18. package/dist/prod/signals.js +1 -1
  19. package/dist/prod/store/optimistic.js +103 -68
  20. package/dist/prod/store/reconcile.js +80 -46
  21. package/dist/prod/store/store.js +204 -73
  22. package/dist/prod/store/utils.js +61 -42
  23. package/dist/types/core/action.d.ts +35 -6
  24. package/dist/types/core/constants.d.ts +12 -0
  25. package/dist/types/core/dev.d.ts +7 -0
  26. package/dist/types/core/lanes.d.ts +2 -0
  27. package/dist/types/core/scheduler.d.ts +2 -6
  28. package/dist/types/core/types.d.ts +9 -1
  29. package/dist/types/store/store.d.ts +8 -2
  30. package/dist/types-cjs/core/action.d.cts +35 -6
  31. package/dist/types-cjs/core/constants.d.cts +12 -0
  32. package/dist/types-cjs/core/dev.d.cts +7 -0
  33. package/dist/types-cjs/core/lanes.d.cts +2 -0
  34. package/dist/types-cjs/core/scheduler.d.cts +2 -6
  35. package/dist/types-cjs/core/types.d.cts +9 -1
  36. package/dist/types-cjs/store/store.d.cts +8 -2
  37. package/package.json +1 -1
@@ -1,12 +1,12 @@
1
- import { forEachDependent, notifyStatus, addPendingSource, setPendingError, settlePendingSource } from "./core/async.js";
1
+ import { addPendingSource, setPendingError, settlePendingSource, forEachDependent, notifyStatus } from "./core/async.js";
2
2
 
3
- import { $REFRESH, STATUS_PENDING } from "./core/constants.js";
3
+ import { STATUS_PENDING, $REFRESH } from "./core/constants.js";
4
4
 
5
5
  import { NotReadyError } from "./core/error.js";
6
6
 
7
7
  import "./core/core.js";
8
8
 
9
- import { globalQueue, schedule, GlobalQueue, shiftAffectsMarks } from "./core/scheduler.js";
9
+ import { GlobalQueue, shiftAffectsMarks, globalQueue, schedule } from "./core/scheduler.js";
10
10
 
11
11
  import "./core/verdict.js";
12
12
 
@@ -68,7 +68,7 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
68
68
  const t = getAffectsSentinel(e);
69
69
  const r = new NotReadyError(t);
70
70
  forEachDependent(e, e => {
71
- if (e.m !== t && !e.M?.has(t)) {
71
+ if (!e.m?.has(t)) {
72
72
  notifyStatus(e, STATUS_PENDING, r);
73
73
  }
74
74
  });
@@ -84,7 +84,7 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
84
84
  let r = false;
85
85
  for (let f = 0; f < t.length; f++) {
86
86
  const o = t[f];
87
- if (!o.P) continue;
87
+ if (!o.M) continue;
88
88
  const s = getAffectsSentinel(o);
89
89
  if (addPendingSource(e, s)) {
90
90
  e.i |= STATUS_PENDING;
@@ -92,7 +92,7 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
92
92
  r = true;
93
93
  }
94
94
  }
95
- if (r && GlobalQueue._ !== null) GlobalQueue._(e);
95
+ if (r && GlobalQueue.P !== null) GlobalQueue.P(e);
96
96
  }
97
97
 
98
98
  /**
@@ -101,24 +101,24 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
101
101
  * bumps the refcount and pokes the node's verdict companions so an
102
102
  * already-materialized `false` flips reactively.
103
103
  */ function markAffects(e) {
104
- e.P = (e.P || 0) + 1;
104
+ e.M = (e.M || 0) + 1;
105
105
  shiftAffectsMarks(1);
106
106
  // Companions only exist once the verdict layer (isPending/latest) loaded;
107
107
  // without them there is no materialized verdict to flip.
108
- if (e.P === 1 && GlobalQueue._ !== null) GlobalQueue._(e);
108
+ if (e.M === 1 && GlobalQueue.P !== null) GlobalQueue.P(e);
109
109
  }
110
110
 
111
111
  /**
112
112
  * Registers one `affects()` mark on a node: counts it, records the
113
113
  * registration with the current transaction (after initTransition the queue's
114
- * array aliases the active transition's, mirroring `_optimisticNodes`), and
114
+ * batch IS the active transition, mirroring `_optimisticNodes`), and
115
115
  * propagates STATUS_PENDING downstream on the status rails so everything
116
116
  * DERIVED from the marked data reads pending too. Propagation runs on every
117
117
  * registration (not just the first): subscribers gained since an earlier
118
118
  * overlapping registration get covered, and dedup stops re-descent early.
119
119
  */ function registerAffectsMark(e) {
120
120
  markAffects(e);
121
- globalQueue.N.push(e);
121
+ globalQueue.N._.push(e);
122
122
  propagateAffectsMark(e);
123
123
  schedule();
124
124
  }
@@ -131,8 +131,8 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
131
131
  * can't open a fresh override window that would itself need settlement.
132
132
  */ function releaseAffectsMark(e) {
133
133
  shiftAffectsMarks(-1);
134
- e.P--;
135
- if (!e.P) {
134
+ e.M--;
135
+ if (!e.M) {
136
136
  const t = e.t;
137
137
  if (t) settlePendingSource(e, t, true);
138
138
  GlobalQueue.R !== null && GlobalQueue.R(e);
@@ -156,12 +156,12 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
156
156
  * keeps normal suspension semantics. Core's read path reaches this through
157
157
  * `GlobalQueue._onlyMarkPending`, gated on the `activeAffectsMarks` counter.
158
158
  */ function onlyMarkPending(e) {
159
- const t = e.M;
159
+ const t = e.m;
160
160
  if (t) {
161
161
  for (const e of t) if (!e.l) return false;
162
162
  return true;
163
163
  }
164
- return !!e.m?.l;
164
+ return false;
165
165
  }
166
166
 
167
167
  /**
@@ -175,14 +175,10 @@ import { $TARGET, getStoreAffectsNodes } from "./store/store.js";
175
175
  * the very status it reports on. Reached through
176
176
  * `GlobalQueue._collectMarkSources`, gated on `activeAffectsMarks`.
177
177
  */ function collectMarkSources(e, t) {
178
- const r = e.m;
179
- if (r) {
180
- const e = r.l;
181
- if (e && e.P) t.push(e);
182
- } else if (e.M) {
183
- for (const r of e.M) {
178
+ if (e.m) {
179
+ for (const r of e.m) {
184
180
  const e = r.l;
185
- if (e && e.P) t.push(e);
181
+ if (e && e.M) t.push(e);
186
182
  }
187
183
  }
188
184
  }
@@ -4,23 +4,51 @@ import { isThenable } from "./async.js";
4
4
 
5
5
  import "./core.js";
6
6
 
7
- function restoreTransition(e, n) {
7
+ function restoreTransition(e, r) {
8
8
  globalQueue.initTransition(e);
9
- const t = n();
9
+ const t = r();
10
10
  flush();
11
11
  return t;
12
12
  }
13
13
 
14
14
  /**
15
+ * The primitive for mutations: imperative async workflows whose *writes span
16
+ * an async gap* — optimistic write, server round-trip, reconciling write —
17
+ * where intermediate state must not leak and failure must revert cleanly
18
+ * (pair with `createOptimistic` / `createOptimisticStore`).
19
+ *
20
+ * Navigation-shaped updates do not need an action. A plain setter call is
21
+ * enough: reads pull the async, and downstream async computeds hold their
22
+ * previous values per-node until the new ones are ready (`isPending` /
23
+ * `latest` expose the in-flight state). Reach for `action` only when writes
24
+ * happen *after* async work, not merely upstream of it.
25
+ *
26
+ * Framework-level actions (router form actions, server actions) are
27
+ * specializations of this primitive: they are actions in exactly this sense —
28
+ * the same transactional semantics — with form binding, serialization, and
29
+ * submission tracking layered on top. The shared name is deliberate.
30
+ *
15
31
  * Wraps a generator function so each invocation runs as a single transaction
16
32
  * (a "transition") that batches every signal/store write between yields. The
17
33
  * surrounding UI sees one atomic update per yielded step; nothing is committed
18
34
  * until the action either completes or the next `yield` resolves.
19
35
  *
20
- * Yield promises (or any awaitable) inside the generator the action waits
21
- * for each before continuing, but the writes you made beforehand are already
22
- * visible (or held by `<Loading>` if optimistic). Yield bare values for
23
- * synchronous batched steps.
36
+ * `yield` is the transaction-safe suspension point: the action waits for a
37
+ * yielded promise and re-enters the transaction before running the code after
38
+ * it. A plain `await` does NOT the runtime has no hook into an async
39
+ * generator's internal await continuations, so writes to fresh signals
40
+ * between an `await` and the next `yield` escape the transaction and commit
41
+ * immediately. `await` is still the ergonomic choice for typed results; just
42
+ * put a bare `yield` before any writes that follow it:
43
+ *
44
+ * ```ts
45
+ * const saved = await api.createTodo(text); // typed result
46
+ * yield; // re-enter the transaction before writing
47
+ * setTodos(t => { ... });
48
+ * ```
49
+ *
50
+ * (For the same reason, don't call `flush()` inside an action body — it
51
+ * drains the transaction mid-step.)
24
52
  *
25
53
  * Each call returns a `Promise` that resolves with the generator's return
26
54
  * value, or rejects if it throws. Pair with `createOptimistic` /
@@ -31,10 +59,11 @@ function restoreTransition(e, n) {
31
59
  * ```ts
32
60
  * const [todos, setTodos] = createOptimisticStore<Todo[]>([]);
33
61
  *
34
- * const addTodo = action(function* (text: string) {
62
+ * const addTodo = action(async function* (text: string) {
35
63
  * const tempId = crypto.randomUUID();
36
64
  * setTodos(t => { t.push({ id: tempId, text, pending: true }); }); // optimistic
37
- * const saved = yield api.createTodo(text); // network round-trip
65
+ * const saved = await api.createTodo(text); // network round-trip, typed
66
+ * yield; // re-enter the transaction
38
67
  * setTodos(t => {
39
68
  * const i = t.findIndex(x => x.id === tempId);
40
69
  * if (i >= 0) t[i] = saved;
@@ -45,23 +74,23 @@ function restoreTransition(e, n) {
45
74
  * await addTodo("buy milk");
46
75
  * ```
47
76
  */ function action(e) {
48
- return (...n) => new Promise((t, r) => {
49
- const i = e(...n);
77
+ return (...r) => new Promise((t, n) => {
78
+ const i = e(...r);
50
79
  globalQueue.initTransition();
51
80
  let o = activeTransition;
52
81
  o.Te.push(i);
53
- const done = (e, n, s = false) => {
82
+ const done = (e, r, s = false) => {
54
83
  o = currentTransition(o);
55
84
  const u = o.Te.indexOf(i);
56
85
  if (u >= 0) o.Te.splice(u, 1);
57
86
  setActiveTransition(o);
58
87
  schedule();
59
- s ? r(n) : t(e);
88
+ s ? n(r) : t(e);
60
89
  };
61
- const step = (e, n) => {
90
+ const step = (e, r) => {
62
91
  let t;
63
92
  try {
64
- t = n ? i.throw(e) : i.next(e);
93
+ t = r ? i.throw(e) : i.next(e);
65
94
  } catch (e) {
66
95
  return done(undefined, e, true);
67
96
  }
@@ -73,7 +102,29 @@ function restoreTransition(e, n) {
73
102
  };
74
103
  const run = e => {
75
104
  if (e.done) return done(e.value);
76
- if (isThenable(e.value)) return void e.value.then(e => restoreTransition(o, () => step(e)), e => restoreTransition(o, () => step(e, true)));
105
+ // Thenable assimilation can itself throw synchronously (a `then`
106
+ // getter, or a `then()` method that throws — #2918). Match `await`
107
+ // semantics: the failure is thrown back into the generator at the
108
+ // yield point (catchable there); if uncaught, step()'s guard settles
109
+ // the action so its iterator never leaks in the transition. The
110
+ // settled flag implements A+ 2.3.3.3.4.1: a throw after the thenable
111
+ // already called a callback is ignored.
112
+ let r = false;
113
+ try {
114
+ if (isThenable(e.value)) return void e.value.then(e => {
115
+ if (r) return;
116
+ r = true;
117
+ restoreTransition(o, () => step(e));
118
+ }, e => {
119
+ if (r) return;
120
+ r = true;
121
+ restoreTransition(o, () => step(e, true));
122
+ });
123
+ } catch (e) {
124
+ if (r) return;
125
+ r = true;
126
+ return void restoreTransition(o, () => step(e, true));
127
+ }
77
128
  restoreTransition(o, () => step(e.value));
78
129
  };
79
130
  step();
@@ -14,42 +14,28 @@ import { resolveTransition, hasActiveOverride, assignOrMergeLane, resolveLane }
14
14
 
15
15
  import { cleanup } from "./owner.js";
16
16
 
17
- import { globalQueue, GlobalQueue, clock, schedule, queuePendingNode, flush, insertSubs } from "./scheduler.js";
17
+ import { globalQueue, GlobalQueue, clock, schedule, flush, queuePendingNode, insertSubs } from "./scheduler.js";
18
18
 
19
+ // The lazily-created Set is the ONE container for pending sources. Its
20
+ // predecessor — a singular slot promoted to a Set on the second source —
21
+ // created dual state whose migration invariant was easy to break: a third
22
+ // overlapping source landed beside the Set and removePendingSource refused
23
+ // to clear it, stranding the Set members' pending forever (#2893).
19
24
  function addPendingSource(e, n) {
20
- if (e.m === n || e.M?.has(n)) return false;
21
- // Once the Set exists it is THE container — the singular slot stays empty
22
- // from migration until removePendingSource collapses back to one entry.
23
- // Landing a third source in the singular slot instead created dual state
24
- // that removePendingSource refused to clear, stranding the Set members'
25
- // pending forever (#2893).
26
- if (e.M) e.M.add(n); else if (!e.m) e.m = n; else {
27
- e.M = new Set([ e.m, n ]);
28
- e.m = undefined;
29
- }
25
+ if (e.m?.has(n)) return false;
26
+ (e.m ??= new Set).add(n);
30
27
  return true;
31
28
  }
32
29
 
33
30
  function removePendingSource(e, n) {
34
- if (e.m) {
35
- if (e.m !== n) return false;
36
- e.m = undefined;
37
- return true;
38
- }
39
- if (!e.M?.delete(n)) return false;
40
- if (e.M.size === 1) {
41
- e.m = e.M.values().next().value;
42
- e.M = undefined;
43
- } else if (e.M.size === 0) {
44
- e.M = undefined;
45
- }
31
+ if (!e.m?.delete(n)) return false;
32
+ if (e.m.size === 0) e.m = undefined;
46
33
  return true;
47
34
  }
48
35
 
49
36
  function clearPendingSources(e) {
37
+ e.m?.clear();
50
38
  e.m = undefined;
51
- e.M?.clear();
52
- e.M = undefined;
53
39
  }
54
40
 
55
41
  function setPendingError(e, n, r) {
@@ -84,21 +70,21 @@ function settlePendingSource(e, n = e,
84
70
  // open a fresh transition-scoped override window that nothing reverts.
85
71
  r = false) {
86
72
  let t = false;
87
- const u = new Set;
73
+ const o = new Set;
88
74
  // Companion updates no-op without the verdict layer (null hooks).
89
- const o = r ? GlobalQueue.R : GlobalQueue._;
75
+ const u = r ? GlobalQueue.R : GlobalQueue.P;
90
76
  const settle = e => {
91
- if (u.has(e) || !removePendingSource(e, n)) return;
92
- u.add(e);
77
+ if (o.has(e) || !removePendingSource(e, n)) return;
78
+ o.add(e);
93
79
  e.Ie = clock;
94
- const r = e.m ?? e.M?.values().next().value;
80
+ const r = e.m?.values().next().value;
95
81
  if (r) {
96
82
  setPendingError(e, r);
97
- o !== null && o(e);
83
+ u !== null && u(e);
98
84
  } else {
99
85
  e.i &= ~STATUS_PENDING;
100
86
  setPendingError(e);
101
- o !== null && o(e);
87
+ u !== null && u(e);
102
88
  if (e.Re) {
103
89
  enqueueSub(e);
104
90
  t = true;
@@ -118,19 +104,19 @@ function isThenable(e) {
118
104
 
119
105
  function handleAsync(e, n, r) {
120
106
  let t = false;
121
- let u = false;
107
+ let o = false;
122
108
  if (typeof n === "object" && n !== null) {
123
109
  untrack(() => {
124
110
  t = n[Symbol.asyncIterator];
125
- u = !t && isThenable(n);
111
+ o = !t && isThenable(n);
126
112
  });
127
113
  }
128
- if (!u && !t) {
114
+ if (!o && !t) {
129
115
  e.Ae = null;
130
116
  return n;
131
117
  }
132
118
  e.Ae = n;
133
- let o;
119
+ let u;
134
120
  const handleError = r => {
135
121
  if (e.Ae !== n) return;
136
122
  globalQueue.initTransition(resolveTransition(e));
@@ -138,21 +124,21 @@ function handleAsync(e, n, r) {
138
124
  notifyStatus(e, r instanceof NotReadyError ? STATUS_PENDING : STATUS_ERROR, r);
139
125
  e.Ie = clock;
140
126
  };
141
- const asyncWrite = (t, u) => {
127
+ const asyncWrite = (t, o) => {
142
128
  if (e.Ae !== n) return;
143
129
  // If the node was dirtied by a newer write (optimistic override or regular),
144
130
  // skip this stale async result — the upcoming flush will recompute the node
145
131
  // with the new value, creating a fresh Promise that supersedes this one.
146
132
  if (e.u & (REACTIVE_DIRTY | REACTIVE_OPTIMISTIC_DIRTY)) return;
147
133
  globalQueue.initTransition(resolveTransition(e));
148
- const o = !!(e.i & STATUS_UNINITIALIZED);
134
+ const u = !!(e.i & STATUS_UNINITIALIZED);
149
135
  trimStaleDeps(e);
150
136
  clearStatus(e);
151
137
  const l = resolveLane(e);
152
138
  if (l) l.Pe.delete(e);
153
139
  if (r) {
154
140
  r(t);
155
- if (o) clearStatus(e, true);
141
+ if (u) clearStatus(e, true);
156
142
  } else if (e.be !== undefined) {
157
143
  // Optimistic node — resting OR covered by an active override — holds
158
144
  // through the shared pending-node path, exactly like a plain async memo,
@@ -179,9 +165,9 @@ function handleAsync(e, n, r) {
179
165
  // Route through lane's effect queue for independent flushing
180
166
  const n = e.De;
181
167
  const r = e.Ue;
182
- const u = e.Ge;
168
+ const o = e.Ge;
183
169
  try {
184
- if (!n && o || !u || !u(t, r)) {
170
+ if (!n && u || !o || !o(t, r)) {
185
171
  e.Ue = t;
186
172
  e.Ie = clock;
187
173
  // The latest() shadow write gives latest() effects independent lanes; the
@@ -209,18 +195,18 @@ function handleAsync(e, n, r) {
209
195
  settlePendingSource(e);
210
196
  schedule();
211
197
  flush();
212
- u?.();
198
+ o?.();
213
199
  };
214
- if (u) {
215
- let r = false, t = false, u, l = true;
200
+ if (o) {
201
+ let r = false, t = false, o, l = true;
216
202
  n.then(e => {
217
203
  if (l) {
218
- o = e;
204
+ u = e;
219
205
  r = true;
220
206
  } else asyncWrite(e);
221
207
  }, e => {
222
208
  if (l) {
223
- u = e;
209
+ o = e;
224
210
  t = true;
225
211
  } else handleError(e);
226
212
  });
@@ -229,8 +215,8 @@ function handleAsync(e, n, r) {
229
215
  // Settle through the same status path an async rejection uses, then
230
216
  // unwind the in-progress synchronous read so the errored node isn't
231
217
  // momentarily read as `undefined`.
232
- handleError(u);
233
- throw u;
218
+ handleError(o);
219
+ throw o;
234
220
  } else if (!r) {
235
221
  globalQueue.initTransition(resolveTransition(e));
236
222
  throw new NotReadyError(context);
@@ -239,11 +225,11 @@ function handleAsync(e, n, r) {
239
225
  if (t) {
240
226
  const r = n[Symbol.asyncIterator]();
241
227
  let t = false;
242
- let u = false;
228
+ let o = false;
243
229
  let l = true;
244
230
  cleanup(() => {
245
- if (u) return;
246
- u = true;
231
+ if (o) return;
232
+ o = true;
247
233
  try {
248
234
  const e = r.return?.();
249
235
  if (isThenable(e)) e.then(undefined, () => {});
@@ -255,14 +241,14 @@ function handleAsync(e, n, r) {
255
241
  if (c) {
256
242
  i = r;
257
243
  f = true;
258
- if (r.done) u = true;
244
+ if (r.done) o = true;
259
245
  } else if (e.Ae !== n) {
260
246
  return;
261
247
  } else if (!r.done) {
262
248
  t = true;
263
249
  asyncWrite(r.value, iterate);
264
250
  } else {
265
- u = true;
251
+ o = true;
266
252
  if (t) {
267
253
  schedule();
268
254
  flush();
@@ -276,20 +262,20 @@ function handleAsync(e, n, r) {
276
262
  s = r;
277
263
  a = true;
278
264
  } else if (e.Ae === n) {
279
- u = true;
265
+ o = true;
280
266
  handleError(r);
281
267
  }
282
268
  });
283
269
  c = false;
284
270
  if (a) {
285
271
  // Match the promise branch, but only rethrow during the initial read.
286
- u = true;
272
+ o = true;
287
273
  handleError(s);
288
274
  if (l) throw s;
289
275
  return true;
290
276
  }
291
277
  if (f && !i.done) {
292
- o = i.value;
278
+ u = i.value;
293
279
  t = true;
294
280
  return iterate();
295
281
  }
@@ -303,11 +289,11 @@ function handleAsync(e, n, r) {
303
289
  throw new NotReadyError(context);
304
290
  }
305
291
  }
306
- return o;
292
+ return u;
307
293
  }
308
294
 
309
295
  function clearStatus(e, n = false) {
310
- if (e.m || e.M) clearPendingSources(e);
296
+ if (e.m) clearPendingSources(e);
311
297
  if (e.Re) e.Re = false;
312
298
  // The pending window is over; its quiet classification dies with it.
313
299
  // (Unconditional: _reask is baked into the node literals, so this is a
@@ -317,50 +303,50 @@ function clearStatus(e, n = false) {
317
303
  if (e.k) setPendingError(e);
318
304
  // Update pending signal for isPending() reactivity (companions only exist
319
305
  // once the verdict layer created them, which installs the hooks).
320
- if (e.ye || e.pe) GlobalQueue._(e);
306
+ if (e.ye || e.pe) GlobalQueue.P(e);
321
307
  if (e.G && GlobalQueue.me !== null) GlobalQueue.me(e);
322
308
  if (e.C) e.C();
323
309
  }
324
310
 
325
- function notifyStatus(e, n, r, t, u) {
311
+ function notifyStatus(e, n, r, t, o) {
326
312
  // Wrap regular errors to track source node
327
313
  if (n === STATUS_ERROR && !(r instanceof StatusError) && !(r instanceof NotReadyError)) r = new StatusError(e, r);
328
- const o = n === STATUS_PENDING && r instanceof NotReadyError ? r.source : undefined;
314
+ const u = n === STATUS_PENDING && r instanceof NotReadyError ? r.source : undefined;
329
315
  // Mark-sourced propagation must not capture subscribers into the marking
330
316
  // action's transaction (#2893): they carry no held value needing a
331
317
  // transition-scheduled commit, and stamping `_transition` on them would
332
318
  // freeze unrelated writes that share a downstream memo until the action
333
319
  // settles. Real async keeps queuing (its commits ride the transition).
334
- const l = o?.l !== undefined;
320
+ const l = u?.l !== undefined;
335
321
  // A real error is a settled verdict a mark must not erase (#2893): landing
336
322
  // STATUS_PENDING here would clobber `_error` with the sentinel's
337
323
  // NotReadyError, and unlike real async there is no arriving value whose
338
324
  // recompute would surface the error again. Descent stops too — everything
339
325
  // downstream holds the propagated error for the same reason.
340
326
  if (l && e.i & STATUS_ERROR) return;
341
- const i = o === e;
327
+ const i = u === e;
342
328
  const s = n === STATUS_PENDING && e.be !== undefined && !i;
343
329
  const f = s && hasActiveOverride(e);
344
330
  if (!t) {
345
- if (n === STATUS_PENDING && o) {
346
- addPendingSource(e, o);
331
+ if (n === STATUS_PENDING && u) {
332
+ addPendingSource(e, u);
347
333
  e.i = STATUS_PENDING | e.i & STATUS_UNINITIALIZED;
348
334
  // Preserve the current source on this propagation so render-effect notification
349
335
  // can register every distinct pending source with the transition.
350
- setPendingError(e, o, r);
336
+ setPendingError(e, u, r);
351
337
  } else {
352
338
  clearPendingSources(e);
353
339
  e.i = n | (n !== STATUS_ERROR ? e.i & STATUS_UNINITIALIZED : 0);
354
340
  e.k = r;
355
341
  }
356
- GlobalQueue._ !== null && GlobalQueue._(e);
342
+ GlobalQueue.P !== null && GlobalQueue.P(e);
357
343
  if (e.G && GlobalQueue.me !== null) GlobalQueue.me(e);
358
344
  }
359
- if (u && !t) {
360
- assignOrMergeLane(e, u);
345
+ if (o && !t) {
346
+ assignOrMergeLane(e, o);
361
347
  }
362
348
  const a = t || f;
363
- const c = t || s ? undefined : u;
349
+ const c = t || s ? undefined : o;
364
350
  if (e.C) {
365
351
  if (t && n === STATUS_PENDING) {
366
352
  return;
@@ -374,7 +360,7 @@ function notifyStatus(e, n, r, t, u) {
374
360
  }
375
361
  forEachDependent(e, (e, t) => {
376
362
  e.Ie = clock;
377
- if (n === STATUS_PENDING && o && e.m !== o && !e.M?.has(o) || n !== STATUS_PENDING && (e.k !== r || e.m || e.M)) {
363
+ if (n === STATUS_PENDING && u && !e.m?.has(u) || n !== STATUS_PENDING && (e.k !== r || e.m)) {
378
364
  // A pending-observer link is the subscription an `isPending` read created.
379
365
  // It exists so the observer re-runs when the source settles, but it must
380
366
  // not carry a real (non-NotReadyError) error — the synchronous `isPending`
@@ -61,6 +61,20 @@ const NOT_PENDING = {};
61
61
 
62
62
  const NO_SNAPSHOT = {};
63
63
 
64
+ /**
65
+ * Stand-in stored in `_overrideValue` for an optimistic write of literal
66
+ * `undefined` (#2898). The slot doubles as the optimistic-node brand
67
+ * (`undefined` = not optimistic, `NOT_PENDING` = at rest), so the raw value
68
+ * would erase the node's optimistic identity: the write turns invisible and
69
+ * follow-up writes route off the optimistic path and commit permanently.
70
+ * Same shape as NO_SNAPSHOT. Sites that surface the override VALUE unwrap
71
+ * via `visibleOverrideValue`; slot identity tests stay raw.
72
+ */ const OVERRIDE_UNDEFINED = {};
73
+
74
+ /** Unwrap an active override's stored value for surfacing to readers (#2898). */ function unwrapOverride(E) {
75
+ return E === OVERRIDE_UNDEFINED ? undefined : E;
76
+ }
77
+
64
78
  const STORE_SNAPSHOT_PROPS = "sp";
65
79
 
66
80
  const SUPPORTS_PROXY = typeof Proxy === "function";
@@ -75,4 +89,4 @@ const defaultContext = {};
75
89
  * @internal
76
90
  */ const $REFRESH = Symbol("refresh");
77
91
 
78
- export { $REFRESH, CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, CONFIG_IN_SNAPSHOT_SCOPE, CONFIG_NO_SNAPSHOT, CONFIG_OWNED_WRITE, CONFIG_SYNC, CONFIG_TRANSPARENT, EFFECT_RENDER, EFFECT_TRACKED, EFFECT_USER, NOT_PENDING, NO_SNAPSHOT, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_DISPOSED, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_LAZY, REACTIVE_MANUAL_WRITE, REACTIVE_NONE, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, REACTIVE_SNAPSHOT_STALE, REACTIVE_ZOMBIE, STATUS_ERROR, STATUS_PENDING, STATUS_UNINITIALIZED, STORE_SNAPSHOT_PROPS, SUPPORTS_PROXY, defaultContext };
92
+ export { $REFRESH, CONFIG_AUTO_DISPOSE, CONFIG_CHILDREN_FORBIDDEN, CONFIG_IN_SNAPSHOT_SCOPE, CONFIG_NO_SNAPSHOT, CONFIG_OWNED_WRITE, CONFIG_SYNC, CONFIG_TRANSPARENT, EFFECT_RENDER, EFFECT_TRACKED, EFFECT_USER, NOT_PENDING, NO_SNAPSHOT, OVERRIDE_UNDEFINED, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_DISPOSED, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, REACTIVE_LAZY, REACTIVE_MANUAL_WRITE, REACTIVE_NONE, REACTIVE_OPTIMISTIC_DIRTY, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, REACTIVE_SNAPSHOT_STALE, REACTIVE_ZOMBIE, STATUS_ERROR, STATUS_PENDING, STATUS_UNINITIALIZED, STORE_SNAPSHOT_PROPS, SUPPORTS_PROXY, defaultContext, unwrapOverride };