Merge branch 'main' into svelte-custom-renderer

pull/18317/head
Paolo Ricciuti 2 months ago committed by GitHub
commit 0baca1be7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't unset batch when calling `{#await ...}` promise

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: deduplicate dependencies that are added outside the init/update cycle

@ -43,7 +43,7 @@ The maintainers meet on the final Saturday of each month. While these meetings a
### Prioritization
We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active ones repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch.
We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch.
## Bugs

@ -71,14 +71,14 @@ export function AwaitBlock(node, context) {
'await'
);
if (node.metadata.expression.has_blockers()) {
if (node.metadata.expression.has_blockers() || node.metadata.expression.has_await) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
node.metadata.expression.blockers(),
b.array([]),
b.array([]), // {#await await ...} is special insofar that the await should not be waited on
b.arrow([context.state.node], b.block([stmt]))
)
)

@ -386,13 +386,17 @@ export function VariableDeclaration(node, context) {
* @param {Expression} value
*/
function create_state_declarators(declarator, context, value) {
/**
* @param {Expression} value
* @param {string} name
*/
const mutable_source = (value, name) => {
const call = b.call('$.mutable_source', value, context.state.analysis.immutable && b.true);
return dev ? b.call('$.tag', call, b.literal(name)) : call;
};
if (declarator.id.type === 'Identifier') {
return [
b.declarator(
declarator.id,
b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
)
];
return [b.declarator(declarator.id, mutable_source(value, declarator.id.name))];
}
const tmp = b.id(context.state.scope.generate('tmp'));
@ -414,7 +418,7 @@ function create_state_declarators(declarator, context, value) {
return b.declarator(
path.node,
binding?.kind === 'state'
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
? mutable_source(value, /** @type {Identifier} */ (path.node).name)
: value
);
})

@ -9,12 +9,19 @@ import { block_close, create_child_block } from './shared/utils.js';
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
let expression = /** @type {Expression} */ (context.visit(node.expression));
if (node.metadata.expression.has_await) {
// If this is an await expression, turn it into a IIFE so that the result is a promise.
// {#await await ...} is special insofar that the await should not be waited on.
expression = b.call(b.arrow([], expression, true));
}
/** @type {Statement} */
let statement = b.stmt(
b.call(
'$.await',
b.id('$$renderer'),
/** @type {Expression} */ (context.visit(node.expression)),
expression,
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
),

@ -7,7 +7,8 @@ import {
hydrating,
skip_nodes,
set_hydrate_node,
set_hydrating
set_hydrating,
hydrate_node
} from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
@ -16,6 +17,7 @@ import { Batch, current_batch, flushSync, is_flushing_sync } from '../../reactiv
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';
import { get_node_value } from '../operations.js';
import { DEV } from 'esm-env';
const PENDING = 0;
const THEN = 1;
@ -43,16 +45,16 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
var value = runes ? source(v) : mutable_source(v, false, false);
var error = runes ? source(v) : mutable_source(v, false, false);
if (DEV) {
value.label = '{#await ...} value';
error.label = '{#await ...} error';
}
var branches = new BranchManager(node);
block(() => {
var batch = /** @type {Batch} */ (current_batch);
// we null out `current_batch` because otherwise `save(...)` will incorrectly restore it —
// the batch will already have been committed by the time it resolves
batch.deactivate();
var input = get_input();
batch.activate();
var destroyed = false;
@ -81,15 +83,17 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
// We don't want to restore the previous batch here; {#await} blocks don't follow the async logic
// we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak.
restore(false);
// ...but it might still be set here. That means a `save(...)` has restored it — but that batch will
// likely already have been committed by the time it resolves, and this resolve should be processed
// in a separate batch. We're not using batch.deactivate()/activate() above because get_input()
// could write to sources, which would then incorrectly create a new batch or could mess with
// async_derived expecting a current_batch to exist.
if (current_batch === batch) {
batch.deactivate();
}
// Make sure we have a batch, since the branch manager expects one to exist
Batch.ensure();
if (hydrating) {
// `restore()` could set `hydrating` to `true`, which we very much
// don't want — we want to restore everything _except_ this
set_hydrating(false);
}
try {
fn();
} finally {

@ -565,9 +565,15 @@ export function get(signal) {
}
}
} else {
// we're adding a dependency outside the init/update cycle
// (i.e. after an `await`)
(active_reaction.deps ??= []).push(signal);
// We're adding a dependency outside the init/update cycle (i.e. after an `await`).
// We have to deduplicate deps/reactions in this case or remove_reactions could
// disconnect deps/reactions that are actually still in use (if skip_deps says
// "disconnect all after this index" and some of the signals are also present in
// list prior to the cutoff index, i.e. that should be kept).
active_reaction.deps ??= [];
if (!includes.call(active_reaction.deps, signal)) {
active_reaction.deps.push(signal);
}
var reactions = signal.reactions;

@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [incremet, resolve] = target.querySelectorAll('button');
incremet.click();
await tick();
incremet.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(target.innerHTML, '<button>increment</button> <button>resolve</button> 4 4 1');
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const queue = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => queue.push(() => fulfil(v)));
}
async function request(v) {
const result = $derived(await push(v));
return result + count;
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queue.shift()?.()}>resolve</button>
{#await request(count) then result}{result}{/await}
{#await await push(count) + count then result}{result}{/await}
{#await await 1 then result}{result}{/await}

@ -0,0 +1,43 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [increment, resolve] = target.querySelectorAll('button');
increment.click();
await tick();
increment.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>resolve</button>
4
`
);
increment.click();
await tick();
increment.click();
await tick();
resolve.click();
await tick();
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<button>resolve</button>
8
`
);
}
});

@ -0,0 +1,20 @@
<script>
let count = $state(0);
const queue = [];
function push(v) {
if (v === 0) return v;
return new Promise((fulfil) => queue.push(() => fulfil(v)));
}
async function request(v) {
const result = $derived(await push(v));
return result + count; // read existing dependency `count` once more
}
</script>
<button onclick={() => count++}>increment</button>
<button onclick={() => queue.shift()?.()}>resolve</button>
{await request(count)}
Loading…
Cancel
Save