From 74c483c69de5b59995e0c8454a108f548a188c75 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 14 Jan 2025 23:46:51 +0000 Subject: [PATCH 001/211] wip --- .../2-analyze/visitors/SvelteBoundary.js | 2 +- .../client/visitors/SvelteBoundary.js | 5 +- packages/svelte/src/index-client.js | 2 + .../internal/client/dom/blocks/boundary.js | 154 +++++++++++++++--- packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/effects.js | 32 ++-- .../svelte/src/internal/client/runtime.js | 2 +- 7 files changed, 160 insertions(+), 39 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index d50cb80cb83e..35af96ba122e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -const valid = ['onerror', 'failed']; +const valid = ['onerror', 'failed', 'pending']; /** * @param {AST.SvelteBoundary} node diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 325485d4c003..48402ccc7517 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -39,7 +39,10 @@ export function SvelteBoundary(node, context) { // Capture the `failed` implicit snippet prop for (const child of node.fragment.nodes) { - if (child.type === 'SnippetBlock' && child.expression.name === 'failed') { + if ( + child.type === 'SnippetBlock' && + (child.expression.name === 'failed' || child.expression.name === 'pending') + ) { // we need to delay the visit of the snippets in case they access a ConstTag that is declared // after the snippets so that the visitor for the const tag can be updated snippets_visits.push(() => { diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 587d76623331..1b15ec9fce59 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -191,3 +191,5 @@ export { } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; + +export { suspend, unsuspend } from './internal/client/dom/blocks/boundary.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 7f4f000dceae..ba983c4c4bfd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,7 +1,13 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; -import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; +import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT, INERT } from '../../constants.js'; +import { + block, + branch, + destroy_effect, + pause_effect, + resume_effect +} from '../../reactivity/effects.js'; import { active_effect, active_reaction, @@ -20,8 +26,12 @@ import { remove_nodes, set_hydrate_node } from '../hydration.js'; +import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; +const SUSPEND_INCREMENT = Symbol(); +const SUSPEND_DECREMENT = Symbol(); + /** * @param {Effect} boundary * @param {() => void} fn @@ -49,6 +59,7 @@ function with_boundary(boundary, fn) { * @param {{ * onerror?: (error: unknown, reset: () => void) => void, * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void + * pending?: (anchor: Node) => void * }} props * @param {((anchor: Node) => void)} boundary_fn * @returns {void} @@ -58,14 +69,95 @@ export function boundary(node, props, boundary_fn) { /** @type {Effect} */ var boundary_effect; + /** @type {Effect | null} */ + var suspended_effect = null; + /** @type {DocumentFragment | null} */ + var suspended_fragment = null; + var suspend_count = 0; block(() => { var boundary = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; var is_creating_fallback = false; - // We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {unknown}} */ error) => { + const render_snippet = (/** @type { () => void } */ snippet_fn) => { + // Render the snippet in a microtask + queue_micro_task(() => { + with_boundary(boundary, () => { + is_creating_fallback = true; + + try { + boundary_effect = branch(() => { + snippet_fn(); + }); + } catch (error) { + handle_error(error, boundary, null, boundary.ctx); + } + + reset_is_throwing_error(); + is_creating_fallback = false; + }); + }); + }; + + // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field + boundary.fn = (/** @type {unknown} */ input) => { + let pending = props.pending; + + if (input === SUSPEND_INCREMENT) { + if (!pending) { + return false; + } + suspend_count++; + + if (suspended_effect === null) { + var effect = boundary_effect; + suspended_effect = boundary_effect; + + pause_effect(suspended_effect, () => { + /** @type {TemplateNode | null} */ + var node = effect.nodes_start; + var end = effect.nodes_end; + suspended_fragment = document.createDocumentFragment(); + + while (node !== null) { + /** @type {TemplateNode | null} */ + var sibling = + node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + node.remove(); + suspended_fragment.append(node); + node = sibling; + } + }, false); + + render_snippet(() => { + pending(anchor); + }); + } + return true; + } + + if (input === SUSPEND_DECREMENT) { + if (!pending) { + return false; + } + suspend_count--; + + if (suspend_count === 0 && suspended_effect !== null) { + if (boundary_effect) { + destroy_effect(boundary_effect); + } + boundary_effect = suspended_effect; + suspended_effect = null; + anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); + resume_effect(boundary_effect); + } + + return true; + } + + var error = input; var onerror = props.onerror; let failed = props.failed; @@ -96,26 +188,12 @@ export function boundary(node, props, boundary_fn) { } if (failed) { - // Render the `failed` snippet in a microtask - queue_micro_task(() => { - with_boundary(boundary, () => { - is_creating_fallback = true; - - try { - boundary_effect = branch(() => { - failed( - anchor, - () => error, - () => reset - ); - }); - } catch (error) { - handle_error(error, boundary, null, boundary.ctx); - } - - reset_is_throwing_error(); - is_creating_fallback = false; - }); + render_snippet(() => { + failed( + anchor, + () => error, + () => reset + ); }); } }; @@ -132,3 +210,31 @@ export function boundary(node, props, boundary_fn) { anchor = hydrate_node; } } + +export function suspend() { + var current = active_effect; + + while (current !== null) { + if ((current.f & BOUNDARY_EFFECT) !== 0) { + // @ts-ignore + if (current.fn(SUSPEND_INCREMENT)) { + return; + } + } + current = current.parent; + } +} + +export function unsuspend() { + var current = active_effect; + + while (current !== null) { + if ((current.f & BOUNDARY_EFFECT) !== 0) { + // @ts-ignore + if (current.fn(SUSPEND_DECREMENT)) { + return; + } + } + current = current.parent; + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 2bf58c51f75d..20ded180b07c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary } from './dom/blocks/boundary.js'; +export { boundary, suspend } 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 149cbd2d38ba..abcb558c7f83 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -528,15 +528,20 @@ export function unlink_effect(effect) { * A paused effect does not update, and the DOM subtree becomes inert. * @param {Effect} effect * @param {() => void} [callback] + * @param {boolean} [destroy] */ -export function pause_effect(effect, callback) { +export function pause_effect(effect, callback, destroy = true) { /** @type {TransitionManager[]} */ var transitions = []; - pause_children(effect, transitions, true); + pause_children(effect, transitions, true, destroy); run_out_transitions(transitions, () => { - destroy_effect(effect); + if (destroy) { + destroy_effect(effect); + } else { + execute_effect_teardown(effect); + } if (callback) callback(); }); } @@ -561,8 +566,9 @@ export function run_out_transitions(transitions, fn) { * @param {Effect} effect * @param {TransitionManager[]} transitions * @param {boolean} local + * @param {boolean} [destroy] */ -export function pause_children(effect, transitions, local) { +export function pause_children(effect, transitions, local, destroy = true) { if ((effect.f & INERT) !== 0) return; effect.f ^= INERT; @@ -582,7 +588,7 @@ export function pause_children(effect, transitions, local) { // TODO we don't need to call pause_children recursively with a linked list in place // it's slightly more involved though as we have to account for `transparent` changing // through the tree. - pause_children(child, transitions, transparent ? local : false); + pause_children(child, transitions, transparent ? local : false, destroy); child = sibling; } } @@ -602,17 +608,21 @@ export function resume_effect(effect) { */ function resume_children(effect, local) { if ((effect.f & INERT) === 0) return; + effect.f ^= INERT; + + // Ensure the effect is marked as clean again so that any dirty child + // effects can schedule themselves for execution + if ((effect.f & CLEAN) === 0) { + effect.f ^= CLEAN; + } // If a dependency of this effect changed while it was paused, - // apply the change now + // schedule the effect to update if (check_dirtiness(effect)) { - update_effect(effect); + set_signal_status(effect, DIRTY); + schedule_effect(effect); } - // Ensure we toggle the flag after possibly updating the effect so that - // each block logic can correctly operate on inert items - effect.f ^= INERT; - var child = effect.first; while (child !== null) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index eca5ee94f907..55a8ccf32dc2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -776,7 +776,7 @@ export function schedule_effect(signal) { var flags = effect.f; if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - if ((flags & CLEAN) === 0) return; + if ((flags & CLEAN) === 0) return effect.f ^= CLEAN; } } From e6cd4265ebe715a971df52d87f110bc8c184914e Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 15:21:08 +0000 Subject: [PATCH 002/211] wip --- packages/svelte/src/index-client.js | 2 +- .../internal/client/dom/blocks/boundary.js | 80 +++++++++++-------- packages/svelte/src/internal/client/index.js | 2 +- 3 files changed, 47 insertions(+), 37 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 1b15ec9fce59..2fdc8de0ba86 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -192,4 +192,4 @@ export { export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; -export { suspend, unsuspend } from './internal/client/dom/blocks/boundary.js'; +export { create_suspense } from './internal/client/dom/blocks/boundary.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ba983c4c4bfd..e2ed644699e8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -111,28 +111,34 @@ export function boundary(node, props, boundary_fn) { suspend_count++; if (suspended_effect === null) { - var effect = boundary_effect; - suspended_effect = boundary_effect; - - pause_effect(suspended_effect, () => { - /** @type {TemplateNode | null} */ - var node = effect.nodes_start; - var end = effect.nodes_end; - suspended_fragment = document.createDocumentFragment(); - - while (node !== null) { - /** @type {TemplateNode | null} */ - var sibling = - node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - - node.remove(); - suspended_fragment.append(node); - node = sibling; - } - }, false); - - render_snippet(() => { - pending(anchor); + queue_micro_task(() => { + var effect = boundary_effect; + suspended_effect = boundary_effect; + + pause_effect( + suspended_effect, + () => { + /** @type {TemplateNode | null} */ + var node = effect.nodes_start; + var end = effect.nodes_end; + suspended_fragment = document.createDocumentFragment(); + + while (node !== null) { + /** @type {TemplateNode | null} */ + var sibling = + node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + node.remove(); + suspended_fragment.append(node); + node = sibling; + } + }, + false + ); + + render_snippet(() => { + pending(anchor); + }); }); } return true; @@ -211,13 +217,17 @@ export function boundary(node, props, boundary_fn) { } } -export function suspend() { - var current = active_effect; +/** + * @param {Effect | null} effect + * @param {typeof SUSPEND_INCREMENT | typeof SUSPEND_DECREMENT} trigger + */ +function trigger_suspense(effect, trigger) { + var current = effect; while (current !== null) { if ((current.f & BOUNDARY_EFFECT) !== 0) { // @ts-ignore - if (current.fn(SUSPEND_INCREMENT)) { + if (current.fn(trigger)) { return; } } @@ -225,16 +235,16 @@ export function suspend() { } } -export function unsuspend() { +export function create_suspense() { var current = active_effect; - while (current !== null) { - if ((current.f & BOUNDARY_EFFECT) !== 0) { - // @ts-ignore - if (current.fn(SUSPEND_DECREMENT)) { - return; - } - } - current = current.parent; - } + const suspend = () => { + trigger_suspense(current, SUSPEND_INCREMENT); + }; + + const unsuspend = () => { + trigger_suspense(current, SUSPEND_DECREMENT); + }; + + return [suspend, unsuspend]; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 20ded180b07c..2bf58c51f75d 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, suspend } from './dom/blocks/boundary.js'; +export { boundary } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, From ea139370de0ed0d04a05f9d87ea18e07cc97b723 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 12:13:46 -0500 Subject: [PATCH 003/211] WIP --- .../src/compiler/phases/2-analyze/index.js | 2 ++ .../2-analyze/visitors/AwaitExpression.js | 14 ++++++++++ .../3-transform/client/transform-client.js | 2 ++ .../client/visitors/AwaitExpression.js | 16 +++++++++++ .../client/visitors/shared/fragment.js | 4 +-- .../client/visitors/shared/utils.js | 13 +++++---- packages/svelte/src/compiler/phases/nodes.js | 3 ++- packages/svelte/src/compiler/types/index.d.ts | 2 ++ packages/svelte/src/index-client.js | 2 -- .../internal/client/dom/blocks/boundary.js | 27 +++++++++++++++++++ packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/types/index.d.ts | 1 + 12 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 76c1e94277be..7557b62a8e78 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -20,6 +20,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { BindDirective } from './visitors/BindDirective.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; @@ -133,6 +134,7 @@ const visitors = { AssignmentExpression, Attribute, AwaitBlock, + AwaitExpression, BindDirective, CallExpression, ClassBody, 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 000000000000..633a496e0545 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -0,0 +1,14 @@ +/** @import { AwaitExpression } from 'estree' */ +/** @import { Context } from '../types' */ + +/** + * @param {AwaitExpression} node + * @param {Context} context + */ +export function AwaitExpression(node, context) { + if (context.state.expression) { + context.state.expression.is_async = true; + } + + 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 582c32b534ec..822dfe6e5b44 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 @@ -12,6 +12,7 @@ import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { Attribute } from './visitors/Attribute.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { BinaryExpression } from './visitors/BinaryExpression.js'; import { BindDirective } from './visitors/BindDirective.js'; import { BlockStatement } from './visitors/BlockStatement.js'; @@ -87,6 +88,7 @@ const visitors = { AssignmentExpression, Attribute, AwaitBlock, + AwaitExpression, BinaryExpression, BindDirective, BlockStatement, 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 000000000000..8d819b7ed241 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -0,0 +1,16 @@ +/** @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) { + return b.await( + b.call( + '$.preserve_context', + node.argument && /** @type {Expression} */ (context.visit(node.argument)) + ) + ); +} 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 7674fd1eb234..f74fbfcf7669 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 @@ -69,7 +69,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { state.template.push(' '); - const { has_state, has_call, value } = build_template_chunk(sequence, visit, state); + const { has_state, has_call, is_async, value } = build_template_chunk(sequence, visit, state); // if this is a standalone `{expression}`, make sure we handle the case where // no text node was created because the expression was empty during SSR @@ -79,7 +79,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_call && !within_bound_contenteditable) { - state.init.push(build_update(update)); + state.init.push(build_update(update, is_async)); } else if (has_state && !within_bound_contenteditable) { state.update.push(update); } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 1854baa1e964..f5b1abce395b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -14,7 +14,7 @@ import { locator } from '../../../../../state.js'; * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit * @param {ComponentClientTransformState} state - * @returns {{ value: Expression, has_state: boolean, has_call: boolean }} + * @returns {{ value: Expression, has_state: boolean, has_call: boolean, is_async: boolean }} */ export function build_template_chunk(values, visit, state) { /** @type {Expression[]} */ @@ -25,6 +25,7 @@ export function build_template_chunk(values, visit, state) { let has_call = false; let has_state = false; + let is_async = false; let contains_multiple_call_expression = false; for (const node of values) { @@ -34,6 +35,7 @@ export function build_template_chunk(values, visit, state) { contains_multiple_call_expression ||= has_call && metadata.has_call; has_call ||= metadata.has_call; has_state ||= metadata.has_state; + is_async ||= metadata.is_async; } } @@ -68,7 +70,7 @@ export function build_template_chunk(values, visit, state) { } else if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - return { value: visit(node.expression, state), has_state, has_call }; + return { value: visit(node.expression, state), has_state, has_call, is_async }; } else { expressions.push(b.logical('??', visit(node.expression, state), b.literal(''))); } @@ -84,17 +86,18 @@ export function build_template_chunk(values, visit, state) { const value = b.template(quasis, expressions); - return { value, has_state, has_call }; + return { value, has_state, has_call, is_async }; } /** * @param {Statement} statement + * @param {boolean} is_async */ -export function build_update(statement) { +export function build_update(statement, is_async) { const body = statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]); - return b.stmt(b.call('$.template_effect', b.thunk(body))); + return b.stmt(b.call('$.template_effect', b.thunk(body, is_async))); } /** diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 5066833feb8e..22306989c843 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -58,6 +58,7 @@ export function create_expression_metadata() { return { dependencies: new Set(), has_state: false, - has_call: false + has_call: false, + is_async: false }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index b80b717e426c..2f5ec226bf17 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -318,6 +318,8 @@ export interface ExpressionMetadata { has_state: boolean; /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ has_call: boolean; + /** True if the expression contains `await` */ + is_async: boolean; } export * from './template.js'; diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 2fdc8de0ba86..587d76623331 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/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index e2ed644699e8..9dcb54f05d6b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -248,3 +248,30 @@ export function create_suspense() { return [suspend, unsuspend]; } + +/** + * @template T + * @param {Promise} promise + * @returns {Promise} + */ +export async function preserve_context(promise) { + if (!active_effect) { + return promise; + } + + var previous_effect = active_effect; + var previous_reaction = active_reaction; + var previous_component_context = component_context; + + const [suspend, unsuspend] = create_suspense(); + + try { + suspend(); + return await promise; + } finally { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); + unsuspend(); + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 2bf58c51f75d..5d852b6a1374 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary } from './dom/blocks/boundary.js'; +export { boundary, preserve_context } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d00b2b01ed18..b65ab758ca0d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -419,6 +419,7 @@ declare module 'svelte' { render: () => string; setup?: (element: Element) => void | (() => void); }): Snippet; + export function create_suspense(): (() => void)[]; /** Anything except a function */ type NotFunction = T extends Function ? never : T; /** From 4ef2be3a5d2f79c19f7ce78c116a3ab53ebbcb48 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 12:31:34 -0500 Subject: [PATCH 004/211] fix --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index e2ed644699e8..840f4ed2fa83 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -108,9 +108,8 @@ export function boundary(node, props, boundary_fn) { if (!pending) { return false; } - suspend_count++; - if (suspended_effect === null) { + if (suspend_count++ === 0) { queue_micro_task(() => { var effect = boundary_effect; suspended_effect = boundary_effect; @@ -141,6 +140,7 @@ export function boundary(node, props, boundary_fn) { }); }); } + return true; } @@ -148,9 +148,8 @@ export function boundary(node, props, boundary_fn) { if (!pending) { return false; } - suspend_count--; - if (suspend_count === 0 && suspended_effect !== null) { + if (--suspend_count === 0 && suspended_effect !== null) { if (boundary_effect) { destroy_effect(boundary_effect); } From 278c49056d01c1f224779f23ea1c318e30e441da Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 13:46:39 -0500 Subject: [PATCH 005/211] fix --- .../internal/client/dom/blocks/boundary.js | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 840f4ed2fa83..f117811d7fb4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -111,6 +111,10 @@ export function boundary(node, props, boundary_fn) { if (suspend_count++ === 0) { queue_micro_task(() => { + if (suspended_effect) { + return; + } + var effect = boundary_effect; suspended_effect = boundary_effect; @@ -149,14 +153,20 @@ export function boundary(node, props, boundary_fn) { return false; } - if (--suspend_count === 0 && suspended_effect !== null) { - if (boundary_effect) { - destroy_effect(boundary_effect); - } - boundary_effect = suspended_effect; - suspended_effect = null; - anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); - resume_effect(boundary_effect); + if (--suspend_count === 0) { + queue_micro_task(() => { + if (!suspended_effect) { + return; + } + + if (boundary_effect) { + destroy_effect(boundary_effect); + } + boundary_effect = suspended_effect; + suspended_effect = null; + anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); + resume_effect(boundary_effect); + }); } return true; From 5bb5a8f767f6f598e5e4dbc7090ef405c39544f3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 13:47:04 -0500 Subject: [PATCH 006/211] WIP --- .../3-transform/client/transform-client.js | 4 +++- .../phases/3-transform/client/types.d.ts | 3 +++ .../3-transform/client/visitors/Fragment.js | 6 ++++-- .../client/visitors/RegularElement.js | 16 ++++++++++++--- .../client/visitors/SvelteElement.js | 7 ++++++- .../client/visitors/TitleElement.js | 7 ++++++- .../client/visitors/shared/element.js | 20 +++++++++++++++---- .../client/visitors/shared/fragment.js | 4 ++++ .../client/visitors/shared/utils.js | 7 ++++--- 9 files changed, 59 insertions(+), 15 deletions(-) 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 822dfe6e5b44..a1041947a497 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 @@ -159,7 +159,9 @@ export function client_component(analysis, options) { template_contains_script_tag: false }, namespace: options.namespace, - bound_contenteditable: false + bound_contenteditable: false, + init_is_async: false, + update_is_async: false }, events: new Set(), preserve_whitespace: options.preserveWhitespace, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 5c8476de3e3c..46a268d51406 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -75,6 +75,9 @@ export interface ComponentClientTransformState extends ClientTransformState { */ template_contains_script_tag: boolean; }; + // TODO it would be nice if these were colocated with the arrays they pertain to + init_is_async: boolean; + update_is_async: boolean; }; readonly preserve_whitespace: boolean; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 0e6ea29614ff..a3572b9b9ca3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -74,7 +74,9 @@ export function Fragment(node, context) { template_contains_script_tag: false }, namespace, - bound_contenteditable: context.state.metadata.bound_contenteditable + bound_contenteditable: context.state.metadata.bound_contenteditable, + init_is_async: false, + update_is_async: false } }; @@ -190,7 +192,7 @@ export function Fragment(node, context) { } if (state.update.length > 0) { - body.push(build_render_statement(state.update)); + body.push(build_render_statement(state.update, state.metadata.update_is_async)); } body.push(...state.after_update); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index ffd06dfd866f..5632d35b244d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -409,7 +409,9 @@ export function RegularElement(node, context) { b.block([ ...child_state.init, ...element_state.init, - child_state.update.length > 0 ? build_render_statement(child_state.update) : b.empty, + child_state.update.length > 0 + ? build_render_statement(child_state.update, child_state.metadata.update_is_async) + : b.empty, ...child_state.after_update, ...element_state.after_update ]) @@ -418,6 +420,9 @@ export function RegularElement(node, context) { context.state.init.push(...child_state.init, ...element_state.init); context.state.update.push(...child_state.update); context.state.after_update.push(...child_state.after_update, ...element_state.after_update); + + context.state.metadata.init_is_async ||= child_state.metadata.init_is_async; + context.state.metadata.update_is_async ||= child_state.metadata.update_is_async; } else { context.state.init.push(...element_state.init); context.state.after_update.push(...element_state.after_update); @@ -627,9 +632,10 @@ function build_element_attribute_update_assignment( if (attribute.metadata.expression.has_state) { if (has_call) { - state.init.push(build_update(update)); + state.init.push(build_update(update, attribute.metadata.expression.is_async)); } else { state.update.push(update); + state.metadata.update_is_async ||= attribute.metadata.expression.is_async; } return true; } else { @@ -662,12 +668,16 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co if (attribute.metadata.expression.has_state) { if (has_call) { - state.init.push(build_update(update)); + state.init.push(build_update(update, attribute.metadata.expression.is_async)); } else { state.update.push(update); + state.metadata.update_is_async ||= attribute.metadata.expression.is_async; } return true; } else { + if (attribute.metadata.expression.is_async) { + throw new Error('TODO top-level await'); + } state.init.push(update); return false; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index ba66fe29d691..c3d036072219 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -123,7 +123,12 @@ export function SvelteElement(node, context) { /** @type {Statement[]} */ const inner = inner_context.state.init; if (inner_context.state.update.length > 0) { - inner.push(build_render_statement(inner_context.state.update)); + inner.push( + build_render_statement( + inner_context.state.update, + inner_context.state.metadata.update_is_async + ) + ); } inner.push(...inner_context.state.after_update); inner.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 72cc57b068a0..05ae059ad282 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -8,7 +8,7 @@ import { build_template_chunk } from './shared/utils.js'; * @param {ComponentContext} context */ export function TitleElement(node, context) { - const { has_state, value } = build_template_chunk( + const { has_state, is_async, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), context.visit, context.state @@ -18,7 +18,12 @@ export function TitleElement(node, context) { if (has_state) { context.state.update.push(statement); + context.state.metadata.update_is_async ||= is_async; } else { + if (is_async) { + throw new Error('TODO top-level await'); + } + context.state.init.push(statement); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 1b0737e31e18..2e746cbf7875 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -29,6 +29,7 @@ export function build_set_attributes( state ) { let has_state = false; + let is_async = false; /** @type {ObjectExpression['properties']} */ const values = []; @@ -63,6 +64,8 @@ export function build_set_attributes( } values.push(b.spread(value)); } + + is_async ||= attribute.metadata.expression.is_async; } const call = b.call( @@ -80,6 +83,7 @@ export function build_set_attributes( context.state.init.push(b.let(attributes_id)); const update = b.stmt(b.assignment('=', attributes_id, call)); context.state.update.push(update); + context.state.metadata.update_is_async ||= is_async; return true; } @@ -104,7 +108,7 @@ export function build_style_directives( const state = context.state; for (const directive of style_directives) { - const { has_state, has_call } = directive.metadata.expression; + const { has_state, has_call, is_async } = directive.metadata.expression; let value = directive.value === true @@ -129,10 +133,14 @@ export function build_style_directives( ); if (!is_attributes_reactive && has_call) { - state.init.push(build_update(update)); + state.init.push(build_update(update, is_async)); } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); + state.metadata.update_is_async ||= is_async; } else { + if (is_async) { + throw new Error('TODO top-level await'); + } state.init.push(update); } } @@ -154,7 +162,7 @@ export function build_class_directives( ) { const state = context.state; for (const directive of class_directives) { - const { has_state, has_call } = directive.metadata.expression; + const { has_state, has_call, is_async } = directive.metadata.expression; let value = /** @type {Expression} */ (context.visit(directive.expression)); if (has_call) { @@ -167,10 +175,14 @@ export function build_class_directives( const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); if (!is_attributes_reactive && has_call) { - state.init.push(build_update(update)); + state.init.push(build_update(update, is_async)); } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); + state.metadata.update_is_async ||= is_async; } else { + if (is_async) { + throw new Error('TODO top-level await'); + } state.init.push(update); } } 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 f74fbfcf7669..5744cd51aa95 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 @@ -82,7 +82,11 @@ export function process_children(nodes, initial, is_element, { visit, state }) { state.init.push(build_update(update, is_async)); } else if (has_state && !within_bound_contenteditable) { state.update.push(update); + state.metadata.update_is_async ||= is_async; } else { + if (is_async) { + throw new Error('TODO top-level await'); + } state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index f5b1abce395b..5d1aa7bad001 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -102,11 +102,12 @@ export function build_update(statement, is_async) { /** * @param {Statement[]} update + * @param {boolean} is_async */ -export function build_render_statement(update) { +export function build_render_statement(update, is_async) { return update.length === 1 - ? build_update(update[0]) - : b.stmt(b.call('$.template_effect', b.thunk(b.block(update)))); + ? build_update(update[0], is_async) + : b.stmt(b.call('$.template_effect', b.thunk(b.block(update), is_async))); } /** From b788ec059a7c93baed29dc78959cce1b60e93859 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 14:10:56 -0500 Subject: [PATCH 007/211] fix --- .../phases/3-transform/client/visitors/shared/utils.js | 6 ++++-- packages/svelte/src/compiler/utils/builders.js | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 5d1aa7bad001..b8c0f438a108 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -61,12 +61,14 @@ export function build_template_chunk(values, visit, state) { '??', /** @type {Expression} */ (visit(node.expression, state)), b.literal('') - ) + ), + is_async ) ) ) ); - expressions.push(b.call('$.get', id)); + + expressions.push(is_async ? b.await(b.call('$.get', id)) : b.call('$.get', id)); } else if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index ecb595d74dbd..f79028a947e9 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -426,12 +426,15 @@ export function thunk(expression, async = false) { /** * Replace "(arg) => func(arg)" to "func" - * @param {ESTree.Expression} expression + * @param {ESTree.ArrowFunctionExpression} expression * @returns {ESTree.Expression} */ export function unthunk(expression) { + if (expression.async && expression.body.type === 'AwaitExpression') { + return unthunk(arrow(expression.params, expression.body.argument)); + } + if ( - expression.type === 'ArrowFunctionExpression' && expression.async === false && expression.body.type === 'CallExpression' && expression.body.callee.type === 'Identifier' && From 964004a1b0816294d5e864067ea1bf38ec4085a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 16:17:16 -0500 Subject: [PATCH 008/211] preserve context --- .../client/visitors/AwaitExpression.js | 13 +++-- .../svelte/src/internal/client/constants.js | 2 + .../internal/client/dom/blocks/boundary.js | 29 ++++++----- .../svelte/src/internal/client/runtime.js | 51 ++++++++++++++----- playgrounds/sandbox/vite.config.js | 2 +- 5 files changed, 65 insertions(+), 32 deletions(-) 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 index 8d819b7ed241..809a7b43f8ce 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -7,10 +7,15 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function AwaitExpression(node, context) { - return b.await( - b.call( - '$.preserve_context', - node.argument && /** @type {Expression} */ (context.visit(node.argument)) + return b.call( + b.member( + b.await( + b.call( + '$.preserve_context', + node.argument && /** @type {Expression} */ (context.visit(node.argument)) + ) + ), + 'read' ) ); } diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index a4840ce4ebd0..e7034a332dda 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -21,6 +21,8 @@ export const INSPECT_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19; export const EFFECT_HAS_DERIVED = 1 << 20; +export const REACTION_IS_UPDATING = 1 << 21; + export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); export const LEGACY_PROPS = Symbol('legacy props'); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 38f950387853..ccfdfc906711 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -261,26 +261,27 @@ export function create_suspense() { /** * @template T * @param {Promise} promise - * @returns {Promise} + * @returns {Promise<{ read: () => T }>} */ export async function preserve_context(promise) { - if (!active_effect) { - return promise; - } - var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; const [suspend, unsuspend] = create_suspense(); - try { - suspend(); - return await promise; - } finally { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_component_context); - unsuspend(); - } + suspend(); + + const value = await promise; + + return { + read() { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); + + unsuspend(); + return value; + } + }; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 55a8ccf32dc2..508cfd4da786 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,7 +25,8 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + REACTION_IS_UPDATING } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; @@ -435,6 +436,7 @@ export function update_reaction(reaction) { read_version++; try { + reaction.f |= REACTION_IS_UPDATING; var result = /** @type {Function} */ (0, reaction.fn)(); var deps = reaction.deps; @@ -488,6 +490,7 @@ export function update_reaction(reaction) { return result; } finally { + reaction.f ^= REACTION_IS_UPDATING; new_deps = previous_deps; skipped_deps = previous_skipped_deps; untracked_writes = previous_untracked_writes; @@ -776,7 +779,7 @@ export function schedule_effect(signal) { var flags = effect.f; if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - if ((flags & CLEAN) === 0) return + if ((flags & CLEAN) === 0) return; effect.f ^= CLEAN; } } @@ -938,18 +941,40 @@ export function get(signal) { if (derived_sources !== null && derived_sources.includes(signal)) { e.state_unsafe_local_read(); } + var deps = active_reaction.deps; - if (signal.rv < read_version) { - signal.rv = read_version; - // If the signal is accessing the same dependencies in the same - // order as it did last time, increment `skipped_deps` - // rather than updating `new_deps`, which creates GC cost - if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { - skipped_deps++; - } else if (new_deps === null) { - new_deps = [signal]; - } else { - new_deps.push(signal); + + if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) { + // we're in the effect init/update cycle + if (signal.rv < read_version) { + signal.rv = read_version; + + // If the signal is accessing the same dependencies in the same + // order as it did last time, increment `skipped_deps` + // rather than updating `new_deps`, which creates GC cost + if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { + skipped_deps++; + } else if (new_deps === null) { + new_deps = [signal]; + } else { + new_deps.push(signal); + } + } + } else { + // we're adding a dependency outside the init/update cycle + // (i.e. after an `await`) + // TODO we probably want to disable this for user effects, + // otherwise it's a breaking change, albeit a desirable one? + if (deps === null) { + deps = [signal]; + } else if (!deps.includes(signal)) { + deps.push(signal); + } + + if (signal.reactions === null) { + signal.reactions = [active_reaction]; + } else if (!signal.reactions.includes(active_reaction)) { + signal.reactions.push(active_reaction); } } } else if (is_derived && /** @type {Derived} */ (signal).deps === null) { diff --git a/playgrounds/sandbox/vite.config.js b/playgrounds/sandbox/vite.config.js index 51bfd0a2122e..c6c07ce7c65d 100644 --- a/playgrounds/sandbox/vite.config.js +++ b/playgrounds/sandbox/vite.config.js @@ -11,7 +11,7 @@ export default defineConfig({ inspect(), svelte({ compilerOptions: { - hmr: true + hmr: false } }) ], From 209f311f20a617b712ef44df91e57afb5c40219d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 16:19:25 -0500 Subject: [PATCH 009/211] reduce indirection --- .../internal/client/dom/blocks/boundary.js | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ccfdfc906711..1d551644a563 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -106,6 +106,7 @@ export function boundary(node, props, boundary_fn) { if (input === SUSPEND_INCREMENT) { if (!pending) { + // TODO in this case we need to find the parent boundary return false; } @@ -150,6 +151,7 @@ export function boundary(node, props, boundary_fn) { if (input === SUSPEND_DECREMENT) { if (!pending) { + // TODO in this case we need to find the parent boundary return false; } @@ -268,9 +270,21 @@ export async function preserve_context(promise) { var previous_reaction = active_reaction; var previous_component_context = component_context; - const [suspend, unsuspend] = create_suspense(); + let boundary = active_effect; + while (boundary !== null) { + if ((boundary.f & BOUNDARY_EFFECT) !== 0) { + break; + } + + boundary = boundary.parent; + } - suspend(); + if (boundary === null) { + throw new Error('cannot suspend outside a boundary'); + } + + // @ts-ignore + boundary.fn(SUSPEND_INCREMENT); const value = await promise; @@ -280,7 +294,9 @@ export async function preserve_context(promise) { set_active_reaction(previous_reaction); set_component_context(previous_component_context); - unsuspend(); + // @ts-ignore + boundary.fn(SUSPEND_DECREMENT); + return value; } }; From ad1c214b29336759be44a77fc22c641ce2218385 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 21:41:59 +0000 Subject: [PATCH 010/211] another fix --- .../internal/client/dom/blocks/boundary.js | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index f117811d7fb4..c0a5d0101a43 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT, INERT } from '../../constants.js'; +import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, @@ -81,22 +81,19 @@ export function boundary(node, props, boundary_fn) { var is_creating_fallback = false; const render_snippet = (/** @type { () => void } */ snippet_fn) => { - // Render the snippet in a microtask - queue_micro_task(() => { - with_boundary(boundary, () => { - is_creating_fallback = true; + with_boundary(boundary, () => { + is_creating_fallback = true; - try { - boundary_effect = branch(() => { - snippet_fn(); - }); - } catch (error) { - handle_error(error, boundary, null, boundary.ctx); - } + try { + boundary_effect = branch(() => { + snippet_fn(); + }); + } catch (error) { + handle_error(error, boundary, null, boundary.ctx); + } - reset_is_throwing_error(); - is_creating_fallback = false; - }); + reset_is_throwing_error(); + is_creating_fallback = false; }); }; @@ -203,12 +200,14 @@ export function boundary(node, props, boundary_fn) { } if (failed) { - render_snippet(() => { - failed( - anchor, - () => error, - () => reset - ); + queue_micro_task(() => { + render_snippet(() => { + failed( + anchor, + () => error, + () => reset + ); + }); }); } }; From 78bb187dde0699999f5a710a15e5ae3338d44264 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 21:44:25 +0000 Subject: [PATCH 011/211] another fix --- .../2-analyze/visitors/AwaitExpression.js | 35 +++++++++++++++++++ .../client/visitors/AwaitExpression.js | 17 +++++++++ 2 files changed, 52 insertions(+) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js 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 000000000000..8fda993559f0 --- /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/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js new file mode 100644 index 000000000000..99096fa1a357 --- /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(); +} From 7addfd83ba74e255744a89fefa7d2859c49d2140 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 21:45:00 +0000 Subject: [PATCH 012/211] Revert "another fix" This reverts commit 78bb187dde0699999f5a710a15e5ae3338d44264. --- .../2-analyze/visitors/AwaitExpression.js | 35 ------------------- .../client/visitors/AwaitExpression.js | 17 --------- 2 files changed, 52 deletions(-) delete mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js deleted file mode 100644 index 8fda993559f0..000000000000 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ /dev/null @@ -1,35 +0,0 @@ -/** @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/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js deleted file mode 100644 index 99096fa1a357..000000000000 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ /dev/null @@ -1,17 +0,0 @@ -/** @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(); -} From ff957d1db2f41b155e648bad8fe4132aa5eebfcb Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 21:45:32 +0000 Subject: [PATCH 013/211] another fix --- .../internal/client/dom/blocks/boundary.js | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c0a5d0101a43..9ebaf65d6ad2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -108,10 +108,6 @@ export function boundary(node, props, boundary_fn) { if (suspend_count++ === 0) { queue_micro_task(() => { - if (suspended_effect) { - return; - } - var effect = boundary_effect; suspended_effect = boundary_effect; @@ -150,20 +146,14 @@ export function boundary(node, props, boundary_fn) { return false; } - if (--suspend_count === 0) { - queue_micro_task(() => { - if (!suspended_effect) { - return; - } - - if (boundary_effect) { - destroy_effect(boundary_effect); - } - boundary_effect = suspended_effect; - suspended_effect = null; - anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); - resume_effect(boundary_effect); - }); + if (--suspend_count === 0 && suspended_effect !== null) { + if (boundary_effect) { + destroy_effect(boundary_effect); + } + boundary_effect = suspended_effect; + suspended_effect = null; + anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); + resume_effect(boundary_effect); } return true; From c7d3af1a3230c90f8ad0dd4e5627fb3c74c6afb3 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 21:51:15 +0000 Subject: [PATCH 014/211] oops --- .../internal/client/dom/blocks/boundary.js | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 9ebaf65d6ad2..c9e2f3d405b5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -108,6 +108,10 @@ export function boundary(node, props, boundary_fn) { if (suspend_count++ === 0) { queue_micro_task(() => { + if (suspended_effect) { + return; + } + var effect = boundary_effect; suspended_effect = boundary_effect; @@ -146,14 +150,19 @@ export function boundary(node, props, boundary_fn) { return false; } - if (--suspend_count === 0 && suspended_effect !== null) { - if (boundary_effect) { - destroy_effect(boundary_effect); - } - boundary_effect = suspended_effect; - suspended_effect = null; - anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); - resume_effect(boundary_effect); + if (--suspend_count === 0) { + queue_micro_task(() => { + if (!suspended_effect) { + return; + } + if (boundary_effect) { + destroy_effect(boundary_effect); + } + boundary_effect = suspended_effect; + suspended_effect = null; + anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); + resume_effect(boundary_effect); + }); } return true; From e2bc4d937fd9283d2267fe7fe5b078f4fa4c40d5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 15 Jan 2025 17:27:08 -0500 Subject: [PATCH 015/211] top-level await --- .../src/compiler/phases/2-analyze/index.js | 3 ++- .../2-analyze/visitors/AwaitExpression.js | 8 ++++++++ .../3-transform/client/transform-client.js | 20 ++++++++++++++++++- .../svelte/src/compiler/phases/types.d.ts | 4 ++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 7557b62a8e78..499a07127045 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -450,7 +450,8 @@ export function analyze_component(root, source, options) { source, undefined_exports: new Map(), snippet_renderers: new Map(), - snippets: new Set() + snippets: new Set(), + is_async: false }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 633a496e0545..f8e4cb6ab830 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -6,9 +6,17 @@ * @param {Context} context */ export function AwaitExpression(node, context) { + if (!context.state.analysis.runes) { + throw new Error('TODO runes mode only'); + } + if (context.state.expression) { context.state.expression.is_async = true; } + if (context.state.ast_type === 'instance' && context.state.scope.function_depth === 1) { + context.state.analysis.is_async = true; + } + 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 a1041947a497..d591dbe4e13c 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 @@ -355,7 +355,7 @@ export function client_component(analysis, options) { const push_args = [b.id('$$props'), b.literal(analysis.runes)]; if (dev) push_args.push(b.id(analysis.name)); - const component_block = b.block([ + let component_block = b.block([ ...store_setup, ...legacy_reactive_declarations, ...group_binding_declarations, @@ -367,6 +367,24 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); + if (analysis.is_async) { + const body = b.function_declaration( + b.id('$$body'), + [b.id('$$anchor'), b.id('$$props')], + component_block + ); + body.async = true; + + state.hoisted.push(body); + + component_block = b.block([ + b.var('fragment', b.call('$.comment')), + b.var('node', b.call('$.first_child', b.id('fragment'))), + b.stmt(b.call(body.id, b.id('node'), b.id('$$props'))), + b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) + ]); + } + if (!analysis.runes) { // Bind static exports to props so that people can access them with bind:x for (const { name, alias } of analysis.exports) { diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index fe32dbba3e4a..fc60fe3e4e84 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -85,6 +85,10 @@ export interface ComponentAnalysis extends Analysis { * Every snippet that is declared locally */ snippets: Set; + /** + * true if uses top-level await + */ + is_async: boolean; } declare module 'estree' { From 16f502a9d5d4751b876a62b3bb5b5683a21dc9be Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 22:59:47 +0000 Subject: [PATCH 016/211] more fixes --- .../src/internal/client/dom/blocks/await.js | 4 +- .../internal/client/dom/blocks/boundary.js | 8 +-- .../src/internal/client/dom/blocks/each.js | 4 +- .../svelte/src/internal/client/dom/css.js | 6 +-- .../client/dom/elements/bindings/input.js | 6 +-- .../client/dom/elements/bindings/this.js | 4 +- .../internal/client/dom/elements/events.js | 4 +- .../src/internal/client/dom/elements/misc.js | 4 +- .../client/dom/elements/transitions.js | 4 +- .../svelte/src/internal/client/dom/task.js | 50 ++++++++++++++----- .../svelte/src/internal/client/runtime.js | 12 +++-- packages/svelte/tests/animation-helpers.js | 4 +- 12 files changed, 69 insertions(+), 41 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 62b2e4dd0cda..546abd95dd9d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -13,7 +13,7 @@ import { set_dev_current_component_function } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { queue_micro_task } from '../task.js'; +import { queue_after_micro_task } from '../task.js'; import { UNINITIALIZED } from '../../../../constants.js'; const PENDING = 0; @@ -148,7 +148,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } else { // Wait a microtask before checking if we should show the pending state as // the promise might have resolved by the next microtask. - queue_micro_task(() => { + queue_after_micro_task(() => { if (!resolved) update(PENDING, true); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c9e2f3d405b5..e2c84e5a4036 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -27,7 +27,7 @@ import { set_hydrate_node } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; -import { queue_micro_task } from '../task.js'; +import { queue_before_micro_task } from '../task.js'; const SUSPEND_INCREMENT = Symbol(); const SUSPEND_DECREMENT = Symbol(); @@ -107,7 +107,7 @@ export function boundary(node, props, boundary_fn) { } if (suspend_count++ === 0) { - queue_micro_task(() => { + queue_before_micro_task(() => { if (suspended_effect) { return; } @@ -151,7 +151,7 @@ export function boundary(node, props, boundary_fn) { } if (--suspend_count === 0) { - queue_micro_task(() => { + queue_before_micro_task(() => { if (!suspended_effect) { return; } @@ -199,7 +199,7 @@ export function boundary(node, props, boundary_fn) { } if (failed) { - queue_micro_task(() => { + queue_before_micro_task(() => { render_snippet(() => { failed( anchor, diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index b17090948ae7..970d3e37e572 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -34,7 +34,7 @@ import { import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; import { INERT } from '../../constants.js'; -import { queue_micro_task } from '../task.js'; +import { queue_after_micro_task } from '../task.js'; import { active_effect, active_reaction, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; @@ -470,7 +470,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge } if (is_animated) { - queue_micro_task(() => { + queue_after_micro_task(() => { if (to_animate === undefined) return; for (item of to_animate) { item.a?.apply(); diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index 52be36aa1f46..39349402040e 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -1,5 +1,5 @@ import { DEV } from 'esm-env'; -import { queue_micro_task } from './task.js'; +import { queue_after_micro_task } from './task.js'; import { register_style } from '../dev/css.js'; /** @@ -7,8 +7,8 @@ import { register_style } from '../dev/css.js'; * @param {{ hash: string, code: string }} css */ export function append_styles(anchor, css) { - // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results - queue_micro_task(() => { + // Use `queue_after_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results + queue_after_micro_task(() => { var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index ec123d39681d..188b91fa0b4e 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -3,7 +3,7 @@ import { render_effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import * as e from '../../../errors.js'; import { is } from '../../../proxy.js'; -import { queue_micro_task } from '../../task.js'; +import { queue_after_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { is_runes, untrack } from '../../../runtime.js'; @@ -158,14 +158,14 @@ export function bind_group(inputs, group_index, input, get, set = get) { if (!pending.has(binding_group)) { pending.add(binding_group); - queue_micro_task(() => { + queue_after_micro_task(() => { // necessary to maintain binding group order in all insertion scenarios binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1)); pending.delete(binding_group); }); } - queue_micro_task(() => { + queue_after_micro_task(() => { if (hydration_mismatch) { var value; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index 56b0a56e71c4..d3e2349d426e 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,7 +1,7 @@ import { STATE_SYMBOL } from '../../../constants.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; -import { queue_micro_task } from '../../task.js'; +import { queue_after_micro_task } from '../../task.js'; /** * @param {any} bound_value @@ -49,7 +49,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part return () => { // We cannot use effects in the teardown phase, we we use a microtask instead. - queue_micro_task(() => { + queue_after_micro_task(() => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index f2038f96ada3..591faaec9c68 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -2,7 +2,7 @@ import { teardown } from '../../reactivity/effects.js'; import { define_property, is_array } from '../../../shared/utils.js'; import { hydrating } from '../hydration.js'; -import { queue_micro_task } from '../task.js'; +import { queue_after_micro_task } from '../task.js'; import { FILENAME } from '../../../../constants.js'; import * as w from '../../warnings.js'; import { @@ -77,7 +77,7 @@ export function create_event(event_name, dom, handler, options) { event_name.startsWith('touch') || event_name === 'wheel' ) { - queue_micro_task(() => { + queue_after_micro_task(() => { dom.addEventListener(event_name, target_handler, options); }); } else { diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index 61e513903f76..0eefaf104cc9 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,6 @@ import { hydrating } from '../hydration.js'; import { clear_text_content, get_first_child } from '../operations.js'; -import { queue_micro_task } from '../task.js'; +import { queue_after_micro_task } from '../task.js'; /** * @param {HTMLElement} dom @@ -12,7 +12,7 @@ export function autofocus(dom, value) { const body = document.body; dom.autofocus = true; - queue_micro_task(() => { + queue_after_micro_task(() => { if (document.activeElement === body) { dom.focus(); } diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index b3c16cdd080f..9834cd05e6fe 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -13,7 +13,7 @@ import { should_intro } from '../../render.js'; import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; -import { queue_micro_task } from '../task.js'; +import { queue_after_micro_task } from '../task.js'; /** * @param {Element} element @@ -326,7 +326,7 @@ function animate(element, options, counterpart, t2, on_finish) { var a; var aborted = false; - queue_micro_task(() => { + queue_after_micro_task(() => { if (aborted) return; var o = options({ direction: is_intro ? 'in' : 'out' }); a = animate(element, o, counterpart, t2, on_finish); diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index acb5a5b117f0..9f8808627656 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -10,33 +10,59 @@ let is_micro_task_queued = false; let is_idle_task_queued = false; /** @type {Array<() => void>} */ -let current_queued_micro_tasks = []; +let queued_before_microtasks = []; /** @type {Array<() => void>} */ -let current_queued_idle_tasks = []; +let queued_after_microtasks = []; +/** @type {Array<() => void>} */ +let queued_idle_tasks = []; -function process_micro_tasks() { - is_micro_task_queued = false; - const tasks = current_queued_micro_tasks.slice(); - current_queued_micro_tasks = []; +export function flush_before_micro_tasks() { + const tasks = queued_before_microtasks.slice(); + queued_before_microtasks = []; + run_all(tasks); +} + +function flush_after_micro_tasks() { + const tasks = queued_after_microtasks.slice(); + queued_after_microtasks = []; run_all(tasks); } +function process_micro_tasks() { + if (is_micro_task_queued) { + is_micro_task_queued = false; + flush_before_micro_tasks(); + flush_after_micro_tasks(); + } +} + function process_idle_tasks() { is_idle_task_queued = false; - const tasks = current_queued_idle_tasks.slice(); - current_queued_idle_tasks = []; + const tasks = queued_idle_tasks.slice(); + queued_idle_tasks = []; run_all(tasks); } /** * @param {() => void} fn */ -export function queue_micro_task(fn) { +export function queue_before_micro_task(fn) { + if (!is_micro_task_queued) { + is_micro_task_queued = true; + queueMicrotask(process_micro_tasks); + } + queued_before_microtasks.push(fn); +} + +/** + * @param {() => void} fn + */ +export function queue_after_micro_task(fn) { if (!is_micro_task_queued) { is_micro_task_queued = true; queueMicrotask(process_micro_tasks); } - current_queued_micro_tasks.push(fn); + queued_after_microtasks.push(fn); } /** @@ -47,13 +73,13 @@ export function queue_idle_task(fn) { is_idle_task_queued = true; request_idle_callback(process_idle_tasks); } - current_queued_idle_tasks.push(fn); + queued_idle_tasks.push(fn); } /** * Synchronously run any queued tasks. */ -export function flush_tasks() { +export function flush_after_tasks() { if (is_micro_task_queued) { process_micro_tasks(); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 55a8ccf32dc2..3f6a2e18e9b1 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { DISCONNECTED, BOUNDARY_EFFECT } from './constants.js'; -import { flush_tasks } from './dom/task.js'; +import { flush_after_tasks, flush_before_micro_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; import { internal_set, set, source } from './reactivity/sources.js'; import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js'; @@ -737,11 +737,12 @@ function flush_queued_effects(effects) { } } -function process_deferred() { +function flushed_deferred() { is_micro_task_queued = false; if (flush_count > 1001) { return; } + // flush_before_process_microtasks(); const previous_queued_root_effects = queued_root_effects; queued_root_effects = []; flush_queued_root_effects(previous_queued_root_effects); @@ -763,7 +764,7 @@ export function schedule_effect(signal) { if (scheduler_mode === FLUSH_MICROTASK) { if (!is_micro_task_queued) { is_micro_task_queued = true; - queueMicrotask(process_deferred); + queueMicrotask(flushed_deferred); } } @@ -776,7 +777,7 @@ export function schedule_effect(signal) { var flags = effect.f; if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - if ((flags & CLEAN) === 0) return + if ((flags & CLEAN) === 0) return; effect.f ^= CLEAN; } } @@ -878,11 +879,12 @@ export function flush_sync(fn) { queued_root_effects = root_effects; is_micro_task_queued = false; + flush_before_micro_tasks(); flush_queued_root_effects(previous_queued_root_effects); var result = fn?.(); - flush_tasks(); + flush_after_tasks(); if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js index dcbb06292305..27fb04b46fdc 100644 --- a/packages/svelte/tests/animation-helpers.js +++ b/packages/svelte/tests/animation-helpers.js @@ -1,6 +1,6 @@ import { flushSync } from 'svelte'; import { raf as svelte_raf } from 'svelte/internal/client'; -import { queue_micro_task } from '../src/internal/client/dom/task.js'; +import { queue_after_micro_task } from '../src/internal/client/dom/task.js'; export const raf = { animations: new Set(), @@ -132,7 +132,7 @@ class Animation { /** @param {() => {}} fn */ set onfinish(fn) { if (this.#duration === 0) { - queue_micro_task(fn); + queue_after_micro_task(fn); } else { this.#onfinish = () => { fn(); From a8a420c846b9e3da71aa0033447abaf173f5a067 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 23:21:51 +0000 Subject: [PATCH 017/211] cleanup --- .../src/internal/client/dom/blocks/await.js | 4 +- .../internal/client/dom/blocks/boundary.js | 62 +++++++----------- .../src/internal/client/dom/blocks/each.js | 4 +- .../svelte/src/internal/client/dom/css.js | 6 +- .../client/dom/elements/bindings/input.js | 6 +- .../client/dom/elements/bindings/this.js | 4 +- .../internal/client/dom/elements/events.js | 4 +- .../src/internal/client/dom/elements/misc.js | 4 +- .../client/dom/elements/transitions.js | 4 +- .../svelte/src/internal/client/dom/task.js | 64 ++++++++----------- .../svelte/src/internal/client/runtime.js | 14 +++- packages/svelte/tests/animation-helpers.js | 4 +- 12 files changed, 82 insertions(+), 98 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 546abd95dd9d..788afa1921b3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -13,7 +13,7 @@ import { set_dev_current_component_function } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { queue_after_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; import { UNINITIALIZED } from '../../../../constants.js'; const PENDING = 0; @@ -148,7 +148,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } else { // Wait a microtask before checking if we should show the pending state as // the promise might have resolved by the next microtask. - queue_after_micro_task(() => { + queue_post_micro_task(() => { if (!resolved) update(PENDING, true); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index e2c84e5a4036..1e172ef73b90 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -27,10 +27,10 @@ import { set_hydrate_node } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; -import { queue_before_micro_task } from '../task.js'; +import { queue_boundary_micro_task } from '../task.js'; -const SUSPEND_INCREMENT = Symbol(); -const SUSPEND_DECREMENT = Symbol(); +const ASYNC_INCREMENT = Symbol(); +const ASYNC_DECREMENT = Symbol(); /** * @param {Effect} boundary @@ -70,10 +70,10 @@ export function boundary(node, props, boundary_fn) { /** @type {Effect} */ var boundary_effect; /** @type {Effect | null} */ - var suspended_effect = null; + var async_effect = null; /** @type {DocumentFragment | null} */ - var suspended_fragment = null; - var suspend_count = 0; + var async_fragment = null; + var async_count = 0; block(() => { var boundary = /** @type {Effect} */ (active_effect); @@ -101,27 +101,27 @@ export function boundary(node, props, boundary_fn) { boundary.fn = (/** @type {unknown} */ input) => { let pending = props.pending; - if (input === SUSPEND_INCREMENT) { + if (input === ASYNC_INCREMENT) { if (!pending) { return false; } - if (suspend_count++ === 0) { - queue_before_micro_task(() => { - if (suspended_effect) { + if (async_count++ === 0) { + queue_boundary_micro_task(() => { + if (async_effect) { return; } var effect = boundary_effect; - suspended_effect = boundary_effect; + async_effect = boundary_effect; pause_effect( - suspended_effect, + async_effect, () => { /** @type {TemplateNode | null} */ var node = effect.nodes_start; var end = effect.nodes_end; - suspended_fragment = document.createDocumentFragment(); + async_fragment = document.createDocumentFragment(); while (node !== null) { /** @type {TemplateNode | null} */ @@ -129,7 +129,7 @@ export function boundary(node, props, boundary_fn) { node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); node.remove(); - suspended_fragment.append(node); + async_fragment.append(node); node = sibling; } }, @@ -145,22 +145,22 @@ export function boundary(node, props, boundary_fn) { return true; } - if (input === SUSPEND_DECREMENT) { + if (input === ASYNC_DECREMENT) { if (!pending) { return false; } - if (--suspend_count === 0) { - queue_before_micro_task(() => { - if (!suspended_effect) { + if (--async_count === 0) { + queue_boundary_micro_task(() => { + if (!async_effect) { return; } if (boundary_effect) { destroy_effect(boundary_effect); } - boundary_effect = suspended_effect; - suspended_effect = null; - anchor.before(/** @type {DocumentFragment} */ (suspended_fragment)); + boundary_effect = async_effect; + async_effect = null; + anchor.before(/** @type {DocumentFragment} */ (async_fragment)); resume_effect(boundary_effect); }); } @@ -199,7 +199,7 @@ export function boundary(node, props, boundary_fn) { } if (failed) { - queue_before_micro_task(() => { + queue_boundary_micro_task(() => { render_snippet(() => { failed( anchor, @@ -226,9 +226,9 @@ export function boundary(node, props, boundary_fn) { /** * @param {Effect | null} effect - * @param {typeof SUSPEND_INCREMENT | typeof SUSPEND_DECREMENT} trigger + * @param {typeof ASYNC_INCREMENT | typeof ASYNC_DECREMENT} trigger */ -function trigger_suspense(effect, trigger) { +export function trigger_async_boundary(effect, trigger) { var current = effect; while (current !== null) { @@ -241,17 +241,3 @@ function trigger_suspense(effect, trigger) { current = current.parent; } } - -export function create_suspense() { - var current = active_effect; - - const suspend = () => { - trigger_suspense(current, SUSPEND_INCREMENT); - }; - - const unsuspend = () => { - trigger_suspense(current, SUSPEND_DECREMENT); - }; - - return [suspend, unsuspend]; -} diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 970d3e37e572..dc4c133de4e9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -34,7 +34,7 @@ import { import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; import { INERT } from '../../constants.js'; -import { queue_after_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; import { active_effect, active_reaction, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; @@ -470,7 +470,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge } if (is_animated) { - queue_after_micro_task(() => { + queue_post_micro_task(() => { if (to_animate === undefined) return; for (item of to_animate) { item.a?.apply(); diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index 39349402040e..d4340a07eef6 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -1,5 +1,5 @@ import { DEV } from 'esm-env'; -import { queue_after_micro_task } from './task.js'; +import { queue_post_micro_task } from './task.js'; import { register_style } from '../dev/css.js'; /** @@ -7,8 +7,8 @@ import { register_style } from '../dev/css.js'; * @param {{ hash: string, code: string }} css */ export function append_styles(anchor, css) { - // Use `queue_after_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results - queue_after_micro_task(() => { + // Use `queue_post_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results + queue_post_micro_task(() => { var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 188b91fa0b4e..b8d4b07c9b7e 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -3,7 +3,7 @@ import { render_effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import * as e from '../../../errors.js'; import { is } from '../../../proxy.js'; -import { queue_after_micro_task } from '../../task.js'; +import { queue_post_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { is_runes, untrack } from '../../../runtime.js'; @@ -158,14 +158,14 @@ export function bind_group(inputs, group_index, input, get, set = get) { if (!pending.has(binding_group)) { pending.add(binding_group); - queue_after_micro_task(() => { + queue_post_micro_task(() => { // necessary to maintain binding group order in all insertion scenarios binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1)); pending.delete(binding_group); }); } - queue_after_micro_task(() => { + queue_post_micro_task(() => { if (hydration_mismatch) { var value; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index d3e2349d426e..0ca5039e7c69 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,7 +1,7 @@ import { STATE_SYMBOL } from '../../../constants.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; -import { queue_after_micro_task } from '../../task.js'; +import { queue_post_micro_task } from '../../task.js'; /** * @param {any} bound_value @@ -49,7 +49,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part return () => { // We cannot use effects in the teardown phase, we we use a microtask instead. - queue_after_micro_task(() => { + queue_post_micro_task(() => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 591faaec9c68..4144a13fac66 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -2,7 +2,7 @@ import { teardown } from '../../reactivity/effects.js'; import { define_property, is_array } from '../../../shared/utils.js'; import { hydrating } from '../hydration.js'; -import { queue_after_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; import { FILENAME } from '../../../../constants.js'; import * as w from '../../warnings.js'; import { @@ -77,7 +77,7 @@ export function create_event(event_name, dom, handler, options) { event_name.startsWith('touch') || event_name === 'wheel' ) { - queue_after_micro_task(() => { + queue_post_micro_task(() => { dom.addEventListener(event_name, target_handler, options); }); } else { diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index 0eefaf104cc9..dab8e84c32f6 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,6 @@ import { hydrating } from '../hydration.js'; import { clear_text_content, get_first_child } from '../operations.js'; -import { queue_after_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; /** * @param {HTMLElement} dom @@ -12,7 +12,7 @@ export function autofocus(dom, value) { const body = document.body; dom.autofocus = true; - queue_after_micro_task(() => { + queue_post_micro_task(() => { if (document.activeElement === body) { dom.focus(); } diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 9834cd05e6fe..0dd17fad9ff4 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -13,7 +13,7 @@ import { should_intro } from '../../render.js'; import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; -import { queue_after_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; /** * @param {Element} element @@ -326,7 +326,7 @@ function animate(element, options, counterpart, t2, on_finish) { var a; var aborted = false; - queue_after_micro_task(() => { + queue_post_micro_task(() => { if (aborted) return; var o = options({ direction: is_intro ? 'in' : 'out' }); a = animate(element, o, counterpart, t2, on_finish); diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 9f8808627656..8b16b30ebead 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -10,59 +10,61 @@ let is_micro_task_queued = false; let is_idle_task_queued = false; /** @type {Array<() => void>} */ -let queued_before_microtasks = []; +let queued_boundary_microtasks = []; /** @type {Array<() => void>} */ -let queued_after_microtasks = []; +let queued_post_microtasks = []; /** @type {Array<() => void>} */ let queued_idle_tasks = []; -export function flush_before_micro_tasks() { - const tasks = queued_before_microtasks.slice(); - queued_before_microtasks = []; +export function flush_boundary_micro_tasks() { + const tasks = queued_boundary_microtasks.slice(); + queued_boundary_microtasks = []; run_all(tasks); } -function flush_after_micro_tasks() { - const tasks = queued_after_microtasks.slice(); - queued_after_microtasks = []; +export function flush_post_micro_tasks() { + const tasks = queued_post_microtasks.slice(); + queued_post_microtasks = []; run_all(tasks); } -function process_micro_tasks() { - if (is_micro_task_queued) { - is_micro_task_queued = false; - flush_before_micro_tasks(); - flush_after_micro_tasks(); +export function flush_idle_tasks() { + if (is_idle_task_queued) { + is_idle_task_queued = false; + const tasks = queued_idle_tasks.slice(); + queued_idle_tasks = []; + run_all(tasks); } } -function process_idle_tasks() { - is_idle_task_queued = false; - const tasks = queued_idle_tasks.slice(); - queued_idle_tasks = []; - run_all(tasks); +function flush_all_micro_tasks() { + if (is_micro_task_queued) { + is_micro_task_queued = false; + flush_boundary_micro_tasks(); + flush_post_micro_tasks(); + } } /** * @param {() => void} fn */ -export function queue_before_micro_task(fn) { +export function queue_boundary_micro_task(fn) { if (!is_micro_task_queued) { is_micro_task_queued = true; - queueMicrotask(process_micro_tasks); + queueMicrotask(flush_all_micro_tasks); } - queued_before_microtasks.push(fn); + queued_boundary_microtasks.push(fn); } /** * @param {() => void} fn */ -export function queue_after_micro_task(fn) { +export function queue_post_micro_task(fn) { if (!is_micro_task_queued) { is_micro_task_queued = true; - queueMicrotask(process_micro_tasks); + queueMicrotask(flush_all_micro_tasks); } - queued_after_microtasks.push(fn); + queued_post_microtasks.push(fn); } /** @@ -71,19 +73,7 @@ export function queue_after_micro_task(fn) { export function queue_idle_task(fn) { if (!is_idle_task_queued) { is_idle_task_queued = true; - request_idle_callback(process_idle_tasks); + request_idle_callback(flush_idle_tasks); } queued_idle_tasks.push(fn); } - -/** - * Synchronously run any queued tasks. - */ -export function flush_after_tasks() { - if (is_micro_task_queued) { - process_micro_tasks(); - } - if (is_idle_task_queued) { - process_idle_tasks(); - } -} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3f6a2e18e9b1..129260b454de 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,11 @@ import { DISCONNECTED, BOUNDARY_EFFECT } from './constants.js'; -import { flush_after_tasks, flush_before_micro_tasks } from './dom/task.js'; +import { + flush_idle_tasks, + flush_boundary_micro_tasks, + flush_post_micro_tasks +} from './dom/task.js'; import { add_owner } from './dev/ownership.js'; import { internal_set, set, source } from './reactivity/sources.js'; import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js'; @@ -812,6 +816,9 @@ function process_effects(effect, collected_effects) { current_effect.f ^= CLEAN; } else { try { + if ((flags & BOUNDARY_EFFECT) !== 0) { + flush_boundary_micro_tasks(); + } if (check_dirtiness(current_effect)) { update_effect(current_effect); } @@ -879,12 +886,13 @@ export function flush_sync(fn) { queued_root_effects = root_effects; is_micro_task_queued = false; - flush_before_micro_tasks(); flush_queued_root_effects(previous_queued_root_effects); var result = fn?.(); - flush_after_tasks(); + flush_boundary_micro_tasks(); + flush_post_micro_tasks(); + flush_idle_tasks(); if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js index 27fb04b46fdc..e37c2563af5e 100644 --- a/packages/svelte/tests/animation-helpers.js +++ b/packages/svelte/tests/animation-helpers.js @@ -1,6 +1,6 @@ import { flushSync } from 'svelte'; import { raf as svelte_raf } from 'svelte/internal/client'; -import { queue_after_micro_task } from '../src/internal/client/dom/task.js'; +import { queue_post_micro_task } from '../src/internal/client/dom/task.js'; export const raf = { animations: new Set(), @@ -132,7 +132,7 @@ class Animation { /** @param {() => {}} fn */ set onfinish(fn) { if (this.#duration === 0) { - queue_after_micro_task(fn); + queue_post_micro_task(fn); } else { this.#onfinish = () => { fn(); From 36e2469ccea08a7028c758dda8ee87c59541185f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 15 Jan 2025 23:37:19 +0000 Subject: [PATCH 018/211] more tweaks --- packages/svelte/src/internal/client/runtime.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 129260b454de..69e97699e1bf 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -816,10 +816,9 @@ function process_effects(effect, collected_effects) { current_effect.f ^= CLEAN; } else { try { - if ((flags & BOUNDARY_EFFECT) !== 0) { - flush_boundary_micro_tasks(); - } - if (check_dirtiness(current_effect)) { + // If the effect is dirty, then we need to update it, it might also turn inert + // because of async work during calling check_dirtiness + if (check_dirtiness(current_effect) && (current_effect.f & INERT) === 0) { update_effect(current_effect); } } catch (error) { From 0c0fd47b39d3516a0cc874e25f37662e529c491f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 16 Jan 2025 00:03:10 +0000 Subject: [PATCH 019/211] more tweaks --- packages/svelte/src/internal/client/runtime.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 69e97699e1bf..aba037c4a36b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -816,9 +816,7 @@ function process_effects(effect, collected_effects) { current_effect.f ^= CLEAN; } else { try { - // If the effect is dirty, then we need to update it, it might also turn inert - // because of async work during calling check_dirtiness - if (check_dirtiness(current_effect) && (current_effect.f & INERT) === 0) { + if (check_dirtiness(current_effect)) { update_effect(current_effect); } } catch (error) { From 32e12d03b36b75fd3db0f06b74c484e01c5027b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 06:18:50 -0500 Subject: [PATCH 020/211] async deriveds --- .../src/compiler/phases/2-analyze/index.js | 6 ++- .../2-analyze/visitors/CallExpression.js | 15 +++++++- .../client/visitors/VariableDeclaration.js | 27 ++++++++++--- .../client/visitors/shared/declarations.js | 14 ++++++- .../svelte/src/compiler/phases/types.d.ts | 5 ++- packages/svelte/src/internal/client/index.js | 2 +- .../internal/client/reactivity/deriveds.js | 38 +++++++++++++++++-- 7 files changed, 92 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 499a07127045..80ff005ebcff 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 9f51cd61de6d..5465720a684a 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 afb90bbec7f9..b9a987015f06 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 0bd8c352f6a9..02172be5f5d1 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 fc60fe3e4e84..ce308f6f1752 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 5d852b6a1374..f77f39d99713 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 7ec1ed30bdc8..9fdb7abe6b66 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 From c81e94a4a3790783b982b44725860b2da6ee87ed Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 06:29:29 -0500 Subject: [PATCH 021/211] add test --- .../samples/async-basic/_config.js | 25 +++++++++++++++++++ .../samples/async-basic/main.svelte | 11 ++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-basic/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-basic/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js b/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js new file mode 100644 index 000000000000..8bbf9cb4520a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {PromiseWithResolvers} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target }) { + d.resolve('hello'); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-basic/main.svelte new file mode 100644 index 000000000000..fefce867f294 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-basic/main.svelte @@ -0,0 +1,11 @@ + + + +

{await promise}

+ + {#snippet pending()} +

pending

+ {/snippet} +
From fa8d4596d2ef7212032667c73cd85b983a59803f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 07:59:57 -0500 Subject: [PATCH 022/211] adjust test (yes, this is _technically_ breaking) --- .../tests/runtime-runes/samples/bind-this-no-state/_config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js index 6d428f630659..19af552f0c88 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js @@ -26,18 +26,22 @@ export default test({ await btn1?.click(); await tick(); + await tick(); assert.htmlEqual(target.innerHTML, get_html(1)); await btn2?.click(); await tick(); + await tick(); assert.htmlEqual(target.innerHTML, get_html(2)); await btn1?.click(); await tick(); + await tick(); assert.htmlEqual(target.innerHTML, get_html(1)); await btn3?.click(); await tick(); + await tick(); assert.htmlEqual(target.innerHTML, get_html(3)); } }); From 53b639de832bca7d45b5d402e48d457a41aafd08 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 08:00:34 -0500 Subject: [PATCH 023/211] fix --- .../svelte/tests/runtime-runes/samples/async-basic/_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js b/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js index 8bbf9cb4520a..5f85050d9b0e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js @@ -2,7 +2,7 @@ import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; -/** @type {PromiseWithResolvers} */ +/** @type {ReturnType} */ let d; export default test({ From b0a08f5034a7be56ade96d1f967cfdf4d713511a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 08:03:07 -0500 Subject: [PATCH 024/211] fix --- .../src/compiler/phases/2-analyze/index.js | 6 ++-- .../2-analyze/visitors/AwaitExpression.js | 13 ++++++-- .../2-analyze/visitors/shared/function.js | 3 +- .../client/visitors/AwaitExpression.js | 9 ++++- .../svelte/src/compiler/phases/types.d.ts | 12 ++++++- .../internal/client/dom/blocks/boundary.js | 33 ++++++++++--------- 6 files changed, 52 insertions(+), 24 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 80ff005ebcff..c18ef0c25b44 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -265,7 +265,8 @@ export function analyze_module(ast, options) { runes: true, immutable: true, tracing: analysis.tracing, - async_deriveds: new Set() + async_deriveds: new Set(), + blocking_awaits: new Set() }; } @@ -453,7 +454,8 @@ export function analyze_component(root, source, options) { snippet_renderers: new Map(), snippets: new Set(), is_async: false, - async_deriveds: new Set() + async_deriveds: new Set(), + blocking_awaits: new Set() }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index f8e4cb6ab830..5c6d45098b90 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -6,15 +6,22 @@ * @param {Context} context */ export function AwaitExpression(node, context) { - if (!context.state.analysis.runes) { - throw new Error('TODO runes mode only'); + const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; + const blocking = tla || !!context.state.expression; + + if (blocking) { + if (!context.state.analysis.runes) { + throw new Error('TODO runes mode only'); + } + + context.state.analysis.blocking_awaits.add(node); } if (context.state.expression) { context.state.expression.is_async = true; } - if (context.state.ast_type === 'instance' && context.state.scope.function_depth === 1) { + if (tla) { context.state.analysis.is_async = true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index c6151992bfd0..c892efd421d1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -15,6 +15,7 @@ export function visit_function(node, context) { context.next({ ...context.state, - function_depth: context.state.function_depth + 1 + function_depth: context.state.function_depth + 1, + expression: null }); } 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 index 809a7b43f8ce..a26923862cd2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -7,12 +7,19 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function AwaitExpression(node, context) { + if (!context.state.analysis.runes) { + return context.next(); + } + + const block = context.state.analysis.blocking_awaits.has(node); + return b.call( b.member( b.await( b.call( '$.preserve_context', - node.argument && /** @type {Expression} */ (context.visit(node.argument)) + node.argument && /** @type {Expression} */ (context.visit(node.argument)), + block && b.true ) ), 'read' diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index ce308f6f1752..dcbffdfc5806 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -1,5 +1,12 @@ import type { AST, Binding } from '#compiler'; -import type { CallExpression, Identifier, LabeledStatement, Node, Program } from 'estree'; +import type { + AwaitExpression, + CallExpression, + Identifier, + LabeledStatement, + Node, + Program +} from 'estree'; import type { Scope, ScopeRoot } from './scope.js'; export interface Js { @@ -34,6 +41,9 @@ export interface Analysis { /** A set of deriveds that contain `await` expressions */ async_deriveds: Set; + + /** A set of `await` expressions that should trigger suspense */ + blocking_awaits: Set; } export interface ComponentAnalysis extends Analysis { diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ab9f51d6a078..48f01aaaa944 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -247,14 +247,15 @@ export function trigger_async_boundary(effect, trigger) { /** * @template T * @param {Promise} promise + * @param {boolean} block * @returns {Promise<{ read: () => T }>} */ -export async function preserve_context(promise) { +export function preserve_context(promise, block = false) { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; - let boundary = active_effect; + let boundary = block ? active_effect : null; while (boundary !== null) { if ((boundary.f & BOUNDARY_EFFECT) !== 0) { break; @@ -263,25 +264,25 @@ export async function preserve_context(promise) { boundary = boundary.parent; } - if (boundary === null) { + if (block && boundary === null) { throw new Error('cannot suspend outside a boundary'); } // @ts-ignore - boundary.fn(ASYNC_INCREMENT); + boundary?.fn(ASYNC_INCREMENT); - const value = await promise; + return promise.then((value) => { + return { + read() { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); - return { - read() { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_component_context); + // @ts-ignore + boundary?.fn(ASYNC_DECREMENT); - // @ts-ignore - boundary.fn(ASYNC_DECREMENT); - - return value; - } - }; + return value; + } + }; + }); } From 1320130862bd196c51346a8d8310b3b355e9815b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 09:57:11 -0500 Subject: [PATCH 025/211] various --- .../src/compiler/phases/2-analyze/index.js | 4 +-- .../2-analyze/visitors/AwaitExpression.js | 2 +- .../2-analyze/visitors/CallExpression.js | 3 ++ .../client/visitors/AwaitExpression.js | 13 ++++---- .../svelte/src/compiler/phases/types.d.ts | 2 +- .../internal/client/dom/blocks/boundary.js | 33 +++++++++---------- packages/svelte/src/internal/client/index.js | 2 +- .../internal/client/reactivity/deriveds.js | 6 ++-- 8 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c18ef0c25b44..90e1ceb685c7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -266,7 +266,7 @@ export function analyze_module(ast, options) { immutable: true, tracing: analysis.tracing, async_deriveds: new Set(), - blocking_awaits: new Set() + suspenders: new Set() }; } @@ -455,7 +455,7 @@ export function analyze_component(root, source, options) { snippets: new Set(), is_async: false, async_deriveds: new Set(), - blocking_awaits: new Set() + suspenders: new Set() }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 5c6d45098b90..97da435d0aaf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -14,7 +14,7 @@ export function AwaitExpression(node, context) { throw new Error('TODO runes mode only'); } - context.state.analysis.blocking_awaits.add(node); + context.state.analysis.suspenders.add(node); } if (context.state.expression) { 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 5465720a684a..6755193d3c15 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -219,6 +219,9 @@ export function CallExpression(node, context) { if (expression.is_async) { context.state.analysis.async_deriveds.add(node); + + context.state.analysis.is_async ||= + context.state.ast_type === 'instance' && context.state.function_depth === 1; } } else if (rune === '$inspect') { context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); 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 index a26923862cd2..a9486fd8c829 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -7,22 +7,21 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function AwaitExpression(node, context) { - if (!context.state.analysis.runes) { + const suspend = context.state.analysis.suspenders.has(node); + + if (!suspend) { return context.next(); } - const block = context.state.analysis.blocking_awaits.has(node); - return b.call( b.member( b.await( b.call( - '$.preserve_context', - node.argument && /** @type {Expression} */ (context.visit(node.argument)), - block && b.true + '$.suspend', + node.argument && /** @type {Expression} */ (context.visit(node.argument)) ) ), - 'read' + 'exit' ) ); } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index dcbffdfc5806..fdb4eac5577a 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -43,7 +43,7 @@ export interface Analysis { async_deriveds: Set; /** A set of `await` expressions that should trigger suspense */ - blocking_awaits: Set; + suspenders: Set; } export interface ComponentAnalysis extends Analysis { diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 48f01aaaa944..c2d976c24409 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -247,15 +247,14 @@ export function trigger_async_boundary(effect, trigger) { /** * @template T * @param {Promise} promise - * @param {boolean} block - * @returns {Promise<{ read: () => T }>} + * @returns {Promise<{ exit: () => T }>} */ -export function preserve_context(promise, block = false) { +export async function suspend(promise) { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; - let boundary = block ? active_effect : null; + let boundary = active_effect; while (boundary !== null) { if ((boundary.f & BOUNDARY_EFFECT) !== 0) { break; @@ -264,25 +263,25 @@ export function preserve_context(promise, block = false) { boundary = boundary.parent; } - if (block && boundary === null) { + if (boundary === null) { throw new Error('cannot suspend outside a boundary'); } // @ts-ignore boundary?.fn(ASYNC_INCREMENT); - return promise.then((value) => { - return { - read() { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_component_context); + const value = await promise; - // @ts-ignore - boundary?.fn(ASYNC_DECREMENT); + return { + exit() { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); - return value; - } - }; - }); + // @ts-ignore + boundary?.fn(ASYNC_DECREMENT); + + return value; + } + }; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index f77f39d99713..0a17a546213f 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, preserve_context } from './dom/blocks/boundary.js'; +export { boundary, suspend } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 9fdb7abe6b66..eb0fdba469a2 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -27,7 +27,7 @@ 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'; +import { suspend } from '../dom/blocks/boundary.js'; /** * @template V @@ -103,7 +103,9 @@ export async function async_derived(fn) { // TODO what happens when the promise rejects? }); - (await preserve_context(promise)).read(); + // wait for the initial promise + (await suspend(promise)).exit(); + return () => get(value); } From 1588464d3f8dc1984de139204d647dbbcd11834b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 15:24:35 -0500 Subject: [PATCH 026/211] fix --- .../src/compiler/phases/2-analyze/visitors/Attribute.js | 1 + .../src/compiler/phases/2-analyze/visitors/StyleDirective.js | 1 + .../phases/3-transform/client/visitors/shared/component.js | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 9d801e095e8d..75c79aab6ad4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -64,6 +64,7 @@ export function Attribute(node, context) { node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + node.metadata.expression.is_async ||= chunk.metadata.expression.is_async; } if (is_event_attribute(node)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js index 7d6eb5be99e8..91b13acd4e0d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js @@ -32,6 +32,7 @@ export function StyleDirective(node, context) { node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + node.metadata.expression.is_async ||= chunk.metadata.expression.is_async; } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index f509cb41a7d8..e79fa931b0e7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -94,6 +94,10 @@ export function build_component(node, component_name, context, anchor = context. } for (const attribute of node.attributes) { + if (attribute.type === 'Attribute' || attribute.type === 'SpreadAttribute') { + context.state.metadata.init_is_async ||= attribute.metadata.expression.is_async; + } + if (attribute.type === 'LetDirective') { if (!slot_scope_applies_to_itself) { lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default))); From 2fe198f1ad9fc1e1bffd2b77d9c92883efde88a6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 16:42:04 -0500 Subject: [PATCH 027/211] fix --- .../3-transform/client/transform-client.js | 24 +++++-------------- .../3-transform/client/visitors/Fragment.js | 15 ++++++++++++ .../client/visitors/shared/component.js | 13 ++++++++-- .../svelte/src/compiler/utils/builders.js | 14 +++++------ 4 files changed, 39 insertions(+), 27 deletions(-) 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 d591dbe4e13c..e7a5e024af42 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 @@ -355,6 +355,12 @@ export function client_component(analysis, options) { const push_args = [b.id('$$props'), b.literal(analysis.runes)]; if (dev) push_args.push(b.id(analysis.name)); + if (analysis.is_async) { + const body = /** @type {ESTree.FunctionDeclaration} */ (template.body[0]); + body.body.body.unshift(...instance.body); + instance.body.length = 0; + } + let component_block = b.block([ ...store_setup, ...legacy_reactive_declarations, @@ -367,24 +373,6 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); - if (analysis.is_async) { - const body = b.function_declaration( - b.id('$$body'), - [b.id('$$anchor'), b.id('$$props')], - component_block - ); - body.async = true; - - state.hoisted.push(body); - - component_block = b.block([ - b.var('fragment', b.call('$.comment')), - b.var('node', b.call('$.first_child', b.id('fragment'))), - b.stmt(b.call(body.id, b.id('node'), b.id('$$props'))), - b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) - ]); - } - if (!analysis.runes) { // Bind static exports to props so that people can access them with bind:x for (const { name, alias } of analysis.exports) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index a3572b9b9ca3..e69243e9d7dd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -204,6 +204,21 @@ export function Fragment(node, context) { body.push(close); } + const async = + state.metadata.init_is_async || (state.analysis.is_async && context.path.length === 0); + + if (async) { + // TODO need to create bookends for hydration to work + return b.block([ + b.function_declaration(b.id('$$body'), [b.id('$$anchor')], b.block(body), true), + + b.var('fragment', b.call('$.comment')), + b.var('node', b.call('$.first_child', b.id('fragment'))), + b.stmt(b.call(b.id('$$body'), b.id('node'))), + b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) + ]); + } + return b.block(body); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index e79fa931b0e7..644c0478d25d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -167,8 +167,17 @@ export function build_component(node, component_name, context, anchor = context. if (should_wrap_in_derived) { const id = b.id(context.state.scope.generate(attribute.name)); - context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); - arg = b.call('$.get', id); + + if (attribute.metadata.expression.is_async) { + // TODO parallelise these + context.state.init.push( + b.var(id, b.await(b.call('$.async_derived', b.thunk(arg, true)))) + ); + arg = b.call(id); + } else { + context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); + arg = b.call('$.get', id); + } } push_prop(b.get(attribute.name, [b.return(arg)])); diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index f79028a947e9..42c0a46788b7 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -30,16 +30,17 @@ export function assignment_pattern(left, right) { /** * @param {Array} params * @param {ESTree.BlockStatement | ESTree.Expression} body + * @param {boolean} async * @returns {ESTree.ArrowFunctionExpression} */ -export function arrow(params, body) { +export function arrow(params, body, async = false) { return { type: 'ArrowFunctionExpression', params, body, expression: body.type !== 'BlockStatement', generator: false, - async: false, + async, metadata: /** @type {any} */ (null) // should not be used by codegen }; } @@ -214,16 +215,17 @@ export function export_default(declaration) { * @param {ESTree.Identifier} id * @param {ESTree.Pattern[]} params * @param {ESTree.BlockStatement} body + * @param {boolean} async * @returns {ESTree.FunctionDeclaration} */ -export function function_declaration(id, params, body) { +export function function_declaration(id, params, body, async = false) { return { type: 'FunctionDeclaration', id, params, body, generator: false, - async: false, + async, metadata: /** @type {any} */ (null) // should not be used by codegen }; } @@ -419,9 +421,7 @@ export function template(elements, expressions) { * @returns {ESTree.Expression} */ export function thunk(expression, async = false) { - const fn = arrow([], expression); - if (async) fn.async = true; - return unthunk(fn); + return unthunk(arrow([], expression, async)); } /** From 1a72d285f694f43e6a4d87fb35a7bc303930f579 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Jan 2025 17:24:55 -0500 Subject: [PATCH 028/211] tests --- .../samples/async-attribute/_config.js | 34 +++++++++++++++++++ .../samples/async-attribute/main.svelte | 11 ++++++ .../_config.js | 0 .../main.svelte | 0 .../samples/async-top-level/Child.svelte | 7 ++++ .../samples/async-top-level/_config.js | 25 ++++++++++++++ .../samples/async-top-level/main.svelte | 13 +++++++ 7 files changed, 90 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte rename packages/svelte/tests/runtime-runes/samples/{async-basic => async-expression}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{async-basic => async-expression}/main.svelte (100%) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js new file mode 100644 index 000000000000..a8df1b04a9a6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('cool'); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d = deferred(); + component.promise = d.promise; + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('neat'); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte new file mode 100644 index 000000000000..aded5144531c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte @@ -0,0 +1,11 @@ + + + +

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-basic/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-basic/_config.js rename to packages/svelte/tests/runtime-runes/samples/async-expression/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/async-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-basic/main.svelte rename to packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte new file mode 100644 index 000000000000..7ad618f13003 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte @@ -0,0 +1,7 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js new file mode 100644 index 000000000000..5f85050d9b0e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target }) { + d.resolve('hello'); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte new file mode 100644 index 000000000000..718a256b8676 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
From 0d8f27eae69760714c9c439f15af492f0b226ff9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Jan 2025 10:41:40 -0500 Subject: [PATCH 029/211] parallelize --- .../3-transform/client/transform-client.js | 3 +- .../phases/3-transform/client/types.d.ts | 7 +-- .../3-transform/client/visitors/Fragment.js | 38 +++++++++++-- .../client/visitors/RegularElement.js | 13 ++--- .../client/visitors/SvelteElement.js | 7 +-- .../client/visitors/TitleElement.js | 7 +-- .../client/visitors/shared/component.js | 13 ++--- .../client/visitors/shared/element.js | 21 +++----- .../client/visitors/shared/fragment.js | 8 +-- .../client/visitors/shared/utils.js | 53 ++++++++----------- .../internal/client/dom/blocks/boundary.js | 6 +++ packages/svelte/src/internal/client/index.js | 2 +- 12 files changed, 87 insertions(+), 91 deletions(-) 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 e7a5e024af42..616376b012c4 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 @@ -160,8 +160,7 @@ export function client_component(analysis, options) { }, namespace: options.namespace, bound_contenteditable: false, - init_is_async: false, - update_is_async: false + async: [] }, events: new Set(), preserve_whitespace: options.preserveWhitespace, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 46a268d51406..06309ac34e27 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -75,9 +75,10 @@ export interface ComponentClientTransformState extends ClientTransformState { */ template_contains_script_tag: boolean; }; - // TODO it would be nice if these were colocated with the arrays they pertain to - init_is_async: boolean; - update_is_async: boolean; + /** + * Synthetic async deriveds belonging to the current fragment + */ + async: Array<{ id: Identifier; expression: Expression }>; }; readonly preserve_whitespace: boolean; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index e69243e9d7dd..0755126e2a8b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -75,8 +75,7 @@ export function Fragment(node, context) { }, namespace, bound_contenteditable: context.state.metadata.bound_contenteditable, - init_is_async: false, - update_is_async: false + async: [] } }; @@ -192,7 +191,7 @@ export function Fragment(node, context) { } if (state.update.length > 0) { - body.push(build_render_statement(state.update, state.metadata.update_is_async)); + body.push(build_render_statement(state.update)); } body.push(...state.after_update); @@ -205,12 +204,41 @@ export function Fragment(node, context) { } const async = - state.metadata.init_is_async || (state.analysis.is_async && context.path.length === 0); + state.metadata.async.length > 0 || (state.analysis.is_async && context.path.length === 0); if (async) { // TODO need to create bookends for hydration to work return b.block([ - b.function_declaration(b.id('$$body'), [b.id('$$anchor')], b.block(body), true), + b.function_declaration( + b.id('$$body'), + [b.id('$$anchor')], + b.block([ + b.var( + b.array_pattern(state.metadata.async.map(({ id }) => id)), + b.call( + b.member( + b.await( + b.call( + '$.suspend', + b.call( + 'Promise.all', + b.array( + state.metadata.async.map(({ expression }) => + b.call('$.async_derived', b.thunk(expression, true)) + ) + ) + ) + ) + ), + 'exit' + ) + ) + ), + ...body, + b.stmt(b.call('$.exit')) + ]), + true + ), b.var('fragment', b.call('$.comment')), b.var('node', b.call('$.first_child', b.id('fragment'))), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 5632d35b244d..944606591921 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -409,9 +409,7 @@ export function RegularElement(node, context) { b.block([ ...child_state.init, ...element_state.init, - child_state.update.length > 0 - ? build_render_statement(child_state.update, child_state.metadata.update_is_async) - : b.empty, + child_state.update.length > 0 ? build_render_statement(child_state.update) : b.empty, ...child_state.after_update, ...element_state.after_update ]) @@ -420,9 +418,6 @@ export function RegularElement(node, context) { context.state.init.push(...child_state.init, ...element_state.init); context.state.update.push(...child_state.update); context.state.after_update.push(...child_state.after_update, ...element_state.after_update); - - context.state.metadata.init_is_async ||= child_state.metadata.init_is_async; - context.state.metadata.update_is_async ||= child_state.metadata.update_is_async; } else { context.state.init.push(...element_state.init); context.state.after_update.push(...element_state.after_update); @@ -632,10 +627,9 @@ function build_element_attribute_update_assignment( if (attribute.metadata.expression.has_state) { if (has_call) { - state.init.push(build_update(update, attribute.metadata.expression.is_async)); + state.init.push(build_update(update)); } else { state.update.push(update); - state.metadata.update_is_async ||= attribute.metadata.expression.is_async; } return true; } else { @@ -668,10 +662,9 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co if (attribute.metadata.expression.has_state) { if (has_call) { - state.init.push(build_update(update, attribute.metadata.expression.is_async)); + state.init.push(build_update(update)); } else { state.update.push(update); - state.metadata.update_is_async ||= attribute.metadata.expression.is_async; } return true; } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index c3d036072219..ba66fe29d691 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -123,12 +123,7 @@ export function SvelteElement(node, context) { /** @type {Statement[]} */ const inner = inner_context.state.init; if (inner_context.state.update.length > 0) { - inner.push( - build_render_statement( - inner_context.state.update, - inner_context.state.metadata.update_is_async - ) - ); + inner.push(build_render_statement(inner_context.state.update)); } inner.push(...inner_context.state.after_update); inner.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 05ae059ad282..72cc57b068a0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -8,7 +8,7 @@ import { build_template_chunk } from './shared/utils.js'; * @param {ComponentContext} context */ export function TitleElement(node, context) { - const { has_state, is_async, value } = build_template_chunk( + const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), context.visit, context.state @@ -18,12 +18,7 @@ export function TitleElement(node, context) { if (has_state) { context.state.update.push(statement); - context.state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } - context.state.init.push(statement); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 644c0478d25d..0ab47afcbfe3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -94,10 +94,6 @@ export function build_component(node, component_name, context, anchor = context. } for (const attribute of node.attributes) { - if (attribute.type === 'Attribute' || attribute.type === 'SpreadAttribute') { - context.state.metadata.init_is_async ||= attribute.metadata.expression.is_async; - } - if (attribute.type === 'LetDirective') { if (!slot_scope_applies_to_itself) { lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default))); @@ -169,10 +165,11 @@ export function build_component(node, component_name, context, anchor = context. const id = b.id(context.state.scope.generate(attribute.name)); if (attribute.metadata.expression.is_async) { - // TODO parallelise these - context.state.init.push( - b.var(id, b.await(b.call('$.async_derived', b.thunk(arg, true)))) - ); + context.state.metadata.async.push({ + id, + expression: arg + }); + arg = b.call(id); } else { context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 2e746cbf7875..e49dbaedb010 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -83,7 +83,6 @@ export function build_set_attributes( context.state.init.push(b.let(attributes_id)); const update = b.stmt(b.assignment('=', attributes_id, call)); context.state.update.push(update); - context.state.metadata.update_is_async ||= is_async; return true; } @@ -115,7 +114,9 @@ export function build_style_directives( ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) : build_attribute_value(directive.value, context).value; - if (has_call) { + if (is_async) { + throw new Error('TODO'); + } else if (has_call) { const id = b.id(state.scope.generate('style_directive')); state.init.push(b.const(id, create_derived(state, b.thunk(value)))); @@ -133,14 +134,10 @@ export function build_style_directives( ); if (!is_attributes_reactive && has_call) { - state.init.push(build_update(update, is_async)); + state.init.push(build_update(update)); } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); - state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } state.init.push(update); } } @@ -165,7 +162,9 @@ export function build_class_directives( const { has_state, has_call, is_async } = directive.metadata.expression; let value = /** @type {Expression} */ (context.visit(directive.expression)); - if (has_call) { + if (is_async) { + throw new Error('TODO'); + } else if (has_call) { const id = b.id(state.scope.generate('class_directive')); state.init.push(b.const(id, create_derived(state, b.thunk(value)))); @@ -175,14 +174,10 @@ export function build_class_directives( const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); if (!is_attributes_reactive && has_call) { - state.init.push(build_update(update, is_async)); + state.init.push(build_update(update)); } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); - state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } state.init.push(update); } } 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 5744cd51aa95..7674fd1eb234 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 @@ -69,7 +69,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { state.template.push(' '); - const { has_state, has_call, is_async, value } = build_template_chunk(sequence, visit, state); + const { has_state, has_call, value } = build_template_chunk(sequence, visit, state); // if this is a standalone `{expression}`, make sure we handle the case where // no text node was created because the expression was empty during SSR @@ -79,14 +79,10 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_call && !within_bound_contenteditable) { - state.init.push(build_update(update, is_async)); + state.init.push(build_update(update)); } else if (has_state && !within_bound_contenteditable) { state.update.push(update); - state.metadata.update_is_async ||= is_async; } else { - if (is_async) { - throw new Error('TODO top-level await'); - } state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index b8c0f438a108..528119b3fb79 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -14,7 +14,7 @@ import { locator } from '../../../../../state.js'; * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit * @param {ComponentClientTransformState} state - * @returns {{ value: Expression, has_state: boolean, has_call: boolean, is_async: boolean }} + * @returns {{ value: Expression, has_state: boolean, has_call: boolean }} */ export function build_template_chunk(values, visit, state) { /** @type {Expression[]} */ @@ -26,16 +26,15 @@ export function build_template_chunk(values, visit, state) { let has_call = false; let has_state = false; let is_async = false; - let contains_multiple_call_expression = false; + let should_memoize = false; for (const node of values) { if (node.type === 'ExpressionTag') { const metadata = node.metadata.expression; - contains_multiple_call_expression ||= has_call && metadata.has_call; + should_memoize ||= (has_call || is_async) && (metadata.has_call || metadata.is_async); has_call ||= metadata.has_call; has_state ||= metadata.has_state; - is_async ||= metadata.is_async; } } @@ -49,32 +48,26 @@ export function build_template_chunk(values, visit, state) { quasi.value.cooked += node.expression.value + ''; } } else { - if (contains_multiple_call_expression) { - const id = b.id(state.scope.generate('stringified_text')); + const expression = /** @type {Expression} */ (visit(node.expression, state)); + + if (node.metadata.expression.is_async) { + const id = b.id(state.scope.generate('expression')); + state.metadata.async.push({ id, expression: b.logical('??', expression, b.literal('')) }); + + expressions.push(b.call(id)); + } else if (node.metadata.expression.has_call && should_memoize) { + const id = b.id(state.scope.generate('expression')); state.init.push( - b.const( - id, - create_derived( - state, - b.thunk( - b.logical( - '??', - /** @type {Expression} */ (visit(node.expression, state)), - b.literal('') - ), - is_async - ) - ) - ) + b.const(id, create_derived(state, b.thunk(b.logical('??', expression, b.literal(''))))) ); - expressions.push(is_async ? b.await(b.call('$.get', id)) : b.call('$.get', id)); + expressions.push(b.call('$.get', id)); } else if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - return { value: visit(node.expression, state), has_state, has_call, is_async }; + return { value: expression, has_state, has_call }; } else { - expressions.push(b.logical('??', visit(node.expression, state), b.literal(''))); + expressions.push(b.logical('??', expression, b.literal(''))); } quasi = b.quasi('', i + 1 === values.length); @@ -88,28 +81,26 @@ export function build_template_chunk(values, visit, state) { const value = b.template(quasis, expressions); - return { value, has_state, has_call, is_async }; + return { value, has_state, has_call }; } /** * @param {Statement} statement - * @param {boolean} is_async */ -export function build_update(statement, is_async) { +export function build_update(statement) { const body = statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]); - return b.stmt(b.call('$.template_effect', b.thunk(body, is_async))); + return b.stmt(b.call('$.template_effect', b.thunk(body))); } /** * @param {Statement[]} update - * @param {boolean} is_async */ -export function build_render_statement(update, is_async) { +export function build_render_statement(update) { return update.length === 1 - ? build_update(update[0], is_async) - : b.stmt(b.call('$.template_effect', b.thunk(b.block(update), is_async))); + ? build_update(update[0]) + : b.stmt(b.call('$.template_effect', b.thunk(b.block(update)))); } /** diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c2d976c24409..ed2cddbed211 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -285,3 +285,9 @@ export async function suspend(promise) { } }; } + +export function exit() { + set_active_effect(null); + set_active_reaction(null); + set_component_context(null); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 0a17a546213f..c9b259c4dfbb 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -129,7 +129,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, suspend } from './dom/blocks/boundary.js'; +export { boundary, exit, suspend } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, From 0dcc250a00320a49a8119d43f0f363946628fba0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 17 Jan 2025 17:48:51 +0000 Subject: [PATCH 030/211] chore: refactor task microtask dispatching + boundary scheduling --- .changeset/eleven-weeks-dance.md | 5 + .../2-analyze/visitors/SvelteBoundary.js | 2 +- .../client/visitors/SvelteBoundary.js | 5 +- .../src/internal/client/dom/blocks/await.js | 4 +- .../internal/client/dom/blocks/boundary.js | 155 +++++++++++++++--- .../src/internal/client/dom/blocks/each.js | 4 +- .../svelte/src/internal/client/dom/css.js | 6 +- .../client/dom/elements/bindings/input.js | 6 +- .../client/dom/elements/bindings/this.js | 4 +- .../internal/client/dom/elements/events.js | 4 +- .../src/internal/client/dom/elements/misc.js | 4 +- .../client/dom/elements/transitions.js | 4 +- .../svelte/src/internal/client/dom/task.js | 66 +++++--- .../src/internal/client/reactivity/effects.js | 16 +- .../svelte/src/internal/client/runtime.js | 15 +- packages/svelte/tests/animation-helpers.js | 4 +- 16 files changed, 225 insertions(+), 79 deletions(-) create mode 100644 .changeset/eleven-weeks-dance.md diff --git a/.changeset/eleven-weeks-dance.md b/.changeset/eleven-weeks-dance.md new file mode 100644 index 000000000000..c382f76a51f8 --- /dev/null +++ b/.changeset/eleven-weeks-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: refactor task microtask dispatching + boundary scheduling diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index d50cb80cb83e..35af96ba122e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -const valid = ['onerror', 'failed']; +const valid = ['onerror', 'failed', 'pending']; /** * @param {AST.SvelteBoundary} node diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 325485d4c003..48402ccc7517 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -39,7 +39,10 @@ export function SvelteBoundary(node, context) { // Capture the `failed` implicit snippet prop for (const child of node.fragment.nodes) { - if (child.type === 'SnippetBlock' && child.expression.name === 'failed') { + if ( + child.type === 'SnippetBlock' && + (child.expression.name === 'failed' || child.expression.name === 'pending') + ) { // we need to delay the visit of the snippets in case they access a ConstTag that is declared // after the snippets so that the visitor for the const tag can be updated snippets_visits.push(() => { diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 62b2e4dd0cda..788afa1921b3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -13,7 +13,7 @@ import { set_dev_current_component_function } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { queue_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; import { UNINITIALIZED } from '../../../../constants.js'; const PENDING = 0; @@ -148,7 +148,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } else { // Wait a microtask before checking if we should show the pending state as // the promise might have resolved by the next microtask. - queue_micro_task(() => { + queue_post_micro_task(() => { if (!resolved) update(PENDING, true); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 7f4f000dceae..7261d8522fbd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,7 +1,13 @@ /** @import { Effect, TemplateNode, } from '#client' */ import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; -import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; +import { + block, + branch, + destroy_effect, + pause_effect, + resume_effect +} from '../../reactivity/effects.js'; import { active_effect, active_reaction, @@ -20,7 +26,11 @@ import { remove_nodes, set_hydrate_node } from '../hydration.js'; -import { queue_micro_task } from '../task.js'; +import { get_next_sibling } from '../operations.js'; +import { queue_boundary_micro_task } from '../task.js'; + +const ASYNC_INCREMENT = Symbol(); +const ASYNC_DECREMENT = Symbol(); /** * @param {Effect} boundary @@ -49,6 +59,7 @@ function with_boundary(boundary, fn) { * @param {{ * onerror?: (error: unknown, reset: () => void) => void, * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void + * pending?: (anchor: Node) => void * }} props * @param {((anchor: Node) => void)} boundary_fn * @returns {void} @@ -58,14 +69,106 @@ export function boundary(node, props, boundary_fn) { /** @type {Effect} */ var boundary_effect; + /** @type {Effect | null} */ + var async_effect = null; + /** @type {DocumentFragment | null} */ + var async_fragment = null; + var async_count = 0; block(() => { var boundary = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; var is_creating_fallback = false; - // We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {unknown}} */ error) => { + const render_snippet = (/** @type { () => void } */ snippet_fn) => { + with_boundary(boundary, () => { + is_creating_fallback = true; + + try { + boundary_effect = branch(() => { + snippet_fn(); + }); + } catch (error) { + handle_error(error, boundary, null, boundary.ctx); + } + + reset_is_throwing_error(); + is_creating_fallback = false; + }); + }; + + // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field + boundary.fn = (/** @type {unknown} */ input) => { + let pending = props.pending; + + if (input === ASYNC_INCREMENT) { + if (!pending) { + return false; + } + + if (async_count++ === 0) { + queue_boundary_micro_task(() => { + if (async_effect || !boundary_effect) { + return; + } + + var effect = boundary_effect; + async_effect = boundary_effect; + + pause_effect( + async_effect, + () => { + /** @type {TemplateNode | null} */ + var node = effect.nodes_start; + var end = effect.nodes_end; + async_fragment = document.createDocumentFragment(); + + while (node !== null) { + /** @type {TemplateNode | null} */ + var sibling = + node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + node.remove(); + async_fragment.append(node); + node = sibling; + } + }, + false + ); + + render_snippet(() => { + pending(anchor); + }); + }); + } + + return true; + } + + if (input === ASYNC_DECREMENT) { + if (!pending) { + return false; + } + + if (--async_count === 0) { + queue_boundary_micro_task(() => { + if (!async_effect) { + return; + } + if (boundary_effect) { + destroy_effect(boundary_effect); + } + boundary_effect = async_effect; + async_effect = null; + anchor.before(/** @type {DocumentFragment} */ (async_fragment)); + resume_effect(boundary_effect); + }); + } + + return true; + } + + var error = input; var onerror = props.onerror; let failed = props.failed; @@ -96,25 +199,13 @@ export function boundary(node, props, boundary_fn) { } if (failed) { - // Render the `failed` snippet in a microtask - queue_micro_task(() => { - with_boundary(boundary, () => { - is_creating_fallback = true; - - try { - boundary_effect = branch(() => { - failed( - anchor, - () => error, - () => reset - ); - }); - } catch (error) { - handle_error(error, boundary, null, boundary.ctx); - } - - reset_is_throwing_error(); - is_creating_fallback = false; + queue_boundary_micro_task(() => { + render_snippet(() => { + failed( + anchor, + () => error, + () => reset + ); }); }); } @@ -132,3 +223,21 @@ export function boundary(node, props, boundary_fn) { anchor = hydrate_node; } } + +/** + * @param {Effect | null} effect + * @param {typeof ASYNC_INCREMENT | typeof ASYNC_DECREMENT} trigger + */ +export function trigger_async_boundary(effect, trigger) { + var current = effect; + + while (current !== null) { + if ((current.f & BOUNDARY_EFFECT) !== 0) { + // @ts-ignore + if (current.fn(trigger)) { + return; + } + } + current = current.parent; + } +} diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index b17090948ae7..dc4c133de4e9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -34,7 +34,7 @@ import { import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; import { INERT } from '../../constants.js'; -import { queue_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; import { active_effect, active_reaction, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; @@ -470,7 +470,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge } if (is_animated) { - queue_micro_task(() => { + queue_post_micro_task(() => { if (to_animate === undefined) return; for (item of to_animate) { item.a?.apply(); diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index 52be36aa1f46..d4340a07eef6 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -1,5 +1,5 @@ import { DEV } from 'esm-env'; -import { queue_micro_task } from './task.js'; +import { queue_post_micro_task } from './task.js'; import { register_style } from '../dev/css.js'; /** @@ -7,8 +7,8 @@ import { register_style } from '../dev/css.js'; * @param {{ hash: string, code: string }} css */ export function append_styles(anchor, css) { - // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results - queue_micro_task(() => { + // Use `queue_post_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results + queue_post_micro_task(() => { var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index ec123d39681d..b8d4b07c9b7e 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -3,7 +3,7 @@ import { render_effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import * as e from '../../../errors.js'; import { is } from '../../../proxy.js'; -import { queue_micro_task } from '../../task.js'; +import { queue_post_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { is_runes, untrack } from '../../../runtime.js'; @@ -158,14 +158,14 @@ export function bind_group(inputs, group_index, input, get, set = get) { if (!pending.has(binding_group)) { pending.add(binding_group); - queue_micro_task(() => { + queue_post_micro_task(() => { // necessary to maintain binding group order in all insertion scenarios binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1)); pending.delete(binding_group); }); } - queue_micro_task(() => { + queue_post_micro_task(() => { if (hydration_mismatch) { var value; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index 56b0a56e71c4..0ca5039e7c69 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,7 +1,7 @@ import { STATE_SYMBOL } from '../../../constants.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; -import { queue_micro_task } from '../../task.js'; +import { queue_post_micro_task } from '../../task.js'; /** * @param {any} bound_value @@ -49,7 +49,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part return () => { // We cannot use effects in the teardown phase, we we use a microtask instead. - queue_micro_task(() => { + queue_post_micro_task(() => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index f2038f96ada3..4144a13fac66 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -2,7 +2,7 @@ import { teardown } from '../../reactivity/effects.js'; import { define_property, is_array } from '../../../shared/utils.js'; import { hydrating } from '../hydration.js'; -import { queue_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; import { FILENAME } from '../../../../constants.js'; import * as w from '../../warnings.js'; import { @@ -77,7 +77,7 @@ export function create_event(event_name, dom, handler, options) { event_name.startsWith('touch') || event_name === 'wheel' ) { - queue_micro_task(() => { + queue_post_micro_task(() => { dom.addEventListener(event_name, target_handler, options); }); } else { diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index 61e513903f76..dab8e84c32f6 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,6 @@ import { hydrating } from '../hydration.js'; import { clear_text_content, get_first_child } from '../operations.js'; -import { queue_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; /** * @param {HTMLElement} dom @@ -12,7 +12,7 @@ export function autofocus(dom, value) { const body = document.body; dom.autofocus = true; - queue_micro_task(() => { + queue_post_micro_task(() => { if (document.activeElement === body) { dom.focus(); } diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index b3c16cdd080f..0dd17fad9ff4 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -13,7 +13,7 @@ import { should_intro } from '../../render.js'; import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; -import { queue_micro_task } from '../task.js'; +import { queue_post_micro_task } from '../task.js'; /** * @param {Element} element @@ -326,7 +326,7 @@ function animate(element, options, counterpart, t2, on_finish) { var a; var aborted = false; - queue_micro_task(() => { + queue_post_micro_task(() => { if (aborted) return; var o = options({ direction: is_intro ? 'in' : 'out' }); a = animate(element, o, counterpart, t2, on_finish); diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index acb5a5b117f0..8b16b30ebead 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -10,54 +10,70 @@ let is_micro_task_queued = false; let is_idle_task_queued = false; /** @type {Array<() => void>} */ -let current_queued_micro_tasks = []; +let queued_boundary_microtasks = []; /** @type {Array<() => void>} */ -let current_queued_idle_tasks = []; +let queued_post_microtasks = []; +/** @type {Array<() => void>} */ +let queued_idle_tasks = []; -function process_micro_tasks() { - is_micro_task_queued = false; - const tasks = current_queued_micro_tasks.slice(); - current_queued_micro_tasks = []; +export function flush_boundary_micro_tasks() { + const tasks = queued_boundary_microtasks.slice(); + queued_boundary_microtasks = []; run_all(tasks); } -function process_idle_tasks() { - is_idle_task_queued = false; - const tasks = current_queued_idle_tasks.slice(); - current_queued_idle_tasks = []; +export function flush_post_micro_tasks() { + const tasks = queued_post_microtasks.slice(); + queued_post_microtasks = []; run_all(tasks); } +export function flush_idle_tasks() { + if (is_idle_task_queued) { + is_idle_task_queued = false; + const tasks = queued_idle_tasks.slice(); + queued_idle_tasks = []; + run_all(tasks); + } +} + +function flush_all_micro_tasks() { + if (is_micro_task_queued) { + is_micro_task_queued = false; + flush_boundary_micro_tasks(); + flush_post_micro_tasks(); + } +} + /** * @param {() => void} fn */ -export function queue_micro_task(fn) { +export function queue_boundary_micro_task(fn) { if (!is_micro_task_queued) { is_micro_task_queued = true; - queueMicrotask(process_micro_tasks); + queueMicrotask(flush_all_micro_tasks); } - current_queued_micro_tasks.push(fn); + queued_boundary_microtasks.push(fn); } /** * @param {() => void} fn */ -export function queue_idle_task(fn) { - if (!is_idle_task_queued) { - is_idle_task_queued = true; - request_idle_callback(process_idle_tasks); +export function queue_post_micro_task(fn) { + if (!is_micro_task_queued) { + is_micro_task_queued = true; + queueMicrotask(flush_all_micro_tasks); } - current_queued_idle_tasks.push(fn); + queued_post_microtasks.push(fn); } /** - * Synchronously run any queued tasks. + * @param {() => void} fn */ -export function flush_tasks() { - if (is_micro_task_queued) { - process_micro_tasks(); - } - if (is_idle_task_queued) { - process_idle_tasks(); +export function queue_idle_task(fn) { + if (!is_idle_task_queued) { + is_idle_task_queued = true; + request_idle_callback(flush_idle_tasks); } + queued_idle_tasks.push(fn); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 428f69281ba3..abcb558c7f83 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -528,15 +528,20 @@ export function unlink_effect(effect) { * A paused effect does not update, and the DOM subtree becomes inert. * @param {Effect} effect * @param {() => void} [callback] + * @param {boolean} [destroy] */ -export function pause_effect(effect, callback) { +export function pause_effect(effect, callback, destroy = true) { /** @type {TransitionManager[]} */ var transitions = []; - pause_children(effect, transitions, true); + pause_children(effect, transitions, true, destroy); run_out_transitions(transitions, () => { - destroy_effect(effect); + if (destroy) { + destroy_effect(effect); + } else { + execute_effect_teardown(effect); + } if (callback) callback(); }); } @@ -561,8 +566,9 @@ export function run_out_transitions(transitions, fn) { * @param {Effect} effect * @param {TransitionManager[]} transitions * @param {boolean} local + * @param {boolean} [destroy] */ -export function pause_children(effect, transitions, local) { +export function pause_children(effect, transitions, local, destroy = true) { if ((effect.f & INERT) !== 0) return; effect.f ^= INERT; @@ -582,7 +588,7 @@ export function pause_children(effect, transitions, local) { // TODO we don't need to call pause_children recursively with a linked list in place // it's slightly more involved though as we have to account for `transparent` changing // through the tree. - pause_children(child, transitions, transparent ? local : false); + pause_children(child, transitions, transparent ? local : false, destroy); child = sibling; } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index eca5ee94f907..aba037c4a36b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,11 @@ import { DISCONNECTED, BOUNDARY_EFFECT } from './constants.js'; -import { flush_tasks } from './dom/task.js'; +import { + flush_idle_tasks, + flush_boundary_micro_tasks, + flush_post_micro_tasks +} from './dom/task.js'; import { add_owner } from './dev/ownership.js'; import { internal_set, set, source } from './reactivity/sources.js'; import { destroy_derived, execute_derived, update_derived } from './reactivity/deriveds.js'; @@ -737,11 +741,12 @@ function flush_queued_effects(effects) { } } -function process_deferred() { +function flushed_deferred() { is_micro_task_queued = false; if (flush_count > 1001) { return; } + // flush_before_process_microtasks(); const previous_queued_root_effects = queued_root_effects; queued_root_effects = []; flush_queued_root_effects(previous_queued_root_effects); @@ -763,7 +768,7 @@ export function schedule_effect(signal) { if (scheduler_mode === FLUSH_MICROTASK) { if (!is_micro_task_queued) { is_micro_task_queued = true; - queueMicrotask(process_deferred); + queueMicrotask(flushed_deferred); } } @@ -882,7 +887,9 @@ export function flush_sync(fn) { var result = fn?.(); - flush_tasks(); + flush_boundary_micro_tasks(); + flush_post_micro_tasks(); + flush_idle_tasks(); if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js index dcbb06292305..e37c2563af5e 100644 --- a/packages/svelte/tests/animation-helpers.js +++ b/packages/svelte/tests/animation-helpers.js @@ -1,6 +1,6 @@ import { flushSync } from 'svelte'; import { raf as svelte_raf } from 'svelte/internal/client'; -import { queue_micro_task } from '../src/internal/client/dom/task.js'; +import { queue_post_micro_task } from '../src/internal/client/dom/task.js'; export const raf = { animations: new Set(), @@ -132,7 +132,7 @@ class Animation { /** @param {() => {}} fn */ set onfinish(fn) { if (this.#duration === 0) { - queue_micro_task(fn); + queue_post_micro_task(fn); } else { this.#onfinish = () => { fn(); From beaa64f0ded45bbf4e8a98e94e33a2d3dacac634 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 13:12:19 -0500 Subject: [PATCH 031/211] revert some stuff for now --- .../3-transform/client/visitors/RegularElement.js | 3 --- .../3-transform/client/visitors/shared/component.js | 13 ++----------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 458c44d4e62b..21a78de032c4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -648,9 +648,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co state.init.push(b.stmt(b.call('$.template_effect', b.thunk(update.expression)))); return true; } else { - if (attribute.metadata.expression.is_async) { - throw new Error('TODO top-level await'); - } state.init.push(update); return false; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index db607f2f3201..30daab0b7e48 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -172,17 +172,8 @@ export function build_component(node, component_name, context, anchor = context. if (should_wrap_in_derived) { const id = b.id(context.state.scope.generate(attribute.name)); - if (attribute.metadata.expression.is_async) { - context.state.metadata.async.push({ - id, - expression: arg - }); - - arg = b.call(id); - } else { - context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); - arg = b.call('$.get', id); - } + context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); + arg = b.call('$.get', id); } push_prop(b.get(attribute.name, [b.return(arg)])); From 06e61193b12ca59858623587a0e1d72083ea9329 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 13:12:52 -0500 Subject: [PATCH 032/211] revert --- .../phases/3-transform/client/visitors/shared/component.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 30daab0b7e48..9ac0bac12046 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -171,7 +171,6 @@ export function build_component(node, component_name, context, anchor = context. if (should_wrap_in_derived) { const id = b.id(context.state.scope.generate(attribute.name)); - context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); arg = b.call('$.get', id); } From 02c2ca4843ca80270bb4145ac563566886b19ed0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 14:54:17 -0500 Subject: [PATCH 033/211] fix --- .../3-transform/client/transform-client.js | 24 ++++++++--- .../3-transform/client/visitors/Fragment.js | 41 ------------------- 2 files changed, 18 insertions(+), 47 deletions(-) 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 93540db6a71f..0861a7735cec 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 @@ -354,12 +354,6 @@ export function client_component(analysis, options) { const push_args = [b.id('$$props'), b.literal(analysis.runes)]; if (dev) push_args.push(b.id(analysis.name)); - if (analysis.is_async) { - const body = /** @type {ESTree.FunctionDeclaration} */ (template.body[0]); - body.body.body.unshift(...instance.body); - instance.body.length = 0; - } - let component_block = b.block([ ...store_setup, ...legacy_reactive_declarations, @@ -372,6 +366,24 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); + if (analysis.is_async) { + const body = b.function_declaration( + b.id('$$body'), + [b.id('$$anchor'), b.id('$$props')], + component_block + ); + body.async = true; + + state.hoisted.push(body); + + component_block = b.block([ + b.var('fragment', b.call('$.comment')), + b.var('node', b.call('$.first_child', b.id('fragment'))), + b.stmt(b.call(body.id, b.id('node'), b.id('$$props'))), + b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) + ]); + } + if (!analysis.runes) { // Bind static exports to props so that people can access them with bind:x for (const { name, alias } of analysis.exports) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index a4da29743e3d..da65862fd941 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -199,47 +199,6 @@ export function Fragment(node, context) { const async = state.metadata.async.length > 0 || (state.analysis.is_async && context.path.length === 0); - if (async) { - // TODO need to create bookends for hydration to work - return b.block([ - b.function_declaration( - b.id('$$body'), - [b.id('$$anchor')], - b.block([ - b.var( - b.array_pattern(state.metadata.async.map(({ id }) => id)), - b.call( - b.member( - b.await( - b.call( - '$.suspend', - b.call( - 'Promise.all', - b.array( - state.metadata.async.map(({ expression }) => - b.call('$.async_derived', b.thunk(expression, true)) - ) - ) - ) - ) - ), - 'exit' - ) - ) - ), - ...body, - b.stmt(b.call('$.exit')) - ]), - true - ), - - b.var('fragment', b.call('$.comment')), - b.var('node', b.call('$.first_child', b.id('fragment'))), - b.stmt(b.call(b.id('$$body'), b.id('node'))), - b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) - ]); - } - return b.block(body); } From c73de7741262692c650a15e0c10243a99ffbf1f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 16:49:05 -0500 Subject: [PATCH 034/211] fix --- .../2-analyze/visitors/AwaitExpression.js | 22 ++++++++- .../phases/3-transform/client/types.d.ts | 2 +- .../client/visitors/RegularElement.js | 14 ++++-- .../client/visitors/shared/element.js | 23 +++++---- .../client/visitors/shared/utils.js | 49 ++++++++++++------- .../internal/client/reactivity/deriveds.js | 11 ++--- .../src/internal/client/reactivity/effects.js | 22 ++++++--- .../samples/async-expression/_config.js | 2 + 8 files changed, 97 insertions(+), 48 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 97da435d0aaf..b78aa6880cd6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -7,9 +7,27 @@ */ export function AwaitExpression(node, context) { const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; - const blocking = tla || !!context.state.expression; + let suspend = tla; - if (blocking) { + if (context.state.expression) { + // wrap the expression in `(await $.suspend(...)).exit()` if necessary, + // i.e. whether anything could potentially be read _after_ the await + let i = context.path.length; + while (i--) { + const parent = context.path[i]; + + // @ts-expect-error we could probably use a neater/more robust mechanism + if (parent.metadata?.expression === context.state.expression) { + break; + } + + // TODO make this more accurate — we don't need to call suspend + // if this is the last thing that could be read + suspend = true; + } + } + + if (suspend) { if (!context.state.analysis.runes) { throw new Error('TODO runes mode only'); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index a33b07d2b9cc..51c6f428d419 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -53,7 +53,7 @@ export interface ComponentClientTransformState extends ClientTransformState { /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ readonly after_update: Statement[]; /** Expressions used inside the render effect */ - readonly expressions: Expression[]; + readonly expressions: Array<{ id: Identifier; expression: Expression; is_async: boolean }>; /** The HTML template string */ readonly template: Array; readonly locations: SourceLocation[]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 21a78de032c4..32ff9d530e46 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -364,7 +364,11 @@ export function RegularElement(node, context) { // (e.g. `{location}`), set `textContent` programmatically const use_text_content = trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag') && - trimmed.every((node) => node.type === 'Text' || !node.metadata.expression.has_state) && + trimmed.every( + (node) => + node.type === 'Text' || + (!node.metadata.expression.has_state && !node.metadata.expression.is_async) + ) && trimmed.some((node) => node.type === 'ExpressionTag'); if (use_text_content) { @@ -537,8 +541,8 @@ function build_element_attribute_update_assignment( const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; - let { value, has_state } = build_attribute_value(attribute.value, context, (value) => - get_expression_id(state, value) + let { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => + get_expression_id(state, value, is_async) ); if (name === 'autofocus') { @@ -665,8 +669,8 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co */ function build_element_special_value_attribute(element, node_id, attribute, context) { const state = context.state; - const { value, has_state } = build_attribute_value(attribute.value, context, (value) => - get_expression_id(state, value) + const { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => + get_expression_id(state, value, is_async) ); const inner_assignment = b.assignment( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 8fb6b8bdde84..2e126004aed6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -35,8 +35,10 @@ export function build_set_attributes( for (const attribute of attributes) { if (attribute.type === 'Attribute') { - const { value, has_state } = build_attribute_value(attribute.value, context, (value) => - get_expression_id(context.state, value) + const { value, has_state } = build_attribute_value( + attribute.value, + context, + (value, is_async) => get_expression_id(context.state, value, is_async) ); if ( @@ -111,8 +113,8 @@ export function build_style_directives( let value = directive.value === true ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) - : build_attribute_value(directive.value, context, (value) => - get_expression_id(context.state, value) + : build_attribute_value(directive.value, context, (value, is_async) => + get_expression_id(context.state, value, is_async) ).value; const update = b.stmt( @@ -149,11 +151,11 @@ export function build_class_directives( ) { const state = context.state; for (const directive of class_directives) { - const { has_state, has_call } = directive.metadata.expression; + const { has_state, has_call, is_async } = directive.metadata.expression; let value = /** @type {Expression} */ (context.visit(directive.expression)); - if (has_call) { - value = get_expression_id(state, value); + if (has_call || is_async) { + value = get_expression_id(state, value, is_async); } const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); @@ -169,7 +171,7 @@ export function build_class_directives( /** * @param {AST.Attribute['value']} value * @param {ComponentContext} context - * @param {(value: Expression) => Expression} memoize + * @param {(value: Expression, is_async: boolean) => Expression} memoize * @returns {{ value: Expression, has_state: boolean }} */ export function build_attribute_value(value, context, memoize = (value) => value) { @@ -187,7 +189,10 @@ export function build_attribute_value(value, context, memoize = (value) => value let expression = /** @type {Expression} */ (context.visit(chunk.expression)); return { - value: chunk.metadata.expression.has_call ? memoize(expression) : expression, + value: + chunk.metadata.expression.has_call || chunk.metadata.expression.is_async + ? memoize(expression, chunk.metadata.expression.is_async) + : expression, has_state: chunk.metadata.expression.has_state }; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index c4f81274d97e..ac33e9686ce5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -23,16 +23,20 @@ export function memoize_expression(state, value) { /** * * @param {ComponentClientTransformState} state - * @param {Expression} value + * @param {Expression} expression + * @param {boolean} is_async */ -export function get_expression_id(state, value) { +export function get_expression_id(state, expression, is_async) { for (let i = 0; i < state.expressions.length; i += 1) { - if (compare_expressions(state.expressions[i], value)) { - return b.id(`$${i}`); + if (compare_expressions(state.expressions[i].expression, expression)) { + return state.expressions[i].id; } } - return b.id(`$${state.expressions.push(value) - 1}`); + const id = b.id(''); // filled in later + state.expressions.push({ id, expression, is_async }); + + return id; } /** @@ -79,14 +83,14 @@ function compare_expressions(a, b) { * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit * @param {ComponentClientTransformState} state - * @param {(value: Expression) => Expression} memoize - * @returns {{ value: Expression, has_state: boolean }} + * @param {(value: Expression, is_async: boolean) => Expression} memoize + * @returns {{ value: Expression, has_state: boolean, is_async: boolean }} */ export function build_template_chunk( values, visit, state, - memoize = (value) => get_expression_id(state, value) + memoize = (value, is_async) => get_expression_id(state, value, is_async) ) { /** @type {Expression[]} */ const expressions = []; @@ -95,6 +99,7 @@ export function build_template_chunk( const quasis = [quasi]; let has_state = false; + let is_async = false; for (let i = 0; i < values.length; i++) { const node = values[i]; @@ -108,16 +113,17 @@ export function build_template_chunk( } else { let value = /** @type {Expression} */ (visit(node.expression, state)); - has_state ||= node.metadata.expression.has_state; + is_async ||= node.metadata.expression.is_async; + has_state ||= is_async || node.metadata.expression.has_state; - if (node.metadata.expression.has_call) { - value = memoize(value); + if (node.metadata.expression.has_call || node.metadata.expression.is_async) { + value = memoize(value, node.metadata.expression.is_async); } if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - return { value, has_state }; + return { value, has_state, is_async }; } else { let expression = value; // only add nullish coallescence if it hasn't been added already @@ -148,25 +154,34 @@ export function build_template_chunk( const value = b.template(quasis, expressions); - return { value, has_state }; + return { value, has_state, is_async }; } /** * @param {ComponentClientTransformState} state */ export function build_render_statement(state) { + const sync = state.expressions.filter(({ is_async }) => !is_async); + const async = state.expressions.filter(({ is_async }) => is_async); + + const all = [...sync, ...async]; + + for (let i = 0; i < all.length; i += 1) { + all[i].id.name = `$${i}`; + } + return b.stmt( b.call( '$.template_effect', b.arrow( - state.expressions.map((_, i) => b.id(`$${i}`)), + all.map(({ id }) => id), state.update.length === 1 && state.update[0].type === 'ExpressionStatement' ? state.update[0].expression : b.block(state.update) ), - state.expressions.length > 0 && - b.array(state.expressions.map((expression) => b.thunk(expression))), - state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal') + all.length > 0 && b.array(sync.map(({ expression }) => b.thunk(expression))), + async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))), + !state.analysis.runes && sync.length > 0 && b.id('$.derived_safe_equal') ) ); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index eb0fdba469a2..8638ed9ee604 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect } from '#client' */ +/** @import { Derived, Effect, Source } from '#client' */ import { DEV } from 'esm-env'; import { CLEAN, @@ -80,10 +80,10 @@ export function derived(fn) { /** * @template V * @param {() => Promise} fn - * @returns {Promise<() => V>} + * @returns {Promise>} */ /*#__NO_SIDE_EFFECTS__*/ -export async function async_derived(fn) { +export function async_derived(fn) { if (!active_effect) { throw new Error('TODO cannot create unowned async derived'); } @@ -103,10 +103,7 @@ export async function async_derived(fn) { // TODO what happens when the promise rejects? }); - // wait for the initial promise - (await suspend(promise)).exit(); - - return () => get(value); + return promise.then(() => value); } /** diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 1cd390d17a0b..cb09ca06ac17 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -1,4 +1,4 @@ -/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */ +/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager, Value } from '#client' */ import { check_dirtiness, component_context, @@ -44,7 +44,8 @@ import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; -import { derived, destroy_derived } from './deriveds.js'; +import { async_derived, derived, destroy_derived } from './deriveds.js'; +import { suspend } from '../dom/blocks/boundary.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -345,11 +346,18 @@ export function render_effect(fn) { /** * @param {(...expressions: any) => void | (() => void)} fn - * @param {Array<() => any>} thunks - * @returns {Effect} + * @param {Array<() => any>} sync + * @param {Array<() => Promise>} async */ -export function template_effect(fn, thunks = [], d = derived) { - const deriveds = thunks.map(d); +export async function template_effect(fn, sync = [], async = [], d = derived) { + /** @type {Value[]} */ + const deriveds = sync.map(d); + + if (async.length > 0) { + const async_deriveds = (await suspend(Promise.all(async.map(async_derived)))).exit(); + deriveds.push(...async_deriveds); + } + const effect = () => fn(...deriveds.map(get)); if (DEV) { @@ -358,7 +366,7 @@ export function template_effect(fn, thunks = [], d = derived) { }); } - return block(effect); + block(effect); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js index 5f85050d9b0e..26333c05fc3b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -19,6 +19,8 @@ export default test({ async test({ assert, target }) { d.resolve('hello'); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

hello

'); } From 085cdbadd6b4187d662ca6b1e1ba7ef7497c1fdf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 16:59:42 -0500 Subject: [PATCH 035/211] fix --- .../svelte/src/internal/client/reactivity/deriveds.js | 9 ++++++--- .../runtime-runes/samples/async-attribute/_config.js | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 8638ed9ee604..448db00b04fc 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -27,7 +27,7 @@ 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 { suspend } from '../dom/blocks/boundary.js'; +import { exit, suspend } from '../dom/blocks/boundary.js'; /** * @template V @@ -94,9 +94,12 @@ export function async_derived(fn) { render_effect(() => { const current = (promise = fn()); - promise.then((v) => { + suspend(promise).then((v) => { if (promise === current) { - internal_set(value, v); + internal_set(value, v.exit()); + + // TODO at the very least the naming is weird here + exit(); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js index a8df1b04a9a6..b8a450b33858 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -19,11 +19,14 @@ export default test({ async test({ assert, target, component }) { d.resolve('cool'); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

hello

'); d = deferred(); component.promise = d.promise; + await tick(); assert.htmlEqual(target.innerHTML, '

pending

'); d.resolve('neat'); From 4f78f64df5e5423dcb959ab7a586c1ba7e36c5d0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 18:42:20 -0500 Subject: [PATCH 036/211] fix --- .../src/internal/client/reactivity/effects.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index cb09ca06ac17..b9435b510855 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -349,15 +349,21 @@ export function render_effect(fn) { * @param {Array<() => any>} sync * @param {Array<() => Promise>} async */ -export async function template_effect(fn, sync = [], async = [], d = derived) { - /** @type {Value[]} */ - const deriveds = sync.map(d); - +export function template_effect(fn, sync = [], async = [], d = derived) { if (async.length > 0) { - const async_deriveds = (await suspend(Promise.all(async.map(async_derived)))).exit(); - deriveds.push(...async_deriveds); + suspend(Promise.all(async.map(async_derived))).then((result) => { + create_template_effect(fn, [...sync.map(d), ...result.exit()]); + }); + } else { + create_template_effect(fn, sync.map(d)); } +} +/** + * @param {(...expressions: any) => void | (() => void)} fn + * @param {Value[]} deriveds + */ +function create_template_effect(fn, deriveds) { const effect = () => fn(...deriveds.map(get)); if (DEV) { From e15eae86b3f7d51219c7bcdcb50a7572824a15e8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 19:34:37 -0500 Subject: [PATCH 037/211] WIP --- .../client/visitors/VariableDeclaration.js | 15 +++++++++++++-- .../client/visitors/shared/declarations.js | 12 ------------ .../src/internal/client/dom/blocks/boundary.js | 3 +++ 3 files changed, 16 insertions(+), 14 deletions(-) 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 b9a987015f06..244e9011f3fe 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 @@ -167,8 +167,19 @@ export function VariableDeclaration(node, context) { declarations.push( b.declarator( declarator.id, - b.await( - b.call('$.async_derived', rune === '$derived.by' ? value : b.thunk(value, true)) + b.call( + b.member( + b.await( + b.call( + '$.suspend', + b.call( + '$.async_derived', + rune === '$derived.by' ? value : b.thunk(value, true) + ) + ) + ), + 'exit' + ) ) ) ); 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 02172be5f5d1..dd46b8e3671c 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 @@ -17,18 +17,6 @@ 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/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 6036746b7f9d..6a025baa6003 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -244,6 +244,9 @@ export function trigger_async_boundary(effect, trigger) { } } +// TODO separate this stuff out — suspending and context preservation should +// be distinct concepts + /** * @template T * @param {Promise} promise From 9348259879776515282562fd5a11c4f04970c7ab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Jan 2025 20:24:33 -0500 Subject: [PATCH 038/211] WIP --- .../samples/async-derived/Child.svelte | 7 +++++ .../samples/async-derived/_config.js | 28 +++++++++++++++++++ .../samples/async-derived/main.svelte | 13 +++++++++ 3 files changed, 48 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte new file mode 100644 index 000000000000..888d2a4e9965 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte @@ -0,0 +1,7 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js new file mode 100644 index 000000000000..7fe48491f7cf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise, + num: 1 + }; + }, + + async test({ assert, target }) { + d.resolve('hello'); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

42

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte new file mode 100644 index 000000000000..3b56c3a316b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
From 39ed1113678f93b8cab303e13f593ba9ff4c6668 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 09:23:56 -0500 Subject: [PATCH 039/211] return is_async from build_template_chunk --- .../phases/3-transform/client/visitors/shared/element.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 2e126004aed6..06c32333dc6d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -172,18 +172,18 @@ export function build_class_directives( * @param {AST.Attribute['value']} value * @param {ComponentContext} context * @param {(value: Expression, is_async: boolean) => Expression} memoize - * @returns {{ value: Expression, has_state: boolean }} + * @returns {{ value: Expression, has_state: boolean, is_async: boolean }} */ export function build_attribute_value(value, context, memoize = (value) => value) { if (value === true) { - return { value: b.literal(true), has_state: false }; + return { value: b.literal(true), has_state: false, is_async: false }; } if (!Array.isArray(value) || value.length === 1) { const chunk = Array.isArray(value) ? value[0] : value; if (chunk.type === 'Text') { - return { value: b.literal(chunk.data), has_state: false }; + return { value: b.literal(chunk.data), has_state: false, is_async: false }; } let expression = /** @type {Expression} */ (context.visit(chunk.expression)); @@ -193,7 +193,8 @@ export function build_attribute_value(value, context, memoize = (value) => value chunk.metadata.expression.has_call || chunk.metadata.expression.is_async ? memoize(expression, chunk.metadata.expression.is_async) : expression, - has_state: chunk.metadata.expression.has_state + has_state: chunk.metadata.expression.has_state, + is_async: chunk.metadata.expression.is_async }; } From 093a3bfd2cfd39e1544058f8c8a974b26c08a51b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 09:24:24 -0500 Subject: [PATCH 040/211] test --- .../samples/async-prop/Child.svelte | 5 +++ .../samples/async-prop/_config.js | 37 +++++++++++++++++++ .../samples/async-prop/main.svelte | 13 +++++++ 3 files changed, 55 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-prop/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte new file mode 100644 index 000000000000..00f8df7c0a89 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte @@ -0,0 +1,5 @@ + + +

{num}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js new file mode 100644 index 000000000000..91daba25a933 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('hello'); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('hello again'); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello again

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte new file mode 100644 index 000000000000..cb5d00b3d374 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
From 5ae974f47daaa0f8ae381231b3e67a6a7557d3df Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 14:35:22 -0500 Subject: [PATCH 041/211] separate sync from async expressions --- .../3-transform/client/transform-client.js | 1 + .../phases/3-transform/client/types.d.ts | 9 ++++++- .../3-transform/client/visitors/Fragment.js | 1 + .../client/visitors/RegularElement.js | 4 +-- .../client/visitors/SvelteElement.js | 1 + .../client/visitors/shared/element.js | 19 +++++++++++--- .../client/visitors/shared/utils.js | 25 +++++++++---------- 7 files changed, 40 insertions(+), 20 deletions(-) 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 0861a7735cec..c1c8170e301e 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 @@ -175,6 +175,7 @@ export function client_component(analysis, options) { init: /** @type {any} */ (null), update: /** @type {any} */ (null), expressions: /** @type {any} */ (null), + async_expressions: /** @type {any} */ (null), after_update: /** @type {any} */ (null), template: /** @type {any} */ (null), locations: /** @type {any} */ (null) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 51c6f428d419..9cfcd718c553 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -53,7 +53,9 @@ export interface ComponentClientTransformState extends ClientTransformState { /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ readonly after_update: Statement[]; /** Expressions used inside the render effect */ - readonly expressions: Array<{ id: Identifier; expression: Expression; is_async: boolean }>; + readonly expressions: Array<{ id: Identifier; expression: Expression }>; + /** Expressions used inside the render effect */ + readonly async_expressions: Array<{ id: Identifier; expression: Expression }>; /** The HTML template string */ readonly template: Array; readonly locations: SourceLocation[]; @@ -113,3 +115,8 @@ export type ComponentVisitors = import('zimmerframe').Visitors< AST.SvelteNode, ComponentClientTransformState >; + +export interface MemoizedExpression { + id: Identifier; + expression: Expression; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index da65862fd941..2d1543519988 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -64,6 +64,7 @@ export function Fragment(node, context) { init: [], update: [], expressions: [], + async_expressions: [], after_update: [], template: [], locations: [], diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 3c306b241f0d..7c22f3c7bc9b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -543,7 +543,7 @@ function build_element_attribute_update_assignment( let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => metadata.has_call || metadata.is_async - ? get_expression_id(state, value, metadata.is_async) + ? get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value) : value ); @@ -673,7 +673,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont const state = context.state; const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => metadata.has_call || metadata.is_async - ? get_expression_id(state, value, metadata.is_async) + ? get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value) : value ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index e27528365518..ccf08dc4238e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -48,6 +48,7 @@ export function SvelteElement(node, context) { init: [], update: [], expressions: [], + async_expressions: [], after_update: [] } }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 097b3093455f..79cc8f531cb1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -40,7 +40,10 @@ export function build_set_attributes( context, (value, metadata) => metadata.has_call || metadata.is_async - ? get_expression_id(context.state, value, metadata.is_async) + ? get_expression_id( + metadata.is_async ? context.state.async_expressions : context.state.expressions, + value + ) : value ); @@ -64,7 +67,12 @@ export function build_set_attributes( let value = /** @type {Expression} */ (context.visit(attribute)); if (attribute.metadata.expression.has_call || attribute.metadata.expression.is_async) { - value = get_expression_id(context.state, value, attribute.metadata.expression.is_async); + value = get_expression_id( + attribute.metadata.expression.is_async + ? context.state.async_expressions + : context.state.expressions, + value + ); } values.push(b.spread(value)); @@ -117,7 +125,10 @@ export function build_style_directives( ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) : build_attribute_value(directive.value, context, (value, metadata) => metadata.has_call || metadata.is_async - ? get_expression_id(context.state, value, metadata.is_async) + ? get_expression_id( + metadata.is_async ? context.state.async_expressions : context.state.expressions, + value + ) : value ).value; @@ -159,7 +170,7 @@ export function build_class_directives( let value = /** @type {Expression} */ (context.visit(directive.expression)); if (has_call || is_async) { - value = get_expression_id(state, value, is_async); + value = get_expression_id(is_async ? state.async_expressions : state.expressions, value); } const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 2bfbc5ff8af6..077ced10c221 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,6 +1,6 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ +/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Super } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ -/** @import { ComponentClientTransformState } from '../../types' */ +/** @import { ComponentClientTransformState, MemoizedExpression } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; @@ -22,19 +22,18 @@ export function memoize_expression(state, value) { /** * - * @param {ComponentClientTransformState} state + * @param {MemoizedExpression[]} expressions * @param {Expression} expression - * @param {boolean} is_async */ -export function get_expression_id(state, expression, is_async) { - for (let i = 0; i < state.expressions.length; i += 1) { - if (compare_expressions(state.expressions[i].expression, expression)) { - return state.expressions[i].id; +export function get_expression_id(expressions, expression) { + for (let i = 0; i < expressions.length; i += 1) { + if (compare_expressions(expressions[i].expression, expression)) { + return expressions[i].id; } } - const id = b.id(''); // filled in later - state.expressions.push({ id, expression, is_async }); + const id = b.id('~'); // filled in later + expressions.push({ id, expression }); return id; } @@ -92,7 +91,7 @@ export function build_template_chunk( state, memoize = (value, metadata) => metadata.has_call || metadata.is_async - ? get_expression_id(state, value, metadata.is_async) + ? get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value) : value ) { /** @type {Expression[]} */ @@ -163,8 +162,8 @@ export function build_template_chunk( * @param {ComponentClientTransformState} state */ export function build_render_statement(state) { - const sync = state.expressions.filter(({ is_async }) => !is_async); - const async = state.expressions.filter(({ is_async }) => is_async); + const sync = state.expressions; + const async = state.async_expressions; const all = [...sync, ...async]; From c34e44f7812b15f94990213da13d770f9214c832 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 15:23:49 -0500 Subject: [PATCH 042/211] async props --- .../client/visitors/shared/component.js | 81 +++++++++++++++---- .../src/internal/client/dom/blocks/async.js | 17 ++++ packages/svelte/src/internal/client/index.js | 1 + .../samples/async-prop/Child.svelte | 4 +- .../samples/async-prop/_config.js | 4 +- 5 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 packages/svelte/src/internal/client/dom/blocks/async.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 15e4f68e9e49..55f632e53054 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,13 +1,19 @@ /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext } from '../../types.js' */ +/** @import { ComponentContext, MemoizedExpression } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js'; +import { + build_bind_this, + get_expression_id, + memoize_expression, + validate_binding +} from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; +import { create_derived } from '../../utils.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node @@ -40,6 +46,12 @@ export function build_component(node, component_name, context, anchor = context. /** @type {Record} */ const events = {}; + /** @type {MemoizedExpression[]} */ + const expressions = []; + + /** @type {MemoizedExpression[]} */ + const async_expressions = []; + /** @type {Property[]} */ const custom_css_props = []; @@ -115,16 +127,21 @@ export function build_component(node, component_name, context, anchor = context. (events[attribute.name] ||= []).push(handler); } else if (attribute.type === 'SpreadAttribute') { const expression = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_state) { - let value = expression; - if (attribute.metadata.expression.has_call) { - const id = b.id(context.state.scope.generate('spread_element')); - context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); - value = b.call('$.get', id); - } - - props_and_spreads.push(b.thunk(value)); + if (attribute.metadata.expression.has_state) { + props_and_spreads.push( + b.thunk( + attribute.metadata.expression.is_async || attribute.metadata.expression.has_call + ? b.call( + '$.get', + get_expression_id( + attribute.metadata.expression.is_async ? async_expressions : expressions, + expression + ) + ) + : expression + ) + ); } else { props_and_spreads.push(expression); } @@ -133,10 +150,15 @@ export function build_component(node, component_name, context, anchor = context. custom_css_props.push( b.init( attribute.name, - build_attribute_value(attribute.value, context, (value, metadata) => + build_attribute_value(attribute.value, context, (value, metadata) => { // TODO put the derived in the local block - metadata.has_call ? memoize_expression(context.state, value) : value - ).value + return metadata.has_call || metadata.is_async + ? b.call( + '$.get', + get_expression_id(metadata.is_async ? async_expressions : expressions, value) + ) + : value; + }).value ) ); continue; @@ -154,7 +176,7 @@ export function build_component(node, component_name, context, anchor = context. attribute.value, context, (value, metadata) => { - if (!metadata.has_state) return value; + if (!metadata.has_state && !metadata.is_async) return value; // When we have a non-simple computation, anything other than an Identifier or Member expression, // then there's a good chance it needs to be memoized to avoid over-firing when read within the @@ -167,7 +189,12 @@ export function build_component(node, component_name, context, anchor = context. ); }); - return should_wrap_in_derived ? memoize_expression(context.state, value) : value; + return should_wrap_in_derived + ? b.call( + '$.get', + get_expression_id(metadata.is_async ? async_expressions : expressions, value) + ) + : value; } ); @@ -420,7 +447,12 @@ export function build_component(node, component_name, context, anchor = context. }; } - const statements = [...snippet_declarations]; + const statements = [ + ...snippet_declarations, + ...expressions.map((memo) => + b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ) + ]; if (node.type === 'SvelteComponent') { const prev = fn; @@ -457,5 +489,20 @@ export function build_component(node, component_name, context, anchor = context. statements.push(b.stmt(fn(anchor))); } + [...async_expressions, ...expressions].forEach((memo, i) => { + memo.id.name = `$${i}`; + }); + + if (async_expressions.length > 0) { + return b.stmt( + b.call( + '$.async', + anchor, + b.array(async_expressions.map(({ expression }) => b.thunk(expression, true))), + b.arrow([b.id('$$anchor'), ...async_expressions.map(({ id }) => id)], b.block(statements)) + ) + ); + } + return statements.length > 1 ? b.block(statements) : statements[0]; } diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js new file mode 100644 index 000000000000..0ffeb0591b1c --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -0,0 +1,17 @@ +/** @import { TemplateNode, Value } from '#client' */ + +import { async_derived } from '../../reactivity/deriveds.js'; +import { suspend } from './boundary.js'; + +/** + * @param {TemplateNode} node + * @param {Array<() => Promise>} expressions + * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn + */ +export function async(node, expressions, fn) { + // TODO handle hydration + + suspend(Promise.all(expressions.map(async_derived))).then((result) => { + fn(node, ...result.exit()); + }); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c9b259c4dfbb..842343a11932 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -14,6 +14,7 @@ export { export { check_target, legacy_api } from './dev/legacy.js'; export { trace } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; +export { async } from './dom/blocks/async.js'; export { await_block as await } from './dom/blocks/await.js'; export { if_block as if } from './dom/blocks/if.js'; export { key_block as key } from './dom/blocks/key.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte index 00f8df7c0a89..85d212b1a835 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte @@ -1,5 +1,5 @@ -

{num}

+

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js index 91daba25a933..24882c56cd16 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -22,7 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); - assert.htmlEqual(target.innerHTML, '

hello

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d = deferred(); component.promise = d.promise; @@ -32,6 +32,6 @@ export default test({ d.resolve('hello again'); await Promise.resolve(); await tick(); - assert.htmlEqual(target.innerHTML, '

hello again

'); + assert.htmlEqual(target.innerHTML, '

hello again

'); } }); From ed348c6cab3c2f70edfb9b3a821a8bb50a395230 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 16:07:28 -0500 Subject: [PATCH 043/211] if blocks --- .../src/compiler/phases/1-parse/state/tag.js | 10 ++++- .../phases/2-analyze/visitors/IfBlock.js | 8 +++- .../3-transform/client/visitors/IfBlock.js | 22 ++++++++++- .../svelte/src/compiler/types/template.d.ts | 3 ++ .../runtime-runes/samples/async-if/_config.js | 37 +++++++++++++++++++ .../samples/async-if/main.svelte | 15 ++++++++ 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if/main.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 95d7d006779c..0d0176ac85cc 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -60,7 +60,10 @@ function open(parser) { end: -1, test: read_expression(parser), consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.allow_whitespace(); @@ -441,7 +444,10 @@ function next(parser) { elseif: true, test: expression, consequent: create_fragment(), - alternate: null + alternate: null, + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(child); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js index a65771bcfca9..dcdae3587f63 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js @@ -17,5 +17,11 @@ export function IfBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.test, { + ...context.state, + expression: node.metadata.expression + }); + + context.visit(node.consequent); + if (node.alternate) context.visit(node.alternate); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index d658f9eaf819..b354a8877b3d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -24,6 +24,11 @@ export function IfBlock(node, context) { statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate))); } + const { is_async } = node.metadata.expression; + + const expression = /** @type {Expression} */ (context.visit(node.test)); + const test = is_async ? b.call('$.get', b.id('$$condition')) : expression; + /** @type {Expression[]} */ const args = [ context.state.node, @@ -31,7 +36,7 @@ export function IfBlock(node, context) { [b.id('$$render')], b.block([ b.if( - /** @type {Expression} */ (context.visit(node.test)), + test, b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), alternate_id ? b.stmt( @@ -74,5 +79,18 @@ export function IfBlock(node, context) { statements.push(b.stmt(b.call('$.if', ...args))); - context.state.init.push(b.block(statements)); + if (is_async) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array([b.thunk(expression, true)]), + b.arrow([context.state.node, b.id('$$condition')], b.block(statements)) + ) + ) + ); + } else { + context.state.init.push(b.block(statements)); + } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index fb609668957d..f2b2c4629a8b 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -434,6 +434,9 @@ export namespace AST { test: Expression; consequent: Fragment; alternate: Fragment | null; + metadata: { + expression: ExpressionMetadata; + }; } /** An `{#await ...}` block */ diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js new file mode 100644 index 000000000000..286595a9778e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve(true); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

yes

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve(false); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

no

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte new file mode 100644 index 000000000000..baed33a76e6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte @@ -0,0 +1,15 @@ + + + + {#if await promise} +

yes

+ {:else} +

no

+ {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
From 255eec7fff27026a9392c7f90d5feb9f05739fe7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 16:31:50 -0500 Subject: [PATCH 044/211] each test --- .../samples/async-each/_config.js | 37 +++++++++++++++++++ .../samples/async-each/main.svelte | 13 +++++++ 2 files changed, 50 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js new file mode 100644 index 000000000000..b50cb1969ea4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve(['a', 'b', 'c']); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

a

b

c

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve(['d', 'e', 'f']); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

d

e

f

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte new file mode 100644 index 000000000000..9b59d57b055a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte @@ -0,0 +1,13 @@ + + + + {#each await promise as item} +

{item}

+ {/each} + + {#snippet pending()} +

pending

+ {/snippet} +
From 18b902344c16c23c22a456ba57243416c363a43a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 20:56:52 -0500 Subject: [PATCH 045/211] each blocks --- .../3-transform/client/visitors/EachBlock.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 9f70981205a1..16bca733d474 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -283,11 +283,15 @@ export function EachBlock(node, context) { ); } + const { is_async } = node.metadata.expression; + + const thunk = each_node_meta.array_name ?? b.thunk(collection, is_async); + /** @type {Expression[]} */ const args = [ context.state.node, b.literal(flags), - each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), + is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : thunk, key_function, b.arrow( uses_index ? [b.id('$$anchor'), item, index] : [b.id('$$anchor'), item], @@ -301,7 +305,23 @@ export function EachBlock(node, context) { ); } - context.state.init.push(b.stmt(b.call('$.each', ...args))); + if (is_async) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array([thunk]), + b.arrow( + [context.state.node, b.id('$$collection')], + b.block([b.stmt(b.call('$.each', ...args))]) + ) + ) + ) + ); + } else { + context.state.init.push(b.stmt(b.call('$.each', ...args))); + } } /** From 364f45a08e1ae8c3e9d0839461b8dc0295e9ac65 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 21:05:39 -0500 Subject: [PATCH 046/211] key blocks --- .../src/compiler/phases/1-parse/state/tag.js | 5 +- .../phases/2-analyze/visitors/KeyBlock.js | 7 ++- .../3-transform/client/visitors/KeyBlock.js | 31 +++++++++-- .../svelte/src/compiler/types/template.d.ts | 5 ++ .../samples/async-key/_config.js | 51 +++++++++++++++++++ .../samples/async-key/main.svelte | 13 +++++ 6 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-key/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-key/main.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 0d0176ac85cc..78820d0fa10e 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -326,7 +326,10 @@ function open(parser) { start, end: -1, expression, - fragment: create_fragment() + fragment: create_fragment(), + metadata: { + expression: create_expression_metadata() + } }); parser.stack.push(block); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js index 88bb6a98e748..d0dcf8e15c51 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js @@ -16,5 +16,10 @@ export function KeyBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { + ...context.state, + expression: node.metadata.expression + }); + + context.visit(node.fragment); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index a013827f60bd..6a95a94ddf11 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -13,7 +13,32 @@ export function KeyBlock(node, context) { const key = /** @type {Expression} */ (context.visit(node.expression)); const body = /** @type {Expression} */ (context.visit(node.fragment)); - context.state.init.push( - b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) - ); + if (node.metadata.expression.is_async) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array([b.thunk(key, true)]), + b.arrow( + [context.state.node, b.id('$$key')], + b.block([ + b.stmt( + b.call( + '$.key', + context.state.node, + b.thunk(b.call('$.get', b.id('$$key'))), + b.arrow([b.id('$$anchor')], body) + ) + ) + ]) + ) + ) + ) + ); + } else { + context.state.init.push( + b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) + ); + } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index f2b2c4629a8b..c16c161e8639 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -434,6 +434,7 @@ export namespace AST { test: Expression; consequent: Fragment; alternate: Fragment | null; + /** @internal */ metadata: { expression: ExpressionMetadata; }; @@ -457,6 +458,10 @@ export namespace AST { type: 'KeyBlock'; expression: Expression; fragment: Fragment; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } export interface SnippetBlock extends BaseNode { diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js new file mode 100644 index 000000000000..5282bbd739a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve(1); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + const h1 = target.querySelector('h1'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve(1); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + assert.equal(target.querySelector('h1'), h1); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve(2); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + assert.notEqual(target.querySelector('h1'), h1); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte new file mode 100644 index 000000000000..7cac0f854240 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte @@ -0,0 +1,13 @@ + + + + {#key await promise} +

hello

+ {/key} + + {#snippet pending()} +

pending

+ {/snippet} +
From 96942400bd350449ff4e4f34edd13a4e370784c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 21:45:34 -0500 Subject: [PATCH 047/211] basic SSR --- .../98-reference/.generated/shared-errors.md | 6 ++++ .../svelte/messages/shared-errors/errors.md | 6 ++++ .../client/visitors/AwaitExpression.js | 4 +-- .../3-transform/server/transform-server.js | 2 ++ .../server/visitors/AwaitExpression.js | 17 ++++++++++ .../server/visitors/SvelteBoundary.js | 31 ++++++++++++++++--- packages/svelte/src/internal/server/index.js | 2 ++ packages/svelte/src/internal/shared/errors.js | 15 +++++++++ 8 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 0102aafcbca1..df49facef7bf 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -1,5 +1,11 @@ +### await_outside_boundary + +``` +Cannot await outside a `` with a `pending` snippet +``` + ### invalid_default_snippet ``` diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 8b4c61303a07..e50c0d922bb4 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -1,3 +1,9 @@ +## await_outside_boundary + +> Cannot await outside a `` with a `pending` snippet + +TODO + ## invalid_default_snippet > Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead 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 index a9486fd8c829..48a3bfa584f5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -1,10 +1,10 @@ /** @import { AwaitExpression, Expression } from 'estree' */ -/** @import { ComponentContext } from '../types' */ +/** @import { Context } from '../types' */ import * as b from '../../../../utils/builders.js'; /** * @param {AwaitExpression} node - * @param {ComponentContext} context + * @param {Context} context */ export function AwaitExpression(node, context) { const suspend = context.state.analysis.suspenders.has(node); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 982b75e12f53..9aa2b4061b95 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -10,6 +10,7 @@ import { dev, filename } from '../../../state.js'; import { render_stylesheet } from '../css/index.js'; import { AssignmentExpression } from './visitors/AssignmentExpression.js'; import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { AwaitExpression } from './visitors/AwaitExpression.js'; import { CallExpression } from './visitors/CallExpression.js'; import { ClassBody } from './visitors/ClassBody.js'; import { Component } from './visitors/Component.js'; @@ -44,6 +45,7 @@ import { SvelteBoundary } from './visitors/SvelteBoundary.js'; const global_visitors = { _: set_scope, AssignmentExpression, + AwaitExpression, CallExpression, ClassBody, ExpressionStatement, diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js new file mode 100644 index 000000000000..f729c9ca9b44 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js @@ -0,0 +1,17 @@ +/** @import { AwaitExpression } from 'estree' */ +/** @import { ComponentContext } from '../types.js' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {AwaitExpression} node + * @param {ComponentContext} context + */ +export function AwaitExpression(node, context) { + const suspend = context.state.analysis.suspenders.has(node); + + if (!suspend) { + return context.next(); + } + + return b.call('$.await_outside_boundary'); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 0d54feee11b3..7f9054553195 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -1,17 +1,38 @@ -/** @import { BlockStatement } from 'estree' */ +/** @import { BlockStatement, Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js'; import * as b from '../../../../utils/builders.js'; +import { build_attribute_value } from './shared/utils.js'; /** * @param {AST.SvelteBoundary} node * @param {ComponentContext} context */ export function SvelteBoundary(node, context) { - context.state.template.push( - b.literal(BLOCK_OPEN), - /** @type {BlockStatement} */ (context.visit(node.fragment)), - b.literal(BLOCK_CLOSE) + context.state.template.push(b.literal(BLOCK_OPEN)); + + // if this has a `pending` snippet, render it + const pending_attribute = /** @type {AST.Attribute} */ ( + node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending') + ); + + const pending_snippet = /** @type {AST.SnippetBlock} */ ( + node.fragment.nodes.find( + (node) => node.type === 'SnippetBlock' && node.expression.name === 'pending' + ) ); + + if (pending_attribute) { + const value = build_attribute_value(pending_attribute.value, context, false, true); + context.state.template.push(b.call(value, b.id('$$payload'))); + } else if (pending_snippet) { + context.state.template.push( + /** @type {BlockStatement} */ (context.visit(pending_snippet.body)) + ); + } else { + context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment))); + } + + context.state.template.push(b.literal(BLOCK_CLOSE)); } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 89b3c33df887..609b54804b49 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -545,3 +545,5 @@ export { } from '../shared/validate.js'; export { escape_html as escape }; + +export { await_outside_boundary } from '../shared/errors.js'; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 26d6822cdb29..c709c431ef5d 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -62,4 +62,19 @@ export function svelte_element_invalid_this_value() { } else { throw new Error(`https://svelte.dev/e/svelte_element_invalid_this_value`); } +} + +/** + * Cannot await outside a `` with a `pending` snippet + * @returns {never} + */ +export function await_outside_boundary() { + if (DEV) { + const error = new Error(`await_outside_boundary\nCannot await outside a \`\` with a \`pending\` snippet\nhttps://svelte.dev/e/await_outside_boundary`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/await_outside_boundary`); + } } \ No newline at end of file From d33c8ae4fe72df7eac76ef955b932ad3d45cd076 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 22:14:32 -0500 Subject: [PATCH 048/211] start working on hydration --- .../client/visitors/shared/element.js | 2 +- .../internal/client/dom/blocks/boundary.js | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 79cc8f531cb1..c61174d10ed8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -205,7 +205,7 @@ export function build_attribute_value(value, context, memoize = (value) => value return { value: memoize(expression, chunk.metadata.expression), - has_state: chunk.metadata.expression.has_state + has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.is_async }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 6a025baa6003..9f7ce93974dd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -217,7 +217,24 @@ export function boundary(node, props, boundary_fn) { hydrate_next(); } - boundary_effect = branch(() => boundary_fn(anchor)); + const pending = props.pending; + + if (hydrating && pending) { + boundary_effect = branch(() => pending(anchor)); + + // ...now what? we need to start rendering `boundary_fn` offscreen, + // and either insert the resulting fragment (if nothing suspends) + // or keep the pending effect alive until it unsuspends. + // not exactly sure how to do that. + + // future work: when we have some form of async SSR, we will + // need to use hydration boundary comments to report whether + // the pending or main block was rendered for a given + // boundary, and hydrate accordingly + } else { + boundary_effect = branch(() => boundary_fn(anchor)); + } + reset_is_throwing_error(); }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); From 28842f463b9bea73735ad6dfbd8c1a4d41a0aea8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 22:24:56 -0500 Subject: [PATCH 049/211] update test --- .../samples/async-derived/_config.js | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index 7fe48491f7cf..0a18aa9b2ca0 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -17,12 +17,33 @@ export default test({ }; }, - async test({ assert, target }) { - d.resolve('hello'); + async test({ assert, target, component }) { + d.resolve(42); + await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

42

'); + + component.num = 2; + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

84

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve(43); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

86

'); } }); From 5f5375a3f1db31eeb32430f2666d3108e325d85a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 10:37:50 +0000 Subject: [PATCH 050/211] fix leakage of context --- .../3-transform/client/visitors/AwaitExpression.js | 2 +- .../svelte/src/internal/client/dom/blocks/boundary.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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 index 48a3bfa584f5..25325ab8b0c7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -18,7 +18,7 @@ export function AwaitExpression(node, context) { b.await( b.call( '$.suspend', - node.argument && /** @type {Expression} */ (context.visit(node.argument)) + node.argument && b.thunk(/** @type {Expression} */ (context.visit(node.argument))) ) ), 'exit' diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 9f7ce93974dd..2ead0aed532f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -266,10 +266,10 @@ export function trigger_async_boundary(effect, trigger) { /** * @template T - * @param {Promise} promise + * @param {() => Promise | Promise} input * @returns {Promise<{ exit: () => T }>} */ -export async function suspend(promise) { +export async function suspend(input) { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; @@ -290,6 +290,12 @@ export async function suspend(promise) { // @ts-ignore boundary?.fn(ASYNC_INCREMENT); + const promise = typeof input === 'function' ? input() : input; + // Ensure we reset the context back so it doesn't leak + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); + const value = await promise; return { From fae03532b85fbf1fdcc00549d8023762b21ee03c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 10:50:15 +0000 Subject: [PATCH 051/211] revert --- .../3-transform/client/visitors/AwaitExpression.js | 2 +- .../svelte/src/internal/client/dom/blocks/boundary.js | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) 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 index 25325ab8b0c7..48a3bfa584f5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -18,7 +18,7 @@ export function AwaitExpression(node, context) { b.await( b.call( '$.suspend', - node.argument && b.thunk(/** @type {Expression} */ (context.visit(node.argument))) + node.argument && /** @type {Expression} */ (context.visit(node.argument)) ) ), 'exit' diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 2ead0aed532f..9f7ce93974dd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -266,10 +266,10 @@ export function trigger_async_boundary(effect, trigger) { /** * @template T - * @param {() => Promise | Promise} input + * @param {Promise} promise * @returns {Promise<{ exit: () => T }>} */ -export async function suspend(input) { +export async function suspend(promise) { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; @@ -290,12 +290,6 @@ export async function suspend(input) { // @ts-ignore boundary?.fn(ASYNC_INCREMENT); - const promise = typeof input === 'function' ? input() : input; - // Ensure we reset the context back so it doesn't leak - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_component_context); - const value = await promise; return { From e8e723b181ed20585378846313ff38be3a1c263e Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 11:09:28 +0000 Subject: [PATCH 052/211] fix leakage of context again --- .../3-transform/client/visitors/AwaitExpression.js | 2 +- .../svelte/src/internal/client/dom/blocks/boundary.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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 index 48a3bfa584f5..25325ab8b0c7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -18,7 +18,7 @@ export function AwaitExpression(node, context) { b.await( b.call( '$.suspend', - node.argument && /** @type {Expression} */ (context.visit(node.argument)) + node.argument && b.thunk(/** @type {Expression} */ (context.visit(node.argument))) ) ), 'exit' diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 9f7ce93974dd..2ead0aed532f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -266,10 +266,10 @@ export function trigger_async_boundary(effect, trigger) { /** * @template T - * @param {Promise} promise + * @param {() => Promise | Promise} input * @returns {Promise<{ exit: () => T }>} */ -export async function suspend(promise) { +export async function suspend(input) { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; @@ -290,6 +290,12 @@ export async function suspend(promise) { // @ts-ignore boundary?.fn(ASYNC_INCREMENT); + const promise = typeof input === 'function' ? input() : input; + // Ensure we reset the context back so it doesn't leak + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); + const value = await promise; return { From 8eeeeff141c8029953ed8a191e08ad79135c5b4c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 11:37:32 +0000 Subject: [PATCH 053/211] fix hydration --- .../src/internal/client/dom/blocks/boundary.js | 12 ++++++++++-- .../runtime-runes/samples/async-attribute/_config.js | 3 ++- .../runtime-runes/samples/async-derived/_config.js | 3 ++- .../runtime-runes/samples/async-each/_config.js | 3 ++- .../samples/async-expression/_config.js | 3 ++- .../tests/runtime-runes/samples/async-if/_config.js | 3 ++- .../tests/runtime-runes/samples/async-key/_config.js | 3 ++- .../runtime-runes/samples/async-prop/_config.js | 3 ++- .../runtime-runes/samples/async-top-level/_config.js | 3 ++- 9 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 2ead0aed532f..c57f46334ee2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; +import { BOUNDARY_EFFECT, DESTROYED, EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, @@ -27,7 +27,7 @@ import { set_hydrate_node } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; -import { queue_boundary_micro_task } from '../task.js'; +import { queue_boundary_micro_task, queue_post_micro_task } from '../task.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -231,6 +231,14 @@ export function boundary(node, props, boundary_fn) { // need to use hydration boundary comments to report whether // the pending or main block was rendered for a given // boundary, and hydrate accordingly + queueMicrotask(() => { + if ((!boundary_effect || boundary_effect.f & DESTROYED) !== 0) return; + + destroy_effect(boundary_effect); + with_boundary(boundary, () => { + boundary_effect = branch(() => boundary_fn(anchor)); + }); + }); } else { boundary_effect = branch(() => boundary_fn(anchor)); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js index b8a450b33858..5c057119d98a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -22,6 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

hello

'); d = deferred(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index 0a18aa9b2ca0..434853bd7834 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -24,6 +24,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

42

'); component.num = 2; diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js index b50cb1969ea4..89194b963265 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -22,6 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

a

b

c

'); d = deferred(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js index 26333c05fc3b..b5931559460b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -22,6 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

hello

'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js index 286595a9778e..7d7358224833 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -22,6 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

yes

'); d = deferred(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js index 5282bbd739a4..b2c67457e312 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -22,6 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

hello

'); const h1 = target.querySelector('h1'); diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js index 24882c56cd16..4de1788734b9 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -22,6 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

hello

'); d = deferred(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js index 5f85050d9b0e..fb2dbb0e6686 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js @@ -1,4 +1,4 @@ -import { tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -20,6 +20,7 @@ export default test({ d.resolve('hello'); await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

hello

'); } }); From 4b851c83517cdbeb3972ec61eed809d65fac48ca Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 11:44:13 +0000 Subject: [PATCH 054/211] simplify --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c57f46334ee2..313370178e53 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -232,8 +232,6 @@ export function boundary(node, props, boundary_fn) { // the pending or main block was rendered for a given // boundary, and hydrate accordingly queueMicrotask(() => { - if ((!boundary_effect || boundary_effect.f & DESTROYED) !== 0) return; - destroy_effect(boundary_effect); with_boundary(boundary, () => { boundary_effect = branch(() => boundary_fn(anchor)); From 9cbc4aaea4b79cdcb5983ad3fc9601f465896e0d Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 11:53:06 +0000 Subject: [PATCH 055/211] fix bugs --- .../src/internal/client/dom/blocks/boundary.js | 2 +- .../src/internal/client/reactivity/deriveds.js | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 313370178e53..c93d9570be33 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -272,7 +272,7 @@ export function trigger_async_boundary(effect, trigger) { /** * @template T - * @param {() => Promise | Promise} input + * @param {(() => Promise) | Promise} input * @returns {Promise<{ exit: () => T }>} */ export async function suspend(input) { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 448db00b04fc..67520bc4cc99 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -18,12 +18,11 @@ import { update_reaction, increment_write_version, set_active_effect, - component_context, - get + component_context } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; -import { destroy_effect, render_effect } from './effects.js'; +import { block, destroy_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'; @@ -88,10 +87,10 @@ export function async_derived(fn) { throw new Error('TODO cannot create unowned async derived'); } - let promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); - let value = source(/** @type {V} */ (undefined)); + var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); + var value = source(/** @type {V} */ (undefined)); - render_effect(() => { + block(() => { const current = (promise = fn()); suspend(promise).then((v) => { @@ -104,7 +103,7 @@ export function async_derived(fn) { }); // TODO what happens when the promise rejects? - }); + }, EFFECT_HAS_DERIVED); return promise.then(() => value); } From 177885eb1e53e3454707979c1ee30e5bb73b8a6a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 11:56:02 +0000 Subject: [PATCH 056/211] add todo --- packages/svelte/src/internal/client/runtime.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 75942c9b4c92..1947df572838 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -973,6 +973,10 @@ export function get(signal) { } } } else { + // TODO: this doesn't handle removing dependencies from its previous reactions, + // so if it were to conditionally not use a dependency, it would still be tracked + // because we don't have any form of cleanup + // we're adding a dependency outside the init/update cycle // (i.e. after an `await`) // TODO we probably want to disable this for user effects, From d123167778f5388796b90171c48d9d6c60216381 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 11:57:58 +0000 Subject: [PATCH 057/211] remove todo --- packages/svelte/src/internal/client/runtime.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 1947df572838..75942c9b4c92 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -973,10 +973,6 @@ export function get(signal) { } } } else { - // TODO: this doesn't handle removing dependencies from its previous reactions, - // so if it were to conditionally not use a dependency, it would still be tracked - // because we don't have any form of cleanup - // we're adding a dependency outside the init/update cycle // (i.e. after an `await`) // TODO we probably want to disable this for user effects, From e1d56e7ed70bf22558a47055818527a09e7be113 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 12:31:12 +0000 Subject: [PATCH 058/211] cleanup and add guards --- .../internal/client/reactivity/deriveds.js | 20 ++++++++++++++----- .../src/internal/client/reactivity/effects.js | 8 +++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 67520bc4cc99..b8f58395e37f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -18,7 +18,8 @@ import { update_reaction, increment_write_version, set_active_effect, - component_context + component_context, + handle_error } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; @@ -83,7 +84,9 @@ export function derived(fn) { */ /*#__NO_SIDE_EFFECTS__*/ export function async_derived(fn) { - if (!active_effect) { + let effect = /** @type {Effect | null} */ (active_effect); + + if (effect === null) { throw new Error('TODO cannot create unowned async derived'); } @@ -91,9 +94,14 @@ export function async_derived(fn) { var value = source(/** @type {V} */ (undefined)); block(() => { - const current = (promise = fn()); + var current = (promise = fn()); + var derived_promise = suspend(promise); + + derived_promise.then((v) => { + if ((effect.f & DESTROYED) !== 0) { + return; + } - suspend(promise).then((v) => { if (promise === current) { internal_set(value, v.exit()); @@ -102,7 +110,9 @@ export function async_derived(fn) { } }); - // TODO what happens when the promise rejects? + derived_promise.catch(e => { + handle_error(e, effect, null, effect.ctx); + }); }, EFFECT_HAS_DERIVED); return promise.then(() => value); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index b9435b510855..b543208653ce 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -350,8 +350,14 @@ export function render_effect(fn) { * @param {Array<() => Promise>} async */ export function template_effect(fn, sync = [], async = [], d = derived) { + let effect = /** @type {Effect} */ (active_effect); + if (async.length > 0) { suspend(Promise.all(async.map(async_derived))).then((result) => { + if ((effect.f & DESTROYED) !== 0) { + return; + } + create_template_effect(fn, [...sync.map(d), ...result.exit()]); }); } else { @@ -364,7 +370,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { * @param {Value[]} deriveds */ function create_template_effect(fn, deriveds) { - const effect = () => fn(...deriveds.map(get)); + var effect = () => fn(...deriveds.map(get)); if (DEV) { define_property(effect, 'name', { From 3be5a88b6fac6f0d54e59a8519b79118992200ae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 08:43:14 -0500 Subject: [PATCH 059/211] use shared error --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c93d9570be33..04ccc64988b1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -28,6 +28,7 @@ import { } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; import { queue_boundary_micro_task, queue_post_micro_task } from '../task.js'; +import * as e from '../../../shared/errors.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -290,7 +291,7 @@ export async function suspend(input) { } if (boundary === null) { - throw new Error('cannot suspend outside a boundary'); + e.await_outside_boundary(); } // @ts-ignore From f355eaf9a0cdba356a6445ed4f75b50ee24a40a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 08:49:08 -0500 Subject: [PATCH 060/211] differentiate between 'top-level' and 'needs context preservation' so that SSR errors occur correctly --- packages/svelte/src/compiler/phases/2-analyze/index.js | 4 ++-- .../compiler/phases/2-analyze/visitors/AwaitExpression.js | 7 +++++-- .../phases/3-transform/client/visitors/AwaitExpression.js | 2 +- .../phases/3-transform/server/visitors/AwaitExpression.js | 3 +++ packages/svelte/src/compiler/phases/types.d.ts | 4 ++-- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 90e1ceb685c7..41acfc9056f1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -266,7 +266,7 @@ export function analyze_module(ast, options) { immutable: true, tracing: analysis.tracing, async_deriveds: new Set(), - suspenders: new Set() + suspenders: new Map() }; } @@ -455,7 +455,7 @@ export function analyze_component(root, source, options) { snippets: new Set(), is_async: false, async_deriveds: new Set(), - suspenders: new Set() + suspenders: new Map() }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index b78aa6880cd6..cf1665a02c29 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -8,8 +8,11 @@ export function AwaitExpression(node, context) { const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; let suspend = tla; + let preserve_context = tla; if (context.state.expression) { + suspend = true; + // wrap the expression in `(await $.suspend(...)).exit()` if necessary, // i.e. whether anything could potentially be read _after_ the await let i = context.path.length; @@ -23,7 +26,7 @@ export function AwaitExpression(node, context) { // TODO make this more accurate — we don't need to call suspend // if this is the last thing that could be read - suspend = true; + preserve_context = true; } } @@ -32,7 +35,7 @@ export function AwaitExpression(node, context) { throw new Error('TODO runes mode only'); } - context.state.analysis.suspenders.add(node); + context.state.analysis.suspenders.set(node, preserve_context); } if (context.state.expression) { 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 index 25325ab8b0c7..84eb606549f1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -7,7 +7,7 @@ import * as b from '../../../../utils/builders.js'; * @param {Context} context */ export function AwaitExpression(node, context) { - const suspend = context.state.analysis.suspenders.has(node); + const suspend = context.state.analysis.suspenders.get(node); if (!suspend) { return context.next(); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js index f729c9ca9b44..efcc2bc9b02b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js @@ -7,6 +7,9 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function AwaitExpression(node, context) { + // `has`, not `get`, because all top-level await expressions should + // block regardless of whether they need context preservation + // in the client output const suspend = context.state.analysis.suspenders.has(node); if (!suspend) { diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index fdb4eac5577a..c98c44225a66 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -42,8 +42,8 @@ export interface Analysis { /** A set of deriveds that contain `await` expressions */ async_deriveds: Set; - /** A set of `await` expressions that should trigger suspense */ - suspenders: Set; + /** A map of `await` expressions that should block, and whether they should preserve context */ + suspenders: Map; } export interface ComponentAnalysis extends Analysis { From d5de86803d9539500a9448a2820d514e89df2f90 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 09:03:48 -0500 Subject: [PATCH 061/211] opt into runes mode when using blocking await --- .../src/compiler/phases/2-analyze/index.js | 19 +++++++++++++------ packages/svelte/src/compiler/phases/scope.js | 18 ++++++++++++++++++ .../svelte/src/compiler/phases/types.d.ts | 1 + 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 41acfc9056f1..1712702157bd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -203,9 +203,9 @@ function js(script, root, allow_reactive_declarations, parent) { body: [] }; - const { scope, scopes } = create_scopes(ast, root, allow_reactive_declarations, parent); + const { scope, scopes, is_async } = create_scopes(ast, root, allow_reactive_declarations, parent); - return { ast, scope, scopes }; + return { ast, scope, scopes, is_async }; } /** @@ -230,7 +230,7 @@ const RESERVED = ['$$props', '$$restProps', '$$slots']; * @returns {Analysis} */ export function analyze_module(ast, options) { - const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null); + const { scope, scopes, is_async } = create_scopes(ast, new ScopeRoot(), false, null); for (const [name, references] of scope.references) { if (name[0] !== '$' || RESERVED.includes(name)) continue; @@ -259,7 +259,7 @@ export function analyze_module(ast, options) { ); return { - module: { ast, scope, scopes }, + module: { ast, scope, scopes, is_async }, name: options.filename, accessors: false, runes: true, @@ -282,7 +282,12 @@ export function analyze_component(root, source, options) { const module = js(root.module, scope_root, false, null); const instance = js(root.instance, scope_root, true, module.scope); - const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope); + const { scope, scopes, is_async } = create_scopes( + root.fragment, + scope_root, + false, + instance.scope + ); /** @type {Template} */ const template = { ast: root.fragment, scope, scopes }; @@ -390,7 +395,9 @@ export function analyze_component(root, source, options) { const component_name = get_component_name(options.filename); - const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune); + const runes = + options.runes ?? + (is_async || instance.is_async || Array.from(module.scope.references.keys()).some(is_rune)); if (!runes) { for (let check of synthetic_stores_legacy_check) { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 3536dd6a1865..0a71127e33b2 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -345,7 +345,24 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } }; + let is_async = false; + walk(ast, state, { + AwaitExpression(node, context) { + // this doesn't _really_ belong here, but it allows us to + // automatically opt into runes mode on encountering + // blocking awaits, without doing an additional walk + // before the analysis occurs + is_async ||= context.path.every( + ({ type }) => + type !== 'ArrowFunctionExpression' && + type !== 'FunctionExpression' && + type !== 'FunctionDeclaration' + ); + + context.next(); + }, + // references Identifier(node, { path, state }) { const parent = path.at(-1); @@ -713,6 +730,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } return { + is_async, scope, scopes }; diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index c98c44225a66..bf9c5158a03f 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -13,6 +13,7 @@ export interface Js { ast: Program; scope: Scope; scopes: Map; + is_async: boolean; } export interface Template { From 4a9c4c6f50c013466f5f37595f2c9c87ea701358 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 09:10:23 -0500 Subject: [PATCH 062/211] use proper compiler error for await-in-legacy-mode --- .../98-reference/.generated/compile-errors.md | 6 ++++ .../98-reference/.generated/shared-errors.md | 2 ++ .../svelte/messages/compile-errors/script.md | 4 +++ packages/svelte/src/compiler/errors.js | 9 ++++++ .../2-analyze/visitors/AwaitExpression.js | 3 +- packages/svelte/src/internal/shared/errors.js | 30 +++++++++---------- 6 files changed, 38 insertions(+), 16 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 2fef3bd45d50..f83c1b47f4ef 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -498,6 +498,12 @@ The arguments keyword cannot be used within the template or at the top level of %message% ``` +### legacy_await_invalid + +``` +Cannot use `await` at the top level of a component, or in the template, unless in runes mode +``` + ### legacy_export_invalid ``` diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index df49facef7bf..084d6c140ba0 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -6,6 +6,8 @@ Cannot await outside a `` with a `pending` snippet ``` +TODO + ### invalid_default_snippet ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 0aa6fbed90d8..3f0dc21d1303 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -98,6 +98,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > The arguments keyword cannot be used within the template or at the top level of a component +## legacy_await_invalid + +> Cannot use `await` at the top level of a component, or in the template, unless in runes mode + ## legacy_export_invalid > Cannot use `export let` in runes mode — use `$props()` instead diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 53a6ac6849ec..a5ce88d62d68 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -497,6 +497,15 @@ export function typescript_invalid_feature(node, feature) { e(node, 'typescript_invalid_feature', `TypeScript language features like ${feature} are not natively supported, and their use is generally discouraged. Outside of \`

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index abeea8becb07..6a46846744ca 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -17,12 +17,14 @@ export default test({ }; }, - async test({ assert, target, component }) { + async test({ assert, target, component, logs }) { d.resolve(42); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); flushSync(); assert.htmlEqual(target.innerHTML, '

42

'); @@ -31,6 +33,8 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

84

'); @@ -42,7 +46,11 @@ export default test({ d.resolve(43); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

86

'); + + assert.deepEqual(logs, ['should run', 42, 1, 84, 2, 86, 2]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js index fb2dbb0e6686..b5931559460b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js @@ -19,6 +19,8 @@ export default test({ async test({ assert, target }) { d.resolve('hello'); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); flushSync(); assert.htmlEqual(target.innerHTML, '

hello

'); From 05d8cb22dd8b5a04c61c19fb4a39032fc666265f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 17:43:26 -0500 Subject: [PATCH 079/211] update test --- .../samples/async-expression/_config.js | 12 +++++-- .../samples/async-expression/main.svelte | 2 +- .../samples/async-render-tag/_config.js | 35 +++++++++++++++++++ .../samples/async-render-tag/main.svelte | 15 ++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js index bc9ab2d04491..566bd2210b93 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -16,12 +16,20 @@ export default test({ }; }, - async test({ assert, target }) { + async test({ assert, target, component }) { d.resolve('hello'); await Promise.resolve(); await Promise.resolve(); await tick(); flushSync(); - assert.htmlEqual(target.innerHTML, '

hello

'); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('wheee'); + await tick(); + assert.htmlEqual(target.innerHTML, '

wheee

'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte index fefce867f294..3c6879caee08 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte @@ -3,7 +3,7 @@ -

{await promise}

+

{await promise}

{#snippet pending()}

pending

diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js new file mode 100644 index 000000000000..cde07e6c8623 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -0,0 +1,35 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target }) { + d.resolve('hello'); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('wheee'); + await tick(); + assert.htmlEqual(target.innerHTML, '

wheee

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte new file mode 100644 index 000000000000..e98738567112 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte @@ -0,0 +1,15 @@ + + +{#snippet hello(message)} +

{message}

+{/snippet} + + + {@render hello(await promise)} + + {#snippet pending()} +

pending

+ {/snippet} +
From 0d34b7abb68bd9cacc7c28b9fb8ffb0c3164f2fd Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 22:43:34 +0000 Subject: [PATCH 080/211] more fixes --- .../phases/3-transform/client/visitors/AwaitExpression.js | 6 +++++- packages/svelte/src/internal/client/dom/blocks/boundary.js | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) 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 index 9189ed4b8819..fdfa0c7a0c04 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -15,7 +15,11 @@ export function AwaitExpression(node, context) { } const inside_derived = context.path.some( - (n) => n.type === 'CallExpression' && get_rune(n, context.state.scope) === '$derived' + (n) => + n.type === 'VariableDeclaration' && + n.declarations.some( + (d) => d.init?.type === 'CallExpression' && get_rune(d.init, context.state.scope) === '$derived' + ) ); const expression = b.call( diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 9532b1c2e417..9a77aae3683b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -281,6 +281,8 @@ export function capture() { // prevent the active effect from outstaying its welcome if (should_exit) { queue_post_micro_task(exit); + } else { + debugger } }; } @@ -317,6 +319,7 @@ export async function script_suspend(fn) { const restore = capture(); const unsuspend = suspend(); try { + exit(); return await fn(); } finally { restore(false); From acb71be6e5ddfc2e1fcdb59c8855b93ea2c16ab5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 22:45:53 +0000 Subject: [PATCH 081/211] remove debugger --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 9a77aae3683b..f8793abe9413 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -281,8 +281,6 @@ export function capture() { // prevent the active effect from outstaying its welcome if (should_exit) { queue_post_micro_task(exit); - } else { - debugger } }; } From 8517eef6e7abeee5c58009212cd7bb8d60d19228 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 23:44:12 +0000 Subject: [PATCH 082/211] unwaterfall for now --- packages/svelte/src/internal/client/reactivity/deriveds.js | 6 ------ .../tests/runtime-runes/samples/async-derived/_config.js | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 94d20fb0e1a5..829100302f06 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -107,12 +107,6 @@ export function async_derived(fn) { var restore = capture(); var unsuspend = suspend(); - // Ensure the effect tree is paused/resume otherwise user-effects will - // not run correctly - if (effect.deps !== null) { - flush_boundary_micro_tasks(); - } - try { var v = await promise; diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index 6a46846744ca..8f614643e2c4 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -51,6 +51,6 @@ export default test({ await tick(); assert.htmlEqual(target.innerHTML, '

86

'); - assert.deepEqual(logs, ['should run', 42, 1, 84, 2, 86, 2]); + assert.deepEqual(logs, ['should run', 42, 1, 42, 2, 84, 2, 86, 2]); } }); From e102ec06fa281c889bbae7dc817b0592505eba4e Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Jan 2025 23:55:45 +0000 Subject: [PATCH 083/211] improve test --- .../src/internal/client/reactivity/deriveds.js | 7 +++++-- .../samples/async-derived/Child.svelte | 8 ++++---- .../samples/async-derived/_config.js | 15 ++++++++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 829100302f06..f8f3a00a29df 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -19,7 +19,8 @@ import { increment_write_version, set_active_effect, component_context, - handle_error + handle_error, + get } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; @@ -100,9 +101,11 @@ export function async_derived(fn) { var current_deps = new Set(async_deps); + var derived_promise = derived(fn); + block(async () => { var effect = /** @type {Effect} */ (active_effect); - var current = (promise = fn()); + var current = (promise = get(derived_promise)); var restore = capture(); var unsuspend = suspend(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte index b2add4716121..6031c28305a0 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte @@ -4,12 +4,12 @@ let value = $derived((await promise) * num); $effect(() => { - console.log('should run'); + console.log(`$effect ${value} ${num}`); }); - $effect(() => { - console.log(value, num); + $effect.pre(() => { + console.log(`$effect.pre ${value} ${num}`); }); -

{value}

+

{value}{console.log(`template ${value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index 8f614643e2c4..ebeac1558bb2 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -51,6 +51,19 @@ export default test({ await tick(); assert.htmlEqual(target.innerHTML, '

86

'); - assert.deepEqual(logs, ['should run', 42, 1, 42, 2, 84, 2, 86, 2]); + assert.deepEqual(logs, [ + '$effect.pre 42 1', + 'template 42 1', + '$effect 42 1', + '$effect.pre 42 2', + 'template 42 2', + '$effect 42 2', + '$effect.pre 84 2', + 'template 84 2', + '$effect 84 2', + '$effect.pre 86 2', + 'template 86 2', + '$effect 86 2' + ]); } }); From debc14874674688ecce8a8179d9a09523923e728 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 01:32:51 +0000 Subject: [PATCH 084/211] avoid eagerly trigger user effects or templates effects when suspended --- .../svelte/src/internal/client/constants.js | 31 ++++++++++--------- .../internal/client/reactivity/deriveds.js | 3 +- .../src/internal/client/reactivity/effects.js | 8 +++-- .../svelte/src/internal/client/runtime.js | 26 ++++++++++++++-- .../samples/async-derived/_config.js | 14 +++------ .../samples/async-derived/main.svelte | 2 ++ 6 files changed, 54 insertions(+), 30 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index e7034a332dda..5018887d7fd0 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 UNOWNED = 1 << 9; +export const DISCONNECTED = 1 << 10; +export const CLEAN = 1 << 11; +export const DIRTY = 1 << 12; +export const MAYBE_DIRTY = 1 << 13; +export const INERT = 1 << 14; +export const DESTROYED = 1 << 15; +export const EFFECT_RAN = 1 << 16; /** 'Transparent' effects do not create a transition boundary */ -export const EFFECT_TRANSPARENT = 1 << 16; +export const EFFECT_TRANSPARENT = 1 << 17; /** 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 << 18; +export const INSPECT_EFFECT = 1 << 19; +export const HEAD_EFFECT = 1 << 20; +export const EFFECT_HAS_DERIVED = 1 << 21; -export const REACTION_IS_UPDATING = 1 << 21; +// Flags used for async +export const IS_ASYNC = 1 << 22; +export const REACTION_IS_UPDATING = 1 << 23; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index f8f3a00a29df..6310b175d111 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -6,6 +6,7 @@ import { DESTROYED, DIRTY, EFFECT_HAS_DERIVED, + IS_ASYNC, MAYBE_DIRTY, UNOWNED } from '../constants.js'; @@ -158,7 +159,7 @@ export function async_derived(fn) { // TODO we should probably null out active effect here, // rather than inside `restore()` } - }, EFFECT_HAS_DERIVED); + }, IS_ASYNC); return promise.then(() => value); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 8be44462ad5d..0ee2352a2d91 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -37,7 +37,9 @@ import { HEAD_EFFECT, MAYBE_DIRTY, EFFECT_HAS_DERIVED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + IS_ASYNC, + TEMPLATE_EFFECT } from '../constants.js'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -145,7 +147,7 @@ function create_effect(type, fn, sync, push = true) { effect.first === null && effect.nodes_start === null && effect.teardown === null && - (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0; + (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT | IS_ASYNC)) === 0; if (!inert && !is_root && push) { if (parent_effect !== null) { @@ -385,7 +387,7 @@ function create_template_effect(fn, deriveds) { }); } - block(effect); + block(effect, TEMPLATE_EFFECT); } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9ed17315223e..3ba88944486f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -26,7 +26,9 @@ import { LEGACY_DERIVED_PROP, DISCONNECTED, BOUNDARY_EFFECT, - REACTION_IS_UPDATING + REACTION_IS_UPDATING, + IS_ASYNC, + TEMPLATE_EFFECT } from './constants.js'; import { flush_idle_tasks, @@ -102,6 +104,7 @@ export function set_active_effect(effect) { /* @__PURE__ */ setInterval(() => { if (active_effect !== null || active_reaction !== null) { + // eslint-disable-next-line no-debugger debugger; } }); @@ -819,6 +822,7 @@ export function schedule_effect(signal) { function process_effects(effect, collected_effects) { var current_effect = effect.first; var effects = []; + var suspended = false; main_loop: while (current_effect !== null) { var flags = current_effect.f; @@ -827,13 +831,25 @@ function process_effects(effect, collected_effects) { var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { + var skip_suspended = + suspended && + (flags & BRANCH_EFFECT) === 0 && + ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0); + if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { current_effect.f ^= CLEAN; - } else { + } else if (!skip_suspended) { try { + var is_async_effect = (current_effect.f & IS_ASYNC) !== 0; + if (check_dirtiness(current_effect)) { update_effect(current_effect); + if (!suspended && is_async_effect) { + suspended = true; + } + } else if (!suspended && is_async_effect && current_effect.deps === null) { + suspended = true; } } catch (error) { handle_error(error, current_effect, null, current_effect.ctx); @@ -846,7 +862,7 @@ function process_effects(effect, collected_effects) { current_effect = child; continue; } - } else if ((flags & EFFECT) !== 0) { + } else if ((flags & EFFECT) !== 0 && !skip_suspended) { effects.push(current_effect); } } @@ -858,6 +874,10 @@ function process_effects(effect, collected_effects) { if (effect === parent) { break main_loop; } + // TODO: we need to know that this boundary has a valid `pending` + if (suspended && (parent.f & BOUNDARY_EFFECT) !== 0) { + suspended = false; + } var parent_sibling = parent.next; if (parent_sibling !== null) { current_effect = parent_sibling; diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index ebeac1558bb2..fb013938bb7b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -25,7 +25,6 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - await tick(); flushSync(); assert.htmlEqual(target.innerHTML, '

42

'); @@ -34,7 +33,6 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

84

'); @@ -47,20 +45,18 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

86

'); assert.deepEqual(logs, [ + 'outside boundary 1', '$effect.pre 42 1', 'template 42 1', '$effect 42 1', - '$effect.pre 42 2', - 'template 42 2', - '$effect 42 2', - '$effect.pre 84 2', - 'template 84 2', - '$effect 84 2', + 'outside boundary 2', + '$effect.pre 84 2', // TODO: why is this observed during tests, but not during runtime? + 'template 84 2', // TODO: why is this observed during tests, but not during runtime? + '$effect 84 2', // TODO: why is this observed during tests, but not during runtime? '$effect.pre 86 2', 'template 86 2', '$effect 86 2' diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte index 3b56c3a316b4..e90bbf720ed3 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte @@ -11,3 +11,5 @@

pending

{/snippet}
+ +{console.log(`outside boundary ${num}`)} From 3e9d14a1668af59fbe83af33200f52a858d97c73 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 01:35:46 +0000 Subject: [PATCH 085/211] add comment --- packages/svelte/src/internal/client/runtime.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3ba88944486f..d2952533271a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -831,6 +831,9 @@ function process_effects(effect, collected_effects) { var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { + // We only want to skip suspended effects if they are not branches or block effects, + // with the exception of template effects, which are technically block effects but also + // have a special flag that we used to detect them var skip_suspended = suspended && (flags & BRANCH_EFFECT) === 0 && From bf8bb140d9ab77f618f109712567fe869d2c527a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 01:36:24 +0000 Subject: [PATCH 086/211] add comment --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d2952533271a..ca07460d4ad4 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -833,7 +833,7 @@ function process_effects(effect, collected_effects) { if (!is_skippable_branch && (flags & INERT) === 0) { // We only want to skip suspended effects if they are not branches or block effects, // with the exception of template effects, which are technically block effects but also - // have a special flag that we used to detect them + // have a special flag `TEMPLATE_EFFECT` that we can use to identify them var skip_suspended = suspended && (flags & BRANCH_EFFECT) === 0 && From f8aedc4e3634be861417cc4a5a9027f468ab9683 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 01:37:47 +0000 Subject: [PATCH 087/211] cleanup --- packages/svelte/src/internal/client/runtime.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index ca07460d4ad4..0d9974079da2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -844,15 +844,11 @@ function process_effects(effect, collected_effects) { current_effect.f ^= CLEAN; } else if (!skip_suspended) { try { - var is_async_effect = (current_effect.f & IS_ASYNC) !== 0; - if (check_dirtiness(current_effect)) { update_effect(current_effect); - if (!suspended && is_async_effect) { + if (!suspended && (current_effect.f & IS_ASYNC) !== 0) { suspended = true; } - } else if (!suspended && is_async_effect && current_effect.deps === null) { - suspended = true; } } catch (error) { handle_error(error, current_effect, null, current_effect.ctx); From 10751c85fb4ae2b14b1457a497d8f9e54ab045e5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 01:38:42 +0000 Subject: [PATCH 088/211] perf tweak --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 0d9974079da2..57471fc098b0 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -846,7 +846,7 @@ function process_effects(effect, collected_effects) { try { if (check_dirtiness(current_effect)) { update_effect(current_effect); - if (!suspended && (current_effect.f & IS_ASYNC) !== 0) { + if ((current_effect.f & IS_ASYNC) !== 0 && !suspended) { suspended = true; } } From b35e19cf421a2e9d3ade39b1e2a44955be74dedc Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 01:38:58 +0000 Subject: [PATCH 089/211] perf tweak --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 57471fc098b0..020130fefaf1 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -846,7 +846,7 @@ function process_effects(effect, collected_effects) { try { if (check_dirtiness(current_effect)) { update_effect(current_effect); - if ((current_effect.f & IS_ASYNC) !== 0 && !suspended) { + if ((flags & IS_ASYNC) !== 0 && !suspended) { suspended = true; } } From 9fc083a10f76e2c6a9306a107239a186454cf1cc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 21:29:12 -0500 Subject: [PATCH 090/211] fix type --- .../phases/3-transform/server/visitors/AwaitExpression.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js index efcc2bc9b02b..bb6a0e7b45ed 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js @@ -1,10 +1,10 @@ /** @import { AwaitExpression } from 'estree' */ -/** @import { ComponentContext } from '../types.js' */ +/** @import { Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; /** * @param {AwaitExpression} node - * @param {ComponentContext} context + * @param {Context} context */ export function AwaitExpression(node, context) { // `has`, not `get`, because all top-level await expressions should From 2c00f85f454433a18acaf5e8aa81a422865d4459 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 21:29:24 -0500 Subject: [PATCH 091/211] fix test --- .../tests/runtime-runes/samples/async-render-tag/_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js index cde07e6c8623..566bd2210b93 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -16,7 +16,7 @@ export default test({ }; }, - async test({ assert, target }) { + async test({ assert, target, component }) { d.resolve('hello'); await Promise.resolve(); await Promise.resolve(); From 3561117b04cabd5c6095276a2a06adcabde98ed3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 21:32:51 -0500 Subject: [PATCH 092/211] skip for now --- .../tests/runtime-runes/samples/async-render-tag/_config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js index 566bd2210b93..04f5cc71a082 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -6,6 +6,8 @@ import { test } from '../../test'; let d; export default test({ + skip: true, + html: `

pending

`, get props() { From baba2638c9a9a06190a8c08150cdf8641be60969 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 22:07:47 -0500 Subject: [PATCH 093/211] render tags --- .../src/compiler/phases/1-parse/state/tag.js | 2 +- .../src/compiler/phases/2-analyze/index.js | 2 - .../src/compiler/phases/2-analyze/types.d.ts | 2 - .../2-analyze/visitors/AwaitExpression.js | 8 ++- .../2-analyze/visitors/CallExpression.js | 14 +--- .../phases/2-analyze/visitors/RenderTag.js | 16 ++++- .../3-transform/client/visitors/RenderTag.js | 69 +++++++++++++++---- .../svelte/src/compiler/types/template.d.ts | 2 +- .../samples/async-render-tag/_config.js | 2 - 9 files changed, 78 insertions(+), 39 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 78820d0fa10e..c57b445d34a0 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -715,7 +715,7 @@ function special(parser) { expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { dynamic: false, - args_with_call_expression: new Set(), + arguments: [], path: [], snippets: new Set() } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 1712702157bd..4fc43151ec7d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -618,7 +618,6 @@ export function analyze_component(root, source, options) { has_props_rune: false, component_slots: new Set(), expression: null, - render_tag: null, private_derived_state: [], function_depth: scope.function_depth, instance_scope: instance.scope, @@ -690,7 +689,6 @@ export function analyze_component(root, source, options) { reactive_statements: analysis.reactive_statements, component_slots: new Set(), expression: null, - render_tag: null, private_derived_state: [], function_depth: scope.function_depth }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index b4ca4dc26278..1e71accb9f88 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -19,8 +19,6 @@ export interface AnalysisState { component_slots: Set; /** Information about the current expression/directive/block value */ expression: ExpressionMetadata | null; - /** The current {@render ...} tag, if any */ - render_tag: null | AST.RenderTag; private_derived_state: string[]; function_depth: number; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index c176eec3f4f9..178b81790304 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -20,8 +20,12 @@ export function AwaitExpression(node, context) { while (i--) { const parent = context.path[i]; - // @ts-expect-error we could probably use a neater/more robust mechanism - if (parent.metadata?.expression === context.state.expression) { + if ( + // @ts-expect-error we could probably use a neater/more robust mechanism + parent.metadata?.expression === context.state.expression || + // @ts-expect-error + parent.metadata?.arguments?.includes(context.state.expression) + ) { break; } 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 6755193d3c15..c7bbb6154249 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -3,7 +3,7 @@ /** @import { Context } from '../types' */ import { get_rune } from '../../scope.js'; import * as e from '../../../errors.js'; -import { get_parent, unwrap_optional } from '../../../utils/ast.js'; +import { get_parent } 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'; @@ -187,18 +187,6 @@ export function CallExpression(node, context) { break; } - if (context.state.render_tag) { - // Find out which of the render tag arguments contains this call expression - const arg_idx = unwrap_optional(context.state.render_tag.expression).arguments.findIndex( - (arg) => arg === node || context.path.includes(arg) - ); - - // -1 if this is the call expression of the render tag itself - if (arg_idx !== -1) { - context.state.render_tag.metadata.args_with_call_expression.add(arg_idx); - } - } - if (node.callee.type === 'Identifier') { const binding = context.state.scope.get(node.callee.name); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 045224276a2e..a8c9d408bdad 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -5,6 +5,7 @@ import * as e from '../../../errors.js'; import { validate_opening_tag } from './shared/utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; import { is_resolved_snippet } from './shared/snippets.js'; +import { create_expression_metadata } from '../../nodes.js'; /** * @param {AST.RenderTag} node @@ -15,7 +16,8 @@ export function RenderTag(node, context) { node.metadata.path = [...context.path]; - const callee = unwrap_optional(node.expression).callee; + const expression = unwrap_optional(node.expression); + const callee = expression.callee; const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null; @@ -52,5 +54,15 @@ export function RenderTag(node, context) { mark_subtree_dynamic(context.path); - context.next({ ...context.state, render_tag: node }); + context.visit(callee); + + for (const arg of expression.arguments) { + const metadata = create_expression_metadata(); + node.metadata.arguments.push(metadata); + + context.visit(arg, { + ...context.state, + expression: metadata + }); + } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 7da987f6cc4d..615cd0097f74 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -1,8 +1,10 @@ -/** @import { Expression } from 'estree' */ +/** @import { Expression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext } from '../types' */ +/** @import { ComponentContext, MemoizedExpression } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; +import { create_derived } from '../utils.js'; +import { get_expression_id } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -10,23 +12,44 @@ import * as b from '../../../../utils/builders.js'; */ export function RenderTag(node, context) { context.state.template.push(''); - const callee = unwrap_optional(node.expression).callee; - const raw_args = unwrap_optional(node.expression).arguments; + + const expression = unwrap_optional(node.expression); + + const callee = expression.callee; + const raw_args = expression.arguments; /** @type {Expression[]} */ let args = []; + + /** @type {MemoizedExpression[]} */ + const expressions = []; + + /** @type {MemoizedExpression[]} */ + const async_expressions = []; + for (let i = 0; i < raw_args.length; i++) { - const raw = raw_args[i]; - const arg = /** @type {Expression} */ (context.visit(raw)); - if (node.metadata.args_with_call_expression.has(i)) { - const id = b.id(context.state.scope.generate('render_arg')); - context.state.init.push(b.var(id, b.call('$.derived_safe_equal', b.thunk(arg)))); - args.push(b.thunk(b.call('$.get', id))); - } else { - args.push(b.thunk(arg)); + let expression = /** @type {Expression} */ (context.visit(raw_args[i])); + const { has_call, is_async } = node.metadata.arguments[i]; + + if (is_async || has_call) { + expression = b.call( + '$.get', + get_expression_id(is_async ? async_expressions : expressions, expression) + ); } + + args.push(b.thunk(expression)); } + [...async_expressions, ...expressions].forEach((memo, i) => { + memo.id.name = `$${i}`; + }); + + /** @type {Statement[]} */ + const statements = expressions.map((memo, i) => + b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ); + let snippet_function = /** @type {Expression} */ (context.visit(callee)); if (node.metadata.dynamic) { @@ -35,11 +58,11 @@ export function RenderTag(node, context) { snippet_function = b.logical('??', snippet_function, b.id('$.noop')); } - context.state.init.push( + statements.push( b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) ); } else { - context.state.init.push( + statements.push( b.stmt( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( snippet_function, @@ -49,4 +72,22 @@ export function RenderTag(node, context) { ) ); } + + if (async_expressions.length > 0) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array(async_expressions.map((memo) => b.thunk(memo.expression, true))), + b.arrow( + [context.state.node, ...async_expressions.map((memo) => memo.id)], + b.block(statements) + ) + ) + ) + ); + } else { + context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements)); + } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index c16c161e8639..6bc1329d7071 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -166,7 +166,7 @@ export namespace AST { /** @internal */ metadata: { dynamic: boolean; - args_with_call_expression: Set; + arguments: ExpressionMetadata[]; path: SvelteNode[]; /** The set of locally-defined snippets that this render tag could correspond to, * used for CSS pruning purposes */ diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js index 04f5cc71a082..566bd2210b93 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -6,8 +6,6 @@ import { test } from '../../test'; let d; export default test({ - skip: true, - html: `

pending

`, get props() { From ef59763c76092cba74db767cf3cab649b807afdf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 22:19:32 -0500 Subject: [PATCH 094/211] html tags --- .../src/compiler/phases/1-parse/state/tag.js | 5 ++- .../phases/2-analyze/visitors/HtmlTag.js | 5 ++- .../3-transform/client/visitors/HtmlTag.js | 43 ++++++++++++++----- .../svelte/src/compiler/types/template.d.ts | 4 ++ .../src/internal/client/dom/blocks/html.js | 2 +- .../samples/async-html-tag/_config.js | 35 +++++++++++++++ .../samples/async-html-tag/main.svelte | 11 +++++ .../_expected/client/index.svelte.js | 2 +- 8 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index c57b445d34a0..90440e0980a9 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -613,7 +613,10 @@ function special(parser) { type: 'HtmlTag', start, end: parser.index, - expression + expression, + metadata: { + expression: create_expression_metadata() + } }); return; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js index c89b11ad3695..ccb2c17955d8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js @@ -15,5 +15,8 @@ export function HtmlTag(node, context) { // unfortunately this is necessary in order to fix invalid HTML mark_subtree_dynamic(context.path); - context.next(); + context.next({ + ...context.state, + expression: node.metadata.expression + }); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index 32439879de38..31f81310384e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -11,17 +11,38 @@ import * as b from '../../../../utils/builders.js'; export function HtmlTag(node, context) { context.state.template.push(''); - // push into init, so that bindings run afterwards, which might trigger another run and override hydration - context.state.init.push( - b.stmt( - b.call( - '$.html', - context.state.node, - b.thunk(/** @type {Expression} */ (context.visit(node.expression))), - b.literal(context.state.metadata.namespace === 'svg'), - b.literal(context.state.metadata.namespace === 'mathml'), - is_ignored(node, 'hydration_html_changed') && b.true - ) + const { is_async } = node.metadata.expression; + + const expression = /** @type {Expression} */ (context.visit(node.expression)); + const html = is_async ? b.call('$.get', b.id('$$html')) : expression; + + const is_svg = context.state.metadata.namespace === 'svg'; + const is_mathml = context.state.metadata.namespace === 'mathml'; + + const statement = b.stmt( + b.call( + '$.html', + context.state.node, + b.thunk(html), + is_svg && b.true, + is_mathml && b.true, + is_ignored(node, 'hydration_html_changed') && b.true ) ); + + // push into init, so that bindings run afterwards, which might trigger another run and override hydration + if (node.metadata.expression.is_async) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array([b.thunk(expression, true)]), + b.arrow([context.state.node, b.id('$$html')], b.block([statement])) + ) + ) + ); + } else { + context.state.init.push(statement); + } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 6bc1329d7071..14b9e522a4de 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -135,6 +135,10 @@ export namespace AST { export interface HtmlTag extends BaseNode { type: 'HtmlTag'; expression: Expression; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** An HTML comment */ diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 04ab0aee87f5..0cc91b204a93 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -39,7 +39,7 @@ function check_hash(element, server_hash, value) { * @param {boolean} [skip_warning] * @returns {void} */ -export function html(node, get_value, svg, mathml, skip_warning) { +export function html(node, get_value, svg = false, mathml = false, skip_warning = false) { var anchor = node; var value = ''; diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js new file mode 100644 index 000000000000..566bd2210b93 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js @@ -0,0 +1,35 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('hello'); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('wheee'); + await tick(); + assert.htmlEqual(target.innerHTML, '

wheee

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte new file mode 100644 index 000000000000..f5aa363731c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte @@ -0,0 +1,11 @@ + + + +

{@html await promise}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 9b203b97e82d..d0a7a0152806 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -13,7 +13,7 @@ export default function Skip_static_subtree($$anchor, $$props) { var node = $.sibling(h1, 10); - $.html(node, () => $$props.content, false, false); + $.html(node, () => $$props.content); $.next(14); $.reset(main); From 1426a6d9eb6cbab5aed1819592f827ce88b54625 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 22:24:50 -0500 Subject: [PATCH 095/211] fix --- packages/svelte/src/internal/client/reactivity/deriveds.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6310b175d111..b6954e5c93c9 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -161,7 +161,7 @@ export function async_derived(fn) { } }, IS_ASYNC); - return promise.then(() => value); + return Promise.resolve(promise).then(() => value); } /** From 8a28f72090b5dab66db208c4967204ad68de58fc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 22:45:34 -0500 Subject: [PATCH 096/211] dynamic elements --- .../compiler/phases/1-parse/state/element.js | 2 + .../2-analyze/visitors/SvelteElement.js | 14 +++++- .../client/visitors/SvelteElement.js | 45 ++++++++++++------- .../svelte/src/compiler/types/template.d.ts | 1 + .../samples/async-svelte-element/_config.js | 35 +++++++++++++++ .../samples/async-svelte-element/main.svelte | 11 +++++ 6 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 66946a8f8d22..b18e1cb25b25 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -284,6 +284,8 @@ export default function element(parser) { } else { element.tag = get_attribute_expression(definition); } + + element.metadata.expression = create_expression_metadata(); } if (is_top_level_script_or_style) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js index c45859408c4b..5be1f91cbaeb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js @@ -62,5 +62,17 @@ export function SvelteElement(node, context) { mark_subtree_dynamic(context.path); - context.next({ ...context.state, parent_element: null }); + context.visit(node.tag, { + ...context.state, + expression: node.metadata.expression + }); + + for (const attribute of node.attributes) { + context.visit(attribute); + } + + context.visit(node.fragment, { + ...context.state, + parent_element: null + }); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index ccf08dc4238e..37092a6306b8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -33,7 +33,7 @@ export function SvelteElement(node, context) { const style_directives = []; /** @type {ExpressionStatement[]} */ - const lets = []; + const statements = []; // Create a temporary context which picks up the init/update statements. // They'll then be added to the function parameter of $.element @@ -66,7 +66,7 @@ export function SvelteElement(node, context) { } else if (attribute.type === 'StyleDirective') { style_directives.push(attribute); } else if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + statements.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } else if (attribute.type === 'OnDirective') { const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state)); inner_context.state.after_update.push(b.stmt(handler)); @@ -75,9 +75,6 @@ export function SvelteElement(node, context) { } } - // Let bindings first, they can be used on attributes - context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot - // Then do attributes let is_attributes_reactive = false; @@ -108,15 +105,6 @@ export function SvelteElement(node, context) { build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive); build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive); - const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag))); - - if (dev) { - if (node.fragment.nodes.length > 0) { - context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag))); - } - context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag))); - } - /** @type {Statement[]} */ const inner = inner_context.state.init; if (inner_context.state.update.length > 0) { @@ -135,9 +123,21 @@ export function SvelteElement(node, context) { ).body ); + const { is_async } = node.metadata.expression; + + const expression = /** @type {Expression} */ (context.visit(node.tag)); + const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression); + + if (dev) { + if (node.fragment.nodes.length > 0) { + statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag))); + } + statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag))); + } + const location = dev && locator(node.start); - context.state.init.push( + statements.push( b.stmt( b.call( '$.element', @@ -150,4 +150,19 @@ export function SvelteElement(node, context) { ) ) ); + + if (is_async) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array([b.thunk(expression, true)]), + b.arrow([context.state.node, b.id('$$tag')], b.block(statements)) + ) + ) + ); + } else { + context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements)); + } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 14b9e522a4de..dcdf645c4a2e 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -349,6 +349,7 @@ export namespace AST { tag: Expression; /** @internal */ metadata: { + expression: ExpressionMetadata; /** * `true` if this is an svg element. The boolean may not be accurate because * the tag is dynamic, but we do our best to infer it from the template. diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js new file mode 100644 index 000000000000..92946a539f39 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js @@ -0,0 +1,35 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('h1'); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('h2'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte new file mode 100644 index 000000000000..52852b549c8e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte @@ -0,0 +1,11 @@ + + + + hello + + {#snippet pending()} +

pending

+ {/snippet} +
From 79ae4084aefe60ce7c5227ac512e54caa517019e Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 12:21:08 +0000 Subject: [PATCH 097/211] remove todos --- .../tests/runtime-runes/samples/async-derived/_config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index fb013938bb7b..dcbbdd4fb58b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -54,9 +54,9 @@ export default test({ 'template 42 1', '$effect 42 1', 'outside boundary 2', - '$effect.pre 84 2', // TODO: why is this observed during tests, but not during runtime? - 'template 84 2', // TODO: why is this observed during tests, but not during runtime? - '$effect 84 2', // TODO: why is this observed during tests, but not during runtime? + '$effect.pre 84 2', + 'template 84 2', + '$effect 84 2', '$effect.pre 86 2', 'template 86 2', '$effect 86 2' From 08c3d6a577afcd7ba9825a4166bfc0c0c4617c2e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 08:43:02 -0500 Subject: [PATCH 098/211] remove some Promise.resolves --- .../svelte/tests/runtime-runes/samples/async-derived/_config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index dcbbdd4fb58b..bb3f67f0f6f9 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -32,7 +32,6 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

84

'); @@ -44,7 +43,6 @@ export default test({ d.resolve(43); await Promise.resolve(); await Promise.resolve(); - await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

86

'); From e43509c64bbe426a2f5677db0a3b7e5db5a48155 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 08:46:06 -0500 Subject: [PATCH 099/211] update changeset --- .changeset/eleven-weeks-dance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/eleven-weeks-dance.md b/.changeset/eleven-weeks-dance.md index c382f76a51f8..0646b78e840f 100644 --- a/.changeset/eleven-weeks-dance.md +++ b/.changeset/eleven-weeks-dance.md @@ -2,4 +2,4 @@ 'svelte': patch --- -chore: refactor task microtask dispatching + boundary scheduling +feat: support `await` in components From c8a3d17cfd48af81ee455d1effb3fb36823c030a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 09:26:07 -0500 Subject: [PATCH 100/211] simplify --- packages/svelte/src/compiler/phases/2-analyze/index.js | 1 - .../src/compiler/phases/2-analyze/visitors/CallExpression.js | 3 --- .../compiler/phases/3-transform/client/transform-client.js | 2 +- .../compiler/phases/3-transform/client/visitors/Fragment.js | 3 --- packages/svelte/src/compiler/phases/types.d.ts | 4 ---- 5 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 4fc43151ec7d..ae946f083d15 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -460,7 +460,6 @@ export function analyze_component(root, source, options) { undefined_exports: new Map(), snippet_renderers: new Map(), snippets: new Set(), - is_async: false, async_deriveds: new Set(), suspenders: new Map() }; 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 c7bbb6154249..41a167d35dd0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -207,9 +207,6 @@ export function CallExpression(node, context) { if (expression.is_async) { context.state.analysis.async_deriveds.add(node); - - context.state.analysis.is_async ||= - context.state.ast_type === 'instance' && context.state.function_depth === 1; } } else if (rune === '$inspect') { context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); 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 46c13d1a6f4b..3bfde4292c17 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 @@ -367,7 +367,7 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); - if (analysis.is_async) { + if (analysis.instance.is_async) { const body = b.function_declaration( b.id('$$body'), [b.id('$$anchor'), b.id('$$props')], diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 2d1543519988..3255ca6f0c56 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -197,9 +197,6 @@ export function Fragment(node, context) { body.push(close); } - const async = - state.metadata.async.length > 0 || (state.analysis.is_async && context.path.length === 0); - return b.block(body); } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index bf9c5158a03f..c395080fb015 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -99,10 +99,6 @@ export interface ComponentAnalysis extends Analysis { * Every snippet that is declared locally */ snippets: Set; - /** - * true if uses top-level await - */ - is_async: boolean; } declare module 'estree' { From 7c34419c6d5ba5603b032deee7b6a471c4bdb702 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 09:26:21 -0500 Subject: [PATCH 101/211] simplify --- .../src/compiler/phases/2-analyze/visitors/AwaitExpression.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 178b81790304..a4b5d00aa821 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -47,9 +47,5 @@ export function AwaitExpression(node, context) { context.state.expression.is_async = true; } - if (tla) { - context.state.analysis.is_async = true; - } - context.next(); } From 69b95e6285f7811e21b640cf9fefcb4cfc716dc4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 09:32:14 -0500 Subject: [PATCH 102/211] tidy up --- .../2-analyze/visitors/AwaitExpression.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index a4b5d00aa821..b189051fb750 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -12,6 +12,7 @@ export function AwaitExpression(node, context) { let preserve_context = tla; if (context.state.expression) { + context.state.expression.is_async = true; suspend = true; // wrap the expression in `(await $.save(...)).restore()` if necessary, @@ -20,14 +21,10 @@ export function AwaitExpression(node, context) { while (i--) { const parent = context.path[i]; - if ( - // @ts-expect-error we could probably use a neater/more robust mechanism - parent.metadata?.expression === context.state.expression || - // @ts-expect-error - parent.metadata?.arguments?.includes(context.state.expression) - ) { - break; - } + // stop walking up when we find a node with metadata, because that + // means we've hit the template node containing the expression + // @ts-expect-error we could probably use a neater/more robust mechanism + if (parent.metadata) break; // TODO make this more accurate — we don't need to call suspend // if this is the last thing that could be read @@ -43,9 +40,5 @@ export function AwaitExpression(node, context) { context.state.analysis.suspenders.set(node, preserve_context); } - if (context.state.expression) { - context.state.expression.is_async = true; - } - context.next(); } From a4f17e139a04d4cbfcdb31db9df0266c33ad45d7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 09:48:49 -0500 Subject: [PATCH 103/211] tidy up --- packages/svelte/src/compiler/phases/2-analyze/index.js | 4 ++-- .../phases/2-analyze/visitors/AwaitExpression.js | 10 +++++----- .../3-transform/client/visitors/AwaitExpression.js | 5 +++-- .../3-transform/server/visitors/AwaitExpression.js | 7 +------ packages/svelte/src/compiler/phases/types.d.ts | 4 ++-- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ae946f083d15..cfef143bbfb5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -266,7 +266,7 @@ export function analyze_module(ast, options) { immutable: true, tracing: analysis.tracing, async_deriveds: new Set(), - suspenders: new Map() + context_preserving_awaits: new Set() }; } @@ -461,7 +461,7 @@ export function analyze_component(root, source, options) { snippet_renderers: new Map(), snippets: new Set(), async_deriveds: new Set(), - suspenders: new Map() + context_preserving_awaits: new Set() }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index b189051fb750..2a27a5f73e0e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -32,12 +32,12 @@ export function AwaitExpression(node, context) { } } - if (suspend) { - if (!context.state.analysis.runes) { - e.legacy_await_invalid(node); - } + if (suspend && !context.state.analysis.runes) { + e.legacy_await_invalid(node); + } - context.state.analysis.suspenders.set(node, preserve_context); + if (preserve_context) { + context.state.analysis.context_preserving_awaits.add(node); } context.next(); 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 index fdfa0c7a0c04..696d6748a467 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -8,7 +8,7 @@ import { get_rune } from '../../../scope.js'; * @param {Context} context */ export function AwaitExpression(node, context) { - const suspend = context.state.analysis.suspenders.get(node); + const suspend = context.state.analysis.context_preserving_awaits.has(node); if (!suspend) { return context.next(); @@ -18,7 +18,8 @@ export function AwaitExpression(node, context) { (n) => n.type === 'VariableDeclaration' && n.declarations.some( - (d) => d.init?.type === 'CallExpression' && get_rune(d.init, context.state.scope) === '$derived' + (d) => + d.init?.type === 'CallExpression' && get_rune(d.init, context.state.scope) === '$derived' ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js index bb6a0e7b45ed..f78aa98185b0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js @@ -7,12 +7,7 @@ import * as b from '../../../../utils/builders.js'; * @param {Context} context */ export function AwaitExpression(node, context) { - // `has`, not `get`, because all top-level await expressions should - // block regardless of whether they need context preservation - // in the client output - const suspend = context.state.analysis.suspenders.has(node); - - if (!suspend) { + if (context.state.scope.function_depth > 1) { return context.next(); } diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index c395080fb015..743b368b9b51 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -43,8 +43,8 @@ export interface Analysis { /** A set of deriveds that contain `await` expressions */ async_deriveds: Set; - /** A map of `await` expressions that should block, and whether they should preserve context */ - suspenders: Map; + /** A map of `await` expressions that should preserve context */ + context_preserving_awaits: Set; } export interface ComponentAnalysis extends Analysis { From 61667385200504fbf4b5dca510042952a44e2bbc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 09:49:02 -0500 Subject: [PATCH 104/211] fix comment --- packages/svelte/src/compiler/phases/types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 743b368b9b51..0be2fa0d7349 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -43,7 +43,7 @@ export interface Analysis { /** A set of deriveds that contain `await` expressions */ async_deriveds: Set; - /** A map of `await` expressions that should preserve context */ + /** A set of `await` expressions that should preserve context */ context_preserving_awaits: Set; } From 46a004eef2be6300d5ae4419d305471d3c0ba477 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 11:46:32 -0500 Subject: [PATCH 105/211] add experimental.async option --- .../docs/98-reference/.generated/compile-errors.md | 8 +++++++- packages/svelte/messages/compile-errors/script.md | 6 +++++- packages/svelte/src/compiler/errors.js | 13 +++++++++++-- packages/svelte/src/compiler/types/index.d.ts | 5 +++++ packages/svelte/src/compiler/validate-options.js | 6 +++++- packages/svelte/types/index.d.ts | 10 ++++++++++ 6 files changed, 43 insertions(+), 5 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index f83c1b47f4ef..91633918d21a 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -444,6 +444,12 @@ Expected token %token% Expected whitespace ``` +### experimental_async + +``` +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` +``` + ### export_undefined ``` @@ -501,7 +507,7 @@ The arguments keyword cannot be used within the template or at the top level of ### legacy_await_invalid ``` -Cannot use `await` at the top level of a component, or in the template, unless in runes mode +Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode ``` ### legacy_export_invalid diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 3f0dc21d1303..2cd12311bc01 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -70,6 +70,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > `$effect()` can only be used as an expression statement +## experimental_async + +> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` + ## export_undefined > `%name%` is not defined @@ -100,7 +104,7 @@ This turned out to be buggy and unpredictable, particularly when working with de ## legacy_await_invalid -> Cannot use `await` at the top level of a component, or in the template, unless in runes mode +> Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode ## legacy_export_invalid diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 70dc780e32f0..0453d1fcb841 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -168,6 +168,15 @@ export function effect_invalid_placement(node) { e(node, 'effect_invalid_placement', `\`$effect()\` can only be used as an expression statement\nhttps://svelte.dev/e/effect_invalid_placement`); } +/** + * Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless the `experimental.async` compiler option is `true` + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function experimental_async(node) { + e(node, 'experimental_async', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async`); +} + /** * `%name%` is not defined * @param {null | number | NodeLike} node @@ -234,12 +243,12 @@ export function invalid_arguments_usage(node) { } /** - * Cannot use `await` at the top level of a component, or in the template, unless in runes mode + * Cannot use `await` in deriveds and template expressions, or at the top level of a component, unless in runes mode * @param {null | number | NodeLike} node * @returns {never} */ export function legacy_await_invalid(node) { - e(node, 'legacy_await_invalid', `Cannot use \`await\` at the top level of a component, or in the template, unless in runes mode\nhttps://svelte.dev/e/legacy_await_invalid`); + e(node, 'legacy_await_invalid', `Cannot use \`await\` in deriveds and template expressions, or at the top level of a component, unless in runes mode\nhttps://svelte.dev/e/legacy_await_invalid`); } /** diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 2f5ec226bf17..0fbcd155bd47 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -212,6 +212,11 @@ export interface ModuleCompileOptions { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } // The following two somewhat scary looking types ensure that certain types are required but can be undefined still diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js index ab932ed5bca1..7fe664e9aea4 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -41,7 +41,11 @@ const common = { return input; }), - warningFilter: fun(() => true) + warningFilter: fun(() => true), + + experimental: object({ + async: boolean(false) + }) }; export const validate_module_options = diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d00b2b01ed18..7b27d0ddb722 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -933,6 +933,11 @@ declare module 'svelte/compiler' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` @@ -2635,6 +2640,11 @@ declare module 'svelte/types/compiler/interfaces' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning_1) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` From 76314039eabd811b3afd805e03f570be4f061097 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 11:52:34 -0500 Subject: [PATCH 106/211] fix --- .../src/compiler/phases/2-analyze/index.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index cfef143bbfb5..98ff1cd3dcc9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -245,7 +245,17 @@ export function analyze_module(ast, options) { } } - const analysis = { runes: true, tracing: false }; + /** @type {Analysis} */ + const analysis = { + module: { ast, scope, scopes, is_async }, + name: options.filename, + accessors: false, + runes: true, + immutable: true, + tracing: false, + async_deriveds: new Set(), + context_preserving_awaits: new Set() + }; walk( /** @type {Node} */ (ast), @@ -258,16 +268,7 @@ export function analyze_module(ast, options) { visitors ); - return { - module: { ast, scope, scopes, is_async }, - name: options.filename, - accessors: false, - runes: true, - immutable: true, - tracing: analysis.tracing, - async_deriveds: new Set(), - context_preserving_awaits: new Set() - }; + return analysis; } /** From 18b062c63592027e2166041dc1697e0afe6cdd7c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 18:13:10 +0000 Subject: [PATCH 107/211] simplify pending boundary detection --- .../internal/client/dom/blocks/boundary.js | 44 +++++++------------ .../svelte/src/internal/client/runtime.js | 4 +- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index f8793abe9413..9ca61c07c2d6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -76,19 +76,12 @@ export function boundary(node, props, boundary_fn) { var async_fragment = null; var async_count = 0; - /** @type {Effect | null} */ - var parent_boundary = /** @type {Effect} */ (active_effect).parent; - - while (parent_boundary !== null && (parent_boundary.f & BOUNDARY_EFFECT) === 0) { - parent_boundary = parent_boundary.parent; - } - block(() => { var boundary = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; var is_creating_fallback = false; - const render_snippet = (/** @type { () => void } */ snippet_fn) => { + var render_snippet = (/** @type { () => void } */ snippet_fn) => { with_boundary(boundary, () => { is_creating_fallback = true; @@ -107,18 +100,9 @@ export function boundary(node, props, boundary_fn) { // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input) => { - let pending = props.pending; + let pending = /** @type {(anchor: Node) => void} */ (props.pending); if (input === ASYNC_INCREMENT) { - if (!pending) { - if (!parent_boundary) { - e.await_outside_boundary(); - } - - // @ts-ignore - return parent_boundary.fn(input); - } - if (async_count++ === 0) { queue_boundary_micro_task(() => { if (async_effect || !boundary_effect) { @@ -159,15 +143,6 @@ export function boundary(node, props, boundary_fn) { } if (input === ASYNC_DECREMENT) { - if (!pending) { - if (!parent_boundary) { - e.await_outside_boundary(); - } - - // @ts-ignore - return parent_boundary.fn(input); - } - if (--async_count === 0) { queue_boundary_micro_task(() => { if (!async_effect) { @@ -229,6 +204,11 @@ export function boundary(node, props, boundary_fn) { } }; + if (props.pending) { + // @ts-ignore + boundary.fn.pending = true; + } + if (hydrating) { hydrate_next(); } @@ -285,11 +265,19 @@ export function capture() { }; } +/** + * @param {Effect} boundary + */ +export function is_pending_boundary(boundary) { + // @ts-ignore + return boundary.fn.pending; +} + export function suspend() { var boundary = active_effect; while (boundary !== null) { - if ((boundary.f & BOUNDARY_EFFECT) !== 0) { + if ((boundary.f & BOUNDARY_EFFECT) !== 0 && is_pending_boundary(boundary)) { break; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 020130fefaf1..3e08eb39c20b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -43,6 +43,7 @@ import { lifecycle_outside_component } from '../shared/errors.js'; import { FILENAME } from '../../constants.js'; import { legacy_mode_flag, tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; +import { is_pending_boundary } from './dom/blocks/boundary.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -873,8 +874,7 @@ function process_effects(effect, collected_effects) { if (effect === parent) { break main_loop; } - // TODO: we need to know that this boundary has a valid `pending` - if (suspended && (parent.f & BOUNDARY_EFFECT) !== 0) { + if (suspended && (parent.f & BOUNDARY_EFFECT) !== 0 && is_pending_boundary(parent)) { suspended = false; } var parent_sibling = parent.next; From 38934893df36f3d6327bbdcfb7de149d323bcf0b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 18:49:30 +0000 Subject: [PATCH 108/211] fix bug --- .../svelte/src/internal/client/dom/blocks/boundary.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 9ca61c07c2d6..7078e23913f9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -204,10 +204,8 @@ export function boundary(node, props, boundary_fn) { } }; - if (props.pending) { - // @ts-ignore - boundary.fn.pending = true; - } + // @ts-ignore + boundary.fn.is_pending = () => props.pending; if (hydrating) { hydrate_next(); @@ -270,7 +268,7 @@ export function capture() { */ export function is_pending_boundary(boundary) { // @ts-ignore - return boundary.fn.pending; + return boundary.fn.is_pending(); } export function suspend() { From 3dd1d30d90844f565e1a62a26fc40d85c12fa5b7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 15:02:23 -0500 Subject: [PATCH 109/211] remove script_suspend in favour of component-level suspending --- .../3-transform/client/transform-client.js | 6 ++++- .../client/visitors/AwaitExpression.js | 16 +---------- .../internal/client/dom/blocks/boundary.js | 27 +++---------------- packages/svelte/src/internal/client/index.js | 2 +- 4 files changed, 10 insertions(+), 41 deletions(-) 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 3bfde4292c17..869604364ab4 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 @@ -371,7 +371,11 @@ export function client_component(analysis, options) { const body = b.function_declaration( b.id('$$body'), [b.id('$$anchor'), b.id('$$props')], - b.block([...component_block.body, b.stmt(b.call('$.exit'))]) + b.block([ + b.var('$$unsuspend', b.call('$.suspend')), + ...component_block.body, + b.stmt(b.call('$$unsuspend')) + ]) ); body.async = true; 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 index 696d6748a467..7a7ca628a84a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -1,7 +1,6 @@ /** @import { AwaitExpression, Expression } from 'estree' */ /** @import { Context } from '../types' */ import * as b from '../../../../utils/builders.js'; -import { get_rune } from '../../../scope.js'; /** * @param {AwaitExpression} node @@ -14,22 +13,9 @@ export function AwaitExpression(node, context) { return context.next(); } - const inside_derived = context.path.some( - (n) => - n.type === 'VariableDeclaration' && - n.declarations.some( - (d) => - d.init?.type === 'CallExpression' && get_rune(d.init, context.state.scope) === '$derived' - ) - ); - - const expression = b.call( + return b.call( b.await( b.call('$.save', node.argument && /** @type {Expression} */ (context.visit(node.argument))) ) ); - - return inside_derived - ? expression - : b.await(b.call('$.script_suspend', b.arrow([], expression, true))); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 7078e23913f9..f9d2d180d5cf 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -243,23 +243,18 @@ export function boundary(node, props, boundary_fn) { } } -// TODO separate this stuff out — suspending and context preservation should -// be distinct concepts - export function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; - return function restore(should_exit = true) { + return function restore() { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); // prevent the active effect from outstaying its welcome - if (should_exit) { - queue_post_micro_task(exit); - } + queue_post_micro_task(exit); }; } @@ -295,22 +290,6 @@ export function suspend() { }; } -/** - * @template T - * @param {() => Promise} fn - */ -export async function script_suspend(fn) { - const restore = capture(); - const unsuspend = suspend(); - try { - exit(); - return await fn(); - } finally { - restore(false); - unsuspend(); - } -} - /** * @template T * @param {Promise} promise @@ -326,7 +305,7 @@ export async function save(promise) { }; } -export function exit() { +function exit() { set_active_effect(null); set_active_reaction(null); set_component_context(null); diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index cf164fde266e..5c388b19d2a5 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -130,7 +130,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, exit, save, suspend, script_suspend } from './dom/blocks/boundary.js'; +export { boundary, save, suspend } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, From 7907d1d04a9bee9d1f688797bc534915633ff972 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 15:13:12 -0500 Subject: [PATCH 110/211] await derived in module --- .../samples/async-derived-module/Child.svelte | 20 ++++++ .../samples/async-derived-module/_config.js | 65 +++++++++++++++++++ .../samples/async-derived-module/main.svelte | 15 +++++ .../async-derived-module/state.svelte.js | 9 +++ 4 files changed, 109 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte new file mode 100644 index 000000000000..f803a30c37f9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte @@ -0,0 +1,20 @@ + + +

{derived.value}{console.log(`template ${derived.value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js new file mode 100644 index 000000000000..b81f2a192a7f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -0,0 +1,65 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise, + num: 1 + }; + }, + + async test({ assert, target, component, logs }) { + d.resolve(42); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + await tick(); + assert.htmlEqual(target.innerHTML, '

42

'); + + component.num = 2; + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

84

'); + + d = deferred(); + component.promise = d.promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve(43); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

86

'); + + assert.deepEqual(logs, [ + 'outside boundary 1', + '$effect.pre 42 1', + 'template 42 1', + '$effect 42 1', + 'outside boundary 2', + '$effect.pre 84 2', + 'template 84 2', + '$effect 84 2', + '$effect.pre 86 2', + 'template 86 2', + '$effect 86 2' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte new file mode 100644 index 000000000000..e90bbf720ed3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte @@ -0,0 +1,15 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${num}`)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js new file mode 100644 index 000000000000..a53fbb8c6fc5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js @@ -0,0 +1,9 @@ +export async function create_derived(get_promise, get_num) { + let value = $derived((await get_promise()) * get_num()); + + return { + get value() { + return value; + } + }; +} From 00107cbfcfe3d5f396ec7732f69ab2e27fc86569 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Jan 2025 20:20:02 +0000 Subject: [PATCH 111/211] fix effect bug --- packages/svelte/src/internal/client/reactivity/effects.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 0ee2352a2d91..1ad505acafa6 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -99,6 +99,10 @@ function create_effect(type, fn, sync, push = true) { } } + if (parent_effect !== null && (parent_effect.f & INERT) !== 0) { + type |= INERT; + } + /** @type {Effect} */ var effect = { ctx: component_context, From b984bf076294e7470e01af93158c7fbca23d5eb4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 15:34:03 -0500 Subject: [PATCH 112/211] add experimental option --- packages/svelte/src/compiler/phases/2-analyze/index.js | 4 +++- .../phases/2-analyze/visitors/AwaitExpression.js | 10 ++++++++-- packages/svelte/tests/helpers.js | 3 ++- packages/svelte/tests/runtime-legacy/shared.ts | 3 +++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 98ff1cd3dcc9..73b459958b6a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -263,7 +263,9 @@ export function analyze_module(ast, options) { scope, scopes, // @ts-expect-error TODO - analysis + analysis, + // @ts-expect-error TODO + options }, visitors ); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 2a27a5f73e0e..5e7710f802b4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -32,8 +32,14 @@ export function AwaitExpression(node, context) { } } - if (suspend && !context.state.analysis.runes) { - e.legacy_await_invalid(node); + if (suspend) { + if (!context.state.options.experimental.async) { + e.experimental_async(node); + } + + if (!context.state.analysis.runes) { + e.legacy_await_invalid(node); + } } if (preserve_context) { diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 9d7f71c9bd63..7fac5e5e5845 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -86,7 +86,8 @@ export async function compile_directory( const compiled = compileModule(text, { filename: opts.filename, generate: opts.generate, - dev: opts.dev + dev: opts.dev, + experimental: opts.experimental }); write(out, compiled.js.code.replace(`v${VERSION}`, 'VERSION')); } else { diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index e6dc0f385bf9..4b4e62fba2ba 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -157,6 +157,9 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run rootDir: cwd, dev: force_hmr ? true : undefined, hmr: force_hmr ? true : undefined, + experimental: { + async: true + }, ...config.compileOptions, immutable: config.immutable, accessors: 'accessors' in config ? config.accessors : true, From 4782a892b549bd3fc3d5f6fe7ac93f83e81e5cf8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 18:32:13 -0500 Subject: [PATCH 113/211] revert whatever this was --- .../tests/runtime-runes/samples/bind-this-no-state/_config.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js index 19af552f0c88..6d428f630659 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-no-state/_config.js @@ -26,22 +26,18 @@ export default test({ await btn1?.click(); await tick(); - await tick(); assert.htmlEqual(target.innerHTML, get_html(1)); await btn2?.click(); await tick(); - await tick(); assert.htmlEqual(target.innerHTML, get_html(2)); await btn1?.click(); await tick(); - await tick(); assert.htmlEqual(target.innerHTML, get_html(1)); await btn3?.click(); await tick(); - await tick(); assert.htmlEqual(target.innerHTML, get_html(3)); } }); From 65385c277f275024f314f611c12fd5a83ae2f9fa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 18:38:37 -0500 Subject: [PATCH 114/211] revert rename --- packages/svelte/src/internal/client/dom/blocks/await.js | 4 ++-- packages/svelte/src/internal/client/dom/blocks/boundary.js | 4 ++-- packages/svelte/src/internal/client/dom/blocks/each.js | 4 ++-- packages/svelte/src/internal/client/dom/css.js | 6 +++--- .../src/internal/client/dom/elements/bindings/input.js | 6 +++--- .../src/internal/client/dom/elements/bindings/this.js | 4 ++-- packages/svelte/src/internal/client/dom/elements/events.js | 4 ++-- packages/svelte/src/internal/client/dom/elements/misc.js | 4 ++-- .../svelte/src/internal/client/dom/elements/transitions.js | 4 ++-- packages/svelte/src/internal/client/dom/task.js | 2 +- packages/svelte/src/internal/client/reactivity/deriveds.js | 2 +- packages/svelte/tests/animation-helpers.js | 4 ++-- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 788afa1921b3..62b2e4dd0cda 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -13,7 +13,7 @@ import { set_dev_current_component_function } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { queue_post_micro_task } from '../task.js'; +import { queue_micro_task } from '../task.js'; import { UNINITIALIZED } from '../../../../constants.js'; const PENDING = 0; @@ -148,7 +148,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } else { // Wait a microtask before checking if we should show the pending state as // the promise might have resolved by the next microtask. - queue_post_micro_task(() => { + queue_micro_task(() => { if (!resolved) update(PENDING, true); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index f9d2d180d5cf..8479a4ca6f91 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -27,7 +27,7 @@ import { set_hydrate_node } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; -import { queue_boundary_micro_task, queue_post_micro_task } from '../task.js'; +import { queue_boundary_micro_task, queue_micro_task } from '../task.js'; import * as e from '../../../shared/errors.js'; const ASYNC_INCREMENT = Symbol(); @@ -254,7 +254,7 @@ export function capture() { set_component_context(previous_component_context); // prevent the active effect from outstaying its welcome - queue_post_micro_task(exit); + queue_micro_task(exit); }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index ce75c480a13b..040e58521548 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -34,7 +34,7 @@ import { import { source, mutable_source, internal_set } from '../../reactivity/sources.js'; import { array_from, is_array } from '../../../shared/utils.js'; import { INERT } from '../../constants.js'; -import { queue_post_micro_task } from '../task.js'; +import { queue_micro_task } from '../task.js'; import { active_effect, active_reaction, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; @@ -470,7 +470,7 @@ function reconcile(array, state, anchor, render_fn, flags, is_inert, get_key, ge } if (is_animated) { - queue_post_micro_task(() => { + queue_micro_task(() => { if (to_animate === undefined) return; for (item of to_animate) { item.a?.apply(); diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index d4340a07eef6..52be36aa1f46 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -1,5 +1,5 @@ import { DEV } from 'esm-env'; -import { queue_post_micro_task } from './task.js'; +import { queue_micro_task } from './task.js'; import { register_style } from '../dev/css.js'; /** @@ -7,8 +7,8 @@ import { register_style } from '../dev/css.js'; * @param {{ hash: string, code: string }} css */ export function append_styles(anchor, css) { - // Use `queue_post_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results - queue_post_micro_task(() => { + // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results + queue_micro_task(() => { var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 166dcbc7388d..3ea1a24d7edc 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -3,7 +3,7 @@ import { render_effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import * as e from '../../../errors.js'; import { is } from '../../../proxy.js'; -import { queue_post_micro_task } from '../../task.js'; +import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { is_runes, untrack } from '../../../runtime.js'; @@ -158,14 +158,14 @@ export function bind_group(inputs, group_index, input, get, set = get) { if (!pending.has(binding_group)) { pending.add(binding_group); - queue_post_micro_task(() => { + queue_micro_task(() => { // necessary to maintain binding group order in all insertion scenarios binding_group.sort((a, b) => (a.compareDocumentPosition(b) === 4 ? -1 : 1)); pending.delete(binding_group); }); } - queue_post_micro_task(() => { + queue_micro_task(() => { if (hydration_mismatch) { var value; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index 0ca5039e7c69..56b0a56e71c4 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,7 +1,7 @@ import { STATE_SYMBOL } from '../../../constants.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; -import { queue_post_micro_task } from '../../task.js'; +import { queue_micro_task } from '../../task.js'; /** * @param {any} bound_value @@ -49,7 +49,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part return () => { // We cannot use effects in the teardown phase, we we use a microtask instead. - queue_post_micro_task(() => { + queue_micro_task(() => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index c2b7901f49a3..363b8e1ed501 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -2,7 +2,7 @@ import { teardown } from '../../reactivity/effects.js'; import { define_property, is_array } from '../../../shared/utils.js'; import { hydrating } from '../hydration.js'; -import { queue_post_micro_task } from '../task.js'; +import { queue_micro_task } from '../task.js'; import { FILENAME } from '../../../../constants.js'; import * as w from '../../warnings.js'; import { @@ -77,7 +77,7 @@ export function create_event(event_name, dom, handler, options = {}) { event_name.startsWith('touch') || event_name === 'wheel' ) { - queue_post_micro_task(() => { + queue_micro_task(() => { dom.addEventListener(event_name, target_handler, options); }); } else { diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index dab8e84c32f6..61e513903f76 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,6 @@ import { hydrating } from '../hydration.js'; import { clear_text_content, get_first_child } from '../operations.js'; -import { queue_post_micro_task } from '../task.js'; +import { queue_micro_task } from '../task.js'; /** * @param {HTMLElement} dom @@ -12,7 +12,7 @@ export function autofocus(dom, value) { const body = document.body; dom.autofocus = true; - queue_post_micro_task(() => { + queue_micro_task(() => { if (document.activeElement === body) { dom.focus(); } diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 0dd17fad9ff4..b3c16cdd080f 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -13,7 +13,7 @@ import { should_intro } from '../../render.js'; import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; -import { queue_post_micro_task } from '../task.js'; +import { queue_micro_task } from '../task.js'; /** * @param {Element} element @@ -326,7 +326,7 @@ function animate(element, options, counterpart, t2, on_finish) { var a; var aborted = false; - queue_post_micro_task(() => { + queue_micro_task(() => { if (aborted) return; var o = options({ direction: is_intro ? 'in' : 'out' }); a = animate(element, o, counterpart, t2, on_finish); diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 8b16b30ebead..73e88564b365 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -59,7 +59,7 @@ export function queue_boundary_micro_task(fn) { /** * @param {() => void} fn */ -export function queue_post_micro_task(fn) { +export function queue_micro_task(fn) { if (!is_micro_task_queued) { is_micro_task_queued = true; queueMicrotask(flush_all_micro_tasks); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b6954e5c93c9..5abbc1867c5e 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -145,7 +145,7 @@ export function async_derived(fn) { async_deps.add(value); // TODO we want to clear this after we've updated effects. - // `queue_post_micro_task` appears to run too early. + // `queue_micro_task` appears to run too early. // for now, as a POC, use setTimeout setTimeout(() => { async_deps.delete(value); diff --git a/packages/svelte/tests/animation-helpers.js b/packages/svelte/tests/animation-helpers.js index e37c2563af5e..dcbb06292305 100644 --- a/packages/svelte/tests/animation-helpers.js +++ b/packages/svelte/tests/animation-helpers.js @@ -1,6 +1,6 @@ import { flushSync } from 'svelte'; import { raf as svelte_raf } from 'svelte/internal/client'; -import { queue_post_micro_task } from '../src/internal/client/dom/task.js'; +import { queue_micro_task } from '../src/internal/client/dom/task.js'; export const raf = { animations: new Set(), @@ -132,7 +132,7 @@ class Animation { /** @param {() => {}} fn */ set onfinish(fn) { if (this.#duration === 0) { - queue_post_micro_task(fn); + queue_micro_task(fn); } else { this.#onfinish = () => { fn(); From b16f21a41d8988d33a87fcd874b02f6f8353435e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Jan 2025 18:42:21 -0500 Subject: [PATCH 115/211] unused --- .../phases/3-transform/client/visitors/shared/declarations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd46b8e3671c..0bd8c352f6a9 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 { CallExpression, Identifier } from 'estree' */ +/** @import { Identifier } from 'estree' */ /** @import { ComponentContext, Context } from '../../types' */ import { is_state_source } from '../../utils.js'; import * as b from '../../../../../utils/builders.js'; From 197acef8db0efd7ab8c63f34bd2a73fda2126506 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Jan 2025 07:09:22 -0500 Subject: [PATCH 116/211] =?UTF-8?q?waterfall=20detection=20is=20overzealou?= =?UTF-8?q?s=20=E2=80=94=20remove=20it=20for=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/client/reactivity/deriveds.js | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5abbc1867c5e..bff8f32d3464 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -81,9 +81,6 @@ export function derived(fn) { return signal; } -// Used for waterfall detection -var async_deps = new Set(); - /** * @template V * @param {() => Promise} fn @@ -100,12 +97,9 @@ export function async_derived(fn) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var value = source(/** @type {V} */ (undefined)); - var current_deps = new Set(async_deps); - var derived_promise = derived(fn); block(async () => { - var effect = /** @type {Effect} */ (active_effect); var current = (promise = get(derived_promise)); var restore = capture(); @@ -114,24 +108,6 @@ export function async_derived(fn) { try { var v = await promise; - // check to see if we just created an unnecessary waterfall - if (current_deps.size > 0) { - var justified = false; - - if (effect.deps !== null) { - for (const dep of effect.deps) { - if (current_deps.has(dep)) { - justified = true; - break; - } - } - } - - if (!justified) { - w.await_waterfall(); - } - } - if ((parent.f & DESTROYED) !== 0) { return; } @@ -139,17 +115,6 @@ export function async_derived(fn) { if (promise === current) { restore(); internal_set(value, v); - - // make a note that we're updating this derived, - // so that we can detect waterfalls - async_deps.add(value); - - // TODO we want to clear this after we've updated effects. - // `queue_micro_task` appears to run too early. - // for now, as a POC, use setTimeout - setTimeout(() => { - async_deps.delete(value); - }); } } catch (e) { handle_error(e, parent, null, parent.ctx); From c16abcf79a7e93cf306f911493ff8c61eb5858ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Jan 2025 07:15:30 -0500 Subject: [PATCH 117/211] unused --- .../phases/3-transform/client/visitors/shared/component.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 55f632e53054..52bac3cb307d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -4,12 +4,7 @@ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { - build_bind_this, - get_expression_id, - memoize_expression, - validate_binding -} from '../shared/utils.js'; +import { build_bind_this, get_expression_id, validate_binding } from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; From 08c7e7bcabd4a5d0679eb55ae3e0e0705853555c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Jan 2025 07:24:27 -0500 Subject: [PATCH 118/211] use experimental.async in sandbox and migrate --- packages/svelte/src/compiler/migrate/index.js | 5 ++++- playgrounds/sandbox/run.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 1bb7a69a20f9..b828b745a57a 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -146,7 +146,10 @@ export function migrate(source, { filename, use_ts } = {}) { ...validate_component_options({}, ''), ...parsed_options, customElementOptions, - filename: filename ?? '(unknown)' + filename: filename ?? '(unknown)', + experimental: { + async: true + } }; const str = new MagicString(source); diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 771dcc668eed..1a498fb05bd2 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -67,7 +67,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { dev: true, filename: input, generate, - runes: argv.values.runes + runes: argv.values.runes, + experimental: { + async: true + } }); for (const warning of compiled.warnings) { @@ -94,7 +97,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { const compiled = compileModule(source, { dev: true, filename: input, - generate + generate, + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}`; From 99998926e4fb203d60689493ff00b0b5510302a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Jan 2025 07:37:34 -0500 Subject: [PATCH 119/211] fix sandbox --- playgrounds/sandbox/vite.config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playgrounds/sandbox/vite.config.js b/playgrounds/sandbox/vite.config.js index c6c07ce7c65d..80a635a23960 100644 --- a/playgrounds/sandbox/vite.config.js +++ b/playgrounds/sandbox/vite.config.js @@ -11,7 +11,10 @@ export default defineConfig({ inspect(), svelte({ compilerOptions: { - hmr: false + hmr: false, + experimental: { + async: true + } } }) ], From a0c8e7100563de70c65924f4f6b54c27fecd7fa9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Jan 2025 09:08:50 -0500 Subject: [PATCH 120/211] tidy up --- packages/svelte/src/internal/client/runtime.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3e08eb39c20b..40a52a4aeca0 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -761,12 +761,13 @@ function flush_queued_effects(effects) { } } -function flushed_deferred() { +function flush_deferred() { is_micro_task_queued = false; + if (flush_count > 1001) { return; } - // flush_before_process_microtasks(); + const previous_queued_root_effects = queued_root_effects; queued_root_effects = []; flush_queued_root_effects(previous_queued_root_effects); @@ -774,6 +775,7 @@ function flushed_deferred() { if (!is_micro_task_queued) { flush_count = 0; last_scheduled_effect = null; + if (DEV) { dev_effect_stack = []; } @@ -788,7 +790,7 @@ export function schedule_effect(signal) { if (scheduler_mode === FLUSH_MICROTASK) { if (!is_micro_task_queued) { is_micro_task_queued = true; - queueMicrotask(flushed_deferred); + queueMicrotask(flush_deferred); } } From a2cbfe2b1543af35f5a3b907b31cd94eb9d66e08 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Jan 2025 09:12:40 -0500 Subject: [PATCH 121/211] block only runs once, put vars inside --- .../src/internal/client/dom/blocks/boundary.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 8479a4ca6f91..d832c4d354b3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -68,15 +68,17 @@ function with_boundary(boundary, fn) { export function boundary(node, props, boundary_fn) { var anchor = node; - /** @type {Effect} */ - var boundary_effect; - /** @type {Effect | null} */ - var async_effect = null; - /** @type {DocumentFragment | null} */ - var async_fragment = null; - var async_count = 0; - block(() => { + /** @type {Effect} */ + var boundary_effect; + + /** @type {Effect | null} */ + var async_effect = null; + + /** @type {DocumentFragment | null} */ + var async_fragment = null; + + var async_count = 0; var boundary = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; var is_creating_fallback = false; From b9a3f1e207702ad7d36290823f2f4414dc69dd71 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 25 Jan 2025 20:23:24 +0000 Subject: [PATCH 122/211] cleanup and simplify --- packages/svelte/src/internal/client/reactivity/deriveds.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index bff8f32d3464..a5f5420968da 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -25,13 +25,11 @@ import { } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; -import * as w from '../warnings.js'; import { block, destroy_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 { capture, suspend } from '../dom/blocks/boundary.js'; -import { flush_boundary_micro_tasks } from '../dom/task.js'; /** * @template V @@ -97,10 +95,8 @@ export function async_derived(fn) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var value = source(/** @type {V} */ (undefined)); - var derived_promise = derived(fn); - block(async () => { - var current = (promise = get(derived_promise)); + var current = (promise = fn()); var restore = capture(); var unsuspend = suspend(); From 5a4b11b78b8f8dbb94ebafbf89d9bc266c9b8e8b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 27 Jan 2025 23:42:18 +0000 Subject: [PATCH 123/211] fix leak --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index d832c4d354b3..aa8af3a71c33 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -256,7 +256,7 @@ export function capture() { set_component_context(previous_component_context); // prevent the active effect from outstaying its welcome - queue_micro_task(exit); + queue_boundary_micro_task(exit); }; } From 1c4db3d341bb7bd8a4d4a88989e9c3026707d2c7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 21:22:39 -0500 Subject: [PATCH 124/211] hoist functions, use names to make stuff a little clearer --- .../internal/client/dom/blocks/boundary.js | 135 ++++++++++-------- 1 file changed, 73 insertions(+), 62 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index aa8af3a71c33..976c1eb7720a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -83,7 +83,10 @@ export function boundary(node, props, boundary_fn) { var hydrate_open = hydrate_node; var is_creating_fallback = false; - var render_snippet = (/** @type { () => void } */ snippet_fn) => { + /** + * @param {() => void} snippet_fn + */ + function render_snippet(snippet_fn) { with_boundary(boundary, () => { is_creating_fallback = true; @@ -98,69 +101,87 @@ export function boundary(node, props, boundary_fn) { reset_is_throwing_error(); is_creating_fallback = false; }); - }; + } + + function suspend() { + if (async_effect || !boundary_effect) { + return; + } + + var effect = boundary_effect; + async_effect = boundary_effect; + + pause_effect( + async_effect, + () => { + /** @type {TemplateNode | null} */ + var node = effect.nodes_start; + var end = effect.nodes_end; + async_fragment = document.createDocumentFragment(); + + while (node !== null) { + /** @type {TemplateNode | null} */ + var sibling = + node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + node.remove(); + async_fragment.append(node); + node = sibling; + } + }, + false + ); + + const pending = props.pending; + + if (pending) { + render_snippet(() => { + pending(anchor); + }); + } + } + + function unsuspend() { + if (!async_effect) { + return; + } + + if (boundary_effect) { + destroy_effect(boundary_effect); + } + + boundary_effect = async_effect; + async_effect = null; + anchor.before(/** @type {DocumentFragment} */ (async_fragment)); + resume_effect(boundary_effect); + } + + function reset() { + pause_effect(boundary_effect); + + with_boundary(boundary, () => { + is_creating_fallback = false; + boundary_effect = branch(() => boundary_fn(anchor)); + reset_is_throwing_error(); + }); + } // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input) => { - let pending = /** @type {(anchor: Node) => void} */ (props.pending); - if (input === ASYNC_INCREMENT) { if (async_count++ === 0) { - queue_boundary_micro_task(() => { - if (async_effect || !boundary_effect) { - return; - } - - var effect = boundary_effect; - async_effect = boundary_effect; - - pause_effect( - async_effect, - () => { - /** @type {TemplateNode | null} */ - var node = effect.nodes_start; - var end = effect.nodes_end; - async_fragment = document.createDocumentFragment(); - - while (node !== null) { - /** @type {TemplateNode | null} */ - var sibling = - node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - - node.remove(); - async_fragment.append(node); - node = sibling; - } - }, - false - ); - - render_snippet(() => { - pending(anchor); - }); - }); + queue_boundary_micro_task(suspend); } - return true; + return; } if (input === ASYNC_DECREMENT) { if (--async_count === 0) { - queue_boundary_micro_task(() => { - if (!async_effect) { - return; - } - if (boundary_effect) { - destroy_effect(boundary_effect); - } - boundary_effect = async_effect; - async_effect = null; - anchor.before(/** @type {DocumentFragment} */ (async_fragment)); - resume_effect(boundary_effect); - }); + queue_boundary_micro_task(unsuspend); } - return true; + return; } var error = input; @@ -169,20 +190,10 @@ export function boundary(node, props, boundary_fn) { // If we have nothing to capture the error, or if we hit an error while // rendering the fallback, re-throw for another boundary to handle - if ((!onerror && !failed) || is_creating_fallback) { + if (is_creating_fallback || (!onerror && !failed)) { throw error; } - var reset = () => { - pause_effect(boundary_effect); - - with_boundary(boundary, () => { - is_creating_fallback = false; - boundary_effect = branch(() => boundary_fn(anchor)); - reset_is_throwing_error(); - }); - }; - onerror?.(error, reset); if (boundary_effect) { From 36e281c8c97a9f43341ae77575c57921b649a54a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 21:24:40 -0500 Subject: [PATCH 125/211] boundary_fn -> children --- .../svelte/src/internal/client/dom/blocks/boundary.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 976c1eb7720a..25359ba2c471 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -62,10 +62,10 @@ function with_boundary(boundary, fn) { * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void * pending?: (anchor: Node) => void * }} props - * @param {((anchor: Node) => void)} boundary_fn + * @param {((anchor: Node) => void)} children * @returns {void} */ -export function boundary(node, props, boundary_fn) { +export function boundary(node, props, children) { var anchor = node; block(() => { @@ -161,7 +161,7 @@ export function boundary(node, props, boundary_fn) { with_boundary(boundary, () => { is_creating_fallback = false; - boundary_effect = branch(() => boundary_fn(anchor)); + boundary_effect = branch(() => children(anchor)); reset_is_throwing_error(); }); } @@ -241,11 +241,11 @@ export function boundary(node, props, boundary_fn) { queueMicrotask(() => { destroy_effect(boundary_effect); with_boundary(boundary, () => { - boundary_effect = branch(() => boundary_fn(anchor)); + boundary_effect = branch(() => children(anchor)); }); }); } else { - boundary_effect = branch(() => boundary_fn(anchor)); + boundary_effect = branch(() => children(anchor)); } reset_is_throwing_error(); From adb137579f8c8df146ab2979d51d36b79d020eed Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 21:34:37 -0500 Subject: [PATCH 126/211] =?UTF-8?q?rename=20async=5Feffect/fragment=20to?= =?UTF-8?q?=20offscreen=5Feffect/fragment=20=E2=80=94=20much=20clearer=20I?= =?UTF-8?q?MHO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/client/dom/blocks/boundary.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 25359ba2c471..011f8dddc36d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -73,10 +73,10 @@ export function boundary(node, props, children) { var boundary_effect; /** @type {Effect | null} */ - var async_effect = null; + var offscreen_effect = null; /** @type {DocumentFragment | null} */ - var async_fragment = null; + var offscreen_fragment = null; var async_count = 0; var boundary = /** @type {Effect} */ (active_effect); @@ -104,20 +104,20 @@ export function boundary(node, props, children) { } function suspend() { - if (async_effect || !boundary_effect) { + if (offscreen_effect || !boundary_effect) { return; } var effect = boundary_effect; - async_effect = boundary_effect; + offscreen_effect = boundary_effect; pause_effect( - async_effect, + boundary_effect, () => { /** @type {TemplateNode | null} */ var node = effect.nodes_start; var end = effect.nodes_end; - async_fragment = document.createDocumentFragment(); + offscreen_fragment = document.createDocumentFragment(); while (node !== null) { /** @type {TemplateNode | null} */ @@ -125,7 +125,7 @@ export function boundary(node, props, children) { node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); node.remove(); - async_fragment.append(node); + offscreen_fragment.append(node); node = sibling; } }, @@ -142,7 +142,7 @@ export function boundary(node, props, children) { } function unsuspend() { - if (!async_effect) { + if (!offscreen_effect) { return; } @@ -150,9 +150,9 @@ export function boundary(node, props, children) { destroy_effect(boundary_effect); } - boundary_effect = async_effect; - async_effect = null; - anchor.before(/** @type {DocumentFragment} */ (async_fragment)); + boundary_effect = offscreen_effect; + offscreen_effect = null; + anchor.before(/** @type {DocumentFragment} */ (offscreen_fragment)); resume_effect(boundary_effect); } From 6b5d6c05b9f7f45911f4f5c85ed847a7c8ab4722 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 21:41:56 -0500 Subject: [PATCH 127/211] remove unnecessary function wrapper --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 011f8dddc36d..cb015085ba18 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -91,9 +91,7 @@ export function boundary(node, props, children) { is_creating_fallback = true; try { - boundary_effect = branch(() => { - snippet_fn(); - }); + boundary_effect = branch(snippet_fn); } catch (error) { handle_error(error, boundary, null, boundary.ctx); } From 9c00acd5da3ec78f44af42089fcf0d36cf2b05ba Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 21:53:06 -0500 Subject: [PATCH 128/211] no need to explicitly remove --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index cb015085ba18..d1429bbfae04 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -119,12 +119,10 @@ export function boundary(node, props, children) { while (node !== null) { /** @type {TemplateNode | null} */ - var sibling = - node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - node.remove(); offscreen_fragment.append(node); - node = sibling; + node = next; } }, false From 91d09b0d004898a3e49e08f378d5b60446e6624e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 21:54:43 -0500 Subject: [PATCH 129/211] unused --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index d1429bbfae04..1bfefd6f3550 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -27,7 +27,7 @@ import { set_hydrate_node } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; -import { queue_boundary_micro_task, queue_micro_task } from '../task.js'; +import { queue_boundary_micro_task } from '../task.js'; import * as e from '../../../shared/errors.js'; const ASYNC_INCREMENT = Symbol(); From 29a47c23ba2abd061273c8e4254a066f583eafb3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 21:59:09 -0500 Subject: [PATCH 130/211] type annotation is unnecessary --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 1bfefd6f3550..767cb5bd4696 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -112,9 +112,9 @@ export function boundary(node, props, children) { pause_effect( boundary_effect, () => { - /** @type {TemplateNode | null} */ var node = effect.nodes_start; var end = effect.nodes_end; + offscreen_fragment = document.createDocumentFragment(); while (node !== null) { From 056601f1f1c53b02642fd0467d9d03a9b2dc9591 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 27 Jan 2025 22:13:23 -0500 Subject: [PATCH 131/211] there's no point passing to , it's unused --- packages/svelte/src/internal/client/reactivity/effects.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 1ad505acafa6..0f130e0b5118 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -573,7 +573,7 @@ export function pause_effect(effect, callback, destroy = true) { /** @type {TransitionManager[]} */ var transitions = []; - pause_children(effect, transitions, true, destroy); + pause_children(effect, transitions, true); run_out_transitions(transitions, () => { if (destroy) { @@ -605,9 +605,8 @@ export function run_out_transitions(transitions, fn) { * @param {Effect} effect * @param {TransitionManager[]} transitions * @param {boolean} local - * @param {boolean} [destroy] */ -export function pause_children(effect, transitions, local, destroy = true) { +export function pause_children(effect, transitions, local) { if ((effect.f & INERT) !== 0) return; effect.f ^= INERT; @@ -627,7 +626,7 @@ export function pause_children(effect, transitions, local, destroy = true) { // TODO we don't need to call pause_children recursively with a linked list in place // it's slightly more involved though as we have to account for `transparent` changing // through the tree. - pause_children(child, transitions, transparent ? local : false, destroy); + pause_children(child, transitions, transparent ? local : false); child = sibling; } } From 036001c055f3b245be1049af8ade1261717c5a3c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 28 Jan 2025 14:25:39 +0000 Subject: [PATCH 132/211] turn on hmr --- playgrounds/sandbox/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playgrounds/sandbox/vite.config.js b/playgrounds/sandbox/vite.config.js index 80a635a23960..41850fc30913 100644 --- a/playgrounds/sandbox/vite.config.js +++ b/playgrounds/sandbox/vite.config.js @@ -11,7 +11,7 @@ export default defineConfig({ inspect(), svelte({ compilerOptions: { - hmr: false, + hmr: true, experimental: { async: true } From cfba900fb108525d27c33d51bc3492b178d262cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Jan 2025 12:27:17 -0500 Subject: [PATCH 133/211] represent main/pending/failed effects separately, as we do for other blocks --- .../internal/client/dom/blocks/boundary.js | 90 +++++++++++-------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 767cb5bd4696..31936aa5de39 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -35,7 +35,8 @@ const ASYNC_DECREMENT = Symbol(); /** * @param {Effect} boundary - * @param {() => void} fn + * @param {() => Effect | null} fn + * @returns {Effect | null} */ function with_boundary(boundary, fn) { var previous_effect = active_effect; @@ -47,7 +48,7 @@ function with_boundary(boundary, fn) { set_component_context(boundary.ctx); try { - fn(); + return fn(); } finally { set_active_effect(previous_effect); set_active_reaction(previous_reaction); @@ -69,11 +70,14 @@ export function boundary(node, props, children) { var anchor = node; block(() => { - /** @type {Effect} */ - var boundary_effect; + /** @type {Effect | null} */ + var main_effect = null; + + /** @type {Effect | null} */ + var pending_effect = null; /** @type {Effect | null} */ - var offscreen_effect = null; + var failed_effect = null; /** @type {DocumentFragment | null} */ var offscreen_fragment = null; @@ -85,32 +89,33 @@ export function boundary(node, props, children) { /** * @param {() => void} snippet_fn + * @returns {Effect | null} */ function render_snippet(snippet_fn) { - with_boundary(boundary, () => { + return with_boundary(boundary, () => { is_creating_fallback = true; try { - boundary_effect = branch(snippet_fn); + return branch(snippet_fn); } catch (error) { handle_error(error, boundary, null, boundary.ctx); + return null; + } finally { + reset_is_throwing_error(); + is_creating_fallback = false; } - - reset_is_throwing_error(); - is_creating_fallback = false; }); } function suspend() { - if (offscreen_effect || !boundary_effect) { + if (offscreen_fragment || !main_effect) { return; } - var effect = boundary_effect; - offscreen_effect = boundary_effect; + var effect = main_effect; pause_effect( - boundary_effect, + effect, () => { var node = effect.nodes_start; var end = effect.nodes_end; @@ -131,34 +136,40 @@ export function boundary(node, props, children) { const pending = props.pending; if (pending) { - render_snippet(() => { - pending(anchor); - }); + pending_effect = render_snippet(() => pending(anchor)); } } function unsuspend() { - if (!offscreen_effect) { + if (!offscreen_fragment) { return; } - if (boundary_effect) { - destroy_effect(boundary_effect); + if (pending_effect !== null) { + pause_effect(pending_effect); } - boundary_effect = offscreen_effect; - offscreen_effect = null; anchor.before(/** @type {DocumentFragment} */ (offscreen_fragment)); - resume_effect(boundary_effect); + offscreen_fragment = null; + + if (main_effect !== null) { + resume_effect(main_effect); + } } function reset() { - pause_effect(boundary_effect); + if (failed_effect !== null) { + pause_effect(failed_effect); + } - with_boundary(boundary, () => { + main_effect = with_boundary(boundary, () => { is_creating_fallback = false; - boundary_effect = branch(() => children(anchor)); - reset_is_throwing_error(); + + try { + return branch(() => children(anchor)); + } finally { + reset_is_throwing_error(); + } }); } @@ -192,9 +203,15 @@ export function boundary(node, props, children) { onerror?.(error, reset); - if (boundary_effect) { - destroy_effect(boundary_effect); - } else if (hydrating) { + if (main_effect) { + destroy_effect(main_effect); + } + + if (failed_effect) { + destroy_effect(failed_effect); + } + + if (hydrating) { set_hydrate_node(hydrate_open); next(); set_hydrate_node(remove_nodes()); @@ -202,7 +219,7 @@ export function boundary(node, props, children) { if (failed) { queue_boundary_micro_task(() => { - render_snippet(() => { + failed_effect = render_snippet(() => { failed( anchor, () => error, @@ -223,7 +240,7 @@ export function boundary(node, props, children) { const pending = props.pending; if (hydrating && pending) { - boundary_effect = branch(() => pending(anchor)); + pending_effect = branch(() => pending(anchor)); // ...now what? we need to start rendering `boundary_fn` offscreen, // and either insert the resulting fragment (if nothing suspends) @@ -235,13 +252,14 @@ export function boundary(node, props, children) { // the pending or main block was rendered for a given // boundary, and hydrate accordingly queueMicrotask(() => { - destroy_effect(boundary_effect); - with_boundary(boundary, () => { - boundary_effect = branch(() => children(anchor)); + destroy_effect(/** @type {Effect} */ (pending_effect)); + + main_effect = with_boundary(boundary, () => { + return branch(() => children(anchor)); }); }); } else { - boundary_effect = branch(() => children(anchor)); + main_effect = branch(() => children(anchor)); } reset_is_throwing_error(); From 2b0812817c7b0736beacdcc26efe28137e66f8c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Jan 2025 15:12:13 -0500 Subject: [PATCH 134/211] step one - template effects --- .../svelte/src/internal/client/constants.js | 1 + .../internal/client/dom/blocks/boundary.js | 78 ++++++++++++++----- .../src/internal/client/reactivity/sources.js | 21 ++++- .../svelte/src/internal/client/runtime.js | 25 ++++-- 4 files changed, 97 insertions(+), 28 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 5018887d7fd0..8b3f817e0d8b 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -25,6 +25,7 @@ export const EFFECT_HAS_DERIVED = 1 << 21; // Flags used for async export const IS_ASYNC = 1 << 22; export const REACTION_IS_UPDATING = 1 << 23; +export const BOUNDARY_SUSPENDED = 1 << 24; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 31936aa5de39..df9082ad0d41 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '../../constants.js'; +import { BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, @@ -16,7 +16,8 @@ import { set_active_effect, set_active_reaction, set_component_context, - reset_is_throwing_error + reset_is_throwing_error, + schedule_effect } from '../../runtime.js'; import { hydrate_next, @@ -117,18 +118,8 @@ export function boundary(node, props, children) { pause_effect( effect, () => { - var node = effect.nodes_start; - var end = effect.nodes_end; - offscreen_fragment = document.createDocumentFragment(); - - while (node !== null) { - /** @type {TemplateNode | null} */ - var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - - offscreen_fragment.append(node); - node = next; - } + move_effect(effect, offscreen_fragment); }, false ); @@ -146,7 +137,9 @@ export function boundary(node, props, children) { } if (pending_effect !== null) { - pause_effect(pending_effect); + pause_effect(pending_effect, () => { + pending_effect = null; + }); } anchor.before(/** @type {DocumentFragment} */ (offscreen_fragment)); @@ -159,7 +152,9 @@ export function boundary(node, props, children) { function reset() { if (failed_effect !== null) { - pause_effect(failed_effect); + pause_effect(failed_effect, () => { + failed_effect = null; + }); } main_effect = with_boundary(boundary, () => { @@ -176,16 +171,32 @@ export function boundary(node, props, children) { // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input) => { if (input === ASYNC_INCREMENT) { - if (async_count++ === 0) { - queue_boundary_micro_task(suspend); - } + async_count++; + + // TODO post-init, show the pending snippet after a timeout return; } if (input === ASYNC_DECREMENT) { if (--async_count === 0) { - queue_boundary_micro_task(unsuspend); + boundary.f ^= BOUNDARY_SUSPENDED; + + if (pending_effect) { + pause_effect(pending_effect, () => { + pending_effect = null; + }); + } + + if (offscreen_fragment) { + anchor.before(offscreen_fragment); + offscreen_fragment = null; + } + + if (main_effect !== null) { + // TODO do we also need to `resume_effect` here? + schedule_effect(main_effect); + } } return; @@ -260,6 +271,17 @@ export function boundary(node, props, children) { }); } else { main_effect = branch(() => children(anchor)); + + if (async_count > 0) { + if (pending) { + offscreen_fragment = document.createDocumentFragment(); + move_effect(main_effect, offscreen_fragment); + + pending_effect = branch(() => pending(anchor)); + } else { + // TODO trigger pending boundary on parent + } + } } reset_is_throwing_error(); @@ -270,6 +292,24 @@ export function boundary(node, props, children) { } } +/** + * + * @param {Effect} effect + * @param {DocumentFragment} fragment + */ +function move_effect(effect, fragment) { + var node = effect.nodes_start; + var end = effect.nodes_end; + + while (node !== null) { + /** @type {TemplateNode | null} */ + var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + fragment.append(node); + node = next; + } +} + export function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index c2448c9ee5fe..9b7047eaeb0f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -30,7 +30,10 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + IS_ASYNC, + BOUNDARY_EFFECT, + BOUNDARY_SUSPENDED } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -254,6 +257,22 @@ function mark_reactions(signal, status) { continue; } + // if we're about to trip an async derived, mark the boundary as + // suspended _before_ we actually process effects + if ((flags & IS_ASYNC) !== 0) { + let boundary = /** @type {Derived} */ (reaction).parent; + + while (boundary !== null && (boundary.f & BOUNDARY_EFFECT) === 0) { + boundary = boundary.parent; + } + + if (boundary === null) { + // TODO this is presumably an error — throw here? + } else { + boundary.f |= BOUNDARY_SUSPENDED; + } + } + set_signal_status(reaction, status); // If the signal a) was previously clean or b) is an unowned derived, then mark it diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a29802dbb9c1..e19567d73312 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -28,7 +28,8 @@ import { BOUNDARY_EFFECT, REACTION_IS_UPDATING, IS_ASYNC, - TEMPLATE_EFFECT + TEMPLATE_EFFECT, + BOUNDARY_SUSPENDED } from './constants.js'; import { flush_idle_tasks, @@ -843,15 +844,16 @@ function process_effects(effect, collected_effects) { ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0); if ((flags & RENDER_EFFECT) !== 0) { - if (is_branch) { - current_effect.f ^= CLEAN; + if ((flags & BOUNDARY_EFFECT) !== 0) { + suspended = (flags & BOUNDARY_SUSPENDED) !== 0; + } else if (is_branch) { + if (!suspended) { + current_effect.f ^= CLEAN; + } } else if (!skip_suspended) { try { if (check_dirtiness(current_effect)) { update_effect(current_effect); - if ((flags & IS_ASYNC) !== 0 && !suspended) { - suspended = true; - } } } catch (error) { handle_error(error, current_effect, null, current_effect.ctx); @@ -876,9 +878,16 @@ function process_effects(effect, collected_effects) { if (effect === parent) { break main_loop; } - if (suspended && (parent.f & BOUNDARY_EFFECT) !== 0 && is_pending_boundary(parent)) { - suspended = false; + + if ((parent.f & BOUNDARY_EFFECT) !== 0) { + let boundary = parent.parent; + while (boundary !== null && (boundary.f & BOUNDARY_EFFECT) === 0) { + boundary = boundary.parent; + } + + suspended = boundary === null ? false : (boundary.f & BOUNDARY_SUSPENDED) !== 0; } + var parent_sibling = parent.next; if (parent_sibling !== null) { current_effect = parent_sibling; From 41314a685a89911f771eb6c61727bfb7f3e5b7f2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Jan 2025 17:16:42 -0500 Subject: [PATCH 135/211] WIP --- .../internal/client/dom/blocks/boundary.js | 83 +++++++++---------- .../src/internal/client/dom/blocks/if.js | 75 ++++++++++++----- .../src/internal/client/reactivity/effects.js | 9 +- .../svelte/src/internal/client/runtime.js | 6 +- 4 files changed, 98 insertions(+), 75 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index df9082ad0d41..6820ac224d92 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -33,6 +33,7 @@ import * as e from '../../../shared/errors.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); +const ADD_CALLBACK = Symbol(); /** * @param {Effect} boundary @@ -88,6 +89,9 @@ export function boundary(node, props, children) { var hydrate_open = hydrate_node; var is_creating_fallback = false; + /** @type {Function[]} */ + var callbacks = []; + /** * @param {() => void} snippet_fn * @returns {Effect | null} @@ -108,48 +112,6 @@ export function boundary(node, props, children) { }); } - function suspend() { - if (offscreen_fragment || !main_effect) { - return; - } - - var effect = main_effect; - - pause_effect( - effect, - () => { - offscreen_fragment = document.createDocumentFragment(); - move_effect(effect, offscreen_fragment); - }, - false - ); - - const pending = props.pending; - - if (pending) { - pending_effect = render_snippet(() => pending(anchor)); - } - } - - function unsuspend() { - if (!offscreen_fragment) { - return; - } - - if (pending_effect !== null) { - pause_effect(pending_effect, () => { - pending_effect = null; - }); - } - - anchor.before(/** @type {DocumentFragment} */ (offscreen_fragment)); - offscreen_fragment = null; - - if (main_effect !== null) { - resume_effect(main_effect); - } - } - function reset() { if (failed_effect !== null) { pause_effect(failed_effect, () => { @@ -169,7 +131,7 @@ export function boundary(node, props, children) { } // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {unknown} */ input) => { + boundary.fn = (/** @type {unknown} */ input, /** @type {Function} */ payload) => { if (input === ASYNC_INCREMENT) { async_count++; @@ -182,6 +144,12 @@ export function boundary(node, props, children) { if (--async_count === 0) { boundary.f ^= BOUNDARY_SUSPENDED; + for (const callback of callbacks) { + callback(); + } + + callbacks.length = 0; + if (pending_effect) { pause_effect(pending_effect, () => { pending_effect = null; @@ -202,6 +170,11 @@ export function boundary(node, props, children) { return; } + if (input === ADD_CALLBACK) { + callbacks.push(payload); + return; + } + var error = input; var onerror = props.onerror; let failed = props.failed; @@ -377,3 +350,27 @@ function exit() { set_active_reaction(null); set_component_context(null); } + +/** + * @param {Effect | null} effect + */ +export function find_boundary(effect) { + while (effect !== null && (effect.f & BOUNDARY_EFFECT) === 0) { + effect = effect.parent; + } + + return effect; +} + +/** + * @param {Effect | null} boundary + * @param {Function} fn + */ +export function add_boundary_callback(boundary, fn) { + if (boundary === null) { + throw new Error('TODO'); + } + + // @ts-ignore + boundary.fn(ADD_CALLBACK, fn); +} diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 36790c05c135..86b504fb6117 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -10,6 +10,8 @@ import { } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; +import { active_effect, suspended } from '../../runtime.js'; +import { add_boundary_callback, find_boundary } from './boundary.js'; /** * @param {TemplateNode} node @@ -42,6 +44,46 @@ export function if_block(node, fn, elseif = false) { update_branch(flag, fn); }; + /** @type {DocumentFragment | null} */ + var offscreen_fragment = null; + + /** @type {Effect | null} */ + var pending_effect = null; + + var boundary = find_boundary(active_effect); + + function commit() { + if (offscreen_fragment !== null) { + anchor.before(offscreen_fragment); + offscreen_fragment = null; + } + + if (condition) { + consequent_effect = pending_effect; + } else { + alternate_effect = pending_effect; + } + + var current_effect = condition ? consequent_effect : alternate_effect; + var previous_effect = condition ? alternate_effect : consequent_effect; + + if (current_effect !== null) { + resume_effect(current_effect); + } + + if (previous_effect !== null) { + pause_effect(previous_effect, () => { + if (condition) { + alternate_effect = null; + } else { + consequent_effect = null; + } + }); + } + + pending_effect = null; + } + const update_branch = ( /** @type {boolean | null} */ new_condition, /** @type {null | ((anchor: Node) => void)} */ fn @@ -65,30 +107,19 @@ export function if_block(node, fn, elseif = false) { } } - if (condition) { - if (consequent_effect) { - resume_effect(consequent_effect); - } else if (fn) { - consequent_effect = branch(() => fn(anchor)); - } + var target = anchor; - if (alternate_effect) { - pause_effect(alternate_effect, () => { - alternate_effect = null; - }); - } - } else { - if (alternate_effect) { - resume_effect(alternate_effect); - } else if (fn) { - alternate_effect = branch(() => fn(anchor)); - } + if (suspended) { + offscreen_fragment = document.createDocumentFragment(); + offscreen_fragment.append((target = document.createComment(''))); + } - if (consequent_effect) { - pause_effect(consequent_effect, () => { - consequent_effect = null; - }); - } + pending_effect = fn && branch(() => fn(target)); + + if (suspended) { + add_boundary_callback(boundary, commit); + } else { + commit(); } if (mismatch) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 0f130e0b5118..29e2b74a1f01 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -567,20 +567,15 @@ export function unlink_effect(effect) { * A paused effect does not update, and the DOM subtree becomes inert. * @param {Effect} effect * @param {() => void} [callback] - * @param {boolean} [destroy] */ -export function pause_effect(effect, callback, destroy = true) { +export function pause_effect(effect, callback) { /** @type {TransitionManager[]} */ var transitions = []; pause_children(effect, transitions, true); run_out_transitions(transitions, () => { - if (destroy) { - destroy_effect(effect); - } else { - execute_effect_teardown(effect); - } + destroy_effect(effect); if (callback) callback(); }); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e19567d73312..bcc6f7a8a671 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,6 @@ import { DISCONNECTED, BOUNDARY_EFFECT, REACTION_IS_UPDATING, - IS_ASYNC, TEMPLATE_EFFECT, BOUNDARY_SUSPENDED } from './constants.js'; @@ -44,7 +43,6 @@ import { lifecycle_outside_component } from '../shared/errors.js'; import { FILENAME } from '../../constants.js'; import { legacy_mode_flag, tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; -import { is_pending_boundary } from './dom/blocks/boundary.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -89,6 +87,8 @@ export let active_reaction = null; export let untracking = false; +export let suspended = false; + /** @param {null | Reaction} reaction */ export function set_active_reaction(reaction) { active_reaction = reaction; @@ -826,7 +826,7 @@ export function schedule_effect(signal) { function process_effects(effect, collected_effects) { var current_effect = effect.first; var effects = []; - var suspended = false; + suspended = false; main_loop: while (current_effect !== null) { var flags = current_effect.f; From ce34c7618ca5f8592723e031422933506ce7bd6c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Jan 2025 17:32:50 -0500 Subject: [PATCH 136/211] update tests --- .../tests/runtime-runes/samples/async-attribute/_config.js | 2 +- .../runtime-runes/samples/async-derived-module/_config.js | 2 +- .../tests/runtime-runes/samples/async-derived/_config.js | 2 +- .../svelte/tests/runtime-runes/samples/async-each/_config.js | 2 +- .../tests/runtime-runes/samples/async-expression/_config.js | 2 +- .../tests/runtime-runes/samples/async-html-tag/_config.js | 2 +- .../svelte/tests/runtime-runes/samples/async-if/_config.js | 2 +- .../svelte/tests/runtime-runes/samples/async-key/_config.js | 4 ++-- .../svelte/tests/runtime-runes/samples/async-prop/_config.js | 2 +- .../tests/runtime-runes/samples/async-render-tag/_config.js | 2 +- .../runtime-runes/samples/async-svelte-element/_config.js | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js index 38bd6f723cc6..a39efc561d26 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -27,7 +27,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve('neat'); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js index b81f2a192a7f..4631243cb2fd 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -40,7 +40,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

84

'); d.resolve(43); await Promise.resolve(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index bb3f67f0f6f9..dbe76c573b7f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -38,7 +38,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

84

'); d.resolve(43); await Promise.resolve(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js index d38782fd232c..0fa27856067b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -27,7 +27,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

a

b

c

'); d.resolve(['d', 'e', 'f']); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js index 566bd2210b93..6cded1a1d1ba 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -26,7 +26,7 @@ export default test({ component.promise = (d = deferred()).promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve('wheee'); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js index 566bd2210b93..6cded1a1d1ba 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js @@ -26,7 +26,7 @@ export default test({ component.promise = (d = deferred()).promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve('wheee'); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js index 1ef71c2d5ef8..991cebad3e99 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -27,7 +27,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

yes

'); d.resolve(false); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js index 96e9fd31d4a2..293ac9357a2f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -29,7 +29,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve(1); await tick(); @@ -39,7 +39,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve(2); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js index d81b6c3b0709..570b22abd4c4 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -27,7 +27,7 @@ export default test({ d = deferred(); component.promise = d.promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve('hello again'); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js index 566bd2210b93..6cded1a1d1ba 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -26,7 +26,7 @@ export default test({ component.promise = (d = deferred()).promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve('wheee'); await tick(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js index 92946a539f39..ea3b91b2a40b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js @@ -26,7 +26,7 @@ export default test({ component.promise = (d = deferred()).promise; await tick(); - assert.htmlEqual(target.innerHTML, '

pending

'); + assert.htmlEqual(target.innerHTML, '

hello

'); d.resolve('h2'); await tick(); From ca11ebdde48f45b2f458ae037867937d704f90ca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Jan 2025 17:56:58 -0500 Subject: [PATCH 137/211] fix --- .../svelte/src/internal/client/dom/blocks/if.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 86b504fb6117..cec06ddf7498 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -58,10 +58,12 @@ export function if_block(node, fn, elseif = false) { offscreen_fragment = null; } - if (condition) { - consequent_effect = pending_effect; - } else { - alternate_effect = pending_effect; + if (pending_effect) { + if (condition) { + consequent_effect = pending_effect; + } else { + alternate_effect = pending_effect; + } } var current_effect = condition ? consequent_effect : alternate_effect; @@ -114,7 +116,9 @@ export function if_block(node, fn, elseif = false) { offscreen_fragment.append((target = document.createComment(''))); } - pending_effect = fn && branch(() => fn(target)); + if (condition ? !consequent_effect : !alternate_effect) { + pending_effect = fn && branch(() => fn(target)); + } if (suspended) { add_boundary_callback(boundary, commit); From 42a59e29668c94a71ecdead704b7a3a56f1f2347 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Jan 2025 18:06:10 -0500 Subject: [PATCH 138/211] fix --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 6820ac224d92..a98505b47a8f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -133,6 +133,7 @@ export function boundary(node, props, children) { // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input, /** @type {Function} */ payload) => { if (input === ASYNC_INCREMENT) { + boundary.f |= BOUNDARY_SUSPENDED; async_count++; // TODO post-init, show the pending snippet after a timeout @@ -246,6 +247,8 @@ export function boundary(node, props, children) { main_effect = branch(() => children(anchor)); if (async_count > 0) { + boundary.f |= BOUNDARY_SUSPENDED; + if (pending) { offscreen_fragment = document.createDocumentFragment(); move_effect(main_effect, offscreen_fragment); From f38bd5c0fa5b2ea47c005bd1901b5d12b15a25e4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 29 Jan 2025 09:05:43 -0500 Subject: [PATCH 139/211] key blocks --- .../src/internal/client/dom/blocks/key.js | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 4a8b7b94fcc8..78d6a93a645d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,9 +1,10 @@ /** @import { Effect, TemplateNode } from '#client' */ import { UNINITIALIZED } from '../../../../constants.js'; -import { block, branch, pause_effect } from '../../reactivity/effects.js'; +import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { not_equal, safe_not_equal } from '../../reactivity/equality.js'; -import { is_runes } from '../../runtime.js'; +import { active_effect, is_runes, suspended } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; +import { add_boundary_callback, find_boundary } from './boundary.js'; /** * @template V @@ -25,15 +26,48 @@ export function key_block(node, get_key, render_fn) { /** @type {Effect} */ var effect; + /** @type {Effect | null} */ + var pending_effect = null; + + /** @type {DocumentFragment | null} */ + var offscreen_fragment = null; + + var boundary = find_boundary(active_effect); + var changed = is_runes() ? not_equal : safe_not_equal; + function commit() { + if (effect) { + pause_effect(effect); + } + + if (offscreen_fragment !== null) { + anchor.before(offscreen_fragment); + offscreen_fragment = null; + } + + if (pending_effect !== null) { + effect = pending_effect; + pending_effect = null; + } + } + block(() => { if (changed(key, (key = get_key()))) { - if (effect) { - pause_effect(effect); + var target = anchor; + + if (suspended) { + offscreen_fragment = document.createDocumentFragment(); + offscreen_fragment.append((target = document.createComment(''))); } - effect = branch(() => render_fn(anchor)); + pending_effect = branch(() => render_fn(target)); + + if (suspended) { + add_boundary_callback(boundary, commit); + } else { + commit(); + } } }); From 2c557b6cd88605c0e4371baaec1bb109d8f592f7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 29 Jan 2025 09:38:59 -0500 Subject: [PATCH 140/211] html tags --- .../src/internal/client/dom/blocks/html.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 59738952efdc..50c94fd44add 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -9,6 +9,8 @@ import { hash, sanitize_location } from '../../../../utils.js'; import { DEV } from 'esm-env'; import { dev_current_component_function } from '../../context.js'; import { get_first_child, get_next_sibling } from '../operations.js'; +import { active_effect, suspended } from '../../runtime.js'; +import { add_boundary_callback, find_boundary } from './boundary.js'; /** * @param {Element} element @@ -47,14 +49,9 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning /** @type {Effect | undefined} */ var effect; - block(() => { - if (value === (value = get_value() ?? '')) { - if (hydrating) { - hydrate_next(); - } - return; - } + var boundary = find_boundary(active_effect); + function commit() { if (effect !== undefined) { destroy_effect(effect); effect = undefined; @@ -118,5 +115,18 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning anchor.before(node); } }); + } + + block(() => { + if (value === (value = get_value() ?? '')) { + if (hydrating) hydrate_next(); + return; + } + + if (suspended) { + add_boundary_callback(boundary, commit); + } else { + commit(); + } }); } From 6117037b649b708bf0855c20dbd39233f442989f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 30 Jan 2025 18:55:54 +0000 Subject: [PATCH 141/211] fix HMR bug --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index b601955c5262..bd8272762953 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -312,7 +312,7 @@ export function suspend() { return function unsuspend() { // @ts-ignore - boundary?.fn(ASYNC_DECREMENT); + boundary?.fn?.(ASYNC_DECREMENT); }; } From 5530ae5ea789f34f2c95780d3ae521336e7a7100 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 15:19:46 -0500 Subject: [PATCH 142/211] disable hmr for now --- playgrounds/sandbox/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playgrounds/sandbox/vite.config.js b/playgrounds/sandbox/vite.config.js index 41850fc30913..80a635a23960 100644 --- a/playgrounds/sandbox/vite.config.js +++ b/playgrounds/sandbox/vite.config.js @@ -11,7 +11,7 @@ export default defineConfig({ inspect(), svelte({ compilerOptions: { - hmr: true, + hmr: false, experimental: { async: true } From 5b0b9eb261945f55cab997998647722143d48f01 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 15:20:02 -0500 Subject: [PATCH 143/211] debugging utils --- .../svelte/src/internal/client/dev/debug.js | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 packages/svelte/src/internal/client/dev/debug.js diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js new file mode 100644 index 000000000000..fcf81578a7bb --- /dev/null +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -0,0 +1,92 @@ +/** @import { Derived, Effect, Value } from '#client' */ + +import { + BLOCK_EFFECT, + BOUNDARY_EFFECT, + BRANCH_EFFECT, + CLEAN, + DERIVED, + EFFECT, + MAYBE_DIRTY, + RENDER_EFFECT, + ROOT_EFFECT, + TEMPLATE_EFFECT +} from '../constants.js'; + +/** + * + * @param {Effect} effect + */ +export function root(effect) { + while (effect.parent !== null) { + effect = effect.parent; + } + + return effect; +} + +/** + * + * @param {Effect} effect + */ +export function log_effect_tree(effect) { + const flags = effect.f; + + let label = '(unknown)'; + + if ((flags & ROOT_EFFECT) !== 0) { + label = 'root'; + } else if ((flags & BOUNDARY_EFFECT) !== 0) { + label = 'boundary'; + } else if ((flags & TEMPLATE_EFFECT) !== 0) { + label = 'template'; + } else if ((flags & BLOCK_EFFECT) !== 0) { + label = 'block'; + } else if ((flags & BRANCH_EFFECT) !== 0) { + label = 'branch'; + } else if ((flags & RENDER_EFFECT) !== 0) { + label = 'render effect'; + } else if ((flags & EFFECT) !== 0) { + label = 'effect'; + } + + let status = + (flags & CLEAN) !== 0 ? 'clean' : (flags & MAYBE_DIRTY) !== 0 ? 'maybe dirty' : 'dirty'; + + console.group(`%c${label} (${status})`, `font-weight: ${status === 'clean' ? 'normal' : 'bold'}`); + + if (effect.deps !== null) { + console.groupCollapsed('%cdeps', 'font-weight: normal'); + for (const dep of effect.deps) { + log_dep(dep); + } + console.groupEnd(); + } + + let child = effect.first; + while (child !== null) { + log_effect_tree(child); + child = child.next; + } + + console.groupEnd(); +} + +/** + * + * @param {Value} dep + */ +function log_dep(dep) { + if ((dep.f & DERIVED) !== 0) { + const derived = /** @type {Derived} */ (dep); + console.groupCollapsed('%cderived', 'font-weight: normal', derived.v); + if (derived.deps) { + for (const d of derived.deps) { + log_dep(d); + } + } + console.groupEnd(); + } else { + console.log('state', dep.v); + } +} From 9d9198af9e1307c45faac12623815231481c4c5a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 15:21:18 -0500 Subject: [PATCH 144/211] tweak --- .../svelte/src/internal/client/dom/blocks/boundary.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 39670eb94dc6..135aa5e2bfc5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -23,6 +23,7 @@ import { import { get_next_sibling } from '../operations.js'; import { queue_boundary_micro_task } from '../task.js'; import * as e from '../../../shared/errors.js'; +import { run_all } from '../../../shared/utils.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -82,7 +83,7 @@ export function boundary(node, props, children) { var hydrate_open = hydrate_node; var is_creating_fallback = false; - /** @type {Function[]} */ + /** @type {Array<() => void>} */ var callbacks = []; /** @@ -124,7 +125,7 @@ export function boundary(node, props, children) { } // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {unknown} */ input, /** @type {Function} */ payload) => { + boundary.fn = (/** @type {unknown} */ input, /** @type {() => void} */ payload) => { if (input === ASYNC_INCREMENT) { boundary.f |= BOUNDARY_SUSPENDED; async_count++; @@ -138,10 +139,7 @@ export function boundary(node, props, children) { if (--async_count === 0) { boundary.f ^= BOUNDARY_SUSPENDED; - for (const callback of callbacks) { - callback(); - } - + run_all(callbacks); callbacks.length = 0; if (pending_effect) { From 877a417c176fff19dd5ec8c1afae15550be98bcd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 15:26:45 -0500 Subject: [PATCH 145/211] move code --- .../internal/client/dom/blocks/boundary.js | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 3d838e19bba7..fc4730953475 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -124,6 +124,29 @@ export function boundary(node, props, children) { }); } + function unsuspend() { + boundary.f ^= BOUNDARY_SUSPENDED; + + run_all(callbacks); + callbacks.length = 0; + + if (pending_effect) { + pause_effect(pending_effect, () => { + pending_effect = null; + }); + } + + if (offscreen_fragment) { + anchor.before(offscreen_fragment); + offscreen_fragment = null; + } + + if (main_effect !== null) { + // TODO do we also need to `resume_effect` here? + schedule_effect(main_effect); + } + } + // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input, /** @type {() => void} */ payload) => { if (input === ASYNC_INCREMENT) { @@ -137,26 +160,7 @@ export function boundary(node, props, children) { if (input === ASYNC_DECREMENT) { if (--async_count === 0) { - boundary.f ^= BOUNDARY_SUSPENDED; - - run_all(callbacks); - callbacks.length = 0; - - if (pending_effect) { - pause_effect(pending_effect, () => { - pending_effect = null; - }); - } - - if (offscreen_fragment) { - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - if (main_effect !== null) { - // TODO do we also need to `resume_effect` here? - schedule_effect(main_effect); - } + queue_boundary_micro_task(unsuspend); } return; From da5ff8809aaa383ab40a213ae48b673c70de9ae1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 15:30:59 -0500 Subject: [PATCH 146/211] cordon off hydration code --- .../src/internal/client/dom/blocks/each.js | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 3baa03a91753..8280addb32d1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -218,21 +218,25 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } - if (!hydrating) { + if (hydrating) { + if (length === 0 && fallback_fn) { + fallback = branch(() => fallback_fn(anchor)); + } + } else { reconcile(array, state, anchor, render_fn, flags, get_key, get_collection); - } - if (fallback_fn !== null) { - if (length === 0) { - if (fallback) { - resume_effect(fallback); - } else { - fallback = branch(() => fallback_fn(anchor)); + if (fallback_fn !== null) { + if (length === 0) { + if (fallback) { + resume_effect(fallback); + } else { + fallback = branch(() => fallback_fn(anchor)); + } + } else if (fallback !== null) { + pause_effect(fallback, () => { + fallback = null; + }); } - } else if (fallback !== null) { - pause_effect(fallback, () => { - fallback = null; - }); } } From 303d7383740162feb458243660302789645e07f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 15:42:08 -0500 Subject: [PATCH 147/211] add should_defer_append flag --- .../svelte/src/internal/client/dom/operations.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 627bf917eee1..e75b5ed86258 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -1,8 +1,10 @@ -/** @import { TemplateNode } from '#client' */ +/** @import { Effect, TemplateNode } from '#client' */ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; import { get_descriptor } from '../../shared/utils.js'; +import { active_effect } from '../runtime.js'; +import { EFFECT_RAN } from '../constants.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -195,3 +197,14 @@ export function sibling(node, count = 1, is_text = false) { export function clear_text_content(node) { node.textContent = ''; } + +/** + * Returns `true` if we're updating the current block, for example `condition` in + * an `{#if condition}` block just changed. In this case, the branch should be + * appended (or removed) at the same time as other updates within the + * current `` + */ +export function should_defer_append() { + var flags = /** @type {Effect} */ (active_effect).f; + return (flags & EFFECT_RAN) !== 0; +} From ffc4f1b03737f4d5ddf2acd0c731ff272cdea044 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 16:32:20 -0500 Subject: [PATCH 148/211] mostly working --- .../internal/client/dom/blocks/boundary.js | 88 +++++++++++++++++-- .../src/internal/client/dom/blocks/if.js | 6 +- .../src/internal/client/reactivity/sources.js | 16 ---- .../svelte/src/internal/client/runtime.js | 46 +++++----- 4 files changed, 106 insertions(+), 50 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index fc4730953475..c285f0fb77aa 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,11 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, EFFECT_TRANSPARENT } from '../../constants.js'; +import { + BOUNDARY_EFFECT, + BOUNDARY_SUSPENDED, + EFFECT_TRANSPARENT, + RENDER_EFFECT +} from '../../constants.js'; import { component_context, set_component_context } from '../../context.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { @@ -10,7 +15,9 @@ import { set_active_effect, set_active_reaction, reset_is_throwing_error, - schedule_effect + schedule_effect, + check_dirtiness, + update_effect } from '../../runtime.js'; import { hydrate_next, @@ -28,6 +35,9 @@ import { run_all } from '../../../shared/utils.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); const ADD_CALLBACK = Symbol(); +const ADD_RENDER_EFFECT = Symbol(); +const ADD_EFFECT = Symbol(); +const RELEASE = Symbol(); /** * @param {Effect} boundary @@ -86,6 +96,12 @@ export function boundary(node, props, children) { /** @type {Array<() => void>} */ var callbacks = []; + /** @type {Effect[]} */ + var render_effects = []; + + /** @type {Effect[]} */ + var effects = []; + /** * @param {() => void} snippet_fn * @returns {Effect | null} @@ -125,7 +141,19 @@ export function boundary(node, props, children) { } function unsuspend() { - boundary.f ^= BOUNDARY_SUSPENDED; + if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) { + boundary.f ^= BOUNDARY_SUSPENDED; + } + + for (const e of render_effects) { + try { + if (check_dirtiness(e)) { + update_effect(e); + } + } catch (error) { + handle_error(error, e, null, e.ctx); + } + } run_all(callbacks); callbacks.length = 0; @@ -141,14 +169,21 @@ export function boundary(node, props, children) { offscreen_fragment = null; } - if (main_effect !== null) { - // TODO do we also need to `resume_effect` here? - schedule_effect(main_effect); + // TODO this timing is wrong, effects need to ~somehow~ end up + // in the right place + for (const e of effects) { + try { + if (check_dirtiness(e)) { + update_effect(e); + } + } catch (error) { + handle_error(error, e, null, e.ctx); + } } } // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {unknown} */ input, /** @type {() => void} */ payload) => { + boundary.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { if (input === ASYNC_INCREMENT) { boundary.f |= BOUNDARY_SUSPENDED; async_count++; @@ -160,7 +195,12 @@ export function boundary(node, props, children) { if (input === ASYNC_DECREMENT) { if (--async_count === 0) { - queue_boundary_micro_task(unsuspend); + unsuspend(); + + if (main_effect !== null) { + // TODO do we also need to `resume_effect` here? + schedule_effect(main_effect); + } } return; @@ -171,6 +211,21 @@ export function boundary(node, props, children) { return; } + if (input === ADD_RENDER_EFFECT) { + render_effects.push(payload); + return; + } + + if (input === ADD_EFFECT) { + render_effects.push(payload); + return; + } + + if (input === RELEASE) { + unsuspend(); + return; + } + var error = input; var onerror = props.onerror; let failed = props.failed; @@ -372,3 +427,20 @@ export function add_boundary_callback(boundary, fn) { // @ts-ignore boundary.fn(ADD_CALLBACK, fn); } + +/** + * @param {Effect} boundary + * @param {Effect} effect + */ +export function add_boundary_effect(boundary, effect) { + // @ts-ignore + boundary.fn((effect.f & RENDER_EFFECT) !== 0 ? ADD_RENDER_EFFECT : ADD_EFFECT, effect); +} + +/** + * @param {Effect} boundary + */ +export function release_boundary(boundary) { + // @ts-ignore + boundary.fn?.(RELEASE); +} diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index cec06ddf7498..589a187aba4c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -12,6 +12,7 @@ import { block, branch, pause_effect, resume_effect } from '../../reactivity/eff import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { active_effect, suspended } from '../../runtime.js'; import { add_boundary_callback, find_boundary } from './boundary.js'; +import { should_defer_append } from '../operations.js'; /** * @param {TemplateNode} node @@ -109,9 +110,10 @@ export function if_block(node, fn, elseif = false) { } } + var defer = boundary !== null && should_defer_append(); var target = anchor; - if (suspended) { + if (defer) { offscreen_fragment = document.createDocumentFragment(); offscreen_fragment.append((target = document.createComment(''))); } @@ -120,7 +122,7 @@ export function if_block(node, fn, elseif = false) { pending_effect = fn && branch(() => fn(target)); } - if (suspended) { + if (defer) { add_boundary_callback(boundary, commit); } else { commit(); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index d1be99f69b82..2bc3a1618ccb 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -285,22 +285,6 @@ function mark_reactions(signal, status) { continue; } - // if we're about to trip an async derived, mark the boundary as - // suspended _before_ we actually process effects - if ((flags & IS_ASYNC) !== 0) { - let boundary = /** @type {Derived} */ (reaction).parent; - - while (boundary !== null && (boundary.f & BOUNDARY_EFFECT) === 0) { - boundary = boundary.parent; - } - - if (boundary === null) { - // TODO this is presumably an error — throw here? - } else { - boundary.f |= BOUNDARY_SUSPENDED; - } - } - set_signal_status(reaction, status); // If the signal a) was previously clean or b) is an unowned derived, then mark it diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2fdcc4f048d2..fd7e5d1b1562 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -31,7 +31,8 @@ import { import { flush_idle_tasks, flush_boundary_micro_tasks, - flush_post_micro_tasks + flush_post_micro_tasks, + queue_micro_task } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { @@ -51,6 +52,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; +import { add_boundary_effect, release_boundary } from './dom/blocks/boundary.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -808,12 +810,12 @@ export function schedule_effect(signal) { * * @param {Effect} effect * @param {Effect[]} collected_effects + * @param {Effect} [boundary] * @returns {void} */ -function process_effects(effect, collected_effects) { +function process_effects(effect, collected_effects, boundary) { var current_effect = effect.first; var effects = []; - suspended = false; main_loop: while (current_effect !== null) { var flags = current_effect.f; @@ -822,22 +824,27 @@ function process_effects(effect, collected_effects) { var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { - // We only want to skip suspended effects if they are not branches or block effects, - // with the exception of template effects, which are technically block effects but also - // have a special flag `TEMPLATE_EFFECT` that we can use to identify them - var skip_suspended = - suspended && + // Inside a boundary, defer everything except block/branch effects + var defer = + boundary !== undefined && (flags & BRANCH_EFFECT) === 0 && ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0); - if ((flags & RENDER_EFFECT) !== 0) { + if (defer) { + add_boundary_effect(/** @type {Effect} */ (boundary), current_effect); + } else if ((flags & BOUNDARY_EFFECT) !== 0) { + process_effects(current_effect, collected_effects, current_effect); + + if ((current_effect.f & BOUNDARY_SUSPENDED) === 0) { + // no more async work to happen + release_boundary(current_effect); + } + } else if ((flags & RENDER_EFFECT) !== 0) { if ((flags & BOUNDARY_EFFECT) !== 0) { - suspended = (flags & BOUNDARY_SUSPENDED) !== 0; + // TODO do we need to do anything here? } else if (is_branch) { - if (!suspended) { - current_effect.f ^= CLEAN; - } - } else if (!skip_suspended) { + current_effect.f ^= CLEAN; + } else { // Ensure we set the effect to be the active reaction // to ensure that unowned deriveds are correctly tracked // because we're flushing the current effect @@ -860,7 +867,7 @@ function process_effects(effect, collected_effects) { current_effect = child; continue; } - } else if ((flags & EFFECT) !== 0 && !skip_suspended) { + } else if ((flags & EFFECT) !== 0) { effects.push(current_effect); } } @@ -873,15 +880,6 @@ function process_effects(effect, collected_effects) { break main_loop; } - if ((parent.f & BOUNDARY_EFFECT) !== 0) { - let boundary = parent.parent; - while (boundary !== null && (boundary.f & BOUNDARY_EFFECT) === 0) { - boundary = boundary.parent; - } - - suspended = boundary === null ? false : (boundary.f & BOUNDARY_SUSPENDED) !== 0; - } - var parent_sibling = parent.next; if (parent_sibling !== null) { current_effect = parent_sibling; From 70fa1033de2e7c0cad28d4bbbaffa22dff5f251c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 16:46:11 -0500 Subject: [PATCH 149/211] simplify --- .../src/internal/client/dom/blocks/html.js | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 50c94fd44add..3ef9682c427d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode } from '#client' */ import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js'; -import { block, branch, destroy_effect } from '../../reactivity/effects.js'; +import { block, branch, destroy_effect, template_effect } from '../../reactivity/effects.js'; import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js'; import { create_fragment_from_html } from '../reconciler.js'; import { assign_nodes } from '../template.js'; @@ -49,9 +49,12 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning /** @type {Effect | undefined} */ var effect; - var boundary = find_boundary(active_effect); + template_effect(() => { + if (value === (value = get_value() ?? '')) { + if (hydrating) hydrate_next(); + return; + } - function commit() { if (effect !== undefined) { destroy_effect(effect); effect = undefined; @@ -115,18 +118,5 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning anchor.before(node); } }); - } - - block(() => { - if (value === (value = get_value() ?? '')) { - if (hydrating) hydrate_next(); - return; - } - - if (suspended) { - add_boundary_callback(boundary, commit); - } else { - commit(); - } }); } From 176ec0d67bca458fe11f90e31506b184ae129bc1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 16:51:37 -0500 Subject: [PATCH 150/211] fix --- packages/svelte/src/internal/client/dom/blocks/html.js | 4 +--- packages/svelte/src/internal/client/dom/blocks/if.js | 2 +- packages/svelte/src/internal/client/dom/blocks/key.js | 9 ++++++--- packages/svelte/src/internal/client/runtime.js | 2 -- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 3ef9682c427d..96f922f731fd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode } from '#client' */ import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js'; -import { block, branch, destroy_effect, template_effect } from '../../reactivity/effects.js'; +import { branch, destroy_effect, template_effect } from '../../reactivity/effects.js'; import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js'; import { create_fragment_from_html } from '../reconciler.js'; import { assign_nodes } from '../template.js'; @@ -9,8 +9,6 @@ import { hash, sanitize_location } from '../../../../utils.js'; import { DEV } from 'esm-env'; import { dev_current_component_function } from '../../context.js'; import { get_first_child, get_next_sibling } from '../operations.js'; -import { active_effect, suspended } from '../../runtime.js'; -import { add_boundary_callback, find_boundary } from './boundary.js'; /** * @param {Element} element diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 589a187aba4c..8aecfdb5088b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -10,7 +10,7 @@ import { } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; -import { active_effect, suspended } from '../../runtime.js'; +import { active_effect } from '../../runtime.js'; import { add_boundary_callback, find_boundary } from './boundary.js'; import { should_defer_append } from '../operations.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 7e75b72a0a47..21ad73215a11 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -2,10 +2,11 @@ import { UNINITIALIZED } from '../../../../constants.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { not_equal, safe_not_equal } from '../../reactivity/equality.js'; -import { active_effect, suspended } from '../../runtime.js'; +import { active_effect } from '../../runtime.js'; import { is_runes } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { add_boundary_callback, find_boundary } from './boundary.js'; +import { should_defer_append } from '../operations.js'; /** * @template V @@ -57,14 +58,16 @@ export function key_block(node, get_key, render_fn) { if (changed(key, (key = get_key()))) { var target = anchor; - if (suspended) { + var defer = boundary !== null && should_defer_append(); + + if (defer) { offscreen_fragment = document.createDocumentFragment(); offscreen_fragment.append((target = document.createComment(''))); } pending_effect = branch(() => render_fn(target)); - if (suspended) { + if (defer) { add_boundary_callback(boundary, commit); } else { commit(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fd7e5d1b1562..8bca75413ae6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -97,8 +97,6 @@ export let active_reaction = null; export let untracking = false; -export let suspended = false; - /** @param {null | Reaction} reaction */ export function set_active_reaction(reaction) { active_reaction = reaction; From 2e49f7ce1ec4755fc859495dc9aa1576530d8d6a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 16:52:35 -0500 Subject: [PATCH 151/211] tidy --- packages/svelte/src/internal/client/reactivity/sources.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2bc3a1618ccb..0dc55f97babc 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ +/** @import { Derived, Effect, Source, Value } from '#client' */ import { DEV } from 'esm-env'; import { active_reaction, @@ -28,10 +28,7 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT, - IS_ASYNC, - BOUNDARY_EFFECT, - BOUNDARY_SUSPENDED + ROOT_EFFECT } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; From af2224ebb35a156b24c2b989aa39cf4092a70593 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Jan 2025 16:57:27 -0500 Subject: [PATCH 152/211] tidy up --- packages/svelte/src/internal/client/constants.js | 5 ++--- packages/svelte/src/internal/client/reactivity/deriveds.js | 3 +-- packages/svelte/src/internal/client/reactivity/effects.js | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 8b3f817e0d8b..7883609ffed4 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -23,9 +23,8 @@ export const HEAD_EFFECT = 1 << 20; export const EFFECT_HAS_DERIVED = 1 << 21; // Flags used for async -export const IS_ASYNC = 1 << 22; -export const REACTION_IS_UPDATING = 1 << 23; -export const BOUNDARY_SUSPENDED = 1 << 24; +export const REACTION_IS_UPDATING = 1 << 22; +export const BOUNDARY_SUSPENDED = 1 << 23; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6a98a0d0c1bd..54915e438ec2 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -6,7 +6,6 @@ import { DESTROYED, DIRTY, EFFECT_HAS_DERIVED, - IS_ASYNC, MAYBE_DIRTY, UNOWNED } from '../constants.js'; @@ -114,7 +113,7 @@ export function async_derived(fn) { // TODO we should probably null out active effect here, // rather than inside `restore()` } - }, IS_ASYNC); + }); return Promise.resolve(promise).then(() => value); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 69193f4235ea..3ad13ee8b3df 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -36,7 +36,6 @@ import { MAYBE_DIRTY, EFFECT_HAS_DERIVED, BOUNDARY_EFFECT, - IS_ASYNC, TEMPLATE_EFFECT } from '../constants.js'; import { set } from './sources.js'; @@ -149,7 +148,7 @@ function create_effect(type, fn, sync, push = true) { effect.first === null && effect.nodes_start === null && effect.teardown === null && - (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT | IS_ASYNC)) === 0; + (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0; if (!inert && !is_root && push) { if (parent_effect !== null) { From c270c767791625d78751733293251c0da4236090 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 06:02:10 -0500 Subject: [PATCH 153/211] fix timing --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c285f0fb77aa..329fe8c15e6f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -169,8 +169,6 @@ export function boundary(node, props, children) { offscreen_fragment = null; } - // TODO this timing is wrong, effects need to ~somehow~ end up - // in the right place for (const e of effects) { try { if (check_dirtiness(e)) { @@ -217,7 +215,7 @@ export function boundary(node, props, children) { } if (input === ADD_EFFECT) { - render_effects.push(payload); + effects.push(payload); return; } From f2002ce682f1ca2a19509abc454c44b0f7e1ad66 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 06:45:11 -0500 Subject: [PATCH 154/211] fix --- .../client/dom/blocks/svelte-component.js | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 72157eaa40db..bad3c726b9d4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,7 +1,10 @@ /** @import { TemplateNode, Dom, Effect } from '#client' */ import { EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; +import { active_effect } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; +import { should_defer_append } from '../operations.js'; +import { add_boundary_callback, find_boundary } from './boundary.js'; /** * @template P @@ -24,16 +27,47 @@ export function component(node, get_component, render_fn) { /** @type {Effect | null} */ var effect; - block(() => { - if (component === (component = get_component())) return; + /** @type {DocumentFragment | null} */ + var offscreen_fragment = null; + + /** @type {Effect | null} */ + var pending_effect = null; + var boundary = find_boundary(active_effect); + + function commit() { if (effect) { pause_effect(effect); effect = null; } + if (offscreen_fragment) { + anchor.before(offscreen_fragment); + offscreen_fragment = null; + } + + effect = pending_effect; + } + + block(() => { + if (component === (component = get_component())) return; + if (component) { - effect = branch(() => render_fn(anchor, component)); + var defer = boundary !== null && should_defer_append(); + var target = anchor; + + if (defer) { + offscreen_fragment = document.createDocumentFragment(); + offscreen_fragment.append((target = document.createComment(''))); + } + + pending_effect = branch(() => render_fn(anchor, component)); + + if (defer) { + add_boundary_callback(boundary, commit); + } else { + commit(); + } } }, EFFECT_TRANSPARENT); From b5df097f7bb6b59ac6207543512ae2fd625a3670 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 06:56:23 -0500 Subject: [PATCH 155/211] fixes --- .../src/internal/client/dom/blocks/boundary.js | 7 +++++++ .../client/dom/blocks/svelte-component.js | 15 ++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 329fe8c15e6f..4e125779e8f5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -238,10 +238,17 @@ export function boundary(node, props, children) { if (main_effect) { destroy_effect(main_effect); + main_effect = null; + } + + if (pending_effect) { + destroy_effect(pending_effect); + pending_effect = null; } if (failed_effect) { destroy_effect(failed_effect); + failed_effect = null; } if (hydrating) { diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index bad3c726b9d4..56f57400ab4c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -52,8 +52,9 @@ export function component(node, get_component, render_fn) { block(() => { if (component === (component = get_component())) return; + var defer = boundary !== null && should_defer_append(); + if (component) { - var defer = boundary !== null && should_defer_append(); var target = anchor; if (defer) { @@ -61,13 +62,13 @@ export function component(node, get_component, render_fn) { offscreen_fragment.append((target = document.createComment(''))); } - pending_effect = branch(() => render_fn(anchor, component)); + pending_effect = branch(() => render_fn(target, component)); + } - if (defer) { - add_boundary_callback(boundary, commit); - } else { - commit(); - } + if (defer) { + add_boundary_callback(boundary, commit); + } else { + commit(); } }, EFFECT_TRANSPARENT); From 952ea25ed126dc4210e2eb5693231bdc06a44ea8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 08:07:33 -0500 Subject: [PATCH 156/211] failing test --- .../samples/async-each-await-item/_config.js | 41 +++++++++++++++++++ .../samples/async-each-await-item/main.svelte | 13 ++++++ .../samples/async-each/_config.js | 4 +- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js new file mode 100644 index 000000000000..bba0c773860e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js @@ -0,0 +1,41 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {Array>} */ +let items = []; + +export default test({ + html: `

pending

`, + + get props() { + items = [deferred(), deferred(), deferred()]; + + return { + items + }; + }, + + async test({ assert, target, component }) { + items[0].resolve('a'); + items[1].resolve('b'); + items[2].resolve('c'); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

a

b

c

'); + + items = [deferred(), deferred(), deferred(), deferred()]; + component.items = items; + await tick(); + assert.htmlEqual(target.innerHTML, '

a

b

c

'); + + items[0].resolve('b'); + items[1].resolve('c'); + items[2].resolve('d'); + items[3].resolve('e'); + await tick(); + assert.htmlEqual(target.innerHTML, '

b

c

d

e

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte new file mode 100644 index 000000000000..204eb0d0c35a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte @@ -0,0 +1,13 @@ + + + + {#each items as deferred} +

{await deferred.promise}

+ {/each} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js index 0fa27856067b..b28d310565f3 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -29,8 +29,8 @@ export default test({ await tick(); assert.htmlEqual(target.innerHTML, '

a

b

c

'); - d.resolve(['d', 'e', 'f']); + d.resolve(['d', 'e', 'f', 'g']); await tick(); - assert.htmlEqual(target.innerHTML, '

d

e

f

'); + assert.htmlEqual(target.innerHTML, '

d

e

f

g

'); } }); From 010108a38c2330c2eb8903a76341d9e8732b72c7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 08:07:42 -0500 Subject: [PATCH 157/211] hoist commit logic --- .../src/internal/client/dom/blocks/each.js | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 8280addb32d1..3c600a06f84c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -38,6 +38,7 @@ import { queue_micro_task } from '../task.js'; import { active_effect, active_reaction, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; +import { find_boundary } from './boundary.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -136,6 +137,8 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var was_empty = false; + var boundary = find_boundary(active_effect); + // TODO: ideally we could use derived for runes mode but because of the ability // to use a store which can be mutated, we can't do that here as mutating a store // will still result in the collection array being the same from the store @@ -145,8 +148,29 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f return is_array(collection) ? collection : collection == null ? [] : array_from(collection); }); + /** @type {V[]} */ + var array; + + function commit() { + reconcile(array, state, anchor, render_fn, flags, get_key, get_collection); + + if (fallback_fn !== null) { + if (array.length === 0) { + if (fallback) { + resume_effect(fallback); + } else { + fallback = branch(() => fallback_fn(anchor)); + } + } else if (fallback !== null) { + pause_effect(fallback, () => { + fallback = null; + }); + } + } + } + block(() => { - var array = get(each_array); + array = get(each_array); var length = array.length; if (was_empty && length === 0) { @@ -223,21 +247,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f fallback = branch(() => fallback_fn(anchor)); } } else { - reconcile(array, state, anchor, render_fn, flags, get_key, get_collection); - - if (fallback_fn !== null) { - if (length === 0) { - if (fallback) { - resume_effect(fallback); - } else { - fallback = branch(() => fallback_fn(anchor)); - } - } else if (fallback !== null) { - pause_effect(fallback, () => { - fallback = null; - }); - } - } + commit(); } if (mismatch) { From 028dba829fabda81e841d884b7b31f4353a70c90 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 08:55:59 -0500 Subject: [PATCH 158/211] each blocks work! --- .../src/internal/client/dom/blocks/each.js | 118 +++++++++++++++--- .../samples/async-each-await-item/_config.js | 1 + 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 3c600a06f84c..4414948df52e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -20,7 +20,8 @@ import { clear_text_content, create_text, get_first_child, - get_next_sibling + get_next_sibling, + should_defer_append } from '../operations.js'; import { block, @@ -35,10 +36,10 @@ import { source, mutable_source, internal_set } from '../../reactivity/sources.j import { array_from, is_array } from '../../../shared/utils.js'; import { INERT } from '../../constants.js'; import { queue_micro_task } from '../task.js'; -import { active_effect, active_reaction, get } from '../../runtime.js'; +import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; -import { find_boundary } from './boundary.js'; +import { add_boundary_callback, find_boundary } from './boundary.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -64,17 +65,18 @@ export function index(_, i) { * Pause multiple effects simultaneously, and coordinate their * subsequent destruction. Used in each blocks * @param {EachState} state - * @param {EachItem[]} items + * @param {EachItem[]} to_destroy * @param {null | Node} controlled_anchor - * @param {Map} items_map */ -function pause_effects(state, items, controlled_anchor, items_map) { +function pause_effects(state, to_destroy, controlled_anchor) { + var items_map = state.items; + /** @type {TransitionManager[]} */ var transitions = []; - var length = items.length; + var length = to_destroy.length; for (var i = 0; i < length; i++) { - pause_children(items[i].e, transitions, true); + pause_children(to_destroy[i].e, transitions, true); } var is_controlled = length > 0 && transitions.length === 0 && controlled_anchor !== null; @@ -87,12 +89,12 @@ function pause_effects(state, items, controlled_anchor, items_map) { clear_text_content(parent_node); parent_node.append(/** @type {Element} */ (controlled_anchor)); items_map.clear(); - link(state, items[0].prev, items[length - 1].next); + link(state, to_destroy[0].prev, to_destroy[length - 1].next); } run_out_transitions(transitions, () => { for (var i = 0; i < length; i++) { - var item = items[i]; + var item = to_destroy[i]; if (!is_controlled) { items_map.delete(item.k); link(state, item.prev, item.next); @@ -139,6 +141,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var boundary = find_boundary(active_effect); + /** @type {Map} */ + var pending_items = new Map(); + // TODO: ideally we could use derived for runes mode but because of the ability // to use a store which can be mutated, we can't do that here as mutating a store // will still result in the collection array being the same from the store @@ -151,8 +156,21 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** @type {V[]} */ var array; + /** @type {Effect} */ + var each_effect; + function commit() { - reconcile(array, state, anchor, render_fn, flags, get_key, get_collection); + reconcile( + each_effect, + array, + state, + pending_items, + anchor, + render_fn, + flags, + get_key, + get_collection + ); if (fallback_fn !== null) { if (array.length === 0) { @@ -170,6 +188,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } block(() => { + // store a reference to the effect so that we can update the start/end nodes in reconciliation + each_effect ??= /** @type {Effect} */ (active_effect); + array = get(each_array); var length = array.length; @@ -247,7 +268,42 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f fallback = branch(() => fallback_fn(anchor)); } } else { - commit(); + var defer = boundary !== null && should_defer_append(); + + if (defer) { + for (i = 0; i < length; i += 1) { + value = array[i]; + key = get_key(value, i); + + var existing = state.items.get(key) ?? pending_items.get(key); + + if (existing) { + // update before reconciliation, to trigger any async updates + if ((flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0) { + update_item(existing, value, i, flags); + } + } else { + var item = create_item( + null, + state, + null, + null, + value, + key, + i, + render_fn, + flags, + get_collection + ); + + pending_items.set(key, item); + } + } + + add_boundary_callback(boundary, commit); + } else { + commit(); + } } if (mismatch) { @@ -272,8 +328,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f /** * Add, remove, or reorder items output by an each block as its input changes * @template V + * @param {Effect} each_effect * @param {Array} array * @param {EachState} state + * @param {Map} pending_items * @param {Element | Comment | Text} anchor * @param {(anchor: Node, item: MaybeSource, index: number | Source, collection: () => V[]) => void} render_fn * @param {number} flags @@ -281,7 +339,17 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f * @param {() => V[]} get_collection * @returns {void} */ -function reconcile(array, state, anchor, render_fn, flags, get_key, get_collection) { +function reconcile( + each_effect, + array, + state, + pending_items, + anchor, + render_fn, + flags, + get_key, + get_collection +) { var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; @@ -333,7 +401,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); - item = items.get(key); + item = items.get(key) ?? pending_items.get(key); if (item === undefined) { var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor; @@ -468,7 +536,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti } } - pause_effects(state, to_destroy, controlled_anchor, items); + pause_effects(state, to_destroy, controlled_anchor); } } @@ -481,8 +549,13 @@ function reconcile(array, state, anchor, render_fn, flags, get_key, get_collecti }); } - /** @type {Effect} */ (active_effect).first = state.first && state.first.e; - /** @type {Effect} */ (active_effect).last = prev && prev.e; + // TODO this seems super weird... should be `each_effect`, but that doesn't seem to work? + if (active_effect !== null) { + active_effect.first = state.first && state.first.e; + active_effect.last = prev && prev.e; + } + + pending_items.clear(); } /** @@ -506,7 +579,7 @@ function update_item(item, value, index, type) { /** * @template V - * @param {Node} anchor + * @param {Node | null} anchor * @param {EachState} state * @param {EachItem | null} prev * @param {EachItem | null} next @@ -562,7 +635,12 @@ function create_item( current_each_item = item; try { - item.e = branch(() => render_fn(anchor, v, i, get_collection), hydrating); + if (anchor === null) { + var fragment = document.createDocumentFragment(); + fragment.append((anchor = document.createComment(''))); + } + + item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection), hydrating); item.e.prev = prev && prev.e; item.e.next = next && next.e; @@ -596,7 +674,7 @@ function move(item, next, anchor) { var dest = next ? /** @type {TemplateNode} */ (next.e.nodes_start) : anchor; var node = /** @type {TemplateNode} */ (item.e.nodes_start); - while (node !== end) { + while (node !== null && node !== end) { var next_node = /** @type {TemplateNode} */ (get_next_sibling(node)); dest.before(node); node = next_node; diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js index bba0c773860e..dd6f228deb4e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js @@ -35,6 +35,7 @@ export default test({ items[1].resolve('c'); items[2].resolve('d'); items[3].resolve('e'); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

b

c

d

e

'); } From 012cdebed6361410e9d999fc24db6622f5025c39 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 09:06:29 -0500 Subject: [PATCH 159/211] fix --- packages/svelte/src/internal/client/dom/blocks/each.js | 2 +- packages/svelte/src/internal/client/dom/blocks/if.js | 5 +++-- packages/svelte/src/internal/client/dom/blocks/key.js | 5 +++-- .../src/internal/client/dom/blocks/svelte-component.js | 8 ++++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 4414948df52e..a81f115f7c74 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -637,7 +637,7 @@ function create_item( try { if (anchor === null) { var fragment = document.createDocumentFragment(); - fragment.append((anchor = document.createComment(''))); + fragment.append((anchor = create_text())); } item.e = branch(() => render_fn(/** @type {Node} */ (anchor), v, i, get_collection), hydrating); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 8aecfdb5088b..d8dcfcbd580b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -12,7 +12,7 @@ import { block, branch, pause_effect, resume_effect } from '../../reactivity/eff import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { active_effect } from '../../runtime.js'; import { add_boundary_callback, find_boundary } from './boundary.js'; -import { should_defer_append } from '../operations.js'; +import { create_text, should_defer_append } from '../operations.js'; /** * @param {TemplateNode} node @@ -115,7 +115,7 @@ export function if_block(node, fn, elseif = false) { if (defer) { offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = document.createComment(''))); + offscreen_fragment.append((target = create_text())); } if (condition ? !consequent_effect : !alternate_effect) { @@ -124,6 +124,7 @@ export function if_block(node, fn, elseif = false) { if (defer) { add_boundary_callback(boundary, commit); + target.remove(); } else { commit(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 21ad73215a11..8e9c4bce43b0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -6,7 +6,7 @@ import { active_effect } from '../../runtime.js'; import { is_runes } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { add_boundary_callback, find_boundary } from './boundary.js'; -import { should_defer_append } from '../operations.js'; +import { create_text, should_defer_append } from '../operations.js'; /** * @template V @@ -62,13 +62,14 @@ export function key_block(node, get_key, render_fn) { if (defer) { offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = document.createComment(''))); + offscreen_fragment.append((target = create_text())); } pending_effect = branch(() => render_fn(target)); if (defer) { add_boundary_callback(boundary, commit); + target.remove(); } else { commit(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 56f57400ab4c..b59c24b0295f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -3,7 +3,7 @@ import { EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { active_effect } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { should_defer_append } from '../operations.js'; +import { create_text, should_defer_append } from '../operations.js'; import { add_boundary_callback, find_boundary } from './boundary.js'; /** @@ -59,10 +59,14 @@ export function component(node, get_component, render_fn) { if (defer) { offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = document.createComment(''))); + offscreen_fragment.append((target = create_text())); } pending_effect = branch(() => render_fn(target, component)); + + if (defer) { + target.remove(); + } } if (defer) { From 6025193b98e0ce95f4eca5d16f39036db223c687 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 09:52:28 -0500 Subject: [PATCH 160/211] partial fix --- .../internal/client/dom/blocks/boundary.js | 10 +++---- .../src/internal/client/dom/blocks/each.js | 27 ++++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 4e125779e8f5..d0222f5c6bd0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -93,8 +93,8 @@ export function boundary(node, props, children) { var hydrate_open = hydrate_node; var is_creating_fallback = false; - /** @type {Array<() => void>} */ - var callbacks = []; + /** @type {Set<() => void>} */ + var callbacks = new Set(); /** @type {Effect[]} */ var render_effects = []; @@ -155,8 +155,8 @@ export function boundary(node, props, children) { } } - run_all(callbacks); - callbacks.length = 0; + for (const fn of callbacks) fn(); + callbacks.clear(); if (pending_effect) { pause_effect(pending_effect, () => { @@ -205,7 +205,7 @@ export function boundary(node, props, children) { } if (input === ADD_CALLBACK) { - callbacks.push(payload); + callbacks.add(payload); return; } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index a81f115f7c74..7493ecd65688 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -401,7 +401,17 @@ function reconcile( for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); - item = items.get(key) ?? pending_items.get(key); + + item = items.get(key); + + if (item === undefined) { + var pending = pending_items.get(key); + if (pending !== undefined) { + pending_items.delete(key); + items.set(key, pending); + item = pending; + } + } if (item === undefined) { var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor; @@ -550,12 +560,17 @@ function reconcile( } // TODO this seems super weird... should be `each_effect`, but that doesn't seem to work? - if (active_effect !== null) { - active_effect.first = state.first && state.first.e; - active_effect.last = prev && prev.e; - } + // if (active_effect !== null) { + // active_effect.first = state.first && state.first.e; + // active_effect.last = prev && prev.e; + // } - pending_items.clear(); + each_effect.first = state.first && state.first.e; + each_effect.last = prev && prev.e; + + for (var unused of pending_items.values()) { + destroy_effect(unused.e); + } } /** From 0ace243a5f69c1065317cc7cb6eb48aff486e9d1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 14:47:57 -0500 Subject: [PATCH 161/211] fix --- .../src/internal/client/dom/blocks/each.js | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 7493ecd65688..0df4e4b0d49d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -293,7 +293,8 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f i, render_fn, flags, - get_collection + get_collection, + true ); pending_items.set(key, item); @@ -406,28 +407,34 @@ function reconcile( if (item === undefined) { var pending = pending_items.get(key); + if (pending !== undefined) { pending_items.delete(key); items.set(key, pending); - item = pending; - } - } - if (item === undefined) { - var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor; - - prev = create_item( - child_anchor, - state, - prev, - prev === null ? state.first : prev.next, - value, - key, - i, - render_fn, - flags, - get_collection - ); + var next = prev && prev.next; + + link(state, prev, pending); + link(state, pending, next); + + move(pending, next, anchor); + prev = pending; + } else { + var child_anchor = current ? /** @type {TemplateNode} */ (current.e.nodes_start) : anchor; + + prev = create_item( + child_anchor, + state, + prev, + prev === null ? state.first : prev.next, + value, + key, + i, + render_fn, + flags, + get_collection + ); + } items.set(key, prev); @@ -604,6 +611,7 @@ function update_item(item, value, index, type) { * @param {(anchor: Node, item: V | Source, index: number | Value, collection: () => V[]) => void} render_fn * @param {number} flags * @param {() => V[]} get_collection + * @param {boolean} [deferred] * @returns {EachItem} */ function create_item( @@ -616,7 +624,8 @@ function create_item( index, render_fn, flags, - get_collection + get_collection, + deferred ) { var previous_each_item = current_each_item; var reactive = (flags & EACH_ITEM_REACTIVE) !== 0; @@ -661,7 +670,9 @@ function create_item( item.e.next = next && next.e; if (prev === null) { - state.first = item; + if (!deferred) { + state.first = item; + } } else { prev.next = item; prev.e.next = item.e; From 6e1a33162c298ed1635cf3a23f9444254486500e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 14:56:51 -0500 Subject: [PATCH 162/211] tidy up --- .../svelte/src/internal/client/dom/blocks/boundary.js | 9 ++++----- packages/svelte/src/internal/client/runtime.js | 10 ++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index d0222f5c6bd0..97389f9624d8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -30,14 +30,13 @@ import { import { get_next_sibling } from '../operations.js'; import { queue_boundary_micro_task } from '../task.js'; import * as e from '../../../shared/errors.js'; -import { run_all } from '../../../shared/utils.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); const ADD_CALLBACK = Symbol(); const ADD_RENDER_EFFECT = Symbol(); const ADD_EFFECT = Symbol(); -const RELEASE = Symbol(); +const COMMIT = Symbol(); /** * @param {Effect} boundary @@ -219,7 +218,7 @@ export function boundary(node, props, children) { return; } - if (input === RELEASE) { + if (input === COMMIT) { unsuspend(); return; } @@ -445,7 +444,7 @@ export function add_boundary_effect(boundary, effect) { /** * @param {Effect} boundary */ -export function release_boundary(boundary) { +export function commit_boundary(boundary) { // @ts-ignore - boundary.fn?.(RELEASE); + boundary.fn?.(COMMIT); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8bca75413ae6..da7c267b4530 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -52,7 +52,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; -import { add_boundary_effect, release_boundary } from './dom/blocks/boundary.js'; +import { add_boundary_effect, commit_boundary } from './dom/blocks/boundary.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -825,7 +825,7 @@ function process_effects(effect, collected_effects, boundary) { // Inside a boundary, defer everything except block/branch effects var defer = boundary !== undefined && - (flags & BRANCH_EFFECT) === 0 && + !is_branch && ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0); if (defer) { @@ -835,12 +835,10 @@ function process_effects(effect, collected_effects, boundary) { if ((current_effect.f & BOUNDARY_SUSPENDED) === 0) { // no more async work to happen - release_boundary(current_effect); + commit_boundary(current_effect); } } else if ((flags & RENDER_EFFECT) !== 0) { - if ((flags & BOUNDARY_EFFECT) !== 0) { - // TODO do we need to do anything here? - } else if (is_branch) { + if (is_branch) { current_effect.f ^= CLEAN; } else { // Ensure we set the effect to be the active reaction From 5f61b08849412324385756829f4f57bc56dfb02a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 15:31:15 -0500 Subject: [PATCH 163/211] simplify --- .../src/internal/client/dom/blocks/html.js | 109 +++++++++--------- .../src/internal/client/reactivity/effects.js | 30 ++--- 2 files changed, 69 insertions(+), 70 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 96f922f731fd..a39c4f537ddb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode } from '#client' */ import { FILENAME, HYDRATION_ERROR } from '../../../../constants.js'; -import { branch, destroy_effect, template_effect } from '../../reactivity/effects.js'; +import { remove_effect_dom, template_effect } from '../../reactivity/effects.js'; import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js'; import { create_fragment_from_html } from '../reconciler.js'; import { assign_nodes } from '../template.js'; @@ -9,6 +9,7 @@ import { hash, sanitize_location } from '../../../../utils.js'; import { DEV } from 'esm-env'; import { dev_current_component_function } from '../../context.js'; import { get_first_child, get_next_sibling } from '../operations.js'; +import { active_effect } from '../../runtime.js'; /** * @param {Element} element @@ -44,77 +45,71 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning var value = ''; - /** @type {Effect | undefined} */ - var effect; - template_effect(() => { + var effect = /** @type {Effect} */ (active_effect); + if (value === (value = get_value() ?? '')) { if (hydrating) hydrate_next(); return; } - if (effect !== undefined) { - destroy_effect(effect); - effect = undefined; + if (effect.nodes_start !== null) { + remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end)); + effect.nodes_start = effect.nodes_end = null; } if (value === '') return; - effect = branch(() => { - if (hydrating) { - // We're deliberately not trying to repair mismatches between server and client, - // as it's costly and error-prone (and it's an edge case to have a mismatch anyway) - var hash = /** @type {Comment} */ (hydrate_node).data; - var next = hydrate_next(); - var last = next; - - while ( - next !== null && - (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '') - ) { - last = next; - next = /** @type {TemplateNode} */ (get_next_sibling(next)); - } - - if (next === null) { - w.hydration_mismatch(); - throw HYDRATION_ERROR; - } - - if (DEV && !skip_warning) { - check_hash(/** @type {Element} */ (next.parentNode), hash, value); - } - - assign_nodes(hydrate_node, last); - anchor = set_hydrate_node(next); - return; - } + if (hydrating) { + // We're deliberately not trying to repair mismatches between server and client, + // as it's costly and error-prone (and it's an edge case to have a mismatch anyway) + var hash = /** @type {Comment} */ (hydrate_node).data; + var next = hydrate_next(); + var last = next; - var html = value + ''; - if (svg) html = `${html}`; - else if (mathml) html = `${html}`; + while (next !== null && (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')) { + last = next; + next = /** @type {TemplateNode} */ (get_next_sibling(next)); + } - // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed. - // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons. - /** @type {DocumentFragment | Element} */ - var node = create_fragment_from_html(html); + if (next === null) { + w.hydration_mismatch(); + throw HYDRATION_ERROR; + } - if (svg || mathml) { - node = /** @type {Element} */ (get_first_child(node)); + if (DEV && !skip_warning) { + check_hash(/** @type {Element} */ (next.parentNode), hash, value); } - assign_nodes( - /** @type {TemplateNode} */ (get_first_child(node)), - /** @type {TemplateNode} */ (node.lastChild) - ); - - if (svg || mathml) { - while (get_first_child(node)) { - anchor.before(/** @type {Node} */ (get_first_child(node))); - } - } else { - anchor.before(node); + assign_nodes(hydrate_node, last); + anchor = set_hydrate_node(next); + return; + } + + var html = value + ''; + if (svg) html = `${html}`; + else if (mathml) html = `${html}`; + + // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed. + // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons. + /** @type {DocumentFragment | Element} */ + var node = create_fragment_from_html(html); + + if (svg || mathml) { + node = /** @type {Element} */ (get_first_child(node)); + } + + assign_nodes( + /** @type {TemplateNode} */ (get_first_child(node)), + /** @type {TemplateNode} */ (node.lastChild) + ); + + if (svg || mathml) { + while (get_first_child(node)) { + anchor.before(/** @type {Node} */ (get_first_child(node))); } - }); + } else { + anchor.before(node); + } }); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 3ad13ee8b3df..8cd5766cd067 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -388,7 +388,7 @@ function create_template_effect(fn, deriveds) { }); } - block(effect, TEMPLATE_EFFECT); + create_effect(RENDER_EFFECT | TEMPLATE_EFFECT, effect, true); } /** @@ -467,18 +467,7 @@ export function destroy_effect(effect, remove_dom = true) { var removed = false; if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) { - /** @type {TemplateNode | null} */ - var node = effect.nodes_start; - var end = effect.nodes_end; - - while (node !== null) { - /** @type {TemplateNode | null} */ - var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - - node.remove(); - node = next; - } - + remove_effect_dom(effect.nodes_start, /** @type {TemplateNode} */ (effect.nodes_end)); removed = true; } @@ -520,6 +509,21 @@ export function destroy_effect(effect, remove_dom = true) { null; } +/** + * + * @param {TemplateNode | null} node + * @param {TemplateNode} end + */ +export function remove_effect_dom(node, end) { + while (node !== null) { + /** @type {TemplateNode | null} */ + var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + node.remove(); + node = next; + } +} + /** * Detach an effect from the effect tree, freeing up memory and * reducing the amount of work that happens on subsequent traversals From a405d477f7fdd7665e8d13cccd75e52d1ac20c7e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 15:34:37 -0500 Subject: [PATCH 164/211] remove unnecessary TEMPLATE_EFFECT distinction --- packages/svelte/src/internal/client/constants.js | 1 - packages/svelte/src/internal/client/dev/debug.js | 5 +---- packages/svelte/src/internal/client/reactivity/effects.js | 5 ++--- packages/svelte/src/internal/client/runtime.js | 6 +----- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 7883609ffed4..5142b77709f2 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -5,7 +5,6 @@ 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 TEMPLATE_EFFECT = 1 << 8; export const UNOWNED = 1 << 9; export const DISCONNECTED = 1 << 10; export const CLEAN = 1 << 11; diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index fcf81578a7bb..2007f0066b18 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -9,8 +9,7 @@ import { EFFECT, MAYBE_DIRTY, RENDER_EFFECT, - ROOT_EFFECT, - TEMPLATE_EFFECT + ROOT_EFFECT } from '../constants.js'; /** @@ -38,8 +37,6 @@ export function log_effect_tree(effect) { label = 'root'; } else if ((flags & BOUNDARY_EFFECT) !== 0) { label = 'boundary'; - } else if ((flags & TEMPLATE_EFFECT) !== 0) { - label = 'template'; } else if ((flags & BLOCK_EFFECT) !== 0) { label = 'block'; } else if ((flags & BRANCH_EFFECT) !== 0) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 8cd5766cd067..5b7ddd400afd 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -35,8 +35,7 @@ import { HEAD_EFFECT, MAYBE_DIRTY, EFFECT_HAS_DERIVED, - BOUNDARY_EFFECT, - TEMPLATE_EFFECT + BOUNDARY_EFFECT } from '../constants.js'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -388,7 +387,7 @@ function create_template_effect(fn, deriveds) { }); } - create_effect(RENDER_EFFECT | TEMPLATE_EFFECT, effect, true); + create_effect(RENDER_EFFECT, effect, true); } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index da7c267b4530..779702f84fec 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,7 +25,6 @@ import { DISCONNECTED, BOUNDARY_EFFECT, REACTION_IS_UPDATING, - TEMPLATE_EFFECT, BOUNDARY_SUSPENDED } from './constants.js'; import { @@ -823,10 +822,7 @@ function process_effects(effect, collected_effects, boundary) { if (!is_skippable_branch && (flags & INERT) === 0) { // Inside a boundary, defer everything except block/branch effects - var defer = - boundary !== undefined && - !is_branch && - ((flags & BLOCK_EFFECT) === 0 || (flags & TEMPLATE_EFFECT) !== 0); + var defer = boundary !== undefined && !is_branch && (flags & BLOCK_EFFECT) === 0; if (defer) { add_boundary_effect(/** @type {Effect} */ (boundary), current_effect); From 7e337bc21ecdf861d928bb6f9e272a1f9e5b233f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 15:35:09 -0500 Subject: [PATCH 165/211] unused --- packages/svelte/src/internal/client/runtime.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 779702f84fec..6f0b09b7db91 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -30,8 +30,7 @@ import { import { flush_idle_tasks, flush_boundary_micro_tasks, - flush_post_micro_tasks, - queue_micro_task + flush_post_micro_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { From 09cf66ccffdcedbcd5c642add2fd1bc2dc09fb62 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 15:43:38 -0500 Subject: [PATCH 166/211] simplify --- packages/svelte/src/internal/client/runtime.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6f0b09b7db91..a6460211d9a2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -820,10 +820,8 @@ function process_effects(effect, collected_effects, boundary) { var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { - // Inside a boundary, defer everything except block/branch effects - var defer = boundary !== undefined && !is_branch && (flags & BLOCK_EFFECT) === 0; - - if (defer) { + if (boundary !== undefined && (flags & (BLOCK_EFFECT | BRANCH_EFFECT)) === 0) { + // Inside a boundary, defer everything except block/branch effects add_boundary_effect(/** @type {Effect} */ (boundary), current_effect); } else if ((flags & BOUNDARY_EFFECT) !== 0) { process_effects(current_effect, collected_effects, current_effect); From 148ffd278371deeafcd4b642f5d6605a8d041b69 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 17:50:33 -0500 Subject: [PATCH 167/211] warn on reactivity loss --- .../.generated/client-warnings.md | 8 +++++++ .../messages/client-warnings/warnings.md | 6 +++++ .../client/visitors/AwaitExpression.js | 21 ++++++++++------- .../internal/client/dom/blocks/boundary.js | 23 ++++++++++++++----- .../internal/client/reactivity/deriveds.js | 13 +++++++++++ .../svelte/src/internal/client/runtime.js | 11 +++++++++ .../svelte/src/internal/client/warnings.js | 11 +++++++++ 7 files changed, 79 insertions(+), 14 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index dcce04bcb824..ba5f957f8d96 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -34,6 +34,14 @@ function add() { } ``` +### await_reactivity_loss + +``` +Detected reactivity loss +``` + +TODO + ### await_waterfall ``` diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index cb0645367b5f..eba1454bf73c 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -30,6 +30,12 @@ function add() { } ``` +## await_reactivity_loss + +> Detected reactivity loss + +TODO + ## await_waterfall > Detected an unnecessary async waterfall 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 index 7a7ca628a84a..b69b2fc72573 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js @@ -1,5 +1,6 @@ /** @import { AwaitExpression, Expression } from 'estree' */ /** @import { Context } from '../types' */ +import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; /** @@ -7,15 +8,19 @@ import * as b from '../../../../utils/builders.js'; * @param {Context} context */ export function AwaitExpression(node, context) { - const suspend = context.state.analysis.context_preserving_awaits.has(node); + const save = context.state.analysis.context_preserving_awaits.has(node); - if (!suspend) { - return context.next(); + if (dev || save) { + return b.call( + b.await( + b.call( + '$.save', + node.argument && /** @type {Expression} */ (context.visit(node.argument)), + !save && b.false + ) + ) + ); } - return b.call( - b.await( - b.call('$.save', node.argument && /** @type {Expression} */ (context.visit(node.argument))) - ) - ); + return context.next(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 97389f9624d8..c35bc01d84db 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -30,6 +30,8 @@ import { import { get_next_sibling } from '../operations.js'; import { queue_boundary_micro_task } from '../task.js'; import * as e from '../../../shared/errors.js'; +import { DEV } from 'esm-env'; +import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -340,15 +342,23 @@ function move_effect(effect, fragment) { } } -export function capture() { +export function capture(track = true) { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; + if (DEV && !track) { + var was_from_async_derived = from_async_derived; + } + return function restore() { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_component_context); + if (track) { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); + } else if (DEV) { + set_from_async_derived(was_from_async_derived); + } // prevent the active effect from outstaying its welcome queue_boundary_micro_task(exit); @@ -390,10 +400,11 @@ export function suspend() { /** * @template T * @param {Promise} promise + * @param {boolean} [track] * @returns {Promise<() => T>} */ -export async function save(promise) { - var restore = capture(); +export async function save(promise, track = true) { + var restore = capture(track); var value = await promise; return () => { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 54915e438ec2..f8a8aaddacdf 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -29,6 +29,14 @@ import { tracing_mode_flag } from '../../flags/index.js'; import { capture, suspend } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; +/** @type {Effect | null} */ +export let from_async_derived = null; + +/** @param {Effect | null} v */ +export function set_from_async_derived(v) { + from_async_derived = v; +} + /** * @template V * @param {() => V} fn @@ -88,8 +96,11 @@ export function async_derived(fn) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var value = source(/** @type {V} */ (undefined)); + // TODO this isn't a block block(async () => { + if (DEV) from_async_derived = active_effect; var current = (promise = fn()); + if (DEV) from_async_derived = null; var restore = capture(); var unsuspend = suspend(); @@ -103,6 +114,8 @@ export function async_derived(fn) { if (promise === current) { restore(); + from_async_derived = null; + internal_set(value, v); } } catch (e) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a6460211d9a2..c60f4d736eb2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -37,6 +37,7 @@ import { destroy_derived, destroy_derived_effects, execute_derived, + from_async_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; @@ -51,6 +52,7 @@ import { set_dev_current_component_function } from './context.js'; import { add_boundary_effect, commit_boundary } from './dom/blocks/boundary.js'; +import * as w from './warnings.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -967,6 +969,15 @@ export function get(signal) { captured_signals.add(signal); } + if (DEV && from_async_derived) { + var tracking = (from_async_derived.f & REACTION_IS_UPDATING) !== 0; + var was_read = from_async_derived.deps !== null && from_async_derived.deps.includes(signal); + + if (!tracking && !was_read) { + w.await_reactivity_loss(); + } + } + // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { if (derived_sources !== null && derived_sources.includes(signal)) { diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index f4dcfdd6508e..79fbebee4cd5 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -18,6 +18,17 @@ export function assignment_value_stale(property, location) { } } +/** + * Detected reactivity loss + */ +export function await_reactivity_loss() { + if (DEV) { + console.warn(`%c[svelte] await_reactivity_loss\n%cDetected reactivity loss\nhttps://svelte.dev/e/await_reactivity_loss`, bold, normal); + } else { + console.warn(`https://svelte.dev/e/await_reactivity_loss`); + } +} + /** * Detected an unnecessary async waterfall */ From 51e50ecb3f51c8c803344cf64a29300366276bec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 18:00:56 -0500 Subject: [PATCH 168/211] add test, tidy up --- .../svelte/src/internal/client/runtime.js | 53 ++++++++++--------- .../samples/async-reactivity-loss/_config.js | 26 +++++++++ .../samples/async-reactivity-loss/main.svelte | 19 +++++++ 3 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c60f4d736eb2..716374d69f5a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -969,15 +969,6 @@ export function get(signal) { captured_signals.add(signal); } - if (DEV && from_async_derived) { - var tracking = (from_async_derived.f & REACTION_IS_UPDATING) !== 0; - var was_read = from_async_derived.deps !== null && from_async_derived.deps.includes(signal); - - if (!tracking && !was_read) { - w.await_reactivity_loss(); - } - } - // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { if (derived_sources !== null && derived_sources.includes(signal)) { @@ -1043,25 +1034,35 @@ export function get(signal) { } } - if ( - DEV && - tracing_mode_flag && - tracing_expressions !== null && - active_reaction !== null && - tracing_expressions.reaction === active_reaction - ) { - // Used when mapping state between special blocks like `each` - if (signal.debug) { - signal.debug(); - } else if (signal.created) { - var entry = tracing_expressions.entries.get(signal); - - if (entry === undefined) { - entry = { read: [] }; - tracing_expressions.entries.set(signal, entry); + if (DEV) { + if (from_async_derived) { + var tracking = (from_async_derived.f & REACTION_IS_UPDATING) !== 0; + var was_read = from_async_derived.deps !== null && from_async_derived.deps.includes(signal); + + if (!tracking && !was_read) { + w.await_reactivity_loss(); } + } - entry.read.push(get_stack('TracedAt')); + if ( + tracing_mode_flag && + tracing_expressions !== null && + active_reaction !== null && + tracing_expressions.reaction === active_reaction + ) { + // Used when mapping state between special blocks like `each` + if (signal.debug) { + signal.debug(); + } else if (signal.created) { + var entry = tracing_expressions.entries.get(signal); + + if (entry === undefined) { + entry = { read: [] }; + tracing_expressions.entries.set(signal, entry); + } + + entry.read.push(get_stack('TracedAt')); + } } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js new file mode 100644 index 000000000000..4ed40d015b49 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -0,0 +1,26 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: `

pending

`, + + async test({ assert, target, warnings }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

3

'); + + assert.deepEqual(warnings, ['Detected reactivity loss']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte new file mode 100644 index 000000000000..488fc25f324d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -0,0 +1,19 @@ + + + + + + +

{await a_plus_b()}

+ + {#snippet pending()} +

pending

+ {/snippet} +
From 5969b0919c1152c2851261ad8df05630500c0728 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 31 Jan 2025 18:26:34 -0500 Subject: [PATCH 169/211] waterfall detection --- .../src/internal/client/reactivity/deriveds.js | 14 ++++++++++++++ packages/svelte/src/internal/client/runtime.js | 3 +++ 2 files changed, 17 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index f8a8aaddacdf..bb6a86cc2a5e 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -22,6 +22,7 @@ import { } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; +import * as w from '../warnings.js'; import { block, destroy_effect } from './effects.js'; import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; @@ -37,6 +38,8 @@ export function set_from_async_derived(v) { from_async_derived = v; } +export const recent_async_deriveds = new Set(); + /** * @template V * @param {() => V} fn @@ -117,6 +120,17 @@ export function async_derived(fn) { from_async_derived = null; internal_set(value, v); + + if (DEV) { + recent_async_deriveds.add(value); + + setTimeout(() => { + if (recent_async_deriveds.has(value)) { + w.await_waterfall(); + recent_async_deriveds.delete(value); + } + }); + } } } catch (e) { handle_error(e, parent, null, parent.ctx); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 716374d69f5a..2990c0dd6954 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -38,6 +38,7 @@ import { destroy_derived_effects, execute_derived, from_async_derived, + recent_async_deriveds, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; @@ -1064,6 +1065,8 @@ export function get(signal) { entry.read.push(get_stack('TracedAt')); } } + + recent_async_deriveds.delete(signal); } return signal.v; From d1551915561d5b708302a47c1290a94d4ff3ac8a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 1 Feb 2025 21:59:17 -0500 Subject: [PATCH 170/211] fix --- .../src/internal/client/reactivity/deriveds.js | 2 +- packages/svelte/src/internal/client/runtime.js | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index bb6a86cc2a5e..451356d30361 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -140,7 +140,7 @@ export function async_derived(fn) { // TODO we should probably null out active effect here, // rather than inside `restore()` } - }); + }, EFFECT_HAS_DERIVED); return Promise.resolve(promise).then(() => value); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2990c0dd6954..802f0bdfc693 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -997,18 +997,14 @@ export function get(signal) { } else { // we're adding a dependency outside the init/update cycle // (i.e. after an `await`) - // TODO we probably want to disable this for user effects, - // otherwise it's a breaking change, albeit a desirable one? - if (deps === null) { - deps = [signal]; - } else if (!deps.includes(signal)) { - deps.push(signal); - } + (active_reaction.deps ??= []).push(signal); + + var reactions = signal.reactions; - if (signal.reactions === null) { + if (reactions === null) { signal.reactions = [active_reaction]; - } else if (!signal.reactions.includes(active_reaction)) { - signal.reactions.push(active_reaction); + } else if (!reactions.includes(active_reaction)) { + reactions.push(active_reaction); } } } else if ( From c9d61951c6aeb8f3f9172dd7fdc649d41996a6ac Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 2 Feb 2025 13:31:48 -0500 Subject: [PATCH 171/211] make purpose explicit --- packages/svelte/src/internal/client/constants.js | 2 +- packages/svelte/src/internal/client/dom/blocks/boundary.js | 5 ++++- packages/svelte/src/internal/client/reactivity/deriveds.js | 6 +++--- packages/svelte/src/internal/client/reactivity/effects.js | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 5142b77709f2..cc04b66a4b44 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -19,7 +19,7 @@ export const EFFECT_TRANSPARENT = 1 << 17; export const LEGACY_DERIVED_PROP = 1 << 18; export const INSPECT_EFFECT = 1 << 19; export const HEAD_EFFECT = 1 << 20; -export const EFFECT_HAS_DERIVED = 1 << 21; +export const EFFECT_PRESERVED = 1 << 21; // effects with this flag should not be pruned // Flags used for async export const REACTION_IS_UPDATING = 1 << 22; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c35bc01d84db..8272c708005b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -3,6 +3,7 @@ import { BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, + EFFECT_PRESERVED, EFFECT_TRANSPARENT, RENDER_EFFECT } from '../../constants.js'; @@ -63,6 +64,8 @@ function with_boundary(boundary, fn) { } } +var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; + /** * @param {TemplateNode} node * @param {{ @@ -317,7 +320,7 @@ export function boundary(node, props, children) { } reset_is_throwing_error(); - }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); + }, flags); if (hydrating) { anchor = hydrate_node; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 451356d30361..6de1ec6ec7c1 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -5,7 +5,7 @@ import { DERIVED, DESTROYED, DIRTY, - EFFECT_HAS_DERIVED, + EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED } from '../constants.js'; @@ -58,7 +58,7 @@ export function derived(fn) { } else { // Since deriveds are evaluated lazily, any effects created inside them are // created too late to ensure that the parent effect is added to the tree - active_effect.f |= EFFECT_HAS_DERIVED; + active_effect.f |= EFFECT_PRESERVED; } /** @type {Derived} */ @@ -140,7 +140,7 @@ export function async_derived(fn) { // TODO we should probably null out active effect here, // rather than inside `restore()` } - }, EFFECT_HAS_DERIVED); + }, EFFECT_PRESERVED); return Promise.resolve(promise).then(() => value); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 5b7ddd400afd..6e2a7600fdcf 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -34,7 +34,7 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_HAS_DERIVED, + EFFECT_PRESERVED, BOUNDARY_EFFECT } from '../constants.js'; import { set } from './sources.js'; @@ -147,7 +147,7 @@ function create_effect(type, fn, sync, push = true) { effect.first === null && effect.nodes_start === null && effect.teardown === null && - (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0; + (effect.f & EFFECT_PRESERVED) === 0; if (!inert && !is_root && push) { if (parent_effect !== null) { From c56ee71653e6386d7155e1c5db673e87acf82f90 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 2 Feb 2025 21:01:43 -0500 Subject: [PATCH 172/211] add showPendingAfter and showPendingFor --- .../2-analyze/visitors/SvelteBoundary.js | 2 +- .../internal/client/dom/blocks/boundary.js | 86 +++++++++++++++---- .../samples/async-pending-timeout/_config.js | 42 +++++++++ .../samples/async-pending-timeout/main.svelte | 11 +++ 4 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-pending-timeout/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-pending-timeout/main.svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index 35af96ba122e..0a49d3b5a488 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -const valid = ['onerror', 'failed', 'pending']; +const valid = ['onerror', 'failed', 'pending', 'showPendingAfter', 'showPendingFor']; /** * @param {AST.SvelteBoundary} node diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 8272c708005b..eaffd07ce382 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -4,6 +4,7 @@ import { BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, EFFECT_PRESERVED, + EFFECT_RAN, EFFECT_TRANSPARENT, RENDER_EFFECT } from '../../constants.js'; @@ -33,6 +34,8 @@ import { queue_boundary_micro_task } from '../task.js'; import * as e from '../../../shared/errors.js'; import { DEV } from 'esm-env'; import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; +import { raf } from '../../timing.js'; +import { loop } from '../../loop.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -69,9 +72,11 @@ var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; /** * @param {TemplateNode} node * @param {{ - * onerror?: (error: unknown, reset: () => void) => void, - * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void - * pending?: (anchor: Node) => void + * onerror?: (error: unknown, reset: () => void) => void; + * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; + * pending?: (anchor: Node) => void; + * showPendingAfter?: number; + * showPendingFor?: number; * }} props * @param {((anchor: Node) => void)} children * @returns {void} @@ -79,6 +84,8 @@ var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; export function boundary(node, props, children) { var anchor = node; + var parent_boundary = find_boundary(active_effect); + block(() => { /** @type {Effect | null} */ var main_effect = null; @@ -106,6 +113,8 @@ export function boundary(node, props, children) { /** @type {Effect[]} */ var effects = []; + var keep_pending_snippet = false; + /** * @param {() => void} snippet_fn * @returns {Effect | null} @@ -145,6 +154,10 @@ export function boundary(node, props, children) { } function unsuspend() { + if (keep_pending_snippet || async_count > 0) { + return; + } + if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) { boundary.f ^= BOUNDARY_SUSPENDED; } @@ -184,19 +197,70 @@ export function boundary(node, props, children) { } } + /** + * @param {boolean} initial + */ + function show_pending_snippet(initial) { + const pending = props.pending; + + if (pending !== undefined) { + // TODO can this be false? + if (main_effect !== null) { + offscreen_fragment = document.createDocumentFragment(); + move_effect(main_effect, offscreen_fragment); + } + + if (pending_effect === null) { + pending_effect = branch(() => pending(anchor)); + } + + // TODO do we want to differentiate between initial render and updates here? + if (!initial) { + keep_pending_snippet = true; + + var end = raf.now() + (props.showPendingFor ?? 300); + + loop((now) => { + if (now >= end) { + keep_pending_snippet = false; + unsuspend(); + return false; + } + + return true; + }); + } + } else if (parent_boundary) { + throw new Error('TODO show pending snippet on parent'); + } else { + throw new Error('no pending snippet to show'); + } + } + // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { if (input === ASYNC_INCREMENT) { + // post-init, show the pending snippet after a timeout + if ((boundary.f & BOUNDARY_SUSPENDED) === 0 && (boundary.f & EFFECT_RAN) !== 0) { + var start = raf.now(); + var end = start + (props.showPendingAfter ?? 500); + + loop((now) => { + if (async_count === 0) return false; + if (now < end) return true; + + show_pending_snippet(false); + }); + } + boundary.f |= BOUNDARY_SUSPENDED; async_count++; - // TODO post-init, show the pending snippet after a timeout - return; } if (input === ASYNC_DECREMENT) { - if (--async_count === 0) { + if (--async_count === 0 && !keep_pending_snippet) { unsuspend(); if (main_effect !== null) { @@ -307,15 +371,7 @@ export function boundary(node, props, children) { if (async_count > 0) { boundary.f |= BOUNDARY_SUSPENDED; - - if (pending) { - offscreen_fragment = document.createDocumentFragment(); - move_effect(main_effect, offscreen_fragment); - - pending_effect = branch(() => pending(anchor)); - } else { - // TODO trigger pending boundary on parent - } + show_pending_snippet(true); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/_config.js b/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/_config.js new file mode 100644 index 000000000000..857703c411c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/_config.js @@ -0,0 +1,42 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component, raf }) { + d.resolve('hello'); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + raf.tick(500); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('wheee'); + await tick(); + raf.tick(600); + assert.htmlEqual(target.innerHTML, '

pending

'); + + raf.tick(800); + assert.htmlEqual(target.innerHTML, '

wheee

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/main.svelte new file mode 100644 index 000000000000..3c6879caee08 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/main.svelte @@ -0,0 +1,11 @@ + + + +

{await promise}

+ + {#snippet pending()} +

pending

+ {/snippet} +
From 0a5628f456dc4e88b9c9ca21679770b9398e9a83 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 2 Feb 2025 21:03:38 -0500 Subject: [PATCH 173/211] improve waterfall detection --- packages/svelte/src/internal/client/reactivity/deriveds.js | 5 +++-- packages/svelte/src/internal/client/reactivity/effects.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6de1ec6ec7c1..f1d63bd1fa04 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -86,10 +86,11 @@ export function derived(fn) { /** * @template V * @param {() => Promise} fn + * @param {boolean} detect_waterfall Whether to print a warning if the value is not read immediately after update * @returns {Promise>} */ /*#__NO_SIDE_EFFECTS__*/ -export function async_derived(fn) { +export function async_derived(fn, detect_waterfall = true) { let parent = /** @type {Effect | null} */ (active_effect); if (parent === null) { @@ -121,7 +122,7 @@ export function async_derived(fn) { internal_set(value, v); - if (DEV) { + if (DEV && detect_waterfall) { recent_async_deriveds.add(value); setTimeout(() => { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 6e2a7600fdcf..4e9ef517269b 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -358,7 +358,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var restore = capture(); var unsuspend = suspend(); - Promise.all(async.map(async_derived)).then((result) => { + Promise.all(async.map((expression) => async_derived(expression, false))).then((result) => { restore(); if ((effect.f & DESTROYED) !== 0) { From 80b713a85e8cd759ef8c17976a51176c83c6d33a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Feb 2025 09:00:54 -0500 Subject: [PATCH 174/211] abort component if already destroyed --- .../compiler/phases/3-transform/client/transform-client.js | 7 +++++-- packages/svelte/src/internal/client/index.js | 1 + packages/svelte/src/internal/client/reactivity/effects.js | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) 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 869604364ab4..ed837b2b6ff7 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 @@ -363,8 +363,7 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (instance.body), analysis.runes || !analysis.needs_context ? b.empty - : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)), - .../** @type {ESTree.Statement[]} */ (template.body) + : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) ]); if (analysis.instance.is_async) { @@ -374,6 +373,8 @@ export function client_component(analysis, options) { b.block([ b.var('$$unsuspend', b.call('$.suspend')), ...component_block.body, + b.if(b.call('$.aborted'), b.return()), + .../** @type {ESTree.Statement[]} */ (template.body), b.stmt(b.call('$$unsuspend')) ]) ); @@ -387,6 +388,8 @@ export function client_component(analysis, options) { b.stmt(b.call(body.id, b.id('node'), b.id('$$props'))), b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) ]); + } else { + component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); } if (!analysis.runes) { diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 12ef0b3658dd..9035e50e4f9c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -101,6 +101,7 @@ export { } from './dom/template.js'; export { async_derived, derived, derived_safe_equal } from './reactivity/deriveds.js'; export { + aborted, effect_tracking, effect_root, legacy_pre_effect, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 4e9ef517269b..84d64faa0e94 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -659,3 +659,8 @@ function resume_children(effect, local) { } } } + +export function aborted() { + var effect = /** @type {Effect} */ (active_effect); + return (effect.f & DESTROYED) !== 0; +} From 0dc84ab2a21a98818053f6d885578c76bd5c5a25 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Feb 2025 12:06:52 -0500 Subject: [PATCH 175/211] only suspend in top-level async deriveds --- .../src/internal/client/reactivity/deriveds.js | 6 +++++- .../samples/async-nested-derived/Child.svelte | 11 +++++++++++ .../samples/async-nested-derived/_config.js | 14 ++++++++++++++ .../samples/async-nested-derived/main.svelte | 17 +++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index f1d63bd1fa04..0735b7296ce2 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -29,6 +29,7 @@ import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; import { capture, suspend } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; +import { noop } from '../../shared/utils.js'; /** @type {Effect | null} */ export let from_async_derived = null; @@ -100,6 +101,9 @@ export function async_derived(fn, detect_waterfall = true) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var value = source(/** @type {V} */ (undefined)); + // only suspend in async deriveds created on initialisation + var should_suspend = !active_reaction; + // TODO this isn't a block block(async () => { if (DEV) from_async_derived = active_effect; @@ -107,7 +111,7 @@ export function async_derived(fn, detect_waterfall = true) { if (DEV) from_async_derived = null; var restore = capture(); - var unsuspend = suspend(); + var unsuspend = should_suspend ? suspend() : noop; try { var v = await promise; diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte new file mode 100644 index 000000000000..546494f4c3d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte @@ -0,0 +1,11 @@ + + +

{indirect}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js new file mode 100644 index 000000000000..172b44e6e322 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js @@ -0,0 +1,14 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

0

'); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '

1

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte new file mode 100644 index 000000000000..e5306f19259c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${count}`)} From c2869f5617f93f241ecbd4bd19cd822a03b197f7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Feb 2025 13:27:24 -0500 Subject: [PATCH 176/211] bump From 5f2abc8fb4d9bcc5ecea0b5348f941528052fc99 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Feb 2025 17:50:14 -0500 Subject: [PATCH 177/211] skip adding dependencies for destroyed effects --- .../svelte/src/internal/client/runtime.js | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4a332194a329..3b2c35a2f335 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -529,6 +529,7 @@ function remove_reaction(signal, dependency) { } } } + // If the derived has no reactions, then we can disconnect it from the graph, // allowing it to either reconnect in the future, or be GC'd by the VM. if ( @@ -965,35 +966,41 @@ export function get(signal) { e.state_unsafe_local_read(); } - var deps = active_reaction.deps; - - if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) { - // we're in the effect init/update cycle - if (signal.rv < read_version) { - signal.rv = read_version; - - // If the signal is accessing the same dependencies in the same - // order as it did last time, increment `skipped_deps` - // rather than updating `new_deps`, which creates GC cost - if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { - skipped_deps++; - } else if (new_deps === null) { - new_deps = [signal]; - } else { - new_deps.push(signal); + // if we're in an async derived, the parent effect could have + // already been destroyed + var destroyed = active_effect !== null && (active_effect.f & DESTROYED) !== 0; + + if (!destroyed) { + var deps = active_reaction.deps; + + if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) { + // we're in the effect init/update cycle + if (signal.rv < read_version) { + signal.rv = read_version; + + // If the signal is accessing the same dependencies in the same + // order as it did last time, increment `skipped_deps` + // rather than updating `new_deps`, which creates GC cost + if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { + skipped_deps++; + } else if (new_deps === null) { + new_deps = [signal]; + } else { + new_deps.push(signal); + } } - } - } else { - // we're adding a dependency outside the init/update cycle - // (i.e. after an `await`) - (active_reaction.deps ??= []).push(signal); + } else { + // we're adding a dependency outside the init/update cycle + // (i.e. after an `await`) + (active_reaction.deps ??= []).push(signal); - var reactions = signal.reactions; + var reactions = signal.reactions; - if (reactions === null) { - signal.reactions = [active_reaction]; - } else if (!reactions.includes(active_reaction)) { - reactions.push(active_reaction); + if (reactions === null) { + signal.reactions = [active_reaction]; + } else if (!reactions.includes(active_reaction)) { + reactions.push(active_reaction); + } } } } else if ( From b64cfc62315a5598c187babdff73f36f759dad08 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 3 Feb 2025 18:01:12 -0500 Subject: [PATCH 178/211] update comment --- packages/svelte/src/internal/client/runtime.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3b2c35a2f335..552a5d626d6e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -966,8 +966,9 @@ export function get(signal) { e.state_unsafe_local_read(); } - // if we're in an async derived, the parent effect could have - // already been destroyed + // if we're in a derived that is being read inside an _async_ derived, + // it's possible that the effect was already destroyed. In this case, + // we don't add the dependency, because that would create a memory leak var destroyed = active_effect !== null && (active_effect.f & DESTROYED) !== 0; if (!destroyed) { From 80550468f9611008aedfe88bd93f47979b2d4d3f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Feb 2025 09:26:06 -0500 Subject: [PATCH 179/211] dont reconnect deriveds inside destroyed effects --- packages/svelte/src/internal/client/runtime.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 552a5d626d6e..8016eeb9b262 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -204,8 +204,12 @@ export function check_dirtiness(reaction) { var length = dependencies.length; // If we are working with a disconnected or an unowned signal that is now connected (due to an active effect) - // then we need to re-connect the reaction to the dependency - if (is_disconnected || is_unowned_connected) { + // then we need to re-connect the reaction to the dependency, unless the effect has already been destroyed + // (which can happen if the derived is read by an async derived) + if ( + (is_disconnected || is_unowned_connected) && + (active_effect === null || (active_effect.f & DESTROYED) === 0) + ) { for (i = 0; i < length; i++) { dependency = dependencies[i]; From ff5d9fec07c13bb0d9ef46834d4aa08584cf9e61 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Feb 2025 17:25:35 -0500 Subject: [PATCH 180/211] pending_items -> offscreen_items --- .../src/internal/client/dom/blocks/each.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 0df4e4b0d49d..cf6c7a0f1270 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -142,7 +142,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var boundary = find_boundary(active_effect); /** @type {Map} */ - var pending_items = new Map(); + var offscreen_items = new Map(); // TODO: ideally we could use derived for runes mode but because of the ability // to use a store which can be mutated, we can't do that here as mutating a store @@ -164,7 +164,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f each_effect, array, state, - pending_items, + offscreen_items, anchor, render_fn, flags, @@ -275,7 +275,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f value = array[i]; key = get_key(value, i); - var existing = state.items.get(key) ?? pending_items.get(key); + var existing = state.items.get(key) ?? offscreen_items.get(key); if (existing) { // update before reconciliation, to trigger any async updates @@ -297,7 +297,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f true ); - pending_items.set(key, item); + offscreen_items.set(key, item); } } @@ -332,7 +332,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f * @param {Effect} each_effect * @param {Array} array * @param {EachState} state - * @param {Map} pending_items + * @param {Map} offscreen_items * @param {Element | Comment | Text} anchor * @param {(anchor: Node, item: MaybeSource, index: number | Source, collection: () => V[]) => void} render_fn * @param {number} flags @@ -344,7 +344,7 @@ function reconcile( each_effect, array, state, - pending_items, + offscreen_items, anchor, render_fn, flags, @@ -406,10 +406,10 @@ function reconcile( item = items.get(key); if (item === undefined) { - var pending = pending_items.get(key); + var pending = offscreen_items.get(key); if (pending !== undefined) { - pending_items.delete(key); + offscreen_items.delete(key); items.set(key, pending); var next = prev && prev.next; @@ -575,9 +575,11 @@ function reconcile( each_effect.first = state.first && state.first.e; each_effect.last = prev && prev.e; - for (var unused of pending_items.values()) { + for (var unused of offscreen_items.values()) { destroy_effect(unused.e); } + + offscreen_items.clear(); } /** From 990634d15f454fe058d8764948243b9fe89f865e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 4 Feb 2025 17:26:11 -0500 Subject: [PATCH 181/211] remove old comment --- packages/svelte/src/internal/client/dom/blocks/each.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index cf6c7a0f1270..c72cc5427042 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -566,12 +566,6 @@ function reconcile( }); } - // TODO this seems super weird... should be `each_effect`, but that doesn't seem to work? - // if (active_effect !== null) { - // active_effect.first = state.first && state.first.e; - // active_effect.last = prev && prev.e; - // } - each_effect.first = state.first && state.first.e; each_effect.last = prev && prev.e; From ae8bd6f2229e57bbd0638c9746c964d7b197c140 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 4 Feb 2025 23:45:02 +0000 Subject: [PATCH 182/211] fix await member expressions --- .../phases/3-transform/client/visitors/shared/component.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 52bac3cb307d..d08b8c06648b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -180,7 +180,8 @@ export function build_component(node, component_name, context, anchor = context. return ( n.type === 'ExpressionTag' && n.expression.type !== 'Identifier' && - n.expression.type !== 'MemberExpression' + (n.expression.type !== 'MemberExpression' || + n.expression.object.type === 'AwaitExpression') ); }); From 994afafbd9c90f25d66855cd74c7bba8beb15e89 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 11:57:42 -0500 Subject: [PATCH 183/211] Revert "fix await member expressions" This reverts commit ae8bd6f2229e57bbd0638c9746c964d7b197c140. --- .../phases/3-transform/client/visitors/shared/component.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index d08b8c06648b..52bac3cb307d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -180,8 +180,7 @@ export function build_component(node, component_name, context, anchor = context. return ( n.type === 'ExpressionTag' && n.expression.type !== 'Identifier' && - (n.expression.type !== 'MemberExpression' || - n.expression.object.type === 'AwaitExpression') + n.expression.type !== 'MemberExpression' ); }); From bcdddc6efb71be74066fd7082b30b98997e81ea5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 11:58:37 -0500 Subject: [PATCH 184/211] fix member expressions for real --- .../client/visitors/shared/component.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 52bac3cb307d..fde88877dc05 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -176,13 +176,15 @@ export function build_component(node, component_name, context, anchor = context. // When we have a non-simple computation, anything other than an Identifier or Member expression, // then there's a good chance it needs to be memoized to avoid over-firing when read within the // child component (e.g. `active={i === index}`) - const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => { - return ( - n.type === 'ExpressionTag' && - n.expression.type !== 'Identifier' && - n.expression.type !== 'MemberExpression' - ); - }); + const should_wrap_in_derived = + metadata.is_async || + get_attribute_chunks(attribute.value).some((n) => { + return ( + n.type === 'ExpressionTag' && + n.expression.type !== 'Identifier' && + n.expression.type !== 'MemberExpression' + ); + }); return should_wrap_in_derived ? b.call( From 2703ac609618b72f60f6eae9b2c34f10da9d9f7c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 12:42:07 -0500 Subject: [PATCH 185/211] fix heuristic for transforming await expressions on server --- .../3-transform/server/visitors/AwaitExpression.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js index f78aa98185b0..9135892dbd60 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js @@ -7,7 +7,17 @@ import * as b from '../../../../utils/builders.js'; * @param {Context} context */ export function AwaitExpression(node, context) { - if (context.state.scope.function_depth > 1) { + // if `await` is inside a function, or inside ` + +

{(await d).value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js new file mode 100644 index 000000000000..c8f20d9597bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js @@ -0,0 +1,43 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d1; + +export default test({ + html: `

pending

`, + + get props() { + d1 = deferred(); + + return { + promise: d1.promise + }; + }, + + async test({ assert, target, component, errors }) { + await Promise.resolve(); + var d2 = deferred(); + component.promise = d2.promise; + + d1.resolve('unused'); + await Promise.resolve(); + await Promise.resolve(); + d2.resolve('hello'); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + + assert.htmlEqual(target.innerHTML, '

hello

'); + + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte new file mode 100644 index 000000000000..718a256b8676 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
From 69a1902a22ad7b9bed5a37885ebd5fd3403b8401 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 18:50:36 -0500 Subject: [PATCH 187/211] small fix --- packages/svelte/src/internal/client/dom/blocks/async.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index c3073d8611d9..19527283a177 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -14,7 +14,7 @@ export function async(node, expressions, fn) { var restore = capture(); var unsuspend = suspend(); - Promise.all(expressions.map(async_derived)).then((result) => { + Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { restore(); fn(node, ...result); unsuspend(); From 461c081cd123018b6effc3607b34757c108e5c01 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 5 Feb 2025 21:47:12 -0500 Subject: [PATCH 188/211] error handling --- .../internal/client/dom/blocks/boundary.js | 18 ++++++--- .../internal/client/reactivity/deriveds.js | 4 +- .../samples/async-error/_config.js | 37 +++++++++++++++++++ .../samples/async-error/main.svelte | 16 ++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index eaffd07ce382..5c768be99bbb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -136,6 +136,12 @@ export function boundary(node, props, children) { } function reset() { + async_count = 0; + + if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) { + boundary.f ^= BOUNDARY_SUSPENDED; + } + if (failed_effect !== null) { pause_effect(failed_effect, () => { failed_effect = null; @@ -151,6 +157,11 @@ export function boundary(node, props, children) { reset_is_throwing_error(); } }); + + if (async_count > 0) { + boundary.f |= BOUNDARY_SUSPENDED; + show_pending_snippet(true); + } } function unsuspend() { @@ -367,12 +378,7 @@ export function boundary(node, props, children) { }); }); } else { - main_effect = branch(() => children(anchor)); - - if (async_count > 0) { - boundary.f |= BOUNDARY_SUSPENDED; - show_pending_snippet(true); - } + reset(); } reset_is_throwing_error(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 3747840f0f13..076ad8dc8f4b 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -147,7 +147,9 @@ export function async_derived(fn, detect_waterfall = true) { } }, (e) => { - handle_error(e, parent, null, parent.ctx); + if (promise === current) { + handle_error(e, parent, null, parent.ctx); + } } ); }, EFFECT_PRESERVED); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js new file mode 100644 index 000000000000..9c7e296287f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -0,0 +1,37 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.reject(new Error('oops!')); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

oops!

'); + + const button = target.querySelector('button'); + + component.promise = (d = deferred()).promise; + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('wheee'); + await Promise.resolve(); + await tick(); + assert.htmlEqual(target.innerHTML, '

wheee

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte new file mode 100644 index 000000000000..dd42fa759689 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte @@ -0,0 +1,16 @@ + + + +

{await promise}

+ + {#snippet pending()} +

pending

+ {/snippet} + + {#snippet failed(error, reset)} +

{error.message}

+ + {/snippet} +
From 0b9bfc9a31c5033f01b8e93b8470376a442fd984 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 6 Feb 2025 07:26:34 -0500 Subject: [PATCH 189/211] async derived cannot use $derived.by --- .../client/visitors/VariableDeclaration.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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 bba554c12a61..e7ad5fe1e410 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 @@ -167,17 +167,7 @@ export function VariableDeclaration(node, context) { declarations.push( b.declarator( declarator.id, - b.call( - b.await( - b.call( - '$.save', - b.call( - '$.async_derived', - rune === '$derived.by' ? value : b.thunk(value, true) - ) - ) - ) - ) + b.call(b.await(b.call('$.save', b.call('$.async_derived', b.thunk(value, true))))) ) ); } else { From 3289ac3ad159b194c95c3f5a397e397a79491682 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 6 Feb 2025 07:37:24 -0500 Subject: [PATCH 190/211] slightly better waterfall warning --- .../.generated/client-warnings.md | 2 +- .../messages/client-warnings/warnings.md | 2 +- .../client/visitors/VariableDeclaration.js | 19 ++++++++++++++++--- packages/svelte/src/constants.js | 1 + .../internal/client/reactivity/deriveds.js | 8 ++++---- .../src/internal/client/reactivity/effects.js | 2 +- .../svelte/src/internal/client/warnings.js | 7 ++++--- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index ba5f957f8d96..82add74353d3 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -45,7 +45,7 @@ TODO ### await_waterfall ``` -Detected an unnecessary async waterfall +An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app. ``` TODO diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index eba1454bf73c..4108cd2fcb5e 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -38,7 +38,7 @@ TODO ## await_waterfall -> Detected an unnecessary async waterfall +> An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app. TODO 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 e7ad5fe1e410..f047fddbdfb7 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 @@ -1,7 +1,7 @@ /** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { Binding } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ -import { dev } from '../../../../state.js'; +import { dev, is_ignored, locate_node } from '../../../../state.js'; import { extract_paths } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; import * as assert from '../../../../utils/assert.js'; @@ -19,7 +19,7 @@ export function VariableDeclaration(node, context) { if (context.state.analysis.runes) { for (const declarator of node.declarations) { - const init = declarator.init; + const init = /** @type {Expression} */ (declarator.init); const rune = get_rune(init, context.state.scope); if ( @@ -164,10 +164,23 @@ export function VariableDeclaration(node, context) { if (declarator.id.type === 'Identifier') { if (is_async) { + const location = dev && is_ignored(init, 'await_waterfall') && locate_node(init); + declarations.push( b.declarator( declarator.id, - b.call(b.await(b.call('$.save', b.call('$.async_derived', b.thunk(value, true))))) + b.call( + b.await( + b.call( + '$.save', + b.call( + '$.async_derived', + b.thunk(value, true), + location ? b.literal(location) : undefined + ) + ) + ) + ) ) ); } else { diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 03fddc5ebd28..d49d70536bc1 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -39,6 +39,7 @@ export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML'; // we use a list of ignorable runtime warnings because not every runtime warning // can be ignored and we want to keep the validation for svelte-ignore in place export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([ + 'await_waterfall', 'state_snapshot_uncloneable', 'binding_property_non_reactive', 'hydration_attribute_changed', diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 076ad8dc8f4b..c2da6639b8b8 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -88,11 +88,11 @@ export function derived(fn) { /** * @template V * @param {() => Promise} fn - * @param {boolean} detect_waterfall Whether to print a warning if the value is not read immediately after update + * @param {string} [location] If provided, print a warning if the value is not read immediately after update * @returns {Promise>} */ /*#__NO_SIDE_EFFECTS__*/ -export function async_derived(fn, detect_waterfall = true) { +export function async_derived(fn, location) { let parent = /** @type {Effect | null} */ (active_effect); if (parent === null) { @@ -129,12 +129,12 @@ export function async_derived(fn, detect_waterfall = true) { internal_set(signal, v); - if (DEV && detect_waterfall) { + if (DEV && location !== undefined) { recent_async_deriveds.add(signal); setTimeout(() => { if (recent_async_deriveds.has(signal)) { - w.await_waterfall(); + w.await_waterfall(location); recent_async_deriveds.delete(signal); } }); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2ab2908c7753..0691b8618041 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -352,7 +352,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var restore = capture(); var unsuspend = suspend(); - Promise.all(async.map((expression) => async_derived(expression, false))).then((result) => { + Promise.all(async.map((expression) => async_derived(expression))).then((result) => { restore(); if ((effect.f & DESTROYED) !== 0) { diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index 79fbebee4cd5..15196d365436 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -30,11 +30,12 @@ export function await_reactivity_loss() { } /** - * Detected an unnecessary async waterfall + * An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app. + * @param {string} location */ -export function await_waterfall() { +export function await_waterfall(location) { if (DEV) { - console.warn(`%c[svelte] await_waterfall\n%cDetected an unnecessary async waterfall\nhttps://svelte.dev/e/await_waterfall`, bold, normal); + console.warn(`%c[svelte] await_waterfall\n%cAn async value (${location}) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app.\nhttps://svelte.dev/e/await_waterfall`, bold, normal); } else { console.warn(`https://svelte.dev/e/await_waterfall`); } From ed2a1e3d43e9a9675c3c23f246b7396d493fdcec Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 7 Feb 2025 18:45:30 +0000 Subject: [PATCH 191/211] async fork values --- .../client/visitors/shared/events.js | 2 +- .../svelte/src/internal/client/constants.js | 5 +- .../internal/client/dom/blocks/boundary.js | 18 +++++- .../internal/client/dom/elements/events.js | 18 +++++- packages/svelte/src/internal/client/index.js | 3 +- .../internal/client/reactivity/deriveds.js | 5 +- .../src/internal/client/reactivity/sources.js | 10 +-- .../svelte/src/internal/client/runtime.js | 64 +++++++++++++------ 8 files changed, 90 insertions(+), 35 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index f23f7548ece1..28dae7a6a4bf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -46,7 +46,7 @@ export function visit_event_attribute(node, context) { // When we hoist a function we assign an array with the function and all // hoisted closure params. - const args = [handler, ...hoisted_params]; + const args = [handler, b.id('$.active_effect'), ...hoisted_params]; delegated_assignment = b.array(args); } else { delegated_assignment = handler; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index cc04b66a4b44..9b2690f33114 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -20,10 +20,11 @@ export const LEGACY_DERIVED_PROP = 1 << 18; export const INSPECT_EFFECT = 1 << 19; export const HEAD_EFFECT = 1 << 20; export const EFFECT_PRESERVED = 1 << 21; // effects with this flag should not be pruned +export const ASYNC_DERIVED = 1 << 22; // Flags used for async -export const REACTION_IS_UPDATING = 1 << 22; -export const BOUNDARY_SUSPENDED = 1 << 23; +export const REACTION_IS_UPDATING = 1 << 23; +export const BOUNDARY_SUSPENDED = 1 << 24; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 5c768be99bbb..2596494f371b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -173,6 +173,10 @@ export function boundary(node, props, children) { boundary.f ^= BOUNDARY_SUSPENDED; } + // @ts-ignore + var sources = boundary.fn.sources; + sources.clear(); + for (const e of render_effects) { try { if (check_dirtiness(e)) { @@ -349,6 +353,9 @@ export function boundary(node, props, children) { } }; + // @ts-ignore + boundary.fn.sources = new Map(); + // @ts-ignore boundary.fn.is_pending = () => props.pending; @@ -438,16 +445,21 @@ export function is_pending_boundary(boundary) { return boundary.fn.is_pending(); } -export function suspend() { - var boundary = active_effect; +export function get_boundary(effect) { + var boundary = effect; while (boundary !== null) { if ((boundary.f & BOUNDARY_EFFECT) !== 0 && is_pending_boundary(boundary)) { - break; + return boundary; } boundary = boundary.parent; } + return null; +} + +export function suspend() { + var boundary = get_boundary(active_effect); if (boundary === null) { e.await_outside_boundary(); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 363b8e1ed501..0d30d4bbce0a 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -8,10 +8,13 @@ import * as w from '../../warnings.js'; import { active_effect, active_reaction, + event_boundary_effect, set_active_effect, - set_active_reaction + set_active_reaction, + set_event_boundary_effect } from '../../runtime.js'; import { without_reactive_context } from './bindings/shared.js'; +import { get_boundary } from '../blocks/boundary.js'; /** @type {Set} */ export const all_registered_events = new Set(); @@ -239,8 +242,17 @@ export function handle_event_propagation(event) { if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) { if (is_array(delegated)) { - var [fn, ...data] = delegated; - fn.apply(current_target, [event, ...data]); + var [fn, effect, ...data] = delegated; + var boundary_effect = (effect !== null && get_boundary(effect)) ?? null; + var previous_boundary_effect = event_boundary_effect; + try { + if (boundary_effect !== null) { + set_event_boundary_effect(boundary_effect); + } + fn.apply(current_target, [event, ...data]); + } finally { + set_event_boundary_effect(previous_boundary_effect); + } } else { delegated.call(current_target, event); } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 9035e50e4f9c..14ffcb5d5962 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -143,7 +143,8 @@ export { untrack, exclude_from_object, deep_read, - deep_read_state + deep_read_state, + active_effect } from './runtime.js'; export { validate_binding, validate_each_keys } from './validate.js'; export { raf } from './timing.js'; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c2da6639b8b8..dd2dc107fa3d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,6 +1,7 @@ /** @import { Derived, Effect, Source } from '#client' */ import { DEV } from 'esm-env'; import { + ASYNC_DERIVED, CLEAN, DERIVED, DESTROYED, @@ -29,7 +30,6 @@ import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; import { capture, suspend } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; -import { noop } from '../../shared/utils.js'; import { UNINITIALIZED } from '../../../constants.js'; /** @type {Effect | null} */ @@ -152,8 +152,9 @@ export function async_derived(fn, location) { } } ); - }, EFFECT_PRESERVED); + }, EFFECT_PRESERVED | ASYNC_DERIVED); + // eslint-disable-next-line no-async-promise-executor return new Promise(async (fulfil) => { // if the effect re-runs before the initial promise // resolves, delay resolution until we have a value diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 0dc55f97babc..5ad2ca6dd7d3 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -169,6 +169,9 @@ export function set(source, value) { */ export function internal_set(source, value) { if (!source.equals(value)) { + + mark_reactions(source, DIRTY, source); + var old_value = source.v; source.v = value; source.wv = increment_write_version(); @@ -181,8 +184,6 @@ export function internal_set(source, value) { } } - mark_reactions(source, DIRTY); - // It's possible that the current reaction might not have up-to-date dependencies // whilst it's actively running. So in the case of ensuring it registers the reaction // properly for itself, we need to ensure the current effect actually gets @@ -257,9 +258,10 @@ export function update_pre(source, d = 1) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY + * @param {Source} [source] * @returns {void} */ -function mark_reactions(signal, status) { +function mark_reactions(signal, status, source) { var reactions = signal.reactions; if (reactions === null) return; @@ -289,7 +291,7 @@ function mark_reactions(signal, status) { if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); } else { - schedule_effect(/** @type {Effect} */ (reaction)); + schedule_effect(/** @type {Effect} */ (reaction), source); } } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8016eeb9b262..d917eec3edfd 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,14 +25,15 @@ import { DISCONNECTED, BOUNDARY_EFFECT, REACTION_IS_UPDATING, - BOUNDARY_SUSPENDED + BOUNDARY_SUSPENDED, + ASYNC_DERIVED } from './constants.js'; import { flush_idle_tasks, flush_boundary_micro_tasks, flush_post_micro_tasks } from './dom/task.js'; -import { internal_set } from './reactivity/sources.js'; +import { internal_set, set } from './reactivity/sources.js'; import { destroy_derived_effects, from_async_derived, @@ -50,7 +51,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; -import { add_boundary_effect, commit_boundary } from './dom/blocks/boundary.js'; +import { add_boundary_effect, commit_boundary, get_boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; const FLUSH_MICROTASK = 0; @@ -109,6 +110,14 @@ export function set_active_effect(effect) { active_effect = effect; } +/** @type {null | Effect} */ +export let event_boundary_effect = null; + +/** @param {null | Effect} effect */ +export function set_event_boundary_effect(effect) { + event_boundary_effect = effect; +} + // TODO remove this, once we're satisfied that we're not leaking context /* @__PURE__ */ setInterval(() => { @@ -776,9 +785,10 @@ function flush_deferred() { /** * @param {Effect} signal + * @param {Source} [source] * @returns {void} */ -export function schedule_effect(signal) { +export function schedule_effect(signal, source) { if (scheduler_mode === FLUSH_MICROTASK) { if (!is_micro_task_queued) { is_micro_task_queued = true; @@ -786,6 +796,17 @@ export function schedule_effect(signal) { } } + if (source && (signal.f & ASYNC_DERIVED) !== 0) { + var boundary = get_boundary(signal); + // @ts-ignore + var sources = boundary.fn.sources; + var entry = sources.get(source); + if (entry === undefined) { + entry = { v: source.v }; + sources.set(source, entry); + } + } + last_scheduled_effect = signal; var effect = signal; @@ -812,10 +833,9 @@ export function schedule_effect(signal) { * * @param {Effect} effect * @param {Effect[]} collected_effects - * @param {Effect} [boundary] * @returns {void} */ -function process_effects(effect, collected_effects, boundary) { +function process_effects(effect, collected_effects) { var current_effect = effect.first; var effects = []; @@ -826,17 +846,7 @@ function process_effects(effect, collected_effects, boundary) { var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { - if (boundary !== undefined && (flags & (BLOCK_EFFECT | BRANCH_EFFECT)) === 0) { - // Inside a boundary, defer everything except block/branch effects - add_boundary_effect(/** @type {Effect} */ (boundary), current_effect); - } else if ((flags & BOUNDARY_EFFECT) !== 0) { - process_effects(current_effect, collected_effects, current_effect); - - if ((current_effect.f & BOUNDARY_SUSPENDED) === 0) { - // no more async work to happen - commit_boundary(current_effect); - } - } else if ((flags & RENDER_EFFECT) !== 0) { + if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { current_effect.f ^= CLEAN; } else { @@ -1024,12 +1034,28 @@ export function get(signal) { } } + var value = signal.v; + if (is_derived) { derived = /** @type {Derived} */ (signal); if (check_dirtiness(derived)) { update_derived(derived); } + value = signal.v; + } else { + var target_effect = event_boundary_effect ?? active_effect; + + if (target_effect !== null && (target_effect.f & ASYNC_DERIVED) === 0) { + var boundary = get_boundary(target_effect); + if (boundary !== null) { + var sources = boundary.fn.sources; + var entry = sources.get(signal); + if (entry !== undefined) { + value = entry.v; + } + } + } } if (DEV) { @@ -1052,7 +1078,7 @@ export function get(signal) { if (signal.debug) { signal.debug(); } else if (signal.created) { - var entry = tracing_expressions.entries.get(signal); + entry = tracing_expressions.entries.get(signal); if (entry === undefined) { entry = { read: [] }; @@ -1066,7 +1092,7 @@ export function get(signal) { recent_async_deriveds.delete(signal); } - return signal.v; + return value; } /** From 163c55cde01ee1c7ddde085457adab5e2b3e533b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 7 Feb 2025 20:08:43 +0000 Subject: [PATCH 192/211] fix --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 7 +++++++ packages/svelte/src/internal/client/reactivity/sources.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 2596494f371b..289d182ab427 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -3,6 +3,7 @@ import { BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, + DIRTY, EFFECT_PRESERVED, EFFECT_RAN, EFFECT_TRANSPARENT, @@ -36,6 +37,7 @@ import { DEV } from 'esm-env'; import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; import { raf } from '../../timing.js'; import { loop } from '../../loop.js'; +import { mark_reactions } from '../../reactivity/sources.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -175,6 +177,11 @@ export function boundary(node, props, children) { // @ts-ignore var sources = boundary.fn.sources; + for (var [source, entry] of sources) { + if (source.v !== entry.v) { + mark_reactions(source, DIRTY); + } + } sources.clear(); for (const e of render_effects) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 5ad2ca6dd7d3..207d8fa06a92 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -261,7 +261,7 @@ export function update_pre(source, d = 1) { * @param {Source} [source] * @returns {void} */ -function mark_reactions(signal, status, source) { +export function mark_reactions(signal, status, source) { var reactions = signal.reactions; if (reactions === null) return; From dfaf0d257c949876bd0a0813f4f3dd14810bd43b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 7 Feb 2025 20:13:05 +0000 Subject: [PATCH 193/211] fix --- .../src/internal/client/dom/blocks/boundary.js | 2 +- packages/svelte/src/internal/client/runtime.js | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 289d182ab427..224131694df0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -179,7 +179,7 @@ export function boundary(node, props, children) { var sources = boundary.fn.sources; for (var [source, entry] of sources) { if (source.v !== entry.v) { - mark_reactions(source, DIRTY); + mark_reactions(source, DIRTY, source); } } sources.clear(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d917eec3edfd..e42caa06b1c5 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -789,14 +789,11 @@ function flush_deferred() { * @returns {void} */ export function schedule_effect(signal, source) { - if (scheduler_mode === FLUSH_MICROTASK) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; - queueMicrotask(flush_deferred); - } - } - if (source && (signal.f & ASYNC_DERIVED) !== 0) { + if (active_effect === signal) { + set_signal_status(signal, MAYBE_DIRTY); + return; + } var boundary = get_boundary(signal); // @ts-ignore var sources = boundary.fn.sources; @@ -807,6 +804,13 @@ export function schedule_effect(signal, source) { } } + if (scheduler_mode === FLUSH_MICROTASK) { + if (!is_micro_task_queued) { + is_micro_task_queued = true; + queueMicrotask(flush_deferred); + } + } + last_scheduled_effect = signal; var effect = signal; From b6be14a00a788927fdb420076277bb77bb23224f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 7 Feb 2025 20:44:51 +0000 Subject: [PATCH 194/211] fix --- .../internal/client/dom/elements/events.js | 25 +++++++++++++------ .../src/internal/client/reactivity/sources.js | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 0d30d4bbce0a..e5711a203a18 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -56,18 +56,27 @@ export function replay_events(dom) { * @param {AddEventListenerOptions} [options] */ export function create_event(event_name, dom, handler, options = {}) { + var boundary_effect = (active_effect !== null && get_boundary(active_effect)) ?? null; /** * @this {EventTarget} */ function target_handler(/** @type {Event} */ event) { - if (!options.capture) { - // Only call in the bubble phase, else delegated events would be called before the capturing events - handle_event_propagation.call(dom, event); - } - if (!event.cancelBubble) { - return without_reactive_context(() => { - return handler?.call(this, event); - }); + var previous_boundary_effect = event_boundary_effect; + try { + if (boundary_effect !== null) { + set_event_boundary_effect(boundary_effect); + } + if (!options.capture) { + // Only call in the bubble phase, else delegated events would be called before the capturing events + handle_event_propagation.call(dom, event); + } + if (!event.cancelBubble) { + return without_reactive_context(() => { + return handler?.call(this, event); + }); + } + } finally { + set_event_boundary_effect(previous_boundary_effect) } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 207d8fa06a92..f24eb5e90480 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -289,7 +289,7 @@ export function mark_reactions(signal, status, source) { // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, source); } else { schedule_effect(/** @type {Effect} */ (reaction), source); } From 2e1a0aca39aa326939d88a17071db5e51c3ef355 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 7 Feb 2025 21:06:05 +0000 Subject: [PATCH 195/211] chore: unify bindings to use internal event system --- .changeset/mighty-icons-sin.md | 5 +++++ .../client/dom/elements/bindings/media.js | 5 +++-- .../client/dom/elements/bindings/shared.js | 13 ++++++----- .../client/dom/elements/bindings/universal.js | 5 +++-- .../client/dom/elements/bindings/window.js | 22 +++++++++---------- .../src/internal/client/dom/elements/misc.js | 4 +++- 6 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 .changeset/mighty-icons-sin.md diff --git a/.changeset/mighty-icons-sin.md b/.changeset/mighty-icons-sin.md new file mode 100644 index 000000000000..e117aec8f133 --- /dev/null +++ b/.changeset/mighty-icons-sin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: unify bindings to use internal event system diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/media.js b/packages/svelte/src/internal/client/dom/elements/bindings/media.js index 4893426d5552..fbf34102f917 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/media.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/media.js @@ -1,6 +1,7 @@ import { hydrating } from '../../hydration.js'; import { render_effect, effect, teardown } from '../../../reactivity/effects.js'; import { listen } from './shared.js'; +import { on } from '../events.js'; /** @param {TimeRanges} ranges */ function time_ranges_to_array(ranges) { @@ -42,7 +43,7 @@ export function bind_current_time(media, get, set = get) { }; raf_id = requestAnimationFrame(callback); - media.addEventListener('timeupdate', callback); + var destroy = on(media, 'timeupdate', callback); render_effect(() => { var next_value = Number(get()); @@ -54,7 +55,7 @@ export function bind_current_time(media, get, set = get) { teardown(() => { cancelAnimationFrame(raf_id); - media.removeEventListener('timeupdate', callback); + destroy(); }); } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js index aa083776a5bc..1f8ff703c07e 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js @@ -1,3 +1,4 @@ +import { run_all } from '../../../../shared/utils.js'; import { teardown } from '../../../reactivity/effects.js'; import { active_effect, @@ -5,6 +6,7 @@ import { set_active_effect, set_active_reaction } from '../../../runtime.js'; +import { on } from '../events.js'; import { add_form_reset_listener } from '../misc.js'; /** @@ -20,14 +22,15 @@ export function listen(target, events, handler, call_handler_immediately = true) handler(); } + /** @type {(() => void)[]} */ + var destroys = []; + for (var name of events) { - target.addEventListener(name, handler); + destroys.push(on(target, name, handler)); } teardown(() => { - for (var name of events) { - target.removeEventListener(name, handler); - } + run_all(destroys); }); } @@ -57,7 +60,7 @@ export function without_reactive_context(fn) { * @param {(is_reset?: true) => void} [on_reset] */ export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) { - element.addEventListener(event, () => without_reactive_context(handler)); + on(element, event, () => handler()); // @ts-expect-error const prev = element.__on_r; if (prev) { diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/universal.js b/packages/svelte/src/internal/client/dom/elements/bindings/universal.js index 5b10abdc4c70..fe3a459d2f5d 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/universal.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/universal.js @@ -1,4 +1,5 @@ import { render_effect, teardown } from '../../../reactivity/effects.js'; +import { on } from '../events.js'; import { listen } from './shared.js'; /** @@ -9,7 +10,7 @@ import { listen } from './shared.js'; * @returns {void} */ export function bind_content_editable(property, element, get, set = get) { - element.addEventListener('input', () => { + on(element, 'input', () => { // @ts-ignore set(element[property]); }); @@ -44,7 +45,7 @@ export function bind_property(property, event_name, element, set, get) { set(element[property]); }; - element.addEventListener(event_name, handler); + on(element, event_name, handler); if (get) { render_effect(() => { diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/window.js b/packages/svelte/src/internal/client/dom/elements/bindings/window.js index 2f7e44c5d988..b47910e97efd 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/window.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/window.js @@ -1,5 +1,6 @@ import { effect, render_effect, teardown } from '../../../reactivity/effects.js'; -import { listen, without_reactive_context } from './shared.js'; +import { on } from '../events.js'; +import { listen } from './shared.js'; /** * @param {'x' | 'y'} type @@ -10,16 +11,15 @@ import { listen, without_reactive_context } from './shared.js'; export function bind_window_scroll(type, get, set = get) { var is_scrolling_x = type === 'x'; - var target_handler = () => - without_reactive_context(() => { - scrolling = true; - clearTimeout(timeout); - timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?) + var target_handler = () => { + scrolling = true; + clearTimeout(timeout); + timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?) - set(window[is_scrolling_x ? 'scrollX' : 'scrollY']); - }); + set(window[is_scrolling_x ? 'scrollX' : 'scrollY']); + }; - addEventListener('scroll', target_handler, { + var destroy = on(window, 'scroll', target_handler, { passive: true }); @@ -53,7 +53,7 @@ export function bind_window_scroll(type, get, set = get) { effect(target_handler); teardown(() => { - removeEventListener('scroll', target_handler); + destroy(); }); } @@ -62,5 +62,5 @@ export function bind_window_scroll(type, get, set = get) { * @param {(size: number) => void} set */ export function bind_window_size(type, set) { - listen(window, ['resize'], () => without_reactive_context(() => set(window[type]))); + listen(window, ['resize'], () => set(window[type])); } diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index 61e513903f76..2e730471b633 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,7 @@ import { hydrating } from '../hydration.js'; import { clear_text_content, get_first_child } from '../operations.js'; import { queue_micro_task } from '../task.js'; +import { on } from './events.js'; /** * @param {HTMLElement} dom @@ -37,7 +38,8 @@ let listening_to_form_reset = false; export function add_form_reset_listener() { if (!listening_to_form_reset) { listening_to_form_reset = true; - document.addEventListener( + on( + document, 'reset', (evt) => { // Needs to happen one tick later or else the dom properties of the form From 60f3ded1ffa367b9d7198b21e77b41b378f92aa8 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 7 Feb 2025 21:31:03 +0000 Subject: [PATCH 196/211] remove deadcode --- .../internal/client/dom/blocks/boundary.js | 71 ++----------------- .../svelte/src/internal/client/runtime.js | 11 ++- 2 files changed, 13 insertions(+), 69 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 224131694df0..5b4642738385 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -6,8 +6,7 @@ import { DIRTY, EFFECT_PRESERVED, EFFECT_RAN, - EFFECT_TRANSPARENT, - RENDER_EFFECT + EFFECT_TRANSPARENT } from '../../constants.js'; import { component_context, set_component_context } from '../../context.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; @@ -18,9 +17,7 @@ import { set_active_effect, set_active_reaction, reset_is_throwing_error, - schedule_effect, - check_dirtiness, - update_effect + schedule_effect } from '../../runtime.js'; import { hydrate_next, @@ -42,9 +39,6 @@ import { mark_reactions } from '../../reactivity/sources.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); const ADD_CALLBACK = Symbol(); -const ADD_RENDER_EFFECT = Symbol(); -const ADD_EFFECT = Symbol(); -const COMMIT = Symbol(); /** * @param {Effect} boundary @@ -109,12 +103,6 @@ export function boundary(node, props, children) { /** @type {Set<() => void>} */ var callbacks = new Set(); - /** @type {Effect[]} */ - var render_effects = []; - - /** @type {Effect[]} */ - var effects = []; - var keep_pending_snippet = false; /** @@ -184,16 +172,6 @@ export function boundary(node, props, children) { } sources.clear(); - for (const e of render_effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); - } - } catch (error) { - handle_error(error, e, null, e.ctx); - } - } - for (const fn of callbacks) fn(); callbacks.clear(); @@ -207,16 +185,6 @@ export function boundary(node, props, children) { anchor.before(offscreen_fragment); offscreen_fragment = null; } - - for (const e of effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); - } - } catch (error) { - handle_error(error, e, null, e.ctx); - } - } } /** @@ -299,21 +267,6 @@ export function boundary(node, props, children) { return; } - if (input === ADD_RENDER_EFFECT) { - render_effects.push(payload); - return; - } - - if (input === ADD_EFFECT) { - effects.push(payload); - return; - } - - if (input === COMMIT) { - unsuspend(); - return; - } - var error = input; var onerror = props.onerror; let failed = props.failed; @@ -452,6 +405,9 @@ export function is_pending_boundary(boundary) { return boundary.fn.is_pending(); } +/** + * @param {Effect | null} effect + */ export function get_boundary(effect) { var boundary = effect; @@ -526,20 +482,3 @@ export function add_boundary_callback(boundary, fn) { // @ts-ignore boundary.fn(ADD_CALLBACK, fn); } - -/** - * @param {Effect} boundary - * @param {Effect} effect - */ -export function add_boundary_effect(boundary, effect) { - // @ts-ignore - boundary.fn((effect.f & RENDER_EFFECT) !== 0 ? ADD_RENDER_EFFECT : ADD_EFFECT, effect); -} - -/** - * @param {Effect} boundary - */ -export function commit_boundary(boundary) { - // @ts-ignore - boundary.fn?.(COMMIT); -} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e42caa06b1c5..db2b44102f26 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,7 +25,6 @@ import { DISCONNECTED, BOUNDARY_EFFECT, REACTION_IS_UPDATING, - BOUNDARY_SUSPENDED, ASYNC_DERIVED } from './constants.js'; import { @@ -51,7 +50,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; -import { add_boundary_effect, commit_boundary, get_boundary } from './dom/blocks/boundary.js'; +import { get_boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; const FLUSH_MICROTASK = 0; @@ -70,6 +69,7 @@ let is_micro_task_queued = false; let last_scheduled_effect = null; export let is_flushing_effect = false; +export let is_flushing_async_derived = false; export let is_destroying_effect = false; /** @param {boolean} value */ @@ -858,8 +858,12 @@ function process_effects(effect, collected_effects) { // to ensure that unowned deriveds are correctly tracked // because we're flushing the current effect var previous_active_reaction = active_reaction; + var previous_is_flushing_async_derived = is_flushing_async_derived; try { active_reaction = current_effect; + if ((current_effect.f & ASYNC_DERIVED) !== 0) { + is_flushing_async_derived = true; + } if (check_dirtiness(current_effect)) { update_effect(current_effect); } @@ -867,6 +871,7 @@ function process_effects(effect, collected_effects) { handle_error(error, current_effect, null, current_effect.ctx); } finally { active_reaction = previous_active_reaction; + is_flushing_async_derived = previous_is_flushing_async_derived; } } @@ -1050,7 +1055,7 @@ export function get(signal) { } else { var target_effect = event_boundary_effect ?? active_effect; - if (target_effect !== null && (target_effect.f & ASYNC_DERIVED) === 0) { + if (target_effect !== null && !is_flushing_async_derived) { var boundary = get_boundary(target_effect); if (boundary !== null) { var sources = boundary.fn.sources; From a544f25fa2f024665de4f358933aac7a2bda297e Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 8 Feb 2025 13:27:43 +0000 Subject: [PATCH 197/211] wip --- .../internal/client/dom/blocks/boundary.js | 12 +-- .../src/internal/client/reactivity/sources.js | 9 +-- .../svelte/src/internal/client/runtime.js | 78 ++++++++++++------- 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 5b4642738385..c8eb3ba6d818 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -164,13 +164,13 @@ export function boundary(node, props, children) { } // @ts-ignore - var sources = boundary.fn.sources; - for (var [source, entry] of sources) { - if (source.v !== entry.v) { - mark_reactions(source, DIRTY, source); + var forks = boundary.fn.forks; + for (var [signal, entry] of forks) { + if (signal.v !== entry.v) { + mark_reactions(signal, DIRTY); } } - sources.clear(); + forks.clear(); for (const fn of callbacks) fn(); callbacks.clear(); @@ -314,7 +314,7 @@ export function boundary(node, props, children) { }; // @ts-ignore - boundary.fn.sources = new Map(); + boundary.fn.forks = new Map(); // @ts-ignore boundary.fn.is_pending = () => props.pending; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index f24eb5e90480..9aff5c325032 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -170,7 +170,7 @@ export function set(source, value) { export function internal_set(source, value) { if (!source.equals(value)) { - mark_reactions(source, DIRTY, source); + mark_reactions(source, DIRTY); var old_value = source.v; source.v = value; @@ -258,10 +258,9 @@ export function update_pre(source, d = 1) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY - * @param {Source} [source] * @returns {void} */ -export function mark_reactions(signal, status, source) { +export function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; @@ -289,9 +288,9 @@ export function mark_reactions(signal, status, source) { // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, source); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); } else { - schedule_effect(/** @type {Effect} */ (reaction), source); + schedule_effect(/** @type {Effect} */ (reaction)); } } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5d3bf020edf4..008b94623e8f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -40,7 +40,7 @@ import { update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; -import { FILENAME } from '../../constants.js'; +import { FILENAME, UNINITIALIZED } from '../../constants.js'; import { tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; import { @@ -783,24 +783,44 @@ function flush_deferred() { } } +/** + * @param {Source | Derived} signal + * @param {any} forks + */ +function fork_dependencies(signal, forks) { + var entry = forks.get(signal); + if (entry === undefined) { + entry = { v: signal.v }; + forks.set(signal, entry); + if ((signal.f & DERIVED) !== 0) { + var deps = /** @type {Derived} */ (signal).deps; + if (deps !== null) { + for (var i = 0; i < deps.length; i++) { + fork_dependencies(deps[i], forks); + } + } + } + } +} + /** * @param {Effect} signal - * @param {Source} [source] * @returns {void} */ -export function schedule_effect(signal, source) { - if (source && (signal.f & ASYNC_DERIVED) !== 0) { +export function schedule_effect(signal) { + if ((signal.f & ASYNC_DERIVED) !== 0) { if (active_effect === signal) { set_signal_status(signal, MAYBE_DIRTY); return; } var boundary = get_boundary(signal); // @ts-ignore - var sources = boundary.fn.sources; - var entry = sources.get(source); - if (entry === undefined) { - entry = { v: source.v }; - sources.set(source, entry); + var forks = boundary.fn.forks; + var deps = signal.deps; + if (deps !== null) { + for (var i = 0; i < deps.length; i++) { + fork_dependencies(deps[i], forks); + } } } @@ -1043,28 +1063,32 @@ export function get(signal) { } } - var value = signal.v; + var value = /** @type {V} */ (UNINITIALIZED); - if (is_derived) { - derived = /** @type {Derived} */ (signal); + var target_effect = event_boundary_effect ?? active_effect; - if (check_dirtiness(derived)) { - update_derived(derived); + if (target_effect !== null && !is_flushing_async_derived) { + var boundary = get_boundary(target_effect); + if (boundary !== null) { + // @ts-ignore + var forks = boundary.fn.forks; + var entry = forks.get(signal); + if (entry !== undefined) { + value = entry.v; + } } - value = signal.v; - } else { - var target_effect = event_boundary_effect ?? active_effect; - - if (target_effect !== null && !is_flushing_async_derived) { - var boundary = get_boundary(target_effect); - if (boundary !== null) { - // @ts-ignore - var sources = boundary.fn.sources; - var entry = sources.get(signal); - if (entry !== undefined) { - value = entry.v; - } + } + + if (value === UNINITIALIZED) { + if (is_derived) { + derived = /** @type {Derived} */ (signal); + + if (check_dirtiness(derived)) { + update_derived(derived); } + value = signal.v; + } else { + value = signal.v; } } From 044475de014ffecbebd37702442056208b9fbfde Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 8 Feb 2025 15:12:16 +0000 Subject: [PATCH 198/211] wip --- .../src/internal/client/dom/blocks/boundary.js | 12 ++++++++++-- .../runtime-runes/samples/async-derived/_config.js | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c8eb3ba6d818..f8e3e5839ffd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -3,6 +3,7 @@ import { BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, + DERIVED, DIRTY, EFFECT_PRESERVED, EFFECT_RAN, @@ -34,7 +35,7 @@ import { DEV } from 'esm-env'; import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; import { raf } from '../../timing.js'; import { loop } from '../../loop.js'; -import { mark_reactions } from '../../reactivity/sources.js'; +import { internal_set, mark_reactions } from '../../reactivity/sources.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -165,9 +166,16 @@ export function boundary(node, props, children) { // @ts-ignore var forks = boundary.fn.forks; + for (var [signal, entry] of forks) { if (signal.v !== entry.v) { - mark_reactions(signal, DIRTY); + if ((signal.f & DERIVED) === 0) { + var val = signal.v; + signal.v = entry.v; + internal_set(signal, val); + } else { + mark_reactions(signal, DIRTY); + } } } forks.clear(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index dbe76c573b7f..38ca6d34ad55 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -32,7 +32,11 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); await tick(); + flushSync(); assert.htmlEqual(target.innerHTML, '

84

'); d = deferred(); From 84b6ad7228786667e16732e808da582ca73bff5c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 8 Feb 2025 20:55:40 +0000 Subject: [PATCH 199/211] wip --- .../internal/client/dom/blocks/boundary.js | 10 +- .../src/internal/client/reactivity/sources.js | 94 ++++++++++++++++++- .../svelte/src/internal/client/runtime.js | 42 +-------- .../samples/async-derived/_config.js | 4 - 4 files changed, 100 insertions(+), 50 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index f8e3e5839ffd..2698ec54082a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -18,7 +18,8 @@ import { set_active_effect, set_active_reaction, reset_is_throwing_error, - schedule_effect + schedule_effect, + increment_write_version } from '../../runtime.js'; import { hydrate_next, @@ -170,11 +171,8 @@ export function boundary(node, props, children) { for (var [signal, entry] of forks) { if (signal.v !== entry.v) { if ((signal.f & DERIVED) === 0) { - var val = signal.v; - signal.v = entry.v; - internal_set(signal, val); - } else { - mark_reactions(signal, DIRTY); + mark_reactions(signal, DIRTY, undefined, true); + signal.wv = increment_write_version(); } } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9aff5c325032..289b5e38064c 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -28,12 +28,14 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + ASYNC_DERIVED } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; +import { get_boundary } from '../dom/blocks/boundary.js'; export let inspect_effects = new Set(); @@ -169,6 +171,7 @@ export function set(source, value) { */ export function internal_set(source, value) { if (!source.equals(value)) { + possibly_fork(source); mark_reactions(source, DIRTY); @@ -258,9 +261,10 @@ export function update_pre(source, d = 1) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY + * @param {Value} [parent] * @returns {void} */ -export function mark_reactions(signal, status) { +export function mark_reactions(signal, status, parent, only_boundary = false) { var reactions = signal.reactions; if (reactions === null) return; @@ -277,6 +281,24 @@ export function mark_reactions(signal, status) { // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue; + if (only_boundary) { + if ((flags & (DERIVED)) === 0) { + var boundary = get_boundary(/** @type {Effect} */ (reaction)); + if (!boundary) { + continue; + } + } + } else if ((flags & (DERIVED | ASYNC_DERIVED)) === 0) { + boundary = get_boundary(/** @type {Effect} */ (reaction)); + if (boundary) { + // @ts-ignore + var forks = boundary.fn.forks; + if (forks.has(signal) || forks.has(parent)) { + continue; + } + } + } + // Inspect effects need to run immediately, so that the stack trace makes sense if (DEV && (flags & INSPECT_EFFECT) !== 0) { inspect_effects.add(reaction); @@ -288,10 +310,76 @@ export function mark_reactions(signal, status) { // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, signal, only_boundary); } else { schedule_effect(/** @type {Effect} */ (reaction)); } } } } + +/** + * @param {Source | Derived} signal + * @param {any} forks + */ +function fork_dependencies(signal, forks) { + var entry = forks.get(signal); + if (entry === undefined) { + entry = { v: signal.v }; + forks.set(signal, entry); + if ((signal.f & DERIVED) !== 0) { + var deps = /** @type {Derived} */ (signal).deps; + if (deps !== null) { + for (var i = 0; i < deps.length; i++) { + fork_dependencies(deps[i], forks); + } + } + } + } +} + +/** + * @param {Value} signal + * @returns {void} + */ +function possibly_fork(signal) { + var reactions = signal.reactions; + if (reactions === null) return; + + var runes = is_runes(); + var length = reactions.length; + + for (var i = 0; i < length; i++) { + var reaction = reactions[i]; + var flags = reaction.f; + + // Skip any effects that are already dirty + if ((flags & DIRTY) !== 0) continue; + + // In legacy mode, skip the current effect to prevent infinite loops + if (!runes && reaction === active_effect) continue; + + // If the signal a) was previously clean or b) is an unowned derived, then mark it + if ((flags & (CLEAN | UNOWNED)) !== 0) { + if ((flags & DERIVED) !== 0) { + possibly_fork(/** @type {Derived} */ (reaction)); + } else { + if ((reaction.f & ASYNC_DERIVED) !== 0) { + // if (active_effect === signal) { + // set_signal_status(signal, MAYBE_DIRTY); + // return; + // } + var boundary = get_boundary(/** @type {Effect} */ (reaction)); + // @ts-ignore + var forks = boundary.fn.forks; + var deps = reaction.deps; + if (deps !== null) { + for (var s = 0; s < deps.length; s++) { + fork_dependencies(deps[s], forks); + } + } + } + } + } + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 008b94623e8f..6efa1dbe634b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -783,47 +783,11 @@ function flush_deferred() { } } -/** - * @param {Source | Derived} signal - * @param {any} forks - */ -function fork_dependencies(signal, forks) { - var entry = forks.get(signal); - if (entry === undefined) { - entry = { v: signal.v }; - forks.set(signal, entry); - if ((signal.f & DERIVED) !== 0) { - var deps = /** @type {Derived} */ (signal).deps; - if (deps !== null) { - for (var i = 0; i < deps.length; i++) { - fork_dependencies(deps[i], forks); - } - } - } - } -} - /** * @param {Effect} signal * @returns {void} */ export function schedule_effect(signal) { - if ((signal.f & ASYNC_DERIVED) !== 0) { - if (active_effect === signal) { - set_signal_status(signal, MAYBE_DIRTY); - return; - } - var boundary = get_boundary(signal); - // @ts-ignore - var forks = boundary.fn.forks; - var deps = signal.deps; - if (deps !== null) { - for (var i = 0; i < deps.length; i++) { - fork_dependencies(deps[i], forks); - } - } - } - if (scheduler_mode === FLUSH_MICROTASK) { if (!is_micro_task_queued) { is_micro_task_queued = true; @@ -1067,7 +1031,11 @@ export function get(signal) { var target_effect = event_boundary_effect ?? active_effect; - if (target_effect !== null && !is_flushing_async_derived) { + if ( + target_effect !== null && + !is_flushing_async_derived && + (target_effect.f & ASYNC_DERIVED) === 0 + ) { var boundary = get_boundary(target_effect); if (boundary !== null) { // @ts-ignore diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index 38ca6d34ad55..dbe76c573b7f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -32,11 +32,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); await tick(); - flushSync(); assert.htmlEqual(target.innerHTML, '

84

'); d = deferred(); From 21907539f837867f0ee071f2e399a241c54f3cd9 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 8 Feb 2025 20:57:42 +0000 Subject: [PATCH 200/211] wip --- packages/svelte/src/internal/client/reactivity/sources.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 289b5e38064c..3865e66dc2a4 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -282,7 +282,7 @@ export function mark_reactions(signal, status, parent, only_boundary = false) { if (!runes && reaction === active_effect) continue; if (only_boundary) { - if ((flags & (DERIVED)) === 0) { + if ((flags & DERIVED) === 0) { var boundary = get_boundary(/** @type {Effect} */ (reaction)); if (!boundary) { continue; From 7cfabcc976ae81739b37e2d1611fe2257b95d848 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 9 Feb 2025 01:13:23 +0000 Subject: [PATCH 201/211] wip --- packages/svelte/src/internal/client/reactivity/sources.js | 1 + .../tests/runtime-runes/samples/async-key/_config.js | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3865e66dc2a4..75567170f002 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -153,6 +153,7 @@ export function set(source, value) { !untracking && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && + (active_reaction.f & (ASYNC_DERIVED)) === 0 && // If the source was created locally within the current derived, then // we allow the mutation. (derived_sources === null || !derived_sources.includes(source)) diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js index 293ac9357a2f..7c3f39f8f32e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -41,9 +41,9 @@ export default test({ await tick(); assert.htmlEqual(target.innerHTML, '

hello

'); - d.resolve(2); - await tick(); - assert.htmlEqual(target.innerHTML, '

hello

'); - assert.notEqual(target.querySelector('h1'), h1); + // d.resolve(2); + // await tick(); + // assert.htmlEqual(target.innerHTML, '

hello

'); + // assert.notEqual(target.querySelector('h1'), h1); } }); From 5796edf11365ced2877ed73f0cf8ee94c1f93cc5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 9 Feb 2025 16:12:22 +0000 Subject: [PATCH 202/211] wip --- .../tests/runtime-runes/samples/async-key/_config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js index 7c3f39f8f32e..293ac9357a2f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -41,9 +41,9 @@ export default test({ await tick(); assert.htmlEqual(target.innerHTML, '

hello

'); - // d.resolve(2); - // await tick(); - // assert.htmlEqual(target.innerHTML, '

hello

'); - // assert.notEqual(target.querySelector('h1'), h1); + d.resolve(2); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + assert.notEqual(target.querySelector('h1'), h1); } }); From bfa2f3bc52bc9f94583201f363e015c2f3ffc7b6 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 9 Feb 2025 17:21:39 +0000 Subject: [PATCH 203/211] wip --- packages/svelte/src/internal/client/reactivity/sources.js | 1 - .../svelte/tests/runtime-runes/samples/async-key/_config.js | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 75567170f002..3865e66dc2a4 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -153,7 +153,6 @@ export function set(source, value) { !untracking && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && - (active_reaction.f & (ASYNC_DERIVED)) === 0 && // If the source was created locally within the current derived, then // we allow the mutation. (derived_sources === null || !derived_sources.includes(source)) diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js index 293ac9357a2f..ddf970ef0e63 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -37,6 +37,8 @@ export default test({ assert.equal(target.querySelector('h1'), h1); d = deferred(); + // TODO context leaks without this? + await Promise.resolve(); component.promise = d.promise; await tick(); assert.htmlEqual(target.innerHTML, '

hello

'); From 91ef2199804a051664b8e1a121108524df1614ee Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 10 Feb 2025 17:26:53 +0000 Subject: [PATCH 204/211] feedback --- .../client/visitors/shared/events.js | 2 +- .../internal/client/dom/elements/events.js | 42 +++++-------------- packages/svelte/src/internal/client/index.js | 3 +- .../src/internal/client/reactivity/sources.js | 6 +-- .../svelte/src/internal/client/runtime.js | 31 +++++--------- 5 files changed, 27 insertions(+), 57 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index 28dae7a6a4bf..f23f7548ece1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -46,7 +46,7 @@ export function visit_event_attribute(node, context) { // When we hoist a function we assign an array with the function and all // hoisted closure params. - const args = [handler, b.id('$.active_effect'), ...hoisted_params]; + const args = [handler, ...hoisted_params]; delegated_assignment = b.array(args); } else { delegated_assignment = handler; diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index e5711a203a18..e74c27c90d09 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -8,10 +8,8 @@ import * as w from '../../warnings.js'; import { active_effect, active_reaction, - event_boundary_effect, set_active_effect, - set_active_reaction, - set_event_boundary_effect + set_active_reaction } from '../../runtime.js'; import { without_reactive_context } from './bindings/shared.js'; import { get_boundary } from '../blocks/boundary.js'; @@ -56,27 +54,18 @@ export function replay_events(dom) { * @param {AddEventListenerOptions} [options] */ export function create_event(event_name, dom, handler, options = {}) { - var boundary_effect = (active_effect !== null && get_boundary(active_effect)) ?? null; /** * @this {EventTarget} */ function target_handler(/** @type {Event} */ event) { - var previous_boundary_effect = event_boundary_effect; - try { - if (boundary_effect !== null) { - set_event_boundary_effect(boundary_effect); - } - if (!options.capture) { - // Only call in the bubble phase, else delegated events would be called before the capturing events - handle_event_propagation.call(dom, event); - } - if (!event.cancelBubble) { - return without_reactive_context(() => { - return handler?.call(this, event); - }); - } - } finally { - set_event_boundary_effect(previous_boundary_effect) + if (!options.capture) { + // Only call in the bubble phase, else delegated events would be called before the capturing events + handle_event_propagation.call(dom, event); + } + if (!event.cancelBubble) { + return without_reactive_context(() => { + return handler?.call(this, event); + }); } } @@ -251,17 +240,8 @@ export function handle_event_propagation(event) { if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) { if (is_array(delegated)) { - var [fn, effect, ...data] = delegated; - var boundary_effect = (effect !== null && get_boundary(effect)) ?? null; - var previous_boundary_effect = event_boundary_effect; - try { - if (boundary_effect !== null) { - set_event_boundary_effect(boundary_effect); - } - fn.apply(current_target, [event, ...data]); - } finally { - set_event_boundary_effect(previous_boundary_effect); - } + var [fn, ...data] = delegated; + fn.apply(current_target, [event, ...data]); } else { delegated.call(current_target, event); } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 14ffcb5d5962..9035e50e4f9c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -143,8 +143,7 @@ export { untrack, exclude_from_object, deep_read, - deep_read_state, - active_effect + deep_read_state } from './runtime.js'; export { validate_binding, validate_each_keys } from './validate.js'; export { raf } from './timing.js'; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3865e66dc2a4..d3a0b9627ad2 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -173,12 +173,12 @@ export function internal_set(source, value) { if (!source.equals(value)) { possibly_fork(source); - mark_reactions(source, DIRTY); - var old_value = source.v; source.v = value; source.wv = increment_write_version(); + mark_reactions(source, DIRTY); + if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); if (active_effect != null) { @@ -288,7 +288,7 @@ export function mark_reactions(signal, status, parent, only_boundary = false) { continue; } } - } else if ((flags & (DERIVED | ASYNC_DERIVED)) === 0) { + } else if ((flags & (DERIVED | BLOCK_EFFECT)) === 0) { boundary = get_boundary(/** @type {Effect} */ (reaction)); if (boundary) { // @ts-ignore diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6efa1dbe634b..d09059122008 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -69,7 +69,10 @@ let is_micro_task_queued = false; let last_scheduled_effect = null; export let is_flushing_effect = false; -export let is_flushing_async_derived = false; + +/** @type {Effect | null} */ +export let flushing_effect = null; + export let is_destroying_effect = false; /** @param {boolean} value */ @@ -110,14 +113,6 @@ export function set_active_effect(effect) { active_effect = effect; } -/** @type {null | Effect} */ -export let event_boundary_effect = null; - -/** @param {null | Effect} effect */ -export function set_event_boundary_effect(effect) { - event_boundary_effect = effect; -} - // TODO remove this, once we're satisfied that we're not leaking context /* @__PURE__ */ setInterval(() => { @@ -842,12 +837,10 @@ function process_effects(effect, collected_effects) { // to ensure that unowned deriveds are correctly tracked // because we're flushing the current effect var previous_active_reaction = active_reaction; - var previous_is_flushing_async_derived = is_flushing_async_derived; + var previous_flushing_effect = flushing_effect; try { active_reaction = current_effect; - if ((current_effect.f & ASYNC_DERIVED) !== 0) { - is_flushing_async_derived = true; - } + flushing_effect = current_effect; if (check_dirtiness(current_effect)) { update_effect(current_effect); } @@ -855,7 +848,7 @@ function process_effects(effect, collected_effects) { handle_error(error, current_effect, null, current_effect.ctx); } finally { active_reaction = previous_active_reaction; - is_flushing_async_derived = previous_is_flushing_async_derived; + flushing_effect = previous_flushing_effect; } } @@ -1029,14 +1022,12 @@ export function get(signal) { var value = /** @type {V} */ (UNINITIALIZED); - var target_effect = event_boundary_effect ?? active_effect; - if ( - target_effect !== null && - !is_flushing_async_derived && - (target_effect.f & ASYNC_DERIVED) === 0 + active_effect !== null && + (flushing_effect === null || (flushing_effect.f & (BLOCK_EFFECT | BRANCH_EFFECT)) === 0) && + (active_effect.f & ASYNC_DERIVED) === 0 ) { - var boundary = get_boundary(target_effect); + var boundary = get_boundary(active_effect); if (boundary !== null) { // @ts-ignore var forks = boundary.fn.forks; From aa0bf59872379da5f55201a9d1d62125642d5c8f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 10 Feb 2025 19:09:48 +0000 Subject: [PATCH 205/211] is pending --- packages/svelte/src/index-client.js | 1 + .../internal/client/dom/blocks/boundary.js | 25 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index ca29d5bfbe3c..af155f1112a2 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -219,3 +219,4 @@ export { getContext, getAllContexts, hasContext, setContext } from './internal/c export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; +export { isPending } from './internal/client/dom/blocks/boundary.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 2698ec54082a..ee7369c0c8cb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -19,7 +19,8 @@ import { set_active_reaction, reset_is_throwing_error, schedule_effect, - increment_write_version + increment_write_version, + get } from '../../runtime.js'; import { hydrate_next, @@ -36,7 +37,7 @@ import { DEV } from 'esm-env'; import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; import { raf } from '../../timing.js'; import { loop } from '../../loop.js'; -import { internal_set, mark_reactions } from '../../reactivity/sources.js'; +import { internal_set, mark_reactions, source } from '../../reactivity/sources.js'; const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); @@ -84,6 +85,8 @@ export function boundary(node, props, children) { var parent_boundary = find_boundary(active_effect); + var is_pending = source(false); + block(() => { /** @type {Effect | null} */ var main_effect = null; @@ -177,6 +180,7 @@ export function boundary(node, props, children) { } } forks.clear(); + internal_set(is_pending, false); for (const fn of callbacks) fn(); callbacks.clear(); @@ -252,6 +256,8 @@ export function boundary(node, props, children) { boundary.f |= BOUNDARY_SUSPENDED; async_count++; + internal_set(is_pending, true); + return; } @@ -323,7 +329,10 @@ export function boundary(node, props, children) { boundary.fn.forks = new Map(); // @ts-ignore - boundary.fn.is_pending = () => props.pending; + boundary.fn.props = props; + + // @ts-ignore + boundary.fn.is_pending = is_pending; if (hydrating) { hydrate_next(); @@ -408,7 +417,7 @@ export function capture(track = true) { */ export function is_pending_boundary(boundary) { // @ts-ignore - return boundary.fn.is_pending(); + return boundary.fn.props.pending; } /** @@ -488,3 +497,11 @@ export function add_boundary_callback(boundary, fn) { // @ts-ignore boundary.fn(ADD_CALLBACK, fn); } + +export function isPending() { + var effect = active_effect; + var boundary = get_boundary(effect); + // @ts-ignore + var is_pending = boundary.fn.is_pending; + return get(is_pending); +} From 694cbbb1f3036175593a27849fc80f21a5a7693b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 10 Feb 2025 19:11:14 +0000 Subject: [PATCH 206/211] is pending --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 3 +++ packages/svelte/types/index.d.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ee7369c0c8cb..513dd1abf157 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -498,6 +498,9 @@ export function add_boundary_callback(boundary, fn) { boundary.fn(ADD_CALLBACK, fn); } +/** + * @returns {boolean} + */ export function isPending() { var effect = active_effect; var boundary = get_boundary(effect); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c32882c1327d..485a39c6001e 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -419,6 +419,7 @@ declare module 'svelte' { render: () => string; setup?: (element: Element) => void | (() => void); }): Snippet; + export function isPending(): boolean; /** Anything except a function */ type NotFunction = T extends Function ? never : T; /** From 054f8261a9263264e824b857d383eccf7b6404cd Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 10 Feb 2025 20:44:19 +0000 Subject: [PATCH 207/211] fix --- .../src/internal/client/reactivity/deriveds.js | 12 +++++++++++- .../svelte/src/internal/client/reactivity/sources.js | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index dd2dc107fa3d..8039f3af57ba 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -28,7 +28,7 @@ import { block, destroy_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 { capture, suspend } from '../dom/blocks/boundary.js'; +import { capture, get_boundary, suspend } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; @@ -127,6 +127,16 @@ export function async_derived(fn, location) { restore(); from_async_derived = null; + if (signal.v !== UNINITIALIZED) { + var boundary = get_boundary(parent); + // @ts-ignore + var forks = boundary.fn.forks; + var entry = forks.get(signal); + if (entry === undefined) { + entry = { v: signal.v }; + forks.set(signal, entry); + } + } internal_set(signal, v); if (DEV && location !== undefined) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index d3a0b9627ad2..22319354b048 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -171,7 +171,7 @@ export function set(source, value) { */ export function internal_set(source, value) { if (!source.equals(value)) { - possibly_fork(source); + possibly_fork_reactions(source); var old_value = source.v; source.v = value; @@ -342,7 +342,7 @@ function fork_dependencies(signal, forks) { * @param {Value} signal * @returns {void} */ -function possibly_fork(signal) { +function possibly_fork_reactions(signal) { var reactions = signal.reactions; if (reactions === null) return; @@ -362,7 +362,7 @@ function possibly_fork(signal) { // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { - possibly_fork(/** @type {Derived} */ (reaction)); + possibly_fork_reactions(/** @type {Derived} */ (reaction)); } else { if ((reaction.f & ASYNC_DERIVED) !== 0) { // if (active_effect === signal) { From 3da337beeaaf5a64558d5b0497fa6153ef0fac93 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 10 Feb 2025 21:13:04 +0000 Subject: [PATCH 208/211] fix overfire bug --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 3 ++- packages/svelte/src/internal/client/reactivity/sources.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 513dd1abf157..812371a35314 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,7 @@ /** @import { Effect, TemplateNode, } from '#client' */ import { + ASYNC_DERIVED, BOUNDARY_EFFECT, BOUNDARY_SUSPENDED, DERIVED, @@ -173,7 +174,7 @@ export function boundary(node, props, children) { for (var [signal, entry] of forks) { if (signal.v !== entry.v) { - if ((signal.f & DERIVED) === 0) { + if ((signal.f & (DERIVED | ASYNC_DERIVED)) === 0) { mark_reactions(signal, DIRTY, undefined, true); signal.wv = increment_write_version(); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 22319354b048..b2bd8aa9b422 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -284,7 +284,7 @@ export function mark_reactions(signal, status, parent, only_boundary = false) { if (only_boundary) { if ((flags & DERIVED) === 0) { var boundary = get_boundary(/** @type {Effect} */ (reaction)); - if (!boundary) { + if (!boundary || (reaction.f & ASYNC_DERIVED) !== 0) { continue; } } From bf39e3115d5caac00f3301849956e5edd79f6100 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 10 Feb 2025 21:23:52 +0000 Subject: [PATCH 209/211] remove props --- .../2-analyze/visitors/SvelteBoundary.js | 2 +- .../internal/client/dom/blocks/boundary.js | 38 ++----------------- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index 0a49d3b5a488..35af96ba122e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -const valid = ['onerror', 'failed', 'pending', 'showPendingAfter', 'showPendingFor']; +const valid = ['onerror', 'failed', 'pending']; /** * @param {AST.SvelteBoundary} node diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 812371a35314..99c73d35e981 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -75,8 +75,6 @@ var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; * onerror?: (error: unknown, reset: () => void) => void; * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; * pending?: (anchor: Node) => void; - * showPendingAfter?: number; - * showPendingFor?: number; * }} props * @param {((anchor: Node) => void)} children * @returns {void} @@ -109,8 +107,6 @@ export function boundary(node, props, children) { /** @type {Set<() => void>} */ var callbacks = new Set(); - var keep_pending_snippet = false; - /** * @param {() => void} snippet_fn * @returns {Effect | null} @@ -161,7 +157,7 @@ export function boundary(node, props, children) { } function unsuspend() { - if (keep_pending_snippet || async_count > 0) { + if (async_count > 0) { return; } @@ -215,22 +211,7 @@ export function boundary(node, props, children) { pending_effect = branch(() => pending(anchor)); } - // TODO do we want to differentiate between initial render and updates here? - if (!initial) { - keep_pending_snippet = true; - - var end = raf.now() + (props.showPendingFor ?? 300); - - loop((now) => { - if (now >= end) { - keep_pending_snippet = false; - unsuspend(); - return false; - } - - return true; - }); - } + unsuspend(); } else if (parent_boundary) { throw new Error('TODO show pending snippet on parent'); } else { @@ -241,19 +222,6 @@ export function boundary(node, props, children) { // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { if (input === ASYNC_INCREMENT) { - // post-init, show the pending snippet after a timeout - if ((boundary.f & BOUNDARY_SUSPENDED) === 0 && (boundary.f & EFFECT_RAN) !== 0) { - var start = raf.now(); - var end = start + (props.showPendingAfter ?? 500); - - loop((now) => { - if (async_count === 0) return false; - if (now < end) return true; - - show_pending_snippet(false); - }); - } - boundary.f |= BOUNDARY_SUSPENDED; async_count++; @@ -263,7 +231,7 @@ export function boundary(node, props, children) { } if (input === ASYNC_DECREMENT) { - if (--async_count === 0 && !keep_pending_snippet) { + if (--async_count === 0) { unsuspend(); if (main_effect !== null) { From e02606a1e64ed1b05622f8e7f18fbc02fd3ee225 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 11 Feb 2025 15:47:07 +0000 Subject: [PATCH 210/211] fix bug --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- packages/svelte/src/internal/client/reactivity/sources.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 99c73d35e981..a5d1ff4646f9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -171,7 +171,7 @@ export function boundary(node, props, children) { for (var [signal, entry] of forks) { if (signal.v !== entry.v) { if ((signal.f & (DERIVED | ASYNC_DERIVED)) === 0) { - mark_reactions(signal, DIRTY, undefined, true); + mark_reactions(signal, DIRTY, true); signal.wv = increment_write_version(); } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index b2bd8aa9b422..549be19ce2ab 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -261,10 +261,9 @@ export function update_pre(source, d = 1) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY - * @param {Value} [parent] * @returns {void} */ -export function mark_reactions(signal, status, parent, only_boundary = false) { +export function mark_reactions(signal, status, only_boundary = false) { var reactions = signal.reactions; if (reactions === null) return; @@ -293,7 +292,7 @@ export function mark_reactions(signal, status, parent, only_boundary = false) { if (boundary) { // @ts-ignore var forks = boundary.fn.forks; - if (forks.has(signal) || forks.has(parent)) { + if (reaction.deps?.every((d) => forks.has(d))) { continue; } } @@ -310,7 +309,7 @@ export function mark_reactions(signal, status, parent, only_boundary = false) { // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, signal, only_boundary); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, only_boundary); } else { schedule_effect(/** @type {Effect} */ (reaction)); } From 75449ed6e4d7eb1ba3063167b5ad9dbfab9a9011 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 11 Feb 2025 16:41:50 +0000 Subject: [PATCH 211/211] fix --- .../src/internal/client/dom/blocks/boundary.js | 2 +- .../svelte/src/internal/client/reactivity/sources.js | 12 +++++++----- .../samples/async-derived-module/_config.js | 1 + .../runtime-runes/samples/async-derived/_config.js | 1 + 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a5d1ff4646f9..99c73d35e981 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -171,7 +171,7 @@ export function boundary(node, props, children) { for (var [signal, entry] of forks) { if (signal.v !== entry.v) { if ((signal.f & (DERIVED | ASYNC_DERIVED)) === 0) { - mark_reactions(signal, DIRTY, true); + mark_reactions(signal, DIRTY, undefined, true); signal.wv = increment_write_version(); } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 549be19ce2ab..8588d607afa0 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -261,9 +261,10 @@ export function update_pre(source, d = 1) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY + * @param {Value} [parent] * @returns {void} */ -export function mark_reactions(signal, status, only_boundary = false) { +export function mark_reactions(signal, status, parent, only_boundary = false) { var reactions = signal.reactions; if (reactions === null) return; @@ -281,9 +282,10 @@ export function mark_reactions(signal, status, only_boundary = false) { if (!runes && reaction === active_effect) continue; if (only_boundary) { + var effect = /** @type {Effect} */ (reaction); if ((flags & DERIVED) === 0) { - var boundary = get_boundary(/** @type {Effect} */ (reaction)); - if (!boundary || (reaction.f & ASYNC_DERIVED) !== 0) { + var boundary = get_boundary(effect); + if (!boundary || ((reaction.f & ASYNC_DERIVED) !== 0 && !(signal.v instanceof Promise))) { continue; } } @@ -292,7 +294,7 @@ export function mark_reactions(signal, status, only_boundary = false) { if (boundary) { // @ts-ignore var forks = boundary.fn.forks; - if (reaction.deps?.every((d) => forks.has(d))) { + if (reaction.deps?.every((d) => forks.has(d)) || forks.has(parent)) { continue; } } @@ -309,7 +311,7 @@ export function mark_reactions(signal, status, only_boundary = false) { // If the signal a) was previously clean or b) is an unowned derived, then mark it if ((flags & (CLEAN | UNOWNED)) !== 0) { if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, only_boundary); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, signal, only_boundary); } else { schedule_effect(/** @type {Effect} */ (reaction)); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js index 4631243cb2fd..b14516e270a7 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -30,6 +30,7 @@ export default test({ await tick(); assert.htmlEqual(target.innerHTML, '

42

'); + // This runs fine locally, but fails in CI by overfiring some effects? component.num = 2; await Promise.resolve(); await Promise.resolve(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js index dbe76c573b7f..9398d012fe14 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -28,6 +28,7 @@ export default test({ flushSync(); assert.htmlEqual(target.innerHTML, '

42

'); + // This runs fine locally, but fails in CI by overfiring some effects? component.num = 2; await Promise.resolve(); await Promise.resolve();