@solidjs/signals 2.0.0-beta.20 → 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.
- package/README.md +5 -1
- package/dist/dev.js +85 -9
- package/dist/node.cjs +73 -5
- package/dist/prod/core/action.js +47 -9
- package/dist/prod/core/core.js +1 -1
- package/dist/prod/core/heap.js +9 -1
- package/dist/prod/core/scheduler.js +10 -3
- package/dist/prod/core/verdict.js +19 -4
- package/dist/types/core/action.d.ts +16 -0
- package/dist/types-cjs/core/action.d.cts +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ isPending(data); // true while an unrevealed value change is in flight for this
|
|
|
112
112
|
latest(data); // last resolved value; follows the not-ready path if no value has resolved yet
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
Use `action()`
|
|
115
|
+
Use `action()` for mutations — imperative async workflows whose writes span an async gap. Each invocation runs as a single transaction: every write between yields batches into one atomic update, and nothing commits until the action completes or the next `yield` resolves.
|
|
116
116
|
|
|
117
117
|
```typescript
|
|
118
118
|
const save = action(function* (item) {
|
|
@@ -120,6 +120,10 @@ const save = action(function* (item) {
|
|
|
120
120
|
});
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
Navigation-shaped updates don't need an action. A plain setter call is enough — reads pull the async, downstream async computeds hold their previous values until new ones are ready, and `isPending`/`latest` expose the in-flight state. Reach for `action` when writes happen *after* async work (pairing with optimistic primitives for tentative state that reverts on failure), not when the async is merely downstream of a synchronous write.
|
|
124
|
+
|
|
125
|
+
Framework-level actions (router form actions, server actions) are specializations of this primitive: the same transactional semantics with form binding, serialization, and submission tracking layered on top.
|
|
126
|
+
|
|
123
127
|
## Optimistic Updates
|
|
124
128
|
|
|
125
129
|
Optimistic signals show an immediate value while async work is pending, then automatically revert when it settles:
|
package/dist/dev.js
CHANGED
|
@@ -731,8 +731,12 @@ class GlobalQueue extends Queue {
|
|
|
731
731
|
const stashedTransition = activeTransition;
|
|
732
732
|
runHeap(zombieQueue, GlobalQueue._update);
|
|
733
733
|
// Detach: the stashed transition keeps its batch; ambient work that
|
|
734
|
-
// follows lands in a fresh one.
|
|
735
|
-
|
|
734
|
+
// follows lands in a fresh one. If the batch is already a separate
|
|
735
|
+
// ambient one — action done() restored activeTransition without
|
|
736
|
+
// adopting the batch, and an ordinary write landed there before
|
|
737
|
+
// the scheduled flush (#2916) — keep it: replacing it would strand
|
|
738
|
+
// its queued pending nodes with held _pendingValues forever.
|
|
739
|
+
if (this._batch === stashedTransition) currentBatch = this._batch = createBatch();
|
|
736
740
|
// Run lane effects immediately (before stashing) - lanes with no pending async
|
|
737
741
|
if (activeLanes.size) {
|
|
738
742
|
GlobalQueue._runLaneEffects(EFFECT_RENDER);
|
|
@@ -740,7 +744,10 @@ class GlobalQueue extends Queue {
|
|
|
740
744
|
}
|
|
741
745
|
this.stashQueues(stashedTransition._queueStash);
|
|
742
746
|
clock++;
|
|
743
|
-
|
|
747
|
+
// A kept ambient batch may hold pending nodes (#2916): stay
|
|
748
|
+
// scheduled so the outer drain loop commits them via the plain
|
|
749
|
+
// flush path instead of leaving them until the next natural flush.
|
|
750
|
+
scheduled = dirtyQueue._max >= dirtyQueue._min || this._batch._pendingNodes.length > 0;
|
|
744
751
|
reassignPendingTransition(stashedTransition._pendingNodes);
|
|
745
752
|
activeTransition = null;
|
|
746
753
|
// The stash pass (committed-view rerun of plain optimistic signals)
|
|
@@ -1156,7 +1163,15 @@ function insertIntoHeap(n, heap) {
|
|
|
1156
1163
|
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
|
|
1157
1164
|
if (flags & REACTIVE_CHECK) {
|
|
1158
1165
|
n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
1159
|
-
} else
|
|
1166
|
+
} else {
|
|
1167
|
+
n._flags = flags | REACTIVE_IN_HEAP;
|
|
1168
|
+
// An unmarked node entering a marked heap invalidates the markHeap memo:
|
|
1169
|
+
// `_marked` is only reset by runHeap, so a write between two mid-tick
|
|
1170
|
+
// pulls (read-time markHeap + updateIfNecessary) would otherwise leave
|
|
1171
|
+
// this node unmarked and every downstream pull stale until the next
|
|
1172
|
+
// flush (#2922: the second `latest()` returned the first write's value).
|
|
1173
|
+
if (heap._marked && !(flags & REACTIVE_DIRTY)) heap._marked = false;
|
|
1174
|
+
}
|
|
1160
1175
|
if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
|
|
1161
1176
|
}
|
|
1162
1177
|
function insertIntoHeapHeight(n, heap) {
|
|
@@ -3557,6 +3572,19 @@ function latestRead(el) {
|
|
|
3557
3572
|
: el._value;
|
|
3558
3573
|
let value;
|
|
3559
3574
|
try {
|
|
3575
|
+
// An untracked latest() read has no reading context, so read() never
|
|
3576
|
+
// performs its mid-tick pull — a plain write queued between two latest()
|
|
3577
|
+
// calls left a still-subscribed shadow at its previous speculative value
|
|
3578
|
+
// until the flush (#2922). Mirror the tracked-read pull here: mark the
|
|
3579
|
+
// queued staleness through the graph, then bring the shadow up to date.
|
|
3580
|
+
const queue = queueFor(pendingComputed);
|
|
3581
|
+
if (
|
|
3582
|
+
pendingComputed._height >= queue._min &&
|
|
3583
|
+
!(pendingComputed._flags & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))
|
|
3584
|
+
) {
|
|
3585
|
+
markHeap(queue);
|
|
3586
|
+
prepareComputed(pendingComputed, true);
|
|
3587
|
+
}
|
|
3560
3588
|
value = read(pendingComputed);
|
|
3561
3589
|
} catch (e) {
|
|
3562
3590
|
if (e instanceof NotReadyError && (!context || !(el._statusFlags & STATUS_UNINITIALIZED)))
|
|
@@ -3573,6 +3601,16 @@ function latestRead(el) {
|
|
|
3573
3601
|
return visibleValue;
|
|
3574
3602
|
}
|
|
3575
3603
|
}
|
|
3604
|
+
// A shadow recomputed by the pull above (not at creation) holds its fresh
|
|
3605
|
+
// speculative value in _pendingValue; a contextless read() only surfaces
|
|
3606
|
+
// _value. Overrides stay authoritative (A17), and stale readers keep the
|
|
3607
|
+
// other transition's committed view, matching read()'s own selection.
|
|
3608
|
+
if (
|
|
3609
|
+
pendingComputed._pendingValue !== NOT_PENDING &&
|
|
3610
|
+
!hasActiveOverride(pendingComputed) &&
|
|
3611
|
+
!(stale && pendingComputed._transition && activeTransition !== pendingComputed._transition)
|
|
3612
|
+
)
|
|
3613
|
+
return pendingComputed._pendingValue;
|
|
3576
3614
|
return value;
|
|
3577
3615
|
}
|
|
3578
3616
|
/** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
|
|
@@ -3882,6 +3920,22 @@ function restoreTransition(transition, fn) {
|
|
|
3882
3920
|
return result;
|
|
3883
3921
|
}
|
|
3884
3922
|
/**
|
|
3923
|
+
* The primitive for mutations: imperative async workflows whose *writes span
|
|
3924
|
+
* an async gap* — optimistic write, server round-trip, reconciling write —
|
|
3925
|
+
* where intermediate state must not leak and failure must revert cleanly
|
|
3926
|
+
* (pair with `createOptimistic` / `createOptimisticStore`).
|
|
3927
|
+
*
|
|
3928
|
+
* Navigation-shaped updates do not need an action. A plain setter call is
|
|
3929
|
+
* enough: reads pull the async, and downstream async computeds hold their
|
|
3930
|
+
* previous values per-node until the new ones are ready (`isPending` /
|
|
3931
|
+
* `latest` expose the in-flight state). Reach for `action` only when writes
|
|
3932
|
+
* happen *after* async work, not merely upstream of it.
|
|
3933
|
+
*
|
|
3934
|
+
* Framework-level actions (router form actions, server actions) are
|
|
3935
|
+
* specializations of this primitive: they are actions in exactly this sense —
|
|
3936
|
+
* the same transactional semantics — with form binding, serialization, and
|
|
3937
|
+
* submission tracking layered on top. The shared name is deliberate.
|
|
3938
|
+
*
|
|
3885
3939
|
* Wraps a generator function so each invocation runs as a single transaction
|
|
3886
3940
|
* (a "transition") that batches every signal/store write between yields. The
|
|
3887
3941
|
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
|
@@ -3980,11 +4034,33 @@ function action(genFn) {
|
|
|
3980
4034
|
};
|
|
3981
4035
|
const run = r => {
|
|
3982
4036
|
if (r.done) return done(r.value);
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
4037
|
+
// Thenable assimilation can itself throw synchronously (a `then`
|
|
4038
|
+
// getter, or a `then()` method that throws — #2918). Match `await`
|
|
4039
|
+
// semantics: the failure is thrown back into the generator at the
|
|
4040
|
+
// yield point (catchable there); if uncaught, step()'s guard settles
|
|
4041
|
+
// the action so its iterator never leaks in the transition. The
|
|
4042
|
+
// settled flag implements A+ 2.3.3.3.4.1: a throw after the thenable
|
|
4043
|
+
// already called a callback is ignored.
|
|
4044
|
+
let settled = false;
|
|
4045
|
+
try {
|
|
4046
|
+
if (isThenable(r.value))
|
|
4047
|
+
return void r.value.then(
|
|
4048
|
+
v => {
|
|
4049
|
+
if (settled) return;
|
|
4050
|
+
settled = true;
|
|
4051
|
+
restoreTransition(ctx, () => step(v));
|
|
4052
|
+
},
|
|
4053
|
+
e => {
|
|
4054
|
+
if (settled) return;
|
|
4055
|
+
settled = true;
|
|
4056
|
+
restoreTransition(ctx, () => step(e, true));
|
|
4057
|
+
}
|
|
4058
|
+
);
|
|
4059
|
+
} catch (e) {
|
|
4060
|
+
if (settled) return;
|
|
4061
|
+
settled = true;
|
|
4062
|
+
return void restoreTransition(ctx, () => step(e, true));
|
|
4063
|
+
}
|
|
3988
4064
|
restoreTransition(ctx, () => step(r.value));
|
|
3989
4065
|
};
|
|
3990
4066
|
step();
|
package/dist/node.cjs
CHANGED
|
@@ -634,8 +634,12 @@ class GlobalQueue extends Queue {
|
|
|
634
634
|
const e = activeTransition;
|
|
635
635
|
runHeap(zombieQueue, GlobalQueue.X);
|
|
636
636
|
// Detach: the stashed transition keeps its batch; ambient work that
|
|
637
|
-
// follows lands in a fresh one.
|
|
638
|
-
|
|
637
|
+
// follows lands in a fresh one. If the batch is already a separate
|
|
638
|
+
// ambient one — action done() restored activeTransition without
|
|
639
|
+
// adopting the batch, and an ordinary write landed there before
|
|
640
|
+
// the scheduled flush (#2916) — keep it: replacing it would strand
|
|
641
|
+
// its queued pending nodes with held _pendingValues forever.
|
|
642
|
+
if (this.D === e) currentBatch = this.D = createBatch();
|
|
639
643
|
// Run lane effects immediately (before stashing) - lanes with no pending async
|
|
640
644
|
if (activeLanes.size) {
|
|
641
645
|
GlobalQueue.Ce(EFFECT_RENDER);
|
|
@@ -643,7 +647,10 @@ class GlobalQueue extends Queue {
|
|
|
643
647
|
}
|
|
644
648
|
this.stashQueues(e.K);
|
|
645
649
|
clock++;
|
|
646
|
-
|
|
650
|
+
// A kept ambient batch may hold pending nodes (#2916): stay
|
|
651
|
+
// scheduled so the outer drain loop commits them via the plain
|
|
652
|
+
// flush path instead of leaving them until the next natural flush.
|
|
653
|
+
scheduled = dirtyQueue.C >= dirtyQueue.P || this.D.H.length > 0;
|
|
647
654
|
reassignPendingTransition(e.H);
|
|
648
655
|
activeTransition = null;
|
|
649
656
|
// The stash pass (committed-view rerun of plain optimistic signals)
|
|
@@ -1038,7 +1045,15 @@ function insertIntoHeap(e, t) {
|
|
|
1038
1045
|
if (n & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
|
|
1039
1046
|
if (n & REACTIVE_CHECK) {
|
|
1040
1047
|
e.Qe = n & -4 | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
1041
|
-
} else
|
|
1048
|
+
} else {
|
|
1049
|
+
e.Qe = n | REACTIVE_IN_HEAP;
|
|
1050
|
+
// An unmarked node entering a marked heap invalidates the markHeap memo:
|
|
1051
|
+
// `_marked` is only reset by runHeap, so a write between two mid-tick
|
|
1052
|
+
// pulls (read-time markHeap + updateIfNecessary) would otherwise leave
|
|
1053
|
+
// this node unmarked and every downstream pull stale until the next
|
|
1054
|
+
// flush (#2922: the second `latest()` returned the first write's value).
|
|
1055
|
+
if (t.N && !(n & REACTIVE_DIRTY)) t.N = false;
|
|
1056
|
+
}
|
|
1042
1057
|
if (!(n & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(e, t);
|
|
1043
1058
|
}
|
|
1044
1059
|
|
|
@@ -3174,6 +3189,16 @@ function getLatestValueComputed(e) {
|
|
|
3174
3189
|
const r = e.A !== undefined && e.A !== NOT_PENDING ? unwrapOverride(e.A) : e.Me;
|
|
3175
3190
|
let i;
|
|
3176
3191
|
try {
|
|
3192
|
+
// An untracked latest() read has no reading context, so read() never
|
|
3193
|
+
// performs its mid-tick pull — a plain write queued between two latest()
|
|
3194
|
+
// calls left a still-subscribed shadow at its previous speculative value
|
|
3195
|
+
// until the flush (#2922). Mirror the tracked-read pull here: mark the
|
|
3196
|
+
// queued staleness through the graph, then bring the shadow up to date.
|
|
3197
|
+
const e = queueFor(t);
|
|
3198
|
+
if (t.nt >= e.P && !(t.Qe & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
|
|
3199
|
+
markHeap(e);
|
|
3200
|
+
prepareComputed(t, true);
|
|
3201
|
+
}
|
|
3177
3202
|
i = read(t);
|
|
3178
3203
|
} catch (t) {
|
|
3179
3204
|
if (t instanceof NotReadyError && (!context || !(e.$e & STATUS_UNINITIALIZED))) return r;
|
|
@@ -3189,6 +3214,11 @@ function getLatestValueComputed(e) {
|
|
|
3189
3214
|
return r;
|
|
3190
3215
|
}
|
|
3191
3216
|
}
|
|
3217
|
+
// A shadow recomputed by the pull above (not at creation) holds its fresh
|
|
3218
|
+
// speculative value in _pendingValue; a contextless read() only surfaces
|
|
3219
|
+
// _value. Overrides stay authoritative (A17), and stale readers keep the
|
|
3220
|
+
// other transition's committed view, matching read()'s own selection.
|
|
3221
|
+
if (t.V !== NOT_PENDING && !hasActiveOverride(t) && !(stale && t.T && activeTransition !== t.T)) return t.V;
|
|
3192
3222
|
return i;
|
|
3193
3223
|
}
|
|
3194
3224
|
|
|
@@ -3432,6 +3462,22 @@ function restoreTransition(e, t) {
|
|
|
3432
3462
|
}
|
|
3433
3463
|
|
|
3434
3464
|
/**
|
|
3465
|
+
* The primitive for mutations: imperative async workflows whose *writes span
|
|
3466
|
+
* an async gap* — optimistic write, server round-trip, reconciling write —
|
|
3467
|
+
* where intermediate state must not leak and failure must revert cleanly
|
|
3468
|
+
* (pair with `createOptimistic` / `createOptimisticStore`).
|
|
3469
|
+
*
|
|
3470
|
+
* Navigation-shaped updates do not need an action. A plain setter call is
|
|
3471
|
+
* enough: reads pull the async, and downstream async computeds hold their
|
|
3472
|
+
* previous values per-node until the new ones are ready (`isPending` /
|
|
3473
|
+
* `latest` expose the in-flight state). Reach for `action` only when writes
|
|
3474
|
+
* happen *after* async work, not merely upstream of it.
|
|
3475
|
+
*
|
|
3476
|
+
* Framework-level actions (router form actions, server actions) are
|
|
3477
|
+
* specializations of this primitive: they are actions in exactly this sense —
|
|
3478
|
+
* the same transactional semantics — with form binding, serialization, and
|
|
3479
|
+
* submission tracking layered on top. The shared name is deliberate.
|
|
3480
|
+
*
|
|
3435
3481
|
* Wraps a generator function so each invocation runs as a single transaction
|
|
3436
3482
|
* (a "transition") that batches every signal/store write between yields. The
|
|
3437
3483
|
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
|
@@ -3506,7 +3552,29 @@ function restoreTransition(e, t) {
|
|
|
3506
3552
|
};
|
|
3507
3553
|
const run = e => {
|
|
3508
3554
|
if (e.done) return done(e.value);
|
|
3509
|
-
|
|
3555
|
+
// Thenable assimilation can itself throw synchronously (a `then`
|
|
3556
|
+
// getter, or a `then()` method that throws — #2918). Match `await`
|
|
3557
|
+
// semantics: the failure is thrown back into the generator at the
|
|
3558
|
+
// yield point (catchable there); if uncaught, step()'s guard settles
|
|
3559
|
+
// the action so its iterator never leaks in the transition. The
|
|
3560
|
+
// settled flag implements A+ 2.3.3.3.4.1: a throw after the thenable
|
|
3561
|
+
// already called a callback is ignored.
|
|
3562
|
+
let t = false;
|
|
3563
|
+
try {
|
|
3564
|
+
if (isThenable(e.value)) return void e.value.then(e => {
|
|
3565
|
+
if (t) return;
|
|
3566
|
+
t = true;
|
|
3567
|
+
restoreTransition(o, () => step(e));
|
|
3568
|
+
}, e => {
|
|
3569
|
+
if (t) return;
|
|
3570
|
+
t = true;
|
|
3571
|
+
restoreTransition(o, () => step(e, true));
|
|
3572
|
+
});
|
|
3573
|
+
} catch (e) {
|
|
3574
|
+
if (t) return;
|
|
3575
|
+
t = true;
|
|
3576
|
+
return void restoreTransition(o, () => step(e, true));
|
|
3577
|
+
}
|
|
3510
3578
|
restoreTransition(o, () => step(e.value));
|
|
3511
3579
|
};
|
|
3512
3580
|
step();
|
package/dist/prod/core/action.js
CHANGED
|
@@ -4,14 +4,30 @@ import { isThenable } from "./async.js";
|
|
|
4
4
|
|
|
5
5
|
import "./core.js";
|
|
6
6
|
|
|
7
|
-
function restoreTransition(e,
|
|
7
|
+
function restoreTransition(e, r) {
|
|
8
8
|
globalQueue.initTransition(e);
|
|
9
|
-
const t =
|
|
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
|
|
@@ -58,23 +74,23 @@ function restoreTransition(e, n) {
|
|
|
58
74
|
* await addTodo("buy milk");
|
|
59
75
|
* ```
|
|
60
76
|
*/ function action(e) {
|
|
61
|
-
return (...
|
|
62
|
-
const i = e(...
|
|
77
|
+
return (...r) => new Promise((t, n) => {
|
|
78
|
+
const i = e(...r);
|
|
63
79
|
globalQueue.initTransition();
|
|
64
80
|
let o = activeTransition;
|
|
65
81
|
o.Te.push(i);
|
|
66
|
-
const done = (e,
|
|
82
|
+
const done = (e, r, s = false) => {
|
|
67
83
|
o = currentTransition(o);
|
|
68
84
|
const u = o.Te.indexOf(i);
|
|
69
85
|
if (u >= 0) o.Te.splice(u, 1);
|
|
70
86
|
setActiveTransition(o);
|
|
71
87
|
schedule();
|
|
72
|
-
s ? r
|
|
88
|
+
s ? n(r) : t(e);
|
|
73
89
|
};
|
|
74
|
-
const step = (e,
|
|
90
|
+
const step = (e, r) => {
|
|
75
91
|
let t;
|
|
76
92
|
try {
|
|
77
|
-
t =
|
|
93
|
+
t = r ? i.throw(e) : i.next(e);
|
|
78
94
|
} catch (e) {
|
|
79
95
|
return done(undefined, e, true);
|
|
80
96
|
}
|
|
@@ -86,7 +102,29 @@ function restoreTransition(e, n) {
|
|
|
86
102
|
};
|
|
87
103
|
const run = e => {
|
|
88
104
|
if (e.done) return done(e.value);
|
|
89
|
-
|
|
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
|
+
}
|
|
90
128
|
restoreTransition(o, () => step(e.value));
|
|
91
129
|
};
|
|
92
130
|
step();
|
package/dist/prod/core/core.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { handleAsync, clearStatus, notifyStatus } from "./async.js";
|
|
2
2
|
|
|
3
|
-
import { EFFECT_TRACKED, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, STATUS_UNINITIALIZED, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, CONFIG_SYNC, STATUS_PENDING, STATUS_ERROR, REACTIVE_NONE, REACTIVE_SNAPSHOT_STALE, unwrapOverride, OVERRIDE_UNDEFINED,
|
|
3
|
+
import { EFFECT_TRACKED, REACTIVE_OPTIMISTIC_DIRTY, NOT_PENDING, STATUS_UNINITIALIZED, REACTIVE_REASK, REACTIVE_RECOMPUTING_DEPS, CONFIG_SYNC, STATUS_PENDING, STATUS_ERROR, REACTIVE_NONE, REACTIVE_SNAPSHOT_STALE, unwrapOverride, OVERRIDE_UNDEFINED, REACTIVE_LAZY, REACTIVE_DISPOSED, CONFIG_AUTO_DISPOSE, REACTIVE_CHECK, REACTIVE_DIRTY, REACTIVE_IN_HEAP, REACTIVE_IN_HEAP_HEIGHT, defaultContext, CONFIG_IN_SNAPSHOT_SCOPE, CONFIG_TRANSPARENT, CONFIG_OWNED_WRITE, CONFIG_NO_SNAPSHOT, $REFRESH, REACTIVE_MANUAL_WRITE, NO_SNAPSHOT, STORE_SNAPSHOT_PROPS, EFFECT_USER } from "./constants.js";
|
|
4
4
|
|
|
5
5
|
import { NotReadyError } from "./error.js";
|
|
6
6
|
|
package/dist/prod/core/heap.js
CHANGED
|
@@ -43,7 +43,15 @@ function insertIntoHeap(e, E) {
|
|
|
43
43
|
if (t & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
|
|
44
44
|
if (t & REACTIVE_CHECK) {
|
|
45
45
|
e.u = t & -4 | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
46
|
-
} else
|
|
46
|
+
} else {
|
|
47
|
+
e.u = t | REACTIVE_IN_HEAP;
|
|
48
|
+
// An unmarked node entering a marked heap invalidates the markHeap memo:
|
|
49
|
+
// `_marked` is only reset by runHeap, so a write between two mid-tick
|
|
50
|
+
// pulls (read-time markHeap + updateIfNecessary) would otherwise leave
|
|
51
|
+
// this node unmarked and every downstream pull stale until the next
|
|
52
|
+
// flush (#2922: the second `latest()` returned the first write's value).
|
|
53
|
+
if (E.tE && !(t & REACTIVE_DIRTY)) E.tE = false;
|
|
54
|
+
}
|
|
47
55
|
if (!(t & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(e, E);
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -323,8 +323,12 @@ class GlobalQueue extends Queue {
|
|
|
323
323
|
const e = activeTransition;
|
|
324
324
|
runHeap(zombieQueue, GlobalQueue.Ce);
|
|
325
325
|
// Detach: the stashed transition keeps its batch; ambient work that
|
|
326
|
-
// follows lands in a fresh one.
|
|
327
|
-
|
|
326
|
+
// follows lands in a fresh one. If the batch is already a separate
|
|
327
|
+
// ambient one — action done() restored activeTransition without
|
|
328
|
+
// adopting the batch, and an ordinary write landed there before
|
|
329
|
+
// the scheduled flush (#2916) — keep it: replacing it would strand
|
|
330
|
+
// its queued pending nodes with held _pendingValues forever.
|
|
331
|
+
if (this.N === e) currentBatch = this.N = createBatch();
|
|
328
332
|
// Run lane effects immediately (before stashing) - lanes with no pending async
|
|
329
333
|
if (activeLanes.size) {
|
|
330
334
|
GlobalQueue.On(EFFECT_RENDER);
|
|
@@ -332,7 +336,10 @@ class GlobalQueue extends Queue {
|
|
|
332
336
|
}
|
|
333
337
|
this.stashQueues(e.Bt);
|
|
334
338
|
clock++;
|
|
335
|
-
|
|
339
|
+
// A kept ambient batch may hold pending nodes (#2916): stay
|
|
340
|
+
// scheduled so the outer drain loop commits them via the plain
|
|
341
|
+
// flush path instead of leaving them until the next natural flush.
|
|
342
|
+
scheduled = dirtyQueue.EE >= dirtyQueue.Le || this.N.yt.length > 0;
|
|
336
343
|
reassignPendingTransition(e.yt);
|
|
337
344
|
activeTransition = null;
|
|
338
345
|
// The stash pass (committed-view rerun of plain optimistic signals)
|
|
@@ -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 } from "./constants.js";
|
|
2
2
|
|
|
3
|
-
import { setSignal, read, context, stale, currentOptimisticLane,
|
|
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 } from "./scheduler.js";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* The isPending()/latest() verdict layer, moved out of core.ts. Importing this
|
|
@@ -175,6 +175,16 @@ function getLatestValueComputed(e) {
|
|
|
175
175
|
const i = e.be !== undefined && e.be !== NOT_PENDING ? unwrapOverride(e.be) : e.Ue;
|
|
176
176
|
let r;
|
|
177
177
|
try {
|
|
178
|
+
// An untracked latest() read has no reading context, so read() never
|
|
179
|
+
// performs its mid-tick pull — a plain write queued between two latest()
|
|
180
|
+
// calls left a still-subscribed shadow at its previous speculative value
|
|
181
|
+
// until the flush (#2922). Mirror the tracked-read pull here: mark the
|
|
182
|
+
// queued staleness through the graph, then bring the shadow up to date.
|
|
183
|
+
const e = queueFor(t);
|
|
184
|
+
if (t.qe >= e.Le && !(t.u & (REACTIVE_DISPOSED | REACTIVE_ZOMBIE))) {
|
|
185
|
+
markHeap(e);
|
|
186
|
+
prepareComputed(t, true);
|
|
187
|
+
}
|
|
178
188
|
r = read(t);
|
|
179
189
|
} catch (t) {
|
|
180
190
|
if (t instanceof NotReadyError && (!context || !(e.i & STATUS_UNINITIALIZED))) return i;
|
|
@@ -190,6 +200,11 @@ function getLatestValueComputed(e) {
|
|
|
190
200
|
return i;
|
|
191
201
|
}
|
|
192
202
|
}
|
|
203
|
+
// A shadow recomputed by the pull above (not at creation) holds its fresh
|
|
204
|
+
// speculative value in _pendingValue; a contextless read() only surfaces
|
|
205
|
+
// _value. Overrides stay authoritative (A17), and stale readers keep the
|
|
206
|
+
// other transition's committed view, matching read()'s own selection.
|
|
207
|
+
if (t.ge !== NOT_PENDING && !hasActiveOverride(t) && !(stale && t.ve && activeTransition !== t.ve)) return t.ge;
|
|
193
208
|
return r;
|
|
194
209
|
}
|
|
195
210
|
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* The primitive for mutations: imperative async workflows whose *writes span
|
|
3
|
+
* an async gap* — optimistic write, server round-trip, reconciling write —
|
|
4
|
+
* where intermediate state must not leak and failure must revert cleanly
|
|
5
|
+
* (pair with `createOptimistic` / `createOptimisticStore`).
|
|
6
|
+
*
|
|
7
|
+
* Navigation-shaped updates do not need an action. A plain setter call is
|
|
8
|
+
* enough: reads pull the async, and downstream async computeds hold their
|
|
9
|
+
* previous values per-node until the new ones are ready (`isPending` /
|
|
10
|
+
* `latest` expose the in-flight state). Reach for `action` only when writes
|
|
11
|
+
* happen *after* async work, not merely upstream of it.
|
|
12
|
+
*
|
|
13
|
+
* Framework-level actions (router form actions, server actions) are
|
|
14
|
+
* specializations of this primitive: they are actions in exactly this sense —
|
|
15
|
+
* the same transactional semantics — with form binding, serialization, and
|
|
16
|
+
* submission tracking layered on top. The shared name is deliberate.
|
|
17
|
+
*
|
|
2
18
|
* Wraps a generator function so each invocation runs as a single transaction
|
|
3
19
|
* (a "transition") that batches every signal/store write between yields. The
|
|
4
20
|
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* The primitive for mutations: imperative async workflows whose *writes span
|
|
3
|
+
* an async gap* — optimistic write, server round-trip, reconciling write —
|
|
4
|
+
* where intermediate state must not leak and failure must revert cleanly
|
|
5
|
+
* (pair with `createOptimistic` / `createOptimisticStore`).
|
|
6
|
+
*
|
|
7
|
+
* Navigation-shaped updates do not need an action. A plain setter call is
|
|
8
|
+
* enough: reads pull the async, and downstream async computeds hold their
|
|
9
|
+
* previous values per-node until the new ones are ready (`isPending` /
|
|
10
|
+
* `latest` expose the in-flight state). Reach for `action` only when writes
|
|
11
|
+
* happen *after* async work, not merely upstream of it.
|
|
12
|
+
*
|
|
13
|
+
* Framework-level actions (router form actions, server actions) are
|
|
14
|
+
* specializations of this primitive: they are actions in exactly this sense —
|
|
15
|
+
* the same transactional semantics — with form binding, serialization, and
|
|
16
|
+
* submission tracking layered on top. The shared name is deliberate.
|
|
17
|
+
*
|
|
2
18
|
* Wraps a generator function so each invocation runs as a single transaction
|
|
3
19
|
* (a "transition") that batches every signal/store write between yields. The
|
|
4
20
|
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
package/package.json
CHANGED