@cplieger/reactive 1.3.0 → 1.3.1
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/jsr.json +1 -1
- package/package.json +2 -2
- package/src/reconcile-tree.ts +11 -1
- package/src/signal.ts +10 -2
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cplieger/reactive",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"@vitest/coverage-v8": "4.1.11",
|
|
21
21
|
"eslint": "10.8.1",
|
|
22
22
|
"fast-check": "4.9.0",
|
|
23
|
-
"happy-dom": "20.11.
|
|
23
|
+
"happy-dom": "20.11.6",
|
|
24
24
|
"prettier": "3.9.6",
|
|
25
25
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
26
26
|
"typescript-eslint": "8.67.0",
|
package/src/reconcile-tree.ts
CHANGED
|
@@ -57,6 +57,15 @@ export function reconcileChildren(parent: Node, newChildren: Node[]): void {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
let oldIdx = 0;
|
|
60
|
+
// Old nodes already placed as the resolved child for an earlier index. The
|
|
61
|
+
// keyed path cannot hand one back — each key is consumed from its FIFO queue —
|
|
62
|
+
// but `oldChildren` is a SNAPSHOT taken before the loop, so a node still
|
|
63
|
+
// occupying its original slot there can be handed back by the positional scan
|
|
64
|
+
// after it has been placed. That happens when the caller re-renders a parent
|
|
65
|
+
// with its own current children reordered (drag-and-drop, sort): a match on an
|
|
66
|
+
// already-placed node either removes it as a tag mismatch or patches over it
|
|
67
|
+
// as a tag match, and either way a child the caller asked for is lost.
|
|
68
|
+
const placed = new Set<Node>();
|
|
60
69
|
for (let i = 0; i < newChildren.length; i++) {
|
|
61
70
|
const newChild = newChildren[i];
|
|
62
71
|
if (newChild === undefined) {
|
|
@@ -82,7 +91,7 @@ export function reconcileChildren(parent: Node, newChildren: Node[]): void {
|
|
|
82
91
|
// node would corrupt a node that key-matching also reuses in the same pass.
|
|
83
92
|
while (oldIdx < oldChildren.length) {
|
|
84
93
|
const oc = oldChildren[oldIdx];
|
|
85
|
-
if (oc !== undefined && !nodeKey(oc)) {
|
|
94
|
+
if (oc !== undefined && !nodeKey(oc) && !placed.has(oc)) {
|
|
86
95
|
break;
|
|
87
96
|
}
|
|
88
97
|
oldIdx++;
|
|
@@ -110,6 +119,7 @@ export function reconcileChildren(parent: Node, newChildren: Node[]): void {
|
|
|
110
119
|
// keyed node. The insert and patch branches already target index i.
|
|
111
120
|
const ref = parent.childNodes.item(i);
|
|
112
121
|
parent.insertBefore(newChild, ref);
|
|
122
|
+
placed.add(newChild);
|
|
113
123
|
parent.removeChild(matched);
|
|
114
124
|
continue;
|
|
115
125
|
}
|
package/src/signal.ts
CHANGED
|
@@ -373,12 +373,20 @@ function endBatch(): void {
|
|
|
373
373
|
if (--batchDepth > 0) {
|
|
374
374
|
return;
|
|
375
375
|
}
|
|
376
|
+
// The drain is re-entrant: a write, a batch() or a flushSync() from an effect
|
|
377
|
+
// body sees batchDepth back at 0 and starts a nested drain, and the cycle guard
|
|
378
|
+
// below counts on those nested passes — a two-effect ping-pong recurses one
|
|
379
|
+
// level per pass rather than looping here. So a nested drain must RESTORE the
|
|
380
|
+
// count it inherited instead of zeroing it: zeroing let any nested drain,
|
|
381
|
+
// including a flushSync() that found nothing to flush, wipe the passes the
|
|
382
|
+
// enclosing drain had accumulated and multiply the effective cap.
|
|
383
|
+
const enclosingIteration = batchIteration;
|
|
376
384
|
while (batchedEffect !== undefined) {
|
|
377
385
|
let eff: EffectNode | undefined = batchedEffect;
|
|
378
386
|
batchedEffect = undefined;
|
|
379
387
|
batchIteration++;
|
|
380
388
|
if (batchIteration > MAX_BATCH_ITERATIONS) {
|
|
381
|
-
batchIteration =
|
|
389
|
+
batchIteration = enclosingIteration;
|
|
382
390
|
// Un-notify the chain we are dropping: notifyTargets skips a NOTIFIED
|
|
383
391
|
// effect, so an effect left NOTIFIED here could never be queued again —
|
|
384
392
|
// bailing would silently deafen every effect in flight, including
|
|
@@ -402,7 +410,7 @@ function endBatch(): void {
|
|
|
402
410
|
eff = next;
|
|
403
411
|
}
|
|
404
412
|
}
|
|
405
|
-
batchIteration =
|
|
413
|
+
batchIteration = enclosingIteration;
|
|
406
414
|
}
|
|
407
415
|
|
|
408
416
|
function runCleanup(eff: EffectNode): void {
|