chore: use queue_microtask on `#await` again (#12647)

Now that we've made `mount` and `hydrate` not call `flushSync` anymore, we can go back to using `queue_microtask` inside `#await`, which means people who want to synchronously see the pending block can do so using `flushSync` (as validated by our tests). This essentially reverts #12274
animation-params
Simon H 2 months ago committed by GitHub
parent 32c4e47060
commit 85f2f161ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -139,7 +139,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
} else {
// Wait a microtask before checking if we should show the pending state as
// the promise might have resolved by the next microtask.
Promise.resolve().then(() => {
queue_micro_task(() => {
if (!resolved) update(PENDING, true);
});
}

@ -1,4 +1,4 @@
<div class="a svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="g svelte-xyz"></div>
<div class="h svelte-xyz"></div>
<div class="d svelte-xyz"></div>
<div class="f svelte-xyz"></div>
<div class="h svelte-xyz"></div>

@ -1,3 +1,7 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a ~ .b { color: green; }
.a ~ .c { color: green; }
@ -16,20 +20,19 @@
<div class="a"></div>
<!-- non-promise, so that something renders initially -->
{#await true then value}
{#await promise then value}
<div class="b"></div>
{:catch error}
<div class="c"></div>
{/await}
{#await true}
{#await promise}
<div class="d"></div>
{:catch error}
<div class="e"></div>
{/await}
{#await true}
{#await promise}
<div class="f"></div>
{:then error}
<div class="g"></div>

@ -5,20 +5,20 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .c"',
start: { character: 217, column: 1, line: 13 },
end: { character: 224, column: 8, line: 13 }
start: { character: 269, column: 1, line: 15 },
end: { character: 276, column: 8, line: 15 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c ~ .d"',
start: { character: 242, column: 1, line: 14 },
end: { character: 249, column: 8, line: 14 }
start: { character: 296, column: 1, line: 16 },
end: { character: 303, column: 8, line: 16 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b ~ .d"',
start: { character: 267, column: 1, line: 15 },
end: { character: 274, column: 8, line: 15 }
start: { character: 323, column: 1, line: 17 },
end: { character: 330, column: 8, line: 17 }
}
]
});

@ -7,6 +7,6 @@
.a.svelte-xyz ~ .e:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .b ~ .c { color: red; }*/
/* (unused) .c ~ .d { color: red; }*/
/* (unused) .b ~ .d { color: red; }*/
/* (unused) .b ~ .c { color: green; }*/
/* (unused) .c ~ .d { color: green; }*/
/* (unused) .b ~ .d { color: green; }*/

@ -1,3 +1,3 @@
<div class="a svelte-xyz"></div>
<div class="c svelte-xyz"></div>
<div class="e svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="e svelte-xyz"></div>

@ -1,4 +1,6 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a ~ .b { color: green; }
@ -10,15 +12,14 @@
.a ~ .e { color: green; }
/* no match */
.b ~ .c { color: red; }
.c ~ .d { color: red; }
.b ~ .d { color: red; }
.b ~ .c { color: green; }
.c ~ .d { color: green; }
.b ~ .d { color: green; }
</style>
<div class="a"></div>
<!-- non-promise, so that something renders initially -->
{#await true}
{#await promise}
<div class="b"></div>
{:then value}
<div class="c"></div>

@ -1,4 +1,4 @@
<div class="a svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="g svelte-xyz"></div>
<div class="h svelte-xyz"></div>
<div class="d svelte-xyz"></div>
<div class="f svelte-xyz"></div>
<div class="h svelte-xyz"></div>

@ -1,3 +1,7 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a + .b { color: green; }
.a + .c { color: green; }
@ -16,22 +20,21 @@
<div class="a"></div>
<!-- non-promise, so that something renders initially -->
{#await true then value}
{#await promise then value}
<div class="b"></div>
{:catch error}
<div class="c"></div>
{/await}
{#await true}
{#await promise}
<div class="d"></div>
{:catch error}
<div class="e"></div>
{/await}
{#await true}
{#await promise}
<div class="f"></div>
{:then value}
{:then error}
<div class="g"></div>
{/await}

@ -5,26 +5,26 @@ export default test({
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".a + .e"',
start: { character: 188, column: 1, line: 10 },
end: { character: 195, column: 8, line: 10 }
start: { character: 242, column: 1, line: 14 },
end: { character: 249, column: 8, line: 14 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b + .c"',
start: { character: 213, column: 1, line: 11 },
end: { character: 220, column: 8, line: 11 }
start: { character: 269, column: 1, line: 15 },
end: { character: 276, column: 8, line: 15 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".c + .d"',
start: { character: 238, column: 1, line: 12 },
end: { character: 245, column: 8, line: 12 }
start: { character: 296, column: 1, line: 16 },
end: { character: 303, column: 8, line: 16 }
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".b + .d"',
start: { character: 263, column: 1, line: 13 },
end: { character: 270, column: 8, line: 13 }
start: { character: 323, column: 1, line: 17 },
end: { character: 330, column: 8, line: 17 }
}
]
});

@ -6,7 +6,7 @@
.d.svelte-xyz + .e:where(.svelte-xyz) { color: green; }
/* no match */
/* (unused) .a + .e { color: red; }*/
/* (unused) .b + .c { color: red; }*/
/* (unused) .c + .d { color: red; }*/
/* (unused) .b + .d { color: red; }*/
/* (unused) .a + .e { color: green; }*/
/* (unused) .b + .c { color: green; }*/
/* (unused) .c + .d { color: green; }*/
/* (unused) .b + .d { color: green; }*/

@ -1,3 +1,3 @@
<div class="a svelte-xyz"></div>
<div class="c svelte-xyz"></div>
<div class="e svelte-xyz"></div>
<div class="b svelte-xyz"></div>
<div class="e svelte-xyz"></div>

@ -1,3 +1,7 @@
<script>
let promise = Promise.resolve();
</script>
<style>
.a + .b { color: green; }
.a + .c { color: green; }
@ -7,16 +11,15 @@
.d + .e { color: green; }
/* no match */
.a + .e { color: red; }
.b + .c { color: red; }
.c + .d { color: red; }
.b + .d { color: red; }
.a + .e { color: green; }
.b + .c { color: green; }
.c + .d { color: green; }
.b + .d { color: green; }
</style>
<div class="a"></div>
<!-- non-promise, so that something renders initially -->
{#await true}
{#await promise}
<div class="b"></div>
{:then value}
<div class="c"></div>

@ -4,7 +4,7 @@ import * as fs from 'node:fs';
import { assert } from 'vitest';
import { compile_directory, try_read_file } from '../helpers.js';
import { assert_html_equal } from '../html_equal.js';
import { mount, unmount } from 'svelte';
import { flushSync, mount, unmount } from 'svelte';
import { suite, type BaseTest } from '../suite.js';
import type { CompileOptions, Warning } from '#compiler';
@ -60,6 +60,7 @@ const { test, run } = suite<CssTest>(async (config, cwd) => {
const target = window.document.createElement('main');
const component = mount(ClientComponent, { props: config.props ?? {}, target });
flushSync();
const html = target.innerHTML;

@ -7,6 +7,10 @@ export default test({
};
},
html: `
Waiting...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({ func: 12345 }));

@ -13,6 +13,11 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<br />
<p>the promise is pending</p>
`,
async test({ assert, component, target }) {
deferred.resolve(42);
@ -22,7 +27,6 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<br /><p>the promise is pending</p>');

@ -2,6 +2,10 @@ import { test } from '../../test';
import { sleep } from './sleep.js';
export default test({
html: `
<p>loading...</p>
`,
test({ assert, target }) {
return sleep(50).then(() => {
assert.htmlEqual(

@ -13,6 +13,10 @@ export default test({
return { thePromise: deferred.promise, show: true };
},
html: `
<div><p>loading...</p></div>
`,
test({ assert, component, target }) {
deferred.resolve(42);

@ -19,6 +19,10 @@ export default test({
return { items };
},
html: `
<p>a title: loading...</p>
`,
test({ assert, target }) {
fulfil(42);

@ -1,6 +1,7 @@
import { test } from '../../test';
export default test({
html: 'Loading...',
async test({ assert, component, target }) {
await component.test();

@ -1,8 +1,9 @@
import { test } from '../../test';
export default test({
html: '<p>wait for it...</p>',
test({ assert, component, target }) {
return component.promise.then(() => {
return component.promise.then(async () => {
assert.htmlEqual(
target.innerHTML,
`

@ -7,7 +7,6 @@ export default test({
});
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>wait for it...</p>');

@ -3,6 +3,8 @@ import { ok, test } from '../../test';
export default test({
async test({ assert, component, target }) {
assert.htmlEqual(target.innerHTML, 'Loading...');
await component.promise;
await Promise.resolve();
const span = target.querySelector('span');

@ -13,11 +13,15 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<div><p>loading...</p></div>
`,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(async () => {
.then(() => {
assert.htmlEqual(
target.innerHTML,
`
@ -28,7 +32,6 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<div><p>loading...</p></div>');

@ -15,6 +15,10 @@ export default test({
};
},
html: `
<p>loading...</p>
`,
test({ assert, component, target, window }) {
deferred.resolve(42);

@ -12,6 +12,10 @@ export default test({
return { show: true, thePromise };
},
html: `
<p>loading...</p>
`,
test({ assert, component, target }) {
fulfil(42);

@ -13,6 +13,10 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<p>loading...</p>
`,
async test({ assert, component, target }) {
deferred.resolve(42);
@ -21,7 +25,6 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>loading...</p>');
deferred.reject(new Error('something broke'));

@ -13,11 +13,16 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<p>loading...</p>
<p>loading...</p>
`,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(async () => {
.then(() => {
assert.htmlEqual(
target.innerHTML,
`
@ -29,7 +34,6 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,

@ -13,17 +13,18 @@ export default test({
return { thePromise: deferred.promise };
},
html: 'waiting',
test({ assert, component, target }) {
deferred.resolve(9000);
return deferred.promise
.then(async () => {
.then(() => {
assert.htmlEqual(target.innerHTML, 'resolved');
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'waiting');

@ -12,6 +12,10 @@ export default test({
return { thePromise };
},
html: `
<p>loading...</p><p>true!</p>
`,
test({ assert, target }) {
fulfil(42);

@ -12,11 +12,15 @@ export default test({
return { promise };
},
html: `
<p>loading...</p>
`,
test({ assert, component, target }) {
fulfil(42);
return promise
.then(async () => {
.then(() => {
assert.htmlEqual(
target.innerHTML,
`
@ -29,7 +33,6 @@ export default test({
});
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,

@ -13,11 +13,15 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<p>loading...</p>
`,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(async () => {
.then(() => {
assert.htmlEqual(
target.innerHTML,
`
@ -28,7 +32,6 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>loading...</p>');

@ -7,6 +7,10 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8]));

@ -7,6 +7,10 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([1, 2]));

@ -7,6 +7,10 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve([10, 11, 12, 13, 14, 15]));

@ -7,6 +7,10 @@ export default test({
};
},
html: `
loading...
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({ error: 'error message' }));
assert.htmlEqual(

@ -12,6 +12,10 @@ export default test({
return { thePromise };
},
html: `
loading...
`,
async test({ assert, target }) {
fulfil([]);

@ -13,6 +13,12 @@ export default test({
return { thePromise: deferred.promise };
},
html: `
<br>
<br>
<p>the promise is pending</p>
`,
expect_unhandled_rejections: true,
async test({ assert, component, target }) {
deferred.resolve();
@ -33,7 +39,6 @@ export default test({
const local = (deferred = create_deferred());
component.thePromise = local.promise;
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,

@ -19,7 +19,7 @@ export default test({
deferred.resolve(42);
return deferred.promise
.then(async () => {
.then(() => {
assert.htmlEqual(
target.innerHTML,
`
@ -30,7 +30,6 @@ export default test({
deferred = create_deferred();
component.thePromise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '');

@ -11,7 +11,6 @@ export default test({
let promise = new Promise((ok) => (resolve = ok));
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'Loading...');
resolve(42);
@ -20,7 +19,6 @@ export default test({
promise = new Promise((ok, fail) => (reject = fail));
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'Loading...');
reject(99);
@ -29,7 +27,6 @@ export default test({
promise = new Promise((ok) => (resolve = ok));
component.promise = promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, 'Loading...');
resolve(1);

@ -8,6 +8,10 @@ export default test({
};
},
html: `
<div><p>loading...</p></div>
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve({
value: 'success',

@ -8,6 +8,10 @@ export default test({
};
},
html: `
<div><p>loading...</p></div>
`,
async test({ assert, component, target }) {
await (component.thePromise = Promise.resolve(component.Component));

@ -13,18 +13,21 @@ export default test({
return { promise: deferred.promise };
},
html: `
<p>loading...</p>
`,
expect_unhandled_rejections: true,
test({ assert, component, target }) {
deferred.resolve(42);
return deferred.promise
.then(async () => {
.then(() => {
assert.htmlEqual(target.innerHTML, '<p>loaded</p>');
deferred = create_deferred();
component.promise = deferred.promise;
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p>loading...</p>');

@ -1,6 +1,10 @@
import { test } from '../../test';
export default test({
html: `
<p>...waiting</p>
`,
async test({ assert, component, target }) {
await component.promise;

@ -14,7 +14,6 @@ export default test({
intro: true,
async test({ assert, target, component, raf }) {
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '<p class="pending" foo="0.0">loading...</p>');
let time = 0;

@ -16,8 +16,7 @@ export default test({
intro: true,
async test({ assert, target, raf }) {
await Promise.resolve();
test({ assert, target, raf }) {
const p = /** @type {HTMLParagraphElement & { foo: number }} */ (target.querySelector('p'));
raf.tick(0);

@ -0,0 +1,68 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [b1, b2, b3] = target.querySelectorAll('button');
// not flushing means we wait a tick before showing the pending state ...
b2.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button>`
);
// ... and show the then state directly if the promise resolved by then
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button> then`
);
// reset
b1.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button>`
);
// flushing means we show the pending state immediately
b2.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button> pending`
);
await Promise.resolve();
b1.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button>`
);
// when not flushing ...
b3.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button>`
);
// ... we show the pending state after a tick when the promise hasn't resolved by then
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button> pending`
);
await new Promise((r) => setTimeout(r, 110));
assert.htmlEqual(
target.innerHTML,
`<button>Clear</button> <button>Immediate</button> <button>Takes time</button> then`
);
}
});

@ -0,0 +1,9 @@
<script>
let { promise } = $props();
</script>
{#await promise}
pending
{:then value}
then {value}
{/await}

@ -0,0 +1,13 @@
<script>
import Await from './await.svelte';
let promise = $state();
</script>
<button onclick={() => (promise = null)}>Clear</button>
<button onclick={() => (promise = Promise.resolve())}>Immediate</button>
<button onclick={() => (promise = new Promise((r) => setTimeout(r, 100)))}>Takes time</button>
{#if promise}
<Await {promise} />
{/if}

@ -1,7 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, target, logs, variant }) {
async test({ assert, target, logs }) {
const [b1, b2, b3, b4] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
@ -16,9 +16,6 @@ export default test({
b4.click();
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(
logs,
variant === 'hydrate' ? ['pending', 'a', 'b', 'c', 'pending'] : ['a', 'b', 'c', 'pending']
);
assert.deepEqual(logs, ['pending', 'a', 'b', 'c', 'pending']);
}
});

@ -1,7 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, target, logs, variant }) {
async test({ assert, target, logs }) {
const [b1, b2] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
@ -22,11 +22,6 @@ export default test({
`<p>then b</p><button>Show Promise A</button><button>Show Promise B</button>`
);
assert.deepEqual(
logs,
variant === 'hydrate'
? ['rendering pending block', 'rendering then block']
: ['rendering then block']
);
assert.deepEqual(logs, ['rendering pending block', 'rendering then block']);
}
});

@ -83,6 +83,8 @@ export default {
};
```
Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount` won't have been called by the time the function returns and the pending block of promises will not have been rendered yet (because `#await` waits a microtask to wait for a potentially immediately-resolved promise). If you need that guarantee, call `flushSync` (import from `'svelte'`) after calling `mount/hydrate`.
### Server API changes
Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`:
@ -331,24 +333,3 @@ Since these mismatches are extremely rare, Svelte 5 assumes that the values are
### Hydration works differently
Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions.
### `await` blocks delay render
In Svelte 4, an `{#await ...}` block immediately renders the pending section. In some cases, this is wasteful, because the promise is already resolved.
In Svelte 5 the block remains unrendered when mounting or updating the promise, until we know whether it is already resolved or not — if so, we initally render then `{:then ...}` or `{:catch ...}` section instead.
This does _not_ apply during hydration, since the pending section was already server-rendered.
To wait until the pending section has been rendered (for example during testing), use `await Promise.resolve()` after mounting or updating the promise:
```diff
let props = {
promise: getPromiseSomehow()
};
mount(App, { target, props });
+await Promise.resolve();
assert.equal(target.innerHTML, '...');
```

Loading…
Cancel
Save