@solidjs/signals 2.0.0-beta.19 → 2.0.0-beta.20
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/dist/dev.js +670 -371
- package/dist/node.cjs +1878 -1606
- package/dist/prod/affects.js +17 -21
- package/dist/prod/core/action.js +19 -6
- 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 +2 -2
- 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 +180 -180
- package/dist/prod/core/verdict.js +12 -13
- 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 +19 -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 +19 -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,9 @@ class GlobalQueue extends Queue {
|
|
|
651
730
|
if (!isComplete) {
|
|
652
731
|
const stashedTransition = activeTransition;
|
|
653
732
|
runHeap(zombieQueue, GlobalQueue._update);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
this.
|
|
657
|
-
this._affectsNodes = [];
|
|
658
|
-
this._optimisticStores = new Set();
|
|
733
|
+
// Detach: the stashed transition keeps its batch; ambient work that
|
|
734
|
+
// follows lands in a fresh one.
|
|
735
|
+
currentBatch = this._batch = createBatch();
|
|
659
736
|
// Run lane effects immediately (before stashing) - lanes with no pending async
|
|
660
737
|
if (activeLanes.size) {
|
|
661
738
|
GlobalQueue._runLaneEffects(EFFECT_RENDER);
|
|
@@ -680,14 +757,26 @@ class GlobalQueue extends Queue {
|
|
|
680
757
|
}
|
|
681
758
|
return;
|
|
682
759
|
}
|
|
683
|
-
this._pendingNodes !== activeTransition._pendingNodes &&
|
|
684
|
-
this._pendingNodes.push(...activeTransition._pendingNodes);
|
|
685
|
-
this.restoreQueues(activeTransition._queueStash);
|
|
686
|
-
transitions.delete(activeTransition);
|
|
687
760
|
const completingTransition = activeTransition;
|
|
761
|
+
const batch = this._batch;
|
|
762
|
+
batch !== completingTransition &&
|
|
763
|
+
batch._pendingNodes.push(...completingTransition._pendingNodes);
|
|
764
|
+
this.restoreQueues(completingTransition._queueStash);
|
|
765
|
+
transitions.delete(completingTransition);
|
|
688
766
|
activeTransition = null;
|
|
689
|
-
reassignPendingTransition(
|
|
767
|
+
reassignPendingTransition(batch._pendingNodes);
|
|
690
768
|
finalizePureQueue(completingTransition);
|
|
769
|
+
if (batch === completingTransition) {
|
|
770
|
+
// Drop the dead Transition wrapper but keep its (drained) containers
|
|
771
|
+
// as the ambient batch — late registrations during finalization live
|
|
772
|
+
// there and must survive to the next flush.
|
|
773
|
+
const fresh = createBatch();
|
|
774
|
+
fresh._pendingNodes = batch._pendingNodes;
|
|
775
|
+
fresh._optimisticNodes = batch._optimisticNodes;
|
|
776
|
+
fresh._affectsNodes = batch._affectsNodes;
|
|
777
|
+
fresh._optimisticStores = batch._optimisticStores;
|
|
778
|
+
currentBatch = this._batch = fresh;
|
|
779
|
+
}
|
|
691
780
|
} else {
|
|
692
781
|
if (canUseSimpleSyncFlush(this)) {
|
|
693
782
|
commitPendingNodes();
|
|
@@ -710,12 +799,12 @@ class GlobalQueue extends Queue {
|
|
|
710
799
|
this.run(EFFECT_USER);
|
|
711
800
|
if (true) {
|
|
712
801
|
devCheckActiveOverrides(n => {
|
|
713
|
-
if (this._optimisticNodes.includes(n)) return true;
|
|
802
|
+
if (this._batch._optimisticNodes.includes(n)) return true;
|
|
714
803
|
if (activeTransition?._optimisticNodes.includes(n)) return true;
|
|
715
804
|
for (const t of transitions) if (t._optimisticNodes.includes(n)) return true;
|
|
716
805
|
return false;
|
|
717
806
|
});
|
|
718
|
-
devCensusCompanions(n =>
|
|
807
|
+
devCensusCompanions(n => this._batch._pendingNodes.includes(n));
|
|
719
808
|
}
|
|
720
809
|
if (
|
|
721
810
|
true &&
|
|
@@ -725,7 +814,7 @@ class GlobalQueue extends Queue {
|
|
|
725
814
|
activeLanes.size === 0
|
|
726
815
|
) {
|
|
727
816
|
// Fully drained: no transition-scoped state may survive this point.
|
|
728
|
-
devCheckQuiescent(n =>
|
|
817
|
+
devCheckQuiescent(n => this._batch._pendingNodes.includes(n));
|
|
729
818
|
}
|
|
730
819
|
if (true) DEV$1.hooks.onUpdate?.();
|
|
731
820
|
} finally {
|
|
@@ -756,18 +845,7 @@ class GlobalQueue extends Queue {
|
|
|
756
845
|
if (transition && transition === activeTransition) return;
|
|
757
846
|
if (!transition && activeTransition && activeTransition._time === clock) return;
|
|
758
847
|
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
|
-
};
|
|
848
|
+
activeTransition = transition ?? createBatch();
|
|
771
849
|
} else if (transition) {
|
|
772
850
|
const outgoing = activeTransition;
|
|
773
851
|
mergeTransitionState(transition, outgoing);
|
|
@@ -776,60 +854,36 @@ class GlobalQueue extends Queue {
|
|
|
776
854
|
}
|
|
777
855
|
transitions.add(activeTransition);
|
|
778
856
|
activeTransition._time = clock;
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
857
|
+
const batch = this._batch;
|
|
858
|
+
if (batch !== activeTransition) {
|
|
859
|
+
// Adopt the ambient batch into the transaction, then make the
|
|
860
|
+
// transaction the batch so later registrations land there directly.
|
|
861
|
+
// Pending and optimistic nodes are re-stamped as the transaction's;
|
|
862
|
+
// marks don't hijack the node's _transition — a mark on a plain signal
|
|
863
|
+
// must not entangle unrelated writes to it; the same rule holds one hop
|
|
864
|
+
// downstream: propagation never queues pended subscribers as pending
|
|
865
|
+
// nodes, see propagateAffectsMark, #2893.
|
|
866
|
+
for (let i = 0; i < batch._pendingNodes.length; i++) {
|
|
867
|
+
const node = batch._pendingNodes[i];
|
|
787
868
|
node._transition = activeTransition;
|
|
788
869
|
activeTransition._pendingNodes.push(node);
|
|
789
870
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (this._optimisticNodes !== activeTransition._optimisticNodes) {
|
|
793
|
-
for (let i = 0; i < this._optimisticNodes.length; i++) {
|
|
794
|
-
const node = this._optimisticNodes[i];
|
|
871
|
+
for (let i = 0; i < batch._optimisticNodes.length; i++) {
|
|
872
|
+
const node = batch._optimisticNodes[i];
|
|
795
873
|
node._transition = activeTransition;
|
|
796
874
|
activeTransition._optimisticNodes.push(node);
|
|
797
875
|
}
|
|
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;
|
|
876
|
+
if (batch._affectsNodes.length) activeTransition._affectsNodes.push(...batch._affectsNodes);
|
|
877
|
+
for (const store of batch._optimisticStores) activeTransition._optimisticStores.add(store);
|
|
878
|
+
currentBatch = this._batch = activeTransition;
|
|
809
879
|
}
|
|
810
880
|
for (const lane of activeLanes) {
|
|
811
881
|
if (!lane._transition) lane._transition = activeTransition;
|
|
812
882
|
}
|
|
813
|
-
if (this._optimisticStores !== activeTransition._optimisticStores) {
|
|
814
|
-
for (const store of this._optimisticStores) activeTransition._optimisticStores.add(store);
|
|
815
|
-
this._optimisticStores = activeTransition._optimisticStores;
|
|
816
|
-
}
|
|
817
883
|
}
|
|
818
884
|
}
|
|
819
885
|
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);
|
|
886
|
+
currentBatch._pendingNodes.push(node);
|
|
833
887
|
}
|
|
834
888
|
// Sticky: flips true on the first refresh() ever (the only setter of
|
|
835
889
|
// REACTIVE_REASK) so the hot notification loop skips the per-subscriber flag
|
|
@@ -886,11 +940,7 @@ function commitPendingNode(n) {
|
|
|
886
940
|
if (n._pendingSignal || n._latestValueComputed) GlobalQueue._snapCompanions(n);
|
|
887
941
|
}
|
|
888
942
|
function commitPendingNodes() {
|
|
889
|
-
|
|
890
|
-
commitPendingNode(globalQueue._pendingNode);
|
|
891
|
-
globalQueue._pendingNode = null;
|
|
892
|
-
}
|
|
893
|
-
const pendingNodes = globalQueue._pendingNodes;
|
|
943
|
+
const pendingNodes = currentBatch._pendingNodes;
|
|
894
944
|
for (let i = 0; i < pendingNodes.length; i++) {
|
|
895
945
|
commitPendingNode(pendingNodes[i]);
|
|
896
946
|
}
|
|
@@ -906,12 +956,11 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
906
956
|
if (ranHeap) runHeap(dirtyQueue, GlobalQueue._update);
|
|
907
957
|
if (resolvePending) {
|
|
908
958
|
if (ranHeap) commitPendingNodes();
|
|
959
|
+
// The settling batch: the completing transaction's, or the ambient one.
|
|
960
|
+
const batch = completingTransition ?? globalQueue._batch;
|
|
909
961
|
// Optimistic reversion: a non-empty batch means _optimisticWrite ran,
|
|
910
962
|
// which installed the engine's hooks.
|
|
911
|
-
|
|
912
|
-
? completingTransition._optimisticNodes
|
|
913
|
-
: globalQueue._optimisticNodes;
|
|
914
|
-
if (optimisticNodes.length) GlobalQueue._resolveOptimistic(optimisticNodes);
|
|
963
|
+
if (batch._optimisticNodes.length) GlobalQueue._resolveOptimistic(batch._optimisticNodes);
|
|
915
964
|
// Replay entanglement: subs recorded by the read-time gate get rescheduled
|
|
916
965
|
// so they re-run with the now-committed values visible.
|
|
917
966
|
if (completingTransition && completingTransition._gatedSubs.size) {
|
|
@@ -924,17 +973,13 @@ function finalizePureQueue(completingTransition = null, incomplete = false) {
|
|
|
924
973
|
// Declared motion ends with the transaction: settle (or plain flush end
|
|
925
974
|
// for ambient marks) releases each registration's refcount. A non-empty
|
|
926
975
|
// 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;
|
|
976
|
+
if (batch._affectsNodes.length) GlobalQueue._releaseAffectsMarks(batch._affectsNodes);
|
|
934
977
|
// A non-empty set means trackOptimisticStore ran, which installed the
|
|
935
978
|
// 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
|
-
|
|
979
|
+
// core lets esbuild shake it — rollup already folds the null guard). The
|
|
980
|
+
// completing transition scopes the clear to its own layer keys (#2899).
|
|
981
|
+
if (batch._optimisticStores.size)
|
|
982
|
+
GlobalQueue._clearOptimisticStores(batch._optimisticStores, completingTransition);
|
|
938
983
|
sweepTransientStoreNodes();
|
|
939
984
|
// Lanes only enter activeLanes through the engine's getOrCreateLane.
|
|
940
985
|
if (activeLanes.size) GlobalQueue._cleanupLanes(completingTransition);
|
|
@@ -968,6 +1013,12 @@ function reassignPendingTransition(pendingNodes) {
|
|
|
968
1013
|
}
|
|
969
1014
|
}
|
|
970
1015
|
const globalQueue = new GlobalQueue();
|
|
1016
|
+
// Hot-path mirror of `globalQueue._batch`: `queuePendingNode` runs once per
|
|
1017
|
+
// staged write and `commitPendingNodes` once per flush, and the extra
|
|
1018
|
+
// property hop through `_batch` was a measured instruction-count regression
|
|
1019
|
+
// (CodSpeed update1to1, PR #2905). The field stays authoritative for
|
|
1020
|
+
// cross-module readers; every `_batch` assignment updates both.
|
|
1021
|
+
let currentBatch = globalQueue._batch;
|
|
971
1022
|
function flush(fn) {
|
|
972
1023
|
if (fn) {
|
|
973
1024
|
syncDepth++;
|
|
@@ -1005,7 +1056,7 @@ function runQueue$1(queue, type) {
|
|
|
1005
1056
|
}
|
|
1006
1057
|
function reporterBlocksSource(reporter, source) {
|
|
1007
1058
|
if (reporter._flags & (REACTIVE_ZOMBIE | REACTIVE_DISPOSED)) return false;
|
|
1008
|
-
if (reporter.
|
|
1059
|
+
if (reporter._pendingSources?.has(source)) return true;
|
|
1009
1060
|
for (let dep = reporter._deps; dep; dep = dep._nextDep) {
|
|
1010
1061
|
let current = dep._dep;
|
|
1011
1062
|
while (current) {
|
|
@@ -1585,38 +1636,22 @@ function link(dep, sub, pendingObserver = false) {
|
|
|
1585
1636
|
else dep._subs = newLink;
|
|
1586
1637
|
}
|
|
1587
1638
|
|
|
1639
|
+
// The lazily-created Set is the ONE container for pending sources. Its
|
|
1640
|
+
// predecessor — a singular slot promoted to a Set on the second source —
|
|
1641
|
+
// created dual state whose migration invariant was easy to break: a third
|
|
1642
|
+
// overlapping source landed beside the Set and removePendingSource refused
|
|
1643
|
+
// to clear it, stranding the Set members' pending forever (#2893).
|
|
1588
1644
|
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
|
-
}
|
|
1645
|
+
if (el._pendingSources?.has(source)) return false;
|
|
1646
|
+
(el._pendingSources ??= new Set()).add(source);
|
|
1601
1647
|
return true;
|
|
1602
1648
|
}
|
|
1603
1649
|
function removePendingSource(el, source) {
|
|
1604
|
-
if (el._pendingSource) {
|
|
1605
|
-
if (el._pendingSource !== source) return false;
|
|
1606
|
-
el._pendingSource = undefined;
|
|
1607
|
-
return true;
|
|
1608
|
-
}
|
|
1609
1650
|
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
|
-
}
|
|
1651
|
+
if (el._pendingSources.size === 0) el._pendingSources = undefined;
|
|
1616
1652
|
return true;
|
|
1617
1653
|
}
|
|
1618
1654
|
function clearPendingSources(el) {
|
|
1619
|
-
el._pendingSource = undefined;
|
|
1620
1655
|
el._pendingSources?.clear();
|
|
1621
1656
|
el._pendingSources = undefined;
|
|
1622
1657
|
}
|
|
@@ -1660,7 +1695,7 @@ function settlePendingSource(
|
|
|
1660
1695
|
if (visited.has(node) || !removePendingSource(node, source)) return;
|
|
1661
1696
|
visited.add(node);
|
|
1662
1697
|
node._time = clock;
|
|
1663
|
-
const remaining = node.
|
|
1698
|
+
const remaining = node._pendingSources?.values().next().value;
|
|
1664
1699
|
if (remaining) {
|
|
1665
1700
|
setPendingError(node, remaining);
|
|
1666
1701
|
updateCompanions !== null && updateCompanions(node);
|
|
@@ -1908,7 +1943,7 @@ function handleAsync(el, result, setter) {
|
|
|
1908
1943
|
return syncValue;
|
|
1909
1944
|
}
|
|
1910
1945
|
function clearStatus(el, clearUninitialized = false) {
|
|
1911
|
-
if (el.
|
|
1946
|
+
if (el._pendingSources) clearPendingSources(el);
|
|
1912
1947
|
if (el._blocked) el._blocked = false;
|
|
1913
1948
|
// The pending window is over; its quiet classification dies with it.
|
|
1914
1949
|
// (Unconditional: _reask is baked into the node literals, so this is a
|
|
@@ -1985,12 +2020,8 @@ function notifyStatus(el, status, error, blockStatus, lane) {
|
|
|
1985
2020
|
forEachDependent(el, (sub, link) => {
|
|
1986
2021
|
sub._time = clock;
|
|
1987
2022
|
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))
|
|
2023
|
+
(status === STATUS_PENDING && pendingSource && !sub._pendingSources?.has(pendingSource)) ||
|
|
2024
|
+
(status !== STATUS_PENDING && (sub._error !== error || sub._pendingSources))
|
|
1994
2025
|
) {
|
|
1995
2026
|
// A pending-observer link is the subscription an `isPending` read created.
|
|
1996
2027
|
// It exists so the observer re-runs when the source settles, but it must
|
|
@@ -2209,7 +2240,7 @@ function recompute(el, create = false) {
|
|
|
2209
2240
|
if (!el._error) {
|
|
2210
2241
|
trimStaleDeps(el);
|
|
2211
2242
|
const compareValue = hasOverride
|
|
2212
|
-
? el._overrideValue
|
|
2243
|
+
? unwrapOverride(el._overrideValue)
|
|
2213
2244
|
: el._pendingValue === NOT_PENDING
|
|
2214
2245
|
? el._value
|
|
2215
2246
|
: el._pendingValue;
|
|
@@ -2246,7 +2277,7 @@ function recompute(el, create = false) {
|
|
|
2246
2277
|
// own reveal schedule; drop any superseded older hold so its queued
|
|
2247
2278
|
// commit can't clobber the fresh value.
|
|
2248
2279
|
if (hasOverride && isOptimisticDirty) {
|
|
2249
|
-
el._overrideValue = value;
|
|
2280
|
+
el._overrideValue = value === undefined ? OVERRIDE_UNDEFINED : value;
|
|
2250
2281
|
el._pendingValue = NOT_PENDING;
|
|
2251
2282
|
}
|
|
2252
2283
|
} else {
|
|
@@ -2614,22 +2645,12 @@ function read(el) {
|
|
|
2614
2645
|
}
|
|
2615
2646
|
return !c || el._pendingValue === NOT_PENDING ? el._value : el._pendingValue;
|
|
2616
2647
|
}
|
|
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,
|
|
2648
|
+
if (strictRead && owner._statusFlags & STATUS_PENDING)
|
|
2649
|
+
throwPendingUntrackedRead(strictRead, {
|
|
2626
2650
|
ownerId: c?.id,
|
|
2627
2651
|
ownerName: c?._name,
|
|
2628
|
-
nodeName: owner?._name
|
|
2629
|
-
data: { strictRead }
|
|
2652
|
+
nodeName: owner?._name
|
|
2630
2653
|
});
|
|
2631
|
-
throw new Error(message);
|
|
2632
|
-
}
|
|
2633
2654
|
if (c && tracking) {
|
|
2634
2655
|
link(el, c, pendingCheckActive);
|
|
2635
2656
|
// Mark inheritance through derivation (see the fast path above), and its
|
|
@@ -2715,27 +2736,17 @@ function read(el) {
|
|
|
2715
2736
|
return snapshot;
|
|
2716
2737
|
}
|
|
2717
2738
|
}
|
|
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,
|
|
2739
|
+
if (strictRead)
|
|
2740
|
+
warnStrictReadUntracked(strictRead, {
|
|
2727
2741
|
ownerId: c?.id,
|
|
2728
2742
|
ownerName: c?._name,
|
|
2729
|
-
nodeName: owner?._name
|
|
2730
|
-
data: { strictRead }
|
|
2743
|
+
nodeName: owner?._name
|
|
2731
2744
|
});
|
|
2732
|
-
console.warn(message);
|
|
2733
|
-
}
|
|
2734
2745
|
if (el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING) {
|
|
2735
2746
|
// An active override means the engine is installed (A17: the override IS
|
|
2736
2747
|
// the value for every reader — that check itself stays right here).
|
|
2737
2748
|
if (c && stale && GlobalQueue._readStashed(el)) return el._value;
|
|
2738
|
-
return el._overrideValue;
|
|
2749
|
+
return unwrapOverride(el._overrideValue);
|
|
2739
2750
|
}
|
|
2740
2751
|
// Entanglement gate: a reader recomputing under an optimistic lane that reads
|
|
2741
2752
|
// a pending mid-transition write sees the committed value. Projection-store
|
|
@@ -3131,7 +3142,7 @@ let stashedOptimisticReads = null;
|
|
|
3131
3142
|
/** The optimistic half of setSignal, fired when `_overrideValue !== undefined`. */
|
|
3132
3143
|
function optimisticWrite(el, v) {
|
|
3133
3144
|
const hasOverride = el._overrideValue !== NOT_PENDING;
|
|
3134
|
-
const currentValue = hasOverride ? el._overrideValue : el._value;
|
|
3145
|
+
const currentValue = hasOverride ? unwrapOverride(el._overrideValue) : el._value;
|
|
3135
3146
|
if (typeof v === "function") v = v(currentValue);
|
|
3136
3147
|
const valueChanged =
|
|
3137
3148
|
!!(el._statusFlags & STATUS_UNINITIALIZED) || !el._equals || !el._equals(currentValue, v);
|
|
@@ -3148,10 +3159,17 @@ function optimisticWrite(el, v) {
|
|
|
3148
3159
|
// No revert target is stashed: while the override is active every reader
|
|
3149
3160
|
// sees it (A17), so authoritative arrivals commit silently into _value and
|
|
3150
3161
|
// reverting is just dropping the override — _value is already correct.
|
|
3151
|
-
else globalQueue._optimisticNodes.push(el);
|
|
3162
|
+
else globalQueue._batch._optimisticNodes.push(el);
|
|
3163
|
+
// Stamp ownership on the node (post-merge, so entangled writers share the
|
|
3164
|
+
// joint root). resolveTransition prefers this over the lane's _transition,
|
|
3165
|
+
// which a shared subscriber can merge across transactions (#2912).
|
|
3166
|
+
el._overrideOwner = activeTransition;
|
|
3152
3167
|
const lane = getOrCreateLane(el);
|
|
3153
3168
|
el._optimisticLane = lane;
|
|
3154
|
-
|
|
3169
|
+
// Literal undefined must not land raw: the slot doubles as the optimistic
|
|
3170
|
+
// brand, and erasing it makes the write invisible and routes follow-up
|
|
3171
|
+
// writes off the optimistic path into permanent commits (#2898).
|
|
3172
|
+
el._overrideValue = v === undefined ? OVERRIDE_UNDEFINED : v;
|
|
3155
3173
|
GlobalQueue._syncCompanions !== null && GlobalQueue._syncCompanions(el, v);
|
|
3156
3174
|
el._time = clock;
|
|
3157
3175
|
insertSubs(el, true);
|
|
@@ -3223,8 +3241,10 @@ function resolveOptimisticNodes(nodes) {
|
|
|
3223
3241
|
if (!(node._statusFlags & STATUS_PENDING)) node._statusFlags &= ~STATUS_UNINITIALIZED;
|
|
3224
3242
|
const prevOverride = node._overrideValue;
|
|
3225
3243
|
node._overrideValue = NOT_PENDING;
|
|
3226
|
-
if (prevOverride !== NOT_PENDING && node._value !== prevOverride)
|
|
3244
|
+
if (prevOverride !== NOT_PENDING && node._value !== unwrapOverride(prevOverride))
|
|
3245
|
+
insertSubs(node, true);
|
|
3227
3246
|
node._transition = null;
|
|
3247
|
+
node._overrideOwner = null;
|
|
3228
3248
|
}
|
|
3229
3249
|
// Settlement checkpoint (#2838): companions caught in this batch (or owned
|
|
3230
3250
|
// by a node in it) re-derive from committed state, so verdicts survive the
|
|
@@ -3349,8 +3369,8 @@ function laneAsyncSettled(el) {
|
|
|
3349
3369
|
}
|
|
3350
3370
|
}
|
|
3351
3371
|
function trackOptimisticStore(store) {
|
|
3352
|
-
// After initTransition, globalQueue.
|
|
3353
|
-
globalQueue._optimisticStores.add(store);
|
|
3372
|
+
// After initTransition, globalQueue._batch IS activeTransition (same reference)
|
|
3373
|
+
globalQueue._batch._optimisticStores.add(store);
|
|
3354
3374
|
schedule();
|
|
3355
3375
|
}
|
|
3356
3376
|
/**
|
|
@@ -3420,7 +3440,6 @@ function quietPending(el) {
|
|
|
3420
3440
|
for (const source of el._pendingSources) if (!source._reask) return false;
|
|
3421
3441
|
return true;
|
|
3422
3442
|
}
|
|
3423
|
-
if (el._pendingSource) return el._pendingSource._reask;
|
|
3424
3443
|
return el._reask;
|
|
3425
3444
|
}
|
|
3426
3445
|
function newQuestionInFlight(comp) {
|
|
@@ -3450,7 +3469,7 @@ function computePendingState(el) {
|
|
|
3450
3469
|
}
|
|
3451
3470
|
if (el._pendingValue !== NOT_PENDING && !(comp._statusFlags & STATUS_UNINITIALIZED)) {
|
|
3452
3471
|
if (hasActiveOverride(el))
|
|
3453
|
-
return !el._equals || !el._equals(el._pendingValue, el._overrideValue);
|
|
3472
|
+
return !el._equals || !el._equals(el._pendingValue, unwrapOverride(el._overrideValue));
|
|
3454
3473
|
return true;
|
|
3455
3474
|
}
|
|
3456
3475
|
return newQuestionInFlight(comp);
|
|
@@ -3534,7 +3553,7 @@ function latestRead(el) {
|
|
|
3534
3553
|
setLatestReadActive(false);
|
|
3535
3554
|
const visibleValue =
|
|
3536
3555
|
el._overrideValue !== undefined && el._overrideValue !== NOT_PENDING
|
|
3537
|
-
? el._overrideValue
|
|
3556
|
+
? unwrapOverride(el._overrideValue)
|
|
3538
3557
|
: el._value;
|
|
3539
3558
|
let value;
|
|
3540
3559
|
try {
|
|
@@ -3868,10 +3887,22 @@ function restoreTransition(transition, fn) {
|
|
|
3868
3887
|
* surrounding UI sees one atomic update per yielded step; nothing is committed
|
|
3869
3888
|
* until the action either completes or the next `yield` resolves.
|
|
3870
3889
|
*
|
|
3871
|
-
*
|
|
3872
|
-
*
|
|
3873
|
-
*
|
|
3874
|
-
*
|
|
3890
|
+
* `yield` is the transaction-safe suspension point: the action waits for a
|
|
3891
|
+
* yielded promise and re-enters the transaction before running the code after
|
|
3892
|
+
* it. A plain `await` does NOT — the runtime has no hook into an async
|
|
3893
|
+
* generator's internal await continuations, so writes to fresh signals
|
|
3894
|
+
* between an `await` and the next `yield` escape the transaction and commit
|
|
3895
|
+
* immediately. `await` is still the ergonomic choice for typed results; just
|
|
3896
|
+
* put a bare `yield` before any writes that follow it:
|
|
3897
|
+
*
|
|
3898
|
+
* ```ts
|
|
3899
|
+
* const saved = await api.createTodo(text); // typed result
|
|
3900
|
+
* yield; // re-enter the transaction before writing
|
|
3901
|
+
* setTodos(t => { ... });
|
|
3902
|
+
* ```
|
|
3903
|
+
*
|
|
3904
|
+
* (For the same reason, don't call `flush()` inside an action body — it
|
|
3905
|
+
* drains the transaction mid-step.)
|
|
3875
3906
|
*
|
|
3876
3907
|
* Each call returns a `Promise` that resolves with the generator's return
|
|
3877
3908
|
* value, or rejects if it throws. Pair with `createOptimistic` /
|
|
@@ -3882,10 +3913,11 @@ function restoreTransition(transition, fn) {
|
|
|
3882
3913
|
* ```ts
|
|
3883
3914
|
* const [todos, setTodos] = createOptimisticStore<Todo[]>([]);
|
|
3884
3915
|
*
|
|
3885
|
-
* const addTodo = action(function* (text: string) {
|
|
3916
|
+
* const addTodo = action(async function* (text: string) {
|
|
3886
3917
|
* const tempId = crypto.randomUUID();
|
|
3887
3918
|
* setTodos(t => { t.push({ id: tempId, text, pending: true }); }); // optimistic
|
|
3888
|
-
* const saved =
|
|
3919
|
+
* const saved = await api.createTodo(text); // network round-trip, typed
|
|
3920
|
+
* yield; // re-enter the transaction
|
|
3889
3921
|
* setTodos(t => {
|
|
3890
3922
|
* const i = t.findIndex(x => x.id === tempId);
|
|
3891
3923
|
* if (i >= 0) t[i] = saved;
|
|
@@ -4428,6 +4460,8 @@ function addEnumSymbols(o, syms, keys) {
|
|
|
4428
4460
|
}
|
|
4429
4461
|
}
|
|
4430
4462
|
function getAllKeys(value, override, next) {
|
|
4463
|
+
// Symbols are merged explicitly below; keep the common string-key path on
|
|
4464
|
+
// Object.keys() and avoid reflecting the base symbols twice.
|
|
4431
4465
|
const keys = getKeys(value, override);
|
|
4432
4466
|
const nextKeys = Object.keys(next);
|
|
4433
4467
|
// `value` can be a wrapped store (store-in-store) whose ownKeys trap tracks;
|
|
@@ -4510,6 +4544,43 @@ function applyArrayItem(next, previous, target, node, keyFn) {
|
|
|
4510
4544
|
applyState(next, wrapped, keyFn);
|
|
4511
4545
|
} else node && setSignal(node, wrapValue(next, target));
|
|
4512
4546
|
}
|
|
4547
|
+
/**
|
|
4548
|
+
* The captured-proxy half of the object diff (#2902): descend into keyed-
|
|
4549
|
+
* matching children that have NO node at this level but shelter subscribers
|
|
4550
|
+
* somewhere below (their target's sticky `STORE_DESC` flag, bubbled up by
|
|
4551
|
+
* `getNode`). Without this, a proxy captured through untracked reads — a
|
|
4552
|
+
* `<For>` row handed to a child component — detaches from the diff the
|
|
4553
|
+
* moment no intermediate level happens to be tracked, and its live
|
|
4554
|
+
* subscribers go permanently stale. Never-subscribed branches have no flag
|
|
4555
|
+
* and stay pruned exactly as before; keys the main loop already visited
|
|
4556
|
+
* (node present) are skipped. Callers gate on the parent's own flag and on
|
|
4557
|
+
* `$TRACK` absence (an enumeration-tracked record already diffs every key).
|
|
4558
|
+
*/
|
|
4559
|
+
function applyDescendants(previous, next, target, nodes, keyFn, override, optOverride) {
|
|
4560
|
+
const lookup = target[STORE_LOOKUP] || storeLookup;
|
|
4561
|
+
const keys = (override ? getKeys(previous, override) : Object.keys(previous)).concat(
|
|
4562
|
+
getStoreSymbols(previous, override)
|
|
4563
|
+
);
|
|
4564
|
+
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4565
|
+
const key = keys[i];
|
|
4566
|
+
if (nodes?.[key]) continue; // main loop already diffed this slot
|
|
4567
|
+
const previousValue = unwrap(
|
|
4568
|
+
override ? getOverrideValue(previous, override, key, optOverride) : previous[key]
|
|
4569
|
+
);
|
|
4570
|
+
if (!isWrappable(previousValue)) continue;
|
|
4571
|
+
const childTarget = (lookup.get(previousValue) ?? storeLookup.get(previousValue))?.[$TARGET];
|
|
4572
|
+
if (!childTarget?.[STORE_DESC]) continue;
|
|
4573
|
+
const nextValue = unwrap(next[key]);
|
|
4574
|
+
if (
|
|
4575
|
+
previousValue === nextValue ||
|
|
4576
|
+
!isWrappable(nextValue) ||
|
|
4577
|
+
Array.isArray(previousValue) !== Array.isArray(nextValue) ||
|
|
4578
|
+
(keyFn(previousValue) != null && keyFn(previousValue) !== keyFn(nextValue))
|
|
4579
|
+
)
|
|
4580
|
+
continue;
|
|
4581
|
+
applyState(nextValue, wrap(previousValue, target), keyFn);
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4513
4584
|
// Dispatcher: every applyState call (including recursion) checks for the
|
|
4514
4585
|
// presence of override / optimistic-override slots once and routes to the
|
|
4515
4586
|
// appropriate body. The fast body never calls `getOverrideValue` and never
|
|
@@ -4619,8 +4690,9 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4619
4690
|
}
|
|
4620
4691
|
// values
|
|
4621
4692
|
let nodes = target[STORE_NODE];
|
|
4693
|
+
let tracked;
|
|
4622
4694
|
if (nodes) {
|
|
4623
|
-
|
|
4695
|
+
tracked = nodes[$TRACK];
|
|
4624
4696
|
const keys = tracked ? getAllKeys(previous, undefined, next) : nodeKeys(nodes);
|
|
4625
4697
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4626
4698
|
const key = keys[i];
|
|
@@ -4640,6 +4712,7 @@ function applyStateFast(next, target, keyFn) {
|
|
|
4640
4712
|
} else applyState(nextValue, wrap(previousValue, target), keyFn);
|
|
4641
4713
|
}
|
|
4642
4714
|
}
|
|
4715
|
+
if (!tracked && target[STORE_DESC]) applyDescendants(previous, next, target, nodes, keyFn);
|
|
4643
4716
|
// has
|
|
4644
4717
|
if ((nodes = target[STORE_HAS])) {
|
|
4645
4718
|
const keys = nodeKeys(nodes);
|
|
@@ -4753,8 +4826,9 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4753
4826
|
return;
|
|
4754
4827
|
}
|
|
4755
4828
|
// values
|
|
4829
|
+
let tracked;
|
|
4756
4830
|
if (nodes) {
|
|
4757
|
-
|
|
4831
|
+
tracked = nodes[$TRACK];
|
|
4758
4832
|
const keys = tracked ? getAllKeys(previous, override, next) : nodeKeys(nodes);
|
|
4759
4833
|
for (let i = 0, len = keys.length; i < len; i++) {
|
|
4760
4834
|
const key = keys[i];
|
|
@@ -4774,6 +4848,8 @@ function applyStateSlow(next, target, keyFn) {
|
|
|
4774
4848
|
} else applyState(nextValue, wrap(previousValue, target), keyFn);
|
|
4775
4849
|
}
|
|
4776
4850
|
}
|
|
4851
|
+
if (!tracked && target[STORE_DESC])
|
|
4852
|
+
applyDescendants(previous, next, target, nodes, keyFn, override, optOverride);
|
|
4777
4853
|
// has
|
|
4778
4854
|
if ((nodes = target[STORE_HAS])) {
|
|
4779
4855
|
const keys = nodeKeys(nodes);
|
|
@@ -5005,7 +5081,10 @@ const STORE_VALUE = "v",
|
|
|
5005
5081
|
STORE_WRAP = "w",
|
|
5006
5082
|
STORE_LOOKUP = "l",
|
|
5007
5083
|
STORE_FIREWALL = "f",
|
|
5008
|
-
STORE_OPTIMISTIC = "p"
|
|
5084
|
+
STORE_OPTIMISTIC = "p",
|
|
5085
|
+
STORE_OPTIMISTIC_OWNERS = "t",
|
|
5086
|
+
STORE_PARENT = "u",
|
|
5087
|
+
STORE_DESC = "d";
|
|
5009
5088
|
const STORE_SELF_PENDING = Symbol("STORE_SELF_PENDING");
|
|
5010
5089
|
function createStoreProxy(value, traps = storeTraps, extend) {
|
|
5011
5090
|
let newTarget;
|
|
@@ -5028,9 +5107,17 @@ const storeLookup = new WeakMap();
|
|
|
5028
5107
|
// Lets reconcile enumerate symbols only for records that need it (#2851).
|
|
5029
5108
|
const symbolKeyedRecords = new WeakSet();
|
|
5030
5109
|
function wrap(value, target) {
|
|
5031
|
-
if (target?.[STORE_WRAP])
|
|
5110
|
+
if (target?.[STORE_WRAP]) {
|
|
5111
|
+
const p = target[STORE_WRAP](value, target);
|
|
5112
|
+
const t = p[$TARGET];
|
|
5113
|
+
if (t && !t[STORE_PARENT] && t !== target) t[STORE_PARENT] = target;
|
|
5114
|
+
return p;
|
|
5115
|
+
}
|
|
5032
5116
|
let p = value[$PROXY] || storeLookup.get(value);
|
|
5033
|
-
if (!p)
|
|
5117
|
+
if (!p) {
|
|
5118
|
+
storeLookup.set(value, (p = createStoreProxy(value)));
|
|
5119
|
+
if (target) p[$TARGET][STORE_PARENT] = target;
|
|
5120
|
+
}
|
|
5034
5121
|
return p;
|
|
5035
5122
|
}
|
|
5036
5123
|
function isWrappable(obj) {
|
|
@@ -5058,7 +5145,7 @@ function unwrapStoreValue(value, map, lookup) {
|
|
|
5058
5145
|
const result = isArray ? [] : Object.create(Object.getPrototypeOf(source));
|
|
5059
5146
|
map.set(value, result);
|
|
5060
5147
|
lookup = target[STORE_LOOKUP] ?? storeLookup;
|
|
5061
|
-
for (const key of
|
|
5148
|
+
for (const key of getStoreKeys(source, override)) {
|
|
5062
5149
|
if (isArray && key === "length") continue;
|
|
5063
5150
|
const next = key in override ? override[key] : source[key];
|
|
5064
5151
|
if (next !== $DELETED) result[key] = unwrapStoreValue(next, map, lookup);
|
|
@@ -5073,6 +5160,21 @@ function isPrototypePollutionKey$1(property) {
|
|
|
5073
5160
|
function ownEnumerableKeys(o) {
|
|
5074
5161
|
return Reflect.ownKeys(o).filter(k => Object.prototype.propertyIsEnumerable.call(o, k));
|
|
5075
5162
|
}
|
|
5163
|
+
function ownEnumerableSymbols(o) {
|
|
5164
|
+
const symbols = Object.getOwnPropertySymbols(o);
|
|
5165
|
+
const result = [];
|
|
5166
|
+
for (let i = 0, len = symbols.length; i < len; i++) {
|
|
5167
|
+
const symbol = symbols[i];
|
|
5168
|
+
if (Object.prototype.propertyIsEnumerable.call(o, symbol)) result.push(symbol);
|
|
5169
|
+
}
|
|
5170
|
+
return result;
|
|
5171
|
+
}
|
|
5172
|
+
// Plain-object variant that keeps Object.keys() as the fast path and only pays
|
|
5173
|
+
// descriptor checks for symbols. Do not use this on store proxies: splitting
|
|
5174
|
+
// strings/symbols would invoke their ownKeys trap twice.
|
|
5175
|
+
function ownEnumerableKeysPlain(o) {
|
|
5176
|
+
return Object.keys(o).concat(ownEnumerableSymbols(o));
|
|
5177
|
+
}
|
|
5076
5178
|
/**
|
|
5077
5179
|
* Single chokepoint for the store's layered value resolution: returns the
|
|
5078
5180
|
* override layer (optimistic first, then regular) that shadows `property`, or
|
|
@@ -5093,7 +5195,7 @@ function getOverlayLayer(target, property) {
|
|
|
5093
5195
|
*/
|
|
5094
5196
|
function visibleNodeValue(node) {
|
|
5095
5197
|
return node._overrideValue !== undefined && node._overrideValue !== NOT_PENDING
|
|
5096
|
-
? node._overrideValue
|
|
5198
|
+
? unwrapOverride(node._overrideValue)
|
|
5097
5199
|
: node._pendingValue !== NOT_PENDING
|
|
5098
5200
|
? node._pendingValue
|
|
5099
5201
|
: node._value;
|
|
@@ -5161,22 +5263,37 @@ function getNode(target, nodes, property, value, equals = isEqual, snapshotProps
|
|
|
5161
5263
|
}
|
|
5162
5264
|
if (typeof property === "symbol" && property !== $TRACK && property !== $AFFECTS)
|
|
5163
5265
|
symbolKeyedRecords.add(nodes);
|
|
5164
|
-
// A node born inside a live
|
|
5266
|
+
// A node born inside a live mark's identity scope inherits the mark
|
|
5165
5267
|
// (the declaration walk could only cover nodes that existed then). The
|
|
5166
5268
|
// record's own $AFFECTS carrier is the mark's channel, never a member.
|
|
5167
|
-
if (property !== $AFFECTS && affectsScopes.size)
|
|
5269
|
+
if (property !== $AFFECTS && affectsScopes.size)
|
|
5270
|
+
inheritAffectsMarks(s, target[STORE_VALUE], property);
|
|
5271
|
+
// Node presence bubbles up the wrap chain (sticky), so reconcile can see
|
|
5272
|
+
// "subscribers live somewhere below" through node-less intermediate
|
|
5273
|
+
// records — the captured-proxy diff gate (#2902). Amortized O(1): stops at
|
|
5274
|
+
// the first already-flagged ancestor.
|
|
5275
|
+
let t = target;
|
|
5276
|
+
while (t && !t[STORE_DESC]) {
|
|
5277
|
+
t[STORE_DESC] = true;
|
|
5278
|
+
t = t[STORE_PARENT];
|
|
5279
|
+
}
|
|
5168
5280
|
return (nodes[property] = s);
|
|
5169
5281
|
}
|
|
5170
5282
|
/**
|
|
5171
|
-
* Scope inheritance for late-created nodes: every live
|
|
5172
|
-
*
|
|
5173
|
-
*
|
|
5174
|
-
*
|
|
5283
|
+
* Scope inheritance for late-created nodes: every live mark whose identity
|
|
5284
|
+
* scope contains the owning record's raw — and, for keyed marks, whose key
|
|
5285
|
+
* is this property — gets counted on the new node. Inherited marks live
|
|
5286
|
+
* exactly as long as the scope's carrier — the release hook below drops
|
|
5287
|
+
* them with the entry.
|
|
5175
5288
|
*/
|
|
5176
|
-
function inheritAffectsMarks(node, raw) {
|
|
5289
|
+
function inheritAffectsMarks(node, raw, property) {
|
|
5177
5290
|
// A live scope exists, so affects.ts already installed the mark engine.
|
|
5178
5291
|
for (const [carrier, entry] of affectsScopes) {
|
|
5179
|
-
if (
|
|
5292
|
+
if (
|
|
5293
|
+
carrier._affectsCount &&
|
|
5294
|
+
entry.scope.has(raw) &&
|
|
5295
|
+
(entry.key === undefined || entry.key === property)
|
|
5296
|
+
) {
|
|
5180
5297
|
GlobalQueue._markAffects(node);
|
|
5181
5298
|
entry.inherited.push(node);
|
|
5182
5299
|
}
|
|
@@ -5214,7 +5331,10 @@ function walkAffectsScope(
|
|
|
5214
5331
|
collectRecordNodes(target[STORE_NODE], found);
|
|
5215
5332
|
collectRecordNodes(target[STORE_HAS], found);
|
|
5216
5333
|
override = mergedOverlay(target);
|
|
5217
|
-
lookup
|
|
5334
|
+
// Carry the effective lookup into untouched descendants. Default stores
|
|
5335
|
+
// use the global lookup just like snapshotImpl; without it, nested raw
|
|
5336
|
+
// objects fall back to string-only enumeration and symbol branches vanish.
|
|
5337
|
+
lookup = target[STORE_LOOKUP] ?? lookup ?? storeLookup;
|
|
5218
5338
|
}
|
|
5219
5339
|
if (Array.isArray(raw)) {
|
|
5220
5340
|
const len = override?.length ?? raw.length;
|
|
@@ -5222,8 +5342,18 @@ function walkAffectsScope(
|
|
|
5222
5342
|
const v = override && i in override ? override[i] : raw[i];
|
|
5223
5343
|
if (v !== $DELETED) walkAffectsScope(v, entry, found, lookup, visited);
|
|
5224
5344
|
}
|
|
5345
|
+
// Arrays can also carry symbol metadata. Enumerate symbols separately to
|
|
5346
|
+
// avoid scanning large index lists twice. Outside a store tree, retain the
|
|
5347
|
+
// existing index-only walk.
|
|
5348
|
+
const symbols = target || lookup ? getStoreSymbols(raw, override) : [];
|
|
5349
|
+
for (let i = 0, l = symbols.length; i < l; i++) {
|
|
5350
|
+
const key = symbols[i];
|
|
5351
|
+
const desc = getPropertyDescriptor(raw, override, key);
|
|
5352
|
+
if (!desc || desc.get) continue;
|
|
5353
|
+
walkAffectsScope(desc.value, entry, found, lookup, visited);
|
|
5354
|
+
}
|
|
5225
5355
|
} else {
|
|
5226
|
-
const keys = getKeys(raw, override);
|
|
5356
|
+
const keys = target || lookup ? getStoreKeys(raw, override) : getKeys(raw, override);
|
|
5227
5357
|
for (let i = 0, l = keys.length; i < l; i++) {
|
|
5228
5358
|
const desc = getPropertyDescriptor(raw, override, keys[i]);
|
|
5229
5359
|
if (!desc || desc.get) continue;
|
|
@@ -5252,7 +5382,7 @@ function collectRecordNodes(nodes, found) {
|
|
|
5252
5382
|
*
|
|
5253
5383
|
* @internal
|
|
5254
5384
|
*/
|
|
5255
|
-
function witnessAffectsMark(target) {
|
|
5385
|
+
function witnessAffectsMark(target, property) {
|
|
5256
5386
|
// Callers guard on `pendingCheckActive`, which only flips inside
|
|
5257
5387
|
// isPending() — the verdict layer is loaded and its hook installed.
|
|
5258
5388
|
const own = target[STORE_NODE]?.[$AFFECTS];
|
|
@@ -5260,7 +5390,12 @@ function witnessAffectsMark(target) {
|
|
|
5260
5390
|
if (affectsScopes.size) {
|
|
5261
5391
|
const raw = target[STORE_VALUE];
|
|
5262
5392
|
for (const [carrier, entry] of affectsScopes) {
|
|
5263
|
-
if (
|
|
5393
|
+
if (
|
|
5394
|
+
carrier !== own &&
|
|
5395
|
+
carrier._affectsCount &&
|
|
5396
|
+
entry.scope.has(raw) &&
|
|
5397
|
+
(entry.key === undefined || entry.key === property)
|
|
5398
|
+
)
|
|
5264
5399
|
GlobalQueue._witnessAffects(carrier);
|
|
5265
5400
|
}
|
|
5266
5401
|
}
|
|
@@ -5278,26 +5413,42 @@ function witnessAffectsMark(target) {
|
|
|
5278
5413
|
*/
|
|
5279
5414
|
function getStoreAffectsNodes(target, key) {
|
|
5280
5415
|
const nodes = getNodes(target, STORE_NODE);
|
|
5416
|
+
GlobalQueue._releaseAffectsScope ||= node => {
|
|
5417
|
+
const entry = affectsScopes.get(node);
|
|
5418
|
+
if (!entry) return;
|
|
5419
|
+
affectsScopes.delete(node);
|
|
5420
|
+
for (let i = 0; i < entry.inherited.length; i++)
|
|
5421
|
+
GlobalQueue._releaseAffectsMark(entry.inherited[i]);
|
|
5422
|
+
};
|
|
5281
5423
|
if (key === undefined) {
|
|
5282
5424
|
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
5425
|
let entry = affectsScopes.get(carrier);
|
|
5291
5426
|
if (!entry) affectsScopes.set(carrier, (entry = { scope: new Set(), inherited: [] }));
|
|
5292
5427
|
const result = [carrier];
|
|
5293
5428
|
walkAffectsScope(target[$PROXY], entry, result, target[STORE_LOOKUP], new Set());
|
|
5294
5429
|
return result;
|
|
5295
5430
|
}
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5431
|
+
let node = nodes[key];
|
|
5432
|
+
if (!node) {
|
|
5433
|
+
const layer = getOverlayLayer(target, key);
|
|
5434
|
+
const raw = layer ? layer[key] : target[STORE_VALUE][key];
|
|
5435
|
+
node = upsertStoreNode(
|
|
5436
|
+
target,
|
|
5437
|
+
nodes,
|
|
5438
|
+
key,
|
|
5439
|
+
raw === $DELETED ? undefined : raw,
|
|
5440
|
+
target[STORE_SNAPSHOT_PROPS]
|
|
5441
|
+
);
|
|
5442
|
+
}
|
|
5443
|
+
// Keyed marks resolve by identity too (#2904): another store family's
|
|
5444
|
+
// proxy can share this record's raw (a derived store swaps its backing to
|
|
5445
|
+
// the source's raw when its projection lands), and reads through it never
|
|
5446
|
+
// touch this target's node map. Scope is exactly the owning record's raw,
|
|
5447
|
+
// narrowed to this key for witness and birth inheritance.
|
|
5448
|
+
let entry = affectsScopes.get(node);
|
|
5449
|
+
if (!entry) affectsScopes.set(node, (entry = { scope: new Set(), inherited: [], key }));
|
|
5450
|
+
entry.scope.add(target[STORE_VALUE]);
|
|
5451
|
+
return [node];
|
|
5301
5452
|
}
|
|
5302
5453
|
function trackSelf(target, symbol = $TRACK) {
|
|
5303
5454
|
if (!getObserver()) return;
|
|
@@ -5336,17 +5487,44 @@ function mergedOverlay(target) {
|
|
|
5336
5487
|
const opt = target[STORE_OPTIMISTIC_OVERRIDE];
|
|
5337
5488
|
return override && opt ? { ...override, ...opt } : (opt ?? override);
|
|
5338
5489
|
}
|
|
5339
|
-
function
|
|
5490
|
+
function getKeysImpl(source, override, enumerable, symbols) {
|
|
5340
5491
|
// Plain objects can't trigger proxy traps — only pay for the untrack
|
|
5341
5492
|
// closure when the source is itself a wrapped store (store-in-store).
|
|
5342
5493
|
const baseKeys = source[$TARGET]
|
|
5343
|
-
? untrack(() =>
|
|
5494
|
+
? untrack(() =>
|
|
5495
|
+
enumerable
|
|
5496
|
+
? symbols
|
|
5497
|
+
? ownEnumerableKeys(source)
|
|
5498
|
+
: Object.keys(source)
|
|
5499
|
+
: Reflect.ownKeys(source)
|
|
5500
|
+
)
|
|
5344
5501
|
: enumerable
|
|
5345
|
-
?
|
|
5502
|
+
? symbols
|
|
5503
|
+
? ownEnumerableKeysPlain(source)
|
|
5504
|
+
: Object.keys(source)
|
|
5346
5505
|
: Reflect.ownKeys(source);
|
|
5347
|
-
|
|
5506
|
+
return override ? mergeOverrideKeys(baseKeys, override) : baseKeys;
|
|
5507
|
+
}
|
|
5508
|
+
function getKeys(source, override, enumerable = true) {
|
|
5509
|
+
return getKeysImpl(source, override, enumerable, false);
|
|
5510
|
+
}
|
|
5511
|
+
function getStoreKeys(source, override) {
|
|
5512
|
+
return getKeysImpl(source, override, true, true);
|
|
5513
|
+
}
|
|
5514
|
+
function getStoreSymbols(source, override) {
|
|
5515
|
+
const symbols = source[$TARGET]
|
|
5516
|
+
? untrack(() => ownEnumerableSymbols(source))
|
|
5517
|
+
: ownEnumerableSymbols(source);
|
|
5518
|
+
return override ? mergeOverrideKeys(symbols, override, true) : symbols;
|
|
5519
|
+
}
|
|
5520
|
+
// Shared override-layer merge for key enumeration: adds live override keys,
|
|
5521
|
+
// drops $DELETED ones. `symbolsOnly` scopes the override scan for the
|
|
5522
|
+
// array-metadata passes.
|
|
5523
|
+
function mergeOverrideKeys(baseKeys, override, symbolsOnly) {
|
|
5348
5524
|
const keys = new Set(baseKeys);
|
|
5349
|
-
const overrides =
|
|
5525
|
+
const overrides = symbolsOnly
|
|
5526
|
+
? Object.getOwnPropertySymbols(override)
|
|
5527
|
+
: Reflect.ownKeys(override);
|
|
5350
5528
|
for (const key of overrides) {
|
|
5351
5529
|
if (override[key] !== $DELETED) keys.add(key);
|
|
5352
5530
|
else keys.delete(key);
|
|
@@ -5412,6 +5590,19 @@ function armOptimisticStoreWrite(target, store) {
|
|
|
5412
5590
|
GlobalQueue._trackOptimisticStore(store);
|
|
5413
5591
|
}
|
|
5414
5592
|
}
|
|
5593
|
+
/**
|
|
5594
|
+
* Records which transition owns an optimistic layer entry (#2899), so a
|
|
5595
|
+
* settling action only consumes its own keys — the layer is store-wide, but
|
|
5596
|
+
* concurrent actions writing disjoint keys must revert independently, exactly
|
|
5597
|
+
* like optimistic signal nodes do via the transition's _optimisticNodes.
|
|
5598
|
+
* `activeTransition` is the write's transaction (action() opens it before the
|
|
5599
|
+
* body runs); null marks an ambient write that clears at plain flush end.
|
|
5600
|
+
* Same-key writes across actions keep last-write-wins layer semantics.
|
|
5601
|
+
*/
|
|
5602
|
+
function stampOptimisticOwner(target, overrideKey, property) {
|
|
5603
|
+
if (overrideKey === STORE_OPTIMISTIC_OVERRIDE)
|
|
5604
|
+
(target[STORE_OPTIMISTIC_OWNERS] ??= Object.create(null))[property] = activeTransition;
|
|
5605
|
+
}
|
|
5415
5606
|
function upsertStoreNode(target, nodes, property, prev, snapshotProps) {
|
|
5416
5607
|
if (nodes[property]) return nodes[property];
|
|
5417
5608
|
const initial = isWrappable(prev) ? wrap(prev, target) : prev;
|
|
@@ -5456,12 +5647,26 @@ function notifyStoreProperty(target, property, mode, value, prev, prevHas) {
|
|
|
5456
5647
|
notifySelf(target);
|
|
5457
5648
|
}
|
|
5458
5649
|
let Writing = null;
|
|
5650
|
+
/**
|
|
5651
|
+
* A derived store's seed is a draft for the derive function, never an
|
|
5652
|
+
* observable value (#2897): until the firewall first resolves there is
|
|
5653
|
+
* nothing to read, so every consumer path throws NotReady — tracked reads
|
|
5654
|
+
* through their node (core read()), and the untracked fall-throughs in the
|
|
5655
|
+
* traps through this guard. Returning the seed leaked it; returning
|
|
5656
|
+
* `undefined` would break non-nullable types. Callers exempt the firewall
|
|
5657
|
+
* itself (the derive function works its own draft while uninitialized).
|
|
5658
|
+
*/
|
|
5659
|
+
function throwIfUninitialized(target) {
|
|
5660
|
+
const firewall = target[STORE_FIREWALL];
|
|
5661
|
+
if (firewall && firewall._statusFlags & STATUS_UNINITIALIZED)
|
|
5662
|
+
throw firewall._error ?? new NotReadyError(firewall);
|
|
5663
|
+
}
|
|
5459
5664
|
const storeTraps = {
|
|
5460
5665
|
get(target, property, receiver) {
|
|
5461
5666
|
if (property === $TARGET) return target;
|
|
5462
5667
|
if (property === $PROXY) return receiver;
|
|
5463
5668
|
if (property === $REFRESH) return target[STORE_FIREWALL];
|
|
5464
|
-
if (pendingCheckActive) witnessAffectsMark(target);
|
|
5669
|
+
if (pendingCheckActive) witnessAffectsMark(target, property);
|
|
5465
5670
|
if (property === $TRACK) {
|
|
5466
5671
|
trackSelf(target);
|
|
5467
5672
|
return receiver;
|
|
@@ -5542,24 +5747,26 @@ const storeTraps = {
|
|
|
5542
5747
|
}
|
|
5543
5748
|
}
|
|
5544
5749
|
if (strictRead && typeof property === "string") {
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
nodeName:
|
|
5554
|
-
data: { strictRead, property
|
|
5750
|
+
// Safeguard parity with core read() (#2897): untracked store reads skip
|
|
5751
|
+
// node creation (and with it read()'s PENDING_ASYNC_UNTRACKED_READ
|
|
5752
|
+
// check), so a derived store's in-flight firewall must be consulted
|
|
5753
|
+
// here — otherwise a component-body read of a refetching store silently
|
|
5754
|
+
// returns a value the reader can never observe updating.
|
|
5755
|
+
if ((target[STORE_FIREWALL]?._statusFlags ?? 0) & STATUS_PENDING)
|
|
5756
|
+
throwPendingUntrackedRead(strictRead, { nodeName: property });
|
|
5757
|
+
warnStrictReadUntracked(strictRead, {
|
|
5758
|
+
nodeName: property,
|
|
5759
|
+
data: { strictRead, property, source: "store" }
|
|
5555
5760
|
});
|
|
5556
|
-
console.warn(message);
|
|
5557
5761
|
}
|
|
5762
|
+
// Untracked fall-through (tracked reads already threw via their node in
|
|
5763
|
+
// read(); the dev strictRead error above wins first for memo parity).
|
|
5764
|
+
if (!selfRead) throwIfUninitialized(target);
|
|
5558
5765
|
return isWrappable(value) ? wrap(value, target) : value;
|
|
5559
5766
|
},
|
|
5560
5767
|
has(target, property) {
|
|
5561
5768
|
if (property === $PROXY || property === $TRACK || property === "__proto__") return true;
|
|
5562
|
-
if (pendingCheckActive) witnessAffectsMark(target);
|
|
5769
|
+
if (pendingCheckActive) witnessAffectsMark(target, property);
|
|
5563
5770
|
const hasLayer = getOverlayLayer(target, property);
|
|
5564
5771
|
const has = hasLayer ? hasLayer[property] !== $DELETED : property in target[STORE_VALUE];
|
|
5565
5772
|
if (writeOnly(target[$PROXY]) || getObserver() === target[STORE_FIREWALL]) return has;
|
|
@@ -5574,6 +5781,7 @@ const storeTraps = {
|
|
|
5574
5781
|
if (getObserver()) {
|
|
5575
5782
|
return read(getNode(target, nodes, property, has));
|
|
5576
5783
|
}
|
|
5784
|
+
throwIfUninitialized(target);
|
|
5577
5785
|
return has;
|
|
5578
5786
|
},
|
|
5579
5787
|
set(target, property, rawValue) {
|
|
@@ -5603,12 +5811,18 @@ const storeTraps = {
|
|
|
5603
5811
|
const nextLength = isArrayIndexWrite && nextIndex > len ? nextIndex : undefined;
|
|
5604
5812
|
if (prev === value && nextLength === undefined) return true;
|
|
5605
5813
|
armOptimisticStoreWrite(target, store);
|
|
5606
|
-
if (value !== undefined && value === base && nextLength === undefined)
|
|
5814
|
+
if (value !== undefined && value === base && nextLength === undefined) {
|
|
5607
5815
|
delete target[overrideKey]?.[property];
|
|
5608
|
-
|
|
5816
|
+
if (overrideKey === STORE_OPTIMISTIC_OVERRIDE)
|
|
5817
|
+
delete target[STORE_OPTIMISTIC_OWNERS]?.[property];
|
|
5818
|
+
} else {
|
|
5609
5819
|
const override = target[overrideKey] || (target[overrideKey] = Object.create(null));
|
|
5610
5820
|
override[property] = value;
|
|
5611
|
-
|
|
5821
|
+
stampOptimisticOwner(target, overrideKey, property);
|
|
5822
|
+
if (nextLength !== undefined) {
|
|
5823
|
+
override.length = nextLength;
|
|
5824
|
+
stampOptimisticOwner(target, overrideKey, "length");
|
|
5825
|
+
}
|
|
5612
5826
|
}
|
|
5613
5827
|
notifyStoreProperty(target, property, "set", value, prev, prevHas);
|
|
5614
5828
|
// Shrinking an array's length must remove the truncated indices, otherwise
|
|
@@ -5627,6 +5841,7 @@ const storeTraps = {
|
|
|
5627
5841
|
const prevIndex = i in override ? override[i] : state[i];
|
|
5628
5842
|
if (!(i in override) && !(i in state)) continue;
|
|
5629
5843
|
override[i] = $DELETED;
|
|
5844
|
+
stampOptimisticOwner(target, overrideKey, i);
|
|
5630
5845
|
notifyStoreProperty(target, i, "delete", undefined, prevIndex, true);
|
|
5631
5846
|
}
|
|
5632
5847
|
}
|
|
@@ -5670,6 +5885,7 @@ const storeTraps = {
|
|
|
5670
5885
|
property,
|
|
5671
5886
|
normalizedDescriptor
|
|
5672
5887
|
);
|
|
5888
|
+
stampOptimisticOwner(target, overrideKey, property);
|
|
5673
5889
|
notifyStoreProperty(target, property, "invalidate");
|
|
5674
5890
|
if (true) {
|
|
5675
5891
|
const next =
|
|
@@ -5699,9 +5915,12 @@ const storeTraps = {
|
|
|
5699
5915
|
) {
|
|
5700
5916
|
armOptimisticStoreWrite(target, target[$PROXY]);
|
|
5701
5917
|
(target[overrideKey] || (target[overrideKey] = Object.create(null)))[property] = $DELETED;
|
|
5918
|
+
stampOptimisticOwner(target, overrideKey, property);
|
|
5702
5919
|
} else if (target[overrideKey] && property in target[overrideKey]) {
|
|
5703
5920
|
armOptimisticStoreWrite(target, target[$PROXY]);
|
|
5704
5921
|
delete target[overrideKey][property];
|
|
5922
|
+
if (overrideKey === STORE_OPTIMISTIC_OVERRIDE)
|
|
5923
|
+
delete target[STORE_OPTIMISTIC_OWNERS]?.[property];
|
|
5705
5924
|
} else return true;
|
|
5706
5925
|
notifyStoreProperty(target, property, "delete", undefined, prev, true);
|
|
5707
5926
|
});
|
|
@@ -5710,7 +5929,15 @@ const storeTraps = {
|
|
|
5710
5929
|
},
|
|
5711
5930
|
ownKeys(target) {
|
|
5712
5931
|
if (pendingCheckActive) witnessAffectsMark(target);
|
|
5713
|
-
if (getObserver() !== target[STORE_FIREWALL])
|
|
5932
|
+
if (getObserver() !== target[STORE_FIREWALL]) {
|
|
5933
|
+
trackSelf(target);
|
|
5934
|
+
// trackSelf no-ops untracked, so enumeration of an unresolved derived
|
|
5935
|
+
// store would otherwise leak the seed's structure (#2897). The write
|
|
5936
|
+
// path is exempt (like the get/has traps' writeOnly early returns):
|
|
5937
|
+
// the first landing's reconcile enumerates the store while
|
|
5938
|
+
// STATUS_UNINITIALIZED is still set — it IS the initialization.
|
|
5939
|
+
if (!getObserver() && !writeOnly(target[$PROXY])) throwIfUninitialized(target);
|
|
5940
|
+
}
|
|
5714
5941
|
// Merge optimistic override with regular override for key enumeration
|
|
5715
5942
|
let keys = getKeys(target[STORE_VALUE], target[STORE_OVERRIDE], false);
|
|
5716
5943
|
if (target[STORE_OPTIMISTIC_OVERRIDE]) {
|
|
@@ -5852,7 +6079,7 @@ function propagateAffectsMark(node) {
|
|
|
5852
6079
|
const sentinel = getAffectsSentinel(node);
|
|
5853
6080
|
const error = new NotReadyError(sentinel);
|
|
5854
6081
|
forEachDependent(node, sub => {
|
|
5855
|
-
if (
|
|
6082
|
+
if (!sub._pendingSources?.has(sentinel)) {
|
|
5856
6083
|
notifyStatus(sub, STATUS_PENDING, error);
|
|
5857
6084
|
}
|
|
5858
6085
|
});
|
|
@@ -5895,7 +6122,7 @@ function markAffects(node) {
|
|
|
5895
6122
|
/**
|
|
5896
6123
|
* Registers one `affects()` mark on a node: counts it, records the
|
|
5897
6124
|
* registration with the current transaction (after initTransition the queue's
|
|
5898
|
-
*
|
|
6125
|
+
* batch IS the active transition, mirroring `_optimisticNodes`), and
|
|
5899
6126
|
* propagates STATUS_PENDING downstream on the status rails so everything
|
|
5900
6127
|
* DERIVED from the marked data reads pending too. Propagation runs on every
|
|
5901
6128
|
* registration (not just the first): subscribers gained since an earlier
|
|
@@ -5903,7 +6130,7 @@ function markAffects(node) {
|
|
|
5903
6130
|
*/
|
|
5904
6131
|
function registerAffectsMark(node) {
|
|
5905
6132
|
markAffects(node);
|
|
5906
|
-
globalQueue._affectsNodes.push(node);
|
|
6133
|
+
globalQueue._batch._affectsNodes.push(node);
|
|
5907
6134
|
propagateAffectsMark(node);
|
|
5908
6135
|
schedule();
|
|
5909
6136
|
}
|
|
@@ -5946,7 +6173,7 @@ function onlyMarkPending(el) {
|
|
|
5946
6173
|
for (const s of sources) if (!s._affectsFor) return false;
|
|
5947
6174
|
return true;
|
|
5948
6175
|
}
|
|
5949
|
-
return
|
|
6176
|
+
return false;
|
|
5950
6177
|
}
|
|
5951
6178
|
/**
|
|
5952
6179
|
* Collect the still-live marked nodes behind a pended owner's sentinel
|
|
@@ -5960,11 +6187,7 @@ function onlyMarkPending(el) {
|
|
|
5960
6187
|
* `GlobalQueue._collectMarkSources`, gated on `activeAffectsMarks`.
|
|
5961
6188
|
*/
|
|
5962
6189
|
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) {
|
|
6190
|
+
if (el._pendingSources) {
|
|
5968
6191
|
for (const s of el._pendingSources) {
|
|
5969
6192
|
const marked = s._affectsFor;
|
|
5970
6193
|
if (marked && marked._affectsCount) into.push(marked);
|
|
@@ -6051,54 +6274,90 @@ function createOptimisticStore(first, second, options) {
|
|
|
6051
6274
|
}
|
|
6052
6275
|
// Clear the optimistic overrides of a settling batch of stores and notify
|
|
6053
6276
|
// signals. Owns the whole batch (iterate + clear + reschedule) so the
|
|
6054
|
-
// scheduler's flush tail carries only a size-guarded hook call.
|
|
6055
|
-
|
|
6277
|
+
// scheduler's flush tail carries only a size-guarded hook call. The
|
|
6278
|
+
// completing transition scopes each clear to its own layer keys (#2899).
|
|
6279
|
+
function clearOptimisticStores(stores, completing) {
|
|
6056
6280
|
for (const store of stores) {
|
|
6057
6281
|
const target = store[$TARGET];
|
|
6058
|
-
if (target?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(target);
|
|
6282
|
+
if (target?.[STORE_OPTIMISTIC_OVERRIDE]) clearOptimisticOverride(target, completing);
|
|
6059
6283
|
}
|
|
6060
6284
|
stores.clear();
|
|
6061
6285
|
schedule();
|
|
6062
6286
|
}
|
|
6063
|
-
|
|
6287
|
+
/**
|
|
6288
|
+
* Consume optimistic layer entries and reset their backing nodes to base.
|
|
6289
|
+
* With `completing` (settle path, #2899) only entries the settling
|
|
6290
|
+
* transaction owns are consumed — the layer is store-wide but concurrent
|
|
6291
|
+
* actions revert independently, so keys stamped by a still-in-flight
|
|
6292
|
+
* transition survive (node-level overrides already have this granularity via
|
|
6293
|
+
* _optimisticNodes; this is the layer's half). `null` consumes ambient
|
|
6294
|
+
* (transaction-less) entries at plain flush end. Omitted (projection landing:
|
|
6295
|
+
* fresh authoritative data) consumes everything — the correction supersedes
|
|
6296
|
+
* every tentative layer.
|
|
6297
|
+
*/
|
|
6298
|
+
function clearOptimisticOverride(target, completing) {
|
|
6064
6299
|
const override = target[STORE_OPTIMISTIC_OVERRIDE];
|
|
6065
6300
|
if (!override) return;
|
|
6066
6301
|
const nodes = target[STORE_NODE];
|
|
6067
|
-
|
|
6068
|
-
|
|
6302
|
+
const owners = target[STORE_OPTIMISTIC_OWNERS];
|
|
6303
|
+
const scoped = completing !== undefined;
|
|
6304
|
+
let cleared = false;
|
|
6305
|
+
let remaining = false;
|
|
6069
6306
|
// Use projectionWriteActive to bypass optimistic signal behavior (no lane creation)
|
|
6070
6307
|
// This ensures reversion effects go to regular queues, not lane queues
|
|
6071
6308
|
const wasProjectionWriteActive = projectionWriteActive;
|
|
6072
6309
|
setProjectionWriteActive(true);
|
|
6073
6310
|
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();
|
|
6311
|
+
for (const key of Reflect.ownKeys(override)) {
|
|
6312
|
+
if (scoped) {
|
|
6313
|
+
let owner = owners?.[key] ?? null;
|
|
6314
|
+
// Resolve merge chains (entangled actions settle as one); path-compress
|
|
6315
|
+
// so later keys skip the walk. A dead owner (`_done === true`) settled
|
|
6316
|
+
// through some other path — never strand its entry. A null owner is an
|
|
6317
|
+
// ambient write: its batch belongs to whichever transaction adopted it
|
|
6318
|
+
// (initTransition mid-batch) or to the plain flush, so it clears on
|
|
6319
|
+
// whichever clear call reaches this store first.
|
|
6320
|
+
if (owner) {
|
|
6321
|
+
if (typeof owner._done === "object") owner = owners[key] = currentTransition(owner);
|
|
6322
|
+
if (owner !== completing && owner._done !== true) {
|
|
6323
|
+
remaining = true;
|
|
6324
|
+
continue;
|
|
6093
6325
|
}
|
|
6094
6326
|
}
|
|
6095
6327
|
}
|
|
6096
|
-
|
|
6097
|
-
if (
|
|
6098
|
-
|
|
6099
|
-
|
|
6328
|
+
delete override[key];
|
|
6329
|
+
if (owners) delete owners[key];
|
|
6330
|
+
cleared = true;
|
|
6331
|
+
const node = nodes?.[key];
|
|
6332
|
+
if (node) {
|
|
6333
|
+
// Clear lane association so effects go to regular queue
|
|
6334
|
+
node._optimisticLane = undefined;
|
|
6335
|
+
// Re-read from base — this key left the optimistic layer above, so the
|
|
6336
|
+
// overlay resolves to STORE_OVERRIDE or STORE_VALUE.
|
|
6337
|
+
const layer = getOverlayLayer(target, key);
|
|
6338
|
+
const baseValue = layer ? layer[key] : target[STORE_VALUE][key];
|
|
6339
|
+
const value = baseValue === $DELETED ? undefined : baseValue;
|
|
6340
|
+
const next = isWrappable(value) ? wrap(value, target) : value;
|
|
6341
|
+
const prev = visibleNodeValue(node);
|
|
6342
|
+
node._overrideValue = NOT_PENDING;
|
|
6343
|
+
node._overrideOwner = null;
|
|
6344
|
+
node._pendingValue = NOT_PENDING;
|
|
6345
|
+
node._value = next;
|
|
6346
|
+
if (!node._equals || !node._equals(prev, next)) {
|
|
6347
|
+
insertSubs(node, true);
|
|
6348
|
+
schedule();
|
|
6349
|
+
}
|
|
6100
6350
|
}
|
|
6101
6351
|
}
|
|
6352
|
+
if (!remaining) {
|
|
6353
|
+
delete target[STORE_OPTIMISTIC_OVERRIDE];
|
|
6354
|
+
delete target[STORE_OPTIMISTIC_OWNERS];
|
|
6355
|
+
}
|
|
6356
|
+
// Notify $TRACK
|
|
6357
|
+
if (cleared && nodes?.[$TRACK]) {
|
|
6358
|
+
nodes[$TRACK]._optimisticLane = undefined;
|
|
6359
|
+
notifySelf(target);
|
|
6360
|
+
}
|
|
6102
6361
|
} finally {
|
|
6103
6362
|
setProjectionWriteActive(wasProjectionWriteActive);
|
|
6104
6363
|
}
|
|
@@ -6309,6 +6568,21 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6309
6568
|
result[i] = unwrapped;
|
|
6310
6569
|
}
|
|
6311
6570
|
}
|
|
6571
|
+
// Enumerate array symbols separately to avoid scanning indices twice.
|
|
6572
|
+
// Spread copies omit symbols, so assign them after the numeric walk.
|
|
6573
|
+
const symbols = lookup ? getStoreSymbols(item, override) : [];
|
|
6574
|
+
for (let i = 0, l = symbols.length; i < l; i++) {
|
|
6575
|
+
const prop = symbols[i];
|
|
6576
|
+
const desc = getPropertyDescriptor(item, override, prop);
|
|
6577
|
+
if (!desc || desc.get) continue;
|
|
6578
|
+
v = override && prop in override ? override[prop] : item[prop];
|
|
6579
|
+
if (track && isWrappable(v)) wrap(v, target);
|
|
6580
|
+
unwrapped = snapshotImpl(v, track, map, lookup);
|
|
6581
|
+
if (unwrapped !== v || result) {
|
|
6582
|
+
if (!result) map.set(item, (result = Object.assign([...item], item)));
|
|
6583
|
+
result[prop] = unwrapped;
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6312
6586
|
// Deleted trailing slots are skipped above, so restore length to preserve
|
|
6313
6587
|
// holes instead of truncating the copy (#2846) — mirrors unwrapStoreValue.
|
|
6314
6588
|
if (result) result.length = len;
|
|
@@ -6316,7 +6590,9 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6316
6590
|
// Specialized walk for the common no-overlay case (from #2756): the own
|
|
6317
6591
|
// descriptor gives the value directly, so each property is read once with
|
|
6318
6592
|
// no overlay membership checks.
|
|
6319
|
-
|
|
6593
|
+
// A lookup means this object belongs to an immutable store backing tree,
|
|
6594
|
+
// even if that nested value has not needed its own proxy yet.
|
|
6595
|
+
const keys = lookup ? getStoreKeys(item, undefined) : getKeys(item, undefined);
|
|
6320
6596
|
for (let i = 0, l = keys.length; i < l; i++) {
|
|
6321
6597
|
const prop = keys[i];
|
|
6322
6598
|
const desc = Object.getOwnPropertyDescriptor(item, prop);
|
|
@@ -6332,7 +6608,9 @@ function snapshotImpl(item, track, map, lookup) {
|
|
|
6332
6608
|
}
|
|
6333
6609
|
}
|
|
6334
6610
|
} else {
|
|
6335
|
-
|
|
6611
|
+
// An override only exists on a store record, and the target branch above
|
|
6612
|
+
// always set `lookup` alongside it — so this branch is always store-keyed.
|
|
6613
|
+
const keys = getStoreKeys(item, override);
|
|
6336
6614
|
for (let i = 0, l = keys.length; i < l; i++) {
|
|
6337
6615
|
let prop = keys[i];
|
|
6338
6616
|
const desc = getPropertyDescriptor(item, override, prop);
|
|
@@ -6599,6 +6877,15 @@ function mapArray(list, map, options) {
|
|
|
6599
6877
|
return accessor(node);
|
|
6600
6878
|
}
|
|
6601
6879
|
const pureOptions = { ownedWrite: true };
|
|
6880
|
+
// Exception safety (#2903): a map callback can throw NotReadyError mid-pass
|
|
6881
|
+
// (async read), and the computed re-runs the whole pass after settle. Every
|
|
6882
|
+
// pass therefore STAGES its work — new rows are created into temp arrays and
|
|
6883
|
+
// removals are deferred — and commits to `this` only after every mapper
|
|
6884
|
+
// succeeded. An aborted pass disposes just the owners it created and leaves
|
|
6885
|
+
// `_items`/`_mappings`/`_nodes`/`_rows`/`_indexes`/`_len` exactly as they
|
|
6886
|
+
// were, so the retry diffs against uncorrupted state. Consequence of the
|
|
6887
|
+
// strong-abort ordering: removed rows now dispose AFTER the pass's new rows
|
|
6888
|
+
// are created (you cannot destroy state before knowing the pass will land).
|
|
6602
6889
|
function updateKeyedMap() {
|
|
6603
6890
|
const newItems = this._list() || [],
|
|
6604
6891
|
newLen = newItems.length;
|
|
@@ -6606,25 +6893,26 @@ function updateKeyedMap() {
|
|
|
6606
6893
|
runWithOwner(this._owner, () => {
|
|
6607
6894
|
let i,
|
|
6608
6895
|
j,
|
|
6896
|
+
rows,
|
|
6897
|
+
indexes,
|
|
6898
|
+
// Mappers write freshly-created row/index signals into the STAGE
|
|
6899
|
+
// arrays (`rows`/`indexes`), never into `this._rows`/`this._indexes`.
|
|
6609
6900
|
mapper = this._rows
|
|
6610
6901
|
? this._byIndex
|
|
6611
6902
|
? () => {
|
|
6612
|
-
|
|
6613
|
-
return this._map(accessor(
|
|
6903
|
+
rows[j] = signal(newItems[j], pureOptions);
|
|
6904
|
+
return this._map(accessor(rows[j]), j);
|
|
6614
6905
|
}
|
|
6615
6906
|
: () => {
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
return this._map(
|
|
6619
|
-
accessor(this._rows[j]),
|
|
6620
|
-
this._indexes ? accessor(this._indexes[j]) : undefined
|
|
6621
|
-
);
|
|
6907
|
+
rows[j] = signal(newItems[j], pureOptions);
|
|
6908
|
+
indexes && (indexes[j] = signal(j, pureOptions));
|
|
6909
|
+
return this._map(accessor(rows[j]), indexes ? accessor(indexes[j]) : undefined);
|
|
6622
6910
|
}
|
|
6623
6911
|
: this._indexes
|
|
6624
6912
|
? () => {
|
|
6625
6913
|
const item = newItems[j];
|
|
6626
|
-
|
|
6627
|
-
return this._map(item, accessor(
|
|
6914
|
+
indexes[j] = signal(j, pureOptions);
|
|
6915
|
+
return this._map(item, accessor(indexes[j]));
|
|
6628
6916
|
}
|
|
6629
6917
|
: () => {
|
|
6630
6918
|
const item = newItems[j];
|
|
@@ -6642,19 +6930,31 @@ function updateKeyedMap() {
|
|
|
6642
6930
|
this._indexes && (this._indexes = []);
|
|
6643
6931
|
}
|
|
6644
6932
|
if (this._fallback && !this._mappings[0]) {
|
|
6645
|
-
//
|
|
6933
|
+
// an aborted fallback attempt leaves an owner without a mapping;
|
|
6934
|
+
// dispose it before re-creating
|
|
6935
|
+
this._nodes[0]?.dispose();
|
|
6646
6936
|
this._mappings[0] = runWithOwner((this._nodes[0] = createOwner()), this._fallback);
|
|
6647
6937
|
}
|
|
6648
6938
|
}
|
|
6649
6939
|
// fast path for new create
|
|
6650
6940
|
else if (this._len === 0) {
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
this.
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6941
|
+
const mappings = new Array(newLen);
|
|
6942
|
+
const nodes = new Array(newLen);
|
|
6943
|
+
rows = this._rows && new Array(newLen);
|
|
6944
|
+
indexes = this._indexes && new Array(newLen);
|
|
6945
|
+
try {
|
|
6946
|
+
for (j = 0; j < newLen; j++) mappings[j] = runWithOwner((nodes[j] = createOwner()), mapper);
|
|
6947
|
+
} catch (err) {
|
|
6948
|
+
for (i = 0; i <= j; i++) nodes[i]?.dispose();
|
|
6949
|
+
throw err;
|
|
6657
6950
|
}
|
|
6951
|
+
// commit
|
|
6952
|
+
if (this._nodes[0]) this._nodes[0].dispose(); // previous fallback
|
|
6953
|
+
this._mappings = mappings;
|
|
6954
|
+
this._nodes = nodes;
|
|
6955
|
+
rows && (this._rows = rows);
|
|
6956
|
+
indexes && (this._indexes = indexes);
|
|
6957
|
+
this._items = newItems.slice(0);
|
|
6658
6958
|
this._len = newLen;
|
|
6659
6959
|
} else {
|
|
6660
6960
|
let start,
|
|
@@ -6664,10 +6964,12 @@ function updateKeyedMap() {
|
|
|
6664
6964
|
key,
|
|
6665
6965
|
newIndices,
|
|
6666
6966
|
newIndicesNext,
|
|
6967
|
+
removed,
|
|
6968
|
+
created,
|
|
6667
6969
|
temp = new Array(newLen),
|
|
6668
|
-
tempNodes = new Array(newLen)
|
|
6669
|
-
|
|
6670
|
-
|
|
6970
|
+
tempNodes = new Array(newLen);
|
|
6971
|
+
rows = this._rows ? new Array(newLen) : undefined;
|
|
6972
|
+
indexes = this._indexes ? new Array(newLen) : undefined;
|
|
6671
6973
|
// skip common prefix
|
|
6672
6974
|
for (
|
|
6673
6975
|
start = 0, end = Math.min(this._len, newLen);
|
|
@@ -6689,8 +6991,8 @@ function updateKeyedMap() {
|
|
|
6689
6991
|
) {
|
|
6690
6992
|
temp[newEnd] = this._mappings[end];
|
|
6691
6993
|
tempNodes[newEnd] = this._nodes[end];
|
|
6692
|
-
|
|
6693
|
-
|
|
6994
|
+
rows && (rows[newEnd] = this._rows[end]);
|
|
6995
|
+
indexes && (indexes[newEnd] = this._indexes[end]);
|
|
6694
6996
|
}
|
|
6695
6997
|
// 0) prepare a map of all indices in newItems, scanning backwards so we encounter them in natural order
|
|
6696
6998
|
newIndices = new Map();
|
|
@@ -6702,7 +7004,7 @@ function updateKeyedMap() {
|
|
|
6702
7004
|
newIndicesNext[j] = i === undefined ? -1 : i;
|
|
6703
7005
|
newIndices.set(key, j);
|
|
6704
7006
|
}
|
|
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,
|
|
7007
|
+
// 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
7008
|
for (i = start; i <= end; i++) {
|
|
6707
7009
|
item = this._items[i];
|
|
6708
7010
|
key = this._key ? this._key(item) : item;
|
|
@@ -6710,32 +7012,40 @@ function updateKeyedMap() {
|
|
|
6710
7012
|
if (j !== undefined && j !== -1) {
|
|
6711
7013
|
temp[j] = this._mappings[i];
|
|
6712
7014
|
tempNodes[j] = this._nodes[i];
|
|
6713
|
-
|
|
6714
|
-
|
|
7015
|
+
rows && (rows[j] = this._rows[i]);
|
|
7016
|
+
indexes && (indexes[j] = this._indexes[i]);
|
|
6715
7017
|
j = newIndicesNext[j];
|
|
6716
7018
|
newIndices.set(key, j);
|
|
6717
|
-
} else this._nodes[i]
|
|
7019
|
+
} else (removed ??= []).push(this._nodes[i]);
|
|
7020
|
+
}
|
|
7021
|
+
// 2) create new rows into the temp arrays; an abort disposes only these
|
|
7022
|
+
try {
|
|
7023
|
+
for (j = start; j < newLen; j++) {
|
|
7024
|
+
if (j in temp) continue;
|
|
7025
|
+
(created ??= []).push((tempNodes[j] = createOwner()));
|
|
7026
|
+
temp[j] = runWithOwner(tempNodes[j], mapper);
|
|
7027
|
+
}
|
|
7028
|
+
} catch (err) {
|
|
7029
|
+
if (created) for (i = 0; i < created.length; i++) created[i].dispose();
|
|
7030
|
+
throw err;
|
|
6718
7031
|
}
|
|
6719
|
-
//
|
|
7032
|
+
// 3) commit: land positions, then dispose exited rows
|
|
6720
7033
|
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);
|
|
7034
|
+
this._mappings[j] = temp[j];
|
|
7035
|
+
this._nodes[j] = tempNodes[j];
|
|
7036
|
+
if (rows) {
|
|
7037
|
+
this._rows[j] = rows[j];
|
|
7038
|
+
setSignal(this._rows[j], newItems[j]);
|
|
7039
|
+
}
|
|
7040
|
+
if (indexes) {
|
|
7041
|
+
this._indexes[j] = indexes[j];
|
|
7042
|
+
setSignal(this._indexes[j], j);
|
|
6734
7043
|
}
|
|
6735
7044
|
}
|
|
6736
|
-
|
|
7045
|
+
if (removed) for (i = 0; i < removed.length; i++) removed[i].dispose();
|
|
7046
|
+
// 4) in case the new set is shorter than the old, set the length of the mapped array
|
|
6737
7047
|
this._mappings = this._mappings.slice(0, (this._len = newLen));
|
|
6738
|
-
//
|
|
7048
|
+
// 5) save a copy of the mapped items for the next update
|
|
6739
7049
|
this._items = newItems.slice(0);
|
|
6740
7050
|
}
|
|
6741
7051
|
});
|
|
@@ -6767,22 +7077,32 @@ function repeat(count, map, options) {
|
|
|
6767
7077
|
}
|
|
6768
7078
|
}
|
|
6769
7079
|
: map;
|
|
6770
|
-
const
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
7080
|
+
const data = {
|
|
7081
|
+
_owner: createOwner(),
|
|
7082
|
+
_len: 0,
|
|
7083
|
+
_offset: 0,
|
|
7084
|
+
_count: count,
|
|
7085
|
+
_map: wrappedMap,
|
|
7086
|
+
_nodes: [],
|
|
7087
|
+
_mappings: [],
|
|
7088
|
+
_from: options?.from,
|
|
7089
|
+
_fallback: options?.fallback
|
|
7090
|
+
};
|
|
7091
|
+
const node = computed(updateRepeat.bind(data));
|
|
7092
|
+
// Same as mapArray: untracked reads inside the internal owner resolve via
|
|
7093
|
+
// _parentComputed, so async reads in row callbacks register with the node
|
|
7094
|
+
// (pending tracking + post-settle retry) instead of vanishing.
|
|
7095
|
+
data._owner._parentComputed = node;
|
|
6783
7096
|
node._config &= ~CONFIG_AUTO_DISPOSE;
|
|
6784
7097
|
return accessor(node);
|
|
6785
7098
|
}
|
|
7099
|
+
// Same staged-commit discipline as `updateKeyedMap` (#2903): the retained
|
|
7100
|
+
// window overlap is copied into fresh arrays, missing indexes are created
|
|
7101
|
+
// into them, and `this` is only touched — including disposal of rows leaving
|
|
7102
|
+
// the window — after every `_map` call succeeded. A NotReadyError mid-pass
|
|
7103
|
+
// disposes only the owners this pass created and leaves prior state intact
|
|
7104
|
+
// for the post-settle retry. The overlap math also subsumes the previous
|
|
7105
|
+
// disjoint-window/front-clear/end-clear/shift special cases.
|
|
6786
7106
|
function updateRepeat() {
|
|
6787
7107
|
const newLen = this._count();
|
|
6788
7108
|
const from = this._from?.() || 0;
|
|
@@ -6793,67 +7113,46 @@ function updateRepeat() {
|
|
|
6793
7113
|
this._nodes = [];
|
|
6794
7114
|
this._mappings = [];
|
|
6795
7115
|
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).
|
|
7116
|
+
// Reset offset to match the cleared data (#2767, repro 2).
|
|
6800
7117
|
this._offset = 0;
|
|
6801
7118
|
}
|
|
6802
7119
|
if (this._fallback && !this._mappings[0]) {
|
|
6803
|
-
//
|
|
7120
|
+
// an aborted fallback attempt leaves an owner without a mapping;
|
|
7121
|
+
// dispose it before re-creating
|
|
7122
|
+
this._nodes[0]?.dispose();
|
|
6804
7123
|
this._mappings[0] = runWithOwner((this._nodes[0] = createOwner()), this._fallback);
|
|
6805
7124
|
}
|
|
6806
7125
|
return;
|
|
6807
7126
|
}
|
|
6808
7127
|
const to = from + newLen;
|
|
6809
7128
|
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;
|
|
7129
|
+
// Retained overlap [keepStart, keepEnd) in global indexes; empty when the
|
|
7130
|
+
// windows are disjoint or when coming from empty/fallback.
|
|
7131
|
+
const keepStart = Math.max(from, this._offset);
|
|
7132
|
+
const keepEnd = Math.min(to, prevTo);
|
|
7133
|
+
const mappings = new Array(newLen);
|
|
7134
|
+
const nodes = new Array(newLen);
|
|
7135
|
+
for (let i = keepStart; i < keepEnd; i++) {
|
|
7136
|
+
nodes[i - from] = this._nodes[i - this._offset];
|
|
7137
|
+
mappings[i - from] = this._mappings[i - this._offset];
|
|
6824
7138
|
}
|
|
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
|
-
);
|
|
7139
|
+
try {
|
|
7140
|
+
for (let i = from; i < to; i++) {
|
|
7141
|
+
if (i >= keepStart && i < keepEnd) continue;
|
|
7142
|
+
mappings[i - from] = runWithOwner((nodes[i - from] = createOwner()), () => this._map(i));
|
|
6849
7143
|
}
|
|
6850
|
-
}
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
this.
|
|
7144
|
+
} catch (err) {
|
|
7145
|
+
for (let i = from; i < to; i++)
|
|
7146
|
+
if ((i < keepStart || i >= keepEnd) && nodes[i - from]) nodes[i - from].dispose();
|
|
7147
|
+
throw err;
|
|
7148
|
+
}
|
|
7149
|
+
// commit: dispose the previous fallback or the rows leaving the window
|
|
7150
|
+
if (this._len === 0) this._nodes[0]?.dispose();
|
|
7151
|
+
else
|
|
7152
|
+
for (let i = this._offset; i < prevTo; i++)
|
|
7153
|
+
if (i < from || i >= to) this._nodes[i - this._offset].dispose();
|
|
7154
|
+
this._mappings = mappings;
|
|
7155
|
+
this._nodes = nodes;
|
|
6857
7156
|
this._offset = from;
|
|
6858
7157
|
this._len = newLen;
|
|
6859
7158
|
});
|