@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.
- package/README.md +5 -1
- package/dist/dev.js +753 -378
- package/dist/node.cjs +1947 -1607
- package/dist/prod/affects.js +17 -21
- package/dist/prod/core/action.js +66 -15
- package/dist/prod/core/async.js +57 -71
- package/dist/prod/core/constants.js +15 -1
- package/dist/prod/core/core.js +39 -39
- package/dist/prod/core/effect.js +14 -14
- package/dist/prod/core/graph.js +1 -1
- package/dist/prod/core/heap.js +11 -3
- package/dist/prod/core/lanes.js +23 -13
- package/dist/prod/core/optimistic.js +107 -99
- package/dist/prod/core/owner.js +23 -23
- package/dist/prod/core/scheduler.js +188 -181
- package/dist/prod/core/verdict.js +29 -15
- package/dist/prod/map.js +196 -167
- package/dist/prod/signals.js +1 -1
- package/dist/prod/store/optimistic.js +103 -68
- package/dist/prod/store/reconcile.js +80 -46
- package/dist/prod/store/store.js +204 -73
- package/dist/prod/store/utils.js +61 -42
- package/dist/types/core/action.d.ts +35 -6
- package/dist/types/core/constants.d.ts +12 -0
- package/dist/types/core/dev.d.ts +7 -0
- package/dist/types/core/lanes.d.ts +2 -0
- package/dist/types/core/scheduler.d.ts +2 -6
- package/dist/types/core/types.d.ts +9 -1
- package/dist/types/store/store.d.ts +8 -2
- package/dist/types-cjs/core/action.d.cts +35 -6
- package/dist/types-cjs/core/constants.d.cts +12 -0
- package/dist/types-cjs/core/dev.d.cts +7 -0
- package/dist/types-cjs/core/lanes.d.cts +2 -0
- package/dist/types-cjs/core/scheduler.d.cts +2 -6
- package/dist/types-cjs/core/types.d.cts +9 -1
- package/dist/types-cjs/store/store.d.cts +8 -2
- package/package.json +1 -1
package/dist/dev.js
CHANGED
|
@@ -91,6 +91,20 @@ const EFFECT_USER = 2;
|
|
|
91
91
|
const EFFECT_TRACKED = 3;
|
|
92
92
|
const NOT_PENDING = {};
|
|
93
93
|
const NO_SNAPSHOT = {};
|
|
94
|
+
/**
|
|
95
|
+
* Stand-in stored in `_overrideValue` for an optimistic write of literal
|
|
96
|
+
* `undefined` (#2898). The slot doubles as the optimistic-node brand
|
|
97
|
+
* (`undefined` = not optimistic, `NOT_PENDING` = at rest), so the raw value
|
|
98
|
+
* would erase the node's optimistic identity: the write turns invisible and
|
|
99
|
+
* follow-up writes route off the optimistic path and commit permanently.
|
|
100
|
+
* Same shape as NO_SNAPSHOT. Sites that surface the override VALUE unwrap
|
|
101
|
+
* via `visibleOverrideValue`; slot identity tests stay raw.
|
|
102
|
+
*/
|
|
103
|
+
const OVERRIDE_UNDEFINED = {};
|
|
104
|
+
/** Unwrap an active override's stored value for surfacing to readers (#2898). */
|
|
105
|
+
function unwrapOverride(v) {
|
|
106
|
+
return v === OVERRIDE_UNDEFINED ? undefined : v;
|
|
107
|
+
}
|
|
94
108
|
const STORE_SNAPSHOT_PROPS = "sp";
|
|
95
109
|
const SUPPORTS_PROXY = typeof Proxy === "function";
|
|
96
110
|
const defaultContext = {};
|
|
@@ -147,6 +161,39 @@ function emitDiagnostic(event) {
|
|
|
147
161
|
for (const capture of diagnosticCaptures) capture.push(entry);
|
|
148
162
|
return entry;
|
|
149
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Shared strict-read diagnostics for core read() and the store proxy traps.
|
|
166
|
+
* Single source for the message text — the #2897 safeguard parity between
|
|
167
|
+
* memos and stores is exactly these firing identically from both paths.
|
|
168
|
+
*/
|
|
169
|
+
function throwPendingUntrackedRead(strictReadLabel, fields) {
|
|
170
|
+
const message =
|
|
171
|
+
`[PENDING_ASYNC_UNTRACKED_READ] Reading a pending async value directly in ${strictReadLabel}. ` +
|
|
172
|
+
`Async values must be read within a tracking scope (JSX, a memo, or an effect's compute function).`;
|
|
173
|
+
emitDiagnostic({
|
|
174
|
+
code: "PENDING_ASYNC_UNTRACKED_READ",
|
|
175
|
+
kind: "async",
|
|
176
|
+
severity: "error",
|
|
177
|
+
message,
|
|
178
|
+
...fields,
|
|
179
|
+
data: { strictRead: strictReadLabel }
|
|
180
|
+
});
|
|
181
|
+
throw new Error(message);
|
|
182
|
+
}
|
|
183
|
+
function warnStrictReadUntracked(strictReadLabel, fields) {
|
|
184
|
+
const message =
|
|
185
|
+
`[STRICT_READ_UNTRACKED] Reactive value read directly in ${strictReadLabel} will not update. ` +
|
|
186
|
+
`Move it into a tracking scope (JSX, a memo, or an effect's compute function).`;
|
|
187
|
+
emitDiagnostic({
|
|
188
|
+
code: "STRICT_READ_UNTRACKED",
|
|
189
|
+
kind: "strict-read",
|
|
190
|
+
severity: "warn",
|
|
191
|
+
message,
|
|
192
|
+
data: { strictRead: strictReadLabel },
|
|
193
|
+
...fields
|
|
194
|
+
});
|
|
195
|
+
console.warn(message);
|
|
196
|
+
}
|
|
150
197
|
function registerGraph(value, owner) {
|
|
151
198
|
value._owner = owner;
|
|
152
199
|
if (owner) {
|
|
@@ -320,6 +367,16 @@ function resolveLane(el) {
|
|
|
320
367
|
return undefined;
|
|
321
368
|
}
|
|
322
369
|
function resolveTransition(el) {
|
|
370
|
+
// An active override answers with its owner, not its lane: lanes are
|
|
371
|
+
// scheduling affinity and a shared subscriber merges them across
|
|
372
|
+
// transactions (#2912) — the merged root's _transition would hand this
|
|
373
|
+
// node's override to whichever action wrote last through the shared
|
|
374
|
+
// reader. Chase merge chains; a dead owner settled through another path.
|
|
375
|
+
if (hasActiveOverride(el) && el._overrideOwner) {
|
|
376
|
+
const owner = (el._overrideOwner = currentTransition(el._overrideOwner));
|
|
377
|
+
if (owner._done !== true) return owner;
|
|
378
|
+
el._overrideOwner = null;
|
|
379
|
+
}
|
|
323
380
|
return resolveLane(el)?._transition ?? el._transition;
|
|
324
381
|
}
|
|
325
382
|
/**
|
|
@@ -391,13 +448,14 @@ function registerTransientStoreNode(node) {
|
|
|
391
448
|
transientStoreNodes.add(node);
|
|
392
449
|
}
|
|
393
450
|
function canUseSimpleSyncFlush(queue) {
|
|
451
|
+
const batch = queue._batch;
|
|
394
452
|
return (
|
|
395
453
|
transitions.size === 0 &&
|
|
396
454
|
activeLanes.size === 0 &&
|
|
397
455
|
queue._children.length === 0 &&
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
456
|
+
batch._optimisticNodes.length === 0 &&
|
|
457
|
+
batch._affectsNodes.length === 0 &&
|
|
458
|
+
batch._optimisticStores.size === 0 &&
|
|
401
459
|
transientStoreNodes.size === 0
|
|
402
460
|
);
|
|
403
461
|
}
|
|
@@ -438,20 +496,43 @@ function setProjectionWriteActive(value) {
|
|
|
438
496
|
function setTrackedQueueCallback(value) {
|
|
439
497
|
inTrackedQueueCallback = value;
|
|
440
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Ambient work IS a transaction: the global queue always carries one
|
|
501
|
+
* current-transaction-shaped batch (`globalQueue._batch`). With no transition
|
|
502
|
+
* active, registrations (pending commits, optimistic nodes, affects marks,
|
|
503
|
+
* optimistic stores) land in a plain ambient batch that the plain flush
|
|
504
|
+
* finalizes; when a transition initializes it adopts the ambient batch's
|
|
505
|
+
* contents and `_batch` becomes the transition itself, so later registrations
|
|
506
|
+
* land there directly — no per-field aliasing.
|
|
507
|
+
*/
|
|
508
|
+
function createBatch() {
|
|
509
|
+
return {
|
|
510
|
+
_time: clock,
|
|
511
|
+
_pendingNodes: [],
|
|
512
|
+
_asyncReporters: createAsyncReporters(),
|
|
513
|
+
_optimisticNodes: [],
|
|
514
|
+
_affectsNodes: [],
|
|
515
|
+
_optimisticStores: new Set(),
|
|
516
|
+
_actions: [],
|
|
517
|
+
_queueStash: { _queues: [[], []], _children: [] },
|
|
518
|
+
_done: false,
|
|
519
|
+
_gatedSubs: new Set()
|
|
520
|
+
};
|
|
521
|
+
}
|
|
441
522
|
function mergeTransitionState(target, outgoing) {
|
|
442
523
|
outgoing._done = target;
|
|
443
524
|
target._actions.push(...outgoing._actions);
|
|
444
525
|
for (const lane of activeLanes) if (lane._transition === outgoing) lane._transition = target;
|
|
445
526
|
if (outgoing._optimisticNodes.length) {
|
|
446
|
-
// Move (don't copy): the global queue may still
|
|
447
|
-
//
|
|
527
|
+
// Move (don't copy): the global queue's batch may still be the outgoing
|
|
528
|
+
// transition, and the adoption pass in initTransition would re-push its
|
|
448
529
|
// contents into the target — duplicating every entry.
|
|
449
530
|
target._optimisticNodes.push(...outgoing._optimisticNodes);
|
|
450
531
|
outgoing._optimisticNodes.length = 0;
|
|
451
532
|
}
|
|
452
533
|
if (outgoing._affectsNodes.length) {
|
|
453
|
-
// Move (don't copy): the global queue may still
|
|
454
|
-
//
|
|
534
|
+
// Move (don't copy): the global queue's batch may still be the outgoing
|
|
535
|
+
// transition, and the adoption pass in initTransition would re-push its
|
|
455
536
|
// contents into the target — double-releasing every mark.
|
|
456
537
|
target._affectsNodes.push(...outgoing._affectsNodes);
|
|
457
538
|
outgoing._affectsNodes.length = 0;
|
|
@@ -576,11 +657,9 @@ class Queue {
|
|
|
576
657
|
}
|
|
577
658
|
class GlobalQueue extends Queue {
|
|
578
659
|
_running = false;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
_affectsNodes = [];
|
|
583
|
-
_optimisticStores = new Set();
|
|
660
|
+
// The current transaction-shaped batch: a plain ambient batch while no
|
|
661
|
+
// transition is active, the active transition itself after initTransition.
|
|
662
|
+
_batch = createBatch();
|
|
584
663
|
static _update;
|
|
585
664
|
static _dispose;
|
|
586
665
|
static _runEffect;
|
|
@@ -651,11 +730,13 @@ class GlobalQueue extends Queue {
|
|
|
651
730
|
if (!isComplete) {
|
|
652
731
|
const stashedTransition = activeTransition;
|
|
653
732
|
runHeap(zombieQueue, GlobalQueue._update);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
733
|
+
// Detach: the stashed transition keeps its batch; ambient work that
|
|
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();
|
|
659
740
|
// Run lane effects immediately (before stashing) - lanes with no pending async
|
|
660
741
|
if (activeLanes.size) {
|
|
661
742
|
GlobalQueue._runLaneEffects(EFFECT_RENDER);
|
|
@@ -663,7 +744,10 @@ class GlobalQueue extends Queue {
|
|
|
663
744
|
}
|
|
664
745
|
this.stashQueues(stashedTransition._queueStash);
|
|
665
746
|
clock++;
|
|
666
|
-
|
|
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;
|
|
667
751
|
reassignPendingTransition(stashedTransition._pendingNodes);
|
|
668
752
|
activeTransition = null;
|
|
669
753
|
// The stash pass (committed-view rerun of plain optimistic signals)
|
|
@@ -680,14 +764,26 @@ class GlobalQueue extends Queue {
|
|
|
680
764
|
}
|
|
681
765
|
return;
|
|
682
766
|
}
|
|
683
|
-
this._pendingNodes !== activeTransition._pendingNodes &&
|
|
684
|
-
this._pendingNodes.push(...activeTransition._pendingNodes);
|
|
685
|
-
this.restoreQueues(activeTransition._queueStash);
|
|
686
|
-
transitions.delete(activeTransition);
|
|
687
767
|
const completingTransition = activeTransition;
|
|
768
|
+
const batch = this._batch;
|
|
769
|
+
batch !== completingTransition &&
|
|
770
|
+
batch._pendingNodes.push(...completingTransition._pendingNodes);
|
|
771
|
+
this.restoreQueues(completingTransition._queueStash);
|
|
772
|
+
transitions.delete(completingTransition);
|
|
688
773
|
activeTransition = null;
|
|
689
|
-
reassignPendingTransition(
|
|
774
|
+
reassignPendingTransition(batch._pendingNodes);
|
|
690
775
|
finalizePureQueue(completingTransition);
|
|
776
|
+
if (batch === completingTransition) {
|
|
777
|
+
// Drop the dead Transition wrapper but keep its (drained) containers
|
|
778
|
+
// as the ambient batch — late registrations during finalization live
|
|
779
|
+
// there and must survive to the next flush.
|
|
780
|
+
const fresh = createBatch();
|
|
781
|
+
fresh._pendingNodes = batch._pendingNodes;
|
|
782
|
+
fresh._optimisticNodes = batch._optimisticNodes;
|
|
783
|
+
fresh._affectsNodes = batch._affectsNodes;
|
|
784
|
+
fresh._optimisticStores = batch._optimisticStores;
|
|
785
|
+
currentBatch = this._batch = fresh;
|
|
786
|
+
}
|
|
691
787
|
} else {
|
|
692
788
|
if (canUseSimpleSyncFlush(this)) {
|
|
693
789
|
commitPendingNodes();
|
|
@@ -710,12 +806,12 @@ class GlobalQueue extends Queue {
|
|
|
710
806
|
this.run(EFFECT_USER);
|
|
711
807
|
if (true) {
|
|
712
808
|
devCheckActiveOverrides(n => {
|
|
713
|
-
if (this._optimisticNodes.includes(n)) return true;
|
|
809
|
+
if (this._batch._optimisticNodes.includes(n)) return true;
|
|
714
810
|
if (activeTransition?._optimisticNodes.includes(n)) return true;
|
|
715
811
|
for (const t of transitions) if (t._optimisticNodes.includes(n)) return true;
|
|
716
812
|
return false;
|
|
717
813
|
});
|
|
718
|
-
devCensusCompanions(n =>
|
|
814
|
+
devCensusCompanions(n => this._batch._pendingNodes.includes(n));
|
|
719
815
|
}
|
|
720
816
|
if (
|
|
721
817
|
true &&
|
|
@@ -725,7 +821,7 @@ class GlobalQueue extends Queue {
|
|
|
725
821
|
activeLanes.size === 0
|
|
726
822
|
) {
|
|
727
823
|
// Fully drained: no transition-scoped state may survive this point.
|
|
728
|
-
devCheckQuiescent(n =>
|
|
824
|
+
devCheckQuiescent(n => this._batch._pendingNodes.includes(n));
|
|
729
825
|
}
|
|
730
826
|
if (true) DEV$1.hooks.onUpdate?.();
|
|
731
827
|
} finally {
|
|
@@ -756,18 +852,7 @@ class GlobalQueue extends Queue {
|
|
|
756
852
|
if (transition && transition === activeTransition) return;
|
|
757
853
|
if (!transition && activeTransition && activeTransition._time === clock) return;
|
|
758
854
|
if (!activeTransition) {
|
|
759
|
-
activeTransition = transition ??
|
|
760
|
-
_time: clock,
|
|
761
|
-
_pendingNodes: [],
|
|
762
|
-
_asyncReporters: createAsyncReporters(),
|
|
763
|
-
_optimisticNodes: [],
|
|
764
|
-
_affectsNodes: [],
|
|
765
|
-
_optimisticStores: new Set(),
|
|
766
|
-
_actions: [],
|
|
767
|
-
_queueStash: { _queues: [[], []], _children: [] },
|
|
768
|
-
_done: false,
|
|
769
|
-
_gatedSubs: new Set()
|
|
770
|
-
};
|
|
855
|
+
activeTransition = transition ?? createBatch();
|
|
771
856
|
} else if (transition) {
|
|
772
857
|
const outgoing = activeTransition;
|
|
773
858
|
mergeTransitionState(transition, outgoing);
|
|
@@ -776,60 +861,36 @@ class GlobalQueue extends Queue {
|
|
|
776
861
|
}
|
|
777
862
|
transitions.add(activeTransition);
|
|
778
863
|
activeTransition._time = clock;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
864
|
+
const batch = this._batch;
|
|
865
|
+
if (batch !== activeTransition) {
|
|
866
|
+
// Adopt the ambient batch into the transaction, then make the
|
|
867
|
+
// transaction the batch so later registrations land there directly.
|
|
868
|
+
// Pending and optimistic nodes are re-stamped as the transaction's;
|
|
869
|
+
// marks don't hijack the node's _transition — a mark on a plain signal
|
|
870
|
+
// must not entangle unrelated writes to it; the same rule holds one hop
|
|
871
|
+
// downstream: propagation never queues pended subscribers as pending
|
|
872
|
+
// nodes, see propagateAffectsMark, #2893.
|
|
873
|
+
for (let i = 0; i < batch._pendingNodes.length; i++) {
|
|
874
|
+
const node = batch._pendingNodes[i];
|
|
787
875
|
node._transition = activeTransition;
|
|
788
876
|
activeTransition._pendingNodes.push(node);
|
|
789
877
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (this._optimisticNodes !== activeTransition._optimisticNodes) {
|
|
793
|
-
for (let i = 0; i < this._optimisticNodes.length; i++) {
|
|
794
|
-
const node = this._optimisticNodes[i];
|
|
878
|
+
for (let i = 0; i < batch._optimisticNodes.length; i++) {
|
|
879
|
+
const node = batch._optimisticNodes[i];
|
|
795
880
|
node._transition = activeTransition;
|
|
796
881
|
activeTransition._optimisticNodes.push(node);
|
|
797
882
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
// Adopt ambient marks into the transaction (marks don't hijack the
|
|
802
|
-
// node's _transition — a mark on a plain signal must not entangle
|
|
803
|
-
// unrelated writes to it; the same rule holds one hop downstream:
|
|
804
|
-
// propagation never queues pended subscribers as pending nodes, see
|
|
805
|
-
// propagateAffectsMark, #2893). After adoption the queue aliases the
|
|
806
|
-
// transition's array, so later registrations land there directly.
|
|
807
|
-
activeTransition._affectsNodes.push(...this._affectsNodes);
|
|
808
|
-
this._affectsNodes = activeTransition._affectsNodes;
|
|
883
|
+
if (batch._affectsNodes.length) activeTransition._affectsNodes.push(...batch._affectsNodes);
|
|
884
|
+
for (const store of batch._optimisticStores) activeTransition._optimisticStores.add(store);
|
|
885
|
+
currentBatch = this._batch = activeTransition;
|
|
809
886
|
}
|
|
810
887
|
for (const lane of activeLanes) {
|
|
811
888
|
if (!lane._transition) lane._transition = activeTransition;
|
|
812
889
|
}
|
|
813
|
-
if (this._optimisticStores !== activeTransition._optimisticStores) {
|
|
814
|
-
for (const store of this._optimisticStores) activeTransition._optimisticStores.add(store);
|
|
815
|
-
this._optimisticStores = activeTransition._optimisticStores;
|
|
816
|
-
}
|
|
817
890
|
}
|
|
818
891
|
}
|
|
819
892
|
function queuePendingNode(node) {
|
|
820
|
-
|
|
821
|
-
globalQueue._pendingNodes.push(node);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
if (globalQueue._pendingNode === null && globalQueue._pendingNodes.length === 0) {
|
|
825
|
-
globalQueue._pendingNode = node;
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
if (globalQueue._pendingNode !== null) {
|
|
829
|
-
globalQueue._pendingNodes.push(globalQueue._pendingNode);
|
|
830
|
-
globalQueue._pendingNode = null;
|
|
831
|
-
}
|
|
832
|
-
globalQueue._pendingNodes.push(node);
|
|
893
|
+
currentBatch._pendingNodes.push(node);
|
|
833
894
|
}
|
|
834
895
|
// Sticky: flips true on the first refresh() ever (the only setter of
|
|
835
896
|
// REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
|
|
@@ -886,11 +947,7 @@ function commitPendingNode(n) {
|
|
|
886
947
|
if (n._pendingSignal || n._latestValueComputed) GlobalQueue._snapCompanions(n);
|
|
887
948
|
}
|
|
888
949
|
function commitPendingNodes() {
|
|
889
|
-
|
|
890
|
-
commitPendingNode(globalQueue._pendingNode);
|
|
891
|
-
globalQueue._pendingNode = null;
|
|
892
|
-
}
|
|
893
|
-
const pendingNodes = globalQueue._pendingNodes;
|
|
950
|
+
const pendingNodes = currentBatch._pendingNodes;
|
|
894
951
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
895
952
|
commitPendingNode(pendingNodes[i]);
|
|
896
953
|
}
|
|
@@ -906,12 +963,11 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
906
963
|
if (ranHeap) runHeap(dirtyQueue, GlobalQueue._update);
|
|
907
964
|
if (resolvePending) {
|
|
908
965
|
if (ranHeap) commitPendingNodes();
|
|
966
|
+
// The settling batch: the completing transaction's, or the ambient one.
|
|
967
|
+
const batch = completingTransition ?? globalQueue._batch;
|
|
909
968
|
// Optimistic reversion: a non-empty batch means _optimisticWrite ran,
|
|
910
969
|
// which installed the engine's hooks.
|
|
911
|
-
|
|
912
|
-
? completingTransition._optimisticNodes
|
|
913
|
-
: globalQueue._optimisticNodes;
|
|
914
|
-
if (optimisticNodes.length) GlobalQueue._resolveOptimistic(optimisticNodes);
|
|
970
|
+
if (batch._optimisticNodes.length) GlobalQueue._resolveOptimistic(batch._optimisticNodes);
|
|
915
971
|
// Replay entanglement: subs recorded by the read-time gate get rescheduled
|
|
916
972
|
// so they re-run with the now-committed values visible.
|
|
917
973
|
if (completingTransition && completingTransition._gatedSubs.size) {
|
|
@@ -924,17 +980,13 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
924
980
|
// Declared motion ends with the transaction: settle (or plain flush end
|
|
925
981
|
// for ambient marks) releases each registration's refcount. A non-empty
|
|
926
982
|
// batch means registerAffectsMark ran, which installed the hook.
|
|
927
|
-
|
|
928
|
-
? completingTransition._affectsNodes
|
|
929
|
-
: globalQueue._affectsNodes;
|
|
930
|
-
if (affectsNodes.length) GlobalQueue._releaseAffectsMarks(affectsNodes);
|
|
931
|
-
const optimisticStores = completingTransition
|
|
932
|
-
? completingTransition._optimisticStores
|
|
933
|
-
: globalQueue._optimisticStores;
|
|
983
|
+
if (batch._affectsNodes.length) GlobalQueue._releaseAffectsMarks(batch._affectsNodes);
|
|
934
984
|
// A non-empty set means trackOptimisticStore ran, which installed the
|
|
935
985
|
// hook; the hook iterates, clears, and schedules (keeping the loop out of
|
|
936
|
-
// core lets esbuild shake it — rollup already folds the null guard).
|
|
937
|
-
|
|
986
|
+
// core lets esbuild shake it — rollup already folds the null guard). The
|
|
987
|
+
// completing transition scopes the clear to its own layer keys (#2899).
|
|
988
|
+
if (batch._optimisticStores.size)
|
|
989
|
+
GlobalQueue._clearOptimisticStores(batch._optimisticStores, completingTransition);
|
|
938
990
|
sweepTransientStoreNodes();
|
|
939
991
|
// Lanes only enter activeLanes through the engine's getOrCreateLane.
|
|
940
992
|
if (activeLanes.size) GlobalQueue._cleanupLanes(completingTransition);
|
|
@@ -968,6 +1020,12 @@ function reassignPendingTransition(pendingNodes) {
|
|
|
968
1020
|
}
|
|
969
1021
|
}
|
|
970
1022
|
const globalQueue = new GlobalQueue();
|
|
1023
|
+
// Hot-path mirror of `globalQueue._batch`: `queuePendingNode` runs once per
|
|
1024
|
+
// staged write and `commitPendingNodes` once per flush, and the extra
|
|
1025
|
+
// property hop through `_batch` was a measured instruction-count regression
|
|
1026
|
+
// (CodSpeed update1to1, PR #2905). The field stays authoritative for
|
|
1027
|
+
// cross-module readers; every `_batch` assignment updates both.
|
|
1028
|
+
let currentBatch = globalQueue._batch;
|
|
971
1029
|
function flush(fn) {
|
|
972
1030
|
if (fn) {
|
|
973
1031
|
syncDepth++;
|
|
@@ -1005,7 +1063,7 @@ function runQueue$1(queue, type) {
|
|
|
1005
1063
|
}
|
|
1006
1064
|
function reporterBlocksSource(reporter, source) {
|
|
1007
1065
|
if (reporter._flags & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false;
|
|
1008
|
-
if (reporter.
|
|
1066
|
+
if (reporter._pendingSources?.has(source)) return true;
|
|
1009
1067
|
for (let dep = reporter._deps; dep; dep = dep._nextDep) {
|
|
1010
1068
|
let current = dep._dep;
|
|
1011
1069
|
while (current) {
|
|
@@ -1105,7 +1163,15 @@ function insertIntoHeap(n, heap) {
|
|
|
1105
1163
|
if (flags & (REACTIVE_IN_HEAP | REACTIVE_RECOMPUTING_DEPS | REACTIVE_MANUAL_WRITE)) return;
|
|
1106
1164
|
if (flags & REACTIVE_CHECK) {
|
|
1107
1165
|
n._flags = (flags & -4) | REACTIVE_DIRTY | REACTIVE_IN_HEAP;
|
|
1108
|
-
} 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
|
+
}
|
|
1109
1175
|
if (!(flags & REACTIVE_IN_HEAP_HEIGHT)) actualInsertIntoHeap(n, heap);
|
|
1110
1176
|
}
|
|
1111
1177
|
function insertIntoHeapHeight(n, heap) {
|
|
@@ -1585,38 +1651,22 @@ function link(dep, sub, pendingObserver = false) {
|
|
|
1585
1651
|
else dep._subs = newLink;
|
|
1586
1652
|
}
|
|
1587
1653
|
|
|
1654
|
+
// The lazily-created Set is the ONE container for pending sources. Its
|
|
1655
|
+
// predecessor — a singular slot promoted to a Set on the second source —
|
|
1656
|
+
// created dual state whose migration invariant was easy to break: a third
|
|
1657
|
+
// overlapping source landed beside the Set and removePendingSource refused
|
|
1658
|
+
// to clear it, stranding the Set members' pending forever (#2893).
|
|
1588
1659
|
function addPendingSource(el, source) {
|
|
1589
|
-
if (el.
|
|
1590
|
-
|
|
1591
|
-
// from migration until removePendingSource collapses back to one entry.
|
|
1592
|
-
// Landing a third source in the singular slot instead created dual state
|
|
1593
|
-
// that removePendingSource refused to clear, stranding the Set members'
|
|
1594
|
-
// pending forever (#2893).
|
|
1595
|
-
if (el._pendingSources) el._pendingSources.add(source);
|
|
1596
|
-
else if (!el._pendingSource) el._pendingSource = source;
|
|
1597
|
-
else {
|
|
1598
|
-
el._pendingSources = new Set([el._pendingSource, source]);
|
|
1599
|
-
el._pendingSource = undefined;
|
|
1600
|
-
}
|
|
1660
|
+
if (el._pendingSources?.has(source)) return false;
|
|
1661
|
+
(el._pendingSources ??= new Set()).add(source);
|
|
1601
1662
|
return true;
|
|
1602
1663
|
}
|
|
1603
1664
|
function removePendingSource(el, source) {
|
|
1604
|
-
if (el._pendingSource) {
|
|
1605
|
-
if (el._pendingSource !== source) return false;
|
|
1606
|
-
el._pendingSource = undefined;
|
|
1607
|
-
return true;
|
|
1608
|
-
}
|
|
1609
1665
|
if (!el._pendingSources?.delete(source)) return false;
|
|
1610
|
-
if (el._pendingSources.size ===
|
|
1611
|
-
el._pendingSource = el._pendingSources.values().next().value;
|
|
1612
|
-
el._pendingSources = undefined;
|
|
1613
|
-
} else if (el._pendingSources.size === 0) {
|
|
1614
|
-
el._pendingSources = undefined;
|
|
1615
|
-
}
|
|
1666
|
+
if (el._pendingSources.size === 0) el._pendingSources = undefined;
|
|
1616
1667
|
return true;
|
|
1617
1668
|
}
|
|
1618
1669
|
function clearPendingSources(el) {
|
|
1619
|
-
el._pendingSource = undefined;
|
|
1620
1670
|
el._pendingSources?.clear();
|
|
1621
1671
|
el._pendingSources = undefined;
|
|
1622
1672
|
}
|
|
@@ -1660,7 +1710,7 @@ function settlePendingSource(
|
|
|
1660
1710
|
if (visited.has(node) || !removePendingSource(node, source)) return;
|
|
1661
1711
|
visited.add(node);
|
|
1662
1712
|
node._time = clock;
|
|
1663
|
-
const remaining = node.
|
|
1713
|
+
const remaining = node._pendingSources?.values().next().value;
|
|
1664
1714
|
if (remaining) {
|
|
1665
1715
|
setPendingError(node, remaining);
|
|
1666
1716
|
updateCompanions !== null && updateCompanions(node);
|
|
@@ -1908,7 +1958,7 @@ function handleAsync(el, result, setter) {
|
|
|
1908
1958
|
return syncValue;
|
|
1909
1959
|
}
|
|
1910
1960
|
function clearStatus(el, clearUninitialized = false) {
|
|
1911
|
-
if (el.
|
|
1961
|
+
if (el._pendingSources) clearPendingSources(el);
|
|
1912
1962
|
if (el._blocked) el._blocked = false;
|
|
1913
1963
|
// The pending window is over; its quiet classification dies with it.
|
|
1914
1964
|
// (Unconditional: _reask is baked into the node literals, so this is a
|
|
@@ -1985,12 +2035,8 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
1985
2035
|
forEachDependent(el, (sub, link) => {
|
|
1986
2036
|
sub._time = clock;
|
|
1987
2037
|
if (
|
|
1988
|
-
(status === STATUS_PENDING &&
|
|
1989
|
-
|
|
1990
|
-
sub._pendingSource !== pendingSource &&
|
|
1991
|
-
!sub._pendingSources?.has(pendingSource)) ||
|
|
1992
|
-
(status !== STATUS_PENDING &&
|
|
1993
|
-
(sub._error !== error || sub._pendingSource || sub._pendingSources))
|
|
2038
|
+
(status === STATUS_PENDING && pendingSource && !sub._pendingSources?.has(pendingSource)) ||
|
|
2039
|
+
(status !== STATUS_PENDING && (sub._error !== error || sub._pendingSources))
|
|
1994
2040
|
) {
|
|
1995
2041
|
// A pending-observer link is the subscription an `isPending` read created.
|
|
1996
2042
|
// It exists so the observer re-runs when the source settles, but it must
|
|
@@ -2209,7 +2255,7 @@ function recompute(el, create = false) {
|
|
|
2209
2255
|
if (!el._error) {
|
|
2210
2256
|
trimStaleDeps(el);
|
|
2211
2257
|
const compareValue = hasOverride
|
|
2212
|
-
? el._overrideValue
|
|
2258
|
+
? unwrapOverride(el._overrideValue)
|
|
2213
2259
|
: el._pendingValue === NOT_PENDING
|
|
2214
2260
|
? el._value
|
|
2215
2261
|
: el._pendingValue;
|
|
@@ -2246,7 +2292,7 @@ function recompute(el, create = false) {
|
|
|
2246
2292
|
// own reveal schedule; drop any superseded older hold so its queued
|
|
2247
2293
|
// commit can't clobber the fresh value.
|
|
2248
2294
|
if (hasOverride && isOptimisticDirty) {
|
|
2249
|
-
el._overrideValue = value;
|
|
2295
|
+
el._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
|
|
2250
2296
|
el._pendingValue = NOT_PENDING;
|
|
2251
2297
|
}
|
|
2252
2298
|
} else {
|
|
@@ -2614,22 +2660,12 @@ function read(el) {
|
|
|
2614
2660
|
}
|
|
2615
2661
|
return !c || el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
2616
2662
|
}
|
|
2617
|
-
if (strictRead && owner._statusFlags & STATUS_PENDING)
|
|
2618
|
-
|
|
2619
|
-
`[PENDING_ASYNC_UNTRACKED_READ] Reading a pending async value directly in ${strictRead}. ` +
|
|
2620
|
-
`Async values must be read within a tracking scope (JSX, a memo, or an effect's compute function).`;
|
|
2621
|
-
emitDiagnostic({
|
|
2622
|
-
code: "PENDING_ASYNC_UNTRACKED_READ",
|
|
2623
|
-
kind: "async",
|
|
2624
|
-
severity: "error",
|
|
2625
|
-
message,
|
|
2663
|
+
if (strictRead && owner._statusFlags & STATUS_PENDING)
|
|
2664
|
+
throwPendingUntrackedRead(strictRead, {
|
|
2626
2665
|
ownerId: c?.id,
|
|
2627
2666
|
ownerName: c?._name,
|
|
2628
|
-
nodeName: owner?._name
|
|
2629
|
-
data: { strictRead }
|
|
2667
|
+
nodeName: owner?._name
|
|
2630
2668
|
});
|
|
2631
|
-
throw new Error(message);
|
|
2632
|
-
}
|
|
2633
2669
|
if (c && tracking) {
|
|
2634
2670
|
link(el, c, pendingCheckActive);
|
|
2635
2671
|
// Mark inheritance through derivation (see the fast path above), and its
|
|
@@ -2715,27 +2751,17 @@ function read(el) {
|
|
|
2715
2751
|
return snapshot;
|
|
2716
2752
|
}
|
|
2717
2753
|
}
|
|
2718
|
-
if (strictRead)
|
|
2719
|
-
|
|
2720
|
-
`[STRICT_READ_UNTRACKED] Reactive value read directly in ${strictRead} will not update. ` +
|
|
2721
|
-
`Move it into a tracking scope (JSX, a memo, or an effect's compute function).`;
|
|
2722
|
-
emitDiagnostic({
|
|
2723
|
-
code: "STRICT_READ_UNTRACKED",
|
|
2724
|
-
kind: "strict-read",
|
|
2725
|
-
severity: "warn",
|
|
2726
|
-
message,
|
|
2754
|
+
if (strictRead)
|
|
2755
|
+
warnStrictReadUntracked(strictRead, {
|
|
2727
2756
|
ownerId: c?.id,
|
|
2728
2757
|
ownerName: c?._name,
|
|
2729
|
-
nodeName: owner?._name
|
|
2730
|
-
data: { strictRead }
|
|
2758
|
+
nodeName: owner?._name
|
|
2731
2759
|
});
|
|
2732
|
-
console.warn(message);
|
|
2733
|
-
}
|
|
2734
2760
|
if (el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING) {
|
|
2735
2761
|
// An active override means the engine is installed (A17: the override IS
|
|
2736
2762
|
// the value for every reader — that check itself stays right here).
|
|
2737
2763
|
if (c && stale && GlobalQueue._readStashed(el)) return el._value;
|
|
2738
|
-
return el._overrideValue;
|
|
2764
|
+
return unwrapOverride(el._overrideValue);
|
|
2739
2765
|
}
|
|
2740
2766
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
2741
2767
|
// a pending mid-transition write sees the committed value. Projection-store
|
|
@@ -3131,7 +3157,7 @@ let stashedOptimisticReads = null;
|
|
|
3131
3157
|
/** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */
|
|
3132
3158
|
function optimisticWrite(el, v) {
|
|
3133
3159
|
const hasOverride = el._overrideValue !== NOT_PENDING;
|
|
3134
|
-
const currentValue = hasOverride ? el._overrideValue : el._value;
|
|
3160
|
+
const currentValue = hasOverride ? unwrapOverride(el._overrideValue) : el._value;
|
|
3135
3161
|
if (typeof v === "function") v = v(currentValue);
|
|
3136
3162
|
const valueChanged =
|
|
3137
3163
|
!!(el._statusFlags & STATUS_UNINITIALIZED) || !el._equals || !el._equals(currentValue, v);
|
|
@@ -3148,10 +3174,17 @@ function optimisticWrite(el, v) {
|
|
|
3148
3174
|
// No revert target is stashed: while the override is active every reader
|
|
3149
3175
|
// sees it (A17), so authoritative arrivals commit silently into _value and
|
|
3150
3176
|
// reverting is just dropping the override — _value is already correct.
|
|
3151
|
-
else globalQueue._optimisticNodes.push(el);
|
|
3177
|
+
else globalQueue._batch._optimisticNodes.push(el);
|
|
3178
|
+
// Stamp ownership on the node (post-merge, so entangled writers share the
|
|
3179
|
+
// joint root). resolveTransition prefers this over the lane's _transition,
|
|
3180
|
+
// which a shared subscriber can merge across transactions (#2912).
|
|
3181
|
+
el._overrideOwner = activeTransition;
|
|
3152
3182
|
const lane = getOrCreateLane(el);
|
|
3153
3183
|
el._optimisticLane = lane;
|
|
3154
|
-
|
|
3184
|
+
// Literal undefined must not land raw: the slot doubles as the optimistic
|
|
3185
|
+
// brand, and erasing it makes the write invisible and routes follow-up
|
|
3186
|
+
// writes off the optimistic path into permanent commits (#2898).
|
|
3187
|
+
el._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
|
|
3155
3188
|
GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, v);
|
|
3156
3189
|
el._time = clock;
|
|
3157
3190
|
insertSubs(el, true);
|
|
@@ -3223,8 +3256,10 @@ function resolveOptimisticNodes(nodes) {
|
|
|
3223
3256
|
if (!(node._statusFlags & STATUS_PENDING)) node._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
3224
3257
|
const prevOverride = node._overrideValue;
|
|
3225
3258
|
node._overrideValue = NOT_PENDING;
|
|
3226
|
-
if (prevOverride !== NOT_PENDING && node._value !== prevOverride)
|
|
3259
|
+
if (prevOverride !== NOT_PENDING && node._value !== unwrapOverride(prevOverride))
|
|
3260
|
+
insertSubs(node, true);
|
|
3227
3261
|
node._transition = null;
|
|
3262
|
+
node._overrideOwner = null;
|
|
3228
3263
|
}
|
|
3229
3264
|
// Settlement checkpoint (#2838): companions caught in this batch (or owned
|
|
3230
3265
|
// by a node in it) re-derive from committed state, so verdicts survive the
|
|
@@ -3349,8 +3384,8 @@ function laneAsyncSettled(el) {
|
|
|
3349
3384
|
}
|
|
3350
3385
|
}
|
|
3351
3386
|
function trackOptimisticStore(store) {
|
|
3352
|
-
// After initTransition, globalQueue.
|
|
3353
|
-
globalQueue._optimisticStores.add(store);
|
|
3387
|
+
// After initTransition, globalQueue._batch IS activeTransition (same reference)
|
|
3388
|
+
globalQueue._batch._optimisticStores.add(store);
|
|
3354
3389
|
schedule();
|
|
3355
3390
|
}
|
|
3356
3391
|
/**
|
|
@@ -3420,7 +3455,6 @@ function quietPending(el) {
|
|
|
3420
3455
|
for (const source of el._pendingSources) if (!source._reask) return false;
|
|
3421
3456
|
return true;
|
|
3422
3457
|
}
|
|
3423
|
-
if (el._pendingSource) return el._pendingSource._reask;
|
|
3424
3458
|
return el._reask;
|
|
3425
3459
|
}
|
|
3426
3460
|
function newQuestionInFlight(comp) {
|
|
@@ -3450,7 +3484,7 @@ function computePendingState(el) {
|
|
|
3450
3484
|
}
|
|
3451
3485
|
if (el._pendingValue !== NOT_PENDING && !(comp._statusFlags & STATUS_UNINITIALIZED)) {
|
|
3452
3486
|
if (hasActiveOverride(el))
|
|
3453
|
-
return !el._equals || !el._equals(el._pendingValue, el._overrideValue);
|
|
3487
|
+
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._overrideValue));
|
|
3454
3488
|
return true;
|
|
3455
3489
|
}
|
|
3456
3490
|
return newQuestionInFlight(comp);
|
|
@@ -3534,10 +3568,23 @@ function latestRead(el) {
|
|
|
3534
3568
|
setLatestReadActive(false);
|
|
3535
3569
|
const visibleValue =
|
|
3536
3570
|
el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING
|
|
3537
|
-
? el._overrideValue
|
|
3571
|
+
? unwrapOverride(el._overrideValue)
|
|
3538
3572
|
: el._value;
|
|
3539
3573
|
let value;
|
|
3540
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
|
+
}
|
|
3541
3588
|
value = read(pendingComputed);
|
|
3542
3589
|
} catch (e) {
|
|
3543
3590
|
if (e instanceof NotReadyError && (!context || !(el._statusFlags & STATUS_UNINITIALIZED)))
|
|
@@ -3554,6 +3601,16 @@ function latestRead(el) {
|
|
|
3554
3601
|
return visibleValue;
|
|
3555
3602
|
}
|
|
3556
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;
|
|
3557
3614
|
return value;
|
|
3558
3615
|
}
|
|
3559
3616
|
/** The isPending()-probe read path, installed as GlobalQueue._pendingCheck. */
|
|
@@ -3863,15 +3920,43 @@ function restoreTransition(transition, fn) {
|
|
|
3863
3920
|
return result;
|
|
3864
3921
|
}
|
|
3865
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
|
+
*
|
|
3866
3939
|
* Wraps a generator function so each invocation runs as a single transaction
|
|
3867
3940
|
* (a "transition") that batches every signal/store write between yields. The
|
|
3868
3941
|
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
|
3869
3942
|
* until the action either completes or the next `yield` resolves.
|
|
3870
3943
|
*
|
|
3871
|
-
*
|
|
3872
|
-
*
|
|
3873
|
-
*
|
|
3874
|
-
*
|
|
3944
|
+
* `yield` is the transaction-safe suspension point: the action waits for a
|
|
3945
|
+
* yielded promise and re-enters the transaction before running the code after
|
|
3946
|
+
* it. A plain `await` does NOT — the runtime has no hook into an async
|
|
3947
|
+
* generator's internal await continuations, so writes to fresh signals
|
|
3948
|
+
* between an `await` and the next `yield` escape the transaction and commit
|
|
3949
|
+
* immediately. `await` is still the ergonomic choice for typed results; just
|
|
3950
|
+
* put a bare `yield` before any writes that follow it:
|
|
3951
|
+
*
|
|
3952
|
+
* ```ts
|
|
3953
|
+
* const saved = await api.createTodo(text); // typed result
|
|
3954
|
+
* yield; // re-enter the transaction before writing
|
|
3955
|
+
* setTodos(t => { ... });
|
|
3956
|
+
* ```
|
|
3957
|
+
*
|
|
3958
|
+
* (For the same reason, don't call `flush()` inside an action body — it
|
|
3959
|
+
* drains the transaction mid-step.)
|
|
3875
3960
|
*
|
|
3876
3961
|
* Each call returns a `Promise` that resolves with the generator's return
|
|
3877
3962
|
* value, or rejects if it throws. Pair with `createOptimistic` /
|
|
@@ -3882,10 +3967,11 @@ function restoreTransition(transition, fn) {
|
|
|
3882
3967
|
* ```ts
|
|
3883
3968
|
* const [todos, setTodos] = createOptimisticStore<Todo[]>([]);
|
|
3884
3969
|
*
|
|
3885
|
-
* const addTodo = action(function* (text: string) {
|
|
3970
|
+
* const addTodo = action(async function* (text: string) {
|
|
3886
3971
|
* const tempId = crypto.randomUUID();
|
|
3887
3972
|
* setTodos(t => { t.push({ id: tempId, text, pending: true }); }); // optimistic
|
|
3888
|
-
* const saved =
|
|
3973
|
+
* const saved = await api.createTodo(text); // network round-trip, typed
|
|
3974
|
+
* yield; // re-enter the transaction
|
|
3889
3975
|
* setTodos(t => {
|
|
3890
3976
|
* const i = t.findIndex(x => x.id === tempId);
|
|
3891
3977
|
* if (i >= 0) t[i] = saved;
|
|
@@ -3948,11 +4034,33 @@ function action(genFn) {
|
|
|
3948
4034
|
};
|
|
3949
4035
|
const run = r => {
|
|
3950
4036
|
if (r.done) return done(r.value);
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
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
|
+
}
|
|
3956
4064
|
restoreTransition(ctx, () => step(r.value));
|
|
3957
4065
|
};
|
|
3958
4066
|
step();
|
|
@@ -4428,6 +4536,8 @@ function addEnumSymbols(o, syms, keys) {
|
|
|
4428
4536
|
}
|
|
4429
4537
|
}
|
|
4430
4538
|
function getAllKeys(value, override, next) {
|
|
4539
|
+
// Symbols are merged explicitly below; keep the common string-key path on
|
|
4540
|
+
// Object.keys() and avoid reflecting the base symbols twice.
|
|
4431
4541
|
const keys = getKeys(value, override);
|
|
4432
4542
|
const nextKeys = Object.keys(next);
|
|
4433
4543
|
// `value` can be a wrapped store (store-in-store) whose ownKeys trap tracks;
|
|
@@ -4510,6 +4620,43 @@ function applyArrayItem(next, previous, target, node, keyFn) {
|
|
|
4510
4620
|
applyState(next, wrapped, keyFn);
|
|
4511
4621
|
} else node && setSignal(node, wrapValue(next, target));
|
|
4512
4622
|
}
|
|
4623
|
+
/**
|
|
4624
|
+
* The captured-proxy half of the object diff (#2902): descend into keyed-
|
|
4625
|
+
* matching children that have NO node at this level but shelter subscribers
|
|
4626
|
+
* somewhere below (their target's sticky `STORE_DESC` flag, bubbled up by
|
|
4627
|
+
* `getNode`). Without this, a proxy captured through untracked reads — a
|
|
4628
|
+
* `<For>` row handed to a child component — detaches from the diff the
|
|
4629
|
+
* moment no intermediate level happens to be tracked, and its live
|
|
4630
|
+
* subscribers go permanently stale. Never-subscribed branches have no flag
|
|
4631
|
+
* and stay pruned exactly as before; keys the main loop already visited
|
|
4632
|
+
* (node present) are skipped. Callers gate on the parent's own flag and on
|
|
4633
|
+
* `$TRACK` absence (an enumeration-tracked record already diffs every key).
|
|
4634
|
+
*/
|
|
4635
|
+
function applyDescendants(previous, next, target, nodes, keyFn, override, optOverride) {
|
|
4636
|
+
const lookup = target[STORE_LOOKUP] || storeLookup;
|
|
4637
|
+
const keys = (override ? getKeys(previous, override) : Object.keys(previous)).concat(
|
|
4638
|
+
getStoreSymbols(previous, override)
|
|
4639
|
+
);
|
|
4640
|
+
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4641
|
+
const key = keys[i];
|
|
4642
|
+
if (nodes?.[key]) continue; // main loop already diffed this slot
|
|
4643
|
+
const previousValue = unwrap(
|
|
4644
|
+
override ? getOverrideValue(previous, override, key, optOverride) : previous[key]
|
|
4645
|
+
);
|
|
4646
|
+
if (!isWrappable(previousValue)) continue;
|
|
4647
|
+
const childTarget = (lookup.get(previousValue) ?? storeLookup.get(previousValue))?.[$TARGET];
|
|
4648
|
+
if (!childTarget?.[STORE_DESC]) continue;
|
|
4649
|
+
const nextValue = unwrap(next[key]);
|
|
4650
|
+
if (
|
|
4651
|
+
previousValue === nextValue ||
|
|
4652
|
+
!isWrappable(nextValue) ||
|
|
4653
|
+
Array.isArray(previousValue) !== Array.isArray(nextValue) ||
|
|
4654
|
+
(keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
|
|
4655
|
+
)
|
|
4656
|
+
continue;
|
|
4657
|
+
applyState(nextValue, wrap(previousValue, target), keyFn);
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4513
4660
|
// Dispatcher: every applyState call (including recursion) checks for the
|
|
4514
4661
|
// presence of override / optimistic-override slots once and routes to the
|
|
4515
4662
|
// appropriate body. The fast body never calls `getOverrideValue` and never
|
|
@@ -4619,8 +4766,9 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4619
4766
|
}
|
|
4620
4767
|
// values
|
|
4621
4768
|
let nodes = target[STORE_NODE];
|
|
4769
|
+
let tracked;
|
|
4622
4770
|
if (nodes) {
|
|
4623
|
-
|
|
4771
|
+
tracked = nodes[$TRACK];
|
|
4624
4772
|
const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
|
|
4625
4773
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4626
4774
|
const key = keys[i];
|
|
@@ -4640,6 +4788,7 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4640
4788
|
} else applyState(nextValue, wrap(previousValue, target), keyFn);
|
|
4641
4789
|
}
|
|
4642
4790
|
}
|
|
4791
|
+
if (!tracked && target[STORE_DESC]) applyDescendants(previous, next, target, nodes, keyFn);
|
|
4643
4792
|
// has
|
|
4644
4793
|
if ((nodes = target[STORE_HAS])) {
|
|
4645
4794
|
const keys = nodeKeys(nodes);
|
|
@@ -4753,8 +4902,9 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4753
4902
|
return;
|
|
4754
4903
|
}
|
|
4755
4904
|
// values
|
|
4905
|
+
let tracked;
|
|
4756
4906
|
if (nodes) {
|
|
4757
|
-
|
|
4907
|
+
tracked = nodes[$TRACK];
|
|
4758
4908
|
const keys = tracked ? getAllKeys(previous, override, next) : nodeKeys(nodes);
|
|
4759
4909
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4760
4910
|
const key = keys[i];
|
|
@@ -4774,6 +4924,8 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4774
4924
|
} else applyState(nextValue, wrap(previousValue, target), keyFn);
|
|
4775
4925
|
}
|
|
4776
4926
|
}
|
|
4927
|
+
if (!tracked && target[STORE_DESC])
|
|
4928
|
+
applyDescendants(previous, next, target, nodes, keyFn, override, optOverride);
|
|
4777
4929
|
// has
|
|
4778
4930
|
if ((nodes = target[STORE_HAS])) {
|
|
4779
4931
|
const keys = nodeKeys(nodes);
|
|
@@ -5005,7 +5157,10 @@ const STORE_VALUE = "v",
|
|
|
5005
5157
|
STORE_WRAP = "w",
|
|
5006
5158
|
STORE_LOOKUP = "l",
|
|
5007
5159
|
STORE_FIREWALL = "f",
|
|
5008
|
-
STORE_OPTIMISTIC = "p"
|
|
5160
|
+
STORE_OPTIMISTIC = "p",
|
|
5161
|
+
STORE_OPTIMISTIC_OWNERS = "t",
|
|
5162
|
+
STORE_PARENT = "u",
|
|
5163
|
+
STORE_DESC = "d";
|
|
5009
5164
|
const STORE_SELF_PENDING = Symbol("STORE_SELF_PENDING");
|
|
5010
5165
|
function createStoreProxy(value, traps = storeTraps, extend) {
|
|
5011
5166
|
let newTarget;
|
|
@@ -5028,9 +5183,17 @@ const storeLookup = new WeakMap();
|
|
|
5028
5183
|
// Lets reconcile enumerate symbols only for records that need it (#2851).
|
|
5029
5184
|
const symbolKeyedRecords = new WeakSet();
|
|
5030
5185
|
function wrap(value, target) {
|
|
5031
|
-
if (target?.[STORE_WRAP])
|
|
5186
|
+
if (target?.[STORE_WRAP]) {
|
|
5187
|
+
const p = target[STORE_WRAP](value, target);
|
|
5188
|
+
const t = p[$TARGET];
|
|
5189
|
+
if (t && !t[STORE_PARENT] && t !== target) t[STORE_PARENT] = target;
|
|
5190
|
+
return p;
|
|
5191
|
+
}
|
|
5032
5192
|
let p = value[$PROXY] || storeLookup.get(value);
|
|
5033
|
-
if (!p)
|
|
5193
|
+
if (!p) {
|
|
5194
|
+
storeLookup.set(value, (p = createStoreProxy(value)));
|
|
5195
|
+
if (target) p[$TARGET][STORE_PARENT] = target;
|
|
5196
|
+
}
|
|
5034
5197
|
return p;
|
|
5035
5198
|
}
|
|
5036
5199
|
function isWrappable(obj) {
|
|
@@ -5058,7 +5221,7 @@ function unwrapStoreValue(value, map, lookup) {
|
|
|
5058
5221
|
const result = isArray ? [] : Object.create(Object.getPrototypeOf(source));
|
|
5059
5222
|
map.set(value, result);
|
|
5060
5223
|
lookup = target[STORE_LOOKUP] ?? storeLookup;
|
|
5061
|
-
for (const key of
|
|
5224
|
+
for (const key of getStoreKeys(source, override)) {
|
|
5062
5225
|
if (isArray && key === "length") continue;
|
|
5063
5226
|
const next = key in override ? override[key] : source[key];
|
|
5064
5227
|
if (next !== $DELETED) result[key] = unwrapStoreValue(next, map, lookup);
|
|
@@ -5073,6 +5236,21 @@ function isPrototypePollutionKey$1(property) {
|
|
|
5073
5236
|
function ownEnumerableKeys(o) {
|
|
5074
5237
|
return Reflect.ownKeys(o).filter(k => Object.prototype.propertyIsEnumerable.call(o, k));
|
|
5075
5238
|
}
|
|
5239
|
+
function ownEnumerableSymbols(o) {
|
|
5240
|
+
const symbols = Object.getOwnPropertySymbols(o);
|
|
5241
|
+
const result = [];
|
|
5242
|
+
for (let i = 0, len = symbols.length; i < len; i++) {
|
|
5243
|
+
const symbol = symbols[i];
|
|
5244
|
+
if (Object.prototype.propertyIsEnumerable.call(o, symbol)) result.push(symbol);
|
|
5245
|
+
}
|
|
5246
|
+
return result;
|
|
5247
|
+
}
|
|
5248
|
+
// Plain-object variant that keeps Object.keys() as the fast path and only pays
|
|
5249
|
+
// descriptor checks for symbols. Do not use this on store proxies: splitting
|
|
5250
|
+
// strings/symbols would invoke their ownKeys trap twice.
|
|
5251
|
+
function ownEnumerableKeysPlain(o) {
|
|
5252
|
+
return Object.keys(o).concat(ownEnumerableSymbols(o));
|
|
5253
|
+
}
|
|
5076
5254
|
/**
|
|
5077
5255
|
* Single chokepoint for the store's layered value resolution: returns the
|
|
5078
5256
|
* override layer (optimistic first, then regular) that shadows `property`, or
|
|
@@ -5093,7 +5271,7 @@ function getOverlayLayer(target, property) {
|
|
|
5093
5271
|
*/
|
|
5094
5272
|
function visibleNodeValue(node) {
|
|
5095
5273
|
return node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING
|
|
5096
|
-
? node._overrideValue
|
|
5274
|
+
? unwrapOverride(node._overrideValue)
|
|
5097
5275
|
: node._pendingValue !== NOT_PENDING
|
|
5098
5276
|
? node._pendingValue
|
|
5099
5277
|
: node._value;
|
|
@@ -5161,22 +5339,37 @@ function getNode(target, nodes, property, value, equals = isEqual, snapshotProps
|
|
|
5161
5339
|
}
|
|
5162
5340
|
if (typeof property === "symbol" && property !== $TRACK && property !== $AFFECTS)
|
|
5163
5341
|
symbolKeyedRecords.add(nodes);
|
|
5164
|
-
// A node born inside a live
|
|
5342
|
+
// A node born inside a live mark's identity scope inherits the mark
|
|
5165
5343
|
// (the declaration walk could only cover nodes that existed then). The
|
|
5166
5344
|
// record's own $AFFECTS carrier is the mark's channel, never a member.
|
|
5167
|
-
if (property !== $AFFECTS && affectsScopes.size)
|
|
5345
|
+
if (property !== $AFFECTS && affectsScopes.size)
|
|
5346
|
+
inheritAffectsMarks(s, target[STORE_VALUE], property);
|
|
5347
|
+
// Node presence bubbles up the wrap chain (sticky), so reconcile can see
|
|
5348
|
+
// "subscribers live somewhere below" through node-less intermediate
|
|
5349
|
+
// records — the captured-proxy diff gate (#2902). Amortized O(1): stops at
|
|
5350
|
+
// the first already-flagged ancestor.
|
|
5351
|
+
let t = target;
|
|
5352
|
+
while (t && !t[STORE_DESC]) {
|
|
5353
|
+
t[STORE_DESC] = true;
|
|
5354
|
+
t = t[STORE_PARENT];
|
|
5355
|
+
}
|
|
5168
5356
|
return (nodes[property] = s);
|
|
5169
5357
|
}
|
|
5170
5358
|
/**
|
|
5171
|
-
* Scope inheritance for late-created nodes: every live
|
|
5172
|
-
*
|
|
5173
|
-
*
|
|
5174
|
-
*
|
|
5359
|
+
* Scope inheritance for late-created nodes: every live mark whose identity
|
|
5360
|
+
* scope contains the owning record's raw — and, for keyed marks, whose key
|
|
5361
|
+
* is this property — gets counted on the new node. Inherited marks live
|
|
5362
|
+
* exactly as long as the scope's carrier — the release hook below drops
|
|
5363
|
+
* them with the entry.
|
|
5175
5364
|
*/
|
|
5176
|
-
function inheritAffectsMarks(node, raw) {
|
|
5365
|
+
function inheritAffectsMarks(node, raw, property) {
|
|
5177
5366
|
// A live scope exists, so affects.ts already installed the mark engine.
|
|
5178
5367
|
for (const [carrier, entry] of affectsScopes) {
|
|
5179
|
-
if (
|
|
5368
|
+
if (
|
|
5369
|
+
carrier._affectsCount &&
|
|
5370
|
+
entry.scope.has(raw) &&
|
|
5371
|
+
(entry.key === undefined || entry.key === property)
|
|
5372
|
+
) {
|
|
5180
5373
|
GlobalQueue._markAffects(node);
|
|
5181
5374
|
entry.inherited.push(node);
|
|
5182
5375
|
}
|
|
@@ -5214,7 +5407,10 @@ function walkAffectsScope(
|
|
|
5214
5407
|
collectRecordNodes(target[STORE_NODE], found);
|
|
5215
5408
|
collectRecordNodes(target[STORE_HAS], found);
|
|
5216
5409
|
override = mergedOverlay(target);
|
|
5217
|
-
lookup
|
|
5410
|
+
// Carry the effective lookup into untouched descendants. Default stores
|
|
5411
|
+
// use the global lookup just like snapshotImpl; without it, nested raw
|
|
5412
|
+
// objects fall back to string-only enumeration and symbol branches vanish.
|
|
5413
|
+
lookup = target[STORE_LOOKUP] ?? lookup ?? storeLookup;
|
|
5218
5414
|
}
|
|
5219
5415
|
if (Array.isArray(raw)) {
|
|
5220
5416
|
const len = override?.length ?? raw.length;
|
|
@@ -5222,8 +5418,18 @@ function walkAffectsScope(
|
|
|
5222
5418
|
const v = override && i in override ? override[i] : raw[i];
|
|
5223
5419
|
if (v !== $DELETED) walkAffectsScope(v, entry, found, lookup, visited);
|
|
5224
5420
|
}
|
|
5421
|
+
// Arrays can also carry symbol metadata. Enumerate symbols separately to
|
|
5422
|
+
// avoid scanning large index lists twice. Outside a store tree, retain the
|
|
5423
|
+
// existing index-only walk.
|
|
5424
|
+
const symbols = target || lookup ? getStoreSymbols(raw, override) : [];
|
|
5425
|
+
for (let i = 0, l = symbols.length; i < l; i++) {
|
|
5426
|
+
const key = symbols[i];
|
|
5427
|
+
const desc = getPropertyDescriptor(raw, override, key);
|
|
5428
|
+
if (!desc || desc.get) continue;
|
|
5429
|
+
walkAffectsScope(desc.value, entry, found, lookup, visited);
|
|
5430
|
+
}
|
|
5225
5431
|
} else {
|
|
5226
|
-
const keys = getKeys(raw, override);
|
|
5432
|
+
const keys = target || lookup ? getStoreKeys(raw, override) : getKeys(raw, override);
|
|
5227
5433
|
for (let i = 0, l = keys.length; i < l; i++) {
|
|
5228
5434
|
const desc = getPropertyDescriptor(raw, override, keys[i]);
|
|
5229
5435
|
if (!desc || desc.get) continue;
|
|
@@ -5252,7 +5458,7 @@ function collectRecordNodes(nodes, found) {
|
|
|
5252
5458
|
*
|
|
5253
5459
|
* @internal
|
|
5254
5460
|
*/
|
|
5255
|
-
function witnessAffectsMark(target) {
|
|
5461
|
+
function witnessAffectsMark(target, property) {
|
|
5256
5462
|
// Callers guard on `pendingCheckActive`, which only flips inside
|
|
5257
5463
|
// isPending() — the verdict layer is loaded and its hook installed.
|
|
5258
5464
|
const own = target[STORE_NODE]?.[$AFFECTS];
|
|
@@ -5260,7 +5466,12 @@ function witnessAffectsMark(target) {
|
|
|
5260
5466
|
if (affectsScopes.size) {
|
|
5261
5467
|
const raw = target[STORE_VALUE];
|
|
5262
5468
|
for (const [carrier, entry] of affectsScopes) {
|
|
5263
|
-
if (
|
|
5469
|
+
if (
|
|
5470
|
+
carrier !== own &&
|
|
5471
|
+
carrier._affectsCount &&
|
|
5472
|
+
entry.scope.has(raw) &&
|
|
5473
|
+
(entry.key === undefined || entry.key === property)
|
|
5474
|
+
)
|
|
5264
5475
|
GlobalQueue._witnessAffects(carrier);
|
|
5265
5476
|
}
|
|
5266
5477
|
}
|
|
@@ -5278,26 +5489,42 @@ function witnessAffectsMark(target) {
|
|
|
5278
5489
|
*/
|
|
5279
5490
|
function getStoreAffectsNodes(target, key) {
|
|
5280
5491
|
const nodes = getNodes(target, STORE_NODE);
|
|
5492
|
+
GlobalQueue._releaseAffectsScope ||= node => {
|
|
5493
|
+
const entry = affectsScopes.get(node);
|
|
5494
|
+
if (!entry) return;
|
|
5495
|
+
affectsScopes.delete(node);
|
|
5496
|
+
for (let i = 0; i < entry.inherited.length; i++)
|
|
5497
|
+
GlobalQueue._releaseAffectsMark(entry.inherited[i]);
|
|
5498
|
+
};
|
|
5281
5499
|
if (key === undefined) {
|
|
5282
5500
|
const carrier = getNode(target, nodes, $AFFECTS, undefined, false);
|
|
5283
|
-
GlobalQueue._releaseAffectsScope ||= node => {
|
|
5284
|
-
const entry = affectsScopes.get(node);
|
|
5285
|
-
if (!entry) return;
|
|
5286
|
-
affectsScopes.delete(node);
|
|
5287
|
-
for (let i = 0; i < entry.inherited.length; i++)
|
|
5288
|
-
GlobalQueue._releaseAffectsMark(entry.inherited[i]);
|
|
5289
|
-
};
|
|
5290
5501
|
let entry = affectsScopes.get(carrier);
|
|
5291
5502
|
if (!entry) affectsScopes.set(carrier, (entry = { scope: new Set(), inherited: [] }));
|
|
5292
5503
|
const result = [carrier];
|
|
5293
5504
|
walkAffectsScope(target[$PROXY], entry, result, target[STORE_LOOKUP], new Set());
|
|
5294
5505
|
return result;
|
|
5295
5506
|
}
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5507
|
+
let node = nodes[key];
|
|
5508
|
+
if (!node) {
|
|
5509
|
+
const layer = getOverlayLayer(target, key);
|
|
5510
|
+
const raw = layer ? layer[key] : target[STORE_VALUE][key];
|
|
5511
|
+
node = upsertStoreNode(
|
|
5512
|
+
target,
|
|
5513
|
+
nodes,
|
|
5514
|
+
key,
|
|
5515
|
+
raw === $DELETED ? undefined : raw,
|
|
5516
|
+
target[STORE_SNAPSHOT_PROPS]
|
|
5517
|
+
);
|
|
5518
|
+
}
|
|
5519
|
+
// Keyed marks resolve by identity too (#2904): another store family's
|
|
5520
|
+
// proxy can share this record's raw (a derived store swaps its backing to
|
|
5521
|
+
// the source's raw when its projection lands), and reads through it never
|
|
5522
|
+
// touch this target's node map. Scope is exactly the owning record's raw,
|
|
5523
|
+
// narrowed to this key for witness and birth inheritance.
|
|
5524
|
+
let entry = affectsScopes.get(node);
|
|
5525
|
+
if (!entry) affectsScopes.set(node, (entry = { scope: new Set(), inherited: [], key }));
|
|
5526
|
+
entry.scope.add(target[STORE_VALUE]);
|
|
5527
|
+
return [node];
|
|
5301
5528
|
}
|
|
5302
5529
|
function trackSelf(target, symbol = $TRACK) {
|
|
5303
5530
|
if (!getObserver()) return;
|
|
@@ -5336,17 +5563,44 @@ function mergedOverlay(target) {
|
|
|
5336
5563
|
const opt = target[STORE_OPTIMISTIC_OVERRIDE];
|
|
5337
5564
|
return override && opt ? { ...override, ...opt } : (opt ?? override);
|
|
5338
5565
|
}
|
|
5339
|
-
function
|
|
5566
|
+
function getKeysImpl(source, override, enumerable, symbols) {
|
|
5340
5567
|
// Plain objects can't trigger proxy traps — only pay for the untrack
|
|
5341
5568
|
// closure when the source is itself a wrapped store (store-in-store).
|
|
5342
5569
|
const baseKeys = source[$TARGET]
|
|
5343
|
-
? untrack(() =>
|
|
5570
|
+
? untrack(() =>
|
|
5571
|
+
enumerable
|
|
5572
|
+
? symbols
|
|
5573
|
+
? ownEnumerableKeys(source)
|
|
5574
|
+
: Object.keys(source)
|
|
5575
|
+
: Reflect.ownKeys(source)
|
|
5576
|
+
)
|
|
5344
5577
|
: enumerable
|
|
5345
|
-
?
|
|
5578
|
+
? symbols
|
|
5579
|
+
? ownEnumerableKeysPlain(source)
|
|
5580
|
+
: Object.keys(source)
|
|
5346
5581
|
: Reflect.ownKeys(source);
|
|
5347
|
-
|
|
5582
|
+
return override ? mergeOverrideKeys(baseKeys, override) : baseKeys;
|
|
5583
|
+
}
|
|
5584
|
+
function getKeys(source, override, enumerable = true) {
|
|
5585
|
+
return getKeysImpl(source, override, enumerable, false);
|
|
5586
|
+
}
|
|
5587
|
+
function getStoreKeys(source, override) {
|
|
5588
|
+
return getKeysImpl(source, override, true, true);
|
|
5589
|
+
}
|
|
5590
|
+
function getStoreSymbols(source, override) {
|
|
5591
|
+
const symbols = source[$TARGET]
|
|
5592
|
+
? untrack(() => ownEnumerableSymbols(source))
|
|
5593
|
+
: ownEnumerableSymbols(source);
|
|
5594
|
+
return override ? mergeOverrideKeys(symbols, override, true) : symbols;
|
|
5595
|
+
}
|
|
5596
|
+
// Shared override-layer merge for key enumeration: adds live override keys,
|
|
5597
|
+
// drops $DELETED ones. `symbolsOnly` scopes the override scan for the
|
|
5598
|
+
// array-metadata passes.
|
|
5599
|
+
function mergeOverrideKeys(baseKeys, override, symbolsOnly) {
|
|
5348
5600
|
const keys = new Set(baseKeys);
|
|
5349
|
-
const overrides =
|
|
5601
|
+
const overrides = symbolsOnly
|
|
5602
|
+
? Object.getOwnPropertySymbols(override)
|
|
5603
|
+
: Reflect.ownKeys(override);
|
|
5350
5604
|
for (const key of overrides) {
|
|
5351
5605
|
if (override[key] !== $DELETED) keys.add(key);
|
|
5352
5606
|
else keys.delete(key);
|
|
@@ -5412,6 +5666,19 @@ function armOptimisticStoreWrite(target, store) {
|
|
|
5412
5666
|
GlobalQueue._trackOptimisticStore(store);
|
|
5413
5667
|
}
|
|
5414
5668
|
}
|
|
5669
|
+
/**
|
|
5670
|
+
* Records which transition owns an optimistic layer entry (#2899), so a
|
|
5671
|
+
* settling action only consumes its own keys — the layer is store-wide, but
|
|
5672
|
+
* concurrent actions writing disjoint keys must revert independently, exactly
|
|
5673
|
+
* like optimistic signal nodes do via the transition's _optimisticNodes.
|
|
5674
|
+
* `activeTransition` is the write's transaction (action() opens it before the
|
|
5675
|
+
* body runs); null marks an ambient write that clears at plain flush end.
|
|
5676
|
+
* Same-key writes across actions keep last-write-wins layer semantics.
|
|
5677
|
+
*/
|
|
5678
|
+
function stampOptimisticOwner(target, overrideKey, property) {
|
|
5679
|
+
if (overrideKey === STORE_OPTIMISTIC_OVERRIDE)
|
|
5680
|
+
(target[STORE_OPTIMISTIC_OWNERS] ??= Object.create(null))[property] = activeTransition;
|
|
5681
|
+
}
|
|
5415
5682
|
function upsertStoreNode(target, nodes, property, prev, snapshotProps) {
|
|
5416
5683
|
if (nodes[property]) return nodes[property];
|
|
5417
5684
|
const initial = isWrappable(prev) ? wrap(prev, target) : prev;
|
|
@@ -5456,12 +5723,26 @@ function notifyStoreProperty(target, property, mode, value, prev, prevHas) {
|
|
|
5456
5723
|
notifySelf(target);
|
|
5457
5724
|
}
|
|
5458
5725
|
let Writing = null;
|
|
5726
|
+
/**
|
|
5727
|
+
* A derived store's seed is a draft for the derive function, never an
|
|
5728
|
+
* observable value (#2897): until the firewall first resolves there is
|
|
5729
|
+
* nothing to read, so every consumer path throws NotReady — tracked reads
|
|
5730
|
+
* through their node (core read()), and the untracked fall-throughs in the
|
|
5731
|
+
* traps through this guard. Returning the seed leaked it; returning
|
|
5732
|
+
* `undefined` would break non-nullable types. Callers exempt the firewall
|
|
5733
|
+
* itself (the derive function works its own draft while uninitialized).
|
|
5734
|
+
*/
|
|
5735
|
+
function throwIfUninitialized(target) {
|
|
5736
|
+
const firewall = target[STORE_FIREWALL];
|
|
5737
|
+
if (firewall && firewall._statusFlags & STATUS_UNINITIALIZED)
|
|
5738
|
+
throw firewall._error ?? new NotReadyError(firewall);
|
|
5739
|
+
}
|
|
5459
5740
|
const storeTraps = {
|
|
5460
5741
|
get(target, property, receiver) {
|
|
5461
5742
|
if (property === $TARGET) return target;
|
|
5462
5743
|
if (property === $PROXY) return receiver;
|
|
5463
5744
|
if (property === $REFRESH) return target[STORE_FIREWALL];
|
|
5464
|
-
if (pendingCheckActive) witnessAffectsMark(target);
|
|
5745
|
+
if (pendingCheckActive) witnessAffectsMark(target, property);
|
|
5465
5746
|
if (property === $TRACK) {
|
|
5466
5747
|
trackSelf(target);
|
|
5467
5748
|
return receiver;
|
|
@@ -5542,24 +5823,26 @@ const storeTraps = {
|
|
|
5542
5823
|
}
|
|
5543
5824
|
}
|
|
5544
5825
|
if (strictRead && typeof property === "string") {
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
nodeName:
|
|
5554
|
-
data: { strictRead, property
|
|
5826
|
+
// Safeguard parity with core read() (#2897): untracked store reads skip
|
|
5827
|
+
// node creation (and with it read()'s PENDING_ASYNC_UNTRACKED_READ
|
|
5828
|
+
// check), so a derived store's in-flight firewall must be consulted
|
|
5829
|
+
// here — otherwise a component-body read of a refetching store silently
|
|
5830
|
+
// returns a value the reader can never observe updating.
|
|
5831
|
+
if ((target[STORE_FIREWALL]?._statusFlags ?? 0) & STATUS_PENDING)
|
|
5832
|
+
throwPendingUntrackedRead(strictRead, { nodeName: property });
|
|
5833
|
+
warnStrictReadUntracked(strictRead, {
|
|
5834
|
+
nodeName: property,
|
|
5835
|
+
data: { strictRead, property, source: "store" }
|
|
5555
5836
|
});
|
|
5556
|
-
console.warn(message);
|
|
5557
5837
|
}
|
|
5838
|
+
// Untracked fall-through (tracked reads already threw via their node in
|
|
5839
|
+
// read(); the dev strictRead error above wins first for memo parity).
|
|
5840
|
+
if (!selfRead) throwIfUninitialized(target);
|
|
5558
5841
|
return isWrappable(value) ? wrap(value, target) : value;
|
|
5559
5842
|
},
|
|
5560
5843
|
has(target, property) {
|
|
5561
5844
|
if (property === $PROXY || property === $TRACK || property === "__proto__") return true;
|
|
5562
|
-
if (pendingCheckActive) witnessAffectsMark(target);
|
|
5845
|
+
if (pendingCheckActive) witnessAffectsMark(target, property);
|
|
5563
5846
|
const hasLayer = getOverlayLayer(target, property);
|
|
5564
5847
|
const has = hasLayer ? hasLayer[property] !== $DELETED : property in target[STORE_VALUE];
|
|
5565
5848
|
if (writeOnly(target[$PROXY]) || getObserver() === target[STORE_FIREWALL]) return has;
|
|
@@ -5574,6 +5857,7 @@ const storeTraps = {
|
|
|
5574
5857
|
if (getObserver()) {
|
|
5575
5858
|
return read(getNode(target, nodes, property, has));
|
|
5576
5859
|
}
|
|
5860
|
+
throwIfUninitialized(target);
|
|
5577
5861
|
return has;
|
|
5578
5862
|
},
|
|
5579
5863
|
set(target, property, rawValue) {
|
|
@@ -5603,12 +5887,18 @@ const storeTraps = {
|
|
|
5603
5887
|
const nextLength = isArrayIndexWrite && nextIndex > len ? nextIndex : undefined;
|
|
5604
5888
|
if (prev === value && nextLength === undefined) return true;
|
|
5605
5889
|
armOptimisticStoreWrite(target, store);
|
|
5606
|
-
if (value !== undefined && value === base && nextLength === undefined)
|
|
5890
|
+
if (value !== undefined && value === base && nextLength === undefined) {
|
|
5607
5891
|
delete target[overrideKey]?.[property];
|
|
5608
|
-
|
|
5892
|
+
if (overrideKey === STORE_OPTIMISTIC_OVERRIDE)
|
|
5893
|
+
delete target[STORE_OPTIMISTIC_OWNERS]?.[property];
|
|
5894
|
+
} else {
|
|
5609
5895
|
const override = target[overrideKey] || (target[overrideKey] = Object.create(null));
|
|
5610
5896
|
override[property] = value;
|
|
5611
|
-
|
|
5897
|
+
stampOptimisticOwner(target, overrideKey, property);
|
|
5898
|
+
if (nextLength !== undefined) {
|
|
5899
|
+
override.length = nextLength;
|
|
5900
|
+
stampOptimisticOwner(target, overrideKey, "length");
|
|
5901
|
+
}
|
|
5612
5902
|
}
|
|
5613
5903
|
notifyStoreProperty(target, property, "set", value, prev, prevHas);
|
|
5614
5904
|
// Shrinking an array's length must remove the truncated indices, otherwise
|
|
@@ -5627,6 +5917,7 @@ const storeTraps = {
|
|
|
5627
5917
|
const prevIndex = i in override ? override[i] : state[i];
|
|
5628
5918
|
if (!(i in override) && !(i in state)) continue;
|
|
5629
5919
|
override[i] = $DELETED;
|
|
5920
|
+
stampOptimisticOwner(target, overrideKey, i);
|
|
5630
5921
|
notifyStoreProperty(target, i, "delete", undefined, prevIndex, true);
|
|
5631
5922
|
}
|
|
5632
5923
|
}
|
|
@@ -5670,6 +5961,7 @@ const storeTraps = {
|
|
|
5670
5961
|
property,
|
|
5671
5962
|
normalizedDescriptor
|
|
5672
5963
|
);
|
|
5964
|
+
stampOptimisticOwner(target, overrideKey, property);
|
|
5673
5965
|
notifyStoreProperty(target, property, "invalidate");
|
|
5674
5966
|
if (true) {
|
|
5675
5967
|
const next =
|
|
@@ -5699,9 +5991,12 @@ const storeTraps = {
|
|
|
5699
5991
|
) {
|
|
5700
5992
|
armOptimisticStoreWrite(target, target[$PROXY]);
|
|
5701
5993
|
(target[overrideKey] || (target[overrideKey] = Object.create(null)))[property] = $DELETED;
|
|
5994
|
+
stampOptimisticOwner(target, overrideKey, property);
|
|
5702
5995
|
} else if (target[overrideKey] && property in target[overrideKey]) {
|
|
5703
5996
|
armOptimisticStoreWrite(target, target[$PROXY]);
|
|
5704
5997
|
delete target[overrideKey][property];
|
|
5998
|
+
if (overrideKey === STORE_OPTIMISTIC_OVERRIDE)
|
|
5999
|
+
delete target[STORE_OPTIMISTIC_OWNERS]?.[property];
|
|
5705
6000
|
} else return true;
|
|
5706
6001
|
notifyStoreProperty(target, property, "delete", undefined, prev, true);
|
|
5707
6002
|
});
|
|
@@ -5710,7 +6005,15 @@ const storeTraps = {
|
|
|
5710
6005
|
},
|
|
5711
6006
|
ownKeys(target) {
|
|
5712
6007
|
if (pendingCheckActive) witnessAffectsMark(target);
|
|
5713
|
-
if (getObserver() !== target[STORE_FIREWALL])
|
|
6008
|
+
if (getObserver() !== target[STORE_FIREWALL]) {
|
|
6009
|
+
trackSelf(target);
|
|
6010
|
+
// trackSelf no-ops untracked, so enumeration of an unresolved derived
|
|
6011
|
+
// store would otherwise leak the seed's structure (#2897). The write
|
|
6012
|
+
// path is exempt (like the get/has traps' writeOnly early returns):
|
|
6013
|
+
// the first landing's reconcile enumerates the store while
|
|
6014
|
+
// STATUS_UNINITIALIZED is still set — it IS the initialization.
|
|
6015
|
+
if (!getObserver() && !writeOnly(target[$PROXY])) throwIfUninitialized(target);
|
|
6016
|
+
}
|
|
5714
6017
|
// Merge optimistic override with regular override for key enumeration
|
|
5715
6018
|
let keys = getKeys(target[STORE_VALUE], target[STORE_OVERRIDE], false);
|
|
5716
6019
|
if (target[STORE_OPTIMISTIC_OVERRIDE]) {
|
|
@@ -5852,7 +6155,7 @@ function propagateAffectsMark(node) {
|
|
|
5852
6155
|
const sentinel = getAffectsSentinel(node);
|
|
5853
6156
|
const error = new NotReadyError(sentinel);
|
|
5854
6157
|
forEachDependent(node, sub => {
|
|
5855
|
-
if (
|
|
6158
|
+
if (!sub._pendingSources?.has(sentinel)) {
|
|
5856
6159
|
notifyStatus(sub, STATUS_PENDING, error);
|
|
5857
6160
|
}
|
|
5858
6161
|
});
|
|
@@ -5895,7 +6198,7 @@ function markAffects(node) {
|
|
|
5895
6198
|
/**
|
|
5896
6199
|
* Registers one `affects()` mark on a node: counts it, records the
|
|
5897
6200
|
* registration with the current transaction (after initTransition the queue's
|
|
5898
|
-
*
|
|
6201
|
+
* batch IS the active transition, mirroring `_optimisticNodes`), and
|
|
5899
6202
|
* propagates STATUS_PENDING downstream on the status rails so everything
|
|
5900
6203
|
* DERIVED from the marked data reads pending too. Propagation runs on every
|
|
5901
6204
|
* registration (not just the first): subscribers gained since an earlier
|
|
@@ -5903,7 +6206,7 @@ function markAffects(node) {
|
|
|
5903
6206
|
*/
|
|
5904
6207
|
function registerAffectsMark(node) {
|
|
5905
6208
|
markAffects(node);
|
|
5906
|
-
globalQueue._affectsNodes.push(node);
|
|
6209
|
+
globalQueue._batch._affectsNodes.push(node);
|
|
5907
6210
|
propagateAffectsMark(node);
|
|
5908
6211
|
schedule();
|
|
5909
6212
|
}
|
|
@@ -5946,7 +6249,7 @@ function onlyMarkPending(el) {
|
|
|
5946
6249
|
for (const s of sources) if (!s._affectsFor) return false;
|
|
5947
6250
|
return true;
|
|
5948
6251
|
}
|
|
5949
|
-
return
|
|
6252
|
+
return false;
|
|
5950
6253
|
}
|
|
5951
6254
|
/**
|
|
5952
6255
|
* Collect the still-live marked nodes behind a pended owner's sentinel
|
|
@@ -5960,11 +6263,7 @@ function onlyMarkPending(el) {
|
|
|
5960
6263
|
* `GlobalQueue._collectMarkSources`, gated on `activeAffectsMarks`.
|
|
5961
6264
|
*/
|
|
5962
6265
|
function collectMarkSources(el, into) {
|
|
5963
|
-
|
|
5964
|
-
if (single) {
|
|
5965
|
-
const marked = single._affectsFor;
|
|
5966
|
-
if (marked && marked._affectsCount) into.push(marked);
|
|
5967
|
-
} else if (el._pendingSources) {
|
|
6266
|
+
if (el._pendingSources) {
|
|
5968
6267
|
for (const s of el._pendingSources) {
|
|
5969
6268
|
const marked = s._affectsFor;
|
|
5970
6269
|
if (marked && marked._affectsCount) into.push(marked);
|
|
@@ -6051,54 +6350,90 @@ function createOptimisticStore(first, second, options) {
|
|
|
6051
6350
|
}
|
|
6052
6351
|
// Clear the optimistic overrides of a settling batch of stores and notify
|
|
6053
6352
|
// signals. Owns the whole batch (iterate + clear + reschedule) so the
|
|
6054
|
-
// scheduler's flush tail carries only a size-guarded hook call.
|
|
6055
|
-
|
|
6353
|
+
// scheduler's flush tail carries only a size-guarded hook call. The
|
|
6354
|
+
// completing transition scopes each clear to its own layer keys (#2899).
|
|
6355
|
+
function clearOptimisticStores(stores, completing) {
|
|
6056
6356
|
for (const store of stores) {
|
|
6057
6357
|
const target = store[$TARGET];
|
|
6058
|
-
if (target?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(target);
|
|
6358
|
+
if (target?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(target, completing);
|
|
6059
6359
|
}
|
|
6060
6360
|
stores.clear();
|
|
6061
6361
|
schedule();
|
|
6062
6362
|
}
|
|
6063
|
-
|
|
6363
|
+
/**
|
|
6364
|
+
* Consume optimistic layer entries and reset their backing nodes to base.
|
|
6365
|
+
* With `completing` (settle path, #2899) only entries the settling
|
|
6366
|
+
* transaction owns are consumed — the layer is store-wide but concurrent
|
|
6367
|
+
* actions revert independently, so keys stamped by a still-in-flight
|
|
6368
|
+
* transition survive (node-level overrides already have this granularity via
|
|
6369
|
+
* _optimisticNodes; this is the layer's half). `null` consumes ambient
|
|
6370
|
+
* (transaction-less) entries at plain flush end. Omitted (projection landing:
|
|
6371
|
+
* fresh authoritative data) consumes everything — the correction supersedes
|
|
6372
|
+
* every tentative layer.
|
|
6373
|
+
*/
|
|
6374
|
+
function clearOptimisticOverride(target, completing) {
|
|
6064
6375
|
const override = target[STORE_OPTIMISTIC_OVERRIDE];
|
|
6065
6376
|
if (!override) return;
|
|
6066
6377
|
const nodes = target[STORE_NODE];
|
|
6067
|
-
|
|
6068
|
-
|
|
6378
|
+
const owners = target[STORE_OPTIMISTIC_OWNERS];
|
|
6379
|
+
const scoped = completing !== undefined;
|
|
6380
|
+
let cleared = false;
|
|
6381
|
+
let remaining = false;
|
|
6069
6382
|
// Use projectionWriteActive to bypass optimistic signal behavior (no lane creation)
|
|
6070
6383
|
// This ensures reversion effects go to regular queues, not lane queues
|
|
6071
6384
|
const wasProjectionWriteActive = projectionWriteActive;
|
|
6072
6385
|
setProjectionWriteActive(true);
|
|
6073
6386
|
try {
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
node._pendingValue = NOT_PENDING;
|
|
6089
|
-
node._value = next;
|
|
6090
|
-
if (!node._equals || !node._equals(prev, next)) {
|
|
6091
|
-
insertSubs(node, true);
|
|
6092
|
-
schedule();
|
|
6387
|
+
for (const key of Reflect.ownKeys(override)) {
|
|
6388
|
+
if (scoped) {
|
|
6389
|
+
let owner = owners?.[key] ?? null;
|
|
6390
|
+
// Resolve merge chains (entangled actions settle as one); path-compress
|
|
6391
|
+
// so later keys skip the walk. A dead owner (`_done === true`) settled
|
|
6392
|
+
// through some other path — never strand its entry. A null owner is an
|
|
6393
|
+
// ambient write: its batch belongs to whichever transaction adopted it
|
|
6394
|
+
// (initTransition mid-batch) or to the plain flush, so it clears on
|
|
6395
|
+
// whichever clear call reaches this store first.
|
|
6396
|
+
if (owner) {
|
|
6397
|
+
if (typeof owner._done === "object") owner = owners[key] = currentTransition(owner);
|
|
6398
|
+
if (owner !== completing && owner._done !== true) {
|
|
6399
|
+
remaining = true;
|
|
6400
|
+
continue;
|
|
6093
6401
|
}
|
|
6094
6402
|
}
|
|
6095
6403
|
}
|
|
6096
|
-
|
|
6097
|
-
if (
|
|
6098
|
-
|
|
6099
|
-
|
|
6404
|
+
delete override[key];
|
|
6405
|
+
if (owners) delete owners[key];
|
|
6406
|
+
cleared = true;
|
|
6407
|
+
const node = nodes?.[key];
|
|
6408
|
+
if (node) {
|
|
6409
|
+
// Clear lane association so effects go to regular queue
|
|
6410
|
+
node._optimisticLane = undefined;
|
|
6411
|
+
// Re-read from base — this key left the optimistic layer above, so the
|
|
6412
|
+
// overlay resolves to STORE_OVERRIDE or STORE_VALUE.
|
|
6413
|
+
const layer = getOverlayLayer(target, key);
|
|
6414
|
+
const baseValue = layer ? layer[key] : target[STORE_VALUE][key];
|
|
6415
|
+
const value = baseValue === $DELETED ? undefined : baseValue;
|
|
6416
|
+
const next = isWrappable(value) ? wrap(value, target) : value;
|
|
6417
|
+
const prev = visibleNodeValue(node);
|
|
6418
|
+
node._overrideValue = NOT_PENDING;
|
|
6419
|
+
node._overrideOwner = null;
|
|
6420
|
+
node._pendingValue = NOT_PENDING;
|
|
6421
|
+
node._value = next;
|
|
6422
|
+
if (!node._equals || !node._equals(prev, next)) {
|
|
6423
|
+
insertSubs(node, true);
|
|
6424
|
+
schedule();
|
|
6425
|
+
}
|
|
6100
6426
|
}
|
|
6101
6427
|
}
|
|
6428
|
+
if (!remaining) {
|
|
6429
|
+
delete target[STORE_OPTIMISTIC_OVERRIDE];
|
|
6430
|
+
delete target[STORE_OPTIMISTIC_OWNERS];
|
|
6431
|
+
}
|
|
6432
|
+
// Notify $TRACK
|
|
6433
|
+
if (cleared && nodes?.[$TRACK]) {
|
|
6434
|
+
nodes[$TRACK]._optimisticLane = undefined;
|
|
6435
|
+
notifySelf(target);
|
|
6436
|
+
}
|
|
6102
6437
|
} finally {
|
|
6103
6438
|
setProjectionWriteActive(wasProjectionWriteActive);
|
|
6104
6439
|
}
|
|
@@ -6309,6 +6644,21 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6309
6644
|
result[i] = unwrapped;
|
|
6310
6645
|
}
|
|
6311
6646
|
}
|
|
6647
|
+
// Enumerate array symbols separately to avoid scanning indices twice.
|
|
6648
|
+
// Spread copies omit symbols, so assign them after the numeric walk.
|
|
6649
|
+
const symbols = lookup ? getStoreSymbols(item, override) : [];
|
|
6650
|
+
for (let i = 0, l = symbols.length; i < l; i++) {
|
|
6651
|
+
const prop = symbols[i];
|
|
6652
|
+
const desc = getPropertyDescriptor(item, override, prop);
|
|
6653
|
+
if (!desc || desc.get) continue;
|
|
6654
|
+
v = override && prop in override ? override[prop] : item[prop];
|
|
6655
|
+
if (track && isWrappable(v)) wrap(v, target);
|
|
6656
|
+
unwrapped = snapshotImpl(v, track, map, lookup);
|
|
6657
|
+
if (unwrapped !== v || result) {
|
|
6658
|
+
if (!result) map.set(item, (result = Object.assign([...item], item)));
|
|
6659
|
+
result[prop] = unwrapped;
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6312
6662
|
// Deleted trailing slots are skipped above, so restore length to preserve
|
|
6313
6663
|
// holes instead of truncating the copy (#2846) — mirrors unwrapStoreValue.
|
|
6314
6664
|
if (result) result.length = len;
|
|
@@ -6316,7 +6666,9 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6316
6666
|
// Specialized walk for the common no-overlay case (from #2756): the own
|
|
6317
6667
|
// descriptor gives the value directly, so each property is read once with
|
|
6318
6668
|
// no overlay membership checks.
|
|
6319
|
-
|
|
6669
|
+
// A lookup means this object belongs to an immutable store backing tree,
|
|
6670
|
+
// even if that nested value has not needed its own proxy yet.
|
|
6671
|
+
const keys = lookup ? getStoreKeys(item, undefined) : getKeys(item, undefined);
|
|
6320
6672
|
for (let i = 0, l = keys.length; i < l; i++) {
|
|
6321
6673
|
const prop = keys[i];
|
|
6322
6674
|
const desc = Object.getOwnPropertyDescriptor(item, prop);
|
|
@@ -6332,7 +6684,9 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6332
6684
|
}
|
|
6333
6685
|
}
|
|
6334
6686
|
} else {
|
|
6335
|
-
|
|
6687
|
+
// An override only exists on a store record, and the target branch above
|
|
6688
|
+
// always set `lookup` alongside it — so this branch is always store-keyed.
|
|
6689
|
+
const keys = getStoreKeys(item, override);
|
|
6336
6690
|
for (let i = 0, l = keys.length; i < l; i++) {
|
|
6337
6691
|
let prop = keys[i];
|
|
6338
6692
|
const desc = getPropertyDescriptor(item, override, prop);
|
|
@@ -6599,6 +6953,15 @@ function mapArray(list, map, options) {
|
|
|
6599
6953
|
return accessor(node);
|
|
6600
6954
|
}
|
|
6601
6955
|
const pureOptions = { ownedWrite: true };
|
|
6956
|
+
// Exception safety (#2903): a map callback can throw NotReadyError mid-pass
|
|
6957
|
+
// (async read), and the computed re-runs the whole pass after settle. Every
|
|
6958
|
+
// pass therefore STAGES its work — new rows are created into temp arrays and
|
|
6959
|
+
// removals are deferred — and commits to `this` only after every mapper
|
|
6960
|
+
// succeeded. An aborted pass disposes just the owners it created and leaves
|
|
6961
|
+
// `_items`/`_mappings`/`_nodes`/`_rows`/`_indexes`/`_len` exactly as they
|
|
6962
|
+
// were, so the retry diffs against uncorrupted state. Consequence of the
|
|
6963
|
+
// strong-abort ordering: removed rows now dispose AFTER the pass's new rows
|
|
6964
|
+
// are created (you cannot destroy state before knowing the pass will land).
|
|
6602
6965
|
function updateKeyedMap() {
|
|
6603
6966
|
const newItems = this._list() || [],
|
|
6604
6967
|
newLen = newItems.length;
|
|
@@ -6606,25 +6969,26 @@ function updateKeyedMap() {
|
|
|
6606
6969
|
runWithOwner(this._owner, () => {
|
|
6607
6970
|
let i,
|
|
6608
6971
|
j,
|
|
6972
|
+
rows,
|
|
6973
|
+
indexes,
|
|
6974
|
+
// Mappers write freshly-created row/index signals into the STAGE
|
|
6975
|
+
// arrays (`rows`/`indexes`), never into `this._rows`/`this._indexes`.
|
|
6609
6976
|
mapper = this._rows
|
|
6610
6977
|
? this._byIndex
|
|
6611
6978
|
? () => {
|
|
6612
|
-
|
|
6613
|
-
return this._map(accessor(
|
|
6979
|
+
rows[j] = signal(newItems[j], pureOptions);
|
|
6980
|
+
return this._map(accessor(rows[j]), j);
|
|
6614
6981
|
}
|
|
6615
6982
|
: () => {
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
return this._map(
|
|
6619
|
-
accessor(this._rows[j]),
|
|
6620
|
-
this._indexes ? accessor(this._indexes[j]) : undefined
|
|
6621
|
-
);
|
|
6983
|
+
rows[j] = signal(newItems[j], pureOptions);
|
|
6984
|
+
indexes && (indexes[j] = signal(j, pureOptions));
|
|
6985
|
+
return this._map(accessor(rows[j]), indexes ? accessor(indexes[j]) : undefined);
|
|
6622
6986
|
}
|
|
6623
6987
|
: this._indexes
|
|
6624
6988
|
? () => {
|
|
6625
6989
|
const item = newItems[j];
|
|
6626
|
-
|
|
6627
|
-
return this._map(item, accessor(
|
|
6990
|
+
indexes[j] = signal(j, pureOptions);
|
|
6991
|
+
return this._map(item, accessor(indexes[j]));
|
|
6628
6992
|
}
|
|
6629
6993
|
: () => {
|
|
6630
6994
|
const item = newItems[j];
|
|
@@ -6642,19 +7006,31 @@ function updateKeyedMap() {
|
|
|
6642
7006
|
this._indexes && (this._indexes = []);
|
|
6643
7007
|
}
|
|
6644
7008
|
if (this._fallback && !this._mappings[0]) {
|
|
6645
|
-
//
|
|
7009
|
+
// an aborted fallback attempt leaves an owner without a mapping;
|
|
7010
|
+
// dispose it before re-creating
|
|
7011
|
+
this._nodes[0]?.dispose();
|
|
6646
7012
|
this._mappings[0] = runWithOwner((this._nodes[0] = createOwner()), this._fallback);
|
|
6647
7013
|
}
|
|
6648
7014
|
}
|
|
6649
7015
|
// fast path for new create
|
|
6650
7016
|
else if (this._len === 0) {
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
this.
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
7017
|
+
const mappings = new Array(newLen);
|
|
7018
|
+
const nodes = new Array(newLen);
|
|
7019
|
+
rows = this._rows && new Array(newLen);
|
|
7020
|
+
indexes = this._indexes && new Array(newLen);
|
|
7021
|
+
try {
|
|
7022
|
+
for (j = 0; j < newLen; j++) mappings[j] = runWithOwner((nodes[j] = createOwner()), mapper);
|
|
7023
|
+
} catch (err) {
|
|
7024
|
+
for (i = 0; i <= j; i++) nodes[i]?.dispose();
|
|
7025
|
+
throw err;
|
|
6657
7026
|
}
|
|
7027
|
+
// commit
|
|
7028
|
+
if (this._nodes[0]) this._nodes[0].dispose(); // previous fallback
|
|
7029
|
+
this._mappings = mappings;
|
|
7030
|
+
this._nodes = nodes;
|
|
7031
|
+
rows && (this._rows = rows);
|
|
7032
|
+
indexes && (this._indexes = indexes);
|
|
7033
|
+
this._items = newItems.slice(0);
|
|
6658
7034
|
this._len = newLen;
|
|
6659
7035
|
} else {
|
|
6660
7036
|
let start,
|
|
@@ -6664,10 +7040,12 @@ function updateKeyedMap() {
|
|
|
6664
7040
|
key,
|
|
6665
7041
|
newIndices,
|
|
6666
7042
|
newIndicesNext,
|
|
7043
|
+
removed,
|
|
7044
|
+
created,
|
|
6667
7045
|
temp = new Array(newLen),
|
|
6668
|
-
tempNodes = new Array(newLen)
|
|
6669
|
-
|
|
6670
|
-
|
|
7046
|
+
tempNodes = new Array(newLen);
|
|
7047
|
+
rows = this._rows ? new Array(newLen) : undefined;
|
|
7048
|
+
indexes = this._indexes ? new Array(newLen) : undefined;
|
|
6671
7049
|
// skip common prefix
|
|
6672
7050
|
for (
|
|
6673
7051
|
start = 0, end = Math.min(this._len, newLen);
|
|
@@ -6689,8 +7067,8 @@ function updateKeyedMap() {
|
|
|
6689
7067
|
) {
|
|
6690
7068
|
temp[newEnd] = this._mappings[end];
|
|
6691
7069
|
tempNodes[newEnd] = this._nodes[end];
|
|
6692
|
-
|
|
6693
|
-
|
|
7070
|
+
rows && (rows[newEnd] = this._rows[end]);
|
|
7071
|
+
indexes && (indexes[newEnd] = this._indexes[end]);
|
|
6694
7072
|
}
|
|
6695
7073
|
// 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
|
|
6696
7074
|
newIndices = new Map();
|
|
@@ -6702,7 +7080,7 @@ function updateKeyedMap() {
|
|
|
6702
7080
|
newIndicesNext[j] = i === undefined ? -1 : i;
|
|
6703
7081
|
newIndices.set(key, j);
|
|
6704
7082
|
}
|
|
6705
|
-
// 1) step through all old items and see if they can be found in the new set; if so, save them in a temp array and mark them moved; if not,
|
|
7083
|
+
// 1) step through all old items and see if they can be found in the new set; if so, save them in a temp array and mark them moved; if not, queue them for disposal at commit
|
|
6706
7084
|
for (i = start; i <= end; i++) {
|
|
6707
7085
|
item = this._items[i];
|
|
6708
7086
|
key = this._key ? this._key(item) : item;
|
|
@@ -6710,32 +7088,40 @@ function updateKeyedMap() {
|
|
|
6710
7088
|
if (j !== undefined && j !== -1) {
|
|
6711
7089
|
temp[j] = this._mappings[i];
|
|
6712
7090
|
tempNodes[j] = this._nodes[i];
|
|
6713
|
-
|
|
6714
|
-
|
|
7091
|
+
rows && (rows[j] = this._rows[i]);
|
|
7092
|
+
indexes && (indexes[j] = this._indexes[i]);
|
|
6715
7093
|
j = newIndicesNext[j];
|
|
6716
7094
|
newIndices.set(key, j);
|
|
6717
|
-
} else this._nodes[i]
|
|
7095
|
+
} else (removed ??= []).push(this._nodes[i]);
|
|
6718
7096
|
}
|
|
6719
|
-
// 2)
|
|
7097
|
+
// 2) create new rows into the temp arrays; an abort disposes only these
|
|
7098
|
+
try {
|
|
7099
|
+
for (j = start; j < newLen; j++) {
|
|
7100
|
+
if (j in temp) continue;
|
|
7101
|
+
(created ??= []).push((tempNodes[j] = createOwner()));
|
|
7102
|
+
temp[j] = runWithOwner(tempNodes[j], mapper);
|
|
7103
|
+
}
|
|
7104
|
+
} catch (err) {
|
|
7105
|
+
if (created) for (i = 0; i < created.length; i++) created[i].dispose();
|
|
7106
|
+
throw err;
|
|
7107
|
+
}
|
|
7108
|
+
// 3) commit: land positions, then dispose exited rows
|
|
6720
7109
|
for (j = start; j < newLen; j++) {
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
setSignal(this._indexes[j], j);
|
|
6731
|
-
}
|
|
6732
|
-
} else {
|
|
6733
|
-
this._mappings[j] = runWithOwner((this._nodes[j] = createOwner()), mapper);
|
|
7110
|
+
this._mappings[j] = temp[j];
|
|
7111
|
+
this._nodes[j] = tempNodes[j];
|
|
7112
|
+
if (rows) {
|
|
7113
|
+
this._rows[j] = rows[j];
|
|
7114
|
+
setSignal(this._rows[j], newItems[j]);
|
|
7115
|
+
}
|
|
7116
|
+
if (indexes) {
|
|
7117
|
+
this._indexes[j] = indexes[j];
|
|
7118
|
+
setSignal(this._indexes[j], j);
|
|
6734
7119
|
}
|
|
6735
7120
|
}
|
|
6736
|
-
|
|
7121
|
+
if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
|
|
7122
|
+
// 4) in case the new set is shorter than the old, set the length of the mapped array
|
|
6737
7123
|
this._mappings = this._mappings.slice(0, (this._len = newLen));
|
|
6738
|
-
//
|
|
7124
|
+
// 5) save a copy of the mapped items for the next update
|
|
6739
7125
|
this._items = newItems.slice(0);
|
|
6740
7126
|
}
|
|
6741
7127
|
});
|
|
@@ -6767,22 +7153,32 @@ function repeat(count, map, options) {
|
|
|
6767
7153
|
}
|
|
6768
7154
|
}
|
|
6769
7155
|
: map;
|
|
6770
|
-
const
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
7156
|
+
const data = {
|
|
7157
|
+
_owner: createOwner(),
|
|
7158
|
+
_len: 0,
|
|
7159
|
+
_offset: 0,
|
|
7160
|
+
_count: count,
|
|
7161
|
+
_map: wrappedMap,
|
|
7162
|
+
_nodes: [],
|
|
7163
|
+
_mappings: [],
|
|
7164
|
+
_from: options?.from,
|
|
7165
|
+
_fallback: options?.fallback
|
|
7166
|
+
};
|
|
7167
|
+
const node = computed(updateRepeat.bind(data));
|
|
7168
|
+
// Same as mapArray: untracked reads inside the internal owner resolve via
|
|
7169
|
+
// _parentComputed, so async reads in row callbacks register with the node
|
|
7170
|
+
// (pending tracking + post-settle retry) instead of vanishing.
|
|
7171
|
+
data._owner._parentComputed = node;
|
|
6783
7172
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
6784
7173
|
return accessor(node);
|
|
6785
7174
|
}
|
|
7175
|
+
// Same staged-commit discipline as `updateKeyedMap` (#2903): the retained
|
|
7176
|
+
// window overlap is copied into fresh arrays, missing indexes are created
|
|
7177
|
+
// into them, and `this` is only touched — including disposal of rows leaving
|
|
7178
|
+
// the window — after every `_map` call succeeded. A NotReadyError mid-pass
|
|
7179
|
+
// disposes only the owners this pass created and leaves prior state intact
|
|
7180
|
+
// for the post-settle retry. The overlap math also subsumes the previous
|
|
7181
|
+
// disjoint-window/front-clear/end-clear/shift special cases.
|
|
6786
7182
|
function updateRepeat() {
|
|
6787
7183
|
const newLen = this._count();
|
|
6788
7184
|
const from = this._from?.() || 0;
|
|
@@ -6793,67 +7189,46 @@ function updateRepeat() {
|
|
|
6793
7189
|
this._nodes = [];
|
|
6794
7190
|
this._mappings = [];
|
|
6795
7191
|
this._len = 0;
|
|
6796
|
-
// Reset offset to match the cleared data
|
|
6797
|
-
// nonzero render with a smaller `from` would enter the end-clear loop
|
|
6798
|
-
// with `prevTo = stale_offset + 0` > `to` and dispose `_nodes[-1]`
|
|
6799
|
-
// (#2767, repro 2).
|
|
7192
|
+
// Reset offset to match the cleared data (#2767, repro 2).
|
|
6800
7193
|
this._offset = 0;
|
|
6801
7194
|
}
|
|
6802
7195
|
if (this._fallback && !this._mappings[0]) {
|
|
6803
|
-
//
|
|
7196
|
+
// an aborted fallback attempt leaves an owner without a mapping;
|
|
7197
|
+
// dispose it before re-creating
|
|
7198
|
+
this._nodes[0]?.dispose();
|
|
6804
7199
|
this._mappings[0] = runWithOwner((this._nodes[0] = createOwner()), this._fallback);
|
|
6805
7200
|
}
|
|
6806
7201
|
return;
|
|
6807
7202
|
}
|
|
6808
7203
|
const to = from + newLen;
|
|
6809
7204
|
const prevTo = this._offset + this._len;
|
|
6810
|
-
//
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
this._map(from + i)
|
|
6820
|
-
);
|
|
6821
|
-
this._offset = from;
|
|
6822
|
-
this._len = newLen;
|
|
6823
|
-
return;
|
|
7205
|
+
// Retained overlap [keepStart, keepEnd) in global indexes; empty when the
|
|
7206
|
+
// windows are disjoint or when coming from empty/fallback.
|
|
7207
|
+
const keepStart = Math.max(from, this._offset);
|
|
7208
|
+
const keepEnd = Math.min(to, prevTo);
|
|
7209
|
+
const mappings = new Array(newLen);
|
|
7210
|
+
const nodes = new Array(newLen);
|
|
7211
|
+
for (let i = keepStart; i < keepEnd; i++) {
|
|
7212
|
+
nodes[i - from] = this._nodes[i - this._offset];
|
|
7213
|
+
mappings[i - from] = this._mappings[i - this._offset];
|
|
6824
7214
|
}
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
// front sit at local positions 0..(from - _offset), clamped to old len.
|
|
6830
|
-
const removed = from - this._offset;
|
|
6831
|
-
for (let i = 0; i < removed && i < this._len; i++) this._nodes[i].dispose();
|
|
6832
|
-
// shift indexes
|
|
6833
|
-
this._nodes.splice(0, removed);
|
|
6834
|
-
this._mappings.splice(0, removed);
|
|
6835
|
-
} else if (this._offset > from) {
|
|
6836
|
-
// shift indexes
|
|
6837
|
-
let i = prevTo - this._offset - 1;
|
|
6838
|
-
let difference = this._offset - from;
|
|
6839
|
-
this._nodes.length = this._mappings.length = newLen;
|
|
6840
|
-
while (i >= difference) {
|
|
6841
|
-
this._nodes[i] = this._nodes[i - difference];
|
|
6842
|
-
this._mappings[i] = this._mappings[i - difference];
|
|
6843
|
-
i--;
|
|
6844
|
-
}
|
|
6845
|
-
for (let i = 0; i < difference; i++) {
|
|
6846
|
-
this._mappings[i] = runWithOwner((this._nodes[i] = createOwner()), () =>
|
|
6847
|
-
this._map(i + from)
|
|
6848
|
-
);
|
|
7215
|
+
try {
|
|
7216
|
+
for (let i = from; i < to; i++) {
|
|
7217
|
+
if (i >= keepStart && i < keepEnd) continue;
|
|
7218
|
+
mappings[i - from] = runWithOwner((nodes[i - from] = createOwner()), () => this._map(i));
|
|
6849
7219
|
}
|
|
6850
|
-
}
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
this.
|
|
7220
|
+
} catch (err) {
|
|
7221
|
+
for (let i = from; i < to; i++)
|
|
7222
|
+
if ((i < keepStart || i >= keepEnd) && nodes[i - from]) nodes[i - from].dispose();
|
|
7223
|
+
throw err;
|
|
7224
|
+
}
|
|
7225
|
+
// commit: dispose the previous fallback or the rows leaving the window
|
|
7226
|
+
if (this._len === 0) this._nodes[0]?.dispose();
|
|
7227
|
+
else
|
|
7228
|
+
for (let i = this._offset; i < prevTo; i++)
|
|
7229
|
+
if (i < from || i >= to) this._nodes[i - this._offset].dispose();
|
|
7230
|
+
this._mappings = mappings;
|
|
7231
|
+
this._nodes = nodes;
|
|
6857
7232
|
this._offset = from;
|
|
6858
7233
|
this._len = newLen;
|
|
6859
7234
|
});
|