mirror of https://github.com/sveltejs/svelte
fix: prevent effect tree of batches from interfering with each other (#18508)
While working on another Svelte feature I noticed a bug that boils down to batches interfering with each other through the effect tree: If batch A schedules an effect, it is walked up to the root (possibly). Now if in the meantime batch B also wants to schedule effects, it can have unintended consequences. Normally this does not happen, since it's extremely hard to run into this situation. There's basically two cases: Either an unfortunate timing of microtasks, or during flushing effects are scheduling new effects which messes with a `#commit()` right after (the test case does this). To fix this we now defer walking up the tree until the batch is actually processed. That way we set + unset the markers on the branches synchronously so there's no chance of another batch interfering.main
parent
d16099ef0f
commit
ce89035ecb
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: prevent effect tree of batches from interfering with each other
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
const buttons = '<button>a</button> <button>b</button> <button>shift</button> <button>pop</button>';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target }) {
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
const [a, b, , pop] = target.querySelectorAll('button');
|
||||||
|
const shift = target.querySelectorAll('button')[2];
|
||||||
|
|
||||||
|
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>a</p><p>aa</p><p>1</p>`);
|
||||||
|
|
||||||
|
// start two independent batches, both blocked on their awaited expression
|
||||||
|
a.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>a</p><p>aa</p><p>1</p>`);
|
||||||
|
|
||||||
|
b.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>a</p><p>aa</p><p>1</p>`);
|
||||||
|
|
||||||
|
// resolve the newer (b) batch first. Committing it must not commit the
|
||||||
|
// still-pending `a` batch, whose async work has not completed — `a` must
|
||||||
|
// still read 'a', and the unrelated `c` update must not be blocked
|
||||||
|
pop.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>b</p><p>ab</p><p>2</p>`);
|
||||||
|
|
||||||
|
// stale promise from the `a` batch's first run — resolving it does nothing
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `${buttons} <p>a</p><p>b</p><p>ab</p><p>2</p>`);
|
||||||
|
|
||||||
|
// the `a` batch's re-run await ('bb') resolves — everything is committed
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.htmlEqual(target.innerHTML, `${buttons} <p>b</p><p>b</p><p>bb</p><p>2</p>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<script>
|
||||||
|
let a = $state('a');
|
||||||
|
let b = $state('a');
|
||||||
|
let c = $state(0);
|
||||||
|
|
||||||
|
let n = 0;
|
||||||
|
|
||||||
|
let queued = [];
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
function push(v) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
queued.push(() => resolve(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// when a batch changing `b` commits, this writes `c` during that batch's
|
||||||
|
// flush phase — the write lands in a freshly-created batch
|
||||||
|
$effect(() => {
|
||||||
|
b;
|
||||||
|
c = ++n;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => a = 'b'}>a</button>
|
||||||
|
<button onclick={() => b = 'b'}>b</button>
|
||||||
|
<button onclick={() => queued.shift()?.()}>shift</button>
|
||||||
|
<button onclick={() => queued.pop()?.()}>pop</button>
|
||||||
|
|
||||||
|
<p>{a}</p>
|
||||||
|
<p>{b}</p>
|
||||||
|
<p>{await push(a + b)}</p>
|
||||||
|
<p>{c}</p>
|
||||||
Loading…
Reference in new issue