@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/reactive",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "license": "Apache-2.0",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/reactive",
3
- "version": "1.3.0",
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.2",
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",
@@ -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 = 0;
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 = 0;
413
+ batchIteration = enclosingIteration;
406
414
  }
407
415
 
408
416
  function runCleanup(eff: EffectNode): void {