skip pending block for already-resolved promises on mount

pull/11995/head
Rich Harris 2 months ago
parent 0d51dbae32
commit f5e0751f0f

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: wait a microtask for await blocks to reduce UI churn

@ -10,6 +10,7 @@ import {
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import { INERT } from '../../constants.js'; import { INERT } from '../../constants.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
/** /**
* @template V * @template V
@ -66,41 +67,61 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
return e; return e;
} }
function pause() {
if (pending_effect) pause_effect(pending_effect);
if (then_effect) pause_effect(then_effect);
if (catch_effect) pause_effect(catch_effect);
}
const effect = block(() => { const effect = block(() => {
if (input === (input = get_input())) return; if (input === (input = get_input())) return;
if (is_promise(input)) { if (is_promise(input)) {
const promise = /** @type {Promise<any>} */ (input); if (hydrating && pending_fn) {
if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
destroy_effect(pending_effect);
}
pending_effect = branch(() => pending_fn(anchor)); pending_effect = branch(() => pending_fn(anchor));
return;
} }
if (then_effect) pause_effect(then_effect); var promise = /** @type {Promise<any>} */ (input);
if (catch_effect) pause_effect(catch_effect);
var settled = false;
promise.then( promise.then(
(value) => { (value) => {
settled = true;
if (promise !== input) return; if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect); pause();
if (then_fn) { if (then_fn) {
then_effect = create_effect(then_fn, value); then_effect = create_effect(then_fn, value);
} }
}, },
(error) => { (error) => {
settled = true;
if (promise !== input) return; if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect); pause();
if (catch_fn) { if (catch_fn) {
catch_effect = create_effect(catch_fn, error); catch_effect = create_effect(catch_fn, error);
} }
} }
); );
Promise.resolve().then(() => {
// if the promise was already resolved, avoid thrash
if (settled) return;
if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
destroy_effect(pending_effect);
}
pending_effect = branch(() => pending_fn(anchor));
}
if (then_effect) pause_effect(then_effect);
if (catch_effect) pause_effect(catch_effect);
});
} else { } else {
if (pending_effect) pause_effect(pending_effect); if (pending_effect) pause_effect(pending_effect);
if (catch_effect) pause_effect(catch_effect); if (catch_effect) pause_effect(catch_effect);

@ -59,7 +59,6 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
}; };
logs: any[]; logs: any[];
warnings: any[]; warnings: any[];
hydrate: Function;
}) => void | Promise<void>; }) => void | Promise<void>;
test_ssr?: (args: { assert: Assert }) => void | Promise<void>; test_ssr?: (args: { assert: Assert }) => void | Promise<void>;
accessors?: boolean; accessors?: boolean;
@ -104,10 +103,6 @@ export function runtime_suite(runes: boolean) {
if (config.skip_mode?.includes('hydrate')) return true; if (config.skip_mode?.includes('hydrate')) return true;
} }
if (variant === 'dom' && config.skip_mode?.includes('client')) {
return 'no-test';
}
if (variant === 'ssr') { if (variant === 'ssr') {
if ( if (
(config.mode && !config.mode.includes('server')) || (config.mode && !config.mode.includes('server')) ||
@ -166,7 +161,6 @@ async function run_test_variant(
let logs: string[] = []; let logs: string[] = [];
let warnings: string[] = []; let warnings: string[] = [];
let manual_hydrate = false;
{ {
// use some crude static analysis to determine if logs/warnings are intercepted. // use some crude static analysis to determine if logs/warnings are intercepted.
@ -186,10 +180,6 @@ async function run_test_variant(
console.log = (...args) => logs.push(...args); console.log = (...args) => logs.push(...args);
} }
if (str.slice(0, i).includes('hydrate')) {
manual_hydrate = true;
}
if (str.slice(0, i).includes('warnings') || config.warnings) { if (str.slice(0, i).includes('warnings') || config.warnings) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn = (...args) => { console.warn = (...args) => {
@ -307,30 +297,17 @@ async function run_test_variant(
let instance: any; let instance: any;
let props: any; let props: any;
let hydrate_fn: Function = () => {
throw new Error('Ensure dom mode is skipped');
};
if (runes) { if (runes) {
props = proxy({ ...(config.props || {}) }); props = proxy({ ...(config.props || {}) });
if (manual_hydrate) {
hydrate_fn = () => { const render = variant === 'hydrate' ? hydrate : mount;
instance = hydrate(mod.default, { instance = render(mod.default, {
target, target,
props, props,
intro: config.intro, intro: config.intro,
recover: config.recover ?? false recover: config.recover ?? false
}); });
};
} else {
const render = variant === 'hydrate' ? hydrate : mount;
instance = render(mod.default, {
target,
props,
intro: config.intro,
recover: config.recover ?? false
});
}
} else { } else {
instance = createClassComponent({ instance = createClassComponent({
component: mod.default, component: mod.default,
@ -380,8 +357,7 @@ async function run_test_variant(
raf, raf,
compileOptions, compileOptions,
logs, logs,
warnings, warnings
hydrate: hydrate_fn
}); });
} }

@ -0,0 +1,7 @@
<script>
$effect(() => {
console.log('mounting Pending.svelte');
});
</script>
<p>pending</p>

@ -0,0 +1,9 @@
<script>
let { value } = $props();
$effect(() => {
console.log('mounting Then.svelte');
});
</script>
<p>then {value}</p>

@ -0,0 +1,30 @@
import { test } from '../../test';
export default test({
async test({ assert, target, logs }) {
const [b1, b2] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<p>then a</p><button>Show Promise A</button><button>Show Promise B</button>`
);
b2.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<button>Show Promise A</button><button>Show Promise B</button>`
);
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<p>then b</p><button>Show Promise A</button><button>Show Promise B</button>`
);
assert.deepEqual(logs, [
'mounting Pending.svelte',
'mounting Then.svelte',
'mounting Then.svelte'
]);
}
});

@ -0,0 +1,18 @@
<script>
import Pending from "./Pending.svelte"
import Then from './Then.svelte';
const promise_a = Promise.resolve('a')
const promise_b = Promise.resolve('b')
let current_promise = $state(promise_a);
</script>
{#await current_promise}
<Pending />
{:then value}
<Then {value} />
{/await}
<button onclick={()=>{current_promise = promise_a}}>Show Promise A</button>
<button onclick={()=>{current_promise = promise_b}}>Show Promise B</button>
Loading…
Cancel
Save