fix: transfer effects when merging batches (#18254)

An effect could be gated behind a branch. If we don't defer + transfer
them upon merge, the branch would still be marked clean but the effect
behind it is dirty but no longer reachable. It's not reachable via mark
either because that one only concerns itself with block/async effects,
and the branch gating the effect is not guaranteed to be touched by
that.

Fixes #18249

Little sad side-effect: Since we cannot reliably know _before_ traversal
if we have no blocking pending work left (the traversal could mark an if
block falsy which contains the last blocker), we gotta undo a
performance optimization.
pull/18256/head
Simon H 3 months ago committed by GitHub
parent b65a3f3fc5
commit 65283bc13a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transfer effects when merging batches

@ -289,9 +289,10 @@ export class Batch {
} }
} }
// we only reschedule previously-deferred effects if we expect // We always reschedule previously-deferred effects, not just when
// to be able to run them after processing the batch // #is_deferred() is true, because traversing the tree could make
if (!this.#is_deferred()) { // an if block that contains the last blocking pending effect falsy,
// causing the block to no longer be deferred.
for (const e of this.#dirty_effects) { for (const e of this.#dirty_effects) {
this.#maybe_dirty_effects.delete(e); this.#maybe_dirty_effects.delete(e);
set_signal_status(e, DIRTY); set_signal_status(e, DIRTY);
@ -302,7 +303,6 @@ export class Batch {
set_signal_status(e, MAYBE_DIRTY); set_signal_status(e, MAYBE_DIRTY);
this.schedule(e); this.schedule(e);
} }
}
const roots = this.#roots; const roots = this.#roots;
this.#roots = []; this.#roots = [];
@ -362,6 +362,10 @@ export class Batch {
const earlier_batch = this.#find_earlier_batch(); const earlier_batch = this.#find_earlier_batch();
if (earlier_batch) { if (earlier_batch) {
// If this batch collected deferred effects during traversal, they still need
// to run after being merged into the earlier batch.
this.#defer_effects(render_effects);
this.#defer_effects(effects);
earlier_batch.#merge(this); earlier_batch.#merge(this);
return; return;
} }
@ -503,6 +507,9 @@ export class Batch {
if (d) deferred.promise.then(d.resolve); if (d) deferred.promise.then(d.resolve);
} }
// Mark is not guaranteed not touch these, so we transfer them
this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects);
/** /**
* mark all effects that depend on `batch.current`, except the * mark all effects that depend on `batch.current`, except the
* async effects that we just resolved (TODO unless they depend * async effects that we just resolved (TODO unless they depend

@ -0,0 +1,25 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [x, x_y, pop] = target.querySelectorAll('button');
x.click();
await tick();
x_y.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
pop.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>x</button> <button>x/y</button> <button>pop</button> 2 1 1'
);
}
});

@ -0,0 +1,25 @@
<script>
let x = $state(0);
let y = $state(0);
const queued = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => {
queued.push(() => fulfil(v));
});
}
</script>
<button onclick={() => x++}>x</button>
<button onclick={() => {x++;y++}}>x/y</button>
<button onclick={() => (queued.pop()?.())}>pop</button>
{await push(x)} {await push(y)}
{#if true}
{y}
{/if}
Loading…
Cancel
Save