fix: end a restored reaction context at the end of its synchronous segment (#18694)

When an async expression resumes after a pickled `await`, the thunk
returned by `save()` in `reactivity/async.js` calls `restore()` to
re-arm `active_reaction` for the rest of the expression, then disarms it
with `queue_micro_task(unset_context)`. Any microtask already queued
before that one runs inside the restored context. If it writes to a
source, `set()` throws `state_unsafe_mutation` in production, since the
guard is not dev-only. #18453 introduced the queued disarm and noted
this case in review as unavoidable. SvelteKit hits it in practice: its
fetch continuations write to internal `$state` (sveltejs/kit#16914), and
a user's `$derived((await q()).length)` resuming in the same tick makes
that write throw and drops the update signal.

The context restored by a `save` thunk now ends with the synchronous
segment it was restored in. A `restored` flag is set by the thunk and
consumed on entry to `save` and `track_reactivity_loss`, so every
suspension ends it; once an expression contains a pickled await, the
analysis pickles every later await in it too (`has_pickled_await` on
`ExpressionMetadata`), so a trailing await compiles to `$.save` rather
than a bare `await`. At the end of the body, `async_thunk` in
`3-transform/client/utils.js` wraps the return expression in
`$.unsave(...)` when the metadata has a pickled await. If the body
throws instead, the context is unset by `async_derived`'s existing
`finally`, as before. The queued microtask in `save` is removed.

Output is unchanged for expressions that pickle nothing (`$derived(await
a)` compiles byte for byte the same). Expressions with a pickled await
gain one `$.unsave(` call per body, and their trailing await becomes a
`$.save`, 4 to 6 bytes gzipped in the added tests. At runtime a boolean
write replaces a queued microtask per resume. `bench:compare` shows no
difference outside run-to-run noise.

Two runtime tests reproduce the throw without any library involved, one
in dev and one with the prod `await` shape, and fail on `main`.

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/18702/head
Nic Polumeyv 3 weeks ago committed by GitHub
parent 1be496f0a6
commit f2648b3537
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: end a restored reaction context at the end of its synchronous segment

@ -285,7 +285,7 @@ export function analyze_module(source, options) {
runes: true,
immutable: true,
tracing: false,
async_deriveds: new Set(),
async_deriveds: new Map(),
comments,
classes: new Map(),
pickled_awaits: new Set()
@ -557,7 +557,7 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set(),
async_deriveds: new Map(),
pickled_awaits: new Set(),
instance_body: {
sync: [],

@ -1,6 +1,7 @@
/** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */
/** @import { Context } from '../types' */
/** @import { AST } from '#compiler' */
/** @import { ExpressionMetadata } from '../../nodes.js' */
import * as e from '../../../errors.js';
/**
@ -10,15 +11,20 @@ import * as e from '../../../errors.js';
export function AwaitExpression(node, context) {
const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1;
// preserve context for awaits that precede other expressions in template or `$derived(...)`
if (
is_reactive_expression(
context.path,
context.state.derived_function_depth === context.state.function_depth
) &&
!is_last_evaluated_expression(context.path, node)
)
) {
context.state.analysis.pickled_awaits.add(node);
const expression = /** @type {ExpressionMetadata} */ (context.state.expression);
// preserve context for awaits that precede other expressions in template or `$derived(...)`,
// and for any await that follows one, so the restored context ends at the next suspension
if (expression.has_pickled_await || !is_last_evaluated_expression(context.path, node)) {
context.state.analysis.pickled_awaits.add(node);
expression.has_pickled_await = true;
}
}
let suspend = tla;

@ -253,7 +253,7 @@ export function CallExpression(node, context) {
});
if (expression.has_await) {
context.state.analysis.async_deriveds.add(node);
context.state.analysis.async_deriveds.set(node, expression);
}
// Tell surrounding declaration tag about metadata for correct calculation of blockers etc

@ -2,6 +2,7 @@
/** @import { Binding } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */
/** @import { Analysis } from '../../types.js' */
/** @import { ExpressionMetadata } from '../../nodes.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '#compiler/builders';
import { is_simple_expression, save } from '../../../utils/ast.js';
@ -164,20 +165,46 @@ export function should_proxy(node, scope) {
return true;
}
/**
* An async thunk. If an `await` inside restores the reaction context via `$.save`,
* the body exits through `$.unsave` so the context cannot leak into foreign microtasks
* that run before the returned promise settles
* @param {Expression | BlockStatement} body
* @param {ExpressionMetadata} metadata
*/
export function async_thunk(body, metadata) {
if (!metadata.has_pickled_await) {
return b.arrow([], body, true);
}
const block = body.type === 'BlockStatement' ? body : b.block([b.return(body)]);
return b.arrow(
[],
b.block([
{
type: 'TryStatement',
block,
handler: null,
finalizer: b.block([b.stmt(b.call('$.unsave'))])
}
]),
true
);
}
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state
* @param {Expression | BlockStatement} expression
* @param {boolean} [async]
* @param {ExpressionMetadata} [metadata]
*/
export function create_derived(state, expression, async = false) {
const thunk = b.thunk(expression, async);
if (async) {
return save(b.call('$.async_derived', thunk));
} else {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk);
export function create_derived(state, expression, metadata) {
if (metadata?.has_await) {
return save(b.call('$.async_derived', async_thunk(expression, metadata)));
}
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', b.thunk(expression));
}
/**

@ -1,9 +1,9 @@
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { async_thunk, create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
@ -15,10 +15,10 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(
build_expression(context, node.expression, node.metadata.expression),
node.metadata.expression.has_await
);
const input = build_expression(context, node.expression, node.metadata.expression);
const expression = node.metadata.expression.has_await
? async_thunk(input, node.metadata.expression)
: b.thunk(input);
let then_block;
let catch_block;

@ -19,7 +19,7 @@ export function ConstTag(node, context) {
if (declaration.id.type === 'Identifier') {
const init = build_expression(context, declaration.init, node.metadata.expression);
let expression = create_derived(context.state, init, node.metadata.expression.has_await);
let expression = create_derived(context.state, init, node.metadata.expression);
if (dev) {
expression = b.call('$.tag', expression, b.literal(declaration.id.name));
@ -69,7 +69,7 @@ export function ConstTag(node, context) {
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
]);
let expression = create_derived(context.state, block, node.metadata.expression.has_await);
let expression = create_derived(context.state, block, node.metadata.expression);
if (dev) {
expression = b.call('$.tag', expression, b.literal('[@const]'));

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { add_state_transformers } from './shared/declarations.js';
/**
@ -85,5 +86,5 @@ export function add_async_declaration(context, metadata, ids, assignments, kind
metadata.expression.has_await ||
assignments.some((assignment) => has_await_expression(assignment));
const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments);
run.thunks.push(b.thunk(body, has_await));
run.thunks.push(has_await ? async_thunk(body, metadata.expression) : b.thunk(body));
}

@ -12,6 +12,7 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
@ -313,7 +314,9 @@ export function EachBlock(node, context) {
const has_await = node.metadata.expression.has_await;
const get_collection = b.thunk(collection, has_await);
const get_collection = has_await
? async_thunk(collection, node.metadata.expression)
: b.thunk(collection);
const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection;
const render_args = [b.id('$$anchor'), item];

@ -2,6 +2,7 @@
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { build_expression } from './shared/utils.js';
/**
@ -46,7 +47,7 @@ export function HtmlTag(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$html')] : [context.state.node],
b.block([statement])

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
@ -117,7 +118,7 @@ export function IfBlock(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$condition')] : [context.state.node],
b.block(statements)

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { build_expression, add_svelte_meta } from './shared/utils.js';
/**
@ -31,7 +32,7 @@ export function KeyBlock(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$key')] : [context.state.node],
b.block([statement])

@ -4,6 +4,7 @@
import { dev, locator } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { async_thunk } from '../utils.js';
import { determine_namespace_for_children } from '../../utils.js';
import {
build_attribute_value,
@ -147,7 +148,7 @@ export function SvelteElement(node, context) {
'$.async',
context.state.node,
node.metadata.expression.blockers(),
has_await ? b.array([b.thunk(expression, true)]) : b.void0,
has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0,
b.arrow(
has_await ? [context.state.node, b.id('$$tag')] : [context.state.node],
b.block(statements)

@ -6,7 +6,13 @@ import { extract_paths, save } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import {
async_thunk,
get_prop_source,
is_prop_source,
is_state_source,
should_proxy
} from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
@ -200,9 +206,10 @@ export function VariableDeclaration(node, context) {
}
if (rune === '$derived' || rune === '$derived.by') {
const is_async = context.state.analysis.async_deriveds.has(
const metadata = context.state.analysis.async_deriveds.get(
/** @type {CallExpression} */ (init)
);
const is_async = metadata !== undefined;
if (declarator.id.type === 'Identifier') {
let expression = /** @type {Expression} */ (context.visit(value));
@ -213,7 +220,7 @@ export function VariableDeclaration(node, context) {
/** @type {Expression} */
let call = b.call(
'$.async_derived',
b.thunk(expression, true),
async_thunk(expression, metadata),
dev && b.literal(declarator.id.name),
location ? b.literal(location) : undefined
);
@ -246,7 +253,7 @@ export function VariableDeclaration(node, context) {
call = b.call(
'$.async_derived',
b.thunk(expression, true),
async_thunk(expression, metadata),
dev &&
b.literal(
`[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`

@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, is_state_source } from '../../utils.js';
import { async_thunk, build_getter, is_state_source } from '../../utils.js';
import { ExpressionMetadata } from '../../../../nodes.js';
/**
@ -16,10 +16,10 @@ import { ExpressionMetadata } from '../../../../nodes.js';
* from templates and replacing them with `$0`, `$1` etc
*/
export class Memoizer {
/** @type {Array<{ id: Identifier, expression: Expression }>} */
/** @type {Array<{ id: Identifier, expression: Expression, metadata: ExpressionMetadata }>} */
#sync = [];
/** @type {Array<{ id: Identifier, expression: Expression }>} */
/** @type {Array<{ id: Identifier, expression: Expression, metadata: ExpressionMetadata }>} */
#async = [];
/** @type {Set<Expression>} */
@ -43,7 +43,7 @@ export class Memoizer {
const id = b.id('#'); // filled in later
(metadata.has_await ? this.#async : this.#sync).push({ id, expression });
(metadata.has_await ? this.#async : this.#sync).push({ id, expression, metadata });
return id;
}
@ -84,7 +84,7 @@ export class Memoizer {
if (this.#async.length === 0) return;
// use `b.arrow` rather than `b.thunk` so that deferred async/template effects
// always read live bindings rather than a possibly stale snapshot.
return b.array(this.#async.map((memo) => b.arrow([], memo.expression, true)));
return b.array(this.#async.map((memo) => async_thunk(memo.expression, memo.metadata)));
}
sync_values() {

@ -77,6 +77,9 @@ export class ExpressionMetadata {
/** True if the expression contains `await` */
has_await = false;
/** True if an `await` restores the reaction context afterwards, so the thunk must end it */
has_pickled_await = false;
/** True if the expression includes a member expression */
has_member_expression = false;
@ -142,6 +145,7 @@ export class ExpressionMetadata {
this.has_state ||= source.has_state;
this.has_call ||= source.has_call;
this.has_await ||= source.has_await;
this.has_pickled_await ||= source.has_pickled_await;
this.has_member_expression ||= source.has_member_expression;
this.has_assignment ||= source.has_assignment;
this.#blockers = null; // so that blockers are recalculated

@ -63,7 +63,7 @@ export interface Analysis {
accessors: boolean;
/** A set of deriveds that contain `await` expressions */
async_deriveds: Set<CallExpression>;
async_deriveds: Map<CallExpression, ExpressionMetadata>;
/** Awaits needing context preservation */
pickled_awaits: Set<AwaitExpression>;
}

@ -103,6 +103,7 @@ export {
run,
save,
track_reactivity_loss,
unsave,
run_after_blockers,
wait
} from './reactivity/async.js';

@ -8,7 +8,6 @@ import {
set_component_context,
set_dev_stack
} from '../context.js';
import { Boundary } from '../dom/blocks/boundary.js';
import { invoke_error_boundary } from '../error-handling.js';
import {
active_effect,
@ -25,7 +24,6 @@ import {
set_reactivity_loss_tracker
} from './deriveds.js';
import { aborted } from './effects.js';
import { queue_micro_task } from '../dom/task.js';
/**
* @param {Blocker[]} blockers
@ -156,6 +154,9 @@ export function capture() {
};
}
/** `true` between a `save` thunk restoring a context and the end of that synchronous segment */
var restored = false;
/**
* Wraps an `await` expression in such a way that the effect context that was
* active before the expression evaluated can be reapplied afterwards
@ -166,15 +167,32 @@ export function capture() {
*/
export async function save(promise) {
var restore = capture();
// the context restored by an earlier `save` in this expression must not
// outlive the synchronous segment that is about to end at this `await`
unsave();
var value = await promise;
return () => {
restore();
queue_micro_task(unset_context);
restored = true;
return value;
};
}
/**
* Unset the context if a `save` thunk restored it in the current synchronous segment,
* so that a foreign microtask can never run inside a restored reaction context.
* Called at every suspension point, and at the end of async expression bodies
* `async () => (await $.save(a))().b` becomes `async () => { try { return (await $.save(a))().b; } finally { $.unsave(); } }`
* @template T
* @param {T} [value]
* @returns {T}
*/
export function unsave(value) {
if (restored) unset_context();
return /** @type {T} */ (value);
}
/**
* Reset `current_async_effect` after the `promise` resolves, so
* that we can emit `await_reactivity_loss` warnings
@ -183,6 +201,7 @@ export async function save(promise) {
* @returns {Promise<() => T>}
*/
export async function track_reactivity_loss(promise) {
unsave();
var previous_reactivity_loss_tracker = reactivity_loss_tracker;
// Ensure that unrelated reads after an async operation is kicked off don't cause false positives
queueMicrotask(() => {
@ -269,6 +288,7 @@ export async function* for_await_track_reactivity_loss(iterable) {
}
export function unset_context(deactivate_batch = true) {
restored = false;
set_active_effect(null);
set_active_reaction(null);
set_component_context(null);

@ -0,0 +1,20 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ assert, target, errors }) {
await new Promise((resolve) => setTimeout(resolve, 20));
await tick();
assert.deepEqual(
errors.filter((error) => error.includes('state_unsafe_mutation')),
[]
);
assert.htmlEqual(target.innerHTML, '<p>pending</p><p>1</p>');
}
});

@ -0,0 +1,14 @@
<script>
let foreign = $state(0);
const input = Promise.resolve({ pending: new Promise(() => {}) });
setTimeout(() => {
foreign += 1;
});
</script>
{#await (await input).pending}
<p>pending</p>
{/await}
<p>{foreign}</p>

@ -0,0 +1,22 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: false
},
async test({ assert, target, errors }) {
await tick();
await tick();
await tick();
assert.deepEqual(
errors.filter((error) => error.includes('state_unsafe_mutation')),
[]
);
assert.htmlEqual(target.innerHTML, '<p>4 1</p>');
}
});

@ -0,0 +1,20 @@
<script>
let foreign = $state(0);
const items = Promise.resolve([1, 2, 3]);
const one = Promise.resolve(1);
// lands the write while the derived is suspended on `await one`, after the
// context restored for `.length` — in production that await has no dev hook
items.then(() => {
queueMicrotask(() => {
queueMicrotask(() => {
foreign += 1;
});
});
});
const total = $derived((await items).length + (await one));
</script>
<p>{total} {foreign}</p>

@ -0,0 +1,21 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ assert, target, errors }) {
await tick();
await tick();
assert.deepEqual(
errors.filter((error) => error.includes('state_unsafe_mutation')),
[]
);
assert.htmlEqual(target.innerHTML, '<p>3 1</p>');
}
});

@ -0,0 +1,21 @@
<script>
let foreign = $state(0);
const items = Promise.resolve([1, 2, 3]);
// registered before the derived, so it runs first when `items` settles.
// Two nested microtasks land the write in the window between the compiled
// continuation restoring the derived's context and svelte's queued unset
items.then(() => {
queueMicrotask(() => {
queueMicrotask(() => {
foreign += 1;
});
});
});
// `.length` follows the await, so the compiler pickles it via `$.save`
const length = $derived((await items).length);
</script>
<p>{length} {foreign}</p>

@ -0,0 +1,20 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client'],
compileOptions: {
dev: true
},
async test({ assert, target, errors }) {
await tick();
await tick();
assert.deepEqual(
errors.filter((error) => error.includes('state_unsafe_mutation')),
[]
);
assert.htmlEqual(target.innerHTML, '<p>failed</p><p>1</p>');
}
});

@ -0,0 +1,26 @@
<script>
let foreign = $state(0);
const input = Promise.resolve({
get value() {
throw new Error('boom');
}
});
input.then(() => {
queueMicrotask(() => {
queueMicrotask(() => {
foreign += 1;
});
});
});
</script>
<svelte:boundary onerror={() => {}}>
{#snippet failed()}
<p>failed</p>
{/snippet}
<p>{(await input).value}</p>
</svelte:boundary>
<p>{foreign}</p>

@ -14,7 +14,19 @@ export default function Async_const($$anchor) {
let b;
var promises = $.run([
async () => a = (await $.save($.async_derived(async () => (await $.save(1))())))(),
async () => {
try {
return a = (await $.save($.async_derived(async () => {
try {
return (await $.save(1))();
} finally {
$.unsave();
}
})))();
} finally {
$.unsave();
}
},
() => b = $.derived(() => $.get(a) + 1)
]);

@ -90,52 +90,78 @@ export default function Async_if_chain($$anchor) {
var node_3 = $.sibling(node_1, 2);
$.async(node_3, [$$promises[0]], [async () => (await $.save(foo))() > 10], (node_3, $$condition) => {
var consequent_5 = ($$anchor) => {
var text_7 = $.text('foo');
$.append($$anchor, text_7);
};
var consequent_6 = ($$anchor) => {
var text_8 = $.text('bar');
$.append($$anchor, text_8);
};
var alternate_4 = ($$anchor) => {
var fragment_2 = $.comment();
var node_4 = $.first_child(fragment_2);
$.async(node_4, [$$promises[0]], [async () => (await $.save(foo))() > 5], (node_4, $$condition) => {
var consequent_7 = ($$anchor) => {
var text_9 = $.text('baz');
$.append($$anchor, text_9);
};
var alternate_3 = ($$anchor) => {
var text_10 = $.text('else');
$.append($$anchor, text_10);
};
$.if(
$.async(
node_3,
[$$promises[0]],
[
async () => {
try {
return (await $.save(foo))() > 10;
} finally {
$.unsave();
}
}
],
(node_3, $$condition) => {
var consequent_5 = ($$anchor) => {
var text_7 = $.text('foo');
$.append($$anchor, text_7);
};
var consequent_6 = ($$anchor) => {
var text_8 = $.text('bar');
$.append($$anchor, text_8);
};
var alternate_4 = ($$anchor) => {
var fragment_2 = $.comment();
var node_4 = $.first_child(fragment_2);
$.async(
node_4,
($$render) => {
if ($.get($$condition)) $$render(consequent_7); else $$render(alternate_3, -1);
},
true
[$$promises[0]],
[
async () => {
try {
return (await $.save(foo))() > 5;
} finally {
$.unsave();
}
}
],
(node_4, $$condition) => {
var consequent_7 = ($$anchor) => {
var text_9 = $.text('baz');
$.append($$anchor, text_9);
};
var alternate_3 = ($$anchor) => {
var text_10 = $.text('else');
$.append($$anchor, text_10);
};
$.if(
node_4,
($$render) => {
if ($.get($$condition)) $$render(consequent_7); else $$render(alternate_3, -1);
},
true
);
}
);
});
$.append($$anchor, fragment_2);
};
$.append($$anchor, fragment_2);
};
$.if(node_3, ($$render) => {
if ($.get($$condition)) $$render(consequent_5); else if (bar) $$render(consequent_6, 1); else $$render(alternate_4, -1);
});
});
$.if(node_3, ($$render) => {
if ($.get($$condition)) $$render(consequent_5); else if (bar) $$render(consequent_6, 1); else $$render(alternate_4, -1);
});
}
);
var node_5 = $.sibling(node_3, 2);

@ -34,8 +34,34 @@ export default function Async_in_derived($$anchor, $$props) {
let no2;
var promises = $.run([
async () => yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(),
async () => yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(),
async () => {
try {
return yes1 = (await $.save($.async_derived(async () => {
try {
return (await $.save(1))();
} finally {
$.unsave();
}
})))();
} finally {
$.unsave();
}
},
async () => {
try {
return yes2 = (await $.save($.async_derived(async () => {
try {
return foo((await $.save(1))());
} finally {
$.unsave();
}
})))();
} finally {
$.unsave();
}
},
() => no1 = $.derived(() => (async () => {
return await 1;
})()),

Loading…
Cancel
Save