restore transferring discard callbacks in #merge

If the surviving batch is discarded (e.g. after an unhandled error), the merged batch's discard
callbacks must run too, or each blocks keep its pending state and leak items.
async-another-try-pt-1-prune
Rich Harris 2 days ago
parent 652103bb9f
commit 3e92edfbf4
No known key found for this signature in database

@ -662,6 +662,10 @@ export class Batch {
this.oncommit(() => c(batch)); this.oncommit(() => c(batch));
} }
for (const c of batch.#discard_callbacks) {
this.ondiscard(() => c(batch));
}
for (const [s, v] of batch.#skipped_branches) { for (const [s, v] of batch.#skipped_branches) {
this.#skipped_branches.set(s, v); this.#skipped_branches.set(s, v);
this.unskipped_branches.delete(s); this.unskipped_branches.delete(s);

@ -0,0 +1,9 @@
<script>
let { item } = $props();
$effect(() => {
console.log('mount ' + item);
return () => console.log('destroy ' + item);
});
</script>
<span>{item}</span>

@ -0,0 +1,47 @@
import { flushSync, tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
async test({ assert, target, logs }) {
const [a, push, clear, shift] = target.querySelectorAll('button');
shift.click();
await tick();
assert.deepEqual(logs, ['mount 1', 'mount 2', 'mount 3']);
// batch X: a=1, pending
a.click();
await tick();
// batch Y: list changes; the each block reads `a`, so Y merges into X.
// The each block registers commit/discard callbacks with Y
push.click();
await tick();
// X resolves and throws while processing (not handled by any boundary), so it is discarded
// rather than committed. Y's discard callbacks must still run
let error = null;
shift.click();
try {
for (let i = 0; i < 10; i++) {
await Promise.resolve();
flushSync();
}
} catch (e) {
error = e;
}
assert.match(/** @type {Error} */ (error)?.message, /^boom/);
logs.length = 0;
clear.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>a</button><button>push</button><button>clear</button><button>shift</button><p>a: 0</p>`
);
// the removed items must be destroyed, not kept around for the discarded batch Y
assert.deepEqual(logs, ['destroy 1', 'destroy 2', 'destroy 3']);
}
});

@ -0,0 +1,39 @@
<script>
import Item from './Item.svelte';
let a = $state(0);
let list = $state([1, 2, 3]);
let queue = [];
function push(v) {
const d = Promise.withResolvers();
queue.push(() => d.resolve(v));
return d.promise;
}
function check(v) {
if (v === 1) throw new Error('boom');
return true;
}
</script>
<button onclick={() => a++}>a</button>
<button onclick={() => list.push(4)}>push</button>
<button onclick={() => (list = [])}>clear</button>
<button onclick={() => queue.shift()?.()}>shift</button>
<!-- no failed snippet/onerror: errors are not handled by the boundary -->
<svelte:boundary>
{#if check(await push(a))}
<p>a: {a}</p>
{/if}
<!-- reads `a`, so batches changing `list` depend on (and merge into) a pending batch changing `a` -->
{#each list.slice(0, a + 10) as item (item)}
<Item {item} />
{/each}
{#snippet pending()}
<p>loading...</p>
{/snippet}
</svelte:boundary>
Loading…
Cancel
Save