skip pending block for already-resolved promises on mount

better-await-pending-2
Rich Harris 1 month 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 { INERT } from '../../constants.js';
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
/**
* @template V
@ -66,41 +67,61 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
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(() => {
if (input === (input = get_input())) return;
if (is_promise(input)) {
const promise = /** @type {Promise<any>} */ (input);
if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) {
destroy_effect(pending_effect);
}
if (hydrating && pending_fn) {
pending_effect = branch(() => pending_fn(anchor));
return;
}
if (then_effect) pause_effect(then_effect);
if (catch_effect) pause_effect(catch_effect);
var promise = /** @type {Promise<any>} */ (input);
var settled = false;
promise.then(
(value) => {
settled = true;
if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect);
pause();
if (then_fn) {
then_effect = create_effect(then_fn, value);
}
},
(error) => {
settled = true;
if (promise !== input) return;
if (pending_effect) pause_effect(pending_effect);
pause();
if (catch_fn) {
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 {
if (pending_effect) pause_effect(pending_effect);
if (catch_effect) pause_effect(catch_effect);

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

@ -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