diff --git a/.changeset/clean-ties-start.md b/.changeset/clean-ties-start.md
new file mode 100644
index 0000000000..825ed675a5
--- /dev/null
+++ b/.changeset/clean-ties-start.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't unset batch when calling `{#await ...}` promise
diff --git a/.changeset/fair-otters-notice.md b/.changeset/fair-otters-notice.md
new file mode 100644
index 0000000000..eb5a181847
--- /dev/null
+++ b/.changeset/fair-otters-notice.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client
diff --git a/.changeset/loud-phones-return.md b/.changeset/loud-phones-return.md
new file mode 100644
index 0000000000..02f2b3a2e7
--- /dev/null
+++ b/.changeset/loud-phones-return.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: deduplicate dependencies that are added outside the init/update cycle
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
index 832c56818b..b8def178f2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js
@@ -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]))
)
)
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
index 16aa164d84..dffa79cd7a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
@@ -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
);
})
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
index 84c2a81612..ba05ca1357 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js
@@ -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([])
),
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index d6430547b5..194a7ccc0e 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -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';
@@ -15,6 +16,7 @@ import { is_runes } from '../../context.js';
import { Batch, current_batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js';
import { BranchManager } from './branches.js';
import { capture, unset_context } from '../../reactivity/async.js';
+import { DEV } from 'esm-env';
const PENDING = 0;
const THEN = 1;
@@ -42,16 +44,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;
@@ -79,15 +81,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 {
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 033afe98dc..87abbd71b6 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -560,9 +560,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;
diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-block-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/_config.js
new file mode 100644
index 0000000000..0a927e64f6
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/_config.js
@@ -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, ' 4 4 1');
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-block-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/main.svelte
new file mode 100644
index 0000000000..b619c6a949
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/main.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+{#await request(count) then result}{result}{/await}
+{#await await push(count) + count then result}{result}{/await}
+{#await await 1 then result}{result}{/await}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/_config.js b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/_config.js
new file mode 100644
index 0000000000..b7451282ac
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/_config.js
@@ -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,
+ `
+
+
+ 4
+ `
+ );
+
+ increment.click();
+ await tick();
+ increment.click();
+ await tick();
+ resolve.click();
+ await tick();
+ resolve.click();
+ await tick();
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+ 8
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/main.svelte
new file mode 100644
index 0000000000..58a5848e3b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/main.svelte
@@ -0,0 +1,20 @@
+
+
+
+
+{await request(count)}