mirror of https://github.com/sveltejs/svelte
fix: don't rebase just-created batches (#18117)
It's possible to rebase just-created batches. Case A: - batch A runs effects - one of these effects writes to a source. This creates a new batch B - an effect _after_ that (still part of "flush effects of batch A") executes a derived. This creates an entry in the `current` Map in batch B - batch A commits after processing batch B (`next_batch` etc logic), batch B is pending. Due to derived being part of batchB.current batch A can wrongfully think these are connected and try to rerun/add effects etc on batch B Case B: - like case A but with an additional await inside a pending snippet Case C: - batch A with source a and b, it flushes effects - one of these effects schedules batch B with b and c scheduling an async effect - batch B is deferred - batch A commits. Due to the a/b/c partial overlap it will needlessly rerun the just scheduled async effect All these cases are wrong. We fix it like this: 1. we call `this.#commit()` _before_ running the new batches, which may stick around due to having pending work, and we don't want to rebase these. This fixes case A and C 2. we capture derived values in `previous_batch` if it exists, because it means we're currently flushing effects, and derived writes belong to that batch and not a new one that might have been scheduled already. This fixes case B Discovered this while working on #18097 --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/18163/head
parent
7719a74eef
commit
9521b9f3dc
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: don't rebase just-created batches
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(),
|
||||||
|
// rescheduling an effect on the new batch that shouldn't run.
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target, logs }) {
|
||||||
|
await tick();
|
||||||
|
const [increment, resolve] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
increment.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, []);
|
||||||
|
|
||||||
|
// This resolve
|
||||||
|
// - shouldn't result in the derived execution capturing the new derived value on the new batch, but on the previous batch which is currently flushing
|
||||||
|
// - shouldn't result in #commit() rebasing the new batch
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
|
||||||
|
// As a result, this resolve shouldn't result in another execution of the effect depending on the derived
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<script>
|
||||||
|
let count = $state(0);
|
||||||
|
let double = $derived(count * 2);
|
||||||
|
let count_mirror = $state(0);
|
||||||
|
|
||||||
|
const queued = [];
|
||||||
|
function delay(v) {
|
||||||
|
if (!v) return v;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
queued.push(() => resolve(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => count++}>count {await delay(count)} | count_mirror {await delay(count_mirror)}</button>
|
||||||
|
<button onclick={() => queued.shift()?.()}>resolve</button>
|
||||||
|
|
||||||
|
{#if count}
|
||||||
|
<!-- inside if block so effects are newly created and therefore added to batch.#new_effects -->
|
||||||
|
<!-- first $effect creates new batch ... -->
|
||||||
|
{(() => {
|
||||||
|
$effect(() => {
|
||||||
|
count_mirror = count;
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
<!-- ... which second $effect shouldn't write to because the derived execution belongs to the previous batch -->
|
||||||
|
{(() => {
|
||||||
|
$effect(() => {
|
||||||
|
console.log(double);
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(),
|
||||||
|
// rescheduling an effect on the new batch that shouldn't run.
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target, logs }) {
|
||||||
|
await tick();
|
||||||
|
const [increment, resolve] = target.querySelectorAll('button');
|
||||||
|
assert.deepEqual(logs, ['delay 0']);
|
||||||
|
|
||||||
|
increment.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, ['delay 0', 'delay 2']);
|
||||||
|
|
||||||
|
// This resolve should trigger the async effect only once
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, ['delay 0', 'delay 2', 'effect run', 'delay 4']);
|
||||||
|
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, ['delay 0', 'delay 2', 'effect run', 'delay 4']);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
<script>
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
|
||||||
|
let a = $state(0);
|
||||||
|
let b = $state(0);
|
||||||
|
let c = $state(0);
|
||||||
|
|
||||||
|
const queued = [];
|
||||||
|
function delay(v) {
|
||||||
|
console.log('delay ' + v);
|
||||||
|
if (!v) return v;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
queued.push(() => resolve(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (b + c === 0 || b + c > 2) return;
|
||||||
|
console.log('effect run')
|
||||||
|
untrack(() => {
|
||||||
|
b++;
|
||||||
|
c++;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => { a++; b++; }}>increment</button>
|
||||||
|
<button onclick={() => queued.shift()?.()}>resolve</button>
|
||||||
|
{await delay(a + b + c)}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(),
|
||||||
|
// rescheduling an effect on the new batch that shouldn't run.
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target, logs }) {
|
||||||
|
await tick();
|
||||||
|
const [increment, shift, pop] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
increment.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, []);
|
||||||
|
|
||||||
|
// Resolve the blocking await which shouldn't result in the derived execution capturing
|
||||||
|
// the new derived value on the new batch, but on the previous batch which is currently flushing
|
||||||
|
pop.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
|
||||||
|
// Resolve the non-blocking await which shouldn't result in #commit() rebasing the new batch
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
|
||||||
|
// Resolve the new batch's await
|
||||||
|
shift.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
let count = $state(0);
|
||||||
|
let double = $derived(count * 2);
|
||||||
|
let count_mirror = $state(0);
|
||||||
|
|
||||||
|
const queued = [];
|
||||||
|
function delay(v) {
|
||||||
|
if (!v) return v;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
queued.push(() => resolve(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => count++}>count {await delay(count)} | count_mirror {await delay(count_mirror)}</button>
|
||||||
|
<button onclick={() => queued.shift()?.()}>shift</button>
|
||||||
|
<button onclick={() => queued.pop()?.()}>pop</button>
|
||||||
|
|
||||||
|
{#if count}
|
||||||
|
<svelte:boundary>
|
||||||
|
{await delay(count)}
|
||||||
|
{#snippet pending()}loading{/snippet}
|
||||||
|
</svelte:boundary>
|
||||||
|
<!-- inside if block so effects are newly created and therefore added to batch.#new_effects -->
|
||||||
|
<!-- first $effect creates new batch ... -->
|
||||||
|
{(() => {
|
||||||
|
$effect(() => {
|
||||||
|
count_mirror = count;
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
<!-- ... which second $effect shouldn't write to because the derived execution belongs to the previous batch -->
|
||||||
|
{(() => {
|
||||||
|
$effect(() => {
|
||||||
|
console.log(double);
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { tick } from 'svelte';
|
||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(),
|
||||||
|
// rescheduling an effect on the new batch that shouldn't run.
|
||||||
|
export default test({
|
||||||
|
async test({ assert, target, logs }) {
|
||||||
|
await tick();
|
||||||
|
const [increment, unrelated, resolve] = target.querySelectorAll('button');
|
||||||
|
|
||||||
|
increment.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, []);
|
||||||
|
|
||||||
|
// This resolve
|
||||||
|
// - shouldn't result in the derived execution capturing the new derived value on the new batch, but on the previous batch which is currently flushing
|
||||||
|
// - shouldn't result in #commit() rebasing the new batch
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>count 1 | count_mirror 0 | count_mirror_d 0 | unrelated 0</button>
|
||||||
|
<button>unrelated++</button>
|
||||||
|
<button>resolve</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
// This resolve
|
||||||
|
// - shouldn't result in the derived execution capturing the new derived value on the new batch, but on the previous batch which is currently flushing
|
||||||
|
// - shouldn't result in #commit() rebasing the new batch
|
||||||
|
unrelated.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>count 1 | count_mirror 0 | count_mirror_d 0 | unrelated 1</button>
|
||||||
|
<button>unrelated++</button>
|
||||||
|
<button>resolve</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
// As a result, this resolve shouldn't result in another execution of the effect depending on the derived
|
||||||
|
resolve.click();
|
||||||
|
await tick();
|
||||||
|
assert.deepEqual(logs, [2]);
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<button>count 1 | count_mirror 1 | count_mirror_d 2 | unrelated 1</button>
|
||||||
|
<button>unrelated++</button>
|
||||||
|
<button>resolve</button>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<script>
|
||||||
|
import { untrack } from "svelte";
|
||||||
|
|
||||||
|
let count = $state(0);
|
||||||
|
let double = $derived(count * 2);
|
||||||
|
let count_mirror = $state(0);
|
||||||
|
let unrelated = $state(0);
|
||||||
|
let count_mirror_d = $derived(count_mirror * 2);
|
||||||
|
|
||||||
|
const queued = [];
|
||||||
|
function delay(v) {
|
||||||
|
if (!v) return v;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
queued.push(() => resolve(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => count++}>count {await delay(count)} | count_mirror {await delay(count_mirror)} | count_mirror_d {count_mirror_d} | unrelated {unrelated}</button>
|
||||||
|
<button onclick={() => unrelated++}>unrelated++</button>
|
||||||
|
<button onclick={() => queued.shift()?.()}>resolve</button>
|
||||||
|
|
||||||
|
{#if count}
|
||||||
|
<!-- inside if block so effects are newly created and therefore added to batch.#new_effects -->
|
||||||
|
<!-- first $effect creates new batch ... -->
|
||||||
|
{(() => {
|
||||||
|
$effect(() => {
|
||||||
|
count_mirror = count;
|
||||||
|
untrack(() => count_mirror_d); // execute derived; should associate value with the right batch
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
<!-- ... which second $effect shouldn't write to because the derived execution belongs to the previous batch -->
|
||||||
|
{(() => {
|
||||||
|
$effect(() => {
|
||||||
|
console.log(double);
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
{/if}
|
||||||
Loading…
Reference in new issue