mirror of https://github.com/sveltejs/svelte
fix: ignore stale boundary resets after destruction (#18886)
- `destroy_effect` cleared a boundary's component context while retained reset callbacks and queued error work could still re-enter its run path. - Reset handling is intentionally shared with hydration by [#18556](https://github.com/sveltejs/svelte/pull/18556), while `onerror` is deferred for safe state mutation by [#17561](https://github.com/sveltejs/svelte/pull/17561); neither path checked the boundary lifetime. - Make retained and deferred boundary continuations inert during destruction instead of masking null contexts in `bind:this` or `#run`, which would still let dead effects and callbacks restart. - Cover resets from `onerror` and failed snippets, cleanup ordering, delayed error transforms, DOM rendering, and hydration. Fixes https://github.com/sveltejs/svelte/issues/18885main
parent
7fb397e78f
commit
7fe14a508a
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: ignore stale boundary reset callbacks
|
||||
@ -0,0 +1,50 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
/** @type {Array<() => void>} */
|
||||
const resolvers = [];
|
||||
|
||||
export default test({
|
||||
transformError: (error) => new Promise((resolve) => resolvers.push(() => resolve(error))),
|
||||
|
||||
async test({ assert, target, logs }) {
|
||||
const [error, toggle, reset, destroy] = target.querySelectorAll('button');
|
||||
const paragraph = /** @type {HTMLParagraphElement} */ (target.querySelector('p'));
|
||||
|
||||
error.click();
|
||||
await tick();
|
||||
resolvers.shift()?.();
|
||||
await tick();
|
||||
assert.htmlEqual(paragraph.innerHTML, 'boom');
|
||||
|
||||
// A retained reset is inert after its boundary has been destroyed
|
||||
toggle.click();
|
||||
await tick();
|
||||
reset.click();
|
||||
await tick();
|
||||
assert.htmlEqual(paragraph.innerHTML, 'boom');
|
||||
|
||||
// Resolving an error transform cannot resume a destroyed boundary
|
||||
toggle.click();
|
||||
await tick();
|
||||
error.click();
|
||||
await tick();
|
||||
toggle.click();
|
||||
await tick();
|
||||
resolvers.shift()?.();
|
||||
await tick();
|
||||
assert.htmlEqual(paragraph.innerHTML, 'boom');
|
||||
|
||||
// A failed snippet's reset is also inert while the boundary is being destroyed
|
||||
toggle.click();
|
||||
await tick();
|
||||
error.click();
|
||||
await tick();
|
||||
resolvers.shift()?.();
|
||||
await tick();
|
||||
destroy.click();
|
||||
await tick();
|
||||
assert.htmlEqual(paragraph.innerHTML, 'boom,boom');
|
||||
assert.deepEqual(logs, ['render', 'render', 'render']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
<script>
|
||||
let show = $state(true);
|
||||
let must_throw = $state(false);
|
||||
let reset;
|
||||
let element;
|
||||
let errors = $state([]);
|
||||
let reset_during_cleanup = false;
|
||||
|
||||
function throw_error() {
|
||||
throw new Error('boom');
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
must_throw = false;
|
||||
show = !show;
|
||||
}
|
||||
|
||||
function reset_on_cleanup(_, reset_boundary) {
|
||||
return {
|
||||
destroy() {
|
||||
if (reset_during_cleanup) reset_boundary();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function track_render() {
|
||||
console.log('render');
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => (must_throw = true)}>error</button>
|
||||
<button onclick={toggle}>toggle</button>
|
||||
<button onclick={() => reset()}>reset</button>
|
||||
<button onclick={() => { reset_during_cleanup = true; toggle(); }}>destroy</button>
|
||||
<p>{errors.join(',')}</p>
|
||||
|
||||
{#if show}
|
||||
<svelte:boundary onerror={(error, fn) => { errors.push(error.message); reset = fn; }}>
|
||||
{track_render()}
|
||||
<input bind:this={element} />
|
||||
{must_throw ? throw_error() : ''}
|
||||
|
||||
{#snippet failed(_, failed_reset)}
|
||||
<input bind:this={element} use:reset_on_cleanup={failed_reset} />
|
||||
{/snippet}
|
||||
</svelte:boundary>
|
||||
{/if}
|
||||
Loading…
Reference in new issue