diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 499a071270..80ff005ebc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -264,7 +264,8 @@ export function analyze_module(ast, options) { accessors: false, runes: true, immutable: true, - tracing: analysis.tracing + tracing: analysis.tracing, + async_deriveds: new Set() }; } @@ -451,7 +452,8 @@ export function analyze_component(root, source, options) { undefined_exports: new Map(), snippet_renderers: new Map(), snippets: new Set(), - is_async: false + is_async: false, + async_deriveds: new Set() }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 9f51cd61de..5465720a68 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -7,6 +7,7 @@ import { get_parent, unwrap_optional } from '../../../utils/ast.js'; import { is_pure, is_safe_identifier } from './shared/utils.js'; import { dev, locate_node, source } from '../../../state.js'; import * as b from '../../../utils/builders.js'; +import { create_expression_metadata } from '../../nodes.js'; /** * @param {CallExpression} node @@ -207,7 +208,19 @@ export function CallExpression(node, context) { } // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning - if (rune === '$inspect' || rune === '$derived') { + if (rune === '$derived') { + const expression = create_expression_metadata(); + + context.next({ + ...context.state, + function_depth: context.state.function_depth + 1, + expression + }); + + if (expression.is_async) { + context.state.analysis.async_deriveds.add(node); + } + } else if (rune === '$inspect') { context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); } else { context.next(); 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 afb90bbec7..b9a987015f 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 @@ -158,13 +158,28 @@ export function VariableDeclaration(node, context) { } if (rune === '$derived' || rune === '$derived.by') { + const is_async = context.state.analysis.async_deriveds.has( + /** @type {CallExpression} */ (init) + ); + if (declarator.id.type === 'Identifier') { - declarations.push( - b.declarator( - declarator.id, - b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)) - ) - ); + if (is_async) { + declarations.push( + b.declarator( + declarator.id, + b.await( + b.call('$.async_derived', rune === '$derived.by' ? value : b.thunk(value, true)) + ) + ) + ); + } else { + declarations.push( + b.declarator( + declarator.id, + b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)) + ) + ); + } } else { const bindings = extract_paths(declarator.id); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 0bd8c352f6..02172be5f5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -1,4 +1,4 @@ -/** @import { Identifier } from 'estree' */ +/** @import { CallExpression, Identifier } from 'estree' */ /** @import { ComponentContext, Context } from '../../types' */ import { is_state_source } from '../../utils.js'; import * as b from '../../../../../utils/builders.js'; @@ -17,6 +17,18 @@ export function get_value(node) { */ export function add_state_transformers(context) { for (const [name, binding] of context.state.scope.declarations) { + if ( + binding.kind === 'derived' && + context.state.analysis.async_deriveds.has(/** @type {CallExpression} */ (binding.initial)) + ) { + // async deriveds are a special case + context.state.transform[name] = { + read: b.call + }; + + continue; + } + if ( is_state_source(binding, context.state.analysis) || binding.kind === 'derived' || diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index fc60fe3e4e..ce308f6f17 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -1,5 +1,5 @@ import type { AST, Binding } from '#compiler'; -import type { Identifier, LabeledStatement, Node, Program } from 'estree'; +import type { CallExpression, Identifier, LabeledStatement, Node, Program } from 'estree'; import type { Scope, ScopeRoot } from './scope.js'; export interface Js { @@ -31,6 +31,9 @@ export interface Analysis { // TODO figure out if we can move this to ComponentAnalysis accessors: boolean; + + /** A set of deriveds that contain `await` expressions */ + async_deriveds: Set; } export interface ComponentAnalysis extends Analysis { diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 5d852b6a13..f77f39d997 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -97,7 +97,7 @@ export { template_with_script, text } from './dom/template.js'; -export { derived, derived_safe_equal } from './reactivity/deriveds.js'; +export { async_derived, derived, derived_safe_equal } from './reactivity/deriveds.js'; export { effect_tracking, effect_root, diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 7ec1ed30bd..9fdb7abe6b 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -18,14 +18,16 @@ import { update_reaction, increment_write_version, set_active_effect, - component_context + component_context, + get } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; -import { destroy_effect } from './effects.js'; -import { inspect_effects, set_inspect_effects } from './sources.js'; +import { destroy_effect, render_effect } from './effects.js'; +import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; +import { preserve_context } from '../dom/blocks/boundary.js'; /** * @template V @@ -75,6 +77,36 @@ export function derived(fn) { return signal; } +/** + * @template V + * @param {() => Promise} fn + * @returns {Promise<() => V>} + */ +/*#__NO_SIDE_EFFECTS__*/ +export async function async_derived(fn) { + if (!active_effect) { + throw new Error('TODO cannot create unowned async derived'); + } + + let promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); + let value = source(/** @type {V} */ (undefined)); + + render_effect(() => { + const current = (promise = fn()); + + promise.then((v) => { + if (promise === current) { + internal_set(value, v); + } + }); + + // TODO what happens when the promise rejects? + }); + + (await preserve_context(promise)).read(); + return () => get(value); +} + /** * @template V * @param {() => V} fn