fix: avoid false-positive batch invariant error (#18246)

Thanks to
https://github.com/sveltejs/svelte/issues/17940#issuecomment-4480016550
I was finally able to isolate and reproduce a false-positive invariant
error. I had a hunch this could happen and this shows it. Essentially,
you can end up in situations where two batches are scheduled to run in
the same microtask queue flush, and if the first rebases the second the
invariant will throw, which is wrong. We can avoid this by checking if a
decrement is queued.
pull/18248/head
Simon H 3 months ago committed by GitHub
parent 000c594e05
commit d654db83ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid false-positive batch invariant error

@ -679,7 +679,9 @@ export class Batch {
batch.discard();
}
} else if (sources.length > 0) {
if (DEV) {
// The microtask queue can contain the batch already scheduled to run right
// after this one is finished, so throwing the invariant would be wrong here.
if (DEV && !batch.#decrement_queued) {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
}
@ -732,7 +734,8 @@ export class Batch {
}
// Only apply and traverse when we know we triggered async work with marking the effects
if (batch.#roots.length > 0) {
// and know this won't run anyway right afterwards
if (batch.#roots.length > 0 && !batch.#decrement_queued) {
batch.apply();
for (var root of batch.#roots) {

@ -0,0 +1,16 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
// This test mainly checks that we don't run into the 'Batch has scheduled roots' invariant wrongly.
// It is crafted such that two batches are scheduled to run in the same microtask, and the first
// tries to rebase the second.
async test({ assert, target }) {
await tick();
const [run] = target.querySelectorAll('button');
run.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>run</button> none none 0');
}
});

@ -0,0 +1,8 @@
<script>
let selectedId = $state(1);
let selectedOption = $derived(selectedId ? await selectedId : null);
</script>
<button onclick={() => (selectedId = null)}>run</button>
{selectedId ?? "none"} {selectedOption ?? "none"} {$effect.pending()}
Loading…
Cancel
Save