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