diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 76c1e94277..c3e5f47efc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -69,6 +69,7 @@ import { TransitionDirective } from './visitors/TransitionDirective.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UseDirective } from './visitors/UseDirective.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import is_reference from 'is-reference'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; @@ -158,6 +159,7 @@ const visitors = { LetDirective, MemberExpression, NewExpression, + AwaitExpression, OnDirective, RegularElement, RenderTag, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js new file mode 100644 index 0000000000..8fda993559 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -0,0 +1,35 @@ +/** @import { AwaitExpression } from 'estree' */ +/** @import { Context } from '../types' */ +import { extract_identifiers } from '../../../utils/ast.js'; +import * as w from '../../../warnings.js'; + +/** + * @param {AwaitExpression} node + * @param {Context} context + */ +export function AwaitExpression(node, context) { + const declarator = context.path.at(-1); + const declaration = context.path.at(-2); + const program = context.path.at(-3); + + if (context.state.ast_type === 'instance') { + if ( + declarator?.type !== 'VariableDeclarator' || + context.state.function_depth !== 1 || + declaration?.type !== 'VariableDeclaration' || + program?.type !== 'Program' + ) { + throw new Error('TODO: invalid usage of AwaitExpression in component'); + } + for (const declarator of declaration.declarations) { + for (const id of extract_identifiers(declarator.id)) { + const binding = context.state.scope.get(id.name); + if (binding !== null) { + binding.kind = 'derived'; + } + } + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 582c32b534..6f38e03077 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -55,6 +55,7 @@ import { SvelteWindow } from './visitors/SvelteWindow.js'; import { TitleElement } from './visitors/TitleElement.js'; import { TransitionDirective } from './visitors/TransitionDirective.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { UseDirective } from './visitors/UseDirective.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; @@ -131,7 +132,8 @@ const visitors = { TransitionDirective, UpdateExpression, UseDirective, - VariableDeclaration + VariableDeclaration, + AwaitExpression }; /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index c59a5544df..092631f4da 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,10 +1,11 @@ -/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ +/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement, VariableDeclaration } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '../../../utils/builders.js'; import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; +import { get_rune } from '../../scope.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, @@ -312,3 +313,86 @@ export function create_derived_block_argument(node, context) { export function create_derived(state, arg) { return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); } + +/** + * @param {(import("estree").ModuleDeclaration | Statement | import("estree").Directive)[]} statements + * @returns {(import("estree").ModuleDeclaration | Statement | import("estree").Directive)[]} + * @param {ComponentContext} context + */ +export function wrap_unsafe_async_statements(statements, context) { + /** @type {(import("estree").ModuleDeclaration | Statement | import("estree").Directive)[]} */ + const new_statements = []; + /** @type {Statement[] | null} */ + let async_statements = null; + + const apply_async_statements = () => { + new_statements.push( + b.stmt( + b.call( + '$.script_effect', + b.thunk(b.block(/** @type {Statement[]} */ (async_statements))) + ) + ) + ); + async_statements = null; + }; + + const push_async_statement = (/** @type {Statement} */ statement) => { + if (async_statements === null) { + async_statements = []; + } + async_statements.push(statement); + }; + + for (const statement of statements) { + const visited = /** @type {Statement} */ (context.visit(statement)); + + if (statement.type === 'ExpressionStatement') { + const rune = get_rune(statement.expression, context.state.scope); + + if (rune !== '$effect' && rune !== '$effect.pre') { + push_async_statement(visited); + continue; + } + } else if (statement.type === 'DebuggerStatement') { + push_async_statement(visited); + continue; + } else if (statement.type === 'VariableDeclaration') { + if (statement.declarations.length > 1) { + throw new Error('TODO'); + } + const declarator = statement.declarations[0]; + const rune = get_rune(declarator.init, context.state.scope); + + if (declarator.init?.type === 'AwaitExpression') { + // TODO: do we need to do anything here? + } else if (rune === null || rune === '$state' || rune === '$state.raw') { + const visited_declarator = /** @type {VariableDeclaration} */ (visited).declarations[0]; + new_statements.push(b.let(visited_declarator.id)); + if (rune === '$state') { + debugger; + } + if (visited_declarator.init != null) { + push_async_statement( + b.stmt(b.assignment('=', visited_declarator.id, visited_declarator.init)) + ); + } + continue; + } else if (rune !== '$props' && rune !== '$derived' && rune !== '$derived.by') { + debugger; + } + } + + if (async_statements !== null) { + apply_async_statements(); + } + + new_statements.push(visited); + } + + if (async_statements !== null) { + apply_async_statements(); + } + + return new_statements; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js new file mode 100644 index 0000000000..99096fa1a3 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -0,0 +1,17 @@ +/** @import { AwaitExpression, Expression } from 'estree' */ +/** @import { ComponentContext } from '../types' */ + +import * as b from '../../../../utils/builders.js'; + +/** + * @param {AwaitExpression} node + * @param {ComponentContext} context + */ +export function AwaitExpression(node, context) { + // Inside component + if (context.state.analysis.instance) { + return b.call('$.await_derived', b.thunk(/** @type {Expression} */ (context.visit(node.argument)))); + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 29403ca6ed..fd184820b0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,14 +1,14 @@ /** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */ /** @import { ComponentContext } from '../types' */ -import { build_getter, is_prop_source } from '../utils.js'; +import { build_getter, is_prop_source, wrap_unsafe_async_statements } from '../utils.js'; import * as b from '../../../../utils/builders.js'; import { add_state_transformers } from './shared/declarations.js'; /** - * @param {Program} _ + * @param {Program} node * @param {ComponentContext} context */ -export function Program(_, context) { +export function Program(node, context) { if (!context.state.analysis.runes) { context.state.transform['$$props'] = { read: (node) => ({ ...node, name: '$$sanitized_props' }) @@ -137,5 +137,12 @@ export function Program(_, context) { add_state_transformers(context); + if (context.state.analysis.instance) { + return { + ...node, + body: wrap_unsafe_async_statements(node.body, context) + } + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 7674fd1eb2..32d6e12f5f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -83,7 +83,11 @@ export function process_children(nodes, initial, is_element, { visit, state }) { } else if (has_state && !within_bound_contenteditable) { state.update.push(update); } else { - state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); + state.init.push( + b.stmt( + b.call('$.script_effect', b.thunk(b.assignment('=', b.member(id, 'nodeValue'), value))) + ) + ); } } diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 2fdc8de0ba..587d766233 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -191,5 +191,3 @@ export { } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; - -export { create_suspense } from './internal/client/dom/blocks/boundary.js'; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index a4840ce4eb..5f87365f5e 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -5,23 +5,26 @@ export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; export const ROOT_EFFECT = 1 << 6; export const BOUNDARY_EFFECT = 1 << 7; -export const UNOWNED = 1 << 8; -export const DISCONNECTED = 1 << 9; -export const CLEAN = 1 << 10; -export const DIRTY = 1 << 11; -export const MAYBE_DIRTY = 1 << 12; -export const INERT = 1 << 13; -export const DESTROYED = 1 << 14; -export const EFFECT_RAN = 1 << 15; +export const TEMPLATE_EFFECT = 1 << 8; +export const AWAIT_EFFECT = 1 << 9; +export const UNOWNED = 1 << 10; +export const DISCONNECTED = 1 << 11; +export const CLEAN = 1 << 12; +export const DIRTY = 1 << 13; +export const MAYBE_DIRTY = 1 << 14; +export const INERT = 1 << 15; +export const DESTROYED = 1 << 16; +export const EFFECT_RAN = 1 << 17; /** 'Transparent' effects do not create a transition boundary */ -export const EFFECT_TRANSPARENT = 1 << 16; +export const EFFECT_TRANSPARENT = 1 << 18; /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ -export const LEGACY_DERIVED_PROP = 1 << 17; -export const INSPECT_EFFECT = 1 << 18; -export const HEAD_EFFECT = 1 << 19; -export const EFFECT_HAS_DERIVED = 1 << 20; +export const LEGACY_DERIVED_PROP = 1 << 19; +export const INSPECT_EFFECT = 1 << 20; +export const HEAD_EFFECT = 1 << 21; +export const EFFECT_HAS_DERIVED = 1 << 22; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); export const LEGACY_PROPS = Symbol('legacy props'); +export const PENDING = Symbol(); export const LOADING_ATTR_SYMBOL = Symbol(''); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 1e172ef73b..d9df49160e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,8 @@ -/** @import { Effect, TemplateNode, } from '#client' */ +/** @import { Effect, Source, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; +import { UNINITIALIZED } from '../../../../constants.js'; +import { AWAIT_EFFECT, BOUNDARY_EFFECT, EFFECT_TRANSPARENT, PENDING } from '../../constants.js'; +import { derived } from '../../reactivity/deriveds.js'; import { block, branch, @@ -8,6 +10,7 @@ import { pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { set, source } from '../../reactivity/sources.js'; import { active_effect, active_reaction, @@ -16,7 +19,10 @@ import { set_active_effect, set_active_reaction, set_component_context, - reset_is_throwing_error + reset_is_throwing_error, + get, + set_is_within_await, + untrack } from '../../runtime.js'; import { hydrate_next, @@ -27,7 +33,7 @@ import { set_hydrate_node } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; -import { queue_boundary_micro_task } from '../task.js'; +import { flush_boundary_micro_tasks, queue_boundary_micro_task } from '../task.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -241,3 +247,52 @@ export function trigger_async_boundary(effect, trigger) { current = current.parent; } } + +/** + * @template V + * @param {() => Promise} fn + */ +export function await_derived(fn) { + var current = active_effect; + /** @type {Source} */ + var value = source(PENDING); + /** @type {Promise | typeof UNINITIALIZED} */ + var previous_promise = UNINITIALIZED; + var derived_promise = derived(fn); + + block(() => { + var promise = get(derived_promise) + try { + get(value) + } catch (e) { + if (e !== PENDING) { + throw e; + } + } + + var should_suspend = previous_promise !== promise; + previous_promise = promise; + + if (should_suspend) { + set_is_within_await(true); + untrack(() => { + set(value, PENDING); + }); + trigger_async_boundary(current, ASYNC_INCREMENT); + + // If we're updating, then we need to flush the boundary microtasks + if (current?.parent?.first !== null) { + flush_boundary_micro_tasks(); + } + + promise.then((v) => { + set(value, v); + trigger_async_boundary(current, ASYNC_DECREMENT); + }); + } + + return value.v; + }, AWAIT_EFFECT); + + return value; +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 2bf58c51f7..30b617ed97 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -107,7 +107,8 @@ export { template_effect, effect, user_effect, - user_pre_effect + user_pre_effect, + script_effect } from './reactivity/effects.js'; export { mutable_state, mutate, set, state } from './reactivity/sources.js'; export { @@ -129,7 +130,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary } from './dom/blocks/boundary.js'; +export { boundary, await_derived } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index abcb558c7f..bf1455e4cb 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -16,7 +16,8 @@ import { set_is_flushing_effect, set_signal_status, untrack, - skip_reaction + skip_reaction, + is_within_await } from '../runtime.js'; import { DIRTY, @@ -36,7 +37,8 @@ import { HEAD_EFFECT, MAYBE_DIRTY, EFFECT_HAS_DERIVED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + TEMPLATE_EFFECT } from '../constants.js'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -118,27 +120,35 @@ function create_effect(type, fn, sync, push = true) { effect.component_function = dev_current_component_function; } - if (sync) { - var previously_flushing_effect = is_flushing_effect; + var should_run_effect = + !is_within_await || + ((type & BRANCH_EFFECT) !== 0 || ((type & BLOCK_EFFECT) !== 0 && (type & TEMPLATE_EFFECT) === 0)); - try { - set_is_flushing_effect(true); - update_effect(effect); - effect.f |= EFFECT_RAN; - } catch (e) { - destroy_effect(effect); - throw e; - } finally { - set_is_flushing_effect(previously_flushing_effect); + if (should_run_effect) { + if (sync) { + var previously_flushing_effect = is_flushing_effect; + + try { + set_is_flushing_effect(true); + update_effect(effect); + effect.f |= EFFECT_RAN; + } catch (e) { + destroy_effect(effect); + throw e; + } finally { + set_is_flushing_effect(previously_flushing_effect); + } + } else if (fn !== null) { + schedule_effect(effect); } - } else if (fn !== null) { - schedule_effect(effect); } // if an effect has no dependencies, no DOM and no teardown function, // don't bother adding it to the effect tree var inert = + should_run_effect && sync && + !is_within_await && effect.deps === null && effect.first === null && effect.nodes_start === null && @@ -352,7 +362,7 @@ export function template_effect(fn) { value: '{expression}' }); } - return block(fn); + return block(fn, TEMPLATE_EFFECT); } /** @@ -371,6 +381,16 @@ export function branch(fn, push = true) { return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push); } +/** + * @param {(() => void)} fn + */ +export function script_effect(fn) { + if (active_effect === null || !is_within_await) { + fn(); + } + return create_effect(RENDER_EFFECT, () => untrack(fn), true); +} + /** * @param {Effect} effect */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index aba037c4a3..1fe582d97b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,7 +25,9 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + AWAIT_EFFECT, + PENDING } from './constants.js'; import { flush_idle_tasks, @@ -58,6 +60,7 @@ let last_scheduled_effect = null; export let is_flushing_effect = false; export let is_destroying_effect = false; +export let is_within_await = false; /** @param {boolean} value */ export function set_is_flushing_effect(value) { @@ -69,6 +72,11 @@ export function set_is_destroying_effect(value) { is_destroying_effect = value; } +/** @param {boolean} value */ +export function set_is_within_await(value) { + is_within_await = value; +} + // Handle effect queues /** @type {Effect[]} */ @@ -572,6 +580,7 @@ export function update_effect(effect) { var previous_effect = active_effect; var previous_component_context = component_context; + var previous_is_within_await = is_within_await; active_effect = effect; @@ -614,9 +623,16 @@ export function update_effect(effect) { dev_effect_stack.push(effect); } } catch (error) { + if (error === PENDING) { + set_signal_status(effect, DIRTY); + return; + } handle_error(error, effect, previous_effect, previous_component_context || effect.ctx); } finally { active_effect = previous_effect; + if ((flags & AWAIT_EFFECT) === 0) { + is_within_await = previous_is_within_await; + } if (DEV) { dev_current_component_function = previous_component_fn; @@ -810,7 +826,7 @@ function process_effects(effect, collected_effects) { var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; var sibling = current_effect.next; - if (!is_skippable_branch && (flags & INERT) === 0) { + if (!is_skippable_branch && ((flags & (INERT)) === 0 || (flags & (AWAIT_EFFECT)) !== 0)) { if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { current_effect.f ^= CLEAN; @@ -820,7 +836,11 @@ function process_effects(effect, collected_effects) { update_effect(current_effect); } } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); + if (error === PENDING) { + set_signal_status(current_effect, DIRTY); + } else { + handle_error(error, current_effect, null, current_effect.ctx); + } } } @@ -1013,7 +1033,13 @@ export function get(signal) { } } - return signal.v; + value = signal.v; + + if (is_within_await && value === PENDING) { + throw PENDING; + } + + return value; } /**