From 74c483c69de5b59995e0c8454a108f548a188c75 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 14 Jan 2025 23:46:51 +0000 Subject: [PATCH 001/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] =?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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] =?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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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/582] 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 7bd69697110bf2b842678651644537acacbfc68e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 10 Feb 2025 21:57:31 -0500 Subject: [PATCH 191/582] fix --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 7 ++++++- 1 file changed, 6 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 5c768be99bbb..57a34ed3fa08 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -378,7 +378,12 @@ export function boundary(node, props, children) { }); }); } else { - reset(); + main_effect = branch(() => children(anchor)); + + if (async_count > 0) { + boundary.f |= BOUNDARY_SUSPENDED; + show_pending_snippet(true); + } } reset_is_throwing_error(); From 7bf7e0dd787164e761837cda1ee0d7fbbac650b8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 07:23:49 -0500 Subject: [PATCH 192/582] start converting boundary to a class --- .../internal/client/dom/blocks/boundary.js | 620 +++++++++--------- .../src/internal/client/dom/blocks/each.js | 10 +- .../src/internal/client/dom/blocks/if.js | 7 +- .../src/internal/client/dom/blocks/key.js | 7 +- .../client/dom/blocks/svelte-component.js | 7 +- 5 files changed, 336 insertions(+), 315 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 57a34ed3fa08..1bb591d754a3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -37,361 +37,400 @@ import { from_async_derived, set_from_async_derived } from '../../reactivity/der import { raf } from '../../timing.js'; import { loop } from '../../loop.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(); +/** @type {Boundary | null} */ +export let active_boundary = null; -/** - * @param {Effect} boundary - * @param {() => Effect | null} fn - * @returns {Effect | null} - */ -function with_boundary(boundary, fn) { - var previous_effect = active_effect; - var previous_reaction = active_reaction; - var previous_ctx = component_context; - - set_active_effect(boundary); - set_active_reaction(boundary); - set_component_context(boundary.ctx); - - try { - return fn(); - } finally { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_ctx); - } +/** @param {Boundary | null} boundary */ +export function set_active_boundary(boundary) { + active_boundary = boundary; } +class Boundary { + /** @type {Boundary | null} */ + #parent; + + /** @type {Effect} */ + #effect; + + /** @type {Set<() => void>} */ + #callbacks = new Set(); + + /** + * @param {TemplateNode} node + * @param {{ + * 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 + */ + constructor(node, props, children) { + var anchor = node; + + this.#parent = active_boundary; + + active_boundary = this; + + var parent_boundary = find_boundary(active_effect); + + this.#effect = block(() => { + /** @type {Effect | null} */ + var main_effect = null; + + /** @type {Effect | null} */ + var pending_effect = null; + + /** @type {Effect | null} */ + var failed_effect = null; + + /** @type {DocumentFragment | null} */ + var offscreen_fragment = null; + + var async_count = 0; + var boundary_effect = /** @type {Effect} */ (active_effect); + var hydrate_open = hydrate_node; + var is_creating_fallback = false; + + /** @type {Effect[]} */ + var render_effects = []; + + /** @type {Effect[]} */ + var effects = []; + + var keep_pending_snippet = false; + + /** + * @param {() => void} snippet_fn + * @returns {Effect | null} + */ + const render_snippet = (snippet_fn) => { + return this.#run(() => { + is_creating_fallback = true; + + try { + return branch(snippet_fn); + } catch (error) { + handle_error(error, boundary_effect, null, boundary_effect.ctx); + return null; + } finally { + reset_is_throwing_error(); + is_creating_fallback = false; + } + }); + }; -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; - * showPendingAfter?: number; - * showPendingFor?: number; - * }} props - * @param {((anchor: Node) => void)} children - * @returns {void} - */ -export function boundary(node, props, children) { - var anchor = node; - - var parent_boundary = find_boundary(active_effect); - - block(() => { - /** @type {Effect | null} */ - var main_effect = null; - - /** @type {Effect | null} */ - var pending_effect = null; - - /** @type {Effect | null} */ - var failed_effect = null; - - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - var async_count = 0; - var boundary = /** @type {Effect} */ (active_effect); - var hydrate_open = hydrate_node; - var is_creating_fallback = false; + const reset = () => { + async_count = 0; - /** @type {Set<() => void>} */ - var callbacks = new Set(); + if ((boundary_effect.f & BOUNDARY_SUSPENDED) !== 0) { + boundary_effect.f ^= BOUNDARY_SUSPENDED; + } - /** @type {Effect[]} */ - var render_effects = []; + if (failed_effect !== null) { + pause_effect(failed_effect, () => { + failed_effect = null; + }); + } - /** @type {Effect[]} */ - var effects = []; + main_effect = this.#run(() => { + is_creating_fallback = false; - var keep_pending_snippet = false; + try { + return branch(() => children(anchor)); + } finally { + reset_is_throwing_error(); + } + }); - /** - * @param {() => void} snippet_fn - * @returns {Effect | null} - */ - function render_snippet(snippet_fn) { - return with_boundary(boundary, () => { - is_creating_fallback = true; + if (async_count > 0) { + boundary_effect.f |= BOUNDARY_SUSPENDED; + show_pending_snippet(true); + } + }; - try { - return branch(snippet_fn); - } catch (error) { - handle_error(error, boundary, null, boundary.ctx); - return null; - } finally { - reset_is_throwing_error(); - is_creating_fallback = false; + const unsuspend = () => { + if (keep_pending_snippet || async_count > 0) { + return; } - }); - } - function reset() { - async_count = 0; + if ((boundary_effect.f & BOUNDARY_SUSPENDED) !== 0) { + boundary_effect.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); + } + } - if (failed_effect !== null) { - pause_effect(failed_effect, () => { - failed_effect = null; - }); - } + for (const fn of this.#callbacks) fn(); + this.#callbacks.clear(); - main_effect = with_boundary(boundary, () => { - is_creating_fallback = false; + if (pending_effect) { + pause_effect(pending_effect, () => { + pending_effect = null; + }); + } - try { - return branch(() => children(anchor)); - } finally { - reset_is_throwing_error(); + if (offscreen_fragment) { + anchor.before(offscreen_fragment); + offscreen_fragment = null; } - }); - if (async_count > 0) { - boundary.f |= BOUNDARY_SUSPENDED; - show_pending_snippet(true); - } - } + for (const e of effects) { + try { + if (check_dirtiness(e)) { + update_effect(e); + } + } catch (error) { + handle_error(error, e, null, e.ctx); + } + } + }; - function unsuspend() { - if (keep_pending_snippet || async_count > 0) { - return; - } + /** + * @param {boolean} initial + */ + function show_pending_snippet(initial) { + const pending = props.pending; - if ((boundary.f & BOUNDARY_SUSPENDED) !== 0) { - boundary.f ^= BOUNDARY_SUSPENDED; - } + if (pending !== undefined) { + // TODO can this be false? + if (main_effect !== null) { + offscreen_fragment = document.createDocumentFragment(); + move_effect(main_effect, offscreen_fragment); + } - for (const e of render_effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); + if (pending_effect === null) { + pending_effect = branch(() => pending(anchor)); } - } catch (error) { - handle_error(error, e, null, e.ctx); - } - } - for (const fn of callbacks) fn(); - callbacks.clear(); + // TODO do we want to differentiate between initial render and updates here? + if (!initial) { + keep_pending_snippet = true; - if (pending_effect) { - pause_effect(pending_effect, () => { - pending_effect = null; - }); - } + var end = raf.now() + (props.showPendingFor ?? 300); - if (offscreen_fragment) { - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } + loop((now) => { + if (now >= end) { + keep_pending_snippet = false; + unsuspend(); + return false; + } - for (const e of effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); + return true; + }); } - } catch (error) { - handle_error(error, e, null, e.ctx); + } else if (parent_boundary) { + throw new Error('TODO show pending snippet on parent'); + } else { + throw new Error('no pending snippet to show'); } } - } - /** - * @param {boolean} initial - */ - function show_pending_snippet(initial) { - const pending = props.pending; + // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field + boundary_effect.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { + if (input === ASYNC_INCREMENT) { + // post-init, show the pending snippet after a timeout + if ( + (boundary_effect.f & BOUNDARY_SUSPENDED) === 0 && + (boundary_effect.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); + }); + } - if (pending !== undefined) { - // TODO can this be false? - if (main_effect !== null) { - offscreen_fragment = document.createDocumentFragment(); - move_effect(main_effect, offscreen_fragment); - } + boundary_effect.f |= BOUNDARY_SUSPENDED; + async_count++; - if (pending_effect === null) { - pending_effect = branch(() => pending(anchor)); + return; } - // TODO do we want to differentiate between initial render and updates here? - if (!initial) { - keep_pending_snippet = true; + if (input === ASYNC_DECREMENT) { + if (--async_count === 0 && !keep_pending_snippet) { + unsuspend(); - var end = raf.now() + (props.showPendingFor ?? 300); - - loop((now) => { - if (now >= end) { - keep_pending_snippet = false; - unsuspend(); - return false; + if (main_effect !== null) { + // TODO do we also need to `resume_effect` here? + schedule_effect(main_effect); } + } - return true; - }); + return; } - } 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); + if (input === ADD_RENDER_EFFECT) { + render_effects.push(payload); + return; + } - loop((now) => { - if (async_count === 0) return false; - if (now < end) return true; + if (input === ADD_EFFECT) { + effects.push(payload); + return; + } - show_pending_snippet(false); - }); + if (input === COMMIT) { + unsuspend(); + return; } - boundary.f |= BOUNDARY_SUSPENDED; - async_count++; + var error = input; + var onerror = props.onerror; + let failed = props.failed; - return; - } + // 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 (is_creating_fallback || (!onerror && !failed)) { + throw error; + } - if (input === ASYNC_DECREMENT) { - if (--async_count === 0 && !keep_pending_snippet) { - unsuspend(); + onerror?.(error, reset); - if (main_effect !== null) { - // TODO do we also need to `resume_effect` here? - schedule_effect(main_effect); - } + if (main_effect) { + destroy_effect(main_effect); + main_effect = null; } - return; - } - - if (input === ADD_CALLBACK) { - callbacks.add(payload); - return; - } + if (pending_effect) { + destroy_effect(pending_effect); + pending_effect = null; + } - if (input === ADD_RENDER_EFFECT) { - render_effects.push(payload); - return; - } + if (failed_effect) { + destroy_effect(failed_effect); + failed_effect = null; + } - if (input === ADD_EFFECT) { - effects.push(payload); - return; - } + if (hydrating) { + set_hydrate_node(hydrate_open); + next(); + set_hydrate_node(remove_nodes()); + } - if (input === COMMIT) { - unsuspend(); - return; - } + if (failed) { + queue_boundary_micro_task(() => { + failed_effect = render_snippet(() => { + failed( + anchor, + () => error, + () => reset + ); + }); + }); + } + }; - var error = input; - var onerror = props.onerror; - let failed = props.failed; + // @ts-ignore + boundary_effect.fn.is_pending = () => props.pending; - // 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 (is_creating_fallback || (!onerror && !failed)) { - throw error; + if (hydrating) { + hydrate_next(); } - onerror?.(error, reset); + const pending = props.pending; - if (main_effect) { - destroy_effect(main_effect); - main_effect = null; - } + if (hydrating && pending) { + pending_effect = branch(() => pending(anchor)); - if (pending_effect) { - destroy_effect(pending_effect); - pending_effect = null; - } + // ...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. - if (failed_effect) { - destroy_effect(failed_effect); - failed_effect = null; - } + // 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 + queueMicrotask(() => { + destroy_effect(/** @type {Effect} */ (pending_effect)); - if (hydrating) { - set_hydrate_node(hydrate_open); - next(); - set_hydrate_node(remove_nodes()); - } - - if (failed) { - queue_boundary_micro_task(() => { - failed_effect = render_snippet(() => { - failed( - anchor, - () => error, - () => reset - ); + main_effect = this.#run(() => { + return branch(() => children(anchor)); }); }); + } else { + main_effect = branch(() => children(anchor)); + + if (async_count > 0) { + boundary_effect.f |= BOUNDARY_SUSPENDED; + show_pending_snippet(true); + } } - }; - // @ts-ignore - boundary.fn.is_pending = () => props.pending; + reset_is_throwing_error(); + }, flags); if (hydrating) { - hydrate_next(); + anchor = hydrate_node; } - const pending = props.pending; - - if (hydrating && pending) { - pending_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. + active_boundary = this.#parent; + } - // 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 - queueMicrotask(() => { - destroy_effect(/** @type {Effect} */ (pending_effect)); + /** + * @param {() => Effect | null} fn + */ + #run(fn) { + var previous_boundary = active_boundary; + var previous_effect = active_effect; + var previous_reaction = active_reaction; + var previous_ctx = component_context; + + active_boundary = this; + set_active_effect(this.#effect); + set_active_reaction(this.#effect); + set_component_context(this.#effect.ctx); + + try { + return fn(); + } finally { + active_boundary = previous_boundary; + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_ctx); + } + } - main_effect = with_boundary(boundary, () => { - return branch(() => children(anchor)); - }); - }); - } else { - main_effect = branch(() => children(anchor)); + /** @param {() => void} fn */ + add_callback(fn) { + this.#callbacks.add(fn); + } +} - if (async_count > 0) { - boundary.f |= BOUNDARY_SUSPENDED; - show_pending_snippet(true); - } - } +const ASYNC_INCREMENT = Symbol(); +const ASYNC_DECREMENT = Symbol(); +const ADD_RENDER_EFFECT = Symbol(); +const ADD_EFFECT = Symbol(); +const COMMIT = Symbol(); - reset_is_throwing_error(); - }, flags); +var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; - if (hydrating) { - anchor = hydrate_node; - } +/** + * @param {TemplateNode} node + * @param {{ + * 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} + */ +export function boundary(node, props, children) { + new Boundary(node, props, children); } /** @@ -500,19 +539,6 @@ export function find_boundary(effect) { 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); -} - /** * @param {Effect} boundary * @param {Effect} effect diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index c72cc5427042..e8b4feda99f4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -39,7 +39,7 @@ import { queue_micro_task } from '../task.js'; import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; -import { add_boundary_callback, find_boundary } from './boundary.js'; +import { active_boundary } from './boundary.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -139,7 +139,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var was_empty = false; - var boundary = find_boundary(active_effect); + var boundary = active_boundary; /** @type {Map} */ var offscreen_items = new Map(); @@ -268,9 +268,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f fallback = branch(() => fallback_fn(anchor)); } } else { - var defer = boundary !== null && should_defer_append(); - - if (defer) { + if (boundary !== null && should_defer_append()) { for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); @@ -301,7 +299,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } - add_boundary_callback(boundary, commit); + boundary?.add_callback(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index d8dcfcbd580b..2a6a52c446b0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -10,8 +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 } from '../../runtime.js'; -import { add_boundary_callback, find_boundary } from './boundary.js'; +import { active_boundary } from './boundary.js'; import { create_text, should_defer_append } from '../operations.js'; /** @@ -51,7 +50,7 @@ export function if_block(node, fn, elseif = false) { /** @type {Effect | null} */ var pending_effect = null; - var boundary = find_boundary(active_effect); + var boundary = active_boundary; function commit() { if (offscreen_fragment !== null) { @@ -123,7 +122,7 @@ export function if_block(node, fn, elseif = false) { } if (defer) { - add_boundary_callback(boundary, commit); + boundary?.add_callback(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 8e9c4bce43b0..4c6cce7d793d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -2,10 +2,9 @@ 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 } 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 { active_boundary } from './boundary.js'; import { create_text, should_defer_append } from '../operations.js'; /** @@ -34,7 +33,7 @@ export function key_block(node, get_key, render_fn) { /** @type {DocumentFragment | null} */ var offscreen_fragment = null; - var boundary = find_boundary(active_effect); + var boundary = active_boundary; var changed = is_runes() ? not_equal : safe_not_equal; @@ -68,7 +67,7 @@ export function key_block(node, get_key, render_fn) { pending_effect = branch(() => render_fn(target)); if (defer) { - add_boundary_callback(boundary, commit); + boundary?.add_callback(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 b59c24b0295f..330150a80c91 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,10 +1,9 @@ /** @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 { create_text, should_defer_append } from '../operations.js'; -import { add_boundary_callback, find_boundary } from './boundary.js'; +import { active_boundary } from './boundary.js'; /** * @template P @@ -33,7 +32,7 @@ export function component(node, get_component, render_fn) { /** @type {Effect | null} */ var pending_effect = null; - var boundary = find_boundary(active_effect); + var boundary = active_boundary; function commit() { if (effect) { @@ -70,7 +69,7 @@ export function component(node, get_component, render_fn) { } if (defer) { - add_boundary_callback(boundary, commit); + boundary?.add_callback(commit); } else { commit(); } From ba957b625f1849550c3d49615e73082eb2e1c90c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 07:24:36 -0500 Subject: [PATCH 193/582] unused --- .../internal/client/dom/blocks/boundary.js | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 1bb591d754a3..48d58554707a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -72,8 +72,6 @@ class Boundary { active_boundary = this; - var parent_boundary = find_boundary(active_effect); - this.#effect = block(() => { /** @type {Effect | null} */ var main_effect = null; @@ -196,7 +194,7 @@ class Boundary { /** * @param {boolean} initial */ - function show_pending_snippet(initial) { + const show_pending_snippet = (initial) => { const pending = props.pending; if (pending !== undefined) { @@ -226,12 +224,12 @@ class Boundary { return true; }); } - } else if (parent_boundary) { + } else if (this.#parent) { 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_effect.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { @@ -528,17 +526,6 @@ function exit() { 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} boundary * @param {Effect} effect From fe3b177d976759a757f1aaa0e3fff2e1d32f7dd6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 08:21:57 -0500 Subject: [PATCH 194/582] more --- .../internal/client/dom/blocks/boundary.js | 39 +++++++++---------- .../svelte/src/internal/client/runtime.js | 9 +++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 48d58554707a..a194b093e729 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -44,7 +44,7 @@ export let active_boundary = null; export function set_active_boundary(boundary) { active_boundary = boundary; } -class Boundary { +export class Boundary { /** @type {Boundary | null} */ #parent; @@ -54,6 +54,12 @@ class Boundary { /** @type {Set<() => void>} */ #callbacks = new Set(); + /** @type {Effect[]} */ + #render_effects = []; + + /** @type {Effect[]} */ + #effects = []; + /** * @param {TemplateNode} node * @param {{ @@ -90,12 +96,6 @@ class Boundary { var hydrate_open = hydrate_node; var is_creating_fallback = false; - /** @type {Effect[]} */ - var render_effects = []; - - /** @type {Effect[]} */ - var effects = []; - var keep_pending_snippet = false; /** @@ -156,7 +156,7 @@ class Boundary { boundary_effect.f ^= BOUNDARY_SUSPENDED; } - for (const e of render_effects) { + for (const e of this.#render_effects) { try { if (check_dirtiness(e)) { update_effect(e); @@ -180,7 +180,7 @@ class Boundary { offscreen_fragment = null; } - for (const e of effects) { + for (const e of this.#effects) { try { if (check_dirtiness(e)) { update_effect(e); @@ -270,12 +270,12 @@ class Boundary { } if (input === ADD_RENDER_EFFECT) { - render_effects.push(payload); + this.#render_effects.push(payload); return; } if (input === ADD_EFFECT) { - effects.push(payload); + this.#effects.push(payload); return; } @@ -370,6 +370,9 @@ class Boundary { reset_is_throwing_error(); }, flags); + // @ts-expect-error + this.#effect.fn.boundary = this; + if (hydrating) { anchor = hydrate_node; } @@ -405,6 +408,11 @@ class Boundary { add_callback(fn) { this.#callbacks.add(fn); } + + /** @param {Effect} effect */ + add_effect(effect) { + ((effect.f & RENDER_EFFECT) !== 0 ? this.#render_effects : this.#effects).push(effect); + } } const ASYNC_INCREMENT = Symbol(); @@ -526,15 +534,6 @@ function exit() { set_component_context(null); } -/** - * @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 */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8016eeb9b262..706b8da25bdb 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -50,7 +50,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 { Boundary, commit_boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; const FLUSH_MICROTASK = 0; @@ -812,7 +812,7 @@ export function schedule_effect(signal) { * * @param {Effect} effect * @param {Effect[]} collected_effects - * @param {Effect} [boundary] + * @param {Boundary} [boundary] * @returns {void} */ function process_effects(effect, collected_effects, boundary) { @@ -828,9 +828,10 @@ function process_effects(effect, collected_effects, boundary) { 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); + boundary.add_effect(current_effect); } else if ((flags & BOUNDARY_EFFECT) !== 0) { - process_effects(current_effect, collected_effects, current_effect); + // @ts-expect-error + process_effects(current_effect, collected_effects, current_effect.fn.boundary); if ((current_effect.f & BOUNDARY_SUSPENDED) === 0) { // no more async work to happen From 7b2c677474ac5332203c31791dc1ae873939e4a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 08:22:38 -0500 Subject: [PATCH 195/582] unused --- .../src/internal/client/dom/blocks/boundary.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a194b093e729..bf9f0838bfe2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -269,16 +269,6 @@ export class Boundary { return; } - if (input === ADD_RENDER_EFFECT) { - this.#render_effects.push(payload); - return; - } - - if (input === ADD_EFFECT) { - this.#effects.push(payload); - return; - } - if (input === COMMIT) { unsuspend(); return; @@ -417,8 +407,6 @@ export class Boundary { const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); -const ADD_RENDER_EFFECT = Symbol(); -const ADD_EFFECT = Symbol(); const COMMIT = Symbol(); var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; From 66f0f1b803eaa192cf4aa2a03505b68c89c93833 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:13:32 -0500 Subject: [PATCH 196/582] more --- .../internal/client/dom/blocks/boundary.js | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index bf9f0838bfe2..4e4695bbdfe5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -44,7 +44,24 @@ export let active_boundary = null; export function set_active_boundary(boundary) { active_boundary = boundary; } + +/** + * @typedef {{ + * onerror?: (error: unknown, reset: () => void) => void; + * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; + * pending?: (anchor: Node) => void; + * showPendingAfter?: number; + * showPendingFor?: number; + * }} BoundaryProps + */ + export class Boundary { + /** @type {TemplateNode} */ + #anchor; + + /** @type {BoundaryProps} */ + #props; + /** @type {Boundary | null} */ #parent; @@ -62,18 +79,12 @@ export class Boundary { /** * @param {TemplateNode} node - * @param {{ - * onerror?: (error: unknown, reset: () => void) => void; - * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; - * pending?: (anchor: Node) => void; - * showPendingAfter?: number; - * showPendingFor?: number; - * }} props + * @param {BoundaryProps} props * @param {((anchor: Node) => void)} children */ constructor(node, props, children) { - var anchor = node; - + this.#anchor = node; + this.#props = props; this.#parent = active_boundary; active_boundary = this; @@ -135,7 +146,7 @@ export class Boundary { is_creating_fallback = false; try { - return branch(() => children(anchor)); + return branch(() => children(this.#anchor)); } finally { reset_is_throwing_error(); } @@ -176,7 +187,7 @@ export class Boundary { } if (offscreen_fragment) { - anchor.before(offscreen_fragment); + this.#anchor.before(offscreen_fragment); offscreen_fragment = null; } @@ -195,7 +206,7 @@ export class Boundary { * @param {boolean} initial */ const show_pending_snippet = (initial) => { - const pending = props.pending; + const pending = this.#props.pending; if (pending !== undefined) { // TODO can this be false? @@ -205,14 +216,14 @@ export class Boundary { } if (pending_effect === null) { - pending_effect = branch(() => pending(anchor)); + pending_effect = branch(() => pending(this.#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); + var end = raf.now() + (this.#props.showPendingFor ?? 300); loop((now) => { if (now >= end) { @@ -240,7 +251,7 @@ export class Boundary { (boundary_effect.f & EFFECT_RAN) !== 0 ) { var start = raf.now(); - var end = start + (props.showPendingAfter ?? 500); + var end = start + (this.#props.showPendingAfter ?? 500); loop((now) => { if (async_count === 0) return false; @@ -275,8 +286,8 @@ export class Boundary { } var error = input; - var onerror = props.onerror; - let failed = props.failed; + var onerror = this.#props.onerror; + let failed = this.#props.failed; // 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 @@ -311,7 +322,7 @@ export class Boundary { queue_boundary_micro_task(() => { failed_effect = render_snippet(() => { failed( - anchor, + this.#anchor, () => error, () => reset ); @@ -321,16 +332,16 @@ export class Boundary { }; // @ts-ignore - boundary_effect.fn.is_pending = () => props.pending; + boundary_effect.fn.is_pending = () => this.#props.pending; if (hydrating) { hydrate_next(); } - const pending = props.pending; + const pending = this.#props.pending; if (hydrating && pending) { - pending_effect = branch(() => pending(anchor)); + pending_effect = branch(() => pending(this.#anchor)); // ...now what? we need to start rendering `boundary_fn` offscreen, // and either insert the resulting fragment (if nothing suspends) @@ -345,11 +356,11 @@ export class Boundary { destroy_effect(/** @type {Effect} */ (pending_effect)); main_effect = this.#run(() => { - return branch(() => children(anchor)); + return branch(() => children(this.#anchor)); }); }); } else { - main_effect = branch(() => children(anchor)); + main_effect = branch(() => children(this.#anchor)); if (async_count > 0) { boundary_effect.f |= BOUNDARY_SUSPENDED; @@ -364,7 +375,7 @@ export class Boundary { this.#effect.fn.boundary = this; if (hydrating) { - anchor = hydrate_node; + this.#anchor = hydrate_node; } active_boundary = this.#parent; From 31a9844ba9e7a435f8732b29df347fefa377d9ae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:15:09 -0500 Subject: [PATCH 197/582] more --- .../internal/client/dom/blocks/boundary.js | 72 +++++++++---------- 1 file changed, 36 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 4e4695bbdfe5..2337c8ced1e5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -77,6 +77,15 @@ export class Boundary { /** @type {Effect[]} */ #effects = []; + /** @type {Effect | null} */ + #main_effect = null; + + /** @type {Effect | null} */ + #pending_effect = null; + + /** @type {Effect | null} */ + #failed_effect = null; + /** * @param {TemplateNode} node * @param {BoundaryProps} props @@ -90,15 +99,6 @@ export class Boundary { active_boundary = this; this.#effect = block(() => { - /** @type {Effect | null} */ - var main_effect = null; - - /** @type {Effect | null} */ - var pending_effect = null; - - /** @type {Effect | null} */ - var failed_effect = null; - /** @type {DocumentFragment | null} */ var offscreen_fragment = null; @@ -136,13 +136,13 @@ export class Boundary { boundary_effect.f ^= BOUNDARY_SUSPENDED; } - if (failed_effect !== null) { - pause_effect(failed_effect, () => { - failed_effect = null; + if (this.#failed_effect !== null) { + pause_effect(this.#failed_effect, () => { + this.#failed_effect = null; }); } - main_effect = this.#run(() => { + this.#main_effect = this.#run(() => { is_creating_fallback = false; try { @@ -180,9 +180,9 @@ export class Boundary { for (const fn of this.#callbacks) fn(); this.#callbacks.clear(); - if (pending_effect) { - pause_effect(pending_effect, () => { - pending_effect = null; + if (this.#pending_effect) { + pause_effect(this.#pending_effect, () => { + this.#pending_effect = null; }); } @@ -210,13 +210,13 @@ export class Boundary { if (pending !== undefined) { // TODO can this be false? - if (main_effect !== null) { + if (this.#main_effect !== null) { offscreen_fragment = document.createDocumentFragment(); - move_effect(main_effect, offscreen_fragment); + move_effect(this.#main_effect, offscreen_fragment); } - if (pending_effect === null) { - pending_effect = branch(() => pending(this.#anchor)); + if (this.#pending_effect === null) { + this.#pending_effect = branch(() => pending(this.#anchor)); } // TODO do we want to differentiate between initial render and updates here? @@ -271,9 +271,9 @@ export class Boundary { if (--async_count === 0 && !keep_pending_snippet) { unsuspend(); - if (main_effect !== null) { + if (this.#main_effect !== null) { // TODO do we also need to `resume_effect` here? - schedule_effect(main_effect); + schedule_effect(this.#main_effect); } } @@ -297,19 +297,19 @@ export class Boundary { onerror?.(error, reset); - if (main_effect) { - destroy_effect(main_effect); - main_effect = null; + if (this.#main_effect) { + destroy_effect(this.#main_effect); + this.#main_effect = null; } - if (pending_effect) { - destroy_effect(pending_effect); - pending_effect = null; + if (this.#pending_effect) { + destroy_effect(this.#pending_effect); + this.#pending_effect = null; } - if (failed_effect) { - destroy_effect(failed_effect); - failed_effect = null; + if (this.#failed_effect) { + destroy_effect(this.#failed_effect); + this.#failed_effect = null; } if (hydrating) { @@ -320,7 +320,7 @@ export class Boundary { if (failed) { queue_boundary_micro_task(() => { - failed_effect = render_snippet(() => { + this.#failed_effect = render_snippet(() => { failed( this.#anchor, () => error, @@ -341,7 +341,7 @@ export class Boundary { const pending = this.#props.pending; if (hydrating && pending) { - pending_effect = branch(() => pending(this.#anchor)); + this.#pending_effect = branch(() => pending(this.#anchor)); // ...now what? we need to start rendering `boundary_fn` offscreen, // and either insert the resulting fragment (if nothing suspends) @@ -353,14 +353,14 @@ export class Boundary { // the pending or main block was rendered for a given // boundary, and hydrate accordingly queueMicrotask(() => { - destroy_effect(/** @type {Effect} */ (pending_effect)); + destroy_effect(/** @type {Effect} */ (this.#pending_effect)); - main_effect = this.#run(() => { + this.#main_effect = this.#run(() => { return branch(() => children(this.#anchor)); }); }); } else { - main_effect = branch(() => children(this.#anchor)); + this.#main_effect = branch(() => children(this.#anchor)); if (async_count > 0) { boundary_effect.f |= BOUNDARY_SUSPENDED; From 2e65e6eb5429094842cf420d471198ad45c5f564 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:16:53 -0500 Subject: [PATCH 198/582] more --- .../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 2337c8ced1e5..3d923c992bc7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -86,6 +86,9 @@ export class Boundary { /** @type {Effect | null} */ #failed_effect = null; + #keep_pending_snippet = false; // TODO get rid of this + #is_creating_fallback = false; + /** * @param {TemplateNode} node * @param {BoundaryProps} props @@ -105,9 +108,6 @@ export class Boundary { var async_count = 0; var boundary_effect = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; - var is_creating_fallback = false; - - var keep_pending_snippet = false; /** * @param {() => void} snippet_fn @@ -115,7 +115,7 @@ export class Boundary { */ const render_snippet = (snippet_fn) => { return this.#run(() => { - is_creating_fallback = true; + this.#is_creating_fallback = true; try { return branch(snippet_fn); @@ -124,7 +124,7 @@ export class Boundary { return null; } finally { reset_is_throwing_error(); - is_creating_fallback = false; + this.#is_creating_fallback = false; } }); }; @@ -143,7 +143,7 @@ export class Boundary { } this.#main_effect = this.#run(() => { - is_creating_fallback = false; + this.#is_creating_fallback = false; try { return branch(() => children(this.#anchor)); @@ -159,7 +159,7 @@ export class Boundary { }; const unsuspend = () => { - if (keep_pending_snippet || async_count > 0) { + if (this.#keep_pending_snippet || async_count > 0) { return; } @@ -221,13 +221,13 @@ export class Boundary { // TODO do we want to differentiate between initial render and updates here? if (!initial) { - keep_pending_snippet = true; + this.#keep_pending_snippet = true; var end = raf.now() + (this.#props.showPendingFor ?? 300); loop((now) => { if (now >= end) { - keep_pending_snippet = false; + this.#keep_pending_snippet = false; unsuspend(); return false; } @@ -268,7 +268,7 @@ export class Boundary { } if (input === ASYNC_DECREMENT) { - if (--async_count === 0 && !keep_pending_snippet) { + if (--async_count === 0 && !this.#keep_pending_snippet) { unsuspend(); if (this.#main_effect !== null) { @@ -291,7 +291,7 @@ export class Boundary { // 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 (is_creating_fallback || (!onerror && !failed)) { + if (this.#is_creating_fallback || (!onerror && !failed)) { throw error; } From e9962194f874c7eab237e8fcd37fdcdbcf1ee404 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:21:20 -0500 Subject: [PATCH 199/582] more --- .../internal/client/dom/blocks/boundary.js | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 3d923c992bc7..14f81ec6f68c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -86,6 +86,10 @@ export class Boundary { /** @type {Effect | null} */ #failed_effect = null; + /** @type {DocumentFragment | null} */ + #offscreen_fragment = null; + + #pending_count = 0; #keep_pending_snippet = false; // TODO get rid of this #is_creating_fallback = false; @@ -102,10 +106,6 @@ export class Boundary { active_boundary = this; this.#effect = block(() => { - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - var async_count = 0; var boundary_effect = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; @@ -130,7 +130,7 @@ export class Boundary { }; const reset = () => { - async_count = 0; + this.#pending_count = 0; if ((boundary_effect.f & BOUNDARY_SUSPENDED) !== 0) { boundary_effect.f ^= BOUNDARY_SUSPENDED; @@ -152,56 +152,12 @@ export class Boundary { } }); - if (async_count > 0) { + if (this.#pending_count > 0) { boundary_effect.f |= BOUNDARY_SUSPENDED; show_pending_snippet(true); } }; - const unsuspend = () => { - if (this.#keep_pending_snippet || async_count > 0) { - return; - } - - if ((boundary_effect.f & BOUNDARY_SUSPENDED) !== 0) { - boundary_effect.f ^= BOUNDARY_SUSPENDED; - } - - for (const e of this.#render_effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); - } - } catch (error) { - handle_error(error, e, null, e.ctx); - } - } - - for (const fn of this.#callbacks) fn(); - this.#callbacks.clear(); - - if (this.#pending_effect) { - pause_effect(this.#pending_effect, () => { - this.#pending_effect = null; - }); - } - - if (offscreen_fragment) { - this.#anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - for (const e of this.#effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); - } - } catch (error) { - handle_error(error, e, null, e.ctx); - } - } - }; - /** * @param {boolean} initial */ @@ -211,8 +167,8 @@ export class Boundary { if (pending !== undefined) { // TODO can this be false? if (this.#main_effect !== null) { - offscreen_fragment = document.createDocumentFragment(); - move_effect(this.#main_effect, offscreen_fragment); + this.#offscreen_fragment = document.createDocumentFragment(); + move_effect(this.#main_effect, this.#offscreen_fragment); } if (this.#pending_effect === null) { @@ -228,7 +184,7 @@ export class Boundary { loop((now) => { if (now >= end) { this.#keep_pending_snippet = false; - unsuspend(); + this.commit(); return false; } @@ -254,7 +210,7 @@ export class Boundary { var end = start + (this.#props.showPendingAfter ?? 500); loop((now) => { - if (async_count === 0) return false; + if (this.#pending_count === 0) return false; if (now < end) return true; show_pending_snippet(false); @@ -262,14 +218,14 @@ export class Boundary { } boundary_effect.f |= BOUNDARY_SUSPENDED; - async_count++; + this.#pending_count++; return; } if (input === ASYNC_DECREMENT) { - if (--async_count === 0 && !this.#keep_pending_snippet) { - unsuspend(); + if (--this.#pending_count === 0 && !this.#keep_pending_snippet) { + this.commit(); if (this.#main_effect !== null) { // TODO do we also need to `resume_effect` here? @@ -281,7 +237,7 @@ export class Boundary { } if (input === COMMIT) { - unsuspend(); + this.commit(); return; } @@ -362,7 +318,7 @@ export class Boundary { } else { this.#main_effect = branch(() => children(this.#anchor)); - if (async_count > 0) { + if (this.#pending_count > 0) { boundary_effect.f |= BOUNDARY_SUSPENDED; show_pending_snippet(true); } @@ -414,6 +370,50 @@ export class Boundary { add_effect(effect) { ((effect.f & RENDER_EFFECT) !== 0 ? this.#render_effects : this.#effects).push(effect); } + + commit() { + if (this.#keep_pending_snippet || this.#pending_count > 0) { + return; + } + + if ((this.#effect.f & BOUNDARY_SUSPENDED) !== 0) { + this.#effect.f ^= BOUNDARY_SUSPENDED; + } + + for (const e of this.#render_effects) { + try { + if (check_dirtiness(e)) { + update_effect(e); + } + } catch (error) { + handle_error(error, e, null, e.ctx); + } + } + + for (const fn of this.#callbacks) fn(); + this.#callbacks.clear(); + + if (this.#pending_effect) { + pause_effect(this.#pending_effect, () => { + this.#pending_effect = null; + }); + } + + if (this.#offscreen_fragment) { + this.#anchor.before(this.#offscreen_fragment); + this.#offscreen_fragment = null; + } + + for (const e of this.#effects) { + try { + if (check_dirtiness(e)) { + update_effect(e); + } + } catch (error) { + handle_error(error, e, null, e.ctx); + } + } + } } const ASYNC_INCREMENT = Symbol(); From eb465b56ed0a53c94921d8b74cf8cc3983cc7cf3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:23:08 -0500 Subject: [PATCH 200/582] more --- .../svelte/src/internal/client/dom/blocks/boundary.js | 8 -------- packages/svelte/src/internal/client/runtime.js | 8 +++++--- 2 files changed, 5 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 14f81ec6f68c..364a39826d34 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -532,11 +532,3 @@ function exit() { set_active_reaction(null); set_component_context(null); } - -/** - * @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 706b8da25bdb..b281eb104c82 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -50,7 +50,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; -import { Boundary, commit_boundary } from './dom/blocks/boundary.js'; +import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; const FLUSH_MICROTASK = 0; @@ -831,11 +831,13 @@ function process_effects(effect, collected_effects, boundary) { boundary.add_effect(current_effect); } else if ((flags & BOUNDARY_EFFECT) !== 0) { // @ts-expect-error - process_effects(current_effect, collected_effects, current_effect.fn.boundary); + var b = /** @type {Boundary} */ (current_effect.fn.boundary); + + process_effects(current_effect, collected_effects, b); if ((current_effect.f & BOUNDARY_SUSPENDED) === 0) { // no more async work to happen - commit_boundary(current_effect); + b.commit(); } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { From 85fa8727962ef2ea4dece0913eb716371eeb046b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:24:10 -0500 Subject: [PATCH 201/582] unused --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 6 ------ 1 file changed, 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 364a39826d34..43ad99d99cac 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -236,11 +236,6 @@ export class Boundary { return; } - if (input === COMMIT) { - this.commit(); - return; - } - var error = input; var onerror = this.#props.onerror; let failed = this.#props.failed; @@ -418,7 +413,6 @@ export class Boundary { const ASYNC_INCREMENT = Symbol(); const ASYNC_DECREMENT = Symbol(); -const COMMIT = Symbol(); var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; From 72ab4fc21a76392297a157527488d0e791b0b2f5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:25:20 -0500 Subject: [PATCH 202/582] more --- .../internal/client/dom/blocks/boundary.js | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 43ad99d99cac..5054bbcd6e71 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -154,47 +154,7 @@ export class Boundary { if (this.#pending_count > 0) { boundary_effect.f |= BOUNDARY_SUSPENDED; - show_pending_snippet(true); - } - }; - - /** - * @param {boolean} initial - */ - const show_pending_snippet = (initial) => { - const pending = this.#props.pending; - - if (pending !== undefined) { - // TODO can this be false? - if (this.#main_effect !== null) { - this.#offscreen_fragment = document.createDocumentFragment(); - move_effect(this.#main_effect, this.#offscreen_fragment); - } - - if (this.#pending_effect === null) { - this.#pending_effect = branch(() => pending(this.#anchor)); - } - - // TODO do we want to differentiate between initial render and updates here? - if (!initial) { - this.#keep_pending_snippet = true; - - var end = raf.now() + (this.#props.showPendingFor ?? 300); - - loop((now) => { - if (now >= end) { - this.#keep_pending_snippet = false; - this.commit(); - return false; - } - - return true; - }); - } - } else if (this.#parent) { - throw new Error('TODO show pending snippet on parent'); - } else { - throw new Error('no pending snippet to show'); + this.#show_pending_snippet(true); } }; @@ -213,7 +173,7 @@ export class Boundary { if (this.#pending_count === 0) return false; if (now < end) return true; - show_pending_snippet(false); + this.#show_pending_snippet(false); }); } @@ -315,7 +275,7 @@ export class Boundary { if (this.#pending_count > 0) { boundary_effect.f |= BOUNDARY_SUSPENDED; - show_pending_snippet(true); + this.#show_pending_snippet(true); } } @@ -356,6 +316,46 @@ export class Boundary { } } + /** + * @param {boolean} initial + */ + #show_pending_snippet(initial) { + const pending = this.#props.pending; + + if (pending !== undefined) { + // TODO can this be false? + if (this.#main_effect !== null) { + this.#offscreen_fragment = document.createDocumentFragment(); + move_effect(this.#main_effect, this.#offscreen_fragment); + } + + if (this.#pending_effect === null) { + this.#pending_effect = branch(() => pending(this.#anchor)); + } + + // TODO do we want to differentiate between initial render and updates here? + if (!initial) { + this.#keep_pending_snippet = true; + + var end = raf.now() + (this.#props.showPendingFor ?? 300); + + loop((now) => { + if (now >= end) { + this.#keep_pending_snippet = false; + this.commit(); + return false; + } + + return true; + }); + } + } else if (this.#parent) { + throw new Error('TODO show pending snippet on parent'); + } else { + throw new Error('no pending snippet to show'); + } + } + /** @param {() => void} fn */ add_callback(fn) { this.#callbacks.add(fn); From 7e26a83775b5c130149e239a246bf628a9df17ec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:27:09 -0500 Subject: [PATCH 203/582] simplify --- .../internal/client/dom/blocks/boundary.js | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 5054bbcd6e71..f8aff8c7ba62 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -109,26 +109,6 @@ export class Boundary { var boundary_effect = /** @type {Effect} */ (active_effect); var hydrate_open = hydrate_node; - /** - * @param {() => void} snippet_fn - * @returns {Effect | null} - */ - const render_snippet = (snippet_fn) => { - return this.#run(() => { - this.#is_creating_fallback = true; - - try { - return branch(snippet_fn); - } catch (error) { - handle_error(error, boundary_effect, null, boundary_effect.ctx); - return null; - } finally { - reset_is_throwing_error(); - this.#is_creating_fallback = false; - } - }); - }; - const reset = () => { this.#pending_count = 0; @@ -231,12 +211,24 @@ export class Boundary { if (failed) { queue_boundary_micro_task(() => { - this.#failed_effect = render_snippet(() => { - failed( - this.#anchor, - () => error, - () => reset - ); + this.#failed_effect = this.#run(() => { + this.#is_creating_fallback = true; + + try { + return branch(() => { + failed( + this.#anchor, + () => error, + () => reset + ); + }); + } catch (error) { + handle_error(error, boundary_effect, null, boundary_effect.ctx); + return null; + } finally { + reset_is_throwing_error(); + this.#is_creating_fallback = false; + } }); }); } From 4a9ff233cd515bc243cb806dd8c4562389f2c970 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:29:26 -0500 Subject: [PATCH 204/582] more --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 6 +++--- 1 file changed, 3 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 f8aff8c7ba62..c5f6a358a044 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -112,8 +112,8 @@ export class Boundary { const reset = () => { this.#pending_count = 0; - if ((boundary_effect.f & BOUNDARY_SUSPENDED) !== 0) { - boundary_effect.f ^= BOUNDARY_SUSPENDED; + if ((this.#effect.f & BOUNDARY_SUSPENDED) !== 0) { + this.#effect.f ^= BOUNDARY_SUSPENDED; } if (this.#failed_effect !== null) { @@ -133,7 +133,7 @@ export class Boundary { }); if (this.#pending_count > 0) { - boundary_effect.f |= BOUNDARY_SUSPENDED; + this.#effect.f |= BOUNDARY_SUSPENDED; this.#show_pending_snippet(true); } }; From 6b058526f39495a5cbff5c01e0005dbfd35ffe44 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 09:58:40 -0500 Subject: [PATCH 205/582] more --- .../svelte/src/internal/client/constants.js | 1 - .../internal/client/dom/blocks/boundary.js | 23 +++++++------------ .../svelte/src/internal/client/runtime.js | 5 ++-- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index cc04b66a4b44..530f72b61cde 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -23,7 +23,6 @@ export const EFFECT_PRESERVED = 1 << 21; // effects with this flag should not be // Flags used for async 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/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index c5f6a358a044..e59479399613 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -2,7 +2,6 @@ import { BOUNDARY_EFFECT, - BOUNDARY_SUSPENDED, EFFECT_PRESERVED, EFFECT_RAN, EFFECT_TRANSPARENT, @@ -56,6 +55,8 @@ export function set_active_boundary(boundary) { */ export class Boundary { + suspended = false; + /** @type {TemplateNode} */ #anchor; @@ -111,10 +112,7 @@ export class Boundary { const reset = () => { this.#pending_count = 0; - - if ((this.#effect.f & BOUNDARY_SUSPENDED) !== 0) { - this.#effect.f ^= BOUNDARY_SUSPENDED; - } + this.suspended = false; if (this.#failed_effect !== null) { pause_effect(this.#failed_effect, () => { @@ -133,7 +131,7 @@ export class Boundary { }); if (this.#pending_count > 0) { - this.#effect.f |= BOUNDARY_SUSPENDED; + this.suspended = true; this.#show_pending_snippet(true); } }; @@ -142,10 +140,7 @@ export class Boundary { boundary_effect.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { if (input === ASYNC_INCREMENT) { // post-init, show the pending snippet after a timeout - if ( - (boundary_effect.f & BOUNDARY_SUSPENDED) === 0 && - (boundary_effect.f & EFFECT_RAN) !== 0 - ) { + if (!this.suspended && (boundary_effect.f & EFFECT_RAN) !== 0) { var start = raf.now(); var end = start + (this.#props.showPendingAfter ?? 500); @@ -157,7 +152,7 @@ export class Boundary { }); } - boundary_effect.f |= BOUNDARY_SUSPENDED; + this.suspended = true; this.#pending_count++; return; @@ -266,7 +261,7 @@ export class Boundary { this.#main_effect = branch(() => children(this.#anchor)); if (this.#pending_count > 0) { - boundary_effect.f |= BOUNDARY_SUSPENDED; + this.suspended = true; this.#show_pending_snippet(true); } } @@ -363,9 +358,7 @@ export class Boundary { return; } - if ((this.#effect.f & BOUNDARY_SUSPENDED) !== 0) { - this.#effect.f ^= BOUNDARY_SUSPENDED; - } + this.suspended = false; for (const e of this.#render_effects) { try { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b281eb104c82..4027a094ad01 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -24,8 +24,7 @@ import { LEGACY_DERIVED_PROP, DISCONNECTED, BOUNDARY_EFFECT, - REACTION_IS_UPDATING, - BOUNDARY_SUSPENDED + REACTION_IS_UPDATING } from './constants.js'; import { flush_idle_tasks, @@ -835,7 +834,7 @@ function process_effects(effect, collected_effects, boundary) { process_effects(current_effect, collected_effects, b); - if ((current_effect.f & BOUNDARY_SUSPENDED) === 0) { + if (!b.suspended) { // no more async work to happen b.commit(); } From 8c727cced5505d67051a5301aba989be74fc865d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 10:20:20 -0500 Subject: [PATCH 206/582] more --- .../internal/client/dom/blocks/boundary.js | 104 ++++++++---------- .../src/internal/client/reactivity/effects.js | 3 +- .../src/internal/client/reactivity/types.d.ts | 3 + 3 files changed, 53 insertions(+), 57 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index e59479399613..19550d9df93a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -57,15 +57,15 @@ export function set_active_boundary(boundary) { export class Boundary { suspended = false; + /** @type {Boundary | null} */ + parent; + /** @type {TemplateNode} */ #anchor; /** @type {BoundaryProps} */ #props; - /** @type {Boundary | null} */ - #parent; - /** @type {Effect} */ #effect; @@ -102,12 +102,14 @@ export class Boundary { constructor(node, props, children) { this.#anchor = node; this.#props = props; - this.#parent = active_boundary; + this.parent = active_boundary; active_boundary = this; this.#effect = block(() => { var boundary_effect = /** @type {Effect} */ (active_effect); + boundary_effect.b = this; + var hydrate_open = hydrate_node; const reset = () => { @@ -138,39 +140,6 @@ export class Boundary { // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary_effect.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { - if (input === ASYNC_INCREMENT) { - // post-init, show the pending snippet after a timeout - if (!this.suspended && (boundary_effect.f & EFFECT_RAN) !== 0) { - var start = raf.now(); - var end = start + (this.#props.showPendingAfter ?? 500); - - loop((now) => { - if (this.#pending_count === 0) return false; - if (now < end) return true; - - this.#show_pending_snippet(false); - }); - } - - this.suspended = true; - this.#pending_count++; - - return; - } - - if (input === ASYNC_DECREMENT) { - if (--this.#pending_count === 0 && !this.#keep_pending_snippet) { - this.commit(); - - if (this.#main_effect !== null) { - // TODO do we also need to `resume_effect` here? - schedule_effect(this.#main_effect); - } - } - - return; - } - var error = input; var onerror = this.#props.onerror; let failed = this.#props.failed; @@ -269,6 +238,8 @@ export class Boundary { reset_is_throwing_error(); }, flags); + this.ran = true; + // @ts-expect-error this.#effect.fn.boundary = this; @@ -276,7 +247,11 @@ export class Boundary { this.#anchor = hydrate_node; } - active_boundary = this.#parent; + active_boundary = this.parent; + } + + has_pending_snippet() { + return !!this.#props.pending; } /** @@ -336,7 +311,7 @@ export class Boundary { return true; }); } - } else if (this.#parent) { + } else if (this.parent) { throw new Error('TODO show pending snippet on parent'); } else { throw new Error('no pending snippet to show'); @@ -394,10 +369,36 @@ export class Boundary { } } } -} -const ASYNC_INCREMENT = Symbol(); -const ASYNC_DECREMENT = Symbol(); + increment() { + // post-init, show the pending snippet after a timeout + if (!this.suspended && this.ran) { + var start = raf.now(); + var end = start + (this.#props.showPendingAfter ?? 500); + + loop((now) => { + if (this.#pending_count === 0) return false; + if (now < end) return true; + + this.#show_pending_snippet(false); + }); + } + + this.suspended = true; + this.#pending_count++; + } + + decrement() { + if (--this.#pending_count === 0 && !this.#keep_pending_snippet) { + this.commit(); + + if (this.#main_effect !== null) { + // TODO do we also need to `resume_effect` here? + schedule_effect(this.#main_effect); + } + } + } +} var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; @@ -458,19 +459,12 @@ export function capture(track = true) { }; } -/** - * @param {Effect} boundary - */ -export function is_pending_boundary(boundary) { - // @ts-ignore - return boundary.fn.is_pending(); -} - export function suspend() { - var boundary = active_effect; + let boundary = /** @type {Effect} */ (active_effect).b; while (boundary !== null) { - if ((boundary.f & BOUNDARY_EFFECT) !== 0 && is_pending_boundary(boundary)) { + // TODO pretty sure this is wrong + if (boundary.has_pending_snippet()) { break; } @@ -481,12 +475,10 @@ export function suspend() { e.await_outside_boundary(); } - // @ts-ignore - boundary?.fn(ASYNC_INCREMENT); + boundary.increment(); return function unsuspend() { - // @ts-ignore - boundary?.fn?.(ASYNC_DECREMENT); + boundary.decrement(); }; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 0691b8618041..c54f39a77409 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -43,7 +43,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; -import { capture, suspend } from '../dom/blocks/boundary.js'; +import { active_boundary, capture, suspend } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; /** @@ -112,6 +112,7 @@ function create_effect(type, fn, sync, push = true) { last: null, next: null, parent: is_root ? null : parent_effect, + b: parent_effect && parent_effect.b, prev: null, teardown: null, transitions: null, diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 5ef0097649a4..6c665bbbe133 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,4 +1,5 @@ import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; +import type { Boundary } from '../dom/blocks/boundary'; export interface Signal { /** Flags bitmask */ @@ -67,6 +68,8 @@ export interface Effect extends Reaction { last: null | Effect; /** Parent effect */ parent: Effect | null; + /** THe boundary this effect belongs to */ + b: Boundary | null; /** Dev only */ component_function?: any; } From 1e56ce2c25d79d4c34b17919d0a3532b8f0d2620 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 10:20:39 -0500 Subject: [PATCH 207/582] unused --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 3 --- 1 file changed, 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 19550d9df93a..bdef1453ac3b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -198,9 +198,6 @@ export class Boundary { } }; - // @ts-ignore - boundary_effect.fn.is_pending = () => this.#props.pending; - if (hydrating) { hydrate_next(); } From 4c0405390abdf3276b6eda9e3d02541f5f1dbe45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 10:31:52 -0500 Subject: [PATCH 208/582] more --- .../svelte/src/internal/client/dom/blocks/boundary.js | 9 +++++++-- 1 file changed, 7 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 bdef1453ac3b..e3f4bd161a98 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -66,6 +66,9 @@ export class Boundary { /** @type {BoundaryProps} */ #props; + /** @type {((anchor: Node) => void)} */ + #children; + /** @type {Effect} */ #effect; @@ -102,6 +105,8 @@ export class Boundary { constructor(node, props, children) { this.#anchor = node; this.#props = props; + this.#children = children; + this.parent = active_boundary; active_boundary = this; @@ -126,7 +131,7 @@ export class Boundary { this.#is_creating_fallback = false; try { - return branch(() => children(this.#anchor)); + return branch(() => this.#children(this.#anchor)); } finally { reset_is_throwing_error(); } @@ -220,7 +225,7 @@ export class Boundary { destroy_effect(/** @type {Effect} */ (this.#pending_effect)); this.#main_effect = this.#run(() => { - return branch(() => children(this.#anchor)); + return branch(() => this.#children(this.#anchor)); }); }); } else { From 9cc52e27d202db3cf26cd6d996d037ffe9dbc309 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 10:32:37 -0500 Subject: [PATCH 209/582] simplify --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 3 --- packages/svelte/src/internal/client/runtime.js | 3 +-- 2 files changed, 1 insertion(+), 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 e3f4bd161a98..fa2903414fab 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -242,9 +242,6 @@ export class Boundary { this.ran = true; - // @ts-expect-error - this.#effect.fn.boundary = this; - if (hydrating) { this.#anchor = hydrate_node; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4027a094ad01..acd863c566b7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -829,8 +829,7 @@ function process_effects(effect, collected_effects, boundary) { // Inside a boundary, defer everything except block/branch effects boundary.add_effect(current_effect); } else if ((flags & BOUNDARY_EFFECT) !== 0) { - // @ts-expect-error - var b = /** @type {Boundary} */ (current_effect.fn.boundary); + var b = /** @type {Boundary} */ (current_effect.b); process_effects(current_effect, collected_effects, b); From 1f58d6b7e46e48b12d37570dc2a23f77a62df185 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 10:37:47 -0500 Subject: [PATCH 210/582] simplify --- .../src/internal/client/dom/blocks/boundary.js | 17 +---------------- .../src/internal/client/dom/blocks/each.js | 3 +-- .../svelte/src/internal/client/dom/blocks/if.js | 4 ++-- .../src/internal/client/dom/blocks/key.js | 4 ++-- .../client/dom/blocks/svelte-component.js | 4 ++-- .../src/internal/client/reactivity/effects.js | 6 ++---- 6 files changed, 10 insertions(+), 28 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index fa2903414fab..5de8a8053f1c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -36,14 +36,6 @@ import { from_async_derived, set_from_async_derived } from '../../reactivity/der import { raf } from '../../timing.js'; import { loop } from '../../loop.js'; -/** @type {Boundary | null} */ -export let active_boundary = null; - -/** @param {Boundary | null} boundary */ -export function set_active_boundary(boundary) { - active_boundary = boundary; -} - /** * @typedef {{ * onerror?: (error: unknown, reset: () => void) => void; @@ -107,9 +99,7 @@ export class Boundary { this.#props = props; this.#children = children; - this.parent = active_boundary; - - active_boundary = this; + this.parent = /** @type {Effect} */ (active_effect).b; this.#effect = block(() => { var boundary_effect = /** @type {Effect} */ (active_effect); @@ -245,8 +235,6 @@ export class Boundary { if (hydrating) { this.#anchor = hydrate_node; } - - active_boundary = this.parent; } has_pending_snippet() { @@ -257,12 +245,10 @@ export class Boundary { * @param {() => Effect | null} fn */ #run(fn) { - var previous_boundary = active_boundary; var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_ctx = component_context; - active_boundary = this; set_active_effect(this.#effect); set_active_reaction(this.#effect); set_component_context(this.#effect.ctx); @@ -270,7 +256,6 @@ export class Boundary { try { return fn(); } finally { - active_boundary = previous_boundary; set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_ctx); diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index e8b4feda99f4..ec97bb482872 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -39,7 +39,6 @@ import { queue_micro_task } from '../task.js'; import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; -import { active_boundary } from './boundary.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -139,7 +138,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var was_empty = false; - var boundary = active_boundary; + var boundary = /** @type {Effect} */ (active_effect).b; /** @type {Map} */ var offscreen_items = new Map(); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 2a6a52c446b0..d8ad6f273af0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -10,8 +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_boundary } from './boundary.js'; import { create_text, should_defer_append } from '../operations.js'; +import { active_effect } from '../../runtime.js'; /** * @param {TemplateNode} node @@ -50,7 +50,7 @@ export function if_block(node, fn, elseif = false) { /** @type {Effect | null} */ var pending_effect = null; - var boundary = active_boundary; + var boundary = /** @type {Effect} */ (active_effect).b; function commit() { if (offscreen_fragment !== null) { diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 4c6cce7d793d..2c7e0b4cd6e4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -4,8 +4,8 @@ import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { not_equal, safe_not_equal } from '../../reactivity/equality.js'; import { is_runes } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { active_boundary } from './boundary.js'; import { create_text, should_defer_append } from '../operations.js'; +import { active_effect } from '../../runtime.js'; /** * @template V @@ -33,7 +33,7 @@ export function key_block(node, get_key, render_fn) { /** @type {DocumentFragment | null} */ var offscreen_fragment = null; - var boundary = active_boundary; + var boundary = /** @type {Effect} */ (active_effect).b; var changed = is_runes() ? not_equal : safe_not_equal; 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 330150a80c91..9311fab62a53 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,9 +1,9 @@ /** @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 { create_text, should_defer_append } from '../operations.js'; -import { active_boundary } from './boundary.js'; /** * @template P @@ -32,7 +32,7 @@ export function component(node, get_component, render_fn) { /** @type {Effect | null} */ var pending_effect = null; - var boundary = active_boundary; + var boundary = /** @type {Effect} */ (active_effect).b; function commit() { if (effect) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c54f39a77409..554b3bce27da 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -14,7 +14,6 @@ import { set_is_flushing_effect, set_signal_status, untrack, - skip_reaction, untracking } from '../runtime.js'; import { @@ -34,8 +33,7 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_PRESERVED, - BOUNDARY_EFFECT + EFFECT_PRESERVED } from '../constants.js'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -43,7 +41,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; -import { active_boundary, capture, suspend } from '../dom/blocks/boundary.js'; +import { capture, suspend } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; /** From 58dc13efb1f7bf757198c5fe5c007a0cf79ff1a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 10:57:20 -0500 Subject: [PATCH 211/582] 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 5de8a8053f1c..dd36668873de 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -134,7 +134,7 @@ export class Boundary { }; // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field - boundary_effect.fn = (/** @type {unknown} */ input, /** @type {any} */ payload) => { + boundary_effect.fn = (/** @type {unknown} */ input) => { var error = input; var onerror = this.#props.onerror; let failed = this.#props.failed; From 67b5c09fb306ec2018642ec0f3573492aa176da4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 11:00:18 -0500 Subject: [PATCH 212/582] more --- .../internal/client/dom/blocks/boundary.js | 172 +++++++++--------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index dd36668873de..ff40c04608e6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -55,6 +55,9 @@ export class Boundary { /** @type {TemplateNode} */ #anchor; + /** @type {TemplateNode} */ + #hydrate_open; + /** @type {BoundaryProps} */ #props; @@ -105,92 +108,12 @@ export class Boundary { var boundary_effect = /** @type {Effect} */ (active_effect); boundary_effect.b = this; - var hydrate_open = hydrate_node; - - const reset = () => { - this.#pending_count = 0; - this.suspended = false; - - if (this.#failed_effect !== null) { - pause_effect(this.#failed_effect, () => { - this.#failed_effect = null; - }); - } - - this.#main_effect = this.#run(() => { - this.#is_creating_fallback = false; - - try { - return branch(() => this.#children(this.#anchor)); - } finally { - reset_is_throwing_error(); - } - }); - - if (this.#pending_count > 0) { - this.suspended = true; - this.#show_pending_snippet(true); - } - }; + this.#hydrate_open = hydrate_node; // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary_effect.fn = (/** @type {unknown} */ input) => { var error = input; - var onerror = this.#props.onerror; - let failed = this.#props.failed; - - // 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 (this.#is_creating_fallback || (!onerror && !failed)) { - throw error; - } - - onerror?.(error, reset); - - if (this.#main_effect) { - destroy_effect(this.#main_effect); - this.#main_effect = null; - } - - if (this.#pending_effect) { - destroy_effect(this.#pending_effect); - this.#pending_effect = null; - } - - if (this.#failed_effect) { - destroy_effect(this.#failed_effect); - this.#failed_effect = null; - } - - if (hydrating) { - set_hydrate_node(hydrate_open); - next(); - set_hydrate_node(remove_nodes()); - } - - if (failed) { - queue_boundary_micro_task(() => { - this.#failed_effect = this.#run(() => { - this.#is_creating_fallback = true; - - try { - return branch(() => { - failed( - this.#anchor, - () => error, - () => reset - ); - }); - } catch (error) { - handle_error(error, boundary_effect, null, boundary_effect.ctx); - return null; - } finally { - reset_is_throwing_error(); - this.#is_creating_fallback = false; - } - }); - }); - } + this.error(input); }; if (hydrating) { @@ -382,6 +305,91 @@ export class Boundary { } } } + + /** @param {unknown} error */ + error(error) { + var onerror = this.#props.onerror; + let failed = this.#props.failed; + + const reset = () => { + this.#pending_count = 0; + this.suspended = false; + + if (this.#failed_effect !== null) { + pause_effect(this.#failed_effect, () => { + this.#failed_effect = null; + }); + } + + this.#main_effect = this.#run(() => { + this.#is_creating_fallback = false; + + try { + return branch(() => this.#children(this.#anchor)); + } finally { + reset_is_throwing_error(); + } + }); + + if (this.#pending_count > 0) { + this.suspended = true; + this.#show_pending_snippet(true); + } + }; + + // 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 (this.#is_creating_fallback || (!onerror && !failed)) { + throw error; + } + + onerror?.(error, reset); + + if (this.#main_effect) { + destroy_effect(this.#main_effect); + this.#main_effect = null; + } + + if (this.#pending_effect) { + destroy_effect(this.#pending_effect); + this.#pending_effect = null; + } + + if (this.#failed_effect) { + destroy_effect(this.#failed_effect); + this.#failed_effect = null; + } + + if (hydrating) { + set_hydrate_node(this.#hydrate_open); + next(); + set_hydrate_node(remove_nodes()); + } + + if (failed) { + queue_boundary_micro_task(() => { + this.#failed_effect = this.#run(() => { + this.#is_creating_fallback = true; + + try { + return branch(() => { + failed( + this.#anchor, + () => error, + () => reset + ); + }); + } catch (error) { + handle_error(error, this.#effect, null, this.#effect.ctx); + return null; + } finally { + reset_is_throwing_error(); + this.#is_creating_fallback = false; + } + }); + }); + } + } } var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; From 3b9349e51a29f3ea272fa1a96f6a4499f539bce1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 11:04:31 -0500 Subject: [PATCH 213/582] tweak --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 5 ++--- 1 file changed, 2 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 ff40c04608e6..e1382e2ced85 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -102,17 +102,16 @@ export class Boundary { this.#props = props; this.#children = children; + this.#hydrate_open = hydrate_node; + this.parent = /** @type {Effect} */ (active_effect).b; this.#effect = block(() => { var boundary_effect = /** @type {Effect} */ (active_effect); boundary_effect.b = this; - this.#hydrate_open = hydrate_node; - // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field boundary_effect.fn = (/** @type {unknown} */ input) => { - var error = input; this.error(input); }; From 63be623021a0eb81e751c97b8ac1812a94799c0e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 11:06:03 -0500 Subject: [PATCH 214/582] unused --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 1 - 1 file changed, 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 e1382e2ced85..6525b3e5fb73 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -3,7 +3,6 @@ import { BOUNDARY_EFFECT, EFFECT_PRESERVED, - EFFECT_RAN, EFFECT_TRANSPARENT, RENDER_EFFECT } from '../../constants.js'; From 30cd46de11620e5e733f81b6a2f5fb59c087bb99 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 11:14:03 -0500 Subject: [PATCH 215/582] more --- .../internal/client/dom/blocks/boundary.js | 9 ++------- .../svelte/src/internal/client/runtime.js | 20 +++++++------------ 2 files changed, 9 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 6525b3e5fb73..2c7136ef1093 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -47,6 +47,7 @@ import { loop } from '../../loop.js'; export class Boundary { suspended = false; + inert = false; /** @type {Boundary | null} */ parent; @@ -106,13 +107,7 @@ export class Boundary { this.parent = /** @type {Effect} */ (active_effect).b; this.#effect = block(() => { - var boundary_effect = /** @type {Effect} */ (active_effect); - boundary_effect.b = this; - - // @ts-ignore We re-use the effect's fn property to avoid allocation of an additional field - boundary_effect.fn = (/** @type {unknown} */ input) => { - this.error(input); - }; + /** @type {Effect} */ (active_effect).b = this; if (hydrating) { hydrate_next(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index acd863c566b7..d872503ab6ac 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -252,22 +252,19 @@ export function check_dirtiness(reaction) { * @param {Effect} effect */ function propagate_error(error, effect) { - /** @type {Effect | null} */ - var current = effect; + var boundary = effect.b; - while (current !== null) { - if ((current.f & BOUNDARY_EFFECT) !== 0) { + while (boundary !== null) { + if (!boundary.inert) { try { - // @ts-expect-error - current.fn(error); + boundary.error(error); return; } catch { - // Remove boundary flag from effect - current.f ^= BOUNDARY_EFFECT; + boundary.inert = true; } } - current = current.parent; + boundary = boundary.parent; } is_throwing_error = false; @@ -278,10 +275,7 @@ function propagate_error(error, effect) { * @param {Effect} effect */ function should_rethrow_error(effect) { - return ( - (effect.f & DESTROYED) === 0 && - (effect.parent === null || (effect.parent.f & BOUNDARY_EFFECT) === 0) - ); + return (effect.f & DESTROYED) === 0 && (effect.parent === null || !effect.b || effect.b.inert); } export function reset_is_throwing_error() { From df027d0f34ffd0f50b8ad362e05c1340ae6e2a12 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 11:20:36 -0500 Subject: [PATCH 216/582] shuffle --- .../internal/client/dom/blocks/boundary.js | 30 ++++++++----------- 1 file changed, 12 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 2c7136ef1093..57641c7a9c35 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -45,6 +45,18 @@ import { loop } from '../../loop.js'; * }} BoundaryProps */ +var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; + +/** + * @param {TemplateNode} node + * @param {BoundaryProps} props + * @param {((anchor: Node) => void)} children + * @returns {void} + */ +export function boundary(node, props, children) { + new Boundary(node, props, children); +} + export class Boundary { suspended = false; inert = false; @@ -385,24 +397,6 @@ export class Boundary { } } -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; - * showPendingAfter?: number; - * showPendingFor?: number; - * }} props - * @param {((anchor: Node) => void)} children - * @returns {void} - */ -export function boundary(node, props, children) { - new Boundary(node, props, children); -} - /** * * @param {Effect} effect From 366b59c19410dbca694004116666a9d187140dad Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 12 Feb 2025 21:10:08 -0500 Subject: [PATCH 217/582] move compiler options to svelte.config.js, to remove red squigglies in editor --- playgrounds/sandbox/svelte.config.js | 9 +++++++++ playgrounds/sandbox/vite.config.js | 12 +----------- 2 files changed, 10 insertions(+), 11 deletions(-) create mode 100644 playgrounds/sandbox/svelte.config.js diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js new file mode 100644 index 000000000000..68ac605385aa --- /dev/null +++ b/playgrounds/sandbox/svelte.config.js @@ -0,0 +1,9 @@ +export default { + compilerOptions: { + hmr: false, + + experimental: { + async: true + } + } +}; diff --git a/playgrounds/sandbox/vite.config.js b/playgrounds/sandbox/vite.config.js index 80a635a23960..5ce020421709 100644 --- a/playgrounds/sandbox/vite.config.js +++ b/playgrounds/sandbox/vite.config.js @@ -7,17 +7,7 @@ export default defineConfig({ minify: false }, - plugins: [ - inspect(), - svelte({ - compilerOptions: { - hmr: false, - experimental: { - async: true - } - } - }) - ], + plugins: [inspect(), svelte()], optimizeDeps: { // svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change From 9d7d045310552a60f16c3ac077e392218f811875 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Feb 2025 13:35:24 -0500 Subject: [PATCH 218/582] create separate effect type for async deriveds, as they are not blocks --- packages/svelte/src/internal/client/constants.js | 1 + packages/svelte/src/internal/client/dev/debug.js | 3 +++ .../svelte/src/internal/client/reactivity/deriveds.js | 8 ++++---- packages/svelte/src/internal/client/reactivity/effects.js | 4 ++-- packages/svelte/src/internal/client/reactivity/sources.js | 5 +++-- packages/svelte/src/internal/client/runtime.js | 5 +++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 530f72b61cde..cf9a18f3dd5a 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -23,6 +23,7 @@ export const EFFECT_PRESERVED = 1 << 21; // effects with this flag should not be // Flags used for async export const REACTION_IS_UPDATING = 1 << 22; +export const EFFECT_ASYNC = 1 << 23; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index 2007f0066b18..b65f79697c62 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -7,6 +7,7 @@ import { CLEAN, DERIVED, EFFECT, + EFFECT_ASYNC, MAYBE_DIRTY, RENDER_EFFECT, ROOT_EFFECT @@ -39,6 +40,8 @@ export function log_effect_tree(effect) { label = 'boundary'; } else if ((flags & BLOCK_EFFECT) !== 0) { label = 'block'; + } else if ((flags & EFFECT_ASYNC) !== 0) { + label = 'async'; } else if ((flags & BRANCH_EFFECT) !== 0) { label = 'branch'; } else if ((flags & RENDER_EFFECT) !== 0) { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6fd875c98fb4..8d1a0692d60f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -5,6 +5,7 @@ import { DERIVED, DESTROYED, DIRTY, + EFFECT_ASYNC, EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED @@ -22,7 +23,7 @@ import { 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 { block, 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'; @@ -107,8 +108,7 @@ export function async_derived(fn, location) { /** @type {(() => void) | null} */ var unsuspend = null; - // TODO this isn't a block - block(() => { + render_effect(() => { if (DEV) from_async_derived = active_effect; var current = (promise = fn()); if (DEV) from_async_derived = null; @@ -151,7 +151,7 @@ export function async_derived(fn, location) { } } ); - }, EFFECT_PRESERVED); + }, EFFECT_ASYNC | EFFECT_PRESERVED); return new Promise(async (fulfil) => { // if the effect re-runs before the initial promise diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 554b3bce27da..ab6ee71c4e24 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -335,8 +335,8 @@ export function legacy_pre_effect_reset() { * @param {() => void | (() => void)} fn * @returns {Effect} */ -export function render_effect(fn) { - return create_effect(RENDER_EFFECT, fn, true); +export function render_effect(fn, flags = 0) { + return create_effect(RENDER_EFFECT | flags, fn, true); } /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 0dc55f97babc..efc5aa20fe78 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -28,7 +28,8 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + EFFECT_ASYNC } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -150,7 +151,7 @@ export function set(source, value) { active_reaction !== null && !untracking && is_runes() && - (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && + (active_reaction.f & (DERIVED | BLOCK_EFFECT | EFFECT_ASYNC)) !== 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/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 0b9d22fa56f0..b352d1a75f5a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -24,7 +24,8 @@ import { LEGACY_DERIVED_PROP, DISCONNECTED, BOUNDARY_EFFECT, - REACTION_IS_UPDATING + REACTION_IS_UPDATING, + EFFECT_ASYNC } from './constants.js'; import { flush_idle_tasks, @@ -820,7 +821,7 @@ function process_effects(effect, effects = [], boundary) { var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { - if (boundary !== undefined && (flags & (BLOCK_EFFECT | BRANCH_EFFECT)) === 0) { + if (boundary !== undefined && (flags & (BLOCK_EFFECT | BRANCH_EFFECT | EFFECT_ASYNC)) === 0) { // Inside a boundary, defer everything except block/branch effects boundary.add_effect(current_effect); } else if ((flags & BOUNDARY_EFFECT) !== 0) { From 7923b5a75455120e07dd9e28c7e4c7528026b002 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 13 Feb 2025 15:11:43 -0500 Subject: [PATCH 219/582] simplify --- packages/svelte/src/internal/client/dom/blocks/key.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 2c7e0b4cd6e4..06e9ab73e030 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -27,8 +27,8 @@ export function key_block(node, get_key, render_fn) { /** @type {Effect} */ var effect; - /** @type {Effect | null} */ - var pending_effect = null; + /** @type {Effect} */ + var pending_effect; /** @type {DocumentFragment | null} */ var offscreen_fragment = null; @@ -47,10 +47,7 @@ export function key_block(node, get_key, render_fn) { offscreen_fragment = null; } - if (pending_effect !== null) { - effect = pending_effect; - pending_effect = null; - } + effect = pending_effect; } block(() => { From b18247be3896dba720e4ed54538fe10900f38a44 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Feb 2025 06:45:34 -0500 Subject: [PATCH 220/582] WIP --- .../internal/client/reactivity/deriveds.js | 67 +++++++++------- .../src/internal/client/reactivity/forks.js | 61 +++++++++++++++ .../src/internal/client/reactivity/sources.js | 20 +++-- .../svelte/src/internal/client/runtime.js | 78 ++++++------------- 4 files changed, 130 insertions(+), 96 deletions(-) create mode 100644 packages/svelte/src/internal/client/reactivity/forks.js diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 8d1a0692d60f..7c051079df64 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -18,19 +18,20 @@ import { update_reaction, increment_write_version, set_active_effect, - handle_error + handle_error, + flush_sync } 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, render_effect } from './effects.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 { capture, suspend } from '../dom/blocks/boundary.js'; +import { capture } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; -import { noop } from '../../shared/utils.js'; import { UNINITIALIZED } from '../../../constants.js'; +import { active_fork } from './forks.js'; /** @type {Effect | null} */ export let from_async_derived = null; @@ -105,16 +106,19 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - /** @type {(() => void) | null} */ - var unsuspend = null; - render_effect(() => { if (DEV) from_async_derived = active_effect; - var current = (promise = fn()); + promise = fn(); if (DEV) from_async_derived = null; var restore = capture(); - if (should_suspend) unsuspend ??= suspend(); + + var fork = active_fork; + + if (should_suspend) { + // TODO if nearest pending boundary is not ready, attach to the boundary + fork?.increment(); + } promise.then( (v) => { @@ -122,33 +126,36 @@ export function async_derived(fn, location) { return; } - if (promise === current) { - restore(); - from_async_derived = null; + restore(); + from_async_derived = null; + if (should_suspend) { + fork?.decrement(); + } + + if (fork !== null) { + fork?.enable(); + flush_sync(() => { + internal_set(signal, v); + }); + fork?.disable(); + } else { internal_set(signal, v); + } - if (DEV && location !== undefined) { - recent_async_deriveds.add(signal); - - setTimeout(() => { - if (recent_async_deriveds.has(signal)) { - w.await_waterfall(location); - recent_async_deriveds.delete(signal); - } - }); - } - - // TODO we should probably null out active effect here, - // rather than inside `restore()` - unsuspend?.(); - unsuspend = null; + if (DEV && location !== undefined) { + recent_async_deriveds.add(signal); + + setTimeout(() => { + if (recent_async_deriveds.has(signal)) { + w.await_waterfall(location); + recent_async_deriveds.delete(signal); + } + }); } }, (e) => { - if (promise === current) { - handle_error(e, parent, null, parent.ctx); - } + handle_error(e, parent, null, parent.ctx); } ); }, EFFECT_ASYNC | EFFECT_PRESERVED); diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js new file mode 100644 index 000000000000..2529772b9cf0 --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -0,0 +1,61 @@ +/** @import { Effect, Source } from '#client' */ + +/** @type {Set} */ +const forks = new Set(); + +/** @type {Fork | null} */ +export let active_fork = null; + +let uid = 1; + +export class Fork { + id = uid++; + + /** @type {Map} */ + previous = new Map(); + + /** @type {Set} */ + skipped_effects = new Set(); + + #pending = 0; + + /** + * @param {Source} source + * @param {any} value + */ + capture(source, value) { + if (!this.previous.has(source)) { + this.previous.set(source, value); + } + } + + enable() { + active_fork = this; + // TODO revert other forks + } + + disable() { + active_fork = null; + // TODO restore state + } + + increment() { + this.#pending += 1; + } + + decrement() { + this.#pending -= 1; + } + + settled() { + return this.#pending === 0; + } + + static ensure() { + return (active_fork ??= new Fork()); + } + + static unset() { + active_fork = null; + } +} diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index efc5aa20fe78..33a23251ea4a 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -35,6 +35,7 @@ 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 { active_fork, Fork } from './forks.js'; export let inspect_effects = new Set(); @@ -174,6 +175,9 @@ export function internal_set(source, value) { source.v = value; source.wv = increment_write_version(); + const fork = Fork.ensure(); + fork.capture(source, old_value); + if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); if (active_effect != null) { @@ -260,7 +264,7 @@ export function update_pre(source, d = 1) { * @param {number} status should be DIRTY or MAYBE_DIRTY * @returns {void} */ -function mark_reactions(signal, status) { +export function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; @@ -271,9 +275,6 @@ function mark_reactions(signal, status) { 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; @@ -285,13 +286,10 @@ function mark_reactions(signal, status) { set_signal_status(reaction, 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); - } else { - schedule_effect(/** @type {Effect} */ (reaction)); - } + if ((flags & DERIVED) !== 0) { + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + } else { + schedule_effect(/** @type {Effect} */ (reaction)); } } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b352d1a75f5a..d738ffe40fa2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -53,6 +53,8 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; +import { active_fork, Fork } from './reactivity/forks.js'; +import { log_effect_tree } from './dev/debug.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -702,10 +704,14 @@ function flush_queued_root_effects(root_effects) { } var collected_effects = process_effects(effect); - flush_queued_effects(collected_effects); + + if (/** @type {Fork} */ (active_fork).settled()) { + flush_queued_effects(collected_effects); + } } } finally { is_flushing_effect = previously_flushing_effect; + Fork.unset(); } } @@ -805,14 +811,16 @@ export function schedule_effect(signal) { * effects to be flushed. * * @param {Effect} effect - * @param {Effect[]} effects - * @param {Boundary} [boundary] * @returns {Effect[]} */ -function process_effects(effect, effects = [], boundary) { +function process_effects(effect) { var current_effect = effect.first; - var current_effect = effect.first; + /** @type {Effect[]} */ + var render_effects = []; + + /** @type {Effect[]} */ + var effects = []; main_loop: while (current_effect !== null) { var flags = current_effect.f; @@ -820,64 +828,24 @@ function process_effects(effect, effects = [], boundary) { var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; var sibling = current_effect.next; - if (!is_skippable_branch && (flags & INERT) === 0) { - if (boundary !== undefined && (flags & (BLOCK_EFFECT | BRANCH_EFFECT | EFFECT_ASYNC)) === 0) { - // Inside a boundary, defer everything except block/branch effects - boundary.add_effect(current_effect); - } else if ((flags & BOUNDARY_EFFECT) !== 0) { - var b = /** @type {Boundary} */ (current_effect.b); + var skip = + is_skippable_branch || + (flags & INERT) !== 0 || + active_fork?.skipped_effects.has(current_effect); - process_effects(current_effect, effects, b); - - if (!b.suspended) { - // no more async work to happen - b.commit(); + if (!skip) { + if ((flags & (BLOCK_EFFECT | EFFECT_ASYNC)) !== 0) { + if (check_dirtiness(current_effect)) { + update_effect(current_effect); } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { 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 - var previous_active_reaction = active_reaction; - try { - active_reaction = current_effect; - if (check_dirtiness(current_effect)) { - update_effect(current_effect); - } - } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); - } finally { - active_reaction = previous_active_reaction; - } - } - - var child = current_effect.first; - - if (child !== null) { - current_effect = child; - continue; + render_effects.push(current_effect); } } else if ((flags & EFFECT) !== 0) { effects.push(current_effect); - } else if (is_branch) { - 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 - var previous_active_reaction = active_reaction; - try { - active_reaction = current_effect; - if (check_dirtiness(current_effect)) { - update_effect(current_effect); - } - } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); - } finally { - active_reaction = previous_active_reaction; - } } var child = current_effect.first; @@ -908,7 +876,7 @@ function process_effects(effect, effects = [], boundary) { current_effect = sibling; } - return effects; + return [...render_effects, ...effects]; } /** From 120b086b854dd31c380914ea6b940af7183b1e75 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Feb 2025 14:48:54 -0500 Subject: [PATCH 221/582] WIP --- .../internal/client/reactivity/deriveds.js | 4 +- .../src/internal/client/reactivity/forks.js | 70 +++++++++++++++++-- .../svelte/src/internal/client/runtime.js | 4 +- 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 7c051079df64..390aa511150e 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -134,11 +134,9 @@ export function async_derived(fn, location) { } if (fork !== null) { - fork?.enable(); - flush_sync(() => { + fork.run(() => { internal_set(signal, v); }); - fork?.disable(); } else { internal_set(signal, v); } diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 2529772b9cf0..1c04f3104ccc 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,5 +1,8 @@ /** @import { Effect, Source } from '#client' */ +import { flush_sync } from '../runtime.js'; +import { internal_set } from './sources.js'; + /** @type {Set} */ const forks = new Set(); @@ -29,14 +32,64 @@ export class Fork { } } - enable() { - active_fork = this; - // TODO revert other forks + /** + * + * @param {() => void} fn + */ + flush(fn) { + var values = new Map(); + + for (const fork of forks) { + if (fork === this) continue; + + for (const [source, previous] of fork.previous) { + if (this.previous.has(source)) continue; + + values.set(source, source.v); + source.v = previous; + // internal_set(source, previous); + } + } + + try { + fn(); + } finally { + for (const [source, value] of values) { + // internal_set(source, value); + source.v = value; + } + } } - disable() { + remove() { + forks.delete(this); + + for (var fork of forks) { + if (fork.id < this.id) { + // other fork is older than this + for (var source of this.previous.keys()) { + fork.previous.delete(source); + } + } else { + // other fork is newer than this + for (var source of fork.previous.keys()) { + if (this.previous.has(source)) { + fork.previous.set(source, source.v); + } + } + } + } + } + + /** + * @param {() => void} fn + */ + run(fn) { + active_fork = this; + + flush_sync(fn); + active_fork = null; - // TODO restore state } increment() { @@ -52,7 +105,12 @@ export class Fork { } static ensure() { - return (active_fork ??= new Fork()); + if (active_fork === null) { + active_fork = new Fork(); + forks.add(active_fork); // TODO figure out where we remove this + } + + return active_fork; } static unset() { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d738ffe40fa2..f642d704b462 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -703,10 +703,12 @@ function flush_queued_root_effects(root_effects) { effect.f ^= CLEAN; } + var fork = /** @type {Fork} */ (active_fork); var collected_effects = process_effects(effect); - if (/** @type {Fork} */ (active_fork).settled()) { + if (fork.settled()) { flush_queued_effects(collected_effects); + fork.remove(); } } } finally { From 0bc2af265db4718bacf57605618aa5267aa7145a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Feb 2025 14:05:41 -0500 Subject: [PATCH 222/582] WIP --- .../internal/client/dom/blocks/boundary.js | 2 +- .../internal/client/reactivity/deriveds.js | 3 +- .../src/internal/client/reactivity/forks.js | 58 +++++++++++-------- .../src/internal/client/reactivity/sources.js | 2 + .../svelte/src/internal/client/runtime.js | 15 ++++- 5 files changed, 50 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 57641c7a9c35..8d85b2442140 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -306,7 +306,7 @@ export class Boundary { if (this.#main_effect !== null) { // TODO do we also need to `resume_effect` here? - schedule_effect(this.#main_effect); + // schedule_effect(this.#main_effect); } } } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 390aa511150e..5a385ce0b3bd 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -138,7 +138,8 @@ export function async_derived(fn, location) { internal_set(signal, v); }); } else { - internal_set(signal, v); + signal.v = v; + // internal_set(signal, v); } if (DEV && location !== undefined) { diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 1c04f3104ccc..f450d215f95e 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,5 +1,4 @@ /** @import { Effect, Source } from '#client' */ - import { flush_sync } from '../runtime.js'; import { internal_set } from './sources.js'; @@ -17,48 +16,57 @@ export class Fork { /** @type {Map} */ previous = new Map(); + /** @type {Map} */ + current = new Map(); + /** @type {Set} */ skipped_effects = new Set(); #pending = 0; - /** - * @param {Source} source - * @param {any} value - */ - capture(source, value) { - if (!this.previous.has(source)) { - this.previous.set(source, value); - } - } - - /** - * - * @param {() => void} fn - */ - flush(fn) { + apply() { var values = new Map(); + for (const source of this.previous.keys()) { + values.set(source, source.v); + } + for (const fork of forks) { if (fork === this) continue; for (const [source, previous] of fork.previous) { - if (this.previous.has(source)) continue; - - values.set(source, source.v); - source.v = previous; - // internal_set(source, previous); + if (!values.has(source)) { + values.set(source, source.v); + // internal_set(source, previous); + source.v = previous; + } } } - try { - fn(); - } finally { + for (const [source, current] of this.current) { + source.v = current; + // internal_set(source, current); + } + + return () => { for (const [source, value] of values) { - // internal_set(source, value); source.v = value; } + + active_fork = null; + }; + } + + /** + * @param {Source} source + * @param {any} value + */ + capture(source, value) { + if (!this.previous.has(source)) { + this.previous.set(source, value); } + + this.current.set(source, source.v); } remove() { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 33a23251ea4a..20b36b3cc0b6 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -170,6 +170,8 @@ export function set(source, value) { * @returns {V} */ export function internal_set(source, value) { + // console.trace('internal_set', source.v, value); + if (!source.equals(value)) { var old_value = source.v; source.v = value; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index f642d704b462..340ec0fe9f92 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -695,6 +695,9 @@ function flush_queued_root_effects(root_effects) { var previously_flushing_effect = is_flushing_effect; is_flushing_effect = true; + var fork = /** @type {Fork} */ (active_fork); + var revert = fork.apply(); + try { for (var i = 0; i < length; i++) { var effect = root_effects[i]; @@ -703,17 +706,23 @@ function flush_queued_root_effects(root_effects) { effect.f ^= CLEAN; } - var fork = /** @type {Fork} */ (active_fork); var collected_effects = process_effects(effect); if (fork.settled()) { flush_queued_effects(collected_effects); - fork.remove(); } } } finally { is_flushing_effect = previously_flushing_effect; - Fork.unset(); + + // TODO this doesn't seem quite right — may run into + // interesting cases where there are multiple roots. + // it'll do for now though + if (fork.settled()) { + fork.remove(); + } + + revert(); } } From 2fbf29025eaf31c6ea6ac1f98a12f78e14a5dc1f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Feb 2025 14:15:17 -0500 Subject: [PATCH 223/582] WIP --- .../src/internal/client/reactivity/forks.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index f450d215f95e..7f306aff551c 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,6 +1,7 @@ /** @import { Effect, Source } from '#client' */ +import { DIRTY } from '../constants.js'; import { flush_sync } from '../runtime.js'; -import { internal_set } from './sources.js'; +import { internal_set, mark_reactions } from './sources.js'; /** @type {Set} */ const forks = new Set(); @@ -28,26 +29,26 @@ export class Fork { var values = new Map(); for (const source of this.previous.keys()) { + // mark_reactions(source, DIRTY); values.set(source, source.v); } + for (const [source, current] of this.current) { + source.v = current; + } + for (const fork of forks) { if (fork === this) continue; for (const [source, previous] of fork.previous) { if (!values.has(source)) { + // mark_reactions(source, DIRTY); values.set(source, source.v); - // internal_set(source, previous); source.v = previous; } } } - for (const [source, current] of this.current) { - source.v = current; - // internal_set(source, current); - } - return () => { for (const [source, value] of values) { source.v = value; From 0e4f041ae2f653358a0c5474980d91e54fdde37d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Feb 2025 16:15:28 -0500 Subject: [PATCH 224/582] WIP --- .../src/internal/client/reactivity/forks.js | 7 ------ .../svelte/src/internal/client/runtime.js | 22 +++++++++---------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 7f306aff551c..e01405b5bc79 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -95,10 +95,7 @@ export class Fork { */ run(fn) { active_fork = this; - flush_sync(fn); - - active_fork = null; } increment() { @@ -121,8 +118,4 @@ export class Fork { return active_fork; } - - static unset() { - active_fork = null; - } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 340ec0fe9f92..0de96f95ee49 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -686,6 +686,12 @@ function infinite_loop_guard() { * @returns {void} */ function flush_queued_root_effects(root_effects) { + if (active_fork === null) { + return; + } + + var revert = active_fork.apply(); + var length = root_effects.length; if (length === 0) { return; @@ -695,9 +701,6 @@ function flush_queued_root_effects(root_effects) { var previously_flushing_effect = is_flushing_effect; is_flushing_effect = true; - var fork = /** @type {Fork} */ (active_fork); - var revert = fork.apply(); - try { for (var i = 0; i < length; i++) { var effect = root_effects[i]; @@ -708,7 +711,7 @@ function flush_queued_root_effects(root_effects) { var collected_effects = process_effects(effect); - if (fork.settled()) { + if (active_fork.settled()) { flush_queued_effects(collected_effects); } } @@ -718,8 +721,8 @@ function flush_queued_root_effects(root_effects) { // TODO this doesn't seem quite right — may run into // interesting cases where there are multiple roots. // it'll do for now though - if (fork.settled()) { - fork.remove(); + if (active_fork.settled()) { + active_fork.remove(); } revert(); @@ -903,11 +906,8 @@ export function flush_sync(fn) { try { infinite_loop_guard(); - /** @type {Effect[]} */ - const root_effects = []; - scheduler_mode = FLUSH_SYNC; - queued_root_effects = root_effects; + queued_root_effects = []; is_micro_task_queued = false; flush_queued_root_effects(previous_queued_root_effects); @@ -917,7 +917,7 @@ export function flush_sync(fn) { flush_boundary_micro_tasks(); flush_post_micro_tasks(); flush_idle_tasks(); - if (queued_root_effects.length > 0 || root_effects.length > 0) { + if (queued_root_effects.length > 0) { flush_sync(); } From f9eb2f9f9dc5ab3eba94783458f36d048852ff9d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:01:34 -0500 Subject: [PATCH 225/582] mirror some changes from main --- .../svelte/src/internal/client/dom/task.js | 25 +++++++++---------- .../svelte/src/internal/client/runtime.js | 8 ++---- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 73e88564b365..6e6e4d8d5cf5 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,7 +1,7 @@ import { run_all } from '../../shared/utils.js'; // Fallback for when requestIdleCallback is not available -export const request_idle_callback = +const request_idle_callback = typeof requestIdleCallback === 'undefined' ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; @@ -11,10 +11,12 @@ let is_idle_task_queued = false; /** @type {Array<() => void>} */ let queued_boundary_microtasks = []; + /** @type {Array<() => void>} */ let queued_post_microtasks = []; + /** @type {Array<() => void>} */ -let queued_idle_tasks = []; +let idle_tasks = []; export function flush_boundary_micro_tasks() { const tasks = queued_boundary_microtasks.slice(); @@ -28,13 +30,10 @@ export function flush_post_micro_tasks() { 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); - } +export function run_idle_tasks() { + var tasks = idle_tasks; + idle_tasks = []; + run_all(tasks); } function flush_all_micro_tasks() { @@ -71,9 +70,9 @@ export function queue_micro_task(fn) { * @param {() => void} fn */ export function queue_idle_task(fn) { - if (!is_idle_task_queued) { - is_idle_task_queued = true; - request_idle_callback(flush_idle_tasks); + if (idle_tasks.length === 0) { + request_idle_callback(run_idle_tasks); } - queued_idle_tasks.push(fn); + + idle_tasks.push(fn); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b352d1a75f5a..5048be3e2d48 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,11 +27,7 @@ import { REACTION_IS_UPDATING, EFFECT_ASYNC } from './constants.js'; -import { - flush_idle_tasks, - flush_boundary_micro_tasks, - flush_post_micro_tasks -} from './dom/task.js'; +import { flush_boundary_micro_tasks, flush_post_micro_tasks, run_idle_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { destroy_derived_effects, @@ -937,7 +933,7 @@ export function flush_sync(fn) { flush_boundary_micro_tasks(); flush_post_micro_tasks(); - flush_idle_tasks(); + run_idle_tasks(); if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } From 892dc82aa207873dcf048641268870556e0b6a06 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:03:18 -0500 Subject: [PATCH 226/582] rename --- packages/svelte/src/internal/client/dom/task.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 6e6e4d8d5cf5..4b5cc59fca9c 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -7,13 +7,12 @@ const request_idle_callback = : requestIdleCallback; let is_micro_task_queued = false; -let is_idle_task_queued = false; /** @type {Array<() => void>} */ let queued_boundary_microtasks = []; /** @type {Array<() => void>} */ -let queued_post_microtasks = []; +let micro_tasks = []; /** @type {Array<() => void>} */ let idle_tasks = []; @@ -25,8 +24,8 @@ export function flush_boundary_micro_tasks() { } export function flush_post_micro_tasks() { - const tasks = queued_post_microtasks.slice(); - queued_post_microtasks = []; + const tasks = micro_tasks.slice(); + micro_tasks = []; run_all(tasks); } @@ -63,7 +62,7 @@ export function queue_micro_task(fn) { is_micro_task_queued = true; queueMicrotask(flush_all_micro_tasks); } - queued_post_microtasks.push(fn); + micro_tasks.push(fn); } /** From 527deea929dbd96ef28b39dabd3d08edbaf6db4f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:06:55 -0500 Subject: [PATCH 227/582] more --- packages/svelte/src/internal/client/dom/task.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 4b5cc59fca9c..df9346750a73 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -36,21 +36,18 @@ export function run_idle_tasks() { } function flush_all_micro_tasks() { - if (is_micro_task_queued) { - is_micro_task_queued = false; - flush_boundary_micro_tasks(); - flush_post_micro_tasks(); - } + flush_boundary_micro_tasks(); + flush_post_micro_tasks(); } /** * @param {() => void} fn */ export function queue_boundary_micro_task(fn) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; + if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(flush_all_micro_tasks); } + queued_boundary_microtasks.push(fn); } @@ -58,10 +55,10 @@ export function queue_boundary_micro_task(fn) { * @param {() => void} fn */ export function queue_micro_task(fn) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; + if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(flush_all_micro_tasks); } + micro_tasks.push(fn); } From 5d9bd7f1ef268df23f8d8f79573f2be68cc6a400 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:07:31 -0500 Subject: [PATCH 228/582] more --- packages/svelte/src/internal/client/dom/task.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index df9346750a73..85fb971cef29 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -6,8 +6,6 @@ const request_idle_callback = ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; -let is_micro_task_queued = false; - /** @type {Array<() => void>} */ let queued_boundary_microtasks = []; @@ -35,7 +33,7 @@ export function run_idle_tasks() { run_all(tasks); } -function flush_all_micro_tasks() { +function run_micro_tasks() { flush_boundary_micro_tasks(); flush_post_micro_tasks(); } @@ -45,7 +43,7 @@ function flush_all_micro_tasks() { */ export function queue_boundary_micro_task(fn) { if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { - queueMicrotask(flush_all_micro_tasks); + queueMicrotask(run_micro_tasks); } queued_boundary_microtasks.push(fn); @@ -56,7 +54,7 @@ export function queue_boundary_micro_task(fn) { */ export function queue_micro_task(fn) { if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { - queueMicrotask(flush_all_micro_tasks); + queueMicrotask(run_micro_tasks); } micro_tasks.push(fn); From ed50a6bb3fc0c305a023d74784fbeb72d0339c71 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:09:24 -0500 Subject: [PATCH 229/582] more --- packages/svelte/src/internal/client/dom/task.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 85fb971cef29..77ac446ae100 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -7,7 +7,7 @@ const request_idle_callback = : requestIdleCallback; /** @type {Array<() => void>} */ -let queued_boundary_microtasks = []; +let boundary_micro_tasks = []; /** @type {Array<() => void>} */ let micro_tasks = []; @@ -16,13 +16,13 @@ let micro_tasks = []; let idle_tasks = []; export function flush_boundary_micro_tasks() { - const tasks = queued_boundary_microtasks.slice(); - queued_boundary_microtasks = []; + var tasks = boundary_micro_tasks; + boundary_micro_tasks = []; run_all(tasks); } export function flush_post_micro_tasks() { - const tasks = micro_tasks.slice(); + var tasks = micro_tasks; micro_tasks = []; run_all(tasks); } @@ -42,18 +42,18 @@ function run_micro_tasks() { * @param {() => void} fn */ export function queue_boundary_micro_task(fn) { - if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { + if (boundary_micro_tasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(run_micro_tasks); } - queued_boundary_microtasks.push(fn); + boundary_micro_tasks.push(fn); } /** * @param {() => void} fn */ export function queue_micro_task(fn) { - if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { + if (boundary_micro_tasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(run_micro_tasks); } From db947906f9844e432e7c6458a68c8052621865ab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:15:03 -0500 Subject: [PATCH 230/582] more --- packages/svelte/src/internal/client/dom/task.js | 10 +++++----- packages/svelte/src/internal/client/runtime.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 77ac446ae100..cec3e9d97e10 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -15,13 +15,13 @@ let micro_tasks = []; /** @type {Array<() => void>} */ let idle_tasks = []; -export function flush_boundary_micro_tasks() { +function run_boundary_micro_tasks() { var tasks = boundary_micro_tasks; boundary_micro_tasks = []; run_all(tasks); } -export function flush_post_micro_tasks() { +function run_post_micro_tasks() { var tasks = micro_tasks; micro_tasks = []; run_all(tasks); @@ -33,9 +33,9 @@ export function run_idle_tasks() { run_all(tasks); } -function run_micro_tasks() { - flush_boundary_micro_tasks(); - flush_post_micro_tasks(); +export function run_micro_tasks() { + run_boundary_micro_tasks(); + run_post_micro_tasks(); } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5048be3e2d48..3e63bbb9e08e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { REACTION_IS_UPDATING, EFFECT_ASYNC } from './constants.js'; -import { flush_boundary_micro_tasks, flush_post_micro_tasks, run_idle_tasks } from './dom/task.js'; +import { run_idle_tasks, run_micro_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { destroy_derived_effects, @@ -931,9 +931,9 @@ export function flush_sync(fn) { var result = fn?.(); - flush_boundary_micro_tasks(); - flush_post_micro_tasks(); + run_micro_tasks(); run_idle_tasks(); + if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } From cbc227c75ef8b41d9130409788a4e7c823c20b1a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:18:56 -0500 Subject: [PATCH 231/582] more --- packages/svelte/src/internal/client/dom/task.js | 17 +++++++++++++++-- packages/svelte/src/internal/client/runtime.js | 5 ++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index cec3e9d97e10..fc94d59245c1 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -27,13 +27,13 @@ function run_post_micro_tasks() { run_all(tasks); } -export function run_idle_tasks() { +function run_idle_tasks() { var tasks = idle_tasks; idle_tasks = []; run_all(tasks); } -export function run_micro_tasks() { +function run_micro_tasks() { run_boundary_micro_tasks(); run_post_micro_tasks(); } @@ -70,3 +70,16 @@ export function queue_idle_task(fn) { idle_tasks.push(fn); } + +/** + * Synchronously run any queued tasks. + */ +export function flush_tasks() { + if (boundary_micro_tasks.length > 0 || micro_tasks.length > 0) { + run_micro_tasks(); + } + + if (idle_tasks.length > 0) { + run_idle_tasks(); + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3e63bbb9e08e..1dd69d344fc9 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { REACTION_IS_UPDATING, EFFECT_ASYNC } from './constants.js'; -import { run_idle_tasks, run_micro_tasks } from './dom/task.js'; +import { flush_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { destroy_derived_effects, @@ -931,8 +931,7 @@ export function flush_sync(fn) { var result = fn?.(); - run_micro_tasks(); - run_idle_tasks(); + flush_tasks(); if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); From 1f4be94486302612c89d01c344fc0ca64040685a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 13:14:47 -0500 Subject: [PATCH 232/582] move some stuff --- packages/svelte/src/internal/client/runtime.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2025d0c9b2fc..41d7810eb7d6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -692,10 +692,7 @@ function flush_queued_root_effects() { root.f ^= CLEAN; } - var collected_effects = process_effects(root); - if (active_fork.settled()) { - flush_queued_effects(collected_effects); - } + process_effects(root, active_fork); } } } finally { @@ -787,9 +784,9 @@ export function schedule_effect(signal) { * effects to be flushed. * * @param {Effect} effect - * @returns {Effect[]} + * @param {Fork} fork */ -function process_effects(effect) { +function process_effects(effect, fork) { var current_effect = effect.first; /** @type {Effect[]} */ @@ -852,7 +849,10 @@ function process_effects(effect) { current_effect = sibling; } - return [...render_effects, ...effects]; + if (fork.settled()) { + flush_queued_effects(render_effects); + flush_queued_effects(effects); + } } /** From 97587c3284f4767a7d8149abe4241aaeeb95554b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 13:46:39 -0500 Subject: [PATCH 233/582] WIP --- .../src/internal/client/reactivity/forks.js | 12 +++++-- .../svelte/src/internal/client/runtime.js | 32 ++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 18f94a81198e..322c678b6c8f 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,4 +1,5 @@ /** @import { Effect, Source } from '#client' */ +import { noop } from '../../shared/utils.js'; import { DIRTY } from '../constants.js'; import { flushSync } from '../runtime.js'; import { internal_set, mark_reactions } from './sources.js'; @@ -26,6 +27,11 @@ export class Fork { #pending = 0; apply() { + if (forks.size === 1) { + // if this is the latest (and only) fork, we have nothing to do + return noop; + } + var values = new Map(); for (const source of this.previous.keys()) { @@ -53,8 +59,6 @@ export class Fork { for (const [source, value] of values) { source.v = value; } - - active_fork = null; }; } @@ -119,3 +123,7 @@ export class Fork { return active_fork; } } + +export function remove_active_fork() { + active_fork = null; +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 41d7810eb7d6..2c78e90fbe4c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -49,7 +49,7 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; -import { active_fork, Fork } from './reactivity/forks.js'; +import { active_fork, Fork, remove_active_fork } from './reactivity/forks.js'; import { log_effect_tree } from './dev/debug.js'; // Used for DEV time error handling @@ -670,7 +670,7 @@ function flush_queued_root_effects() { return; } - var revert = active_fork.apply(); + var fork = active_fork; try { var flush_count = 0; @@ -692,18 +692,19 @@ function flush_queued_root_effects() { root.f ^= CLEAN; } - process_effects(root, active_fork); + process_effects(root, fork); } } } finally { // TODO this doesn't seem quite right — may run into // interesting cases where there are multiple roots. // it'll do for now though - if (active_fork.settled()) { - active_fork.remove(); + if (fork.settled()) { + fork.remove(); } - revert(); + remove_active_fork(); + is_flushing = false; last_scheduled_effect = null; @@ -787,8 +788,13 @@ export function schedule_effect(signal) { * @param {Fork} fork */ function process_effects(effect, fork) { + var revert = fork.apply(); + var current_effect = effect.first; + /** @type {Effect[]} */ + var async_effects = []; + /** @type {Effect[]} */ var render_effects = []; @@ -807,7 +813,11 @@ function process_effects(effect, fork) { active_fork?.skipped_effects.has(current_effect); if (!skip) { - if ((flags & (BLOCK_EFFECT | EFFECT_ASYNC)) !== 0) { + if ((flags & EFFECT_ASYNC) !== 0) { + if (check_dirtiness(current_effect)) { + async_effects.push(current_effect); + } + } else if ((flags & BLOCK_EFFECT) !== 0) { if (check_dirtiness(current_effect)) { update_effect(current_effect); } @@ -849,10 +859,16 @@ function process_effects(effect, fork) { current_effect = sibling; } - if (fork.settled()) { + if (async_effects.length === 0 && fork.settled()) { flush_queued_effects(render_effects); flush_queued_effects(effects); } + + revert(); + + for (const effect of async_effects) { + update_effect(effect); + } } /** From c6d9110b78d9f4fb97392fe59871d997d7979eba Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 14:18:02 -0500 Subject: [PATCH 234/582] some progress --- .../src/internal/client/reactivity/forks.js | 8 ++--- .../svelte/src/internal/client/runtime.js | 31 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 322c678b6c8f..33a0c0225e94 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -10,6 +10,10 @@ const forks = new Set(); /** @type {Fork | null} */ export let active_fork = null; +export function remove_active_fork() { + active_fork = null; +} + let uid = 1; export class Fork { @@ -123,7 +127,3 @@ export class Fork { return active_fork; } } - -export function remove_active_fork() { - active_fork = null; -} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2c78e90fbe4c..9661fdbd2b5a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -696,15 +696,6 @@ function flush_queued_root_effects() { } } } finally { - // TODO this doesn't seem quite right — may run into - // interesting cases where there are multiple roots. - // it'll do for now though - if (fork.settled()) { - fork.remove(); - } - - remove_active_fork(); - is_flushing = false; last_scheduled_effect = null; @@ -759,7 +750,18 @@ function flush_queued_effects(effects) { export function schedule_effect(signal) { if (!is_flushing) { is_flushing = true; - queueMicrotask(flush_queued_root_effects); + queueMicrotask(() => { + flush_queued_root_effects(); + + // TODO this doesn't seem quite right — may run into + // interesting cases where there are multiple roots. + // it'll do for now though + if (active_fork?.settled()) { + active_fork.remove(); + } + + remove_active_fork(); + }); } var effect = (last_scheduled_effect = signal); @@ -895,6 +897,15 @@ export function flushSync(fn) { flush_tasks(); } + // TODO this doesn't seem quite right — may run into + // interesting cases where there are multiple roots. + // it'll do for now though + if (active_fork?.settled()) { + active_fork.remove(); + } + + remove_active_fork(); + return /** @type {T} */ (result); } From 29906c5b2aa44e5ee7d6de7dc8726779cb074ca9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 16:09:30 -0500 Subject: [PATCH 235/582] partial fix --- packages/svelte/src/internal/client/reactivity/deriveds.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 68804085ec70..12ea627461c3 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -137,8 +137,7 @@ export function async_derived(fn, location) { internal_set(signal, v); }); } else { - signal.v = v; - // internal_set(signal, v); + internal_set(signal, v); } if (DEV && location !== undefined) { From 8e90bb2f04211ad9ce0905ed83efca51b55dff74 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 16:10:04 -0500 Subject: [PATCH 236/582] remove unused test --- .../samples/async-pending-timeout/_config.js | 42 ------------------- .../samples/async-pending-timeout/main.svelte | 11 ----- 2 files changed, 53 deletions(-) delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-pending-timeout/_config.js delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-pending-timeout/main.svelte 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 deleted file mode 100644 index 857703c411c3..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/_config.js +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 3c6879caee08..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-pending-timeout/main.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - -

{await promise}

- - {#snippet pending()} -

pending

- {/snippet} -
From 14330bd770aebb1ad79fa88647440de76349dfd4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 16:55:07 -0500 Subject: [PATCH 237/582] add Promise.withResolvers shim for convenience --- playgrounds/sandbox/ssr-common.js | 11 +++++++++++ playgrounds/sandbox/ssr-dev.js | 1 + playgrounds/sandbox/ssr-prod.js | 1 + 3 files changed, 13 insertions(+) create mode 100644 playgrounds/sandbox/ssr-common.js diff --git a/playgrounds/sandbox/ssr-common.js b/playgrounds/sandbox/ssr-common.js new file mode 100644 index 000000000000..60c6b52eb1dc --- /dev/null +++ b/playgrounds/sandbox/ssr-common.js @@ -0,0 +1,11 @@ +Promise.withResolvers ??= () => { + let resolve; + let reject; + + const promise = new Promise((f, r) => { + resolve = f; + reject = r; + }); + + return { promise, resolve, reject }; +}; diff --git a/playgrounds/sandbox/ssr-dev.js b/playgrounds/sandbox/ssr-dev.js index 01ce14e2664d..e019b234a613 100644 --- a/playgrounds/sandbox/ssr-dev.js +++ b/playgrounds/sandbox/ssr-dev.js @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import polka from 'polka'; import { createServer as createViteServer } from 'vite'; import { render } from 'svelte/server'; +import './ssr-common.js'; const PORT = process.env.PORT || '5173'; diff --git a/playgrounds/sandbox/ssr-prod.js b/playgrounds/sandbox/ssr-prod.js index 1ed9435249ea..e8f74ee93ae7 100644 --- a/playgrounds/sandbox/ssr-prod.js +++ b/playgrounds/sandbox/ssr-prod.js @@ -3,6 +3,7 @@ import path from 'node:path'; import polka from 'polka'; import { render } from 'svelte/server'; import App from './src/App.svelte'; +import './ssr-common.js'; const { head, body } = render(App); From f77df36ff1e735b5422281ec37603556613a200c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 17:09:18 -0500 Subject: [PATCH 238/582] WIP --- .../internal/client/dom/blocks/boundary.js | 2 +- .../internal/client/reactivity/deriveds.js | 28 +++++++++++++++++-- .../samples/async-derived-module/_config.js | 1 + 3 files changed, 27 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 8d85b2442140..527d5e535fe4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -140,7 +140,7 @@ export class Boundary { // the pending or main block was rendered for a given // boundary, and hydrate accordingly queueMicrotask(() => { - destroy_effect(/** @type {Effect} */ (this.#pending_effect)); + // destroy_effect(/** @type {Effect} */ (this.#pending_effect)); this.#main_effect = this.#run(() => { return branch(() => this.#children(this.#anchor)); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 12ea627461c3..6e3d6f6f9c65 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -105,6 +105,12 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; + var boundary = /** @type {Effect} */ (active_effect).b; + + while (boundary !== null && !boundary.has_pending_snippet()) { + boundary = boundary.parent; + } + render_effect(() => { if (DEV) from_async_derived = active_effect; promise = fn(); @@ -115,8 +121,16 @@ export function async_derived(fn, location) { var fork = active_fork; if (should_suspend) { - // TODO if nearest pending boundary is not ready, attach to the boundary - fork?.increment(); + if (fork !== null) { + fork.increment(); + } else { + if (boundary === null) { + throw new Error('TODO'); + } + + // if nearest pending boundary is not ready, attach to the boundary + boundary.increment(); + } } promise.then( @@ -129,7 +143,15 @@ export function async_derived(fn, location) { from_async_derived = null; if (should_suspend) { - fork?.decrement(); + if (fork !== null) { + fork.decrement(); + } else { + if (boundary === null) { + throw new Error('TODO'); + } + + boundary.decrement(); + } } if (fork !== null) { 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..b8e7e9b84592 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 @@ -26,6 +26,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); flushSync(); await tick(); assert.htmlEqual(target.innerHTML, '

42

'); From 7e0fdb52618237559888e5e701e2ecc2b6edd49d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 17:14:33 -0500 Subject: [PATCH 239/582] update tests --- .../tests/runtime-runes/samples/async-derived-module/_config.js | 2 +- .../svelte/tests/runtime-runes/samples/async-derived/_config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 b8e7e9b84592..30adf19581ac 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 @@ -54,9 +54,9 @@ export default test({ '$effect.pre 42 1', 'template 42 1', '$effect 42 1', - 'outside boundary 2', '$effect.pre 84 2', 'template 84 2', + 'outside boundary 2', '$effect 84 2', '$effect.pre 86 2', 'template 86 2', 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..62aea02de35e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -51,9 +51,9 @@ export default test({ '$effect.pre 42 1', 'template 42 1', '$effect 42 1', - 'outside boundary 2', '$effect.pre 84 2', 'template 84 2', + 'outside boundary 2', '$effect 84 2', '$effect.pre 86 2', 'template 86 2', From ba68a937afd07820a341f3bbac3659cd01c90200 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 17:28:49 -0500 Subject: [PATCH 240/582] update test --- .../svelte/tests/runtime-legacy/shared.ts | 14 ++++++++++ .../runtime-runes/samples/async-if/_config.js | 28 +++++++++++++------ .../samples/async-if/main.svelte | 8 ++++-- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 2c6a55472785..17069a94babc 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -26,6 +26,20 @@ type Assert = typeof import('vitest').assert & { ): void; }; +// TODO remove this shim when we can +// @ts-expect-error +Promise.withResolvers = () => { + let resolve; + let reject; + + const promise = new Promise((f, r) => { + resolve = f; + reject = r; + }); + + return { promise, resolve, reject }; +}; + export interface RuntimeTest = Record> extends BaseTest { /** Use e.g. `mode: ['client']` to indicate that this test should never run in server/hydrate modes */ 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 991cebad3e99..0bf9152dca01 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -6,7 +6,7 @@ import { test } from '../../test'; let d; export default test({ - html: `

pending

`, + html: `

pending

`, get props() { d = deferred(); @@ -16,21 +16,31 @@ export default test({ }; }, - async test({ assert, target, component }) { - d.resolve(true); + async test({ assert, target }) { + const [reset, t, f] = target.querySelectorAll('button'); + + flushSync(() => t.click()); await Promise.resolve(); await Promise.resolve(); await tick(); flushSync(); - assert.htmlEqual(target.innerHTML, '

yes

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

yes

' + ); - d = deferred(); - component.promise = d.promise; + flushSync(() => reset.click()); await tick(); - assert.htmlEqual(target.innerHTML, '

yes

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

yes

' + ); - d.resolve(false); + flushSync(() => f.click()); await tick(); - assert.htmlEqual(target.innerHTML, '

no

'); + 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 index baed33a76e6f..21a4cbef97f2 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte @@ -1,9 +1,13 @@ + + + + - {#if await promise} + {#if await deferred.promise}

yes

{:else}

no

From fde316fcc844d180b8fcb5eb1117880757fb4fe6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 17:43:24 -0500 Subject: [PATCH 241/582] fix --- .../internal/client/dom/blocks/boundary.js | 42 ------------------- .../src/internal/client/dom/blocks/each.js | 5 ++- .../src/internal/client/dom/blocks/if.js | 5 ++- .../src/internal/client/dom/blocks/key.js | 5 ++- .../client/dom/blocks/svelte-component.js | 5 ++- .../src/internal/client/reactivity/forks.js | 14 +++++++ .../svelte/src/internal/client/runtime.js | 1 + 7 files changed, 27 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 527d5e535fe4..04ec7699a7e1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -79,15 +79,6 @@ export class Boundary { /** @type {Effect} */ #effect; - /** @type {Set<() => void>} */ - #callbacks = new Set(); - - /** @type {Effect[]} */ - #render_effects = []; - - /** @type {Effect[]} */ - #effects = []; - /** @type {Effect | null} */ #main_effect = null; @@ -230,16 +221,6 @@ export class Boundary { } } - /** @param {() => void} fn */ - add_callback(fn) { - this.#callbacks.add(fn); - } - - /** @param {Effect} effect */ - add_effect(effect) { - ((effect.f & RENDER_EFFECT) !== 0 ? this.#render_effects : this.#effects).push(effect); - } - commit() { if (this.#keep_pending_snippet || this.#pending_count > 0) { return; @@ -247,19 +228,6 @@ export class Boundary { this.suspended = false; - for (const e of this.#render_effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); - } - } catch (error) { - handle_error(error, e, null, e.ctx); - } - } - - for (const fn of this.#callbacks) fn(); - this.#callbacks.clear(); - if (this.#pending_effect) { pause_effect(this.#pending_effect, () => { this.#pending_effect = null; @@ -270,16 +238,6 @@ export class Boundary { this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } - - for (const e of this.#effects) { - try { - if (check_dirtiness(e)) { - update_effect(e); - } - } catch (error) { - handle_error(error, e, null, e.ctx); - } - } } increment() { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index ec97bb482872..67b16745da5c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -39,6 +39,7 @@ import { queue_micro_task } from '../task.js'; import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; +import { active_fork } from '../../reactivity/forks.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -267,7 +268,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f fallback = branch(() => fallback_fn(anchor)); } } else { - if (boundary !== null && should_defer_append()) { + if (active_fork !== null && should_defer_append()) { for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); @@ -298,7 +299,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } - boundary?.add_callback(commit); + active_fork?.add_callback(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index d8ad6f273af0..9c2f6f18a01e 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 { create_text, should_defer_append } from '../operations.js'; import { active_effect } from '../../runtime.js'; +import { active_fork } from '../../reactivity/forks.js'; /** * @param {TemplateNode} node @@ -109,7 +110,7 @@ export function if_block(node, fn, elseif = false) { } } - var defer = boundary !== null && should_defer_append(); + var defer = active_fork !== null && should_defer_append(); var target = anchor; if (defer) { @@ -122,7 +123,7 @@ export function if_block(node, fn, elseif = false) { } if (defer) { - boundary?.add_callback(commit); + active_fork?.add_callback(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 06e9ab73e030..30f211e603a8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -6,6 +6,7 @@ import { is_runes } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; import { active_effect } from '../../runtime.js'; +import { active_fork } from '../../reactivity/forks.js'; /** * @template V @@ -54,7 +55,7 @@ export function key_block(node, get_key, render_fn) { if (changed(key, (key = get_key()))) { var target = anchor; - var defer = boundary !== null && should_defer_append(); + var defer = active_fork !== null && should_defer_append(); if (defer) { offscreen_fragment = document.createDocumentFragment(); @@ -64,7 +65,7 @@ export function key_block(node, get_key, render_fn) { pending_effect = branch(() => render_fn(target)); if (defer) { - boundary?.add_callback(commit); + active_fork?.add_callback(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 9311fab62a53..0bbb25871fd7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,6 +1,7 @@ /** @import { TemplateNode, Dom, Effect } from '#client' */ import { EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; +import { active_fork } from '../../reactivity/forks.js'; import { active_effect } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; @@ -51,7 +52,7 @@ export function component(node, get_component, render_fn) { block(() => { if (component === (component = get_component())) return; - var defer = boundary !== null && should_defer_append(); + var defer = active_fork !== null && should_defer_append(); if (component) { var target = anchor; @@ -69,7 +70,7 @@ export function component(node, get_component, render_fn) { } if (defer) { - boundary?.add_callback(commit); + active_fork?.add_callback(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 33a0c0225e94..fee44526ec92 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -28,6 +28,9 @@ export class Fork { /** @type {Set} */ skipped_effects = new Set(); + /** @type {Set<() => void>} */ + #callbacks = new Set(); + #pending = 0; apply() { @@ -118,6 +121,17 @@ export class Fork { return this.#pending === 0; } + /** @param {() => void} fn */ + add_callback(fn) { + this.#callbacks.add(fn); + } + + commit() { + for (const fn of this.#callbacks) { + fn(); + } + } + static ensure() { if (active_fork === null) { active_fork = new Fork(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9661fdbd2b5a..88f0b5802795 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -862,6 +862,7 @@ function process_effects(effect, fork) { } if (async_effects.length === 0 && fork.settled()) { + fork.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); } From 807a585c904fbc0605a1575bb7151c666522ac16 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 17:46:53 -0500 Subject: [PATCH 242/582] tidy up --- .../2-analyze/visitors/SvelteBoundary.js | 2 +- .../internal/client/dom/blocks/boundary.js | 69 ++----------------- 2 files changed, 7 insertions(+), 64 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 04ec7699a7e1..87e0d388dd7e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,11 +1,6 @@ /** @import { Effect, TemplateNode, } from '#client' */ -import { - BOUNDARY_EFFECT, - EFFECT_PRESERVED, - EFFECT_TRANSPARENT, - RENDER_EFFECT -} from '../../constants.js'; +import { BOUNDARY_EFFECT, EFFECT_PRESERVED, 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'; import { @@ -14,10 +9,7 @@ import { handle_error, set_active_effect, set_active_reaction, - reset_is_throwing_error, - schedule_effect, - check_dirtiness, - update_effect + reset_is_throwing_error } from '../../runtime.js'; import { hydrate_next, @@ -32,16 +24,12 @@ 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'; /** * @typedef {{ * onerror?: (error: unknown, reset: () => void) => void; * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; * pending?: (anchor: Node) => void; - * showPendingAfter?: number; - * showPendingFor?: number; * }} BoundaryProps */ @@ -58,7 +46,6 @@ export function boundary(node, props, children) { } export class Boundary { - suspended = false; inert = false; /** @type {Boundary | null} */ @@ -92,7 +79,6 @@ export class Boundary { #offscreen_fragment = null; #pending_count = 0; - #keep_pending_snippet = false; // TODO get rid of this #is_creating_fallback = false; /** @@ -141,8 +127,7 @@ export class Boundary { this.#main_effect = branch(() => children(this.#anchor)); if (this.#pending_count > 0) { - this.suspended = true; - this.#show_pending_snippet(true); + this.#show_pending_snippet(); } } @@ -181,10 +166,7 @@ export class Boundary { } } - /** - * @param {boolean} initial - */ - #show_pending_snippet(initial) { + #show_pending_snippet() { const pending = this.#props.pending; if (pending !== undefined) { @@ -197,23 +179,6 @@ export class Boundary { if (this.#pending_effect === null) { this.#pending_effect = branch(() => pending(this.#anchor)); } - - // TODO do we want to differentiate between initial render and updates here? - if (!initial) { - this.#keep_pending_snippet = true; - - var end = raf.now() + (this.#props.showPendingFor ?? 300); - - loop((now) => { - if (now >= end) { - this.#keep_pending_snippet = false; - this.commit(); - return false; - } - - return true; - }); - } } else if (this.parent) { throw new Error('TODO show pending snippet on parent'); } else { @@ -222,12 +187,6 @@ export class Boundary { } commit() { - if (this.#keep_pending_snippet || this.#pending_count > 0) { - return; - } - - this.suspended = false; - if (this.#pending_effect) { pause_effect(this.#pending_effect, () => { this.#pending_effect = null; @@ -241,25 +200,11 @@ export class Boundary { } increment() { - // post-init, show the pending snippet after a timeout - if (!this.suspended && this.ran) { - var start = raf.now(); - var end = start + (this.#props.showPendingAfter ?? 500); - - loop((now) => { - if (this.#pending_count === 0) return false; - if (now < end) return true; - - this.#show_pending_snippet(false); - }); - } - - this.suspended = true; this.#pending_count++; } decrement() { - if (--this.#pending_count === 0 && !this.#keep_pending_snippet) { + if (--this.#pending_count === 0) { this.commit(); if (this.#main_effect !== null) { @@ -276,7 +221,6 @@ export class Boundary { const reset = () => { this.#pending_count = 0; - this.suspended = false; if (this.#failed_effect !== null) { pause_effect(this.#failed_effect, () => { @@ -295,8 +239,7 @@ export class Boundary { }); if (this.#pending_count > 0) { - this.suspended = true; - this.#show_pending_snippet(true); + this.#show_pending_snippet(); } }; From b0b37e6a84b3044df5aa25ec901db124905626eb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Feb 2025 19:15:59 -0500 Subject: [PATCH 243/582] partial fix --- packages/svelte/src/internal/client/runtime.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 88f0b5802795..30355210cf7c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -666,12 +666,6 @@ function infinite_loop_guard() { } function flush_queued_root_effects() { - if (active_fork === null) { - return; - } - - var fork = active_fork; - try { var flush_count = 0; @@ -692,7 +686,7 @@ function flush_queued_root_effects() { root.f ^= CLEAN; } - process_effects(root, fork); + process_effects(root, active_fork); } } } finally { @@ -787,10 +781,10 @@ export function schedule_effect(signal) { * effects to be flushed. * * @param {Effect} effect - * @param {Fork} fork + * @param {Fork | null} fork */ function process_effects(effect, fork) { - var revert = fork.apply(); + var revert = fork?.apply(); var current_effect = effect.first; @@ -861,13 +855,13 @@ function process_effects(effect, fork) { current_effect = sibling; } - if (async_effects.length === 0 && fork.settled()) { - fork.commit(); + if (async_effects.length === 0 && (fork === null || fork.settled())) { + fork?.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); } - revert(); + revert?.(); for (const effect of async_effects) { update_effect(effect); From 9e877be638b4b2881594ef659d465d128cee80a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Feb 2025 19:34:02 -0500 Subject: [PATCH 244/582] fix --- packages/svelte/src/internal/client/dom/blocks/each.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 67b16745da5c..063d251e16d7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -410,7 +410,7 @@ function reconcile( offscreen_items.delete(key); items.set(key, pending); - var next = prev && prev.next; + var next = prev ? prev.next : current; link(state, prev, pending); link(state, pending, next); From 57232ee364d7958bda7e071ff1cfd6d54735d14a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Feb 2025 19:44:40 -0500 Subject: [PATCH 245/582] fix --- .../svelte/src/internal/client/dom/blocks/svelte-component.js | 4 +--- packages/svelte/src/internal/client/reactivity/forks.js | 2 ++ 2 files changed, 3 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 0bbb25871fd7..337f192c29d8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -2,7 +2,6 @@ import { EFFECT_TRANSPARENT } from '../../constants.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { active_fork } from '../../reactivity/forks.js'; -import { active_effect } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; @@ -33,8 +32,6 @@ export function component(node, get_component, render_fn) { /** @type {Effect | null} */ var pending_effect = null; - var boundary = /** @type {Effect} */ (active_effect).b; - function commit() { if (effect) { pause_effect(effect); @@ -47,6 +44,7 @@ export function component(node, get_component, render_fn) { } effect = pending_effect; + pending_effect = null; } block(() => { diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index fee44526ec92..413815132d3a 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -130,6 +130,8 @@ export class Fork { for (const fn of this.#callbacks) { fn(); } + + this.#callbacks.clear(); } static ensure() { From 2b2cdf13c538639a29ad0eb4558ffcc3bce673a3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Feb 2025 19:52:26 -0500 Subject: [PATCH 246/582] fix --- packages/svelte/src/internal/client/dom/blocks/if.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 9c2f6f18a01e..e9974de3449a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -123,6 +123,12 @@ export function if_block(node, fn, elseif = false) { } if (defer) { + const skipped = condition ? alternate_effect : consequent_effect; + if (skipped !== null) { + // TODO need to do this for other kinds of blocks + active_fork?.skipped_effects.add(skipped); + } + active_fork?.add_callback(commit); target.remove(); } else { From 710ae6285a5af6139bc46ae42af09269caa8a09f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 25 Feb 2025 20:09:48 -0500 Subject: [PATCH 247/582] fix --- 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 30355210cf7c..4ec4a11be914 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -814,8 +814,12 @@ function process_effects(effect, fork) { async_effects.push(current_effect); } } else if ((flags & BLOCK_EFFECT) !== 0) { - if (check_dirtiness(current_effect)) { - update_effect(current_effect); + try { + if (check_dirtiness(current_effect)) { + update_effect(current_effect); + } + } catch (error) { + handle_error(error, current_effect, null, null); } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { From b18cd469825f280976c288b5862eaac2005c9ee8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 11:23:19 -0500 Subject: [PATCH 248/582] update tests --- .../samples/async-each-await-item/_config.js | 43 ++++++++----------- .../samples/async-each-await-item/main.svelte | 28 +++++++++++- 2 files changed, 45 insertions(+), 26 deletions(-) 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 dd6f228deb4e..52df1275a9de 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 @@ -1,42 +1,35 @@ 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()]; + html: `

pending

`, - return { - items - }; - }, + async test({ assert, target }) { + const [button1, button2, button3] = target.querySelectorAll('button'); - async test({ assert, target, component }) { - items[0].resolve('a'); - items[1].resolve('b'); - items[2].resolve('c'); + flushSync(() => button1.click()); await Promise.resolve(); await Promise.resolve(); await tick(); flushSync(); - assert.htmlEqual(target.innerHTML, '

a

b

c

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

a

b

c

' + ); - items = [deferred(), deferred(), deferred(), deferred()]; - component.items = items; + flushSync(() => button2.click()); await tick(); - assert.htmlEqual(target.innerHTML, '

a

b

c

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

a

b

c

' + ); - items[0].resolve('b'); - items[1].resolve('c'); - items[2].resolve('d'); - items[3].resolve('e'); + flushSync(() => button3.click()); await Promise.resolve(); await tick(); - assert.htmlEqual(target.innerHTML, '

b

c

d

e

'); + 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 index 204eb0d0c35a..eddcf2b749d7 100644 --- 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 @@ -1,7 +1,33 @@ + + + + + + {#each items as deferred}

{await deferred.promise}

From a5275b2405268b6225b222a849eccfedbc9065ae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 11:52:34 -0500 Subject: [PATCH 249/582] update test, remove unnecessary suspend --- .../src/internal/client/reactivity/effects.js | 3 -- .../samples/async-error/_config.js | 44 +++++++++---------- .../samples/async-error/main.svelte | 8 +++- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 3614acd874e3..0a66d76466dc 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -342,7 +342,6 @@ export function template_effect(fn, sync = [], async = [], d = derived) { if (async.length > 0) { var restore = capture(); - var unsuspend = suspend(); Promise.all(async.map((expression) => async_derived(expression))).then((result) => { restore(); @@ -352,8 +351,6 @@ export function template_effect(fn, sync = [], async = [], d = derived) { } create_template_effect(fn, [...sync.map(d), ...result]); - - unsuspend(); }); } else { create_template_effect(fn, sync.map(d)); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js index 9c7e296287f2..87e7764b3bc0 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -1,37 +1,33 @@ 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(); + html: `

pending

`, - return { - promise: d.promise - }; - }, + async test({ assert, target }) { + let [button1, button2, button3] = target.querySelectorAll('button'); - async test({ assert, target, component }) { - d.reject(new Error('oops!')); + flushSync(() => button1.click()); 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'); + assert.htmlEqual( + target.innerHTML, + '

oops!

' + ); + + flushSync(() => button2.click()); + assert.htmlEqual( + target.innerHTML, + '

pending

' + ); + + flushSync(() => button3.click()); await Promise.resolve(); await tick(); - assert.htmlEqual(target.innerHTML, '

wheee

'); + 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 index dd42fa759689..547255c4c4ae 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte @@ -1,9 +1,13 @@ + + + + -

{await promise}

+

{await deferred.promise}

{#snippet pending()}

pending

From 4e417e1ee2b5ae32cbf61e4b5dc1e8e643e89a9e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 16:35:58 -0500 Subject: [PATCH 250/582] fix --- .../src/internal/client/reactivity/forks.js | 2 -- .../src/internal/client/reactivity/sources.js | 5 ++- .../svelte/src/internal/client/runtime.js | 32 +++++++++++-------- .../samples/async-error/_config.js | 6 +++- .../samples/async-error/main.svelte | 2 +- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 413815132d3a..9c92f27f4f12 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,8 +1,6 @@ /** @import { Effect, Source } from '#client' */ import { noop } from '../../shared/utils.js'; -import { DIRTY } from '../constants.js'; import { flushSync } from '../runtime.js'; -import { internal_set, mark_reactions } from './sources.js'; /** @type {Set} */ const forks = new Set(); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 4bdd99260ccd..85736d001beb 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -14,7 +14,8 @@ import { derived_sources, set_derived_sources, check_dirtiness, - untracking + untracking, + queue_flush } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -221,6 +222,8 @@ export function internal_set(source, value) { inspect_effects.clear(); } + + queue_flush(); } return value; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4ec4a11be914..eef109b8a321 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -742,6 +742,24 @@ function flush_queued_effects(effects) { * @returns {void} */ export function schedule_effect(signal) { + queue_flush(); + + var effect = (last_scheduled_effect = signal); + + while (effect.parent !== null) { + effect = effect.parent; + var flags = effect.f; + + if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { + if ((flags & CLEAN) === 0) return; + effect.f ^= CLEAN; + } + } + + queued_root_effects.push(effect); +} + +export function queue_flush() { if (!is_flushing) { is_flushing = true; queueMicrotask(() => { @@ -757,20 +775,6 @@ export function schedule_effect(signal) { remove_active_fork(); }); } - - var effect = (last_scheduled_effect = signal); - - while (effect.parent !== null) { - effect = effect.parent; - var flags = effect.f; - - if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - if ((flags & CLEAN) === 0) return; - effect.f ^= CLEAN; - } - } - - queued_root_effects.push(effect); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js index 87e7764b3bc0..8f6975f6fb53 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -13,10 +13,14 @@ export default test({ flushSync(); assert.htmlEqual( target.innerHTML, - '

oops!

' + '

oops!

' ); flushSync(() => button2.click()); + + const reset = /** @type {HTMLButtonElement} */ (target.querySelector('[data-id="reset"]')); + flushSync(() => reset.click()); + assert.htmlEqual( target.innerHTML, '

pending

' diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte index 547255c4c4ae..9af5bbaa16a5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte @@ -15,6 +15,6 @@ {#snippet failed(error, reset)}

{error.message}

- + {/snippet}
From f90132c9164c589bbe74b7c317e57ebb092a1924 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 16:45:32 -0500 Subject: [PATCH 251/582] fix --- .../svelte/src/internal/client/dom/blocks/boundary.js | 11 +++++------ 1 file changed, 5 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 87e0d388dd7e..28a123b40cd3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -107,17 +107,16 @@ export class Boundary { if (hydrating && pending) { this.#pending_effect = branch(() => pending(this.#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 queueMicrotask(() => { - // destroy_effect(/** @type {Effect} */ (this.#pending_effect)); + if (this.#pending_count === 0) { + pause_effect(/** @type {Effect} */ (this.#pending_effect), () => { + this.#pending_effect = null; + }); + } this.#main_effect = this.#run(() => { return branch(() => this.#children(this.#anchor)); From ac3385715c0bd0941f3d364abf5ec6eca84db0da Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 16:54:25 -0500 Subject: [PATCH 252/582] fix --- .../svelte/src/internal/client/dom/blocks/boundary.js | 8 ++++---- 1 file changed, 4 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 28a123b40cd3..40e3d79b2bcd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -112,15 +112,15 @@ export class Boundary { // the pending or main block was rendered for a given // boundary, and hydrate accordingly queueMicrotask(() => { + this.#main_effect = this.#run(() => { + return branch(() => this.#children(this.#anchor)); + }); + if (this.#pending_count === 0) { pause_effect(/** @type {Effect} */ (this.#pending_effect), () => { this.#pending_effect = null; }); } - - this.#main_effect = this.#run(() => { - return branch(() => this.#children(this.#anchor)); - }); }); } else { this.#main_effect = branch(() => children(this.#anchor)); From 9b36b6be5354a4b0648f5336cadfe09f6509588f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 22:20:00 -0500 Subject: [PATCH 253/582] add callsite to effect tree logs --- packages/svelte/src/internal/client/dev/debug.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index b65f79697c62..810fb39378ab 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -29,7 +29,7 @@ export function root(effect) { * * @param {Effect} effect */ -export function log_effect_tree(effect) { +export function log_effect_tree(effect, depth = 0) { const flags = effect.f; let label = '(unknown)'; @@ -55,6 +55,14 @@ export function log_effect_tree(effect) { console.group(`%c${label} (${status})`, `font-weight: ${status === 'clean' ? 'normal' : 'bold'}`); + if (depth === 0) { + const callsite = new Error().stack + ?.split('\n')[2] + .replace(/\s+at (?: \w+\(?)?(.+)\)?/, (m, $1) => $1.replace(/\?[^:]+/, '')); + + console.log(callsite); + } + if (effect.deps !== null) { console.groupCollapsed('%cdeps', 'font-weight: normal'); for (const dep of effect.deps) { @@ -65,7 +73,7 @@ export function log_effect_tree(effect) { let child = effect.first; while (child !== null) { - log_effect_tree(child); + log_effect_tree(child, depth + 1); child = child.next; } From 3c350dbb941bf54f6f58bb2820e76379f8040655 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 22:29:36 -0500 Subject: [PATCH 254/582] fix --- .../src/internal/client/reactivity/effects.js | 19 ++++++++++++++----- .../svelte/src/internal/client/runtime.js | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 0a66d76466dc..3ffa558a08ab 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -12,7 +12,8 @@ import { set_is_destroying_effect, set_signal_status, untrack, - untracking + untracking, + flushSync } from '../runtime.js'; import { DIRTY, @@ -41,6 +42,7 @@ import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; import { capture, suspend } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; +import { active_fork } from './forks.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -338,19 +340,26 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => Promise>} async */ export function template_effect(fn, sync = [], async = [], d = derived) { - let effect = /** @type {Effect} */ (active_effect); + var parent = /** @type {Effect} */ (active_effect); if (async.length > 0) { + var fork = active_fork; var restore = capture(); Promise.all(async.map((expression) => async_derived(expression))).then((result) => { restore(); - if ((effect.f & DESTROYED) !== 0) { + if ((parent.f & DESTROYED) !== 0) { return; } - create_template_effect(fn, [...sync.map(d), ...result]); + var effect = create_template_effect(fn, [...sync.map(d), ...result]); + + if (fork !== null) { + fork.run(() => { + schedule_effect(effect); + }); + } }); } else { create_template_effect(fn, sync.map(d)); @@ -370,7 +379,7 @@ function create_template_effect(fn, deriveds) { }); } - create_effect(RENDER_EFFECT, effect, true); + return 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 eef109b8a321..b8f05bb85076 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -751,12 +751,20 @@ export function schedule_effect(signal) { var flags = effect.f; if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - if ((flags & CLEAN) === 0) return; - effect.f ^= CLEAN; + // TODO reinstate this + // if ((flags & CLEAN) === 0) return; + // effect.f ^= CLEAN; + + if ((flags & CLEAN) !== 0) { + effect.f ^= CLEAN; + } } } - queued_root_effects.push(effect); + // TODO reinstate early bail-out when traversing up the graph + if (!queued_root_effects.includes(effect)) { + queued_root_effects.push(effect); + } } export function queue_flush() { @@ -827,7 +835,8 @@ function process_effects(effect, fork) { } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { - current_effect.f ^= CLEAN; + // TODO clean branch later, if fork is settled + // current_effect.f ^= CLEAN; } else { render_effects.push(current_effect); } @@ -848,6 +857,7 @@ function process_effects(effect, fork) { while (parent !== null) { if (effect === parent) { + // TODO is this still necessary? break main_loop; } From 8a96f23883c626be344495dcb4bd3db1446341d6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 22:30:21 -0500 Subject: [PATCH 255/582] tidy --- .../svelte/src/internal/client/runtime.js | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b8f05bb85076..cb42484e890b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -792,13 +792,13 @@ export function queue_flush() { * bitwise flag passed in only. The collected effects array will be populated with all the user * effects to be flushed. * - * @param {Effect} effect + * @param {Effect} root * @param {Fork | null} fork */ -function process_effects(effect, fork) { +function process_effects(root, fork) { var revert = fork?.apply(); - var current_effect = effect.first; + var effect = root.first; /** @type {Effect[]} */ var async_effects = []; @@ -809,68 +809,66 @@ function process_effects(effect, fork) { /** @type {Effect[]} */ var effects = []; - main_loop: while (current_effect !== null) { - var flags = current_effect.f; + main_loop: while (effect !== null) { + var flags = effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - var sibling = current_effect.next; + var sibling = effect.next; var skip = - is_skippable_branch || - (flags & INERT) !== 0 || - active_fork?.skipped_effects.has(current_effect); + is_skippable_branch || (flags & INERT) !== 0 || active_fork?.skipped_effects.has(effect); if (!skip) { if ((flags & EFFECT_ASYNC) !== 0) { - if (check_dirtiness(current_effect)) { - async_effects.push(current_effect); + if (check_dirtiness(effect)) { + async_effects.push(effect); } } else if ((flags & BLOCK_EFFECT) !== 0) { try { - if (check_dirtiness(current_effect)) { - update_effect(current_effect); + if (check_dirtiness(effect)) { + update_effect(effect); } } catch (error) { - handle_error(error, current_effect, null, null); + handle_error(error, effect, null, null); } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { // TODO clean branch later, if fork is settled // current_effect.f ^= CLEAN; } else { - render_effects.push(current_effect); + render_effects.push(effect); } } else if ((flags & EFFECT) !== 0) { - effects.push(current_effect); + effects.push(effect); } - var child = current_effect.first; + var child = effect.first; if (child !== null) { - current_effect = child; + effect = child; continue; } } if (sibling === null) { - let parent = current_effect.parent; + let parent = effect.parent; while (parent !== null) { - if (effect === parent) { + if (root === parent) { // TODO is this still necessary? break main_loop; } var parent_sibling = parent.next; if (parent_sibling !== null) { - current_effect = parent_sibling; + effect = parent_sibling; continue main_loop; } parent = parent.parent; } } - current_effect = sibling; + effect = sibling; } if (async_effects.length === 0 && (fork === null || fork.settled())) { From eb8c8e62e7d71b7f988663f06ac9cf47ac3f15f9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 22:32:22 -0500 Subject: [PATCH 256/582] simplify --- packages/svelte/src/internal/client/runtime.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index cb42484e890b..50d99428b5fb 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -809,7 +809,7 @@ function process_effects(root, fork) { /** @type {Effect[]} */ var effects = []; - main_loop: while (effect !== null) { + while (effect !== null) { var flags = effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; @@ -854,15 +854,10 @@ function process_effects(root, fork) { let parent = effect.parent; while (parent !== null) { - if (root === parent) { - // TODO is this still necessary? - break main_loop; - } - var parent_sibling = parent.next; if (parent_sibling !== null) { effect = parent_sibling; - continue main_loop; + break; } parent = parent.parent; } From 52d4ade90f0ae713885a0a391d75835447f48655 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 26 Feb 2025 22:38:50 -0500 Subject: [PATCH 257/582] simplify --- packages/svelte/src/internal/client/runtime.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 50d99428b5fb..e5b4d8296a4e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -813,7 +813,6 @@ function process_effects(root, fork) { var flags = effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - var sibling = effect.next; var skip = is_skippable_branch || (flags & INERT) !== 0 || active_fork?.skipped_effects.has(effect); @@ -850,20 +849,13 @@ function process_effects(root, fork) { } } - if (sibling === null) { - let parent = effect.parent; + var parent = effect.parent; + effect = effect.next; - while (parent !== null) { - var parent_sibling = parent.next; - if (parent_sibling !== null) { - effect = parent_sibling; - break; - } - parent = parent.parent; - } + while (effect === null && parent !== null) { + effect = parent.next; + parent = parent.parent; } - - effect = sibling; } if (async_effects.length === 0 && (fork === null || fork.settled())) { From 47a1693578c5a91c8eca527db5be9fd6433db4c0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 27 Feb 2025 06:40:50 -0500 Subject: [PATCH 258/582] fix --- packages/svelte/src/internal/client/dom/blocks/each.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 063d251e16d7..d7c53a02480e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -269,6 +269,8 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } else { if (active_fork !== null && should_defer_append()) { + var keys = new Set(); + for (i = 0; i < length; i += 1) { value = array[i]; key = get_key(value, i); @@ -297,6 +299,14 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f offscreen_items.set(key, item); } + + keys.add(key); + } + + for (const [key, item] of state.items) { + if (!keys.has(key)) { + active_fork.skipped_effects.add(item.e); + } } active_fork?.add_callback(commit); From 94da28f97c20692cf2115351ff6695e46d3e10a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 27 Feb 2025 07:47:20 -0500 Subject: [PATCH 259/582] skip test --- .../samples/lifecycle-render-beforeUpdate/_config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js index 98eb7716fb5c..7c2008168b40 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js @@ -2,6 +2,12 @@ import { test } from '../../test'; import { flushSync } from 'svelte'; export default test({ + // this test breaks because of the changes required to make async work + // (namely, running blocks before other render effects including + // beforeUpdate and $effect.pre). Not sure if there's a good + // solution. We may be forced to release 6.0 + skip: true, + async test({ assert, target, logs }) { const input = /** @type {HTMLInputElement} */ (target.querySelector('input')); assert.equal(input?.value, 'rich'); From ee71311e9ddfd4ffa8bb1b00c90581218d339b3e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 27 Feb 2025 20:33:26 -0500 Subject: [PATCH 260/582] fix --- .../svelte/src/internal/client/dom/blocks/async.js | 14 ++++++++++---- .../svelte/src/internal/client/dom/blocks/if.js | 6 ++---- .../src/internal/client/reactivity/effects.js | 5 ++--- .../svelte/src/internal/client/reactivity/forks.js | 8 ++++---- packages/svelte/src/internal/client/runtime.js | 6 +++--- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 19527283a177..8d92cc30edf2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,7 +1,9 @@ -/** @import { TemplateNode, Value } from '#client' */ +/** @import { Effect, TemplateNode, Value } from '#client' */ import { async_derived } from '../../reactivity/deriveds.js'; -import { capture, suspend } from './boundary.js'; +import { active_fork } from '../../reactivity/forks.js'; +import { active_effect, schedule_effect } from '../../runtime.js'; +import { capture } from './boundary.js'; /** * @param {TemplateNode} node @@ -11,12 +13,16 @@ import { capture, suspend } from './boundary.js'; export function async(node, expressions, fn) { // TODO handle hydration + var fork = active_fork; + var effect = /** @type {Effect} */ (active_effect); var restore = capture(); - var unsuspend = suspend(); Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { restore(); fn(node, ...result); - unsuspend(); + + fork?.run(() => { + schedule_effect(effect); + }); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index e9974de3449a..49261611eb1d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -11,7 +11,6 @@ import { import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { create_text, should_defer_append } from '../operations.js'; -import { active_effect } from '../../runtime.js'; import { active_fork } from '../../reactivity/forks.js'; /** @@ -51,10 +50,10 @@ export function if_block(node, fn, elseif = false) { /** @type {Effect | null} */ var pending_effect = null; - var boundary = /** @type {Effect} */ (active_effect).b; - function commit() { if (offscreen_fragment !== null) { + // remove the anchor + /** @type {Text} */ (offscreen_fragment.lastChild).remove(); anchor.before(offscreen_fragment); offscreen_fragment = null; } @@ -130,7 +129,6 @@ export function if_block(node, fn, elseif = false) { } active_fork?.add_callback(commit); - target.remove(); } else { commit(); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 3ffa558a08ab..214425215c97 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -12,8 +12,7 @@ import { set_is_destroying_effect, set_signal_status, untrack, - untracking, - flushSync + untracking } from '../runtime.js'; import { DIRTY, @@ -40,7 +39,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; -import { capture, suspend } from '../dom/blocks/boundary.js'; +import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; import { active_fork } from './forks.js'; diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 9c92f27f4f12..19894db94f06 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -29,7 +29,7 @@ export class Fork { /** @type {Set<() => void>} */ #callbacks = new Set(); - #pending = 0; + pending = 0; apply() { if (forks.size === 1) { @@ -108,15 +108,15 @@ export class Fork { } increment() { - this.#pending += 1; + this.pending += 1; } decrement() { - this.#pending -= 1; + this.pending -= 1; } settled() { - return this.#pending === 0; + return this.pending === 0; } /** @param {() => void} fn */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e5b4d8296a4e..40a10299a54c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -776,7 +776,7 @@ export function queue_flush() { // TODO this doesn't seem quite right — may run into // interesting cases where there are multiple roots. // it'll do for now though - if (active_fork?.settled()) { + if (active_fork?.pending === 0) { active_fork.remove(); } @@ -858,7 +858,7 @@ function process_effects(root, fork) { } } - if (async_effects.length === 0 && (fork === null || fork.settled())) { + if (async_effects.length === 0 && (fork === null || fork.pending === 0)) { fork?.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -898,7 +898,7 @@ export function flushSync(fn) { // TODO this doesn't seem quite right — may run into // interesting cases where there are multiple roots. // it'll do for now though - if (active_fork?.settled()) { + if (active_fork?.pending === 0) { active_fork.remove(); } From a0a4d4f5985e91d64df2c2ac889f01c37afa0b45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 27 Feb 2025 21:24:22 -0500 Subject: [PATCH 261/582] fix --- packages/svelte/src/internal/client/dom/blocks/each.js | 2 -- packages/svelte/src/internal/client/dom/blocks/if.js | 1 + packages/svelte/src/internal/client/dom/blocks/key.js | 7 +++---- .../src/internal/client/dom/blocks/svelte-component.js | 7 +++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index d7c53a02480e..c7f7df218c36 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -139,8 +139,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var was_empty = false; - var boundary = /** @type {Effect} */ (active_effect).b; - /** @type {Map} */ var offscreen_items = new Map(); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 49261611eb1d..43971b79aebb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -54,6 +54,7 @@ export function if_block(node, fn, elseif = false) { if (offscreen_fragment !== null) { // remove the anchor /** @type {Text} */ (offscreen_fragment.lastChild).remove(); + anchor.before(offscreen_fragment); offscreen_fragment = null; } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 30f211e603a8..021d9dec9e5e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -5,7 +5,6 @@ import { not_equal, safe_not_equal } from '../../reactivity/equality.js'; import { is_runes } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; -import { active_effect } from '../../runtime.js'; import { active_fork } from '../../reactivity/forks.js'; /** @@ -34,8 +33,6 @@ export function key_block(node, get_key, render_fn) { /** @type {DocumentFragment | null} */ var offscreen_fragment = null; - var boundary = /** @type {Effect} */ (active_effect).b; - var changed = is_runes() ? not_equal : safe_not_equal; function commit() { @@ -44,6 +41,9 @@ export function key_block(node, get_key, render_fn) { } if (offscreen_fragment !== null) { + // remove the anchor + /** @type {Text} */ (offscreen_fragment.lastChild).remove(); + anchor.before(offscreen_fragment); offscreen_fragment = null; } @@ -66,7 +66,6 @@ export function key_block(node, get_key, render_fn) { if (defer) { active_fork?.add_callback(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 337f192c29d8..cd52950598b6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -39,6 +39,9 @@ export function component(node, get_component, render_fn) { } if (offscreen_fragment) { + // remove the anchor + /** @type {Text} */ (offscreen_fragment.lastChild).remove(); + anchor.before(offscreen_fragment); offscreen_fragment = null; } @@ -61,10 +64,6 @@ export function component(node, get_component, render_fn) { } pending_effect = branch(() => render_fn(target, component)); - - if (defer) { - target.remove(); - } } if (defer) { From 31882d1d2d948611d6826b5e9bf55a1d7fad6aa0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 28 Feb 2025 08:28:27 -0500 Subject: [PATCH 262/582] add `$effect.pending()` --- .../3-transform/client/visitors/CallExpression.js | 3 +++ .../3-transform/server/visitors/CallExpression.js | 4 ++++ packages/svelte/src/internal/client/index.js | 1 + .../svelte/src/internal/client/reactivity/forks.js | 13 +++++++++++++ packages/svelte/src/utils.js | 1 + 5 files changed, 22 insertions(+) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 7a3057451aa1..e7e20dc1504d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -30,6 +30,9 @@ export function CallExpression(node, context) { .../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg))) ); + case '$effect.pending': + return b.call('$.get', b.id('$.pending')); + case '$inspect': case '$inspect().with': return transform_inspect_rune(node, context); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 386c6b6ff393..727947be8963 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -25,6 +25,10 @@ export function CallExpression(node, context) { return b.arrow([], b.block([])); } + if (rune === '$effect.pending') { + return b.false; + } + if (rune === '$state.snapshot') { return b.call( '$.snapshot', diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index a20e1f67dc70..fea7ac1ada59 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -115,6 +115,7 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; +export { pending } from './reactivity/forks.js'; export { mutable_state, mutate, set, state, update, update_pre } from './reactivity/sources.js'; export { prop, diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 19894db94f06..1abefbfe349b 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,6 +1,7 @@ /** @import { Effect, Source } from '#client' */ import { noop } from '../../shared/utils.js'; import { flushSync } from '../runtime.js'; +import { internal_set, source } from './sources.js'; /** @type {Set} */ const forks = new Set(); @@ -12,6 +13,12 @@ export function remove_active_fork() { active_fork = null; } +export let pending = source(false); + +function update_pending() { + internal_set(pending, forks.size > 0); +} + let uid = 1; export class Fork { @@ -97,6 +104,8 @@ export class Fork { } } } + + update_pending(); } /** @@ -134,6 +143,10 @@ export class Fork { static ensure() { if (active_fork === null) { + if (forks.size === 0) { + requestAnimationFrame(update_pending); + } + active_fork = new Fork(); forks.add(active_fork); // TODO figure out where we remove this } diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index d4d106d56deb..bce4e091e2a6 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -441,6 +441,7 @@ const RUNES = /** @type {const} */ ([ '$effect.pre', '$effect.tracking', '$effect.root', + '$effect.pending', '$inspect', '$inspect().with', '$inspect.trace', From 49480f0b6945d6c8af5e927e3b1b74caf8a5a39e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 28 Feb 2025 08:38:27 -0500 Subject: [PATCH 263/582] try this --- .../compiler/phases/2-analyze/visitors/CallExpression.js | 7 +++++++ 1 file changed, 7 insertions(+) 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 481a836f9493..c5cb2ad43a26 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -151,6 +151,13 @@ export function CallExpression(node, context) { break; + case '$effect.pending': + if (context.state.expression) { + context.state.expression.has_state = true; + } + + break; + case '$inspect': if (node.arguments.length < 1) { e.rune_invalid_arguments_length(node, rune, 'one or more arguments'); From 5bcdb13f26929dd145a19ff369b3632ad90bbbac Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 28 Feb 2025 17:23:05 -0500 Subject: [PATCH 264/582] fix --- packages/svelte/src/internal/client/index.js | 11 +++++++++-- .../svelte/src/internal/client/reactivity/forks.js | 7 +++---- .../svelte/src/internal/client/reactivity/sources.js | 2 ++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index fea7ac1ada59..692373d21a66 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -115,8 +115,15 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { pending } from './reactivity/forks.js'; -export { mutable_state, mutate, set, state, update, update_pre } from './reactivity/sources.js'; +export { + mutable_state, + mutate, + pending, + set, + state, + update, + update_pre +} from './reactivity/sources.js'; export { prop, rest_props, diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 1abefbfe349b..6c4705b9347c 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,7 +1,8 @@ /** @import { Effect, Source } from '#client' */ import { noop } from '../../shared/utils.js'; import { flushSync } from '../runtime.js'; -import { internal_set, source } from './sources.js'; +import { raf } from '../timing.js'; +import { internal_set, pending } from './sources.js'; /** @type {Set} */ const forks = new Set(); @@ -13,8 +14,6 @@ export function remove_active_fork() { active_fork = null; } -export let pending = source(false); - function update_pending() { internal_set(pending, forks.size > 0); } @@ -144,7 +143,7 @@ export class Fork { static ensure() { if (active_fork === null) { if (forks.size === 0) { - requestAnimationFrame(update_pending); + raf.tick(update_pending); } active_fork = new Fork(); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 85736d001beb..5b0802828846 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -38,6 +38,8 @@ import { active_fork, Fork } from './forks.js'; export let inspect_effects = new Set(); +export let pending = source(false); + /** * @param {Set} v */ From 3decb679071bf08d999fe4d3fdab226d662f31ca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Apr 2025 16:50:01 -0400 Subject: [PATCH 265/582] add TODO --- packages/svelte/src/internal/client/dom/blocks/if.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 43971b79aebb..16ef6fb18385 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -13,6 +13,8 @@ import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { create_text, should_defer_append } from '../operations.js'; import { active_fork } from '../../reactivity/forks.js'; +// TODO reinstate https://github.com/sveltejs/svelte/pull/15250 + /** * @param {TemplateNode} node * @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn From 90cdc16de2acdf5141ac533163f0d47ee0461929 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Apr 2025 17:23:54 -0400 Subject: [PATCH 266/582] align with main --- 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 8098296203d1..dd2fb0dee659 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -855,7 +855,7 @@ function process_effects(root, fork) { update_effect(effect); } } catch (error) { - handle_error(error, effect, null, null); + handle_error(error, effect, null, effect.ctx); } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { From 02efac920349cda663047ede9f03203b3b4fb8ad Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Apr 2025 18:04:13 -0400 Subject: [PATCH 267/582] fix --- .../svelte/src/internal/client/runtime.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index dd2fb0dee659..727ed8bb06b1 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -258,19 +258,21 @@ export function check_dirtiness(reaction) { * @param {Effect} effect */ function propagate_error(error, effect) { - var boundary = effect.b; + /** @type {Effect | null} */ + var current = effect; - while (boundary !== null) { - if (!boundary.inert) { + while (current !== null) { + if ((current.f & BOUNDARY_EFFECT) !== 0) { try { - boundary.error(error); + /** @type {Boundary} */ (current.b).error(error); return; } catch { - boundary.inert = true; + // Remove boundary flag from effect + current.f ^= BOUNDARY_EFFECT; } } - boundary = boundary.parent; + current = current.parent; } is_throwing_error = false; @@ -281,7 +283,10 @@ function propagate_error(error, effect) { * @param {Effect} effect */ function should_rethrow_error(effect) { - return (effect.f & DESTROYED) === 0 && (effect.parent === null || !effect.b || effect.b.inert); + return ( + (effect.f & DESTROYED) === 0 && + (effect.parent === null || (effect.parent.f & BOUNDARY_EFFECT) === 0) + ); } export function reset_is_throwing_error() { From 888fc31f71ed84dbfbd24d1b05dc0898df6b73c2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Apr 2025 09:18:06 -0400 Subject: [PATCH 268/582] is_async -> has_await --- .../src/compiler/phases/2-analyze/index.js | 17 +++++++++++------ .../2-analyze/visitors/AwaitExpression.js | 2 +- .../phases/2-analyze/visitors/CallExpression.js | 2 +- .../phases/2-analyze/visitors/StyleDirective.js | 2 +- .../3-transform/client/transform-client.js | 6 +++--- .../client/visitors/BlockStatement.js | 2 +- .../3-transform/client/visitors/EachBlock.js | 8 ++++---- .../3-transform/client/visitors/HtmlTag.js | 6 +++--- .../3-transform/client/visitors/IfBlock.js | 6 +++--- .../3-transform/client/visitors/KeyBlock.js | 2 +- .../client/visitors/RegularElement.js | 12 ++++++------ .../3-transform/client/visitors/RenderTag.js | 6 +++--- .../client/visitors/SvelteElement.js | 6 +++--- .../client/visitors/shared/component.js | 14 +++++++------- .../client/visitors/shared/element.js | 16 ++++++++-------- .../3-transform/client/visitors/shared/utils.js | 10 +++++----- packages/svelte/src/compiler/phases/nodes.js | 2 +- packages/svelte/src/compiler/phases/scope.js | 6 +++--- packages/svelte/src/compiler/phases/types.d.ts | 2 +- packages/svelte/src/compiler/types/index.d.ts | 2 +- 20 files changed, 67 insertions(+), 62 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ba46c45cccb4..0950c818812b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -203,9 +203,14 @@ function js(script, root, allow_reactive_declarations, parent) { body: [] }; - const { scope, scopes, is_async } = create_scopes(ast, root, allow_reactive_declarations, parent); + const { scope, scopes, has_await } = create_scopes( + ast, + root, + allow_reactive_declarations, + parent + ); - return { ast, scope, scopes, is_async }; + return { ast, scope, scopes, has_await }; } /** @@ -230,7 +235,7 @@ const RESERVED = ['$$props', '$$restProps', '$$slots']; * @returns {Analysis} */ export function analyze_module(ast, options) { - const { scope, scopes, is_async } = create_scopes(ast, new ScopeRoot(), false, null); + const { scope, scopes, has_await } = create_scopes(ast, new ScopeRoot(), false, null); for (const [name, references] of scope.references) { if (name[0] !== '$' || RESERVED.includes(name)) continue; @@ -247,7 +252,7 @@ export function analyze_module(ast, options) { /** @type {Analysis} */ const analysis = { - module: { ast, scope, scopes, is_async }, + module: { ast, scope, scopes, has_await }, name: options.filename, accessors: false, runes: true, @@ -293,7 +298,7 @@ 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, is_async } = create_scopes( + const { scope, scopes, has_await } = create_scopes( root.fragment, scope_root, false, @@ -408,7 +413,7 @@ export function analyze_component(root, source, options) { const runes = options.runes ?? - (is_async || instance.is_async || Array.from(module.scope.references.keys()).some(is_rune)); + (has_await || instance.has_await || 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/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 5e7710f802b4..8f195f01598b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -12,7 +12,7 @@ export function AwaitExpression(node, context) { let preserve_context = tla; if (context.state.expression) { - context.state.expression.is_async = true; + context.state.expression.has_await = true; suspend = true; // wrap the expression in `(await $.save(...)).restore()` if necessary, 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 662e82ddaffb..149ff38e1397 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -242,7 +242,7 @@ export function CallExpression(node, context) { expression }); - if (expression.is_async) { + if (expression.has_await) { context.state.analysis.async_deriveds.add(node); } } else if (rune === '$inspect') { 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 91b13acd4e0d..9699d3c03b4a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/StyleDirective.js @@ -32,7 +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; + node.metadata.expression.has_await ||= chunk.metadata.expression.has_await; } } } 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 61de8a71eb1b..56eddb9bcb59 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 @@ -369,7 +369,7 @@ export function client_component(analysis, options) { : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) ]); - if (analysis.instance.is_async) { + if (analysis.instance.has_await) { const body = b.function_declaration( b.id('$$body'), [b.id('$$anchor'), b.id('$$props')], @@ -379,9 +379,9 @@ export function client_component(analysis, options) { b.if(b.call('$.aborted'), b.return()), .../** @type {ESTree.Statement[]} */ (template.body), b.stmt(b.call('$$unsuspend')) - ]) + ]), + true ); - body.async = true; state.hoisted.push(body); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js index 5bfc8a3ef999..4d2d385702d1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js @@ -1,4 +1,4 @@ -/** @import { ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */ +/** @import { ArrowFunctionExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Statement } from 'estree' */ /** @import { ComponentContext } from '../types' */ import { add_state_transformers } from './shared/declarations.js'; import * as b from '../../../../utils/builders.js'; 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 d52fdcc182db..c0fa316f59d1 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 @@ -293,9 +293,9 @@ export function EachBlock(node, context) { ); } - const { is_async } = node.metadata.expression; + const { has_await } = node.metadata.expression; - const thunk = b.thunk(collection, is_async); + const thunk = b.thunk(collection, has_await); const render_args = [b.id('$$anchor'), item]; if (uses_index || collection_id) render_args.push(index); @@ -305,7 +305,7 @@ export function EachBlock(node, context) { const args = [ context.state.node, b.literal(flags), - is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : thunk, + has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : thunk, key_function, b.arrow(render_args, b.block(declarations.concat(block.body))) ]; @@ -316,7 +316,7 @@ export function EachBlock(node, context) { ); } - if (is_async) { + if (has_await) { context.state.init.push( b.stmt( b.call( 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 31f81310384e..4f6b255bb264 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,10 +11,10 @@ import * as b from '../../../../utils/builders.js'; export function HtmlTag(node, context) { context.state.template.push(''); - const { is_async } = node.metadata.expression; + const { has_await } = node.metadata.expression; const expression = /** @type {Expression} */ (context.visit(node.expression)); - const html = is_async ? b.call('$.get', b.id('$$html')) : expression; + const html = has_await ? b.call('$.get', b.id('$$html')) : expression; const is_svg = context.state.metadata.namespace === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; @@ -31,7 +31,7 @@ export function HtmlTag(node, context) { ); // push into init, so that bindings run afterwards, which might trigger another run and override hydration - if (node.metadata.expression.is_async) { + if (node.metadata.expression.has_await) { context.state.init.push( b.stmt( b.call( 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 d05e4857c260..18434fcd2984 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,10 +24,10 @@ 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 { has_await } = node.metadata.expression; const expression = /** @type {Expression} */ (context.visit(node.test)); - const test = is_async ? b.call('$.get', b.id('$$condition')) : expression; + const test = has_await ? b.call('$.get', b.id('$$condition')) : expression; /** @type {Expression[]} */ const args = [ @@ -79,7 +79,7 @@ export function IfBlock(node, context) { statements.push(b.stmt(b.call('$.if', ...args))); - if (is_async) { + if (has_await) { context.state.init.push( b.stmt( b.call( 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 6a95a94ddf11..811b25f10590 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,7 @@ export function KeyBlock(node, context) { const key = /** @type {Expression} */ (context.visit(node.expression)); const body = /** @type {Expression} */ (context.visit(node.fragment)); - if (node.metadata.expression.is_async) { + if (node.metadata.expression.has_await) { context.state.init.push( b.stmt( b.call( 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 a9a92823652f..965f4c0a25fb 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 @@ -311,7 +311,7 @@ export function RegularElement(node, context) { (value, metadata) => metadata.has_call ? get_expression_id( - metadata.is_async ? context.state.async_expressions : context.state.expressions, + metadata.has_await ? context.state.async_expressions : context.state.expressions, value ) : value @@ -386,7 +386,7 @@ export function RegularElement(node, context) { trimmed.every( (node) => node.type === 'Text' || - (!node.metadata.expression.has_state && !node.metadata.expression.is_async) + (!node.metadata.expression.has_state && !node.metadata.expression.has_await) ) && trimmed.some((node) => node.type === 'ExpressionTag'); @@ -532,7 +532,7 @@ export function build_class_directives_object(class_directives, context) { const expression = /** @type Expression */ (context.visit(d.expression)); properties.push(b.init(d.name, expression)); has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_async ||= d.metadata.expression.is_async; + has_async ||= d.metadata.expression.has_await; } const directives = b.object(properties); @@ -561,7 +561,7 @@ export function build_style_directives_object(style_directives, context) { : build_attribute_value(directive.value, context, (value, metadata) => metadata.has_call ? get_expression_id( - metadata.is_async ? context.state.async_expressions : context.state.expressions, + metadata.has_await ? context.state.async_expressions : context.state.expressions, value ) : value @@ -699,11 +699,11 @@ function build_element_special_value_attribute(element, node_id, attribute, cont element === 'select' && attribute.value !== true && !is_text_attribute(attribute); const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.is_async + metadata.has_call || metadata.has_await ? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately is_select_with_value ? memoize_expression(context.state, value) - : get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value) + : get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value) : value ); 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 615cd0097f74..06567bed1ae0 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 @@ -29,12 +29,12 @@ export function RenderTag(node, context) { for (let i = 0; i < raw_args.length; i++) { let expression = /** @type {Expression} */ (context.visit(raw_args[i])); - const { has_call, is_async } = node.metadata.arguments[i]; + const { has_call, has_await } = node.metadata.arguments[i]; - if (is_async || has_call) { + if (has_await || has_call) { expression = b.call( '$.get', - get_expression_id(is_async ? async_expressions : expressions, expression) + get_expression_id(has_await ? async_expressions : expressions, expression) ); } 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 85fb2dd7083c..4ef375a63a19 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 @@ -93,10 +93,10 @@ export function SvelteElement(node, context) { ); } - const { is_async } = node.metadata.expression; + const { has_await } = 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); + const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression); /** @type {Statement[]} */ const inner = inner_context.state.init; @@ -139,7 +139,7 @@ export function SvelteElement(node, context) { ) ); - if (is_async) { + if (has_await) { context.state.init.push( b.stmt( b.call( 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 604f222d8a09..1b2b9997768c 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 @@ -126,11 +126,11 @@ export function build_component(node, component_name, context, anchor = context. if (attribute.metadata.expression.has_state) { props_and_spreads.push( b.thunk( - attribute.metadata.expression.is_async || attribute.metadata.expression.has_call + attribute.metadata.expression.has_await || attribute.metadata.expression.has_call ? b.call( '$.get', get_expression_id( - attribute.metadata.expression.is_async ? async_expressions : expressions, + attribute.metadata.expression.has_await ? async_expressions : expressions, expression ) ) @@ -147,10 +147,10 @@ export function build_component(node, component_name, context, anchor = context. attribute.name, build_attribute_value(attribute.value, context, (value, metadata) => { // TODO put the derived in the local block - return metadata.has_call || metadata.is_async + return metadata.has_call || metadata.has_await ? b.call( '$.get', - get_expression_id(metadata.is_async ? async_expressions : expressions, value) + get_expression_id(metadata.has_await ? async_expressions : expressions, value) ) : value; }).value @@ -171,13 +171,13 @@ export function build_component(node, component_name, context, anchor = context. attribute.value, context, (value, metadata) => { - if (!metadata.has_state && !metadata.is_async) return value; + if (!metadata.has_state && !metadata.has_await) 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 // child component (e.g. `active={i === index}`) const should_wrap_in_derived = - metadata.is_async || + metadata.has_await || get_attribute_chunks(attribute.value).some((n) => { return ( n.type === 'ExpressionTag' && @@ -189,7 +189,7 @@ export function build_component(node, component_name, context, anchor = context. return should_wrap_in_derived ? b.call( '$.get', - get_expression_id(metadata.is_async ? async_expressions : expressions, value) + get_expression_id(metadata.has_await ? async_expressions : expressions, value) ) : 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 644206021b61..01e94c72ca58 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 @@ -38,9 +38,9 @@ export function build_set_attributes( attribute.value, context, (value, metadata) => - metadata.has_call || metadata.is_async + metadata.has_call || metadata.has_await ? get_expression_id( - metadata.is_async ? context.state.async_expressions : context.state.expressions, + metadata.has_await ? context.state.async_expressions : context.state.expressions, value ) : value @@ -65,9 +65,9 @@ export function build_set_attributes( let value = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_call || attribute.metadata.expression.is_async) { + if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) { value = get_expression_id( - attribute.metadata.expression.is_async + attribute.metadata.expression.has_await ? context.state.async_expressions : context.state.expressions, value @@ -145,7 +145,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 || chunk.metadata.expression.is_async + has_state: chunk.metadata.expression.has_state || chunk.metadata.expression.has_await }; } @@ -178,9 +178,9 @@ export function build_set_class(element, node_id, attribute, class_directives, c value = b.call('$.clsx', value); } - return metadata.has_call || metadata.is_async + return metadata.has_call || metadata.has_await ? get_expression_id( - metadata.is_async ? context.state.async_expressions : context.state.expressions, + metadata.has_await ? context.state.async_expressions : context.state.expressions, value ) : value; @@ -253,7 +253,7 @@ export function build_set_style(node_id, attribute, style_directives, context) { let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => metadata.has_call ? get_expression_id( - metadata.is_async ? context.state.async_expressions : context.state.expressions, + metadata.has_await ? context.state.async_expressions : context.state.expressions, value ) : 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 d9efc3a6e629..b5a37e02bbbb 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 @@ -45,8 +45,8 @@ export function build_template_chunk( visit, state, memoize = (value, metadata) => - metadata.has_call || metadata.is_async - ? get_expression_id(metadata.is_async ? state.async_expressions : state.expressions, value) + metadata.has_call || metadata.has_await + ? get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value) : value ) { /** @type {Expression[]} */ @@ -56,7 +56,7 @@ export function build_template_chunk( const quasis = [quasi]; let has_state = false; - let is_async = false; + let has_await = false; for (let i = 0; i < values.length; i++) { const node = values[i]; @@ -77,8 +77,8 @@ export function build_template_chunk( node.metadata.expression ); - is_async ||= node.metadata.expression.is_async; - has_state ||= is_async || node.metadata.expression.has_state; + has_await ||= node.metadata.expression.has_await; + has_state ||= has_await || node.metadata.expression.has_state; if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index e92d2d089337..d342156e1ed7 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -62,6 +62,6 @@ export function create_expression_metadata() { dependencies: new Set(), has_state: false, has_call: false, - is_async: false + has_await: false }; } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9ccc553c48c7..c40111ca37d8 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -736,7 +736,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } }; - let is_async = false; + let has_await = false; walk(ast, state, { AwaitExpression(node, context) { @@ -744,7 +744,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { // automatically opt into runes mode on encountering // blocking awaits, without doing an additional walk // before the analysis occurs - is_async ||= context.path.every( + has_await ||= context.path.every( ({ type }) => type !== 'ArrowFunctionExpression' && type !== 'FunctionExpression' && @@ -1108,7 +1108,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } return { - is_async, + has_await, scope, scopes }; diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 7f7ddda7d80f..89ff943486bf 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -13,7 +13,7 @@ export interface Js { ast: Program; scope: Scope; scopes: Map; - is_async: boolean; + has_await: boolean; } export interface Template { diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 4e43166d8ffb..4d50c2db8a42 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -273,7 +273,7 @@ export interface ExpressionMetadata { /** 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; + has_await: boolean; } export * from './template.js'; From ab0ec6f7fdd83c08c8be05f7754a118c995e3f3e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 09:01:42 -0400 Subject: [PATCH 269/582] don't update a focused input (may need to add a blur handler later, we'll see) --- .../svelte/src/internal/client/dom/elements/bindings/input.js | 4 ++++ 1 file changed, 4 insertions(+) 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 f1992007ed7d..4fd2ee0a4b02 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -64,6 +64,10 @@ export function bind_value(input, get, set = get) { var value = get(); + if (input === document.activeElement) { + return; + } + if (is_numberlike_input(input) && value === to_number(input.value)) { // handles 0 vs 00 case (see https://github.com/sveltejs/svelte/issues/9959) return; From 521b22892cfc91d96675f7dbe287522d36a5bf08 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 13:20:11 -0400 Subject: [PATCH 270/582] docs --- packages/svelte/src/internal/client/reactivity/forks.js | 1 + packages/svelte/src/internal/client/reactivity/sources.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 6c4705b9347c..2bcd4a37ef6a 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -14,6 +14,7 @@ export function remove_active_fork() { active_fork = null; } +/** Update `$effect.pending()` */ function update_pending() { internal_set(pending, forks.size > 0); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2ce3c8ba66f7..0781a7dc5074 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -42,6 +42,7 @@ import { execute_derived } from './deriveds.js'; export let inspect_effects = new Set(); export const old_values = new Map(); +/** Internal representation of `$effect.pending()` */ export let pending = source(false); /** From 037e2895b49df571caf34c83ab45d41c99dbb472 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 16:13:13 -0400 Subject: [PATCH 271/582] fix --- packages/svelte/src/internal/client/reactivity/sources.js | 3 --- packages/svelte/src/internal/client/runtime.js | 2 +- .../svelte/tests/runtime-runes/samples/tick-timing/_config.js | 2 ++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 0781a7dc5074..711a252a115f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -14,7 +14,6 @@ import { reaction_sources, check_dirtiness, untracking, - queue_flush, is_destroying_effect, push_reaction_value } from '../runtime.js'; @@ -226,8 +225,6 @@ export function internal_set(source, value) { inspect_effects.clear(); } - - queue_flush(); } return value; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2130c71103a3..90364e44548c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -788,7 +788,7 @@ export function schedule_effect(signal) { } } -export function queue_flush() { +function queue_flush() { if (!is_flushing) { is_flushing = true; queueMicrotask(() => { diff --git a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js index 339cec55c5a2..25414d4b4710 100644 --- a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js @@ -3,6 +3,8 @@ import { test, ok } from '../../test'; // Tests that tick only resolves after all pending effects have been cleared export default test({ + skip: true, // weirdly, this works if you run it by itself + async test({ assert, target }) { const btn = target.querySelector('button'); ok(btn); From 6688eb86427e5b8f84b8f5e540f7575ccf6a89a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 16:14:27 -0400 Subject: [PATCH 272/582] remove indirection --- .../svelte/src/internal/client/runtime.js | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 90364e44548c..738d8f28fb8f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -763,7 +763,21 @@ function flush_queued_effects(effects) { * @returns {void} */ export function schedule_effect(signal) { - queue_flush(); + if (!is_flushing) { + is_flushing = true; + queueMicrotask(() => { + flush_queued_root_effects(); + + // TODO this doesn't seem quite right — may run into + // interesting cases where there are multiple roots. + // it'll do for now though + if (active_fork?.pending === 0) { + active_fork.remove(); + } + + remove_active_fork(); + }); + } var effect = (last_scheduled_effect = signal); @@ -788,24 +802,6 @@ export function schedule_effect(signal) { } } -function queue_flush() { - if (!is_flushing) { - is_flushing = true; - queueMicrotask(() => { - flush_queued_root_effects(); - - // TODO this doesn't seem quite right — may run into - // interesting cases where there are multiple roots. - // it'll do for now though - if (active_fork?.pending === 0) { - active_fork.remove(); - } - - remove_active_fork(); - }); - } -} - /** * * This function both runs render effects and collects user effects in topological order From cb2f68ebc326a3920d5081814e92fdc63c65c0d6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 19:13:27 -0400 Subject: [PATCH 273/582] QOL --- playgrounds/sandbox/index.html | 6 ++++++ playgrounds/sandbox/ssr-common.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html index 845538abf073..d70409ffb63a 100644 --- a/playgrounds/sandbox/index.html +++ b/playgrounds/sandbox/index.html @@ -14,6 +14,12 @@ import { mount, hydrate, unmount } from 'svelte'; import App from '/src/App.svelte'; + globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); + }; + const root = document.getElementById('root'); const render = root.firstChild?.nextSibling ? hydrate : mount; diff --git a/playgrounds/sandbox/ssr-common.js b/playgrounds/sandbox/ssr-common.js index 60c6b52eb1dc..db3e08550868 100644 --- a/playgrounds/sandbox/ssr-common.js +++ b/playgrounds/sandbox/ssr-common.js @@ -9,3 +9,9 @@ Promise.withResolvers ??= () => { return { promise, resolve, reject }; }; + +globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); +}; From 4f450330d4ba56044580eb3a11ce4eaa737fee19 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 18 Apr 2025 19:26:02 -0400 Subject: [PATCH 274/582] move stuff --- .../svelte/src/internal/client/runtime.js | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 738d8f28fb8f..ecc3450aab07 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -693,6 +693,17 @@ function flush_queued_root_effects() { infinite_loop_guard(); } + var revert = active_fork?.apply(); + + /** @type {Effect[]} */ + var async_effects = []; + + /** @type {Effect[]} */ + var render_effects = []; + + /** @type {Effect[]} */ + var effects = []; + var root_effects = queued_root_effects; var length = root_effects.length; @@ -705,8 +716,21 @@ function flush_queued_root_effects() { root.f ^= CLEAN; } - process_effects(root, active_fork); + process_effects(root, async_effects, render_effects, effects); + } + + if (async_effects.length === 0 && (active_fork === null || active_fork.pending === 0)) { + active_fork?.commit(); + flush_queued_effects(render_effects); + flush_queued_effects(effects); } + + revert?.(); + + for (const effect of async_effects) { + update_effect(effect); + } + old_values.clear(); } } finally { @@ -810,22 +834,13 @@ export function schedule_effect(signal) { * effects to be flushed. * * @param {Effect} root - * @param {Fork | null} fork + * @param {Effect[]} async_effects + * @param {Effect[]} render_effects + * @param {Effect[]} effects */ -function process_effects(root, fork) { - var revert = fork?.apply(); - +function process_effects(root, async_effects, render_effects, effects) { var effect = root.first; - /** @type {Effect[]} */ - var async_effects = []; - - /** @type {Effect[]} */ - var render_effects = []; - - /** @type {Effect[]} */ - var effects = []; - while (effect !== null) { var flags = effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; @@ -874,18 +889,6 @@ function process_effects(root, fork) { parent = parent.parent; } } - - if (async_effects.length === 0 && (fork === null || fork.pending === 0)) { - fork?.commit(); - flush_queued_effects(render_effects); - flush_queued_effects(effects); - } - - revert?.(); - - for (const effect of async_effects) { - update_effect(effect); - } } /** From a469c39cc24134f2f26b13439f2e672344ffe75f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 08:52:13 -0400 Subject: [PATCH 275/582] update test to not rely on props --- .../samples/async-derived/Child.svelte | 2 +- .../samples/async-derived/_config.js | 67 +++++++++---------- .../samples/async-derived/main.svelte | 10 ++- 3 files changed, 39 insertions(+), 40 deletions(-) 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 6031c28305a0..b59fd7c08fc3 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte @@ -1,7 +1,7 @@ + + + + + - + {#snippet pending()}

pending

From 43457ccd7de2b43c2c83e8879bea2e548869cf8f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 11:41:55 -0400 Subject: [PATCH 276/582] . --- packages/svelte/src/internal/client/reactivity/forks.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 2bcd4a37ef6a..af9bbf5127a9 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -1,8 +1,9 @@ /** @import { Effect, Source } from '#client' */ +import { DIRTY } from '#client/constants'; import { noop } from '../../shared/utils.js'; import { flushSync } from '../runtime.js'; import { raf } from '../timing.js'; -import { internal_set, pending } from './sources.js'; +import { internal_set, mark_reactions, pending } from './sources.js'; /** @type {Set} */ const forks = new Set(); @@ -16,7 +17,7 @@ export function remove_active_fork() { /** Update `$effect.pending()` */ function update_pending() { - internal_set(pending, forks.size > 0); + // internal_set(pending, forks.size > 0); } let uid = 1; From 8b691e9ea2b86187f9df0bc675becd77ecbcf06d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 11:44:09 -0400 Subject: [PATCH 277/582] rename --- .../svelte/src/internal/client/reactivity/forks.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index af9bbf5127a9..af5555a5712c 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -45,11 +45,11 @@ export class Fork { return noop; } - var values = new Map(); + var current_values = new Map(); for (const source of this.previous.keys()) { // mark_reactions(source, DIRTY); - values.set(source, source.v); + current_values.set(source, source.v); } for (const [source, current] of this.current) { @@ -60,16 +60,16 @@ export class Fork { if (fork === this) continue; for (const [source, previous] of fork.previous) { - if (!values.has(source)) { + if (!current_values.has(source)) { // mark_reactions(source, DIRTY); - values.set(source, source.v); + current_values.set(source, source.v); source.v = previous; } } } return () => { - for (const [source, value] of values) { + for (const [source, value] of current_values) { source.v = value; } }; From 81f066fb8950a740eb7c0a7a94f62a6c0efc7047 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 12:18:52 -0400 Subject: [PATCH 278/582] update test --- .../samples/async-attribute/_config.js | 40 +++++++++---------- .../samples/async-attribute/main.svelte | 8 +++- 2 files changed, 24 insertions(+), 24 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 2312d8ae606c..0c77424e4e63 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -1,37 +1,33 @@ import { flushSync, tick } from 'svelte'; -import { deferred } from '../../../../src/internal/shared/utils.js'; -import { test } from '../../test'; - -/** @type {ReturnType} */ -let d; +import { ok, test } from '../../test'; export default test({ - html: `

pending

`, - - get props() { - d = deferred(); - - return { - promise: d.promise - }; - }, + html: ` + + + +

pending

+ `, async test({ assert, target, component }) { - d.resolve('cool'); + const [cool, neat, reset] = target.querySelectorAll('button'); + + flushSync(() => cool.click()); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await tick(); flushSync(); - assert.htmlEqual(target.innerHTML, '

hello

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

hello

'); + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.outerHTML, '

hello

'); + + flushSync(() => reset.click()); + assert.htmlEqual(p.outerHTML, '

hello

'); - d.resolve('neat'); + flushSync(() => neat.click()); await tick(); - assert.htmlEqual(target.innerHTML, '

hello

'); + assert.htmlEqual(p.outerHTML, '

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 index aded5144531c..6332a9802d5c 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte @@ -1,9 +1,13 @@ + + + + -

hello

+

hello

{#snippet pending()}

pending

From a840f00b67fd451d35f3ee9d9ea59e1f14a9acae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 12:39:10 -0400 Subject: [PATCH 279/582] tweak --- 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 1ac3c5b6695f..5dfa5547329c 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -106,7 +106,7 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - var boundary = /** @type {Effect} */ (active_effect).b; + var boundary = /** @type {Effect} */ parent.b; while (boundary !== null && !boundary.has_pending_snippet()) { boundary = boundary.parent; From 3fe77cdebe4b1e77dbf07c55c758d9c84e1a3dd9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 12:58:12 -0400 Subject: [PATCH 280/582] tweak --- .../internal/client/reactivity/deriveds.js | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5dfa5547329c..17f41d856f8a 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -100,18 +100,22 @@ export function async_derived(fn, location) { throw new Error('TODO cannot create unowned async derived'); } + let boundary = parent.b; + + while (boundary !== null && !boundary.has_pending_snippet()) { + boundary = boundary.parent; + } + + if (boundary === null) { + throw new Error('TODO cannot create async derived outside a boundary with a pending snippet'); + } + var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - var boundary = /** @type {Effect} */ parent.b; - - while (boundary !== null && !boundary.has_pending_snippet()) { - boundary = boundary.parent; - } - render_effect(() => { if (DEV) from_async_derived = active_effect; promise = fn(); @@ -125,10 +129,6 @@ export function async_derived(fn, location) { if (fork !== null) { fork.increment(); } else { - if (boundary === null) { - throw new Error('TODO'); - } - // if nearest pending boundary is not ready, attach to the boundary boundary.increment(); } @@ -147,10 +147,6 @@ export function async_derived(fn, location) { if (fork !== null) { fork.decrement(); } else { - if (boundary === null) { - throw new Error('TODO'); - } - boundary.decrement(); } } From b7c39956ac90f1e25a15cd32c652141a03b27915 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 13:05:37 -0400 Subject: [PATCH 281/582] tweak --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a734e09a79f3..1738a27ff263 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -47,6 +47,7 @@ export function boundary(node, props, children) { export class Boundary { inert = false; + ran = false; /** @type {Boundary | null} */ parent; From 2620a2189fbcc11565bc69b59dfa4b3896c3003a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 13:16:53 -0400 Subject: [PATCH 282/582] tweak --- .../src/internal/client/dom/blocks/boundary.js | 4 ++++ .../src/internal/client/reactivity/deriveds.js | 14 +++++++------- 2 files changed, 11 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 1738a27ff263..a71f604707f1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -228,6 +228,8 @@ export class Boundary { }); } + this.ran = false; + this.#main_effect = this.#run(() => { this.#is_creating_fallback = false; @@ -238,6 +240,8 @@ export class Boundary { } }); + this.ran = true; + if (this.#pending_count > 0) { this.#show_pending_snippet(); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 17f41d856f8a..6b2ea3d39b1c 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -124,13 +124,13 @@ export function async_derived(fn, location) { var restore = capture(); var fork = active_fork; + var ran = boundary.ran; if (should_suspend) { - if (fork !== null) { - fork.increment(); - } else { - // if nearest pending boundary is not ready, attach to the boundary + if (!ran) { boundary.increment(); + } else { + fork?.increment(); } } @@ -144,10 +144,10 @@ export function async_derived(fn, location) { from_async_derived = null; if (should_suspend) { - if (fork !== null) { - fork.decrement(); - } else { + if (!ran) { boundary.decrement(); + } else { + fork?.decrement(); } } From 0abc0a8474c8d8110fece64380b925bc0247ed28 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 13:38:32 -0400 Subject: [PATCH 283/582] tweak --- packages/svelte/src/internal/client/render.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 3256fe827410..3479c87a9d63 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -30,6 +30,7 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; +import { active_fork, Fork } from './reactivity/forks.js'; /** * This is normally true — block effects should run their intro transitions — @@ -205,6 +206,8 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro // @ts-expect-error will be defined because the render effect runs synchronously var component = undefined; + Fork.ensure(); + var unmount = component_root(() => { var anchor_node = anchor ?? target.appendChild(create_text()); From ce09353e93a15866d3dd2fdb7f0eec7afdf02136 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 17:19:13 -0400 Subject: [PATCH 284/582] tidy up --- .../runtime-runes/samples/async-nested-derived/main.svelte | 2 -- 1 file changed, 2 deletions(-) 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 index e5306f19259c..f6b0afe98cba 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte @@ -13,5 +13,3 @@

pending

{/snippet}
- -{console.log(`outside boundary ${count}`)} From e49f81f409ff40f0adfc4665648839bf4c740e14 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 17:52:52 -0400 Subject: [PATCH 285/582] dont use flushSync --- packages/svelte/src/internal/client/reactivity/forks.js | 2 +- .../tests/runtime-runes/samples/async-attribute/_config.js | 1 + .../svelte/tests/runtime-runes/samples/async-derived/_config.js | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index af5555a5712c..632361966e92 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -114,7 +114,7 @@ export class Fork { */ run(fn) { active_fork = this; - flushSync(fn); + fn(); } increment() { 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 0c77424e4e63..f256e6a43c28 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -27,6 +27,7 @@ export default test({ assert.htmlEqual(p.outerHTML, '

hello

'); flushSync(() => neat.click()); + await Promise.resolve(); await tick(); assert.htmlEqual(p.outerHTML, '

hello

'); } 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 1e041c3f6247..d573cf624672 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(() => increment.click()); await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(p.innerHTML, '2a'); @@ -36,6 +37,7 @@ export default test({ flushSync(() => resolve_b.click()); await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(p.innerHTML, '2b'); From e0e48b392a03ee5bc592fd5ec7077788696ed385 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 18:12:05 -0400 Subject: [PATCH 286/582] WIP --- packages/svelte/src/internal/client/reactivity/effects.js | 4 +++- packages/svelte/src/internal/client/render.js | 2 -- packages/svelte/src/internal/client/runtime.js | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index d506168e800c..aa4d51073088 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -41,7 +41,7 @@ import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; -import { active_fork } from './forks.js'; +import { active_fork, Fork } from './forks.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -234,6 +234,7 @@ export function inspect_effect(fn) { * @returns {() => void} */ export function effect_root(fn) { + Fork.ensure(); const effect = create_effect(ROOT_EFFECT, fn, true); return () => { @@ -247,6 +248,7 @@ export function effect_root(fn) { * @returns {(options?: { outro?: boolean }) => Promise} */ export function component_root(fn) { + Fork.ensure(); const effect = create_effect(ROOT_EFFECT, fn, true); return (options = {}) => { diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 3479c87a9d63..404965d9ab89 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -206,8 +206,6 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro // @ts-expect-error will be defined because the render effect runs synchronously var component = undefined; - Fork.ensure(); - var unmount = component_root(() => { var anchor_node = anchor ?? target.appendChild(create_text()); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index ecc3450aab07..6015c39a74af 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -684,6 +684,11 @@ function infinite_loop_guard() { function flush_queued_root_effects() { var was_updating_effect = is_updating_effect; + // TODO it should be impossible to get here without an active fork + if (!active_fork && queued_root_effects.length > 0) { + console.trace('here'); + } + try { var flush_count = 0; is_updating_effect = true; @@ -901,6 +906,8 @@ function process_effects(root, async_effects, render_effects, effects) { export function flushSync(fn) { var result; + Fork.ensure(); + if (fn) { is_flushing = true; flush_queued_root_effects(); From 8cc596196035730a8b965e7310499bca8266814f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 20:39:20 -0400 Subject: [PATCH 287/582] tweak --- .../src/internal/client/reactivity/forks.js | 2 +- .../src/internal/client/reactivity/sources.js | 2 +- packages/svelte/src/internal/client/render.js | 1 - .../svelte/src/internal/client/runtime.js | 26 +++++++++---------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/forks.js index 632361966e92..73bcc177206b 100644 --- a/packages/svelte/src/internal/client/reactivity/forks.js +++ b/packages/svelte/src/internal/client/reactivity/forks.js @@ -149,7 +149,7 @@ export class Fork { } active_fork = new Fork(); - forks.add(active_fork); // TODO figure out where we remove this + forks.add(active_fork); } return active_fork; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 711a252a115f..678c75934abe 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -34,7 +34,7 @@ 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 { active_fork, Fork } from './forks.js'; +import { Fork } from './forks.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 404965d9ab89..3256fe827410 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -30,7 +30,6 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; -import { active_fork, Fork } from './reactivity/forks.js'; /** * This is normally true — block effects should run their intro transitions — diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6015c39a74af..02ead17a9430 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -683,11 +683,7 @@ function infinite_loop_guard() { function flush_queued_root_effects() { var was_updating_effect = is_updating_effect; - - // TODO it should be impossible to get here without an active fork - if (!active_fork && queued_root_effects.length > 0) { - console.trace('here'); - } + var fork = /** @type {Fork} */ (active_fork); try { var flush_count = 0; @@ -698,7 +694,7 @@ function flush_queued_root_effects() { infinite_loop_guard(); } - var revert = active_fork?.apply(); + var revert = fork.apply(); /** @type {Effect[]} */ var async_effects = []; @@ -724,13 +720,13 @@ function flush_queued_root_effects() { process_effects(root, async_effects, render_effects, effects); } - if (async_effects.length === 0 && (active_fork === null || active_fork.pending === 0)) { - active_fork?.commit(); + if (async_effects.length === 0 && fork.pending === 0) { + fork.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); } - revert?.(); + revert(); for (const effect of async_effects) { update_effect(effect); @@ -795,11 +791,13 @@ export function schedule_effect(signal) { if (!is_flushing) { is_flushing = true; queueMicrotask(() => { + if (active_fork === null) { + // a flushSync happened in the meantime + return; + } + flush_queued_root_effects(); - // TODO this doesn't seem quite right — may run into - // interesting cases where there are multiple roots. - // it'll do for now though if (active_fork?.pending === 0) { active_fork.remove(); } @@ -845,14 +843,14 @@ export function schedule_effect(signal) { */ function process_effects(root, async_effects, render_effects, effects) { var effect = root.first; + var fork = /** @type {Fork} */ (active_fork); while (effect !== null) { var flags = effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - var skip = - is_skippable_branch || (flags & INERT) !== 0 || active_fork?.skipped_effects.has(effect); + var skip = is_skippable_branch || (flags & INERT) !== 0 || fork.skipped_effects.has(effect); if (!skip) { if ((flags & EFFECT_ASYNC) !== 0) { From b48c12b8594e5653cd333f6de3bce53e2c54c373 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 20:47:57 -0400 Subject: [PATCH 288/582] out of date --- packages/svelte/src/internal/client/runtime.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 02ead17a9430..a58d50983a8c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -920,9 +920,6 @@ export function flushSync(fn) { flush_tasks(); } - // TODO this doesn't seem quite right — may run into - // interesting cases where there are multiple roots. - // it'll do for now though if (active_fork?.pending === 0) { active_fork.remove(); } From e247f665af47c1508539cde800230a03863b9c65 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 21:02:54 -0400 Subject: [PATCH 289/582] more --- .../src/internal/client/dom/blocks/boundary.js | 2 ++ .../src/internal/client/reactivity/deriveds.js | 15 ++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a71f604707f1..386eff976603 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -24,6 +24,7 @@ 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 { active_fork, Fork } from '../../reactivity/forks.js'; /** * @typedef {{ @@ -114,6 +115,7 @@ export class Boundary { // boundary, and hydrate accordingly queueMicrotask(() => { this.#main_effect = this.#run(() => { + Fork.ensure(); return branch(() => this.#children(this.#anchor)); }); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6b2ea3d39b1c..c6069ef5f679 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,4 +1,5 @@ /** @import { Derived, Effect, Source } from '#client' */ +/** @import { Fork } from './forks.js'; */ import { DEV } from 'esm-env'; import { CLEAN, @@ -123,14 +124,14 @@ export function async_derived(fn, location) { var restore = capture(); - var fork = active_fork; + var fork = /** @type {Fork} */ (active_fork); var ran = boundary.ran; if (should_suspend) { if (!ran) { boundary.increment(); } else { - fork?.increment(); + fork.increment(); } } @@ -147,17 +148,13 @@ export function async_derived(fn, location) { if (!ran) { boundary.decrement(); } else { - fork?.decrement(); + fork.decrement(); } } - if (fork !== null) { - fork.run(() => { - internal_set(signal, v); - }); - } else { + fork.run(() => { internal_set(signal, v); - } + }); if (DEV && location !== undefined) { recent_async_deriveds.add(signal); From d42b358f99c1e073debe2eb3b8af83f1489b6ed3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 21:12:11 -0400 Subject: [PATCH 290/582] guarantee fork --- .../svelte/src/internal/client/dom/blocks/async.js | 7 ++++--- .../svelte/src/internal/client/dom/blocks/boundary.js | 2 +- packages/svelte/src/internal/client/dom/blocks/each.js | 8 +++++--- packages/svelte/src/internal/client/dom/blocks/if.js | 9 ++++++--- packages/svelte/src/internal/client/dom/blocks/key.js | 5 +++-- .../src/internal/client/dom/blocks/svelte-component.js | 5 +++-- .../svelte/src/internal/client/reactivity/effects.js | 10 ++++------ 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 8d92cc30edf2..627e3c7d236b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,5 +1,5 @@ /** @import { Effect, TemplateNode, Value } from '#client' */ - +/** @import { Fork } from '../../reactivity/forks.js' */ import { async_derived } from '../../reactivity/deriveds.js'; import { active_fork } from '../../reactivity/forks.js'; import { active_effect, schedule_effect } from '../../runtime.js'; @@ -13,15 +13,16 @@ import { capture } from './boundary.js'; export function async(node, expressions, fn) { // TODO handle hydration - var fork = active_fork; + var fork = /** @type {Fork} */ (active_fork); var effect = /** @type {Effect} */ (active_effect); + var restore = capture(); Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { restore(); fn(node, ...result); - fork?.run(() => { + fork.run(() => { schedule_effect(effect); }); }); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 386eff976603..b6ecbf058271 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -24,7 +24,7 @@ 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 { active_fork, Fork } from '../../reactivity/forks.js'; +import { Fork } from '../../reactivity/forks.js'; /** * @typedef {{ diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 8379b109a971..8168eddd8759 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -1,4 +1,5 @@ /** @import { EachItem, EachState, Effect, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */ +/** @import { Fork } from '../../reactivity/forks.js'; */ import { EACH_INDEX_REACTIVE, EACH_IS_ANIMATED, @@ -266,8 +267,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f fallback = branch(() => fallback_fn(anchor)); } } else { - if (active_fork !== null && should_defer_append()) { + if (should_defer_append()) { var keys = new Set(); + var fork = /** @type {Fork} */ (active_fork); for (i = 0; i < length; i += 1) { value = array[i]; @@ -303,11 +305,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f for (const [key, item] of state.items) { if (!keys.has(key)) { - active_fork.skipped_effects.add(item.e); + fork.skipped_effects.add(item.e); } } - active_fork?.add_callback(commit); + fork.add_callback(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index e9861e570af2..32a39392f2e6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,4 +1,5 @@ /** @import { Effect, TemplateNode } from '#client' */ +/** @import { Fork } from '../../reactivity/forks.js'; */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_next, @@ -112,7 +113,7 @@ export function if_block(node, fn, elseif = false) { } } - var defer = active_fork !== null && should_defer_append(); + var defer = should_defer_append(); var target = anchor; if (defer) { @@ -125,13 +126,15 @@ export function if_block(node, fn, elseif = false) { } if (defer) { + var fork = /** @type {Fork} */ (active_fork); + const skipped = condition ? alternate_effect : consequent_effect; if (skipped !== null) { // TODO need to do this for other kinds of blocks - active_fork?.skipped_effects.add(skipped); + fork.skipped_effects.add(skipped); } - active_fork?.add_callback(commit); + fork.add_callback(commit); } 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 021d9dec9e5e..d3b9d0a987bc 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,4 +1,5 @@ /** @import { Effect, TemplateNode } from '#client' */ +/** @import { Fork } from '../../reactivity/forks.js'; */ import { UNINITIALIZED } from '../../../../constants.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { not_equal, safe_not_equal } from '../../reactivity/equality.js'; @@ -55,7 +56,7 @@ export function key_block(node, get_key, render_fn) { if (changed(key, (key = get_key()))) { var target = anchor; - var defer = active_fork !== null && should_defer_append(); + var defer = should_defer_append(); if (defer) { offscreen_fragment = document.createDocumentFragment(); @@ -65,7 +66,7 @@ export function key_block(node, get_key, render_fn) { pending_effect = branch(() => render_fn(target)); if (defer) { - active_fork?.add_callback(commit); + /** @type {Fork} */ (active_fork).add_callback(commit); } 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 921f04670e31..fdd635b061f1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,4 +1,5 @@ /** @import { TemplateNode, Dom, Effect } from '#client' */ +/** @import { Fork } from '../../reactivity/forks.js'; */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { active_fork } from '../../reactivity/forks.js'; @@ -53,7 +54,7 @@ export function component(node, get_component, render_fn) { block(() => { if (component === (component = get_component())) return; - var defer = active_fork !== null && should_defer_append(); + var defer = should_defer_append(); if (component) { var target = anchor; @@ -67,7 +68,7 @@ export function component(node, get_component, render_fn) { } if (defer) { - active_fork?.add_callback(commit); + /** @type {Fork} */ (active_fork).add_callback(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index aa4d51073088..c4b088360765 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -343,7 +343,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var parent = /** @type {Effect} */ (active_effect); if (async.length > 0) { - var fork = active_fork; + var fork = /** @type {Fork} */ (active_fork); var restore = capture(); Promise.all(async.map((expression) => async_derived(expression))).then((result) => { @@ -355,11 +355,9 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var effect = create_template_effect(fn, [...sync.map(d), ...result]); - if (fork !== null) { - fork.run(() => { - schedule_effect(effect); - }); - } + fork.run(() => { + schedule_effect(effect); + }); }); } else { create_template_effect(fn, sync.map(d)); From 5000aae094d8f0684e5a6f131eb034b5bd5a4137 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 21:13:28 -0400 Subject: [PATCH 291/582] forks.js -> batch.js --- packages/svelte/src/internal/client/dom/blocks/async.js | 4 ++-- packages/svelte/src/internal/client/dom/blocks/boundary.js | 2 +- packages/svelte/src/internal/client/dom/blocks/each.js | 4 ++-- packages/svelte/src/internal/client/dom/blocks/if.js | 4 ++-- packages/svelte/src/internal/client/dom/blocks/key.js | 4 ++-- .../svelte/src/internal/client/dom/blocks/svelte-component.js | 4 ++-- .../src/internal/client/reactivity/{forks.js => batch.js} | 0 packages/svelte/src/internal/client/reactivity/deriveds.js | 4 ++-- packages/svelte/src/internal/client/reactivity/effects.js | 2 +- packages/svelte/src/internal/client/reactivity/sources.js | 2 +- packages/svelte/src/internal/client/runtime.js | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) rename packages/svelte/src/internal/client/reactivity/{forks.js => batch.js} (100%) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 627e3c7d236b..109a92822284 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,7 +1,7 @@ /** @import { Effect, TemplateNode, Value } from '#client' */ -/** @import { Fork } from '../../reactivity/forks.js' */ +/** @import { Fork } from '../../reactivity/batch.js' */ import { async_derived } from '../../reactivity/deriveds.js'; -import { active_fork } from '../../reactivity/forks.js'; +import { active_fork } from '../../reactivity/batch.js'; import { active_effect, schedule_effect } from '../../runtime.js'; import { capture } from './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 b6ecbf058271..6150a31b28a5 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -24,7 +24,7 @@ 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 { Fork } from '../../reactivity/forks.js'; +import { Fork } from '../../reactivity/batch.js'; /** * @typedef {{ diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 8168eddd8759..48c75df27531 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -1,5 +1,5 @@ /** @import { EachItem, EachState, Effect, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */ -/** @import { Fork } from '../../reactivity/forks.js'; */ +/** @import { Fork } from '../../reactivity/batch.js'; */ import { EACH_INDEX_REACTIVE, EACH_IS_ANIMATED, @@ -40,7 +40,7 @@ import { queue_micro_task } from '../task.js'; import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; -import { active_fork } from '../../reactivity/forks.js'; +import { active_fork } from '../../reactivity/batch.js'; /** * The row of a keyed each block that is currently updating. We track this diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 32a39392f2e6..18884d5734a6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,5 +1,5 @@ /** @import { Effect, TemplateNode } from '#client' */ -/** @import { Fork } from '../../reactivity/forks.js'; */ +/** @import { Fork } from '../../reactivity/batch.js'; */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_next, @@ -12,7 +12,7 @@ import { import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { create_text, should_defer_append } from '../operations.js'; -import { active_fork } from '../../reactivity/forks.js'; +import { active_fork } from '../../reactivity/batch.js'; // TODO reinstate https://github.com/sveltejs/svelte/pull/15250 diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index d3b9d0a987bc..52dd5cc32437 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,12 +1,12 @@ /** @import { Effect, TemplateNode } from '#client' */ -/** @import { Fork } from '../../reactivity/forks.js'; */ +/** @import { Fork } from '../../reactivity/batch.js'; */ 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 { is_runes } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; -import { active_fork } from '../../reactivity/forks.js'; +import { active_fork } from '../../reactivity/batch.js'; /** * @template V 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 fdd635b061f1..dd07d7716fe3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,8 +1,8 @@ /** @import { TemplateNode, Dom, Effect } from '#client' */ -/** @import { Fork } from '../../reactivity/forks.js'; */ +/** @import { Fork } from '../../reactivity/batch.js'; */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; -import { active_fork } from '../../reactivity/forks.js'; +import { active_fork } from '../../reactivity/batch.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; diff --git a/packages/svelte/src/internal/client/reactivity/forks.js b/packages/svelte/src/internal/client/reactivity/batch.js similarity index 100% rename from packages/svelte/src/internal/client/reactivity/forks.js rename to packages/svelte/src/internal/client/reactivity/batch.js diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c6069ef5f679..3e1d186fdf74 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,5 +1,5 @@ /** @import { Derived, Effect, Source } from '#client' */ -/** @import { Fork } from './forks.js'; */ +/** @import { Fork } from './batch.js'; */ import { DEV } from 'esm-env'; import { CLEAN, @@ -32,7 +32,7 @@ import { tracing_mode_flag } from '../../flags/index.js'; import { capture } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { active_fork } from './forks.js'; +import { active_fork } from './batch.js'; /** @type {Effect | null} */ export let from_async_derived = null; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4b088360765..d29d34658e59 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -41,7 +41,7 @@ import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; -import { active_fork, Fork } from './forks.js'; +import { active_fork, Fork } from './batch.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 678c75934abe..987993294543 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -34,7 +34,7 @@ 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 { Fork } from './forks.js'; +import { Fork } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a58d50983a8c..357c62aaf06d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -50,7 +50,7 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; -import { active_fork, Fork, remove_active_fork } from './reactivity/forks.js'; +import { active_fork, Fork, remove_active_fork } from './reactivity/batch.js'; // Used for DEV time error handling /** @param {WeakSet} value */ From d465537dd0f62deca48946a73f72aa2ec54703bc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Apr 2025 21:18:13 -0400 Subject: [PATCH 292/582] rename forks to batches --- .../src/internal/client/dom/blocks/async.js | 8 +-- .../internal/client/dom/blocks/boundary.js | 4 +- .../src/internal/client/dom/blocks/each.js | 10 ++-- .../src/internal/client/dom/blocks/if.js | 10 ++-- .../src/internal/client/dom/blocks/key.js | 6 +-- .../client/dom/blocks/svelte-component.js | 6 +-- .../src/internal/client/reactivity/batch.js | 54 +++++++++---------- .../internal/client/reactivity/deriveds.js | 12 ++--- .../src/internal/client/reactivity/effects.js | 10 ++-- .../src/internal/client/reactivity/sources.js | 6 +-- .../svelte/src/internal/client/runtime.js | 32 +++++------ 11 files changed, 79 insertions(+), 79 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 109a92822284..fe34167d7c04 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,7 +1,7 @@ /** @import { Effect, TemplateNode, Value } from '#client' */ -/** @import { Fork } from '../../reactivity/batch.js' */ +/** @import { Batch } from '../../reactivity/batch.js' */ import { async_derived } from '../../reactivity/deriveds.js'; -import { active_fork } from '../../reactivity/batch.js'; +import { current_batch } from '../../reactivity/batch.js'; import { active_effect, schedule_effect } from '../../runtime.js'; import { capture } from './boundary.js'; @@ -13,7 +13,7 @@ import { capture } from './boundary.js'; export function async(node, expressions, fn) { // TODO handle hydration - var fork = /** @type {Fork} */ (active_fork); + var batch = /** @type {Batch} */ (current_batch); var effect = /** @type {Effect} */ (active_effect); var restore = capture(); @@ -22,7 +22,7 @@ export function async(node, expressions, fn) { restore(); fn(node, ...result); - fork.run(() => { + batch.run(() => { schedule_effect(effect); }); }); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 6150a31b28a5..4979a4179f79 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -24,7 +24,7 @@ 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 { Fork } from '../../reactivity/batch.js'; +import { Batch } from '../../reactivity/batch.js'; /** * @typedef {{ @@ -115,7 +115,7 @@ export class Boundary { // boundary, and hydrate accordingly queueMicrotask(() => { this.#main_effect = this.#run(() => { - Fork.ensure(); + Batch.ensure(); return branch(() => this.#children(this.#anchor)); }); diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 48c75df27531..cb0d45e1ed55 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -1,5 +1,5 @@ /** @import { EachItem, EachState, Effect, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */ -/** @import { Fork } from '../../reactivity/batch.js'; */ +/** @import { Batch } from '../../reactivity/batch.js'; */ import { EACH_INDEX_REACTIVE, EACH_IS_ANIMATED, @@ -40,7 +40,7 @@ import { queue_micro_task } from '../task.js'; import { active_effect, get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; -import { active_fork } from '../../reactivity/batch.js'; +import { current_batch } from '../../reactivity/batch.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -269,7 +269,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } else { if (should_defer_append()) { var keys = new Set(); - var fork = /** @type {Fork} */ (active_fork); + var batch = /** @type {Batch} */ (current_batch); for (i = 0; i < length; i += 1) { value = array[i]; @@ -305,11 +305,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f for (const [key, item] of state.items) { if (!keys.has(key)) { - fork.skipped_effects.add(item.e); + batch.skipped_effects.add(item.e); } } - fork.add_callback(commit); + batch.add_callback(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 18884d5734a6..9a2857e9f89a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,5 +1,5 @@ /** @import { Effect, TemplateNode } from '#client' */ -/** @import { Fork } from '../../reactivity/batch.js'; */ +/** @import { Batch } from '../../reactivity/batch.js'; */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_next, @@ -12,7 +12,7 @@ import { import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { create_text, should_defer_append } from '../operations.js'; -import { active_fork } from '../../reactivity/batch.js'; +import { current_batch } from '../../reactivity/batch.js'; // TODO reinstate https://github.com/sveltejs/svelte/pull/15250 @@ -126,15 +126,15 @@ export function if_block(node, fn, elseif = false) { } if (defer) { - var fork = /** @type {Fork} */ (active_fork); + var batch = /** @type {Batch} */ (current_batch); const skipped = condition ? alternate_effect : consequent_effect; if (skipped !== null) { // TODO need to do this for other kinds of blocks - fork.skipped_effects.add(skipped); + batch.skipped_effects.add(skipped); } - fork.add_callback(commit); + batch.add_callback(commit); } 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 52dd5cc32437..0023764e1bd9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,12 +1,12 @@ /** @import { Effect, TemplateNode } from '#client' */ -/** @import { Fork } from '../../reactivity/batch.js'; */ +/** @import { Batch } from '../../reactivity/batch.js'; */ 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 { is_runes } from '../../context.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; -import { active_fork } from '../../reactivity/batch.js'; +import { current_batch } from '../../reactivity/batch.js'; /** * @template V @@ -66,7 +66,7 @@ export function key_block(node, get_key, render_fn) { pending_effect = branch(() => render_fn(target)); if (defer) { - /** @type {Fork} */ (active_fork).add_callback(commit); + /** @type {Batch} */ (current_batch).add_callback(commit); } 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 dd07d7716fe3..f16da9c42703 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,8 +1,8 @@ /** @import { TemplateNode, Dom, Effect } from '#client' */ -/** @import { Fork } from '../../reactivity/batch.js'; */ +/** @import { Batch } from '../../reactivity/batch.js'; */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; -import { active_fork } from '../../reactivity/batch.js'; +import { current_batch } from '../../reactivity/batch.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; @@ -68,7 +68,7 @@ export function component(node, get_component, render_fn) { } if (defer) { - /** @type {Fork} */ (active_fork).add_callback(commit); + /** @type {Batch} */ (current_batch).add_callback(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 73bcc177206b..a4ee8fc4a004 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -5,24 +5,24 @@ import { flushSync } from '../runtime.js'; import { raf } from '../timing.js'; import { internal_set, mark_reactions, pending } from './sources.js'; -/** @type {Set} */ -const forks = new Set(); +/** @type {Set} */ +const batches = new Set(); -/** @type {Fork | null} */ -export let active_fork = null; +/** @type {Batch | null} */ +export let current_batch = null; -export function remove_active_fork() { - active_fork = null; +export function remove_current_batch() { + current_batch = null; } /** Update `$effect.pending()` */ function update_pending() { - // internal_set(pending, forks.size > 0); + // internal_set(pending, batches.size > 0); } let uid = 1; -export class Fork { +export class Batch { id = uid++; /** @type {Map} */ @@ -40,8 +40,8 @@ export class Fork { pending = 0; apply() { - if (forks.size === 1) { - // if this is the latest (and only) fork, we have nothing to do + if (batches.size === 1) { + // if this is the latest (and only) batch, we have nothing to do return noop; } @@ -56,10 +56,10 @@ export class Fork { source.v = current; } - for (const fork of forks) { - if (fork === this) continue; + for (const batch of batches) { + if (batch === this) continue; - for (const [source, previous] of fork.previous) { + for (const [source, previous] of batch.previous) { if (!current_values.has(source)) { // mark_reactions(source, DIRTY); current_values.set(source, source.v); @@ -88,19 +88,19 @@ export class Fork { } remove() { - forks.delete(this); + batches.delete(this); - for (var fork of forks) { - if (fork.id < this.id) { - // other fork is older than this + for (var batch of batches) { + if (batch.id < this.id) { + // other batch is older than this for (var source of this.previous.keys()) { - fork.previous.delete(source); + batch.previous.delete(source); } } else { - // other fork is newer than this - for (var source of fork.previous.keys()) { + // other batch is newer than this + for (var source of batch.previous.keys()) { if (this.previous.has(source)) { - fork.previous.set(source, source.v); + batch.previous.set(source, source.v); } } } @@ -113,7 +113,7 @@ export class Fork { * @param {() => void} fn */ run(fn) { - active_fork = this; + current_batch = this; fn(); } @@ -143,15 +143,15 @@ export class Fork { } static ensure() { - if (active_fork === null) { - if (forks.size === 0) { + if (current_batch === null) { + if (batches.size === 0) { raf.tick(update_pending); } - active_fork = new Fork(); - forks.add(active_fork); + current_batch = new Batch(); + batches.add(current_batch); } - return active_fork; + return current_batch; } } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 3e1d186fdf74..b46b88fd2c1e 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,5 +1,5 @@ /** @import { Derived, Effect, Source } from '#client' */ -/** @import { Fork } from './batch.js'; */ +/** @import { Batch } from './batch.js'; */ import { DEV } from 'esm-env'; import { CLEAN, @@ -32,7 +32,7 @@ import { tracing_mode_flag } from '../../flags/index.js'; import { capture } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { active_fork } from './batch.js'; +import { current_batch } from './batch.js'; /** @type {Effect | null} */ export let from_async_derived = null; @@ -124,14 +124,14 @@ export function async_derived(fn, location) { var restore = capture(); - var fork = /** @type {Fork} */ (active_fork); + var batch = /** @type {Batch} */ (current_batch); var ran = boundary.ran; if (should_suspend) { if (!ran) { boundary.increment(); } else { - fork.increment(); + batch.increment(); } } @@ -148,11 +148,11 @@ export function async_derived(fn, location) { if (!ran) { boundary.decrement(); } else { - fork.decrement(); + batch.decrement(); } } - fork.run(() => { + batch.run(() => { internal_set(signal, v); }); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index d29d34658e59..28494cec6f81 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -41,7 +41,7 @@ import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; -import { active_fork, Fork } from './batch.js'; +import { current_batch, Batch } from './batch.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -234,7 +234,7 @@ export function inspect_effect(fn) { * @returns {() => void} */ export function effect_root(fn) { - Fork.ensure(); + Batch.ensure(); const effect = create_effect(ROOT_EFFECT, fn, true); return () => { @@ -248,7 +248,7 @@ export function effect_root(fn) { * @returns {(options?: { outro?: boolean }) => Promise} */ export function component_root(fn) { - Fork.ensure(); + Batch.ensure(); const effect = create_effect(ROOT_EFFECT, fn, true); return (options = {}) => { @@ -343,7 +343,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var parent = /** @type {Effect} */ (active_effect); if (async.length > 0) { - var fork = /** @type {Fork} */ (active_fork); + var batch = /** @type {Batch} */ (current_batch); var restore = capture(); Promise.all(async.map((expression) => async_derived(expression))).then((result) => { @@ -355,7 +355,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var effect = create_template_effect(fn, [...sync.map(d), ...result]); - fork.run(() => { + batch.run(() => { schedule_effect(effect); }); }); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 987993294543..d8b609859fd6 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -34,7 +34,7 @@ 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 { Fork } from './batch.js'; +import { Batch } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; @@ -169,8 +169,8 @@ export function internal_set(source, value) { source.v = value; - const fork = Fork.ensure(); - fork.capture(source, old_value); + const batch = Batch.ensure(); + batch.capture(source, old_value); if (DEV && tracing_mode_flag) { source.updated = get_stack('UpdatedAt'); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 357c62aaf06d..10ec5c536da7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -50,7 +50,7 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; -import { active_fork, Fork, remove_active_fork } from './reactivity/batch.js'; +import { current_batch, Batch, remove_current_batch } from './reactivity/batch.js'; // Used for DEV time error handling /** @param {WeakSet} value */ @@ -683,7 +683,7 @@ function infinite_loop_guard() { function flush_queued_root_effects() { var was_updating_effect = is_updating_effect; - var fork = /** @type {Fork} */ (active_fork); + var batch = /** @type {Batch} */ (current_batch); try { var flush_count = 0; @@ -694,7 +694,7 @@ function flush_queued_root_effects() { infinite_loop_guard(); } - var revert = fork.apply(); + var revert = batch.apply(); /** @type {Effect[]} */ var async_effects = []; @@ -720,8 +720,8 @@ function flush_queued_root_effects() { process_effects(root, async_effects, render_effects, effects); } - if (async_effects.length === 0 && fork.pending === 0) { - fork.commit(); + if (async_effects.length === 0 && batch.pending === 0) { + batch.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); } @@ -791,18 +791,18 @@ export function schedule_effect(signal) { if (!is_flushing) { is_flushing = true; queueMicrotask(() => { - if (active_fork === null) { + if (current_batch === null) { // a flushSync happened in the meantime return; } flush_queued_root_effects(); - if (active_fork?.pending === 0) { - active_fork.remove(); + if (current_batch?.pending === 0) { + current_batch.remove(); } - remove_active_fork(); + remove_current_batch(); }); } @@ -843,14 +843,14 @@ export function schedule_effect(signal) { */ function process_effects(root, async_effects, render_effects, effects) { var effect = root.first; - var fork = /** @type {Fork} */ (active_fork); + var batch = /** @type {Batch} */ (current_batch); while (effect !== null) { var flags = effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - var skip = is_skippable_branch || (flags & INERT) !== 0 || fork.skipped_effects.has(effect); + var skip = is_skippable_branch || (flags & INERT) !== 0 || batch.skipped_effects.has(effect); if (!skip) { if ((flags & EFFECT_ASYNC) !== 0) { @@ -867,7 +867,7 @@ function process_effects(root, async_effects, render_effects, effects) { } } else if ((flags & RENDER_EFFECT) !== 0) { if (is_branch) { - // TODO clean branch later, if fork is settled + // TODO clean branch later, if batch is settled // current_effect.f ^= CLEAN; } else { render_effects.push(effect); @@ -904,7 +904,7 @@ function process_effects(root, async_effects, render_effects, effects) { export function flushSync(fn) { var result; - Fork.ensure(); + Batch.ensure(); if (fn) { is_flushing = true; @@ -920,11 +920,11 @@ export function flushSync(fn) { flush_tasks(); } - if (active_fork?.pending === 0) { - active_fork.remove(); + if (current_batch?.pending === 0) { + current_batch.remove(); } - remove_active_fork(); + remove_current_batch(); return /** @type {T} */ (result); } From 9b5f00b9f4053a9743a7d6461f3f8b8ed672b4b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 11:05:08 -0400 Subject: [PATCH 293/582] fix --- packages/svelte/src/internal/client/reactivity/effects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 28494cec6f81..704633b39ce5 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -322,7 +322,7 @@ export function legacy_pre_effect_reset() { token.ran = false; } - context.l.r2.v = false; // set directly to avoid rerunning this effect + set(context.l.r2, false); }); } From 5a3f7c2a565bbcd6dabb8be065dbbbf21f7b10ed Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 11:27:36 -0400 Subject: [PATCH 294/582] simplify --- packages/svelte/src/internal/client/reactivity/batch.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a4ee8fc4a004..8a10277515a7 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -47,11 +47,6 @@ export class Batch { var current_values = new Map(); - for (const source of this.previous.keys()) { - // mark_reactions(source, DIRTY); - current_values.set(source, source.v); - } - for (const [source, current] of this.current) { source.v = current; } @@ -60,7 +55,7 @@ export class Batch { if (batch === this) continue; for (const [source, previous] of batch.previous) { - if (!current_values.has(source)) { + if (!this.previous.has(source)) { // mark_reactions(source, DIRTY); current_values.set(source, source.v); source.v = previous; From 011741ea22ba0e92237f0222eb58f9c2c274c8b3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 11:36:48 -0400 Subject: [PATCH 295/582] note to self --- packages/svelte/src/internal/client/reactivity/batch.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 8a10277515a7..1951077bffa3 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -47,6 +47,8 @@ export class Batch { var current_values = new Map(); + // TODO this shouldn't be necessary, but tests fail otherwise, + // presumably because we need a try-finally somewhere for (const [source, current] of this.current) { source.v = current; } From 623fb5064c86200c976201b941ce340bfbd3c81b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 11:45:44 -0400 Subject: [PATCH 296/582] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 1951077bffa3..ccda0dcc1d17 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -47,9 +47,10 @@ export class Batch { var current_values = new Map(); - // TODO this shouldn't be necessary, but tests fail otherwise, - // presumably because we need a try-finally somewhere for (const [source, current] of this.current) { + // TODO this shouldn't be necessary, but tests fail otherwise, + // presumably because we need a try-finally somewhere, and the + // source wasn't correctly reverted after the previous batch source.v = current; } From 4a56c2a9aa8e2e02b2fcf70d723fd6e7b884c314 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 11:47:17 -0400 Subject: [PATCH 297/582] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ccda0dcc1d17..e22790b8dcf2 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -40,10 +40,8 @@ export class Batch { pending = 0; apply() { - if (batches.size === 1) { - // if this is the latest (and only) batch, we have nothing to do - return noop; - } + // common case: no overlapping batches, nothing to revert + if (batches.size === 1) return noop; var current_values = new Map(); From f30fd267bf42db0a29607ce704b8e5384223a5a6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 12:35:02 -0400 Subject: [PATCH 298/582] privatise --- .../src/internal/client/reactivity/batch.js | 38 +++++++++---------- .../svelte/src/internal/client/runtime.js | 6 +-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e22790b8dcf2..6875e2cd4e5d 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -23,13 +23,13 @@ function update_pending() { let uid = 1; export class Batch { - id = uid++; + #id = uid++; /** @type {Map} */ - previous = new Map(); + #previous = new Map(); /** @type {Map} */ - current = new Map(); + #current = new Map(); /** @type {Set} */ skipped_effects = new Set(); @@ -37,7 +37,7 @@ export class Batch { /** @type {Set<() => void>} */ #callbacks = new Set(); - pending = 0; + #pending = 0; apply() { // common case: no overlapping batches, nothing to revert @@ -45,7 +45,7 @@ export class Batch { var current_values = new Map(); - for (const [source, current] of this.current) { + for (const [source, current] of this.#current) { // TODO this shouldn't be necessary, but tests fail otherwise, // presumably because we need a try-finally somewhere, and the // source wasn't correctly reverted after the previous batch @@ -55,8 +55,8 @@ export class Batch { for (const batch of batches) { if (batch === this) continue; - for (const [source, previous] of batch.previous) { - if (!this.previous.has(source)) { + for (const [source, previous] of batch.#previous) { + if (!this.#previous.has(source)) { // mark_reactions(source, DIRTY); current_values.set(source, source.v); source.v = previous; @@ -76,27 +76,27 @@ export class Batch { * @param {any} value */ capture(source, value) { - if (!this.previous.has(source)) { - this.previous.set(source, value); + if (!this.#previous.has(source)) { + this.#previous.set(source, value); } - this.current.set(source, source.v); + this.#current.set(source, source.v); } remove() { batches.delete(this); for (var batch of batches) { - if (batch.id < this.id) { + if (batch.#id < this.#id) { // other batch is older than this - for (var source of this.previous.keys()) { - batch.previous.delete(source); + for (var source of this.#previous.keys()) { + batch.#previous.delete(source); } } else { // other batch is newer than this - for (var source of batch.previous.keys()) { - if (this.previous.has(source)) { - batch.previous.set(source, source.v); + for (var source of batch.#previous.keys()) { + if (this.#previous.has(source)) { + batch.#previous.set(source, source.v); } } } @@ -114,15 +114,15 @@ export class Batch { } increment() { - this.pending += 1; + this.#pending += 1; } decrement() { - this.pending -= 1; + this.#pending -= 1; } settled() { - return this.pending === 0; + return this.#pending === 0; } /** @param {() => void} fn */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 10ec5c536da7..62ba5d041d0c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -720,7 +720,7 @@ function flush_queued_root_effects() { process_effects(root, async_effects, render_effects, effects); } - if (async_effects.length === 0 && batch.pending === 0) { + if (async_effects.length === 0 && batch.settled()) { batch.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -798,7 +798,7 @@ export function schedule_effect(signal) { flush_queued_root_effects(); - if (current_batch?.pending === 0) { + if (current_batch?.settled()) { current_batch.remove(); } @@ -920,7 +920,7 @@ export function flushSync(fn) { flush_tasks(); } - if (current_batch?.pending === 0) { + if (current_batch?.settled()) { current_batch.remove(); } From 6eac19951443cc6dc29984fa3a355a4823c0c5d3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 13:48:17 -0400 Subject: [PATCH 299/582] failing test --- .../samples/async-child-effect/_config.js | 74 +++++++++++++++++++ .../samples/async-child-effect/main.svelte | 26 +++++++ 2 files changed, 100 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js new file mode 100644 index 000000000000..41d4130470d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js @@ -0,0 +1,74 @@ +import { flushSync, tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` + +

loading

+ `, + + async test({ assert, target, variant }) { + if (variant === 'hydrate') { + await Promise.resolve(); + } + + flushSync(() => { + target.querySelector('button')?.click(); + }); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + const [button1, button2] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

A

+

a

+ ` + ); + + flushSync(() => button2.click()); + flushSync(() => button2.click()); + + flushSync(() => button1.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

AA

+

aa

+ ` + ); + + flushSync(() => button1.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

AAA

+

aaa

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte new file mode 100644 index 000000000000..edb0eaea44fd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte @@ -0,0 +1,26 @@ + + + + + + +

{await push(input.toUpperCase())}

+ + {#if true} +

{input}

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

loading

+ {/snippet} +
From 1f02fdf5a30defbd0e2626a64a434af850ca455c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 13:52:45 -0400 Subject: [PATCH 300/582] note to self --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 4979a4179f79..95db4dfefc5c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -345,6 +345,7 @@ export function capture(track = true) { }; } +// TODO we should probably be incrementing the current batch, not the boundary? export function suspend() { let boundary = /** @type {Effect} */ (active_effect).b; From 3ee25bbe0257a4cd27dbaa5b70d2f92a3521c9e6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 14:42:39 -0400 Subject: [PATCH 301/582] reinstate scheduling optimisation --- .../svelte/src/internal/client/runtime.js | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 62ba5d041d0c..dc7f99d488ef 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -51,6 +51,7 @@ import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; import { current_batch, Batch, remove_current_batch } from './reactivity/batch.js'; +import { log_effect_tree, root } from './dev/debug.js'; // Used for DEV time error handling /** @param {WeakSet} value */ @@ -813,20 +814,12 @@ export function schedule_effect(signal) { var flags = effect.f; if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - // TODO reinstate this - // if ((flags & CLEAN) === 0) return; - // effect.f ^= CLEAN; - - if ((flags & CLEAN) !== 0) { - effect.f ^= CLEAN; - } + if ((flags & CLEAN) === 0) return; + effect.f ^= CLEAN; } } - // TODO reinstate early bail-out when traversing up the graph - if (!queued_root_effects.includes(effect)) { - queued_root_effects.push(effect); - } + queued_root_effects.push(effect); } /** @@ -847,7 +840,7 @@ function process_effects(root, async_effects, render_effects, effects) { while (effect !== null) { var flags = effect.f; - var is_branch = (flags & BRANCH_EFFECT) !== 0; + var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; var skip = is_skippable_branch || (flags & INERT) !== 0 || batch.skipped_effects.has(effect); @@ -865,13 +858,10 @@ function process_effects(root, async_effects, render_effects, effects) { } catch (error) { handle_error(error, effect, null, effect.ctx); } + } else if (is_branch) { + effect.f ^= CLEAN; } else if ((flags & RENDER_EFFECT) !== 0) { - if (is_branch) { - // TODO clean branch later, if batch is settled - // current_effect.f ^= CLEAN; - } else { - render_effects.push(effect); - } + render_effects.push(effect); } else if ((flags & EFFECT) !== 0) { effects.push(effect); } From e5579fd738640d567f333a5e6b4a50cf1fce2dcf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 17:04:19 -0400 Subject: [PATCH 302/582] WIP --- .../svelte/src/internal/client/reactivity/sources.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index d8b609859fd6..6340c6f0b4b2 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -262,9 +262,10 @@ export function update_pre(source, d = 1) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY + * @param {boolean} partial should skip async/block effects * @returns {void} */ -export function mark_reactions(signal, status) { +export function mark_reactions(signal, status, partial = false) { var reactions = signal.reactions; if (reactions === null) return; @@ -284,10 +285,14 @@ export function mark_reactions(signal, status) { continue; } + if (partial && (flags & (EFFECT_ASYNC | BLOCK_EFFECT)) !== 0) { + continue; + } + set_signal_status(reaction, status); if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, partial); } else { schedule_effect(/** @type {Effect} */ (reaction)); } From 2e813f1b8041eb0837ec23ce3158d394ca14a4b5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 17:08:42 -0400 Subject: [PATCH 303/582] consistent behaviour --- packages/svelte/src/internal/client/reactivity/batch.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6875e2cd4e5d..4d5595f67701 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -40,9 +40,6 @@ export class Batch { #pending = 0; apply() { - // common case: no overlapping batches, nothing to revert - if (batches.size === 1) return noop; - var current_values = new Map(); for (const [source, current] of this.#current) { From 6e26478a17bd5216a30c457361521d742f3b9010 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 17:15:38 -0400 Subject: [PATCH 304/582] simplify --- packages/svelte/src/internal/client/runtime.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index dc7f99d488ef..313a8b9ed8ae 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -707,17 +707,10 @@ function flush_queued_root_effects() { var effects = []; var root_effects = queued_root_effects; - var length = root_effects.length; queued_root_effects = []; - for (var i = 0; i < length; i++) { - var root = root_effects[i]; - - if ((root.f & CLEAN) === 0) { - root.f ^= CLEAN; - } - + for (const root of root_effects) { process_effects(root, async_effects, render_effects, effects); } @@ -835,6 +828,8 @@ export function schedule_effect(signal) { * @param {Effect[]} effects */ function process_effects(root, async_effects, render_effects, effects) { + root.f ^= CLEAN; + var effect = root.first; var batch = /** @type {Batch} */ (current_batch); From 5518e98c3185a2d2ef8575f6c26644e97cb27f6e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 19:58:26 -0400 Subject: [PATCH 305/582] WIP --- .../svelte/src/internal/client/reactivity/batch.js | 13 +++++++++++-- packages/svelte/src/internal/client/runtime.js | 12 ++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 4d5595f67701..863259c3c421 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,7 +1,6 @@ /** @import { Effect, Source } from '#client' */ import { DIRTY } from '#client/constants'; -import { noop } from '../../shared/utils.js'; -import { flushSync } from '../runtime.js'; +import { schedule_effect, set_signal_status } from '../runtime.js'; import { raf } from '../timing.js'; import { internal_set, mark_reactions, pending } from './sources.js'; @@ -31,6 +30,9 @@ export class Batch { /** @type {Map} */ #current = new Map(); + /** @type {Set} */ + effects = new Set(); + /** @type {Set} */ skipped_effects = new Set(); @@ -49,6 +51,13 @@ export class Batch { source.v = current; } + for (const e of this.effects) { + if (e.fn) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + } + for (const batch of batches) { if (batch === this) continue; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 313a8b9ed8ae..94c583cc8bb0 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -718,6 +718,18 @@ function flush_queued_root_effects() { batch.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); + } else { + // store the effects on the batch so that they run next time, + // even if they don't get re-dirtied + for (const e of render_effects) { + batch.effects.add(e); + set_signal_status(e, CLEAN); + } + + for (const e of effects) { + batch.effects.add(e); + set_signal_status(e, CLEAN); + } } revert(); From c0ff1d05fb136afd82f16737112e89e3d61516b6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 19:58:40 -0400 Subject: [PATCH 306/582] tidy --- packages/svelte/src/internal/client/reactivity/batch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 863259c3c421..cfdeb679f495 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -63,7 +63,6 @@ export class Batch { for (const [source, previous] of batch.#previous) { if (!this.#previous.has(source)) { - // mark_reactions(source, DIRTY); current_values.set(source, source.v); source.v = previous; } From 0f5b3cd89b1c471e3ec9a100ad265ac09a12b756 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Apr 2025 20:52:03 -0400 Subject: [PATCH 307/582] tweak --- packages/svelte/src/internal/client/reactivity/batch.js | 6 ++---- packages/svelte/src/internal/client/runtime.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index cfdeb679f495..e4dc85919d4c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -52,10 +52,8 @@ export class Batch { } for (const e of this.effects) { - if (e.fn) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } + set_signal_status(e, DIRTY); + schedule_effect(e); } for (const batch of batches) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 94c583cc8bb0..7fb1f4b51d4d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -852,7 +852,7 @@ function process_effects(root, async_effects, render_effects, effects) { var skip = is_skippable_branch || (flags & INERT) !== 0 || batch.skipped_effects.has(effect); - if (!skip) { + if (!skip && effect.fn !== null) { if ((flags & EFFECT_ASYNC) !== 0) { if (check_dirtiness(effect)) { async_effects.push(effect); From 32f753daed64f7e82f688ec0e7ae187bd7f36f5f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 07:07:24 -0400 Subject: [PATCH 308/582] fix --- .../3-transform/client/transform-client.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 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 64719d81759f..3cb8e634693b 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 @@ -369,10 +369,24 @@ export function client_component(analysis, options) { : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) ]); + const should_inject_context = + dev || + analysis.needs_context || + analysis.reactive_statements.size > 0 || + component_returned_object.length > 0; + + let should_inject_props = + should_inject_context || + analysis.needs_props || + analysis.uses_props || + analysis.uses_rest_props || + analysis.uses_slots || + analysis.slot_names.size > 0; + if (analysis.instance.has_await) { const body = b.function_declaration( b.id('$$body'), - [b.id('$$anchor'), b.id('$$props')], + should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')], b.block([ b.var('$$unsuspend', b.call('$.suspend')), ...component_block.body, @@ -388,7 +402,7 @@ export function client_component(analysis, options) { 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(body.id, b.id('node'), should_inject_props && b.id('$$props'))), b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) ]); } else { @@ -428,12 +442,6 @@ export function client_component(analysis, options) { ); } - const should_inject_context = - dev || - analysis.needs_context || - analysis.reactive_statements.size > 0 || - component_returned_object.length > 0; - // we want the cleanup function for the stores to run as the very last thing // so that it can effectively clean up the store subscription even after the user effects runs if (should_inject_context) { @@ -499,14 +507,6 @@ export function client_component(analysis, options) { component_block.body.unshift(b.const('$$slots', b.call('$.sanitize_slots', b.id('$$props')))); } - let should_inject_props = - should_inject_context || - analysis.needs_props || - analysis.uses_props || - analysis.uses_rest_props || - analysis.uses_slots || - analysis.slot_names.size > 0; - // Merge hoisted statements into module body. // Ensure imports are on top, with the order preserved, then module body, then hoisted statements /** @type {ESTree.ImportDeclaration[]} */ From f73a5e94b4487aa41c5b1239d6af851e38535291 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 07:07:36 -0400 Subject: [PATCH 309/582] compile playground with dev: false --- playgrounds/sandbox/run.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 9c6a8616d05f..c053f7e29aac 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -73,7 +73,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { } const compiled = compile(source, { - dev: true, + dev: false, filename: input, generate, runes: argv.values.runes, @@ -101,7 +101,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { const source = fs.readFileSync(input, 'utf-8'); const compiled = compileModule(source, { - dev: true, + dev: false, filename: input, generate, experimental: { From 2087b3eafecaa88a51bb84fd7d29a75ad70d01f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 07:07:41 -0400 Subject: [PATCH 310/582] failing test --- .../samples/async-derived-in-if/Child.svelte | 5 ++++ .../samples/async-derived-in-if/_config.js | 30 +++++++++++++++++++ .../samples/async-derived-in-if/main.svelte | 17 +++++++++++ 3 files changed, 52 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte new file mode 100644 index 000000000000..fb47377513a7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte @@ -0,0 +1,5 @@ + + +

{n}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js new file mode 100644 index 000000000000..ffb31631d388 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js @@ -0,0 +1,30 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

pending

+ `, + + async test({ assert, target }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1

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

pending

+ {/snippet} +
From ec814910933746fb17301c43a180cc042c5140bc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 09:25:15 -0400 Subject: [PATCH 311/582] shuffle --- .../svelte/src/internal/client/reactivity/batch.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e4dc85919d4c..a325fe6e1813 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -30,17 +30,17 @@ export class Batch { /** @type {Map} */ #current = new Map(); + /** @type {Set<() => void>} */ + #callbacks = new Set(); + + #pending = 0; + /** @type {Set} */ effects = new Set(); /** @type {Set} */ skipped_effects = new Set(); - /** @type {Set<() => void>} */ - #callbacks = new Set(); - - #pending = 0; - apply() { var current_values = new Map(); From 45f4cc5ffb4ec11f11d34e2e92758db6fc577705 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:20:44 -0400 Subject: [PATCH 312/582] WIP --- packages/svelte/src/internal/client/reactivity/batch.js | 6 ++++-- packages/svelte/src/internal/client/runtime.js | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a325fe6e1813..3da142f02776 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -35,8 +35,8 @@ export class Batch { #pending = 0; - /** @type {Set} */ - effects = new Set(); + /** @type {Effect[]} */ + effects = []; /** @type {Set} */ skipped_effects = new Set(); @@ -56,6 +56,8 @@ export class Batch { schedule_effect(e); } + this.effects = []; + for (const batch of batches) { if (batch === this) continue; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 7fb1f4b51d4d..e6007cb9ebb8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -722,14 +722,14 @@ function flush_queued_root_effects() { // store the effects on the batch so that they run next time, // even if they don't get re-dirtied for (const e of render_effects) { - batch.effects.add(e); set_signal_status(e, CLEAN); } for (const e of effects) { - batch.effects.add(e); set_signal_status(e, CLEAN); } + + batch.effects.push(...render_effects, ...effects); } revert(); From 9e0bd4f24b189ca93325aa68afe1735421e245ac Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:22:00 -0400 Subject: [PATCH 313/582] WIP --- packages/svelte/src/internal/client/runtime.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e6007cb9ebb8..bd6b0d9a80ec 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -711,7 +711,7 @@ function flush_queued_root_effects() { queued_root_effects = []; for (const root of root_effects) { - process_effects(root, async_effects, render_effects, effects); + process_effects(batch, root, async_effects, render_effects, effects); } if (async_effects.length === 0 && batch.settled()) { @@ -834,16 +834,16 @@ export function schedule_effect(signal) { * bitwise flag passed in only. The collected effects array will be populated with all the user * effects to be flushed. * + * @param {Batch} batch * @param {Effect} root * @param {Effect[]} async_effects * @param {Effect[]} render_effects * @param {Effect[]} effects */ -function process_effects(root, async_effects, render_effects, effects) { +function process_effects(batch, root, async_effects, render_effects, effects) { root.f ^= CLEAN; var effect = root.first; - var batch = /** @type {Batch} */ (current_batch); while (effect !== null) { var flags = effect.f; From 0bc6e6977e5cb386e1436907ba4b7f5a50f5d0c3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:28:36 -0400 Subject: [PATCH 314/582] WIP --- .../src/internal/client/reactivity/batch.js | 11 ++++++++++- packages/svelte/src/internal/client/runtime.js | 16 ++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 3da142f02776..a8849bf1c90e 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,6 +1,6 @@ /** @import { Effect, Source } from '#client' */ import { DIRTY } from '#client/constants'; -import { schedule_effect, set_signal_status } from '../runtime.js'; +import { schedule_effect, set_signal_status, update_effect } from '../runtime.js'; import { raf } from '../timing.js'; import { internal_set, mark_reactions, pending } from './sources.js'; @@ -35,6 +35,9 @@ export class Batch { #pending = 0; + /** @type {Effect[]} */ + async_effects = []; + /** @type {Effect[]} */ effects = []; @@ -73,6 +76,12 @@ export class Batch { for (const [source, value] of current_values) { source.v = value; } + + for (const effect of this.async_effects) { + update_effect(effect); + } + + this.async_effects = []; }; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index bd6b0d9a80ec..599977408e12 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -697,9 +697,6 @@ function flush_queued_root_effects() { var revert = batch.apply(); - /** @type {Effect[]} */ - var async_effects = []; - /** @type {Effect[]} */ var render_effects = []; @@ -711,10 +708,10 @@ function flush_queued_root_effects() { queued_root_effects = []; for (const root of root_effects) { - process_effects(batch, root, async_effects, render_effects, effects); + process_effects(batch, root, render_effects, effects); } - if (async_effects.length === 0 && batch.settled()) { + if (batch.async_effects.length === 0 && batch.settled()) { batch.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -734,10 +731,6 @@ function flush_queued_root_effects() { revert(); - for (const effect of async_effects) { - update_effect(effect); - } - old_values.clear(); } } finally { @@ -836,11 +829,10 @@ export function schedule_effect(signal) { * * @param {Batch} batch * @param {Effect} root - * @param {Effect[]} async_effects * @param {Effect[]} render_effects * @param {Effect[]} effects */ -function process_effects(batch, root, async_effects, render_effects, effects) { +function process_effects(batch, root, render_effects, effects) { root.f ^= CLEAN; var effect = root.first; @@ -855,7 +847,7 @@ function process_effects(batch, root, async_effects, render_effects, effects) { if (!skip && effect.fn !== null) { if ((flags & EFFECT_ASYNC) !== 0) { if (check_dirtiness(effect)) { - async_effects.push(effect); + batch.async_effects.push(effect); } } else if ((flags & BLOCK_EFFECT) !== 0) { try { From 43eeca965b17d4e36865101bec227669d9080ed2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:30:31 -0400 Subject: [PATCH 315/582] WIP --- packages/svelte/src/internal/client/reactivity/batch.js | 6 +++--- packages/svelte/src/internal/client/runtime.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a8849bf1c90e..77622baf8866 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -39,7 +39,7 @@ export class Batch { async_effects = []; /** @type {Effect[]} */ - effects = []; + combined_effects = []; /** @type {Set} */ skipped_effects = new Set(); @@ -54,12 +54,12 @@ export class Batch { source.v = current; } - for (const e of this.effects) { + for (const e of this.combined_effects) { set_signal_status(e, DIRTY); schedule_effect(e); } - this.effects = []; + this.combined_effects = []; for (const batch of batches) { if (batch === this) continue; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 599977408e12..9f6695e9208d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -726,7 +726,7 @@ function flush_queued_root_effects() { set_signal_status(e, CLEAN); } - batch.effects.push(...render_effects, ...effects); + batch.combined_effects.push(...render_effects, ...effects); } revert(); From 1cb4e246778ac157746546d96c636151a7b84687 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:31:52 -0400 Subject: [PATCH 316/582] WIP --- packages/svelte/src/internal/client/reactivity/batch.js | 9 +++++++++ packages/svelte/src/internal/client/runtime.js | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 77622baf8866..13b8cf4709b8 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,6 +38,12 @@ export class Batch { /** @type {Effect[]} */ async_effects = []; + /** @type {Effect[]} */ + render_effects = []; + + /** @type {Effect[]} */ + effects = []; + /** @type {Effect[]} */ combined_effects = []; @@ -72,6 +78,9 @@ export class Batch { } } + this.render_effects = []; + this.effects = []; + return () => { for (const [source, value] of current_values) { source.v = value; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9f6695e9208d..e5e02e058569 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -698,10 +698,10 @@ function flush_queued_root_effects() { var revert = batch.apply(); /** @type {Effect[]} */ - var render_effects = []; + var render_effects = batch.render_effects; /** @type {Effect[]} */ - var effects = []; + var effects = batch.effects; var root_effects = queued_root_effects; From 21e4c440314b55b541ab5ddea1f366b7ac4a9c81 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:35:47 -0400 Subject: [PATCH 317/582] WIP --- .../svelte/src/internal/client/runtime.js | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e5e02e058569..addcfc29a089 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -697,36 +697,29 @@ function flush_queued_root_effects() { var revert = batch.apply(); - /** @type {Effect[]} */ - var render_effects = batch.render_effects; - - /** @type {Effect[]} */ - var effects = batch.effects; - var root_effects = queued_root_effects; - queued_root_effects = []; for (const root of root_effects) { - process_effects(batch, root, render_effects, effects); + process_effects(batch, root); } if (batch.async_effects.length === 0 && batch.settled()) { batch.commit(); - flush_queued_effects(render_effects); - flush_queued_effects(effects); + flush_queued_effects(batch.render_effects); + flush_queued_effects(batch.effects); } else { // store the effects on the batch so that they run next time, // even if they don't get re-dirtied - for (const e of render_effects) { + for (const e of batch.render_effects) { set_signal_status(e, CLEAN); } - for (const e of effects) { + for (const e of batch.effects) { set_signal_status(e, CLEAN); } - batch.combined_effects.push(...render_effects, ...effects); + batch.combined_effects.push(...batch.render_effects, ...batch.effects); } revert(); @@ -829,10 +822,8 @@ export function schedule_effect(signal) { * * @param {Batch} batch * @param {Effect} root - * @param {Effect[]} render_effects - * @param {Effect[]} effects */ -function process_effects(batch, root, render_effects, effects) { +function process_effects(batch, root) { root.f ^= CLEAN; var effect = root.first; @@ -860,9 +851,9 @@ function process_effects(batch, root, render_effects, effects) { } else if (is_branch) { effect.f ^= CLEAN; } else if ((flags & RENDER_EFFECT) !== 0) { - render_effects.push(effect); + batch.render_effects.push(effect); } else if ((flags & EFFECT) !== 0) { - effects.push(effect); + batch.effects.push(effect); } var child = effect.first; From 1c313630d2e2235dd5a3f674986dfbffbe0b5f11 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:45:28 -0400 Subject: [PATCH 318/582] WIP --- .../svelte/src/internal/client/reactivity/batch.js | 7 ++++++- packages/svelte/src/internal/client/runtime.js | 10 ++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 13b8cf4709b8..0381a99cbd26 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -60,7 +60,12 @@ export class Batch { source.v = current; } - for (const e of this.combined_effects) { + for (const e of this.render_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + for (const e of this.effects) { set_signal_status(e, DIRTY); schedule_effect(e); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index addcfc29a089..044f1ab240b3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -705,9 +705,15 @@ function flush_queued_root_effects() { } if (batch.async_effects.length === 0 && batch.settled()) { + var render_effects = batch.render_effects; + var effects = batch.effects; + + batch.render_effects = []; + batch.effects = []; + batch.commit(); - flush_queued_effects(batch.render_effects); - flush_queued_effects(batch.effects); + flush_queued_effects(render_effects); + flush_queued_effects(effects); } else { // store the effects on the batch so that they run next time, // even if they don't get re-dirtied From 4680f386232ef8632f7748882ac3dcbc0f7a505e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:51:02 -0400 Subject: [PATCH 319/582] WIP --- .../src/internal/client/reactivity/batch.js | 29 ++++++++++++++----- .../svelte/src/internal/client/runtime.js | 26 +---------------- 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0381a99cbd26..419f62d4fd48 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,6 +1,11 @@ /** @import { Effect, Source } from '#client' */ -import { DIRTY } from '#client/constants'; -import { schedule_effect, set_signal_status, update_effect } from '../runtime.js'; +import { CLEAN, DIRTY } from '#client/constants'; +import { + flush_queued_effects, + schedule_effect, + set_signal_status, + update_effect +} from '../runtime.js'; import { raf } from '../timing.js'; import { internal_set, mark_reactions, pending } from './sources.js'; @@ -44,9 +49,6 @@ export class Batch { /** @type {Effect[]} */ effects = []; - /** @type {Effect[]} */ - combined_effects = []; - /** @type {Set} */ skipped_effects = new Set(); @@ -70,8 +72,6 @@ export class Batch { schedule_effect(e); } - this.combined_effects = []; - for (const batch of batches) { if (batch === this) continue; @@ -87,6 +87,21 @@ export class Batch { this.effects = []; return () => { + if (this.async_effects.length === 0 && this.settled()) { + var render_effects = this.render_effects; + var effects = this.effects; + + this.render_effects = []; + this.effects = []; + + this.commit(); + flush_queued_effects(render_effects); + flush_queued_effects(effects); + } else { + for (const e of this.render_effects) set_signal_status(e, CLEAN); + for (const e of this.effects) set_signal_status(e, CLEAN); + } + for (const [source, value] of current_values) { source.v = value; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 044f1ab240b3..d39d3842471d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -704,30 +704,6 @@ function flush_queued_root_effects() { process_effects(batch, root); } - if (batch.async_effects.length === 0 && batch.settled()) { - var render_effects = batch.render_effects; - var effects = batch.effects; - - batch.render_effects = []; - batch.effects = []; - - batch.commit(); - flush_queued_effects(render_effects); - flush_queued_effects(effects); - } else { - // store the effects on the batch so that they run next time, - // even if they don't get re-dirtied - for (const e of batch.render_effects) { - set_signal_status(e, CLEAN); - } - - for (const e of batch.effects) { - set_signal_status(e, CLEAN); - } - - batch.combined_effects.push(...batch.render_effects, ...batch.effects); - } - revert(); old_values.clear(); @@ -747,7 +723,7 @@ function flush_queued_root_effects() { * @param {Array} effects * @returns {void} */ -function flush_queued_effects(effects) { +export function flush_queued_effects(effects) { var length = effects.length; if (length === 0) return; From 29147fb70a0192f926c5fba9836f2f7169827c00 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 11:54:46 -0400 Subject: [PATCH 320/582] WIP --- .../src/internal/client/reactivity/batch.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 419f62d4fd48..e321e8478d11 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -94,7 +94,13 @@ export class Batch { this.render_effects = []; this.effects = []; - this.commit(); + // commit changes + for (const fn of this.#callbacks) { + fn(); + } + + this.#callbacks.clear(); + flush_queued_effects(render_effects); flush_queued_effects(effects); } else { @@ -173,14 +179,6 @@ export class Batch { this.#callbacks.add(fn); } - commit() { - for (const fn of this.#callbacks) { - fn(); - } - - this.#callbacks.clear(); - } - static ensure() { if (current_batch === null) { if (batches.size === 0) { From abba96cac0a764b136020daca291b00044870832 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 12:04:30 -0400 Subject: [PATCH 321/582] WIP --- .../src/internal/client/reactivity/batch.js | 72 +++++++++++-------- .../svelte/src/internal/client/runtime.js | 18 ++--- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e321e8478d11..2970a44b6153 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -2,7 +2,9 @@ import { CLEAN, DIRTY } from '#client/constants'; import { flush_queued_effects, + process_effects, schedule_effect, + set_queued_root_effects, set_signal_status, update_effect } from '../runtime.js'; @@ -52,9 +54,28 @@ export class Batch { /** @type {Set} */ skipped_effects = new Set(); - apply() { + apply() {} + + /** + * + * @param {Effect[]} root_effects + */ + process(root_effects) { + set_queued_root_effects([]); + var current_values = new Map(); + for (const batch of batches) { + if (batch === this) continue; + + for (const [source, previous] of batch.#previous) { + if (!this.#current.has(source)) { + current_values.set(source, source.v); + source.v = previous; + } + } + } + for (const [source, current] of this.#current) { // TODO this shouldn't be necessary, but tests fail otherwise, // presumably because we need a try-finally somewhere, and the @@ -72,42 +93,33 @@ export class Batch { schedule_effect(e); } - for (const batch of batches) { - if (batch === this) continue; - - for (const [source, previous] of batch.#previous) { - if (!this.#previous.has(source)) { - current_values.set(source, source.v); - source.v = previous; - } - } - } - this.render_effects = []; this.effects = []; - return () => { - if (this.async_effects.length === 0 && this.settled()) { - var render_effects = this.render_effects; - var effects = this.effects; - - this.render_effects = []; - this.effects = []; + for (const root of root_effects) { + process_effects(this, root); + } - // commit changes - for (const fn of this.#callbacks) { - fn(); - } + if (this.async_effects.length === 0 && this.settled()) { + var render_effects = this.render_effects; + var effects = this.effects; - this.#callbacks.clear(); + this.render_effects = []; + this.effects = []; - flush_queued_effects(render_effects); - flush_queued_effects(effects); - } else { - for (const e of this.render_effects) set_signal_status(e, CLEAN); - for (const e of this.effects) set_signal_status(e, CLEAN); + // commit changes + for (const fn of this.#callbacks) { + fn(); } + this.#callbacks.clear(); + + flush_queued_effects(render_effects); + flush_queued_effects(effects); + } else { + for (const e of this.render_effects) set_signal_status(e, CLEAN); + for (const e of this.effects) set_signal_status(e, CLEAN); + for (const [source, value] of current_values) { source.v = value; } @@ -117,7 +129,7 @@ export class Batch { } this.async_effects = []; - }; + } } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d39d3842471d..43ceb408bd66 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -77,6 +77,11 @@ export function set_is_destroying_effect(value) { /** @type {Effect[]} */ let queued_root_effects = []; +/** @param {Effect[]} v */ +export function set_queued_root_effects(v) { + queued_root_effects = v; +} + /** @type {Effect[]} Stack of effects, dev only */ let dev_effect_stack = []; // Handle signal reactivity tree dependencies and reactions @@ -695,16 +700,7 @@ function flush_queued_root_effects() { infinite_loop_guard(); } - var revert = batch.apply(); - - var root_effects = queued_root_effects; - queued_root_effects = []; - - for (const root of root_effects) { - process_effects(batch, root); - } - - revert(); + batch.process(queued_root_effects); old_values.clear(); } @@ -805,7 +801,7 @@ export function schedule_effect(signal) { * @param {Batch} batch * @param {Effect} root */ -function process_effects(batch, root) { +export function process_effects(batch, root) { root.f ^= CLEAN; var effect = root.first; From 6f8abda5613a6b5243a66e8ea7fc35ef74b2044e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 12:06:20 -0400 Subject: [PATCH 322/582] fix --- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2970a44b6153..f9aa98939e0c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -23,7 +23,7 @@ export function remove_current_batch() { /** Update `$effect.pending()` */ function update_pending() { - // internal_set(pending, batches.size > 0); + internal_set(pending, batches.size > 0); } let uid = 1; From 48293d205dd601d6b8565e03188e476ee5020d8c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Apr 2025 13:42:09 -0400 Subject: [PATCH 323/582] fix --- .../src/internal/client/reactivity/batch.js | 20 +++++++++++++------ .../samples/async-derived-in-if/_config.js | 5 ----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index f9aa98939e0c..ab4497ead92f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -107,12 +107,7 @@ export class Batch { this.render_effects = []; this.effects = []; - // commit changes - for (const fn of this.#callbacks) { - fn(); - } - - this.#callbacks.clear(); + this.commit(); flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -174,12 +169,25 @@ export class Batch { fn(); } + commit() { + // commit changes + for (const fn of this.#callbacks) { + fn(); + } + + this.#callbacks.clear(); + } + increment() { this.#pending += 1; } decrement() { this.#pending -= 1; + + if (this.#pending === 0) { + this.commit(); + } } settled() { diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js index ffb31631d388..ab020d85f749 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js @@ -2,11 +2,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - html: ` - -

pending

- `, - async test({ assert, target }) { const button = target.querySelector('button'); From d7d528c04699b77a9f70bd54a0406c822df5e92e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 23 Apr 2025 19:15:46 +0100 Subject: [PATCH 324/582] fix `$effect.pending()` --- packages/svelte/src/internal/client/reactivity/batch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ab4497ead92f..68d0457fc211 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -157,8 +157,6 @@ export class Batch { } } } - - update_pending(); } /** @@ -176,6 +174,8 @@ export class Batch { } this.#callbacks.clear(); + + raf.tick(update_pending); } increment() { From b8052451b76ce96d8bc93f4f1da43dd309800fa8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 25 Apr 2025 13:56:51 +0100 Subject: [PATCH 325/582] fix --- .../src/internal/client/reactivity/batch.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 68d0457fc211..0e3c1196a56a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -83,19 +83,6 @@ export class Batch { source.v = current; } - for (const e of this.render_effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - this.render_effects = []; - this.effects = []; - for (const root of root_effects) { process_effects(this, root); } @@ -186,6 +173,19 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { + for (const e of this.render_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + for (const e of this.effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + this.render_effects = []; + this.effects = []; + this.commit(); } } From 734f56c6ebb8d673e2dd817b9bcef9d143d0026f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 28 Apr 2025 20:10:18 +0100 Subject: [PATCH 326/582] fix --- .../client/visitors/RegularElement.js | 2 +- .../src/internal/client/reactivity/deriveds.js | 2 +- .../async-attribute-without-state/_config.js | 18 ++++++++++++++++++ .../async-attribute-without-state/main.svelte | 7 +++++++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte 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 5c424a4a5f14..e4159bf3e9cc 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 @@ -309,7 +309,7 @@ export function RegularElement(node, context) { attribute.value, context, (value, metadata) => - metadata.has_call + metadata.has_call || metadata.has_await ? get_expression_id( metadata.has_await ? context.state.async_expressions : context.state.expressions, value diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b46b88fd2c1e..575370af9aa0 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -119,7 +119,7 @@ export function async_derived(fn, location) { render_effect(() => { if (DEV) from_async_derived = active_effect; - promise = fn(); + promise = Promise.resolve(fn()); if (DEV) from_async_derived = null; var restore = capture(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js new file mode 100644 index 000000000000..3de81a507b59 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js @@ -0,0 +1,18 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` +

pending

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

hello

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

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
From d7f580d2cb1620cad9a4bf07fdcd6aa656d42451 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 30 Apr 2025 12:35:25 +0100 Subject: [PATCH 327/582] fix 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 0646b78e840f..eec83c3c2c52 100644 --- a/.changeset/eleven-weeks-dance.md +++ b/.changeset/eleven-weeks-dance.md @@ -1,5 +1,5 @@ --- -'svelte': patch +'svelte': minor --- feat: support `await` in components From 399bda5d7c40f90b79b73398699bfae4f03e2b98 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 May 2025 13:58:22 +0100 Subject: [PATCH 328/582] lint --- .../2-analyze/visitors/AwaitExpression.js | 2 +- .../src/internal/client/dom/blocks/each.js | 2 +- .../src/internal/client/reactivity/batch.js | 7 ++++-- .../internal/client/reactivity/deriveds.js | 23 +++++++++++++------ 4 files changed, 23 insertions(+), 11 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 8f195f01598b..4f50d447f7d6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -26,7 +26,7 @@ export function AwaitExpression(node, context) { // @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 + // TODO make this more accurate — we don't need to call suspend // if this is the last thing that could be read preserve_context = true; } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index cb0d45e1ed55..2dfd657e3454 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -283,7 +283,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f update_item(existing, value, i, flags); } } else { - var item = create_item( + item = create_item( null, state, null, diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0e3c1196a56a..1172a3a33b6f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -130,14 +130,17 @@ export class Batch { batches.delete(this); for (var batch of batches) { + /** @type {Source} */ + var source; + if (batch.#id < this.#id) { // other batch is older than this - for (var source of this.#previous.keys()) { + for (source of this.#previous.keys()) { batch.#previous.delete(source); } } else { // other batch is newer than this - for (var source of batch.#previous.keys()) { + for (source of batch.#previous.keys()) { if (this.#previous.has(source)) { batch.#previous.set(source, source.v); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 575370af9aa0..314595c9378d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -89,7 +89,7 @@ export function derived(fn) { /** * @template V - * @param {() => Promise} fn + * @param {() => V | Promise} fn * @param {string} [location] If provided, print a warning if the value is not read immediately after update * @returns {Promise>} */ @@ -173,12 +173,21 @@ export function async_derived(fn, location) { ); }, EFFECT_ASYNC | EFFECT_PRESERVED); - return new Promise(async (fulfil) => { - // if the effect re-runs before the initial promise - // resolves, delay resolution until we have a value - var p; - while (p !== (p = promise)) await p; - fulfil(signal); + return new Promise((fulfil) => { + /** @param {Promise} p */ + function next(p) { + p.then(() => { + if (p === promise) { + fulfil(signal); + } else { + // if the effect re-runs before the initial promise + // resolves, delay resolution until we have a value + next(promise); + } + }); + } + + next(promise); }); } From a98b5eaf5fabbded4829238063a9df5f0ce13bfb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 May 2025 17:00:18 +0200 Subject: [PATCH 329/582] note to self --- packages/svelte/src/internal/client/dom/blocks/async.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index fe34167d7c04..13116c50fee2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -22,6 +22,7 @@ export function async(node, expressions, fn) { restore(); fn(node, ...result); + // TODO is this necessary? batch.run(() => { schedule_effect(effect); }); From fc18e26bdd2bbdc85519ee59b3a7b04e88f395b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 May 2025 17:36:11 +0200 Subject: [PATCH 330/582] failing test --- .../async-waterfall-on-init/_config.js | 50 +++++++++++++++++++ .../async-waterfall-on-init/main.svelte | 22 ++++++++ 2 files changed, 72 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js new file mode 100644 index 000000000000..91c388e0ca92 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js @@ -0,0 +1,50 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +
+

pending

+ `, + + async test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + flushSync(() => button1.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + + +
+

pending

+ ` + ); + + flushSync(() => button2.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + + +
+ +

true

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte new file mode 100644 index 000000000000..86af9bb07eab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte @@ -0,0 +1,22 @@ + + + + + +
+ + + {#if await d1.promise} + +

{await d2.promise}

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

pending

+ {/snippet} +
From ed172125b282abc8eb2f3d81c11deeaac5d12d11 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 May 2025 18:02:07 +0200 Subject: [PATCH 331/582] fix --- .../src/internal/client/dom/blocks/async.js | 22 +++++++++++++++---- .../internal/client/dom/blocks/boundary.js | 15 ++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 13116c50fee2..db6a7fda7967 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -18,13 +18,27 @@ export function async(node, expressions, fn) { var restore = capture(); - Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { - restore(); - fn(node, ...result); + let boundary = effect.b; + + while (boundary !== null && !boundary.has_pending_snippet()) { + boundary = boundary.parent; + } + + if (boundary === null) { + throw new Error('TODO cannot create async derived outside a boundary with a pending snippet'); + } + + boundary.increment(); - // TODO is this necessary? + Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { batch.run(() => { + restore(); + fn(node, ...result); + + // TODO is this necessary? schedule_effect(effect); }); + + boundary.decrement(); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 95db4dfefc5c..a98a354bd093 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -119,10 +119,13 @@ export class Boundary { return branch(() => this.#children(this.#anchor)); }); - if (this.#pending_count === 0) { + if (this.#pending_count > 0) { + this.#show_pending_snippet(); + } else { pause_effect(/** @type {Effect} */ (this.#pending_effect), () => { this.#pending_effect = null; }); + this.ran = true; } }); } else { @@ -130,14 +133,14 @@ export class Boundary { if (this.#pending_count > 0) { this.#show_pending_snippet(); + } else { + this.ran = true; } } reset_is_throwing_error(); }, flags); - this.ran = true; - if (hydrating) { this.#anchor = hydrate_node; } @@ -189,6 +192,8 @@ export class Boundary { } commit() { + this.ran = true; + if (this.#pending_effect) { pause_effect(this.#pending_effect, () => { this.#pending_effect = null; @@ -242,10 +247,10 @@ export class Boundary { } }); - this.ran = true; - if (this.#pending_count > 0) { this.#show_pending_snippet(); + } else { + this.ran = true; } }; From 78dd1e23ee8dc19e7f73bfdd2c2093da385189c0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 May 2025 18:06:27 +0200 Subject: [PATCH 332/582] DRY --- .../src/internal/client/dom/blocks/async.js | 13 ++------ .../internal/client/dom/blocks/boundary.js | 30 ++++++++++--------- .../internal/client/reactivity/deriveds.js | 12 ++------ 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index db6a7fda7967..c3283081abe9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -3,7 +3,7 @@ import { async_derived } from '../../reactivity/deriveds.js'; import { current_batch } from '../../reactivity/batch.js'; import { active_effect, schedule_effect } from '../../runtime.js'; -import { capture } from './boundary.js'; +import { capture, get_pending_boundary } from './boundary.js'; /** * @param {TemplateNode} node @@ -15,19 +15,10 @@ export function async(node, expressions, fn) { var batch = /** @type {Batch} */ (current_batch); var effect = /** @type {Effect} */ (active_effect); + var boundary = get_pending_boundary(effect); var restore = capture(); - let boundary = effect.b; - - while (boundary !== null && !boundary.has_pending_snippet()) { - boundary = boundary.parent; - } - - if (boundary === null) { - throw new Error('TODO cannot create async derived outside a boundary with a pending snippet'); - } - boundary.increment(); Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a98a354bd093..2f7c0d2e4d37 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -327,6 +327,21 @@ function move_effect(effect, fragment) { } } +/** @param {Effect} effect */ +export function get_pending_boundary(effect) { + let boundary = effect.b; + + while (boundary !== null && !boundary.has_pending_snippet()) { + boundary = boundary.parent; + } + + if (boundary === null) { + e.await_outside_boundary(); + } + + return boundary; +} + export function capture(track = true) { var previous_effect = active_effect; var previous_reaction = active_reaction; @@ -352,20 +367,7 @@ export function capture(track = true) { // TODO we should probably be incrementing the current batch, not the boundary? export function suspend() { - let boundary = /** @type {Effect} */ (active_effect).b; - - while (boundary !== null) { - // TODO pretty sure this is wrong - if (boundary.has_pending_snippet()) { - break; - } - - boundary = boundary.parent; - } - - if (boundary === null) { - e.await_outside_boundary(); - } + let boundary = get_pending_boundary(/** @type {Effect} */ (active_effect)); boundary.increment(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 314595c9378d..5c33a827ddc3 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -29,7 +29,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 { capture } from '../dom/blocks/boundary.js'; +import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; @@ -101,15 +101,7 @@ export function async_derived(fn, location) { throw new Error('TODO cannot create unowned async derived'); } - let boundary = parent.b; - - while (boundary !== null && !boundary.has_pending_snippet()) { - boundary = boundary.parent; - } - - if (boundary === null) { - throw new Error('TODO cannot create async derived outside a boundary with a pending snippet'); - } + let boundary = get_pending_boundary(parent); var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); From 13a9b70f78b286e63909b5970d6ed3dd2148f4b6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 May 2025 18:30:44 +0200 Subject: [PATCH 333/582] failing test for linear order --- .../samples/async-linear-order/_config.js | 42 +++++++++++++++++++ .../samples/async-linear-order/main.svelte | 31 ++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order/_config.js new file mode 100644 index 000000000000..76bfbe56d633 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order/_config.js @@ -0,0 +1,42 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, reset1, reset2, resolve1, resolve2] = target.querySelectorAll('button'); + + flushSync(() => resolve1.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + const p = /** @type {HTMLElement} */ (target.querySelector('#test')); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + flushSync(() => reset1.click()); + flushSync(() => a.click()); + flushSync(() => reset2.click()); + flushSync(() => b.click()); + + flushSync(() => resolve2.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + flushSync(() => resolve1.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + + assert.htmlEqual(p.innerHTML, '2 + 3 = 5'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order/main.svelte new file mode 100644 index 000000000000..cc82db0d7559 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + + + +

{a} + {b} = {await add(a, b)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 1dd383ea5416551ac0314d35ef58254f28232bb0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 May 2025 18:48:46 +0200 Subject: [PATCH 334/582] enforce linear order --- .../internal/client/reactivity/deriveds.js | 19 ++++++++++++++++++- .../_config.js | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5c33a827ddc3..c508e515c03d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -106,14 +106,27 @@ export function async_derived(fn, location) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); + /** @type {Promise | null} */ + var prev = null; + // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; render_effect(() => { if (DEV) from_async_derived = active_effect; - promise = Promise.resolve(fn()); + var p = fn(); if (DEV) from_async_derived = null; + promise = + prev === null + ? Promise.resolve(p) + : prev.then( + () => p, + () => p + ); + + prev = promise; + var restore = capture(); var batch = /** @type {Batch} */ (current_batch); @@ -129,6 +142,8 @@ export function async_derived(fn, location) { promise.then( (v) => { + prev = null; + if ((parent.f & DESTROYED) !== 0) { return; } @@ -160,6 +175,8 @@ export function async_derived(fn, location) { } }, (e) => { + prev = null; + handle_error(e, parent, null, parent.ctx); } ); 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 index c8f20d9597bd..99f91503e139 100644 --- 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 @@ -34,6 +34,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); await tick(); assert.htmlEqual(target.innerHTML, '

hello

'); From 7762f2926072d2c46e8c098e674533aa3f3d85a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 08:41:51 +0200 Subject: [PATCH 335/582] update test --- .../samples/async-expression/_config.js | 53 +++++++++++++------ .../samples/async-expression/main.svelte | 8 ++- 2 files changed, 44 insertions(+), 17 deletions(-) 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 6cded1a1d1ba..17ca961fc611 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -6,30 +6,53 @@ import { test } from '../../test'; let d; export default test({ - html: `

pending

`, + html: ` + + + +

pending

+ `, - get props() { - d = deferred(); + async test({ assert, target }) { + const [reset, hello, goodbye] = target.querySelectorAll('button'); - return { - promise: d.promise - }; - }, - - async test({ assert, target, component }) { - d.resolve('hello'); + flushSync(() => hello.click()); await Promise.resolve(); await Promise.resolve(); await tick(); flushSync(); - assert.htmlEqual(target.innerHTML, '

hello

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

hello

+ ` + ); - component.promise = (d = deferred()).promise; + flushSync(() => reset.click()); await tick(); - assert.htmlEqual(target.innerHTML, '

hello

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

hello

+ ` + ); - d.resolve('wheee'); + flushSync(() => goodbye.click()); await tick(); - assert.htmlEqual(target.innerHTML, '

wheee

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

goodbye

+ ` + ); } }); 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 3c6879caee08..6fc90ff2df73 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte @@ -1,9 +1,13 @@ + + + + -

{await promise}

+

{await deferred.promise}

{#snippet pending()}

pending

From 8baf1644a7ed5976864df2880b3ffadab4ffb0c1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 09:46:29 +0200 Subject: [PATCH 336/582] fix --- .../src/internal/client/reactivity/batch.js | 16 ++++++++-------- .../samples/async-expression/_config.js | 11 ++++++----- .../samples/async-expression/main.svelte | 4 ++++ 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 1172a3a33b6f..8f36e9e69320 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -101,17 +101,17 @@ export class Batch { } else { for (const e of this.render_effects) set_signal_status(e, CLEAN); for (const e of this.effects) set_signal_status(e, CLEAN); + } - for (const [source, value] of current_values) { - source.v = value; - } - - for (const effect of this.async_effects) { - update_effect(effect); - } + for (const [source, value] of current_values) { + source.v = value; + } - this.async_effects = []; + for (const effect of this.async_effects) { + update_effect(effect); } + + this.async_effects = []; } /** 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 17ca961fc611..c44d112625fa 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -1,10 +1,6 @@ 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: ` @@ -13,10 +9,11 @@ export default test({

pending

`, - async test({ assert, target }) { + async test({ assert, target, raf }) { const [reset, hello, goodbye] = target.querySelectorAll('button'); flushSync(() => hello.click()); + raf.tick(0); await Promise.resolve(); await Promise.resolve(); await tick(); @@ -32,6 +29,7 @@ export default test({ ); flushSync(() => reset.click()); + raf.tick(0); await tick(); assert.htmlEqual( target.innerHTML, @@ -40,10 +38,13 @@ export default test({

hello

+

updating...

` ); flushSync(() => goodbye.click()); + await Promise.resolve(); + raf.tick(0); await tick(); assert.htmlEqual( target.innerHTML, 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 6fc90ff2df73..42536ab02a82 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte @@ -9,6 +9,10 @@

{await deferred.promise}

+ {#if $effect.pending()} +

updating...

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

pending

{/snippet} From 666a148f6457fb03260e0dc762068b95201fd271 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 11:47:53 +0200 Subject: [PATCH 337/582] implement getAbortSignal --- packages/svelte/src/index-client.js | 10 ++++- packages/svelte/src/index-server.js | 15 ++++++++ .../svelte/src/internal/client/constants.js | 2 + .../internal/client/reactivity/deriveds.js | 23 ++++++++++-- .../src/internal/client/reactivity/effects.js | 9 ++++- .../src/internal/client/reactivity/types.d.ts | 2 + .../svelte/src/internal/client/runtime.js | 8 +++- .../samples/async-abort-signal/_config.js | 37 +++++++++++++++++++ .../samples/async-abort-signal/main.svelte | 29 +++++++++++++++ packages/svelte/types/index.d.ts | 1 + 10 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index efd5628ae951..c76eacbf1b01 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -1,7 +1,7 @@ /** @import { ComponentContext, ComponentContextLegacy } from '#client' */ /** @import { EventDispatcher } from './index.js' */ /** @import { NotFunction } from './internal/types.js' */ -import { untrack } from './internal/client/runtime.js'; +import { active_reaction, untrack } from './internal/client/runtime.js'; import { is_array } from './internal/shared/utils.js'; import { user_effect } from './internal/client/index.js'; import * as e from './internal/client/errors.js'; @@ -44,6 +44,14 @@ if (DEV) { throw_rune_error('$bindable'); } +export function getAbortSignal() { + if (active_reaction === null) { + throw new Error('TODO getAbortSignal can only be called inside a reaction'); + } + + return (active_reaction.ac ??= new AbortController()).signal; +} + /** * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. * Unlike `$effect`, the provided function only runs once. diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5aa7..f4cb6f8c4147 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -35,6 +35,21 @@ export function unmount() { export async function tick() {} +/** @type {AbortController | null} */ +let controller = null; + +export function getAbortSignal() { + if (controller === null) { + const c = (controller = new AbortController()); + queueMicrotask(() => { + c.abort(); + controller = null; + }); + } + + return controller.signal; +} + export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; export { createRawSnippet } from './internal/server/blocks/snippet.js'; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index ccc853c3bcf5..79b98e357730 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -30,3 +30,5 @@ export const EFFECT_ASYNC = 1 << 25; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); + +export const STALE_REACTION = Symbol('stale reaction'); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c508e515c03d..44e51b412f89 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -9,6 +9,7 @@ import { EFFECT_ASYNC, EFFECT_PRESERVED, MAYBE_DIRTY, + STALE_REACTION, UNOWNED } from '#client/constants'; import { @@ -33,6 +34,7 @@ import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; +import { noop } from '../../shared/utils.js'; /** @type {Effect | null} */ export let from_async_derived = null; @@ -77,7 +79,8 @@ export function derived(fn) { rv: 0, v: /** @type {V} */ (null), wv: 0, - parent: parent_derived ?? active_effect + parent: parent_derived ?? active_effect, + ac: null }; if (DEV && tracing_mode_flag) { @@ -177,7 +180,17 @@ export function async_derived(fn, location) { (e) => { prev = null; - handle_error(e, parent, null, parent.ctx); + if (e === STALE_REACTION) { + if (should_suspend) { + if (!ran) { + boundary.decrement(); + } else { + batch.decrement(); + } + } + } else { + handle_error(e, parent, null, parent.ctx); + } } ); }, EFFECT_ASYNC | EFFECT_PRESERVED); @@ -185,7 +198,7 @@ export function async_derived(fn, location) { return new Promise((fulfil) => { /** @param {Promise} p */ function next(p) { - p.then(() => { + function go() { if (p === promise) { fulfil(signal); } else { @@ -193,7 +206,9 @@ export function async_derived(fn, location) { // resolves, delay resolution until we have a value next(promise); } - }); + } + + p.then(go, go); } next(promise); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 704633b39ce5..051b3f741f31 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -31,7 +31,8 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_PRESERVED + EFFECT_PRESERVED, + STALE_REACTION } from '#client/constants'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -112,7 +113,8 @@ function create_effect(type, fn, sync, push = true) { prev: null, teardown: null, transitions: null, - wv: 0 + wv: 0, + ac: null }; if (DEV) { @@ -425,6 +427,8 @@ export function destroy_effect_children(signal, remove_dom = false) { signal.first = signal.last = null; while (effect !== null) { + effect.ac?.abort(STALE_REACTION); + var next = effect.next; if ((effect.f & ROOT_EFFECT) !== 0) { @@ -502,6 +506,7 @@ export function destroy_effect(effect, remove_dom = true) { effect.fn = effect.nodes_start = effect.nodes_end = + effect.ac = null; } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 6c665bbbe133..5af392c7915d 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -32,6 +32,8 @@ export interface Reaction extends Signal { fn: null | Function; /** Signals that this signal reads from */ deps: null | Value[]; + /** An AbortController that aborts when the signal is destroyed */ + ac: null | AbortController; } export interface Derived extends Value, Reaction { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 43ceb408bd66..4accdb0ce6d8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -26,7 +26,8 @@ import { REACTION_IS_UPDATING, EFFECT_IS_UPDATING, EFFECT_ASYNC, - RENDER_EFFECT + RENDER_EFFECT, + STALE_REACTION } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -439,6 +440,11 @@ export function update_reaction(reaction) { reaction.f |= EFFECT_IS_UPDATING; + if (reaction.ac !== null) { + reaction.ac?.abort(STALE_REACTION); + reaction.ac = null; + } + try { reaction.f |= REACTION_IS_UPDATING; var result = /** @type {Function} */ (0, reaction.fn)(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js new file mode 100644 index 000000000000..1405ee6e9f73 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js @@ -0,0 +1,37 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs, variant }) { + if (variant === 'hydrate') { + await Promise.resolve(); + } + + const [reset, resolve] = target.querySelectorAll('button'); + + flushSync(() => reset.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.deepEqual(logs, ['aborted']); + + flushSync(() => resolve.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte new file mode 100644 index 000000000000..d8d77bf0e9f7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/main.svelte @@ -0,0 +1,29 @@ + + + + + + +

{await load(deferred)}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 4b88ecb58c67..e437bb6babc0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -348,6 +348,7 @@ declare module 'svelte' { */ props: Props; }); + export function getAbortSignal(): AbortSignal; /** * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. * Unlike `$effect`, the provided function only runs once. From 357ff4752f9189dbdccfb279f6a029c8ff499d59 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 14:27:44 +0200 Subject: [PATCH 338/582] docs --- packages/svelte/src/index-client.js | 23 +++++++++++++++++++++++ packages/svelte/types/index.d.ts | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index c76eacbf1b01..d843426ce019 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -44,6 +44,29 @@ if (DEV) { throw_rune_error('$bindable'); } +/** + * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed. + * + * Must be called while a derived or effect is running. + * + * ```svelte + * + * ``` + */ export function getAbortSignal() { if (active_reaction === null) { throw new Error('TODO getAbortSignal can only be called inside a reaction'); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index e437bb6babc0..63e2328101e7 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -348,6 +348,29 @@ declare module 'svelte' { */ props: Props; }); + /** + * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed. + * + * Must be called while a derived or effect is running. + * + * ```svelte + * + * ``` + */ export function getAbortSignal(): AbortSignal; /** * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. From b61c6ad52a9e44f09637e67dabaf9444ef470cbb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 14:41:00 +0200 Subject: [PATCH 339/582] fix --- packages/svelte/src/internal/client/reactivity/deriveds.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 44e51b412f89..9c1390a0bf2d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -182,10 +182,11 @@ export function async_derived(fn, location) { if (e === STALE_REACTION) { if (should_suspend) { + // TODO this feels asymmetrical though it seems to work? if (!ran) { boundary.decrement(); } else { - batch.decrement(); + batch.remove(); } } } else { From b68dcdcf7e8df3f909bf871efa272d42c3d5f7f5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 15:11:50 +0200 Subject: [PATCH 340/582] note to self --- packages/svelte/src/internal/client/dom/task.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index fc94d59245c1..3d58d2215ee9 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -46,6 +46,8 @@ export function queue_boundary_micro_task(fn) { queueMicrotask(run_micro_tasks); } + // TODO do we need to differentiate between `boundary_micro_tasks` and `micro_tasks`? + // nothing breaks if we push everything to `micro_tasks` boundary_micro_tasks.push(fn); } From 5e8bcfa8ccc76c8661ec6ad3c1f0c94c9eb0e7e0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 22:35:03 +0200 Subject: [PATCH 341/582] tweak/fix --- .../src/internal/client/dom/blocks/async.js | 13 ++++---- .../src/internal/client/reactivity/batch.js | 33 ++++++++++++++----- .../internal/client/reactivity/deriveds.js | 6 ++-- .../src/internal/client/reactivity/effects.js | 6 ++-- .../svelte/src/internal/client/runtime.js | 30 +++-------------- 5 files changed, 42 insertions(+), 46 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index c3283081abe9..18b0088d2f88 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -22,14 +22,15 @@ export function async(node, expressions, fn) { boundary.increment(); Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { - batch.run(() => { - restore(); - fn(node, ...result); + batch?.restore(); - // TODO is this necessary? - schedule_effect(effect); - }); + restore(); + fn(node, ...result); + // TODO is this necessary? + schedule_effect(effect); + + batch?.flush(); boundary.decrement(); }); } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 8f36e9e69320..d3b8933ab837 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -2,6 +2,7 @@ import { CLEAN, DIRTY } from '#client/constants'; import { flush_queued_effects, + flush_queued_root_effects, process_effects, schedule_effect, set_queued_root_effects, @@ -17,10 +18,6 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; -export function remove_current_batch() { - current_batch = null; -} - /** Update `$effect.pending()` */ function update_pending() { internal_set(pending, batches.size > 0); @@ -149,12 +146,21 @@ export class Batch { } } - /** - * @param {() => void} fn - */ - run(fn) { + restore() { current_batch = this; - fn(); + } + + flush() { + flush_queued_root_effects(); + + // TODO can this happen? + if (current_batch !== this) return; + + if (this.settled()) { + this.remove(); + } + + current_batch = null; } commit() { @@ -210,6 +216,15 @@ export class Batch { current_batch = new Batch(); batches.add(current_batch); + + queueMicrotask(() => { + if (current_batch === null) { + // a flushSync happened in the meantime + return; + } + + current_batch.flush(); + }); } return current_batch; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 9c1390a0bf2d..03624b55a6c2 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -162,9 +162,9 @@ export function async_derived(fn, location) { } } - batch.run(() => { - internal_set(signal, v); - }); + batch?.restore(); + internal_set(signal, v); + batch?.flush(); if (DEV && location !== undefined) { recent_async_deriveds.add(signal); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 051b3f741f31..e2ffcd41dd92 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -357,9 +357,9 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var effect = create_template_effect(fn, [...sync.map(d), ...result]); - batch.run(() => { - schedule_effect(effect); - }); + batch?.restore(); + schedule_effect(effect); + batch?.flush(); }); } else { create_template_effect(fn, sync.map(d)); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4accdb0ce6d8..085c1fa85083 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -51,7 +51,7 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; -import { current_batch, Batch, remove_current_batch } from './reactivity/batch.js'; +import { current_batch, Batch } from './reactivity/batch.js'; import { log_effect_tree, root } from './dev/debug.js'; // Used for DEV time error handling @@ -693,7 +693,7 @@ function infinite_loop_guard() { } } -function flush_queued_root_effects() { +export function flush_queued_root_effects() { var was_updating_effect = is_updating_effect; var batch = /** @type {Batch} */ (current_batch); @@ -764,24 +764,6 @@ export function flush_queued_effects(effects) { * @returns {void} */ export function schedule_effect(signal) { - if (!is_flushing) { - is_flushing = true; - queueMicrotask(() => { - if (current_batch === null) { - // a flushSync happened in the meantime - return; - } - - flush_queued_root_effects(); - - if (current_batch?.settled()) { - current_batch.remove(); - } - - remove_current_batch(); - }); - } - var effect = (last_scheduled_effect = signal); while (effect.parent !== null) { @@ -868,7 +850,7 @@ export function process_effects(batch, root) { export function flushSync(fn) { var result; - Batch.ensure(); + const batch = Batch.ensure(); if (fn) { is_flushing = true; @@ -884,12 +866,10 @@ export function flushSync(fn) { flush_tasks(); } - if (current_batch?.settled()) { - current_batch.remove(); + if (batch === current_batch) { + batch.flush(); } - remove_current_batch(); - return /** @type {T} */ (result); } From 3d0b6f71c45d2aff5bc7206169ba9951e5bfb45f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 4 May 2025 22:38:51 +0200 Subject: [PATCH 342/582] update test --- .../_config.js | 41 +++++++++++-------- .../main.svelte | 11 ++++- 2 files changed, 32 insertions(+), 20 deletions(-) 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 index 99f91503e139..df3fbe65cd34 100644 --- 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 @@ -1,31 +1,28 @@ 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

`, + html: ` + + + +

pending

+ `, - get props() { - d1 = deferred(); + async test({ assert, target, component, errors, variant }) { + if (variant === 'hydrate') { + await Promise.resolve(); + } - return { - promise: d1.promise - }; - }, + const [toggle, resolve1, resolve2] = target.querySelectorAll('button'); - async test({ assert, target, component, errors }) { - await Promise.resolve(); - var d2 = deferred(); - component.promise = d2.promise; + flushSync(() => toggle.click()); - d1.resolve('unused'); + flushSync(() => resolve1.click()); await Promise.resolve(); await Promise.resolve(); - d2.resolve('hello'); + flushSync(() => resolve2.click()); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); @@ -37,7 +34,15 @@ export default test({ await Promise.resolve(); await tick(); - assert.htmlEqual(target.innerHTML, '

hello

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

two

+ ` + ); 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 index 718a256b8676..9babdb2fe274 100644 --- 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 @@ -1,11 +1,18 @@ + + + + - + {#snippet pending()}

pending

From 48a781e2b14176392f3be571bd98404d9696ad44 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 May 2025 14:53:05 +0200 Subject: [PATCH 343/582] fix --- packages/svelte/src/internal/client/dom/blocks/async.js | 6 ++++-- packages/svelte/src/internal/client/reactivity/batch.js | 8 +++++--- .../svelte/src/internal/client/reactivity/deriveds.js | 4 ++-- .../svelte/src/internal/client/reactivity/effects.js | 9 ++++++--- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 18b0088d2f88..25c37cafb08a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -15,14 +15,16 @@ export function async(node, expressions, fn) { var batch = /** @type {Batch} */ (current_batch); var effect = /** @type {Effect} */ (active_effect); + var boundary = get_pending_boundary(effect); + var ran = boundary.ran; var restore = capture(); boundary.increment(); Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { - batch?.restore(); + if (ran) batch.restore(); restore(); fn(node, ...result); @@ -30,7 +32,7 @@ export function async(node, expressions, fn) { // TODO is this necessary? schedule_effect(effect); - batch?.flush(); + if (ran) batch.flush(); boundary.decrement(); }); } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d3b8933ab837..08f84fc1491f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -74,6 +74,8 @@ export class Batch { } for (const [source, current] of this.#current) { + current_values.set(source, source.v); + // TODO this shouldn't be necessary, but tests fail otherwise, // presumably because we need a try-finally somewhere, and the // source wasn't correctly reverted after the previous batch @@ -214,16 +216,16 @@ export class Batch { raf.tick(update_pending); } - current_batch = new Batch(); + const batch = (current_batch = new Batch()); batches.add(current_batch); queueMicrotask(() => { - if (current_batch === null) { + if (current_batch !== batch) { // a flushSync happened in the meantime return; } - current_batch.flush(); + batch.flush(); }); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 03624b55a6c2..d48f9dd1492b 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -162,9 +162,9 @@ export function async_derived(fn, location) { } } - batch?.restore(); + if (ran) batch.restore(); internal_set(signal, v); - batch?.flush(); + if (ran) batch.flush(); if (DEV && location !== undefined) { recent_async_deriveds.add(signal); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index e2ffcd41dd92..7ab989760abc 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -40,7 +40,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; -import { capture } from '../dom/blocks/boundary.js'; +import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; import { current_batch, Batch } from './batch.js'; @@ -348,6 +348,9 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var batch = /** @type {Batch} */ (current_batch); var restore = capture(); + var boundary = get_pending_boundary(parent); + var ran = boundary.ran; + Promise.all(async.map((expression) => async_derived(expression))).then((result) => { restore(); @@ -357,9 +360,9 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var effect = create_template_effect(fn, [...sync.map(d), ...result]); - batch?.restore(); + if (ran) batch.restore(); schedule_effect(effect); - batch?.flush(); + if (ran) batch.flush(); }); } else { create_template_effect(fn, sync.map(d)); From 693262a48a13371752ff6629c8d940e93e8a6123 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 May 2025 14:56:05 +0200 Subject: [PATCH 344/582] fix --- .../src/internal/client/reactivity/batch.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 08f84fc1491f..bf1b0ea203f7 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -62,26 +62,22 @@ export class Batch { var current_values = new Map(); + for (const [source, current] of this.#current) { + current_values.set(source, source.v); + source.v = current; + } + for (const batch of batches) { if (batch === this) continue; for (const [source, previous] of batch.#previous) { - if (!this.#current.has(source)) { + if (!current_values.has(source)) { current_values.set(source, source.v); source.v = previous; } } } - for (const [source, current] of this.#current) { - current_values.set(source, source.v); - - // TODO this shouldn't be necessary, but tests fail otherwise, - // presumably because we need a try-finally somewhere, and the - // source wasn't correctly reverted after the previous batch - source.v = current; - } - for (const root of root_effects) { process_effects(this, root); } From c599807ef9df66dc25391f44590f8a1f498d7e66 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 May 2025 13:33:08 +0200 Subject: [PATCH 345/582] implement `settled` --- packages/svelte/src/index-client.js | 2 +- packages/svelte/src/index-server.js | 2 ++ packages/svelte/src/internal/client/reactivity/batch.js | 7 +++++-- packages/svelte/src/internal/client/runtime.js | 9 +++++++++ packages/svelte/types/index.d.ts | 5 +++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index d843426ce019..1ee59f72095d 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -241,5 +241,5 @@ function init_update_callbacks(context) { export { flushSync } from './internal/client/runtime.js'; export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; -export { tick, untrack } from './internal/client/runtime.js'; +export { tick, untrack, settled } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index f4cb6f8c4147..219bcfb3605d 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -35,6 +35,8 @@ export function unmount() { export async function tick() {} +export async function settled() {} + /** @type {AbortController | null} */ let controller = null; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index bf1b0ea203f7..138c59ef86c3 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -39,6 +39,9 @@ export class Batch { #pending = 0; + /** @type {PromiseWithResolvers | null} */ + deferred = null; + /** @type {Effect[]} */ async_effects = []; @@ -51,8 +54,6 @@ export class Batch { /** @type {Set} */ skipped_effects = new Set(); - apply() {} - /** * * @param {Effect[]} root_effects @@ -93,6 +94,8 @@ export class Batch { flush_queued_effects(render_effects); flush_queued_effects(effects); + + this.deferred?.resolve(); } else { for (const e of this.render_effects) set_signal_status(e, CLEAN); for (const e of this.effects) set_signal_status(e, CLEAN); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 085c1fa85083..eed6550b93d0 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -884,6 +884,15 @@ export async function tick() { flushSync(); } +/** + * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, + * have resolved and the DOM has been updated + * @returns {Promise} + */ +export function settled() { + return (Batch.ensure().deferred ??= Promise.withResolvers()).promise; +} + /** * @template V * @param {Value} signal diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 63e2328101e7..bd936e924805 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -452,6 +452,11 @@ declare module 'svelte' { * Returns a promise that resolves once any pending state changes have been applied. * */ export function tick(): Promise; + /** + * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, + * have resolved and the DOM has been updated + * */ + export function settled(): Promise; /** * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), * any state read inside `fn` will not be treated as a dependency. From c72d091657320e18051ef56bdf5ba8ab4d47b4a3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 29 May 2025 16:02:23 -0400 Subject: [PATCH 346/582] oops --- .../3-transform/client/visitors/VariableDeclaration.js | 7 +------ 1 file changed, 1 insertion(+), 6 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 4bfae4534264..e99e15839cd4 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,13 +1,8 @@ /** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */ /** @import { Binding } from '#compiler' */ -/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ -<<<<<<< HEAD +/** @import { ComponentContext } from '../types' */ import { dev, is_ignored, locate_node } from '../../../../state.js'; -import { build_pattern, extract_paths } from '../../../../utils/ast.js'; -======= -import { dev } from '../../../../state.js'; import { extract_paths } from '../../../../utils/ast.js'; ->>>>>>> main import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; From 95b9605ea789bcc40bce1e5e7461ebdc1b419304 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 29 May 2025 17:03:08 -0400 Subject: [PATCH 347/582] fix --- .../3-transform/client/visitors/VariableDeclaration.js | 7 +------ 1 file changed, 1 insertion(+), 6 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 e99e15839cd4..fbe1e5edb7de 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 @@ -203,12 +203,7 @@ export function VariableDeclaration(node, context) { let expression = /** @type {Expression} */ (context.visit(value)); if (rune === '$derived') expression = b.thunk(expression); - declarations.push( - b.declarator( - declarator.id, - b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)) - ) - ); + declarations.push(b.declarator(declarator.id, b.call('$.derived', expression))); } } else { const init = /** @type {CallExpression} */ (declarator.init); From 6625971ce17b525325b6cab7138f99dd9e24dc36 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 30 May 2025 08:03:48 -0400 Subject: [PATCH 348/582] work around some quirk of the test environment --- .../tests/runtime-legacy/samples/transition-abort/_config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-abort/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-abort/_config.js index e35b08f2a8ca..dccf5ca669db 100644 --- a/packages/svelte/tests/runtime-legacy/samples/transition-abort/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/transition-abort/_config.js @@ -27,6 +27,8 @@ export default test({ array: ['a', 'b', 'c'] }); + raf.tick(25); + raf.tick(50); assert.htmlEqual( target.innerHTML, From 4136cf20b4837cf5d97c6782b27ec629c64ecd45 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 12:45:26 -0400 Subject: [PATCH 349/582] fix --- packages/svelte/src/internal/client/context.js | 3 +-- .../src/internal/client/reactivity/effects.js | 16 +++++++--------- packages/svelte/src/internal/client/types.d.ts | 4 +--- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 7c7213b7a2de..99d56875775d 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -116,8 +116,7 @@ export function push(props, runes = false, fn) { component_context.l = { s: null, u: null, - r1: [], - r2: source(false) + $: [] }; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 7ab989760abc..431e819dbcd2 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -284,9 +284,10 @@ export function effect(fn) { export function legacy_pre_effect(deps, fn) { var context = /** @type {ComponentContextLegacy} */ (component_context); - /** @type {{ effect: null | Effect, ran: boolean }} */ - var token = { effect: null, ran: false }; - context.l.r1.push(token); + /** @type {{ effect: null | Effect, ran: boolean, deps: () => any }} */ + var token = { effect: null, ran: false, deps }; + + context.l.$.push(token); token.effect = render_effect(() => { deps(); @@ -296,7 +297,6 @@ export function legacy_pre_effect(deps, fn) { if (token.ran) return; token.ran = true; - set(context.l.r2, true); untrack(fn); }); } @@ -305,10 +305,10 @@ export function legacy_pre_effect_reset() { var context = /** @type {ComponentContextLegacy} */ (component_context); render_effect(() => { - if (!get(context.l.r2)) return; - // Run dirty `$:` statements - for (var token of context.l.r1) { + for (var token of context.l.$) { + token.deps(); + var effect = token.effect; // If the effect is CLEAN, then make it MAYBE_DIRTY. This ensures we traverse through @@ -323,8 +323,6 @@ export function legacy_pre_effect_reset() { token.ran = false; } - - set(context.l.r2, false); }); } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 9703c2aac198..01baee04676d 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -51,9 +51,7 @@ export type ComponentContext = { m: Array<() => any>; }; /** `$:` statements */ - r1: any[]; - /** This tracks whether `$:` statements have run in the current cycle, to ensure they only run once */ - r2: Source; + $: any[]; }; /** * dev mode only: the component function From fcd51d48a301ab178ea61a666213f98c37e6d1dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 12:47:26 -0400 Subject: [PATCH 350/582] tidy up --- packages/svelte/src/internal/client/context.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 99d56875775d..c0c4f5fda99e 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -109,17 +109,9 @@ export function push(props, runes = false, fn) { m: false, s: props, x: null, - l: null + l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null }); - if (legacy_mode_flag && !runes) { - component_context.l = { - s: null, - u: null, - $: [] - }; - } - teardown(() => { /** @type {ComponentContext} */ (ctx).d = true; }); From f584d0d7d49db87e75fcd7c6bf55ae9e06ca7bb2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 13:58:32 -0400 Subject: [PATCH 351/582] fix --- .../svelte/src/internal/client/reactivity/batch.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 138c59ef86c3..6744898cf694 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -61,10 +61,11 @@ export class Batch { process(root_effects) { set_queued_root_effects([]); + /** @type {Map} */ var current_values = new Map(); for (const [source, current] of this.#current) { - current_values.set(source, source.v); + current_values.set(source, { v: source.v, wv: source.wv }); source.v = current; } @@ -73,7 +74,7 @@ export class Batch { for (const [source, previous] of batch.#previous) { if (!current_values.has(source)) { - current_values.set(source, source.v); + current_values.set(source, { v: source.v, wv: source.wv }); source.v = previous; } } @@ -101,8 +102,12 @@ export class Batch { for (const e of this.effects) set_signal_status(e, CLEAN); } - for (const [source, value] of current_values) { - source.v = value; + for (const [source, { v, wv }] of current_values) { + // reset the source to the current value (unless + // it got a newer value as a result of effects running) + if (source.wv <= wv) { + source.v = v; + } } for (const effect of this.async_effects) { From 7dc2019e3f26e79d2aad0b2874af96e32c39e7c3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 14:49:06 -0400 Subject: [PATCH 352/582] lint --- .../svelte/src/internal/client/reactivity/batch.js | 3 ++- packages/svelte/src/internal/client/runtime.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6744898cf694..7f5cdea1a17f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -39,7 +39,8 @@ export class Batch { #pending = 0; - /** @type {PromiseWithResolvers | null} */ + /** @type {{ promise: Promise, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null} */ + // TODO replace with Promise.withResolvers once supported widely enough deferred = null; /** @type {Effect[]} */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5a819e1c84a9..47dd0c34dc00 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,6 +1,12 @@ /** @import { ComponentContext, Derived, Effect, Reaction, Signal, Source, Value } from '#client' */ import { DEV } from 'esm-env'; -import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'; +import { + deferred, + define_property, + get_descriptors, + get_prototype_of, + index_of +} from '../shared/utils.js'; import { destroy_block_effect_children, destroy_effect_children, @@ -893,7 +899,7 @@ export async function tick() { * @returns {Promise} */ export function settled() { - return (Batch.ensure().deferred ??= Promise.withResolvers()).promise; + return (Batch.ensure().deferred ??= deferred()).promise; } /** From a2bc5f7d348ae777b52324ae57b49114298bc806 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 15:25:34 -0400 Subject: [PATCH 353/582] add flag --- packages/svelte/package.json | 3 +++ .../compiler/phases/3-transform/client/transform-client.js | 4 ++++ packages/svelte/src/index-client.js | 2 +- packages/svelte/src/internal/client/render.js | 1 + packages/svelte/src/internal/flags/async.js | 3 +++ packages/svelte/src/internal/flags/index.js | 5 +++++ packages/svelte/tests/runtime-legacy/shared.ts | 2 +- 7 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 packages/svelte/src/internal/flags/async.js diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d1cf6c428b13..e1ca8efc086d 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -59,6 +59,9 @@ "./internal/disclose-version": { "default": "./src/internal/disclose-version.js" }, + "./internal/flags/async": { + "default": "./src/internal/flags/async.js" + }, "./internal/flags/legacy": { "default": "./src/internal/flags/legacy.js" }, 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 341382847d85..0f2b0e2f3311 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 @@ -563,6 +563,10 @@ export function client_component(analysis, options) { ); } + if (options.experimental.async) { + body.unshift(b.imports([], 'svelte/internal/flags/async')); + } + if (!analysis.runes) { body.unshift(b.imports([], 'svelte/internal/flags/legacy')); } diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 1ee59f72095d..090284e32735 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -6,7 +6,7 @@ import { is_array } from './internal/shared/utils.js'; import { user_effect } from './internal/client/index.js'; import * as e from './internal/client/errors.js'; import { lifecycle_outside_component } from './internal/shared/errors.js'; -import { legacy_mode_flag } from './internal/flags/index.js'; +import { async_mode_flag, legacy_mode_flag } from './internal/flags/index.js'; import { component_context } from './internal/client/context.js'; import { DEV } from 'esm-env'; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 3256fe827410..222b971bdf7c 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -30,6 +30,7 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; +import { async_mode_flag } from '../flags/index.js'; /** * This is normally true — block effects should run their intro transitions — diff --git a/packages/svelte/src/internal/flags/async.js b/packages/svelte/src/internal/flags/async.js new file mode 100644 index 000000000000..ca4ff9286a4a --- /dev/null +++ b/packages/svelte/src/internal/flags/async.js @@ -0,0 +1,3 @@ +import { enable_async_mode_flag } from './index.js'; + +enable_async_mode_flag(); diff --git a/packages/svelte/src/internal/flags/index.js b/packages/svelte/src/internal/flags/index.js index 017840f2d967..6920f6b8eeda 100644 --- a/packages/svelte/src/internal/flags/index.js +++ b/packages/svelte/src/internal/flags/index.js @@ -1,6 +1,11 @@ +export let async_mode_flag = false; export let legacy_mode_flag = false; export let tracing_mode_flag = false; +export function enable_async_mode_flag() { + async_mode_flag = true; +} + export function enable_legacy_mode_flag() { legacy_mode_flag = true; } diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index f60a926e1b9f..aa496b118dc5 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -173,7 +173,7 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run dev: force_hmr ? true : undefined, hmr: force_hmr ? true : undefined, experimental: { - async: true + async: runes }, fragments, ...config.compileOptions, From bc050c34b7a9abf9a82c6f3d3e6eb10892071f9a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 15:49:07 -0400 Subject: [PATCH 354/582] make everything non-breaking for people who dont opt in --- .../svelte/src/internal/client/dom/operations.js | 3 +++ packages/svelte/src/internal/client/runtime.js | 16 ++++++++++++++-- .../lifecycle-render-beforeUpdate/_config.js | 6 ------ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index b00987bb96ce..a4325fce5ab3 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -5,6 +5,7 @@ import { init_array_prototype_warnings } from '../dev/equality.js'; import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { active_effect } from '../runtime.js'; import { EFFECT_RAN } from '../constants.js'; +import { async_mode_flag } from '../../flags/index.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -214,6 +215,8 @@ export function clear_text_content(node) { * current `` */ export function should_defer_append() { + if (!async_mode_flag) return false; + var flags = /** @type {Effect} */ (active_effect).f; return (flags & EFFECT_RAN) !== 0; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 47dd0c34dc00..39b27f9f7397 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -45,7 +45,7 @@ import { } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; -import { tracing_mode_flag } from '../flags/index.js'; +import { async_mode_flag, tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; import { component_context, @@ -823,7 +823,19 @@ export function process_effects(batch, root) { } else if (is_branch) { effect.f ^= CLEAN; } else if ((flags & RENDER_EFFECT) !== 0) { - batch.render_effects.push(effect); + // we need to branch here because in legacy mode we run render effects + // before running block effects + if (async_mode_flag) { + batch.render_effects.push(effect); + } else { + try { + if (check_dirtiness(effect)) { + update_effect(effect); + } + } catch (error) { + handle_error(error, effect, null, effect.ctx); + } + } } else if ((flags & EFFECT) !== 0) { batch.effects.push(effect); } diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js index 7c2008168b40..98eb7716fb5c 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-beforeUpdate/_config.js @@ -2,12 +2,6 @@ import { test } from '../../test'; import { flushSync } from 'svelte'; export default test({ - // this test breaks because of the changes required to make async work - // (namely, running blocks before other render effects including - // beforeUpdate and $effect.pre). Not sure if there's a good - // solution. We may be forced to release 6.0 - skip: true, - async test({ assert, target, logs }) { const input = /** @type {HTMLInputElement} */ (target.querySelector('input')); assert.equal(input?.value, 'rich'); From 4d05ed1139fe3438da9a1c5c1d2fa46107bf02ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 16:11:21 -0400 Subject: [PATCH 355/582] disallow late setContext calls --- .../98-reference/.generated/client-errors.md | 6 ++++++ .../svelte/messages/client-errors/errors.md | 4 ++++ .../svelte/src/internal/client/context.js | 10 +++++++++- packages/svelte/src/internal/client/errors.js | 15 +++++++++++++++ .../set-context-after-mount/_config.js | 11 +++++++++++ .../set-context-after-mount/main.svelte | 19 +++++++++++++++++++ 6 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 32348bb78182..b9268636b2e9 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -110,6 +110,12 @@ Rest element properties of `$props()` such as `%property%` are readonly The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files ``` +### set_context_after_init + +``` +`setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression +``` + ### state_descriptors_fixed ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index c4e68f8fee80..8748bf8978a6 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -72,6 +72,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files +## set_context_after_init + +> `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression + ## state_descriptors_fixed > Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index c0c4f5fda99e..f326f3a0b714 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -2,6 +2,7 @@ import { DEV } from 'esm-env'; import { lifecycle_outside_component } from '../shared/errors.js'; +import * as e from './errors.js'; import { source } from './reactivity/sources.js'; import { active_effect, @@ -10,7 +11,7 @@ import { set_active_reaction } from './runtime.js'; import { effect, teardown } from './reactivity/effects.js'; -import { legacy_mode_flag } from '../flags/index.js'; +import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -65,6 +66,13 @@ export function getContext(key) { */ export function setContext(key, context) { const context_map = get_or_init_context_map('setContext'); + + if (async_mode_flag) { + if (/** @type {ComponentContext} */ (component_context).m) { + e.set_context_after_init(); + } + } + context_map.set(key, context); return context; } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 429dd99da9b9..0209976b11e5 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -197,6 +197,21 @@ export function hydration_failed() { } } +/** + * `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression + * @returns {never} + */ +export function set_context_after_init() { + if (DEV) { + const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/set_context_after_init`); + } +} + /** * Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}` * @returns {never} diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js new file mode 100644 index 000000000000..cc7c483667cd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js @@ -0,0 +1,11 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ target, assert, logs }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.ok(logs[0].startsWith('set_context_after_init')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte new file mode 100644 index 000000000000..40145c28daa8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte @@ -0,0 +1,19 @@ + + + From 9b1e182d8a176c007bc2df8606b06f2ecf68fd1d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 16:13:40 -0400 Subject: [PATCH 356/582] another test --- .../samples/set-context-after-await/Child.svelte | 11 +++++++++++ .../samples/set-context-after-await/_config.js | 11 +++++++++++ .../samples/set-context-after-await/main.svelte | 11 +++++++++++ 3 files changed, 33 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte new file mode 100644 index 000000000000..122a31672661 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js new file mode 100644 index 000000000000..0f0edc208b87 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, logs }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.ok(logs[0].startsWith('set_context_after_init')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte new file mode 100644 index 000000000000..65d0e623cf38 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} + ... + {/snippet} + From eaaee835050a1884a4017d786f10ca72b566657e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 16:37:50 -0400 Subject: [PATCH 357/582] regenerate --- packages/svelte/src/internal/client/errors.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 0209976b11e5..5beae00aa1d8 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -197,21 +197,6 @@ export function hydration_failed() { } } -/** - * `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression - * @returns {never} - */ -export function set_context_after_init() { - if (DEV) { - const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`); - - error.name = 'Svelte error'; - throw error; - } else { - throw new Error(`https://svelte.dev/e/set_context_after_init`); - } -} - /** * Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}` * @returns {never} @@ -291,6 +276,21 @@ export function rune_outside_svelte(rune) { } } +/** + * `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression + * @returns {never} + */ +export function set_context_after_init() { + if (DEV) { + const error = new Error(`set_context_after_init\n\`setContext\` must be called when a component first initializes, not in a subsequent effect or after an \`await\` expression\nhttps://svelte.dev/e/set_context_after_init`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/set_context_after_init`); + } +} + /** * Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. * @returns {never} From 7e83cdae23ee60766991c783df9f7da9d32d0b6d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 16:42:27 -0400 Subject: [PATCH 358/582] move --- .../_config.js | 0 .../main.svelte | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/svelte/tests/runtime-runes/samples/{async-linear-order => async-linear-order-same-derived}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{async-linear-order => async-linear-order-same-derived}/main.svelte (100%) diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-linear-order/_config.js rename to packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-linear-order/main.svelte rename to packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte From 2071160ce89dab1834c48042b739c725ff634597 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 31 May 2025 21:04:15 -0400 Subject: [PATCH 359/582] keep order --- .../src/internal/client/reactivity/batch.js | 95 ++++++++++++++----- .../_config.js | 40 ++++++++ .../main.svelte | 17 ++++ 3 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 7f5cdea1a17f..8653bafb5c65 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -86,18 +86,61 @@ export class Batch { } if (this.async_effects.length === 0 && this.settled()) { - var render_effects = this.render_effects; - var effects = this.effects; + var merged = false; + + // if there are older batches with overlapping + // state, we can't commit this batch. instead, + // we merge it into the older batches + for (const batch of batches) { + if (batch === this) break; + + for (const [source] of batch.#current) { + if (this.#current.has(source)) { + merged = true; + + for (const [source, value] of this.#current) { + batch.#current.set(source, value); + // TODO what about batch.#previous? + } + + for (const e of this.render_effects) { + set_signal_status(e, CLEAN); + // TODO use sets instead of arrays + if (!batch.render_effects.includes(e)) { + batch.render_effects.push(e); + } + } + + for (const e of this.effects) { + set_signal_status(e, CLEAN); + // TODO use sets instead of arrays + if (!batch.effects.includes(e)) { + batch.effects.push(e); + } + } + + this.remove(); + break; + } + } + } - this.render_effects = []; - this.effects = []; + if (merged) { + this.remove(); + } else { + var render_effects = this.render_effects; + var effects = this.effects; - this.commit(); + this.render_effects = []; + this.effects = []; + + this.commit(); - flush_queued_effects(render_effects); - flush_queued_effects(effects); + flush_queued_effects(render_effects); + flush_queued_effects(effects); - this.deferred?.resolve(); + this.deferred?.resolve(); + } } else { for (const e of this.render_effects) set_signal_status(e, CLEAN); for (const e of this.effects) set_signal_status(e, CLEAN); @@ -133,24 +176,24 @@ export class Batch { remove() { batches.delete(this); - for (var batch of batches) { - /** @type {Source} */ - var source; - - if (batch.#id < this.#id) { - // other batch is older than this - for (source of this.#previous.keys()) { - batch.#previous.delete(source); - } - } else { - // other batch is newer than this - for (source of batch.#previous.keys()) { - if (this.#previous.has(source)) { - batch.#previous.set(source, source.v); - } - } - } - } + // for (var batch of batches) { + // /** @type {Source} */ + // var source; + + // if (batch.#id < this.#id) { + // // other batch is older than this + // for (source of this.#previous.keys()) { + // batch.#previous.delete(source); + // } + // } else { + // // other batch is newer than this + // for (source of batch.#previous.keys()) { + // if (this.#previous.has(source)) { + // batch.#previous.set(source, source.v); + // } + // } + // } + // } } restore() { diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js new file mode 100644 index 000000000000..e4d6979acf57 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js @@ -0,0 +1,40 @@ +import { flushSync, settled, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + const [both, a, b] = target.querySelectorAll('button'); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1 * 2 = 2

+

2 * 2 = 4

+ ` + ); + + flushSync(() => both.click()); + flushSync(() => b.click()); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2 * 2 = 4

+

4 * 2 = 8

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte new file mode 100644 index 000000000000..432eed976c47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte @@ -0,0 +1,17 @@ + + + + + + + +

{a} * 2 = {await (a * 2)}

+

{b} * 2 = {b * 2}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 1a094e7aa3b021acca23914365d471d2f4f22713 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 08:56:47 -0400 Subject: [PATCH 360/582] lint --- packages/svelte/src/internal/client/reactivity/batch.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 8653bafb5c65..d70584647762 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -26,8 +26,6 @@ function update_pending() { let uid = 1; export class Batch { - #id = uid++; - /** @type {Map} */ #previous = new Map(); From 03273b78791c3359865040081fc58b53de782db8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 16:13:30 -0400 Subject: [PATCH 361/582] chore: better HTML normalization test helper --- packages/svelte/tests/html_equal.js | 96 +++++++++++-------- .../samples/binding-select/_config.js | 4 +- .../samples/input-list/_config.js | 4 +- .../samples/namespace-html/_config.js | 2 +- .../samples/select-in-each/_config.js | 4 +- .../svelte/tests/runtime-legacy/shared.ts | 4 +- 6 files changed, 66 insertions(+), 48 deletions(-) diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 4c9e2a725332..f3ab5c54caae 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -3,6 +3,15 @@ import { assert } from 'vitest'; /** @param {Element} node */ function clean_children(node) { let previous = null; + let has_element_children = false; + let template = + node.nodeName === 'TEMPLATE' ? /** @type {HTMLTemplateElement} */ (node) : undefined; + + if (template) { + const div = document.createElement('div'); + div.append(template.content); + node = div; + } // sort attributes const attributes = Array.from(node.attributes).sort((a, b) => { @@ -14,6 +23,10 @@ function clean_children(node) { }); attributes.forEach((attr) => { + if ((attr.name === 'onload' || attr.name === 'onerror') && attr.value === 'this.__e=event') { + return; + } + node.setAttribute(attr.name, attr.value); }); @@ -27,23 +40,35 @@ function clean_children(node) { node.tagName !== 'tspan' ) { node.removeChild(child); + continue; } - text.data = text.data.replace(/[ \t\n\r\f]+/g, '\n'); + text.data = text.data.replace(/[^\S]+/g, ' '); if (previous && previous.nodeType === 3) { const prev = /** @type {Text} */ (previous); prev.data += text.data; - prev.data = prev.data.replace(/[ \t\n\r\f]+/g, '\n'); - node.removeChild(text); + text = prev; + text.data = text.data.replace(/[^\S]+/g, ' '); + + continue; } } else if (child.nodeType === 8) { // comment - // do nothing - } else { + child.remove(); + continue; + } else if (child.nodeType === 1) { + if (previous?.nodeType === 3) { + const prev = /** @type {Text} */ (previous); + prev.data = prev.data.replace(/^[^\S]+$/, '\n'); + } else if (previous?.nodeType === 1) { + node.insertBefore(document.createTextNode('\n'), child); + } + + has_element_children = true; clean_children(/** @type {Element} */ (child)); } @@ -53,37 +78,35 @@ function clean_children(node) { // collapse whitespace if (node.firstChild && node.firstChild.nodeType === 3) { const text = /** @type {Text} */ (node.firstChild); - text.data = text.data.replace(/^[ \t\n\r\f]+/, ''); - if (!text.data.length) node.removeChild(text); + text.data = text.data.trimStart(); } if (node.lastChild && node.lastChild.nodeType === 3) { const text = /** @type {Text} */ (node.lastChild); - text.data = text.data.replace(/[ \t\n\r\f]+$/, ''); - if (!text.data.length) node.removeChild(text); + text.data = text.data.trimEnd(); + } + + if (has_element_children && node.parentNode) { + node.innerHTML = `\n\t${node.innerHTML.replace(/\n/g, '\n\t')}\n`; + } + + if (template) { + template.innerHTML = node.innerHTML; } } /** * @param {Window} window * @param {string} html - * @param {{ removeDataSvelte?: boolean, preserveComments?: boolean }} param2 + * @param {{ preserveComments?: boolean }} opts */ -export function normalize_html( - window, - html, - { removeDataSvelte = false, preserveComments = false } -) { +export function normalize_html(window, html, { preserveComments = false } = {}) { try { const node = window.document.createElement('div'); - node.innerHTML = html - .replace(/()/g, preserveComments ? '$1' : '') - .replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1') - .replace(/>[ \t\n\r\f]+<') - // Strip out the special onload/onerror hydration events from the test output - .replace(/\s?onerror="this.__e=event"|\s?onload="this.__e=event"/g, '') - .trim(); + node.innerHTML = html.replace(/()/g, preserveComments ? '$1' : '').trim(); + clean_children(node); + return node.innerHTML; } catch (err) { throw new Error(`Failed to normalize HTML:\n${html}\nCause: ${err}`); @@ -98,10 +121,7 @@ export function normalize_new_line(html) { return html.replace(/\r\n/g, '\n'); } -/** - * @param {{ removeDataSvelte?: boolean }} options - */ -export function setup_html_equal(options = {}) { +export function setup_html_equal() { /** * @param {string} actual * @param {string} expected @@ -109,11 +129,7 @@ export function setup_html_equal(options = {}) { */ const assert_html_equal = (actual, expected, message) => { try { - assert.deepEqual( - normalize_html(window, actual, options), - normalize_html(window, expected, options), - message - ); + assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message); } catch (e) { if (Error.captureStackTrace) Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); @@ -137,15 +153,17 @@ export function setup_html_equal(options = {}) { try { assert.deepEqual( withoutNormalizeHtml - ? normalize_new_line(actual.trim()) - .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, actual.trim(), { ...options, preserveComments }), + ? normalize_new_line(actual.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, actual.trim(), { preserveComments }), withoutNormalizeHtml - ? normalize_new_line(expected.trim()) - .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, expected.trim(), { ...options, preserveComments }), + ? normalize_new_line(expected.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, expected.trim(), { preserveComments }), message ); } catch (e) { diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js index 2507f5fc83aa..996f68e39f89 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select/_config.js @@ -25,7 +25,7 @@ export default test({

selected: one

@@ -54,7 +54,7 @@ export default test({

selected: two

diff --git a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js index fe6a29207d4c..1e95aaafa6d9 100644 --- a/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/input-list/_config.js @@ -4,7 +4,9 @@ export default test({ html: ` - + + ` }); diff --git a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js index 3be9f0e92539..b7ecd04def65 100644 --- a/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/namespace-html/_config.js @@ -9,7 +9,7 @@ export default test({ - +
hi
`, diff --git a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js index 4c94ea1e0172..df03b7a053bc 100644 --- a/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/select-in-each/_config.js @@ -7,7 +7,7 @@ export default test({ target.innerHTML, ` selected: a @@ -23,7 +23,7 @@ export default test({ target.innerHTML, ` selected: b diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 690a7e3d98fe..c94c4ed42294 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -86,9 +86,7 @@ function unhandled_rejection_handler(err: Error) { const listeners = process.rawListeners('unhandledRejection'); -const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal({ - removeDataSvelte: true -}); +const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal(); beforeAll(() => { // @ts-expect-error TODO huh? From 7c10b237d15c563e5a9160f19fad04be8cc9c77f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 16:20:36 -0400 Subject: [PATCH 362/582] simplify --- packages/svelte/tests/html_equal.js | 108 ++++++++---------- .../svelte/tests/runtime-legacy/shared.ts | 4 +- 2 files changed, 50 insertions(+), 62 deletions(-) diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index f3ab5c54caae..22e52417a2de 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -121,63 +121,53 @@ export function normalize_new_line(html) { return html.replace(/\r\n/g, '\n'); } -export function setup_html_equal() { - /** - * @param {string} actual - * @param {string} expected - * @param {string} [message] - */ - const assert_html_equal = (actual, expected, message) => { - try { - assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message); - } catch (e) { - if (Error.captureStackTrace) - Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); - throw e; - } - }; - - /** - * - * @param {string} actual - * @param {string} expected - * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2 - * @param {string} [message] - */ - const assert_html_equal_with_options = ( - actual, - expected, - { preserveComments, withoutNormalizeHtml }, - message - ) => { - try { - assert.deepEqual( - withoutNormalizeHtml - ? normalize_new_line(actual.trim()).replace( - /()/g, - preserveComments !== false ? '$1' : '' - ) - : normalize_html(window, actual.trim(), { preserveComments }), - withoutNormalizeHtml - ? normalize_new_line(expected.trim()).replace( - /()/g, - preserveComments !== false ? '$1' : '' - ) - : normalize_html(window, expected.trim(), { preserveComments }), - message - ); - } catch (e) { - if (Error.captureStackTrace) - Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options); - throw e; - } - }; - - return { - assert_html_equal, - assert_html_equal_with_options - }; -} +/** + * @param {string} actual + * @param {string} expected + * @param {string} [message] + */ +export const assert_html_equal = (actual, expected, message) => { + try { + assert.deepEqual(normalize_html(window, actual), normalize_html(window, expected), message); + } catch (e) { + if (Error.captureStackTrace) + Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal); + throw e; + } +}; -// Common case without options -export const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal(); +/** + * + * @param {string} actual + * @param {string} expected + * @param {{ preserveComments?: boolean, withoutNormalizeHtml?: boolean }} param2 + * @param {string} [message] + */ +export const assert_html_equal_with_options = ( + actual, + expected, + { preserveComments, withoutNormalizeHtml }, + message +) => { + try { + assert.deepEqual( + withoutNormalizeHtml + ? normalize_new_line(actual.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, actual.trim(), { preserveComments }), + withoutNormalizeHtml + ? normalize_new_line(expected.trim()).replace( + /()/g, + preserveComments !== false ? '$1' : '' + ) + : normalize_html(window, expected.trim(), { preserveComments }), + message + ); + } catch (e) { + if (Error.captureStackTrace) + Error.captureStackTrace(/** @type {Error} */ (e), assert_html_equal_with_options); + throw e; + } +}; diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index c94c4ed42294..c0d1177a823e 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -7,7 +7,7 @@ import { flushSync, hydrate, mount, unmount } from 'svelte'; import { render } from 'svelte/server'; import { afterAll, assert, beforeAll } from 'vitest'; import { compile_directory, fragments } from '../helpers.js'; -import { setup_html_equal } from '../html_equal.js'; +import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js'; import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; @@ -86,8 +86,6 @@ function unhandled_rejection_handler(err: Error) { const listeners = process.rawListeners('unhandledRejection'); -const { assert_html_equal, assert_html_equal_with_options } = setup_html_equal(); - beforeAll(() => { // @ts-expect-error TODO huh? process.prependListener('unhandledRejection', unhandled_rejection_handler); From 73796c49b05b2d2eb8c5120a78f599ebaaade377 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 16:45:38 -0400 Subject: [PATCH 363/582] simplify/robustify --- packages/svelte/tests/html_equal.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 22e52417a2de..b5e18fdd4286 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -1,7 +1,10 @@ import { assert } from 'vitest'; -/** @param {Element} node */ -function clean_children(node) { +/** + * @param {Element} node + * @param {{ preserveComments: boolean }} opts + */ +function clean_children(node, opts) { let previous = null; let has_element_children = false; let template = @@ -56,20 +59,26 @@ function clean_children(node) { continue; } - } else if (child.nodeType === 8) { + } + + if (child.nodeType === 8 && !opts.preserveComments) { // comment child.remove(); continue; - } else if (child.nodeType === 1) { + } + + if (child.nodeType === 1 || child.nodeType === 8) { if (previous?.nodeType === 3) { const prev = /** @type {Text} */ (previous); prev.data = prev.data.replace(/^[^\S]+$/, '\n'); - } else if (previous?.nodeType === 1) { + } else if (previous?.nodeType === 1 || previous?.nodeType === 8) { node.insertBefore(document.createTextNode('\n'), child); } - has_element_children = true; - clean_children(/** @type {Element} */ (child)); + if (child.nodeType === 1) { + has_element_children = true; + clean_children(/** @type {Element} */ (child), opts); + } } previous = child; @@ -103,9 +112,9 @@ function clean_children(node) { export function normalize_html(window, html, { preserveComments = false } = {}) { try { const node = window.document.createElement('div'); - node.innerHTML = html.replace(/()/g, preserveComments ? '$1' : '').trim(); - clean_children(node); + node.innerHTML = html.trim(); + clean_children(node, { preserveComments }); return node.innerHTML; } catch (err) { From 302dff234b000df1ba9c6006624b66fc473008a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 1 Jun 2025 21:05:09 -0400 Subject: [PATCH 364/582] don't write values to deriveds when time travelling --- .../src/internal/client/reactivity/batch.js | 54 +++++++++++------ .../svelte/src/internal/client/runtime.js | 21 ++++++- .../async-with-sync-derived/_config.js | 59 +++++++++++++++++++ .../async-with-sync-derived/main.svelte | 19 ++++++ 4 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d70584647762..9e1fcf70756c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,4 +1,4 @@ -/** @import { Effect, Source } from '#client' */ +/** @import { Derived, Effect, Source } from '#client' */ import { CLEAN, DIRTY } from '#client/constants'; import { flush_queued_effects, @@ -23,7 +23,8 @@ function update_pending() { internal_set(pending, batches.size > 0); } -let uid = 1; +/** @type {Map | null} */ +export let batch_deriveds = null; export class Batch { /** @type {Map} */ @@ -60,21 +61,34 @@ export class Batch { process(root_effects) { set_queued_root_effects([]); - /** @type {Map} */ - var current_values = new Map(); + /** @type {Map | null} */ + var current_values = null; + var time_travelling = false; - for (const [source, current] of this.#current) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = current; + for (const batch of batches) { + if (batch !== this) { + time_travelling = true; + break; + } } - for (const batch of batches) { - if (batch === this) continue; + if (time_travelling) { + current_values = new Map(); + batch_deriveds = new Map(); + + for (const [source, current] of this.#current) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = current; + } - for (const [source, previous] of batch.#previous) { - if (!current_values.has(source)) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = previous; + for (const batch of batches) { + if (batch === this) continue; + + for (const [source, previous] of batch.#previous) { + if (!current_values.has(source)) { + current_values.set(source, { v: source.v, wv: source.wv }); + source.v = previous; + } } } } @@ -144,12 +158,16 @@ export class Batch { for (const e of this.effects) set_signal_status(e, CLEAN); } - for (const [source, { v, wv }] of current_values) { - // reset the source to the current value (unless - // it got a newer value as a result of effects running) - if (source.wv <= wv) { - source.v = v; + if (current_values) { + for (const [source, { v, wv }] of current_values) { + // reset the source to the current value (unless + // it got a newer value as a result of effects running) + if (source.wv <= wv) { + source.v = v; + } } + + batch_deriveds = null; } for (const effect of this.async_effects) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 39b27f9f7397..76c18e63623e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -39,6 +39,7 @@ import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; import { destroy_derived_effects, + execute_derived, from_async_derived, recent_async_deriveds, update_derived @@ -57,7 +58,7 @@ import { import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; import { is_firefox } from './dom/operations.js'; -import { current_batch, Batch } from './reactivity/batch.js'; +import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; import { log_effect_tree, root } from './dev/debug.js'; // Used for DEV time error handling @@ -986,7 +987,10 @@ export function get(signal) { } } - if (is_derived) { + // if this is a derived, we may need to update it, but + // not if `batch_deriveds` is not null (meaning we're + // currently time travelling)) + if (is_derived && batch_deriveds === null) { derived = /** @type {Derived} */ (signal); if (check_dirtiness(derived)) { @@ -1032,6 +1036,19 @@ export function get(signal) { return old_values.get(signal); } + // if we're time travelling, we don't want to update the + // intrinsic value of the derived — we want to compute it + // once and stash it for the duration of batch processing + if (is_derived && batch_deriveds !== null) { + derived = /** @type {Derived} */ (signal); + + if (!batch_deriveds.has(derived)) { + batch_deriveds.set(derived, execute_derived(derived)); + } + + return batch_deriveds.get(derived); + } + return signal.v; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js new file mode 100644 index 000000000000..c09d448f9cd7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js @@ -0,0 +1,59 @@ +import { flushSync, settled, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

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

1

+

1

+

1

+ ` + ); + + const [log, x, other] = target.querySelectorAll('button'); + + flushSync(() => x.click()); + flushSync(() => other.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+

2

+

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte new file mode 100644 index 000000000000..764007e082a3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{x}

+

{await x}

+

{y}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From b608ee24c87d964f9fb19b775fa5e0afe3523279 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 3 Jun 2025 12:37:36 -0400 Subject: [PATCH 365/582] add failing test --- .../async-derived-unchanging/Component.svelte | 29 +++++++++++++ .../async-derived-unchanging/_config.js | 41 +++++++++++++++++++ .../async-derived-unchanging/main.svelte | 11 +++++ 3 files changed, 81 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte new file mode 100644 index 000000000000..a90a9dedf724 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte @@ -0,0 +1,29 @@ + + + + + +

{n}: {Math.min(current, 3)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js new file mode 100644 index 000000000000..749640823c72 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js @@ -0,0 +1,41 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending...

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

0: 0

+ ` + ); + + const [shift, increment] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + for (let i = 1; i < 5; i += 1) { + flushSync(() => increment.click()); + } + + for (let i = 1; i < 5; i += 1) { + shift.click(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.equal(p.innerHTML, `${i}: ${i}`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte new file mode 100644 index 000000000000..2d5ddca4dbfc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

pending...

+ {/snippet} +
From 1a42fc8ea8930a2a0a048f1823bb81f4b909a59a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 3 Jun 2025 20:43:36 -0400 Subject: [PATCH 366/582] fix --- packages/svelte/src/internal/client/reactivity/sources.js | 5 ++++- .../samples/async-derived-unchanging/_config.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 6340c6f0b4b2..69967ab3b937 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -289,7 +289,10 @@ export function mark_reactions(signal, status, partial = false) { continue; } - set_signal_status(reaction, status); + if (status === DIRTY || (flags & DIRTY) === 0) { + // don't make a DIRTY signal MAYBE_DIRTY + set_signal_status(reaction, status); + } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, partial); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js index 749640823c72..423213696477 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js @@ -35,7 +35,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); - assert.equal(p.innerHTML, `${i}: ${i}`); + assert.equal(p.innerHTML, `${i}: ${Math.min(i, 3)}`); } } }); From 855707753a33df9a77a6ee5800434793e3f74540 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 11:50:27 -0400 Subject: [PATCH 367/582] we can remove this now --- packages/svelte/src/internal/client/runtime.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 76c18e63623e..00051cbc2348 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -112,15 +112,6 @@ export function set_active_effect(effect) { active_effect = effect; } -// TODO remove this, once we're satisfied that we're not leaking context -/* @__PURE__ */ -setInterval(() => { - if (active_effect !== null || active_reaction !== null) { - // eslint-disable-next-line no-debugger - debugger; - } -}); - /** * When sources are created within a reaction, reading and writing * them should not cause a re-run From c96d3108c50dd82843a7d95ea7acd0c3b84fbb5e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 14:38:54 -0400 Subject: [PATCH 368/582] failing test --- .../samples/async-error-recovery/_config.js | 82 +++++++++++++++++++ .../samples/async-error-recovery/main.svelte | 24 ++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js new file mode 100644 index 000000000000..bb3e5bc982d3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js @@ -0,0 +1,82 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

pending...

+ `, + + compileOptions: { + // this tests some behaviour that was broken in dev + dev: true + }, + + async test({ assert, target }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` + +

0

+ ` + ); + + let [button] = target.querySelectorAll('button'); + let [p] = target.querySelectorAll('p'); + + flushSync(() => button.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + assert.equal(p.textContent, '1'); + + flushSync(() => button.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + assert.equal(p.textContent, '2'); + + flushSync(() => button.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + const [button1, button2] = target.querySelectorAll('button'); + + flushSync(() => button1.click()); + await Promise.resolve(); + + flushSync(() => button2.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + [p] = target.querySelectorAll('p'); + + assert.equal(p.textContent, '4'); + + flushSync(() => button1.click()); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + assert.equal(p.textContent, '5'); + + console.log(target.innerHTML); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte new file mode 100644 index 000000000000..8c8b306bfe39 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte @@ -0,0 +1,24 @@ + + + + + +

{await process(count)}

+ + {#snippet pending()} +

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
From 17b2f227fa45d761342f48fe3084106c57c0920f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 15:18:57 -0400 Subject: [PATCH 369/582] fix --- packages/svelte/src/internal/client/reactivity/deriveds.js | 1 + .../tests/runtime-runes/samples/async-error-recovery/_config.js | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index f60f5568ae51..0db4f26fab4b 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -192,6 +192,7 @@ export function async_derived(fn, location) { } } else { handle_error(e, parent, null, parent.ctx); + batch.remove(); } } ); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js index bb3e5bc982d3..0848ca0fd969 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js @@ -76,7 +76,5 @@ export default test({ await Promise.resolve(); await Promise.resolve(); assert.equal(p.textContent, '5'); - - console.log(target.innerHTML); } }); From d131e2892cdea810b48b25985d737b568e18397a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 15:19:50 -0400 Subject: [PATCH 370/582] unused --- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- packages/svelte/src/internal/client/reactivity/deriveds.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9e1fcf70756c..87491f4e1e21 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,7 +10,7 @@ import { update_effect } from '../runtime.js'; import { raf } from '../timing.js'; -import { internal_set, mark_reactions, pending } from './sources.js'; +import { internal_set, pending } from './sources.js'; /** @type {Set} */ const batches = new Set(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 0db4f26fab4b..8be140f2e1f2 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -35,7 +35,6 @@ import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; -import { noop } from '../../shared/utils.js'; /** @type {Effect | null} */ export let from_async_derived = null; From f8e4651b7ceb3b2d39486253f404c8c0a7676fb9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 15:41:02 -0400 Subject: [PATCH 371/582] failing test --- .../samples/async-error-recovery/_config.js | 38 +++++++++++++++---- .../samples/async-error-recovery/main.svelte | 2 +- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js index 0848ca0fd969..91784f67472d 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js @@ -3,7 +3,7 @@ import { test } from '../../test'; export default test({ html: ` - +

pending...

`, @@ -23,7 +23,7 @@ export default test({ assert.htmlEqual( target.innerHTML, ` - +

0

` ); @@ -35,13 +35,25 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - assert.equal(p.textContent, '1'); + assert.htmlEqual( + target.innerHTML, + ` + +

1

+ ` + ); flushSync(() => button.click()); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - assert.equal(p.textContent, '2'); + assert.htmlEqual( + target.innerHTML, + ` + +

2

+ ` + ); flushSync(() => button.click()); await Promise.resolve(); @@ -50,7 +62,7 @@ export default test({ assert.htmlEqual( target.innerHTML, ` - + ` ); @@ -69,12 +81,24 @@ export default test({ [p] = target.querySelectorAll('p'); - assert.equal(p.textContent, '4'); + assert.htmlEqual( + target.innerHTML, + ` + +

4

+ ` + ); flushSync(() => button1.click()); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - assert.equal(p.textContent, '5'); + assert.htmlEqual( + target.innerHTML, + ` + +

5

+ ` + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte index 8c8b306bfe39..d5246d330e25 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte @@ -8,7 +8,7 @@ From fdb7a6dc85201df97ecfca9681d2d418143b0622 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:10:36 -0400 Subject: [PATCH 372/582] fix --- .../internal/client/reactivity/deriveds.js | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 8be140f2e1f2..1f95ef59222a 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -180,18 +180,21 @@ export function async_derived(fn, location) { (e) => { prev = null; - if (e === STALE_REACTION) { - if (should_suspend) { - // TODO this feels asymmetrical though it seems to work? - if (!ran) { - boundary.decrement(); - } else { - batch.remove(); - } - } - } else { + if (e !== STALE_REACTION) { handle_error(e, parent, null, parent.ctx); - batch.remove(); + } + + if (should_suspend) { + if (!ran) { + boundary.decrement(); + } else { + batch.decrement(); + } + } + + if (ran) { + batch.restore(); + batch.flush(); } } ); From 23bd5c2e513ab23df644c2c20f4d6bddb82a7a3d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:19:29 -0400 Subject: [PATCH 373/582] DRY --- .../internal/client/reactivity/deriveds.js | 68 +++++++++---------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 1f95ef59222a..69232f3772f0 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -143,28 +143,37 @@ export function async_derived(fn, location) { } } - promise.then( - (v) => { - prev = null; + /** + * @param {any} value + * @param {unknown} error + * @param {boolean} errored + */ + const handler = (value, error = undefined, errored = false) => { + prev = null; + + if ((parent.f & DESTROYED) !== 0) { + return; + } - if ((parent.f & DESTROYED) !== 0) { - return; + restore(); + from_async_derived = null; + + if (should_suspend) { + if (!ran) { + boundary.decrement(); + } else { + batch.decrement(); } + } - restore(); - from_async_derived = null; + if (ran) batch.restore(); - if (should_suspend) { - if (!ran) { - boundary.decrement(); - } else { - batch.decrement(); - } + if (errored) { + if (error !== STALE_REACTION) { + handle_error(error, parent, null, parent.ctx); } - - if (ran) batch.restore(); - internal_set(signal, v); - if (ran) batch.flush(); + } else { + internal_set(signal, value); if (DEV && location !== undefined) { recent_async_deriveds.add(signal); @@ -176,27 +185,14 @@ export function async_derived(fn, location) { } }); } - }, - (e) => { - prev = null; - - if (e !== STALE_REACTION) { - handle_error(e, parent, null, parent.ctx); - } + } - if (should_suspend) { - if (!ran) { - boundary.decrement(); - } else { - batch.decrement(); - } - } + if (ran) batch.flush(); + }; - if (ran) { - batch.restore(); - batch.flush(); - } - } + promise.then( + (v) => handler(v), + (e) => handler(null, e, true) ); }, EFFECT_ASYNC | EFFECT_PRESERVED); From 00ba548d94739d90c4cbfc72d1b680839e8dd295 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:20:53 -0400 Subject: [PATCH 374/582] simplify --- .../svelte/src/internal/client/reactivity/deriveds.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 69232f3772f0..76a9b31ff571 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -146,9 +146,8 @@ export function async_derived(fn, location) { /** * @param {any} value * @param {unknown} error - * @param {boolean} errored */ - const handler = (value, error = undefined, errored = false) => { + const handler = (value, error = undefined) => { prev = null; if ((parent.f & DESTROYED) !== 0) { @@ -168,7 +167,7 @@ export function async_derived(fn, location) { if (ran) batch.restore(); - if (errored) { + if (error) { if (error !== STALE_REACTION) { handle_error(error, parent, null, parent.ctx); } @@ -190,10 +189,7 @@ export function async_derived(fn, location) { if (ran) batch.flush(); }; - promise.then( - (v) => handler(v), - (e) => handler(null, e, true) - ); + promise.then(handler, (e) => handler(null, e || 'unknown')); }, EFFECT_ASYNC | EFFECT_PRESERVED); return new Promise((fulfil) => { From cd24e51bc8014ce9ddf247570a447a51cb47fec4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:27:48 -0400 Subject: [PATCH 375/582] tweak --- .../src/internal/client/reactivity/deriveds.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 76a9b31ff571..4a44636894ac 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -130,17 +130,11 @@ export function async_derived(fn, location) { prev = promise; - var restore = capture(); - var batch = /** @type {Batch} */ (current_batch); var ran = boundary.ran; if (should_suspend) { - if (!ran) { - boundary.increment(); - } else { - batch.increment(); - } + (ran ? batch : boundary).increment(); } /** @@ -154,15 +148,10 @@ export function async_derived(fn, location) { return; } - restore(); from_async_derived = null; if (should_suspend) { - if (!ran) { - boundary.decrement(); - } else { - batch.decrement(); - } + (ran ? batch : boundary).decrement(); } if (ran) batch.restore(); From c8e47cc98ddd00ed7cdcc76313fc769867a48c4d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:40:30 -0400 Subject: [PATCH 376/582] tidy --- .../src/internal/client/dom/blocks/async.js | 18 ++---------------- .../src/internal/client/dom/blocks/boundary.js | 7 +++---- .../src/internal/client/reactivity/deriveds.js | 2 +- .../src/internal/client/reactivity/effects.js | 2 +- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 25c37cafb08a..669992c2e318 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,8 +1,5 @@ -/** @import { Effect, TemplateNode, Value } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js' */ +/** @import { TemplateNode, Value } from '#client' */ import { async_derived } from '../../reactivity/deriveds.js'; -import { current_batch } from '../../reactivity/batch.js'; -import { active_effect, schedule_effect } from '../../runtime.js'; import { capture, get_pending_boundary } from './boundary.js'; /** @@ -13,26 +10,15 @@ import { capture, get_pending_boundary } from './boundary.js'; export function async(node, expressions, fn) { // TODO handle hydration - var batch = /** @type {Batch} */ (current_batch); - var effect = /** @type {Effect} */ (active_effect); - - var boundary = get_pending_boundary(effect); - var ran = boundary.ran; - var restore = capture(); + var boundary = get_pending_boundary(); boundary.increment(); Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { - if (ran) batch.restore(); - restore(); fn(node, ...result); - // TODO is this necessary? - schedule_effect(effect); - - if (ran) batch.flush(); boundary.decrement(); }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 2f7c0d2e4d37..cb9fc6ef6b2b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -327,9 +327,8 @@ function move_effect(effect, fragment) { } } -/** @param {Effect} effect */ -export function get_pending_boundary(effect) { - let boundary = effect.b; +export function get_pending_boundary() { + var boundary = /** @type {Effect} */ (active_effect).b; while (boundary !== null && !boundary.has_pending_snippet()) { boundary = boundary.parent; @@ -367,7 +366,7 @@ export function capture(track = true) { // TODO we should probably be incrementing the current batch, not the boundary? export function suspend() { - let boundary = get_pending_boundary(/** @type {Effect} */ (active_effect)); + let boundary = get_pending_boundary(); boundary.increment(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 4a44636894ac..a264af2a73a4 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -104,7 +104,7 @@ export function async_derived(fn, location) { throw new Error('TODO cannot create unowned async derived'); } - let boundary = get_pending_boundary(parent); + let boundary = get_pending_boundary(); var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 431e819dbcd2..f3d4a9e38223 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -346,7 +346,7 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var batch = /** @type {Batch} */ (current_batch); var restore = capture(); - var boundary = get_pending_boundary(parent); + var boundary = get_pending_boundary(); var ran = boundary.ran; Promise.all(async.map((expression) => async_derived(expression))).then((result) => { From 7f0072bfaa80567428eb5f427bd4f5a1deb6381a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:47:53 -0400 Subject: [PATCH 377/582] tidy --- .../src/internal/client/dom/blocks/async.js | 8 ++++++- .../src/internal/client/reactivity/effects.js | 24 +++++-------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 669992c2e318..c3828fdb2517 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,5 +1,7 @@ -/** @import { TemplateNode, Value } from '#client' */ +/** @import { Effect, TemplateNode, Value } from '#client' */ +import { DESTROYED } from '#client/constants'; import { async_derived } from '../../reactivity/deriveds.js'; +import { active_effect } from '../../runtime.js'; import { capture, get_pending_boundary } from './boundary.js'; /** @@ -10,12 +12,16 @@ import { capture, get_pending_boundary } from './boundary.js'; export function async(node, expressions, fn) { // TODO handle hydration + var parent = /** @type {Effect} */ (active_effect); + var restore = capture(); var boundary = get_pending_boundary(); boundary.increment(); Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { + if ((parent.f & DESTROYED) !== 0) return; + restore(); fn(node, ...result); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index f3d4a9e38223..07b648c4439a 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -34,15 +34,14 @@ import { EFFECT_PRESERVED, STALE_REACTION } from '#client/constants'; -import { set } from './sources.js'; 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 { async_derived, derived } from './deriveds.js'; -import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; +import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; -import { current_batch, Batch } from './batch.js'; +import { Batch } from './batch.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -343,24 +342,13 @@ export function template_effect(fn, sync = [], async = [], d = derived) { var parent = /** @type {Effect} */ (active_effect); if (async.length > 0) { - var batch = /** @type {Batch} */ (current_batch); var restore = capture(); - var boundary = get_pending_boundary(); - var ran = boundary.ran; - Promise.all(async.map((expression) => async_derived(expression))).then((result) => { - restore(); - - if ((parent.f & DESTROYED) !== 0) { - return; - } - - var effect = create_template_effect(fn, [...sync.map(d), ...result]); + if ((parent.f & DESTROYED) !== 0) return; - if (ran) batch.restore(); - schedule_effect(effect); - if (ran) batch.flush(); + restore(); + create_template_effect(fn, [...sync.map(d), ...result]); }); } else { create_template_effect(fn, sync.map(d)); @@ -380,7 +368,7 @@ function create_template_effect(fn, deriveds) { }); } - return create_effect(RENDER_EFFECT, effect, true); + create_effect(RENDER_EFFECT, effect, true); } /** From dfa8d6be89f45432bd72bab8677fe7d2ea096828 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:49:51 -0400 Subject: [PATCH 378/582] yes it can, apparently --- packages/svelte/src/internal/client/reactivity/batch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 87491f4e1e21..0c2e034bc5ad 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -219,8 +219,9 @@ export class Batch { flush() { flush_queued_root_effects(); - // TODO can this happen? - if (current_batch !== this) return; + if (current_batch !== this) { + return; + } if (this.settled()) { this.remove(); From aec7368c0a9c9b8ba6c36dfc9d740af3a1c9a4c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:53:21 -0400 Subject: [PATCH 379/582] tidy up --- .../src/internal/client/reactivity/batch.js | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0c2e034bc5ad..65bd61472e23 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -97,7 +97,7 @@ export class Batch { process_effects(this, root); } - if (this.async_effects.length === 0 && this.settled()) { + if (this.async_effects.length === 0 && this.#pending === 0) { var merged = false; // if there are older batches with overlapping @@ -191,25 +191,6 @@ export class Batch { remove() { batches.delete(this); - - // for (var batch of batches) { - // /** @type {Source} */ - // var source; - - // if (batch.#id < this.#id) { - // // other batch is older than this - // for (source of this.#previous.keys()) { - // batch.#previous.delete(source); - // } - // } else { - // // other batch is newer than this - // for (source of batch.#previous.keys()) { - // if (this.#previous.has(source)) { - // batch.#previous.set(source, source.v); - // } - // } - // } - // } } restore() { @@ -223,7 +204,7 @@ export class Batch { return; } - if (this.settled()) { + if (this.#pending === 0) { this.remove(); } @@ -231,7 +212,6 @@ export class Batch { } commit() { - // commit changes for (const fn of this.#callbacks) { fn(); } @@ -266,10 +246,6 @@ export class Batch { } } - settled() { - return this.#pending === 0; - } - /** @param {() => void} fn */ add_callback(fn) { this.#callbacks.add(fn); From 541ab9757cd02002073364198bd570846e731bc6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Jun 2025 16:56:39 -0400 Subject: [PATCH 380/582] unused --- 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 a264af2a73a4..543b711a790f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -31,7 +31,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 { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; +import { get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; From b1960ce467bcefbd40bb71567878cdc96611d96d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Jun 2025 08:13:36 -0400 Subject: [PATCH 381/582] complete merge --- packages/svelte/src/internal/client/reactivity/batch.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 65bd61472e23..e7fa61f483c4 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -131,6 +131,14 @@ export class Batch { } } + for (const e of this.skipped_effects) { + batch.skipped_effects.add(e); + } + + for (const fn of this.#callbacks) { + batch.#callbacks.add(fn); + } + this.remove(); break; } From 37333df9080314c299cfbf6e2aa5c177c007e9a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Jun 2025 14:45:53 -0400 Subject: [PATCH 382/582] WIP --- .../svelte/src/internal/client/constants.js | 2 ++ .../src/internal/client/dom/blocks/if.js | 10 +++++++-- .../src/internal/client/reactivity/batch.js | 21 +++++++++++++++++++ .../internal/client/reactivity/deriveds.js | 4 +++- .../src/internal/client/reactivity/effects.js | 8 ++++++- .../svelte/src/internal/client/runtime.js | 13 +++++++++++- 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 79b98e357730..44a8839d98de 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -27,6 +27,8 @@ export const EFFECT_PRESERVED = 1 << 23; // effects with this flag should not be export const REACTION_IS_UPDATING = 1 << 24; export const EFFECT_ASYNC = 1 << 25; +export const ASYNC_ERROR = 1; + export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index a4a5b68b576f..b4d98f97d839 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -122,13 +122,19 @@ export function if_block(node, fn, elseif = false) { offscreen_fragment.append((target = create_text())); } + var batch = /** @type {Batch} */ (current_batch); + + // TODO need to do this for other block types + if (pending_effect) { + // batch.skipped_effects.add(pending_effect); + // pending_effect = null; + } + if (condition ? !consequent_effect : !alternate_effect) { pending_effect = fn && branch(() => fn(target)); } if (defer) { - var batch = /** @type {Batch} */ (current_batch); - const skipped = condition ? alternate_effect : consequent_effect; if (skipped !== null) { // TODO need to do this for other kinds of blocks diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e7fa61f483c4..b136dede07fa 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -26,7 +26,12 @@ function update_pending() { /** @type {Map | null} */ export let batch_deriveds = null; +/** TODO handy for debugging, but we should probably eventually delete it */ +let uid = 1; + export class Batch { + id = uid++; + /** @type {Map} */ #previous = new Map(); @@ -259,6 +264,22 @@ export class Batch { this.#callbacks.add(fn); } + /** @param {Effect} effect */ + skips(effect) { + /** @type {Effect | null} */ + var e = effect; + + while (e !== null) { + if (this.skipped_effects.has(e)) { + return true; + } + + e = e.parent; + } + + return false; + } + static ensure() { if (current_batch === null) { if (batches.size === 0) { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 543b711a790f..3f21da8e542c 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -2,6 +2,7 @@ /** @import { Batch } from './batch.js'; */ import { DEV } from 'esm-env'; import { + ASYNC_ERROR, CLEAN, DERIVED, DESTROYED, @@ -158,7 +159,8 @@ export function async_derived(fn, location) { if (error) { if (error !== STALE_REACTION) { - handle_error(error, parent, null, parent.ctx); + signal.f |= ASYNC_ERROR; + internal_set(signal, error); } } else { internal_set(signal, value); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 07b648c4439a..5a5b4d69f5c6 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -41,7 +41,7 @@ import { get_next_sibling } from '../dom/operations.js'; import { async_derived, derived } from './deriveds.js'; import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; -import { Batch } from './batch.js'; +import { Batch, current_batch } from './batch.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -339,6 +339,7 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => Promise>} async */ export function template_effect(fn, sync = [], async = [], d = derived) { + var batch = /** @type {Batch} */ (current_batch); var parent = /** @type {Effect} */ (active_effect); if (async.length > 0) { @@ -347,8 +348,13 @@ export function template_effect(fn, sync = [], async = [], d = derived) { Promise.all(async.map((expression) => async_derived(expression))).then((result) => { if ((parent.f & DESTROYED) !== 0) return; + // TODO probably need to do this in async.js as well + batch.restore(); + restore(); create_template_effect(fn, [...sync.map(d), ...result]); + + batch.flush(); }); } else { create_template_effect(fn, sync.map(d)); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 00051cbc2348..db5ded8d63c3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -33,7 +33,8 @@ import { EFFECT_IS_UPDATING, EFFECT_ASYNC, RENDER_EFFECT, - STALE_REACTION + STALE_REACTION, + ASYNC_ERROR } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -303,6 +304,12 @@ export function reset_is_throwing_error() { * @param {ComponentContext | null} component_context */ export function handle_error(error, effect, previous_effect, component_context) { + // if the error occurred inside an effect that's + // about to be destroyed, look the other way + if (current_batch?.skips(effect)) { + return; + } + if (is_throwing_error) { if (previous_effect === null) { is_throwing_error = false; @@ -1040,6 +1047,10 @@ export function get(signal) { return batch_deriveds.get(derived); } + if ((signal.f & ASYNC_ERROR) !== 0) { + throw signal.v; + } + return signal.v; } From 42d5c7e573f8bb18dbbab723f626a868135ef7b4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Jun 2025 15:09:12 -0400 Subject: [PATCH 383/582] simplify --- .../src/internal/client/dom/blocks/if.js | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index a4a5b68b576f..6ba9ad4936f1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -51,9 +51,6 @@ export function if_block(node, fn, elseif = false) { /** @type {DocumentFragment | null} */ var offscreen_fragment = null; - /** @type {Effect | null} */ - var pending_effect = null; - function commit() { if (offscreen_fragment !== null) { // remove the anchor @@ -63,23 +60,15 @@ export function if_block(node, fn, elseif = false) { offscreen_fragment = null; } - if (pending_effect) { - 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; + var active = condition ? consequent_effect : alternate_effect; + var inactive = condition ? alternate_effect : consequent_effect; - if (current_effect !== null) { - resume_effect(current_effect); + if (active) { + resume_effect(active); } - if (previous_effect !== null) { - pause_effect(previous_effect, () => { + if (inactive) { + pause_effect(inactive, () => { if (condition) { alternate_effect = null; } else { @@ -87,8 +76,6 @@ export function if_block(node, fn, elseif = false) { } }); } - - pending_effect = null; } const update_branch = ( @@ -122,18 +109,20 @@ export function if_block(node, fn, elseif = false) { offscreen_fragment.append((target = create_text())); } - if (condition ? !consequent_effect : !alternate_effect) { - pending_effect = fn && branch(() => fn(target)); + if (condition) { + consequent_effect ??= fn && branch(() => fn(target)); + } else { + alternate_effect ??= fn && branch(() => fn(target)); } if (defer) { var batch = /** @type {Batch} */ (current_batch); - const skipped = condition ? alternate_effect : consequent_effect; - if (skipped !== null) { - // TODO need to do this for other kinds of blocks - batch.skipped_effects.add(skipped); - } + var active = condition ? consequent_effect : alternate_effect; + var inactive = condition ? alternate_effect : consequent_effect; + + if (active) batch.skipped_effects.delete(active); + if (inactive) batch.skipped_effects.add(inactive); batch.add_callback(commit); } else { From 0530c2cb4415e08534f51d3189e14f943cca3310 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Jun 2025 15:11:08 -0400 Subject: [PATCH 384/582] debugging help --- packages/svelte/src/internal/client/reactivity/batch.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e7fa61f483c4..48b1b708366a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -26,7 +26,12 @@ function update_pending() { /** @type {Map | null} */ export let batch_deriveds = null; +/** TODO handy for debugging, but we should probably eventually delete it */ +let uid = 1; + export class Batch { + id = uid++; + /** @type {Map} */ #previous = new Map(); From c3ad8c48f4f0e6e7d33812da7d7a04a5ceff5560 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Jun 2025 17:23:33 -0400 Subject: [PATCH 385/582] WIP --- packages/svelte/src/internal/client/reactivity/deriveds.js | 6 ++++++ packages/svelte/src/internal/client/reactivity/effects.js | 6 +++--- .../tests/runtime-runes/samples/async-error/_config.js | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 3f21da8e542c..fab6e691ff45 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -160,9 +160,15 @@ export function async_derived(fn, location) { if (error) { if (error !== STALE_REACTION) { signal.f |= ASYNC_ERROR; + + // @ts-expect-error the error is the wrong type, but we don't care internal_set(signal, error); } } else { + if ((signal.f & ASYNC_ERROR) !== 0) { + signal.f ^= ASYNC_ERROR; + } + internal_set(signal, value); if (DEV && location !== undefined) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 5a5b4d69f5c6..f434035ffa9c 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -339,7 +339,7 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => Promise>} async */ export function template_effect(fn, sync = [], async = [], d = derived) { - var batch = /** @type {Batch} */ (current_batch); + var batch = current_batch; var parent = /** @type {Effect} */ (active_effect); if (async.length > 0) { @@ -349,12 +349,12 @@ export function template_effect(fn, sync = [], async = [], d = derived) { if ((parent.f & DESTROYED) !== 0) return; // TODO probably need to do this in async.js as well - batch.restore(); + batch?.restore(); restore(); create_template_effect(fn, [...sync.map(d), ...result]); - batch.flush(); + batch?.flush(); }); } else { create_template_effect(fn, sync.map(d)); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js index 8f6975f6fb53..52ac43208353 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -10,6 +10,7 @@ export default test({ flushSync(() => button1.click()); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); flushSync(); assert.htmlEqual( target.innerHTML, From f3b7ce0a4d7c359987fea9a912f86903fe0b1c20 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Jun 2025 17:49:26 -0400 Subject: [PATCH 386/582] unused --- packages/svelte/src/internal/client/reactivity/deriveds.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fab6e691ff45..bf16fa3987b9 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -21,7 +21,6 @@ import { update_reaction, increment_write_version, set_active_effect, - handle_error, push_reaction_value, is_destroying_effect } from '../runtime.js'; From 6cd7ef95e1e0f0651e8f3b86116b4e752a6d6325 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 14:25:54 -0400 Subject: [PATCH 387/582] partial merge --- .../client/visitors/SvelteBoundary.js | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) 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 f2009dd319f9..e108c2bd96e2 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 @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Statement, Expression } from 'estree' */ +/** @import { BlockStatement, Statement, Expression, FunctionDeclaration, VariableDeclaration, ArrowFunctionExpression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -34,65 +34,61 @@ export function SvelteBoundary(node, context) { const nodes = []; /** @type {Statement[]} */ - const external_statements = []; + const const_tags = []; /** @type {Statement[]} */ - const internal_statements = []; + const hoisted = []; - const snippets_visits = []; + // const tags need to live inside the boundary, but might also be referenced in hoisted snippets. + // to resolve this we cheat: we duplicate const tags inside snippets + for (const child of node.fragment.nodes) { + if (child.type === 'ConstTag') { + context.visit(child, { ...context.state, init: const_tags }); + } + } - // Capture the `failed` implicit snippet prop for (const child of node.fragment.nodes) { - 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(() => { - /** @type {Statement[]} */ - const init = []; - context.visit(child, { ...context.state, init }); - props.properties.push(b.prop('init', child.expression, child.expression)); - external_statements.push(...init); - }); - } else if (child.type === 'ConstTag') { + if (child.type === 'ConstTag') { + continue; + } + + if (child.type === 'SnippetBlock') { /** @type {Statement[]} */ - const init = []; - context.visit(child, { ...context.state, init }); - - if (dev) { - // In dev we must separate the declarations from the code - // that eagerly evaluate the expression... - for (const statement of init) { - if (statement.type === 'VariableDeclaration') { - external_statements.push(statement); - } else { - internal_statements.push(statement); - } - } - } else { - external_statements.push(...init); + const statements = []; + + context.visit(child, { ...context.state, init: statements }); + + const snippet = /** @type {VariableDeclaration} */ (statements[0]); + + const snippet_fn = dev + ? // @ts-expect-error we know this shape is correct + snippet.declarations[0].init.arguments[1] + : snippet.declarations[0].init; + + snippet_fn.body.body.unshift( + ...const_tags.filter((node) => node.type === 'VariableDeclaration') + ); + + hoisted.push(snippet); + + if (['failed', 'pending'].includes(child.expression.name)) { + props.properties.push(b.prop('init', child.expression, child.expression)); } - } else { - nodes.push(child); + + continue; } - } - snippets_visits.forEach((visit) => visit()); + nodes.push(child); + } const block = /** @type {BlockStatement} */ (context.visit({ ...node.fragment, nodes })); - if (dev && internal_statements.length) { - block.body.unshift(...internal_statements); - } + block.body.unshift(...const_tags); const boundary = b.stmt( b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) ); context.state.template.push_comment(); - context.state.init.push( - external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary - ); + context.state.init.push(hoisted.length > 0 ? b.block([...hoisted, boundary]) : boundary); } From c566d562d205e7ebb59a85804f67a0b56a260229 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 16:12:55 -0400 Subject: [PATCH 388/582] WIP --- packages/svelte/src/internal/client/reactivity/effects.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 84d4ec973d50..1aa9754a9488 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -42,6 +42,7 @@ import { async_derived, derived } from './deriveds.js'; import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; import { Batch, current_batch } from './batch.js'; +import { invoke_error_boundary } from '../error-handling.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -352,7 +353,12 @@ export function template_effect(fn, sync = [], async = [], d = derived) { batch?.restore(); restore(); - create_template_effect(fn, [...sync.map(d), ...result]); + + try { + create_template_effect(fn, [...sync.map(d), ...result]); + } catch (error) { + invoke_error_boundary(error, parent); + } batch?.flush(); }); From 60f8653417e573aaf97c1a5992b7686b14aa1872 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 16:14:11 -0400 Subject: [PATCH 389/582] fix --- .../svelte/tests/runtime-runes/samples/async-error/_config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js index 52ac43208353..61cfe4510453 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -11,6 +11,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); + await Promise.resolve(); flushSync(); assert.htmlEqual( target.innerHTML, From 858dc357a1d2062a935f235863a62f15c6193eea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 16:37:23 -0400 Subject: [PATCH 390/582] add test --- .../samples/async-error-skipped/_config.js | 56 +++++++++++++++++++ .../samples/async-error-skipped/main.svelte | 43 ++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-skipped/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-skipped/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-skipped/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-skipped/_config.js new file mode 100644 index 000000000000..c73fdbf268fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-skipped/_config.js @@ -0,0 +1,56 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +

a

+ + + + +

a

+ ` + ); + + const [a, b, c, ok] = target.querySelectorAll('button'); + + flushSync(() => b.click()); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + + +

c

+ ` + ); + + flushSync(() => ok.click()); + + flushSync(() => b.click()); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

b

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

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

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

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
From 68e2eee3b12e5cec0735dd5bf8711279d8b045e0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 16:41:31 -0400 Subject: [PATCH 391/582] rename --- .../samples/{async-error-skipped => async-redirect}/_config.js | 0 .../samples/{async-error-skipped => async-redirect}/main.svelte | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/svelte/tests/runtime-runes/samples/{async-error-skipped => async-redirect}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{async-error-skipped => async-redirect}/main.svelte (100%) diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-skipped/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-error-skipped/_config.js rename to packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-skipped/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/async-error-skipped/main.svelte rename to packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte From 5a05dc54f1025667428f311b1b9ed83f7d6f8f05 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Jun 2025 16:52:09 -0400 Subject: [PATCH 392/582] fix --- .../internal/client/reactivity/deriveds.js | 4 -- .../samples/async-redirect-initial/_config.js | 54 +++++++++++++++++++ .../async-redirect-initial/main.svelte | 43 +++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 9041dd87988a..d6a73b8e36f7 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -145,10 +145,6 @@ export function async_derived(fn, location) { const handler = (value, error = undefined) => { prev = null; - if ((parent.f & DESTROYED) !== 0) { - return; - } - from_async_derived = null; if (should_suspend) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js new file mode 100644 index 000000000000..1a0a855c125f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js @@ -0,0 +1,54 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, c, ok] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

pending...

+ ` + ); + + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + + +

c

+ ` + ); + + flushSync(() => ok.click()); + + flushSync(() => b.click()); + await Promise.resolve(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

b

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte new file mode 100644 index 000000000000..b1bb291e2e76 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte @@ -0,0 +1,43 @@ + + +

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

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

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
From 0bd53105ec2d8c70d7029a73f2e0371ed25f41c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 8 Jun 2025 11:08:49 -0400 Subject: [PATCH 393/582] unused --- packages/svelte/src/internal/client/runtime.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3d1b0ff75b65..3a38573aa083 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -28,7 +28,6 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT, REACTION_IS_UPDATING, EFFECT_IS_UPDATING, EFFECT_ASYNC, @@ -46,7 +45,6 @@ import { update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; -import { FILENAME } from '../../constants.js'; import { async_mode_flag, tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; import { @@ -56,20 +54,10 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; -import { Boundary } from './dom/blocks/boundary.js'; import * as w from './warnings.js'; -import { is_firefox } from './dom/operations.js'; import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; -import { log_effect_tree, root } from './dev/debug.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; -// Used for DEV time error handling -/** @param {WeakSet} value */ -const handled_errors = new WeakSet(); -let is_throwing_error = false; - -let is_flushing = false; - /** @type {Effect | null} */ let last_scheduled_effect = null; From bc1a4cad9656b0fe3dc155c7db1df8e0e0801165 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 8 Jun 2025 11:10:18 -0400 Subject: [PATCH 394/582] oops --- 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 3a38573aa083..f051d16cb101 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -570,7 +570,6 @@ export function flush_queued_root_effects() { old_values.clear(); } } finally { - is_flushing = false; is_updating_effect = was_updating_effect; last_scheduled_effect = null; @@ -712,10 +711,8 @@ export function flushSync(fn) { const batch = Batch.ensure(); if (fn) { - is_flushing = true; flush_queued_root_effects(); - is_flushing = true; result = fn(); } @@ -730,7 +727,6 @@ export function flushSync(fn) { return /** @type {T} */ (result); } - is_flushing = true; flush_queued_root_effects(); } } From 61a11a57e8f26a89803dc0f10c9e9dcb7be5ceec Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:20:51 +0200 Subject: [PATCH 395/582] chore: merge main into async branch (#16197) * chore: merge main into async branch * adjust test * fix: make effects depend on state created inside them (#16198) * make effects depend on state created inside them * fix, add github action * disable test in async mode --- .changeset/fair-laws-appear.md | 5 ++ .github/workflows/ci.yml | 17 +++++ documentation/docs/07-misc/02-testing.md | 12 ++-- packages/svelte/CHANGELOG.md | 20 ++++++ packages/svelte/package.json | 2 +- .../src/compiler/phases/1-parse/state/tag.js | 9 ++- .../phases/2-analyze/css/css-prune.js | 17 +++-- .../visitors/AssignmentExpression.js | 4 ++ .../phases/2-analyze/visitors/AwaitBlock.js | 5 +- .../phases/2-analyze/visitors/ConstTag.js | 5 +- .../phases/2-analyze/visitors/HtmlTag.js | 5 +- .../phases/2-analyze/visitors/Identifier.js | 1 + .../phases/2-analyze/visitors/KeyBlock.js | 6 +- .../2-analyze/visitors/MemberExpression.js | 5 +- .../phases/2-analyze/visitors/RenderTag.js | 2 +- .../2-analyze/visitors/UpdateExpression.js | 4 ++ .../2-analyze/visitors/shared/function.js | 10 +++ .../3-transform/client/visitors/AttachTag.js | 13 +--- .../3-transform/client/visitors/AwaitBlock.js | 5 +- .../3-transform/client/visitors/Component.js | 8 +-- .../3-transform/client/visitors/ConstTag.js | 24 +++---- .../3-transform/client/visitors/EachBlock.js | 21 ++++-- .../3-transform/client/visitors/HtmlTag.js | 5 +- .../3-transform/client/visitors/IfBlock.js | 4 +- .../3-transform/client/visitors/KeyBlock.js | 3 +- .../client/visitors/RegularElement.js | 2 +- .../3-transform/client/visitors/RenderTag.js | 14 +++- .../client/visitors/TitleElement.js | 3 +- .../client/visitors/shared/component.js | 23 +++++-- .../client/visitors/shared/element.js | 6 +- .../client/visitors/shared/fragment.js | 28 ++++---- .../client/visitors/shared/utils.js | 59 +++++++++++++++-- packages/svelte/src/compiler/phases/nodes.js | 3 + packages/svelte/src/compiler/types/index.d.ts | 8 ++- .../svelte/src/compiler/types/template.d.ts | 9 +++ .../client/dom/elements/attributes.js | 14 ++-- packages/svelte/src/internal/client/proxy.js | 28 ++++---- .../src/internal/client/reactivity/sources.js | 2 +- .../svelte/src/internal/client/runtime.js | 31 ++++++--- packages/svelte/src/internal/flags/index.js | 5 ++ packages/svelte/src/version.js | 2 +- .../expected.css | 2 + .../input.svelte | 7 ++ .../css/samples/class-directive/_config.js | 20 ++++++ .../css/samples/class-directive/expected.css | 3 + .../css/samples/class-directive/input.svelte | 7 ++ packages/svelte/tests/helpers.js | 2 + .../removes-undefined-attributes/_config.js | 11 ++++ .../_expected.html | 1 + .../removes-undefined-attributes/main.svelte | 9 +++ .../block-expression-assign/_config.js | 12 ++++ .../block-expression-assign/main.svelte | 45 +++++++++++++ .../block-expression-fn-call/_config.js | 12 ++++ .../block-expression-fn-call/main.svelte | 36 ++++++++++ .../block-expression-member-access/_config.js | 12 ++++ .../main.svelte | 46 +++++++++++++ .../Item.svelte | 4 +- .../main.svelte | 4 +- .../svelte/tests/runtime-legacy/shared.ts | 20 ++++-- .../samples/array-sort-in-effect/_config.js | 52 +++++++++++++++ .../samples/array-sort-in-effect/main.svelte | 21 ++++++ .../samples/dynamic-component-nested/A.svelte | 5 ++ .../dynamic-component-nested/_config.js | 8 +++ .../dynamic-component-nested/main.svelte | 9 +++ .../samples/effect-cleanup/_config.js | 9 ++- .../samples/effect-cleanup/main.svelte | 2 +- .../event-attribute-spread-update/_config.js | 18 +++++ .../event-attribute-spread-update/main.svelte | 27 ++++++++ .../samples/proxy-set-with-parent/_config.js | 5 ++ .../samples/proxy-set-with-parent/main.svelte | 15 +++++ .../set-context-after-await/_config.js | 1 + .../set-context-after-mount/_config.js | 7 +- .../set-context-after-mount/main.svelte | 1 + .../samples/untrack-own-deriveds/_config.js | 2 + packages/svelte/tests/signals/test.ts | 65 +++++++++++++++++-- .../purity/_expected/client/index.svelte.js | 6 +- packages/svelte/tests/suite.ts | 6 +- 77 files changed, 795 insertions(+), 166 deletions(-) create mode 100644 .changeset/fair-laws-appear.md create mode 100644 packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css create mode 100644 packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte create mode 100644 packages/svelte/tests/css/samples/class-directive/_config.js create mode 100644 packages/svelte/tests/css/samples/class-directive/expected.css create mode 100644 packages/svelte/tests/css/samples/class-directive/input.svelte create mode 100644 packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js create mode 100644 packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte diff --git a/.changeset/fair-laws-appear.md b/.changeset/fair-laws-appear.md new file mode 100644 index 000000000000..9a1149ff279d --- /dev/null +++ b/.changeset/fair-laws-appear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: match class and style directives against attribute selector diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0e1d3676041..046ad335f3ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,23 @@ jobs: - run: pnpm test env: CI: true + TestNoAsync: + permissions: {} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm playwright install chromium + - run: pnpm test runtime-runes + env: + CI: true + SVELTE_NO_ASYNC: true Lint: permissions: {} runs-on: ubuntu-latest diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index 64bf49d77a27..db99b7077022 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -129,12 +129,12 @@ test('Effect', () => { // effects normally run after a microtask, // use flushSync to execute all pending effects synchronously flushSync(); - expect(log.value).toEqual([0]); + expect(log).toEqual([0]); count = 1; flushSync(); - expect(log.value).toEqual([0, 1]); + expect(log).toEqual([0, 1]); }); cleanup(); @@ -148,17 +148,13 @@ test('Effect', () => { */ export function logger(getValue) { /** @type {any[]} */ - let log = $state([]); + let log = []; $effect(() => { log.push(getValue()); }); - return { - get value() { - return log; - } - }; + return log; } ``` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 020942f5fd8a..618a25c63827 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,25 @@ # svelte +## 5.34.5 + +### Patch Changes + +- fix: keep spread non-delegated event handlers up to date ([#16180](https://github.com/sveltejs/svelte/pull/16180)) + +- fix: remove undefined attributes on hydration ([#16178](https://github.com/sveltejs/svelte/pull/16178)) + +- fix: ensure sources within nested effects still register correctly ([#16193](https://github.com/sveltejs/svelte/pull/16193)) + +- fix: avoid shadowing a variable in dynamic components ([#16185](https://github.com/sveltejs/svelte/pull/16185)) + +## 5.34.4 + +### Patch Changes + +- fix: don't set state withing `with_parent` in proxy ([#16176](https://github.com/sveltejs/svelte/pull/16176)) + +- fix: use compiler-driven reactivity in legacy mode template expressions ([#16100](https://github.com/sveltejs/svelte/pull/16100)) + ## 5.34.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c2a27b2595ec..e01691ff6317 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.34.3", + "version": "5.34.5", "type": "module", "types": "./types/index.d.ts", "engines": { 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 fa6e66634398..5d77d6a8f4b6 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -247,7 +247,10 @@ function open(parser) { error: null, pending: null, then: null, - catch: null + catch: null, + metadata: { + expression: create_expression_metadata() + } }); if (parser.eat('then')) { @@ -711,6 +714,9 @@ function special(parser) { declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }], start: start + 2, // start at const, not at @const end: parser.index - 1 + }, + metadata: { + expression: create_expression_metadata() } }); } @@ -737,6 +743,7 @@ function special(parser) { end: parser.index, expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { + expression: create_expression_metadata(), dynamic: false, arguments: [], path: [], diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index fbe6ca1cd379..b9a5688a87d0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -532,12 +532,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } case 'ClassSelector': { - if ( - !attribute_matches(element, 'class', name, '~=', false) && - !element.attributes.some( - (attribute) => attribute.type === 'ClassDirective' && attribute.name === name - ) - ) { + if (!attribute_matches(element, 'class', name, '~=', false)) { return false; } @@ -633,6 +628,16 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv if (attribute.type === 'SpreadAttribute') return true; if (attribute.type === 'BindDirective' && attribute.name === name) return true; + // match attributes against the corresponding directive but bail out on exact matching + if (attribute.type === 'StyleDirective' && name.toLowerCase() === 'style') return true; + if (attribute.type === 'ClassDirective' && name.toLowerCase() === 'class') { + if (operator == '~=') { + if (attribute.name === expected_value) return true; + } else { + return true; + } + } + if (attribute.type !== 'Attribute') continue; if (attribute.name.toLowerCase() !== name.toLowerCase()) continue; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js index 673c79f2df0f..39358f72fc1b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js @@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js index a71f325154ff..5aa04ba3b9a8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js @@ -41,5 +41,8 @@ export function AwaitBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + if (node.pending) context.visit(node.pending); + if (node.then) context.visit(node.then); + if (node.catch) context.visit(node.catch); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index f723f8447cd2..d5f5f7b2e0a0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -32,5 +32,8 @@ export function ConstTag(node, context) { e.const_tag_invalid_placement(node); } - context.next(); + const declaration = node.declaration.declarations[0]; + + context.visit(declaration.id); + context.visit(declaration.init, { ...context.state, expression: node.metadata.expression }); } 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 ccb2c17955d8..7b0e501760f0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js @@ -15,8 +15,5 @@ export function HtmlTag(node, context) { // unfortunately this is necessary in order to fix invalid HTML mark_subtree_dynamic(context.path); - context.next({ - ...context.state, - expression: node.metadata.expression - }); + context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index abf70769c013..cced326f9baa 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -90,6 +90,7 @@ export function Identifier(node, context) { if (binding) { if (context.state.expression) { context.state.expression.dependencies.add(binding); + context.state.expression.references.add(binding); context.state.expression.has_state ||= binding.kind !== 'static' && !binding.is_function() && 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 d0dcf8e15c51..09e604ea66be 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js @@ -16,10 +16,6 @@ export function KeyBlock(node, context) { mark_subtree_dynamic(context.path); - context.visit(node.expression, { - ...context.state, - expression: node.metadata.expression - }); - + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); context.visit(node.fragment); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 245a164c71fb..0a3b3861986c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -15,8 +15,9 @@ export function MemberExpression(node, context) { } } - if (context.state.expression && !is_pure(node, context)) { - context.state.expression.has_state = true; + if (context.state.expression) { + context.state.expression.has_member_expression = true; + context.state.expression.has_state ||= !is_pure(node, context); } if (!is_safe_identifier(node, context.state.scope)) { 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 a8c9d408bdad..1230ef6b048c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -54,7 +54,7 @@ export function RenderTag(node, context) { mark_subtree_dynamic(context.path); - context.visit(callee); + context.visit(callee, { ...context.state, expression: node.metadata.expression }); for (const arg of expression.arguments) { const metadata = create_expression_metadata(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js index 13f4b9019e8b..ed48e026ac65 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js @@ -21,5 +21,9 @@ export function UpdateExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } 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 c892efd421d1..177616785026 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 @@ -13,6 +13,16 @@ export function visit_function(node, context) { scope: context.state.scope }; + if (context.state.expression) { + for (const [name] of context.state.scope.references) { + const binding = context.state.scope.get(name); + + if (binding && binding.scope.function_depth < context.state.scope.function_depth) { + context.state.expression.references.add(binding); + } + } + } + context.next({ ...context.state, function_depth: context.state.function_depth + 1, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 062604cacc16..8b1570c7dc3c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -1,21 +1,14 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AttachTag} node * @param {ComponentContext} context */ export function AttachTag(node, context) { - context.state.init.push( - b.stmt( - b.call( - '$.attach', - context.state.node, - b.thunk(/** @type {Expression} */ (context.visit(node.expression))) - ) - ) - ); + const expression = build_expression(context, node.expression, node.metadata.expression); + context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)))); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 30e370327fa1..7873cf3ddbd7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,10 +1,11 @@ -/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ +/** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -14,7 +15,7 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression))); + const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression)); let then_block; let catch_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js index d58a24b45559..9b86557536d0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js @@ -8,12 +8,6 @@ import { build_component } from './shared/component.js'; * @param {ComponentContext} context */ export function Component(node, context) { - const component = build_component( - node, - // if it's not dynamic we will just use the node name, if it is dynamic we will use the node name - // only if it's a valid identifier, otherwise we will use a default name - !node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component', - context - ); + const component = build_component(node, node.name, context); context.state.init.push(component); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 2f3c0b3d0ed1..c1be1e3220b0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -1,4 +1,4 @@ -/** @import { Expression, Pattern } from 'estree' */ +/** @import { Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.ConstTag} node @@ -15,15 +16,8 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { - context.state.init.push( - b.const( - declaration.id, - create_derived( - context.state, - b.thunk(/** @type {Expression} */ (context.visit(declaration.init))) - ) - ) - ); + const init = build_expression(context, declaration.init, node.metadata.expression); + context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); context.state.transform[declaration.id.name] = { read: get_value }; @@ -48,13 +42,15 @@ export function ConstTag(node, context) { // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object + const init = build_expression( + { ...context, state: child_state }, + declaration.init, + node.metadata.expression + ); const fn = b.arrow( [], b.block([ - b.const( - /** @type {Pattern} */ (context.visit(declaration.id, child_state)), - /** @type {Expression} */ (context.visit(declaration.init, child_state)) - ), + b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]) ); 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 64967dfc96a9..f5758893b2d5 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 @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { ComponentContext } from '../types' */ /** @import { Scope } from '../../../scope' */ @@ -12,8 +12,8 @@ import { import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_getter } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -24,11 +24,18 @@ export function EachBlock(node, context) { // expression should be evaluated in the parent scope, not the scope // created by the each block itself - const collection = /** @type {Expression} */ ( - context.visit(node.expression, { - ...context.state, - scope: /** @type {Scope} */ (context.state.scope.parent) - }) + const parent_scope_state = { + ...context.state, + scope: /** @type {Scope} */ (context.state.scope.parent) + }; + + const collection = build_expression( + { + ...context, + state: parent_scope_state + }, + node.expression, + node.metadata.expression ); if (!each_node_meta.is_controlled) { 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 590b32885b49..64e84ef2ffc6 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 @@ -1,8 +1,8 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.HtmlTag} node @@ -12,8 +12,7 @@ export function HtmlTag(node, context) { context.state.template.push_comment(); const { has_await } = node.metadata.expression; - - const expression = /** @type {Expression} */ (context.visit(node.expression)); + const expression = build_expression(context, node.expression, node.metadata.expression); const html = has_await ? b.call('$.get', b.id('$$html')) : expression; const is_svg = context.state.metadata.namespace === 'svg'; 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 e06802f0d547..4bd0e1893244 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 @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -25,8 +26,7 @@ export function IfBlock(node, context) { } const { has_await } = node.metadata.expression; - - const expression = /** @type {Expression} */ (context.visit(node.test)); + const expression = build_expression(context, node.test, node.metadata.expression); const test = has_await ? b.call('$.get', b.id('$$condition')) : expression; /** @type {Expression[]} */ 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 80b5a232271e..c5b1d9def3a3 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 @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -10,7 +11,7 @@ import * as b from '#compiler/builders'; export function KeyBlock(node, context) { context.state.template.push_comment(); - const key = /** @type {Expression} */ (context.visit(node.expression)); + const key = build_expression(context, node.expression, node.metadata.expression); const body = /** @type {Expression} */ (context.visit(node.fragment)); if (node.metadata.expression.has_await) { 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 8024b725ae92..81f7229703ed 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 @@ -340,7 +340,7 @@ export function RegularElement(node, context) { trimmed.some((node) => node.type === 'ExpressionTag'); if (use_text_content) { - const { value } = build_template_chunk(trimmed, context.visit, child_state); + const { value } = build_template_chunk(trimmed, context, child_state); const empty_string = value.type === 'Literal' && value.value === ''; if (!empty_string) { 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 12ec2b432a21..e741634c8986 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 @@ -4,7 +4,7 @@ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; -import { get_expression_id } from './shared/utils.js'; +import { get_expression_id, build_expression } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -28,7 +28,11 @@ export function RenderTag(node, context) { const async_expressions = []; for (let i = 0; i < raw_args.length; i++) { - let expression = /** @type {Expression} */ (context.visit(raw_args[i])); + let expression = build_expression( + context, + /** @type {Expression} */ (raw_args[i]), + node.metadata.arguments[i] + ); const { has_call, has_await } = node.metadata.arguments[i]; if (has_await || has_call) { @@ -50,7 +54,11 @@ export function RenderTag(node, context) { b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) ); - let snippet_function = /** @type {Expression} */ (context.visit(callee)); + let snippet_function = build_expression( + context, + /** @type {Expression} */ (callee), + node.metadata.expression + ); if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one 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 7bfdaf1850d2..e6f4202a0189 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 @@ -10,8 +10,7 @@ import { build_template_chunk } from './shared/utils.js'; export function TitleElement(node, context) { const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), - context.visit, - context.state + context ); const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value)); 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 bdfb71152c70..d14a60da672b 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 @@ -59,6 +59,15 @@ export function build_component(node, component_name, context) { /** @type {ExpressionStatement[]} */ const binding_initializers = []; + const is_component_dynamic = + node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic); + + // The variable name used for the component inside $.component() + const intermediate_name = + node.type === 'Component' && node.metadata.dynamic + ? context.state.scope.generate(node.name) + : '$$component'; + /** * If this component has a slot property, it is a named slot within another component. In this case * the slot scope applies to the component itself, too, and not just its children. @@ -223,7 +232,7 @@ export function build_component(node, component_name, context) { b.call( '$$ownership_validator.binding', b.literal(binding.node.name), - b.id(component_name), + b.id(is_component_dynamic ? intermediate_name : component_name), b.thunk(expression) ) ) @@ -299,7 +308,7 @@ export function build_component(node, component_name, context) { ); } - push_prop(b.prop('get', b.call('$.attachment'), expression, true)); + push_prop(b.prop('init', b.call('$.attachment'), expression, true)); } } @@ -438,8 +447,8 @@ export function build_component(node, component_name, context) { // TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components // will be handled separately through the `$.component` function, and then the component name will // always be referenced through just the identifier here. - node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic) - ? component_name + is_component_dynamic + ? intermediate_name : /** @type {Expression} */ (context.visit(b.member_id(component_name))), node_id, props_expression @@ -461,7 +470,7 @@ export function build_component(node, component_name, context) { ) ]; - if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) { + if (is_component_dynamic) { const prev = fn; fn = (node_id) => { @@ -470,11 +479,11 @@ export function build_component(node, component_name, context) { node_id, b.thunk( /** @type {Expression} */ ( - context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression) + context.visit(node.type === 'Component' ? b.member_id(component_name) : node.expression) ) ), b.arrow( - [b.id('$$anchor'), b.id(component_name)], + [b.id('$$anchor'), b.id(intermediate_name)], b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))]) ) ); 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 6733b6932f6c..30f11e3ff62b 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 @@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; -import { build_template_chunk, get_expression_id } from './utils.js'; +import { build_expression, build_template_chunk, get_expression_id } from './utils.js'; /** * @param {Array} attributes @@ -125,7 +125,7 @@ export function build_attribute_value(value, context, memoize = (value) => value return { value: b.literal(chunk.data), has_state: false }; } - let expression = /** @type {Expression} */ (context.visit(chunk.expression)); + let expression = build_expression(context, chunk.expression, chunk.metadata.expression); return { value: memoize(expression, chunk.metadata.expression), @@ -133,7 +133,7 @@ export function build_attribute_value(value, context, memoize = (value) => value }; } - return build_template_chunk(value, context.visit, context.state, memoize); + return build_template_chunk(value, context, context.state, memoize); } /** 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 7af2c2d4aaa8..62d07014eea4 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 @@ -16,8 +16,8 @@ import { build_template_chunk } from './utils.js'; * @param {boolean} is_element * @param {ComponentContext} context */ -export function process_children(nodes, initial, is_element, { visit, state }) { - const within_bound_contenteditable = state.metadata.bound_contenteditable; +export function process_children(nodes, initial, is_element, context) { + const within_bound_contenteditable = context.state.metadata.bound_contenteditable; let prev = initial; let skipped = 0; @@ -48,8 +48,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) { let id = expression; if (id.type !== 'Identifier') { - id = b.id(state.scope.generate(name)); - state.init.push(b.var(id, expression)); + id = b.id(context.state.scope.generate(name)); + context.state.init.push(b.var(id, expression)); } prev = () => id; @@ -64,13 +64,13 @@ export function process_children(nodes, initial, is_element, { visit, state }) { function flush_sequence(sequence) { if (sequence.every((node) => node.type === 'Text')) { skipped += 1; - state.template.push_text(sequence); + context.state.template.push_text(sequence); return; } - state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); + context.state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); - const { has_state, value } = build_template_chunk(sequence, visit, state); + const { has_state, value } = build_template_chunk(sequence, context); // 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 @@ -80,9 +80,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_state && !within_bound_contenteditable) { - state.update.push(update); + context.state.update.push(update); } else { - state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); + context.state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); } } @@ -95,18 +95,18 @@ export function process_children(nodes, initial, is_element, { visit, state }) { sequence = []; } - let child_state = state; + let child_state = context.state; - if (is_static_element(node, state)) { + if (is_static_element(node, context.state)) { skipped += 1; } else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) { node.metadata.is_controlled = true; } else { const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node'); - child_state = { ...state, node: id }; + child_state = { ...context.state, node: id }; } - visit(node, child_state); + context.visit(node, child_state); } } @@ -118,7 +118,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { // traverse to the last (n - 1) one when hydrating if (skipped > 1) { skipped -= 1; - state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); + context.state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); } } 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 fa67bfe3e151..bd3820dc6a9d 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 { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ +/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ -/** @import { ComponentClientTransformState, Context, MemoizedExpression } from '../../types' */ +/** @import { ComponentClientTransformState, ComponentContext, Context, MemoizedExpression } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator } from '../../../../../state.js'; -import { create_derived } from '../../utils.js'; +import { build_getter, create_derived } from '../../utils.js'; /** * @param {ComponentClientTransformState} state @@ -35,15 +35,15 @@ export function get_expression_id(expressions, expression) { /** * @param {Array} values - * @param {(node: AST.SvelteNode, state: any) => any} visit + * @param {ComponentContext} context * @param {ComponentClientTransformState} state * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize * @returns {{ value: Expression, has_state: boolean }} */ export function build_template_chunk( values, - visit, - state, + context, + state = context.state, memoize = (value, metadata) => metadata.has_call || metadata.has_await ? get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value) @@ -73,7 +73,7 @@ export function build_template_chunk( state.scope.get('undefined') ) { let value = memoize( - /** @type {Expression} */ (visit(node.expression, state)), + build_expression(context, node.expression, node.metadata.expression, state), node.metadata.expression ); @@ -377,3 +377,48 @@ export function validate_mutation(node, context, expression) { loc && b.literal(loc.column) ); } + +/** + * + * @param {ComponentContext} context + * @param {Expression} expression + * @param {ExpressionMetadata} metadata + */ +export function build_expression(context, expression, metadata, state = context.state) { + const value = /** @type {Expression} */ (context.visit(expression, state)); + + if (context.state.analysis.runes) { + return value; + } + + if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) { + return value; + } + + // Legacy reactivity is coarse-grained, looking at the statically visible dependencies. Replicate that here + const sequence = b.sequence([]); + + for (const binding of metadata.references) { + if (binding.kind === 'normal' && binding.declaration_kind !== 'import') { + continue; + } + + var getter = build_getter({ ...binding.node }, state); + + if ( + binding.kind === 'bindable_prop' || + binding.kind === 'template' || + binding.declaration_kind === 'import' || + binding.node.name === '$$props' || + binding.node.name === '$$restProps' + ) { + getter = b.call('$.deep_read_state', getter); + } + + sequence.expressions.push(getter); + } + + sequence.expressions.push(b.call('$.untrack', b.thunk(value))); + + return sequence; +} diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index faf11f373d4c..4874554ff0fb 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -62,8 +62,11 @@ export function create_attribute(name, start, end, value) { export function create_expression_metadata() { return { dependencies: new Set(), + references: new Set(), has_state: false, has_call: false, + has_member_expression: false, + has_assignment: false, has_await: false }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 059e4c8839da..c4f41b724ac2 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -284,14 +284,20 @@ export type DeclarationKind = | 'synthetic'; export interface ExpressionMetadata { - /** All the bindings that are referenced inside this expression */ + /** All the bindings that are referenced eagerly (not inside functions) in this expression */ dependencies: Set; + /** All the bindings that are referenced inside this expression, including inside functions */ + references: Set; /** True if the expression references state directly, or _might_ (via member/call expressions) */ 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` */ has_await: boolean; + /** True if the expression includes a member expression */ + has_member_expression: boolean; + /** True if the expression includes an assignment or an update */ + has_assignment: boolean; } export interface StateField { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index f9af18582673..e7abb266d002 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -155,6 +155,10 @@ export namespace AST { declaration: VariableDeclaration & { declarations: [VariableDeclarator & { id: Pattern; init: Expression }]; }; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** A `{@debug ...}` tag */ @@ -169,6 +173,7 @@ export namespace AST { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); /** @internal */ metadata: { + expression: ExpressionMetadata; dynamic: boolean; arguments: ExpressionMetadata[]; path: SvelteNode[]; @@ -470,6 +475,10 @@ export namespace AST { pending: Fragment | null; then: Fragment | null; catch: Fragment | null; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } export interface KeyBlock extends BaseNode { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index fcce0b444f49..2d3d6a921dc1 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -345,7 +345,11 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal } var prev_value = current[key]; - if (value === prev_value) continue; + + // Skip if value is unchanged, unless it's `undefined` and the element still has the attribute + if (value === prev_value && !(value === undefined && element.hasAttribute(key))) { + continue; + } current[key] = value; @@ -483,8 +487,8 @@ export function attribute_effect( block(() => { var next = fn(...deriveds.map(get)); - - set_attributes(element, prev, next, css_hash, skip_warning); + /** @type {Record} */ + var current = set_attributes(element, prev, next, css_hash, skip_warning); if (inited && is_select && 'value' in next) { select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); @@ -501,9 +505,11 @@ export function attribute_effect( if (effects[symbol]) destroy_effect(effects[symbol]); effects[symbol] = branch(() => attach(element, () => n)); } + + current[symbol] = n; } - prev = next; + prev = current; }); if (is_select) { diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 487050669933..d9063aee3436 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -44,6 +44,7 @@ export function proxy(value) { var reaction = active_reaction; /** + * Executes the proxy in the context of the reaction it was originally created in, if any * @template T * @param {() => T} fn */ @@ -93,21 +94,19 @@ export function proxy(value) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants e.state_descriptors_fixed(); } - - with_parent(() => { - var s = sources.get(prop); - - if (s === undefined) { - s = source(descriptor.value, stack); + var s = sources.get(prop); + if (s === undefined) { + s = with_parent(() => { + var s = source(descriptor.value, stack); sources.set(prop, s); - if (DEV && typeof prop === 'string') { tag(s, get_label(path, prop)); } - } else { - set(s, descriptor.value, true); - } - }); + return s; + }); + } else { + set(s, descriptor.value, true); + } return true; }, @@ -268,11 +267,8 @@ export function proxy(value) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - s = with_parent(() => { - var s = source(undefined, stack); - set(s, proxy(value)); - return s; - }); + s = with_parent(() => source(undefined, stack)); + set(s, proxy(value)); sources.set(prop, s); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index b09d079479ab..44185e118f69 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -143,7 +143,7 @@ export function set(source, value, should_proxy = false) { !untracking && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT | EFFECT_ASYNC)) !== 0 && - !reaction_sources?.includes(source) + !(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction) ) { e.state_unsafe_mutation(); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 78b38912e2ba..3b6886467d27 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -57,7 +57,6 @@ import { import * as w from './warnings.js'; import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; -import { snapshot } from '../shared/clone.js'; /** @type {Effect | null} */ let last_scheduled_effect = null; @@ -105,8 +104,8 @@ export function set_active_effect(effect) { /** * When sources are created within a reaction, reading and writing - * them should not cause a re-run - * @type {null | Source[]} + * them within that reaction should not cause a re-run + * @type {null | [active_reaction: Reaction, sources: Source[]]} */ export let reaction_sources = null; @@ -114,9 +113,9 @@ export let reaction_sources = null; export function push_reaction_value(value) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { if (reaction_sources === null) { - reaction_sources = [value]; + reaction_sources = [active_reaction, [value]]; } else { - reaction_sources.push(value); + reaction_sources[1].push(value); } } } @@ -259,7 +258,12 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; - if (reaction_sources?.includes(signal)) continue; + if ( + !async_mode_flag && + reaction_sources?.[1].includes(signal) && + reaction_sources[0] === active_reaction + ) + continue; if ((reaction.f & DERIVED) !== 0) { schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); @@ -299,7 +303,9 @@ export function update_reaction(reaction) { untracking = false; read_version++; - reaction.f |= EFFECT_IS_UPDATING; + if (!async_mode_flag || (reaction.f & DERIVED) !== 0) { + reaction.f |= EFFECT_IS_UPDATING; + } if (reaction.ac !== null) { reaction.ac?.abort(STALE_REACTION); @@ -383,7 +389,9 @@ export function update_reaction(reaction) { set_component_context(previous_component_context); untracking = previous_untracking; - reaction.f ^= EFFECT_IS_UPDATING; + if (!async_mode_flag || (reaction.f & DERIVED) !== 0) { + reaction.f ^= EFFECT_IS_UPDATING; + } } } @@ -774,7 +782,12 @@ export function get(signal) { // 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 && !reaction_sources?.includes(signal)) { + if ( + !destroyed && + ((async_mode_flag && (active_reaction.f & DERIVED) === 0) || + !reaction_sources?.[1].includes(signal) || + reaction_sources[0] !== active_reaction) + ) { var deps = active_reaction.deps; if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) { diff --git a/packages/svelte/src/internal/flags/index.js b/packages/svelte/src/internal/flags/index.js index 6920f6b8eeda..ce7bba604bff 100644 --- a/packages/svelte/src/internal/flags/index.js +++ b/packages/svelte/src/internal/flags/index.js @@ -6,6 +6,11 @@ export function enable_async_mode_flag() { async_mode_flag = true; } +/** ONLY USE THIS DURING TESTING */ +export function disable_async_mode_flag() { + async_mode_flag = false; +} + export function enable_legacy_mode_flag() { legacy_mode_flag = true; } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 01888eaa7853..bffca48eec71 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.34.3'; +export const VERSION = '5.34.5'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css new file mode 100644 index 000000000000..4b5e4bfd091f --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css @@ -0,0 +1,2 @@ + span[class].svelte-xyz { color: green } + div[style].svelte-xyz { color: green } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte new file mode 100644 index 000000000000..2f9ab202ca80 --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte @@ -0,0 +1,7 @@ + +
+ + diff --git a/packages/svelte/tests/css/samples/class-directive/_config.js b/packages/svelte/tests/css/samples/class-directive/_config.js new file mode 100644 index 000000000000..28e9fbc81512 --- /dev/null +++ b/packages/svelte/tests/css/samples/class-directive/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".third"\nhttps://svelte.dev/e/css_unused_selector', + start: { + line: 6, + column: 2, + character: 115 + }, + end: { + line: 6, + column: 8, + character: 121 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/class-directive/expected.css b/packages/svelte/tests/css/samples/class-directive/expected.css new file mode 100644 index 000000000000..1d7d3d4dee61 --- /dev/null +++ b/packages/svelte/tests/css/samples/class-directive/expected.css @@ -0,0 +1,3 @@ + .first.svelte-xyz { color: green } + .second.svelte-xyz { color: green } + /* (unused) .third { color: red }*/ diff --git a/packages/svelte/tests/css/samples/class-directive/input.svelte b/packages/svelte/tests/css/samples/class-directive/input.svelte new file mode 100644 index 000000000000..cf0033596415 --- /dev/null +++ b/packages/svelte/tests/css/samples/class-directive/input.svelte @@ -0,0 +1,7 @@ +
+ + \ No newline at end of file diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 591851e69237..410838829e3a 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -194,6 +194,8 @@ if (typeof window !== 'undefined') { export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html'; +export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true'; + /** * @param {any[]} logs */ diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js new file mode 100644 index 000000000000..bc74f23aac60 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + server_props: { + browser: false + }, + + props: { + browser: true + } +}); diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html new file mode 100644 index 000000000000..cc789c8f5142 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html @@ -0,0 +1 @@ +
diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte new file mode 100644 index 000000000000..1a587eeeebc0 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte @@ -0,0 +1,9 @@ + + +
diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js new file mode 100644 index 000000000000..15adef2c9be7 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte new file mode 100644 index 000000000000..67190669ed98 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte @@ -0,0 +1,45 @@ + + +{#if a = 0}{/if} + +{#each [b = 0] as x}{x,''}{/each} + +{#key c = 0}{/key} + +{#await d = 0}{/await} + +{#snippet snip()}{/snippet} + +{@render (e = 0, snip)()} + +{@html f = 0, ''} + +
+ +{#key 1} + {@const x = (h = 0)} + {x, ''} +{/key} + +{#if 1} + {@const x = (i = 0)} + {x, ''} +{/if} + + +[{a},{b},{c},{d},{e},{f},{g},{h},{i}] + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js new file mode 100644 index 000000000000..523dcd625dce --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
12 - 12`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
13 - 12`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte new file mode 100644 index 000000000000..37838f091fdf --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte @@ -0,0 +1,36 @@ + + +{#if fn(false)}{:else if fn(true)}{/if} + +{#each fn([]) as x}{x, ''}{/each} + +{#key fn(1)}{/key} + +{#await fn(Promise.resolve())}{/await} + +{#snippet snip()}{/snippet} + +{@render fn(snip)()} + +{@html fn('')} + +
{})}>
+ +{#key 1} + {@const x = fn('')} + {x} +{/key} + + +{count1} - {count2} + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js new file mode 100644 index 000000000000..0e1a5a81502f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
10 - 10`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
11 - 10`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte new file mode 100644 index 000000000000..4041be4f6fda --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte @@ -0,0 +1,46 @@ + + +{#if obj.false}{:else if obj.true}{/if} + +{#each obj.array as x}{x, ''}{/each} + +{#key obj.string}{/key} + +{#await obj.promise}{/await} + +{#snippet snip()}{/snippet} + +{@render obj.snippet()} + +{@html obj.string} + +
+ +{#key 1} + {@const x = obj.string} + {x} +{/key} + + +{count1} - {count2} + + diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte index b2e6cd046c8e..4127e857d5d5 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte @@ -5,7 +5,7 @@ export let index; export let n; - function logRender () { + function logRender (n) { order.push(`${index}: render ${n}`); return index; } @@ -24,5 +24,5 @@
  • - {logRender()} + {logRender(n)}
  • diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte index b05b1476fdcc..51dee3bc0c25 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte @@ -5,7 +5,7 @@ export let n = 0; - function logRender () { + function logRender (n) { order.push(`parent: render ${n}`); return 'parent'; } @@ -23,7 +23,7 @@ }) -{logRender()} +{logRender(n)}
      {#each [1,2,3] as index} diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 23759d025af1..7f3673f867bd 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -3,10 +3,10 @@ import { setImmediate } from 'node:timers/promises'; import { globSync } from 'tinyglobby'; import { createClassComponent } from 'svelte/legacy'; import { proxy } from 'svelte/internal/client'; -import { flushSync, hydrate, mount, unmount, untrack } from 'svelte'; +import { flushSync, hydrate, mount, unmount } from 'svelte'; import { render } from 'svelte/server'; import { afterAll, assert, beforeAll } from 'vitest'; -import { compile_directory, fragments } from '../helpers.js'; +import { async_mode, compile_directory, fragments } from '../helpers.js'; import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js'; import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; @@ -45,6 +45,10 @@ export interface RuntimeTest = Record; /** Temporarily skip specific modes, without skipping the entire test */ skip_mode?: Array<'server' | 'client' | 'hydrate'>; + /** Skip if running with process.env.NO_ASYNC */ + skip_no_async?: boolean; + /** Skip if running without process.env.NO_ASYNC */ + skip_async?: boolean; html?: string; ssrHtml?: string; compileOptions?: Partial; @@ -121,7 +125,15 @@ let console_error = console.error; export function runtime_suite(runes: boolean) { return suite_with_variants( ['dom', 'hydrate', 'ssr'], - (variant, config) => { + (variant, config, test_name) => { + if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) { + return true; + } + + if (async_mode && config.skip_async) { + return true; + } + if (variant === 'hydrate') { if (config.mode && !config.mode.includes('hydrate')) return 'no-test'; if (config.skip_mode?.includes('hydrate')) return true; @@ -169,7 +181,7 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run dev: force_hmr ? true : undefined, hmr: force_hmr ? true : undefined, experimental: { - async: runes + async: runes && async_mode }, fragments, ...config.compileOptions, diff --git a/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js new file mode 100644 index 000000000000..cbac36fee8ef --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js @@ -0,0 +1,52 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + /** + * Ensure that sorting an array inside an $effect works correctly + * and re-runs when the array changes (e.g., when items are added). + */ + test({ assert, target }) { + const button = target.querySelector('button'); + + // initial render — array should be sorted + assert.htmlEqual( + target.innerHTML, + ` + +

      0

      +

      50

      +

      100

      + ` + ); + + // add first item (20); effect should re-run and sort the array + flushSync(() => button?.click()); + + assert.htmlEqual( + target.innerHTML, + ` + +

      0

      +

      20

      +

      50

      +

      100

      + ` + ); + + // add second item (80); effect should re-run and sort the array + flushSync(() => button?.click()); + + assert.htmlEqual( + target.innerHTML, + ` + +

      0

      +

      20

      +

      50

      +

      80

      +

      100

      + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte new file mode 100644 index 000000000000..c529f67cf4e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte @@ -0,0 +1,21 @@ + + + +{#each arr as x} +

      {x}

      +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte new file mode 100644 index 000000000000..d37c929273be --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte @@ -0,0 +1,5 @@ + + +{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js new file mode 100644 index 000000000000..cd1fa2b1b9a1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, 'test'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte new file mode 100644 index 000000000000..d0646b319b40 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte @@ -0,0 +1,9 @@ + + + + test + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index e55733c14810..53e938d63f40 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -1,3 +1,4 @@ +import { async_mode } from '../../../helpers'; import { test } from '../../test'; import { flushSync } from 'svelte'; @@ -10,6 +11,12 @@ export default test({ flushSync(() => { b1.click(); }); - assert.deepEqual(logs, ['init 0']); + + // With async mode (which is on by default for runtime-runes) this works as expected, without it + // it works differently: https://github.com/sveltejs/svelte/pull/15564 + assert.deepEqual( + logs, + async_mode ? ['init 0', 'cleanup 2', null, 'init 2', 'cleanup 4', null, 'init 4'] : ['init 0'] + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte index 2cdcfdfb58f2..da38374f8232 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte @@ -14,4 +14,4 @@ }) - + diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js new file mode 100644 index 000000000000..af03eed4c9e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js @@ -0,0 +1,18 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [change, increment] = target.querySelectorAll('button'); + + increment.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ''); + + change.click(); + flushSync(); + increment.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte new file mode 100644 index 000000000000..32d4b242cc55 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte @@ -0,0 +1,27 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js new file mode 100644 index 000000000000..2e4a27cf0912 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte new file mode 100644 index 000000000000..7450eff3faa2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js index 0f0edc208b87..1bf7e71176d4 100644 --- a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js @@ -1,6 +1,7 @@ import { test } from '../../test'; export default test({ + skip_no_async: true, async test({ assert, logs }) { await Promise.resolve(); await Promise.resolve(); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js index cc7c483667cd..4569f42a7379 100644 --- a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js @@ -1,11 +1,16 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; +import { async_mode } from '../../../helpers'; export default test({ async test({ target, assert, logs }) { const button = target.querySelector('button'); flushSync(() => button?.click()); - assert.ok(logs[0].startsWith('set_context_after_init')); + assert.ok( + async_mode + ? logs[0].startsWith('set_context_after_init') + : logs[0] === 'works without experimental async but really shouldnt' + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte index 40145c28daa8..0c3b6c3a0fba 100644 --- a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte @@ -7,6 +7,7 @@ if (condition) { try { setContext('potato', {}); + console.log('works without experimental async but really shouldnt') } catch (e) { console.log(e.message); } diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js index 18062b86fb43..b728c3c0bead 100644 --- a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + // In async mode we _do_ want to run effects that react to their own state changing + skip_async: true, test({ assert, target, logs }) { const button = target.querySelector('button'); diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 8421ae4a7cbf..8155aedcb082 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -9,13 +9,14 @@ import { user_effect } from '../../src/internal/client/reactivity/effects'; import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; -import type { Derived, Effect, Value } from '../../src/internal/client/types'; +import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; import { DESTROYED } from '../../src/internal/client/constants'; import { noop } from 'svelte/internal/client'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; /** * @param runes runes mode @@ -518,7 +519,7 @@ describe('signals', () => { }; }); - test('schedules rerun when writing to signal before reading it', (runes) => { + test.skip('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; const error = console.error; @@ -1010,14 +1011,68 @@ describe('signals', () => { }; }); - test('effects do not depend on state they own', () => { + test('effects do depend on state they own', (runes) => { + // This behavior is important for use cases like a Resource class + // which shares its instance between multiple effects and triggers + // rerenders by self-invalidating its state. + const log: number[] = []; + + let count: any; + + if (runes) { + // We will make this the new default behavior once it's stable but until then + // we need to keep the old behavior to not break existing code. + enable_async_mode_flag(); + } + + effect(() => { + if (!count || $.get(count) < 2) { + count ||= state(0); + log.push($.get(count)); + set(count, $.get(count) + 1); + } + }); + + return () => { + try { + flushSync(); + if (runes) { + assert.deepEqual(log, [0, 1]); + } else { + assert.deepEqual(log, [0]); + } + } finally { + disable_async_mode_flag(); + } + }; + }); + + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + let raw: Source; + let proxied: { current: number }; + user_effect(() => { - const value = state(0); - set(value, $.get(value) + 1); + raw = state(0); + proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_effect(() => { + logs.push($.get(raw)); + }); + + user_effect(() => { + logs.push(proxied.current); + }); }); return () => { flushSync(); + set(raw, $.get(raw) + 1); + proxied.current += 1; + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); }; }); diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index a351851875ed..da6fdf44d881 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,11 +8,13 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = '0'; + p.textContent = ( + $.untrack(() => Math.max(0, Math.min(0, 100))) + ); var p_1 = $.sibling(p, 2); - p_1.textContent = location.href; + p_1.textContent = ($.untrack(() => location.href)); var node = $.sibling(p_1, 2); diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 0ae06e727f87..6954b8b683f6 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -35,7 +35,7 @@ export function suite(fn: (config: Test, test_dir: string export function suite_with_variants( variants: Variants[], - should_skip_variant: (variant: Variants, config: Test) => boolean | 'no-test', + should_skip_variant: (variant: Variants, config: Test, test_name: string) => boolean | 'no-test', common_setup: (config: Test, test_dir: string) => Promise | Common, fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void ) { @@ -46,11 +46,11 @@ export function suite_with_variants Date: Tue, 24 Jun 2025 09:16:05 -0400 Subject: [PATCH 396/582] make batch.#deferred private --- .../svelte/src/internal/client/reactivity/batch.js | 9 +++++++-- packages/svelte/src/internal/client/runtime.js | 10 ++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index b136dede07fa..13dc64026fa6 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,5 +1,6 @@ /** @import { Derived, Effect, Source } from '#client' */ import { CLEAN, DIRTY } from '#client/constants'; +import { deferred } from '../../shared/utils.js'; import { flush_queued_effects, flush_queued_root_effects, @@ -45,7 +46,7 @@ export class Batch { /** @type {{ promise: Promise, resolve: (value?: any) => void, reject: (reason: unknown) => void } | null} */ // TODO replace with Promise.withResolvers once supported widely enough - deferred = null; + #deferred = null; /** @type {Effect[]} */ async_effects = []; @@ -164,7 +165,7 @@ export class Batch { flush_queued_effects(render_effects); flush_queued_effects(effects); - this.deferred?.resolve(); + this.#deferred?.resolve(); } } else { for (const e of this.render_effects) set_signal_status(e, CLEAN); @@ -280,6 +281,10 @@ export class Batch { return false; } + settled() { + return (this.#deferred ??= deferred()).promise; + } + static ensure() { if (current_batch === null) { if (batches.size === 0) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3b6886467d27..8c1d706fa5a8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,12 +1,6 @@ /** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */ import { DEV } from 'esm-env'; -import { - deferred, - define_property, - get_descriptors, - get_prototype_of, - index_of -} from '../shared/utils.js'; +import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'; import { destroy_block_effect_children, destroy_effect_children, @@ -759,7 +753,7 @@ export async function tick() { * @returns {Promise} */ export function settled() { - return (Batch.ensure().deferred ??= deferred()).promise; + return Batch.ensure().settled(); } /** From ea0e2691388756bbc02869edc7636d4dfec83da4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Jun 2025 09:52:05 -0400 Subject: [PATCH 397/582] fix settled when awaits occur inside pending boundary --- .../src/internal/client/dom/blocks/boundary.js | 10 +++++++++- .../src/internal/client/reactivity/batch.js | 8 ++++++++ .../src/internal/client/reactivity/deriveds.js | 2 +- packages/svelte/src/internal/client/runtime.js | 5 ++++- .../samples/async-abort-signal/_config.js | 17 ++++++----------- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 4254c5d82def..e7141e06a8e9 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_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants'; +import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT, INERT } from '#client/constants'; import { component_context, set_component_context } from '../../context.js'; import { invoke_error_boundary } from '../../error-handling.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; @@ -151,6 +151,14 @@ export class Boundary { return !!this.#props.pending; } + is_pending() { + if (!this.ran && this.#props.pending) { + return true; + } + + return this.#pending_effect !== null && (this.#pending_effect.f & INERT) === 0; + } + /** * @param {() => Effect | null} fn */ diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 13dc64026fa6..d48d225a561f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -51,6 +51,9 @@ export class Batch { /** @type {Effect[]} */ async_effects = []; + /** @type {Effect[]} */ + boundary_async_effects = []; + /** @type {Effect[]} */ render_effects = []; @@ -188,7 +191,12 @@ export class Batch { update_effect(effect); } + for (const effect of this.boundary_async_effects) { + update_effect(effect); + } + this.async_effects = []; + this.boundary_async_effects = []; } /** diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index d6a73b8e36f7..c0f55c261443 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -132,7 +132,7 @@ export function async_derived(fn, location) { prev = promise; var batch = /** @type {Batch} */ (current_batch); - var ran = boundary.ran; + var ran = !boundary.is_pending(); if (should_suspend) { (ran ? batch : boundary).increment(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8c1d706fa5a8..d3303704bc7b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -654,8 +654,11 @@ export function process_effects(batch, root) { if (!skip && effect.fn !== null) { if ((flags & EFFECT_ASYNC) !== 0) { + const boundary = effect.b; + if (check_dirtiness(effect)) { - batch.async_effects.push(effect); + var effects = boundary?.is_pending() ? batch.boundary_async_effects : batch.async_effects; + effects.push(effect); } } else if ((flags & BLOCK_EFFECT) !== 0) { if (check_dirtiness(effect)) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js index 1405ee6e9f73..560a2397900e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { settled } from 'svelte'; import { test } from '../../test'; export default test({ @@ -9,22 +9,17 @@ export default test({ const [reset, resolve] = target.querySelectorAll('button'); - flushSync(() => reset.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await tick(); + reset.click(); + await settled(); assert.deepEqual(logs, ['aborted']); - flushSync(() => resolve.click()); + resolve.click(); + await Promise.resolve(); + await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); - await tick(); assert.htmlEqual( target.innerHTML, ` From 6b9f860b8e1287b16f628ff47a214b199c697a1c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Jun 2025 10:19:24 -0400 Subject: [PATCH 398/582] tweak --- .../internal/client/dom/blocks/boundary.js | 23 ++++++++----------- .../internal/client/reactivity/deriveds.js | 10 ++++---- .../svelte/src/internal/client/runtime.js | 2 +- 3 files changed, 15 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 e7141e06a8e9..a6dfc46057d0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -47,7 +47,7 @@ export function boundary(node, props, children) { export class Boundary { inert = false; - ran = false; + pending = false; /** @type {Boundary | null} */ parent; @@ -96,6 +96,8 @@ export class Boundary { this.parent = /** @type {Effect} */ (active_effect).b; + this.pending = !!this.#props.pending; + this.#effect = block(() => { /** @type {Effect} */ (active_effect).b = this; @@ -124,7 +126,8 @@ export class Boundary { pause_effect(/** @type {Effect} */ (this.#pending_effect), () => { this.#pending_effect = null; }); - this.ran = true; + + this.pending = false; } }); } else { @@ -137,7 +140,7 @@ export class Boundary { if (this.#pending_count > 0) { this.#show_pending_snippet(); } else { - this.ran = true; + this.pending = false; } } }, flags); @@ -151,14 +154,6 @@ export class Boundary { return !!this.#props.pending; } - is_pending() { - if (!this.ran && this.#props.pending) { - return true; - } - - return this.#pending_effect !== null && (this.#pending_effect.f & INERT) === 0; - } - /** * @param {() => Effect | null} fn */ @@ -201,7 +196,7 @@ export class Boundary { } commit() { - this.ran = true; + this.pending = false; if (this.#pending_effect) { pause_effect(this.#pending_effect, () => { @@ -244,7 +239,7 @@ export class Boundary { }); } - this.ran = false; + this.pending = true; this.#main_effect = this.#run(() => { this.#is_creating_fallback = false; @@ -254,7 +249,7 @@ export class Boundary { if (this.#pending_count > 0) { this.#show_pending_snippet(); } else { - this.ran = true; + this.pending = false; } }; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c0f55c261443..a98ea1cff828 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -132,10 +132,10 @@ export function async_derived(fn, location) { prev = promise; var batch = /** @type {Batch} */ (current_batch); - var ran = !boundary.is_pending(); + var pending = boundary.pending; if (should_suspend) { - (ran ? batch : boundary).increment(); + (pending ? boundary : batch).increment(); } /** @@ -148,10 +148,10 @@ export function async_derived(fn, location) { from_async_derived = null; if (should_suspend) { - (ran ? batch : boundary).decrement(); + (pending ? boundary : batch).decrement(); } - if (ran) batch.restore(); + if (!pending) batch.restore(); if (error) { if (error !== STALE_REACTION) { @@ -179,7 +179,7 @@ export function async_derived(fn, location) { } } - if (ran) batch.flush(); + if (!pending) batch.flush(); }; promise.then(handler, (e) => handler(null, e || 'unknown')); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d3303704bc7b..16b136de1a23 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -657,7 +657,7 @@ export function process_effects(batch, root) { const boundary = effect.b; if (check_dirtiness(effect)) { - var effects = boundary?.is_pending() ? batch.boundary_async_effects : batch.async_effects; + var effects = boundary?.pending ? batch.boundary_async_effects : batch.async_effects; effects.push(effect); } } else if ((flags & BLOCK_EFFECT) !== 0) { From 66635c5c344e9b049fd716664ec2fac1e667f314 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Jun 2025 15:11:44 -0400 Subject: [PATCH 399/582] change behaviour of `tick()` to be requestAnimationFrame-based --- packages/svelte/src/internal/client/runtime.js | 4 ++++ packages/svelte/src/reactivity/create-subscriber.js | 2 +- .../tests/runtime-runes/samples/async-attribute/_config.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 16b136de1a23..846a4e764800 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -744,6 +744,10 @@ export function flushSync(fn) { * @returns {Promise} */ export async function tick() { + if (async_mode_flag) { + return new Promise((f) => requestAnimationFrame(() => f())); + } + await Promise.resolve(); // By calling flushSync we guarantee that any pending state changes are applied after one tick. // TODO look into whether we can make flushing subsequent updates synchronously in the future. diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index 491ffb45cba7..892aa40dc48d 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -69,7 +69,7 @@ export function createSubscriber(start) { subscribers += 1; return () => { - tick().then(() => { + queueMicrotask(() => { // Only count down after timeout, else we would reach 0 before our own render effect reruns, // but reach 1 again when the tick callback of the prior teardown runs. That would mean we // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up. 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 f256e6a43c28..35ee7606e79e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -9,7 +9,7 @@ export default test({

      pending

      `, - async test({ assert, target, component }) { + async test({ assert, target }) { const [cool, neat, reset] = target.querySelectorAll('button'); flushSync(() => cool.click()); From 8d20a9af0941768908a462d9c1a511b2101b01d7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Jun 2025 15:32:43 -0400 Subject: [PATCH 400/582] get rid of a bunch of Promise.resolve chains --- .../svelte/tests/runtime-legacy/shared.ts | 6 ++++ .../samples/async-abort-signal/_config.js | 13 ++------- .../samples/async-attribute/_config.js | 13 +++------ .../samples/async-child-effect/_config.js | 29 ++++--------------- .../samples/async-derived-in-if/_config.js | 12 ++------ .../_config.js | 26 ++++------------- .../samples/async-derived/_config.js | 18 +++--------- 7 files changed, 29 insertions(+), 88 deletions(-) diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 7f3673f867bd..a129fb9b31f1 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -423,6 +423,12 @@ async function run_test_variant( try { if (config.test) { flushSync(); + + if (variant === 'hydrate') { + // wait for pending boundaries to render + await Promise.resolve(); + } + await config.test({ // @ts-expect-error TS doesn't get it assert: { diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js index 560a2397900e..b721f4dd62c0 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js @@ -1,12 +1,8 @@ -import { settled } from 'svelte'; +import { settled, tick } from 'svelte'; import { test } from '../../test'; export default test({ async test({ assert, target, logs, variant }) { - if (variant === 'hydrate') { - await Promise.resolve(); - } - const [reset, resolve] = target.querySelectorAll('button'); reset.click(); @@ -14,12 +10,7 @@ export default test({ assert.deepEqual(logs, ['aborted']); resolve.click(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, ` 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 35ee7606e79e..0a647384095c 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 { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { ok, test } from '../../test'; export default test({ @@ -12,22 +12,17 @@ export default test({ async test({ assert, target }) { const [cool, neat, reset] = target.querySelectorAll('button'); - flushSync(() => cool.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + cool.click(); await tick(); - flushSync(); const p = target.querySelector('p'); ok(p); assert.htmlEqual(p.outerHTML, '

      hello

      '); - flushSync(() => reset.click()); + reset.click(); assert.htmlEqual(p.outerHTML, '

      hello

      '); - flushSync(() => neat.click()); - await Promise.resolve(); + neat.click(); await tick(); assert.htmlEqual(p.outerHTML, '

      hello

      '); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js index 41d4130470d6..325cb1dcd644 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js @@ -1,5 +1,5 @@ import { flushSync, tick } from 'svelte'; -import { ok, test } from '../../test'; +import { test } from '../../test'; export default test({ html: ` @@ -7,20 +7,9 @@ export default test({

      loading

      `, - async test({ assert, target, variant }) { - if (variant === 'hydrate') { - await Promise.resolve(); - } - - flushSync(() => { - target.querySelector('button')?.click(); - }); - - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + async test({ assert, target }) { + target.querySelector('button')?.click(); await tick(); - flushSync(); const [button1, button2] = target.querySelectorAll('button'); @@ -37,12 +26,8 @@ export default test({ flushSync(() => button2.click()); flushSync(() => button2.click()); - flushSync(() => button1.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button1.click(); await tick(); - flushSync(); assert.htmlEqual( target.innerHTML, @@ -54,12 +39,8 @@ export default test({ ` ); - flushSync(() => button1.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button1.click(); await tick(); - flushSync(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js index ab020d85f749..914b311c97ce 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js @@ -1,18 +1,12 @@ -import { flushSync } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ async test({ assert, target }) { const button = target.querySelector('button'); - flushSync(() => button?.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - flushSync(); + button?.click(); + await tick(); assert.htmlEqual( target.innerHTML, 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 index df3fbe65cd34..fee8e2e6bfee 100644 --- 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 @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -9,29 +9,13 @@ export default test({

      pending

      `, - async test({ assert, target, component, errors, variant }) { - if (variant === 'hydrate') { - await Promise.resolve(); - } - + async test({ assert, target, errors }) { const [toggle, resolve1, resolve2] = target.querySelectorAll('button'); - flushSync(() => toggle.click()); - - flushSync(() => resolve1.click()); - await Promise.resolve(); - await Promise.resolve(); + toggle.click(); + resolve1.click(); + resolve2.click(); - flushSync(() => resolve2.click()); - 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 Promise.resolve(); await tick(); assert.htmlEqual( 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 d573cf624672..72396434642e 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 { flushSync, tick } from 'svelte'; +import { flushSync, settled, tick } from 'svelte'; import { ok, test } from '../../test'; export default test({ @@ -14,30 +14,20 @@ export default test({ const [resolve_a, resolve_b, reset, increment] = target.querySelectorAll('button'); flushSync(() => resolve_a.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - flushSync(); + await tick(); const p = target.querySelector('p'); ok(p); assert.htmlEqual(p.innerHTML, '1a'); flushSync(() => increment.click()); - await Promise.resolve(); - await Promise.resolve(); await tick(); assert.htmlEqual(p.innerHTML, '2a'); - flushSync(() => reset.click()); + reset.click(); assert.htmlEqual(p.innerHTML, '2a'); - flushSync(() => resolve_b.click()); - await Promise.resolve(); - await Promise.resolve(); + resolve_b.click(); await tick(); assert.htmlEqual(p.innerHTML, '2b'); From 2ae79f364d08c03020ceda0edc7fef88319beca0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Jun 2025 15:54:34 -0400 Subject: [PATCH 401/582] more --- .../samples/async-derived-module/_config.js | 9 ++-- .../async-derived-unchanging/_config.js | 14 ++---- .../samples/async-each-await-item/_config.js | 12 ++--- .../samples/async-each/_config.js | 5 +-- .../samples/async-error-recovery/_config.js | 45 ++++++------------- .../samples/async-error/_config.js | 17 +++---- .../samples/async-expression/_config.js | 9 ++-- .../samples/async-html-tag/_config.js | 5 +-- .../runtime-runes/samples/async-if/_config.js | 11 ++--- .../samples/async-key/_config.js | 5 +-- .../_config.js | 15 +++---- .../_config.js | 18 ++------ .../samples/async-prop/_config.js | 5 +-- .../samples/async-reactivity-loss/_config.js | 11 +---- 14 files changed, 50 insertions(+), 131 deletions(-) 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 30adf19581ac..b16ef652aee2 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 @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { flushSync, settled, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -19,6 +19,8 @@ export default test({ async test({ assert, target, component, logs }) { d.resolve(42); + + // TODO why is this necessary? why isn't `await tick()` enough? await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); @@ -32,9 +34,6 @@ export default test({ assert.htmlEqual(target.innerHTML, '

      42

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

      84

      '); @@ -44,8 +43,6 @@ export default test({ assert.htmlEqual(target.innerHTML, '

      84

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

      86

      '); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js index 423213696477..016c311f989a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js @@ -1,16 +1,11 @@ -import { flushSync } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { test } from '../../test'; export default test({ html: `

      pending...

      `, async test({ assert, target }) { - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, @@ -30,10 +25,7 @@ export default test({ for (let i = 1; i < 5; i += 1) { shift.click(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.equal(p.innerHTML, `${i}: ${Math.min(i, 3)}`); } 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 52df1275a9de..54aa68eeb294 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 @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -7,25 +7,21 @@ export default test({ async test({ assert, target }) { const [button1, button2, button3] = target.querySelectorAll('button'); - flushSync(() => button1.click()); - await Promise.resolve(); - await Promise.resolve(); + button1.click(); await tick(); - flushSync(); assert.htmlEqual( target.innerHTML, '

      a

      b

      c

      ' ); - flushSync(() => button2.click()); + button2.click(); await tick(); assert.htmlEqual( target.innerHTML, '

      a

      b

      c

      ' ); - flushSync(() => button3.click()); - await Promise.resolve(); + button3.click(); await tick(); assert.htmlEqual( target.innerHTML, 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 b28d310565f3..9dde2beb3926 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 { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -18,10 +18,7 @@ export default test({ async test({ assert, target, component }) { d.resolve(['a', 'b', 'c']); - 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-error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js index 91784f67472d..1613bf9c6124 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -13,12 +13,7 @@ export default test({ }, async test({ assert, target }) { - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, @@ -31,10 +26,8 @@ export default test({ let [button] = target.querySelectorAll('button'); let [p] = target.querySelectorAll('p'); - flushSync(() => button.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button.click(); + await tick(); assert.htmlEqual( target.innerHTML, ` @@ -43,10 +36,8 @@ export default test({ ` ); - flushSync(() => button.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button.click(); + await tick(); assert.htmlEqual( target.innerHTML, ` @@ -55,10 +46,8 @@ export default test({ ` ); - flushSync(() => button.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button.click(); + await tick(); assert.htmlEqual( target.innerHTML, ` @@ -69,15 +58,11 @@ export default test({ const [button1, button2] = target.querySelectorAll('button'); - flushSync(() => button1.click()); - await Promise.resolve(); + button1.click(); + await tick(); - flushSync(() => button2.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button2.click(); + await tick(); [p] = target.querySelectorAll('p'); @@ -89,10 +74,8 @@ export default test({ ` ); - flushSync(() => button1.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button1.click(); + await tick(); assert.htmlEqual( target.innerHTML, ` diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js index 61cfe4510453..dfbd238eeb67 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -7,29 +7,24 @@ export default test({ async test({ assert, target }) { let [button1, button2, button3] = target.querySelectorAll('button'); - flushSync(() => button1.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - flushSync(); + button1.click(); + await tick(); assert.htmlEqual( target.innerHTML, '

      oops!

      ' ); - flushSync(() => button2.click()); + button2.click(); const reset = /** @type {HTMLButtonElement} */ (target.querySelector('[data-id="reset"]')); - flushSync(() => reset.click()); + reset.click(); assert.htmlEqual( target.innerHTML, '

      pending

      ' ); - flushSync(() => button3.click()); - await Promise.resolve(); + button3.click(); await tick(); assert.htmlEqual( target.innerHTML, 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 c44d112625fa..d626569ba250 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -12,12 +12,9 @@ export default test({ async test({ assert, target, raf }) { const [reset, hello, goodbye] = target.querySelectorAll('button'); - flushSync(() => hello.click()); + hello.click(); raf.tick(0); - await Promise.resolve(); - await Promise.resolve(); await tick(); - flushSync(); assert.htmlEqual( target.innerHTML, ` @@ -28,7 +25,7 @@ export default test({ ` ); - flushSync(() => reset.click()); + reset.click(); raf.tick(0); await tick(); assert.htmlEqual( @@ -42,7 +39,7 @@ export default test({ ` ); - flushSync(() => goodbye.click()); + goodbye.click(); await Promise.resolve(); raf.tick(0); 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 6cded1a1d1ba..22b8b2a1c462 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 @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -18,10 +18,7 @@ export default test({ 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; 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 0bf9152dca01..a4bee8c9956f 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 { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -19,24 +19,21 @@ export default test({ async test({ assert, target }) { const [reset, t, f] = target.querySelectorAll('button'); - flushSync(() => t.click()); - await Promise.resolve(); - await Promise.resolve(); + t.click(); await tick(); - flushSync(); assert.htmlEqual( target.innerHTML, '

      yes

      ' ); - flushSync(() => reset.click()); + reset.click(); await tick(); assert.htmlEqual( target.innerHTML, '

      yes

      ' ); - flushSync(() => f.click()); + f.click(); await tick(); assert.htmlEqual( target.innerHTML, 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..bda922705464 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 { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -18,10 +18,7 @@ export default test({ async test({ assert, target, component }) { d.resolve(1); - 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-linear-order-different-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js index e4d6979acf57..cb8e0cfca90c 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js @@ -1,4 +1,4 @@ -import { flushSync, settled, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -7,10 +7,7 @@ export default test({ async test({ assert, target }) { const [both, a, b] = target.querySelectorAll('button'); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, @@ -21,12 +18,10 @@ export default test({ ` ); - flushSync(() => both.click()); - flushSync(() => b.click()); + both.click(); + b.click(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js index 76bfbe56d633..5e522ebdb536 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js @@ -5,12 +5,8 @@ export default test({ async test({ assert, target }) { const [a, b, reset1, reset2, resolve1, resolve2] = target.querySelectorAll('button'); - flushSync(() => resolve1.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + resolve1.click(); await tick(); - flushSync(); const p = /** @type {HTMLElement} */ (target.querySelector('#test')); @@ -21,21 +17,13 @@ export default test({ flushSync(() => reset2.click()); flushSync(() => b.click()); - flushSync(() => resolve2.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + resolve2.click(); await tick(); - flushSync(); assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); - flushSync(() => resolve1.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + resolve1.click(); await tick(); - flushSync(); assert.htmlEqual(p.innerHTML, '2 + 3 = 5'); } 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 570b22abd4c4..ef4c453b26ce 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 { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -18,10 +18,7 @@ export default test({ async test({ assert, target, component }) { d.resolve('hello'); - 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-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index 4ed40d015b49..5de300a74a8a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -9,16 +9,7 @@ export default test({ 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']); From 0be1f6aca0d20120b71016913bd681edb9fde136 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Jun 2025 16:01:10 -0400 Subject: [PATCH 402/582] more --- .../samples/async-redirect-initial/_config.js | 13 +++++-------- .../samples/async-redirect/_config.js | 16 ++++++---------- .../samples/async-render-tag/_config.js | 5 +---- .../samples/async-svelte-element/_config.js | 5 +---- .../samples/async-top-level/_config.js | 6 +----- .../samples/async-waterfall-on-init/_config.js | 14 +++----------- .../samples/async-with-sync-derived/_config.js | 12 +++--------- 7 files changed, 20 insertions(+), 51 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js index 1a0a855c125f..17bb79af086f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -17,9 +17,7 @@ export default test({ ` ); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, @@ -33,11 +31,10 @@ export default test({ ` ); - flushSync(() => ok.click()); + ok.click(); - flushSync(() => b.click()); - await Promise.resolve(); - await Promise.resolve(); + b.click(); + await tick(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js index c73fdbf268fb..ebbe642860d0 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js @@ -1,10 +1,8 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ async test({ assert, target }) { - await Promise.resolve(); - assert.htmlEqual( target.innerHTML, ` @@ -19,9 +17,8 @@ export default test({ const [a, b, c, ok] = target.querySelectorAll('button'); - flushSync(() => b.click()); - await Promise.resolve(); - await Promise.resolve(); + b.click(); + await tick(); assert.htmlEqual( target.innerHTML, @@ -35,11 +32,10 @@ export default test({ ` ); - flushSync(() => ok.click()); + ok.click(); - flushSync(() => b.click()); - await Promise.resolve(); - await Promise.resolve(); + b.click(); + await tick(); assert.htmlEqual( target.innerHTML, 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 6cded1a1d1ba..22b8b2a1c462 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 @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -18,10 +18,7 @@ export default test({ 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; 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 ea3b91b2a40b..558caa629231 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 @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -18,10 +18,7 @@ export default test({ 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; 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 b5931559460b..108ee7bef092 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 { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; @@ -18,11 +18,7 @@ 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

      '); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js index 91c388e0ca92..e2c8b851c1e5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js @@ -1,4 +1,4 @@ -import { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ @@ -12,12 +12,8 @@ export default test({ async test({ assert, target }) { const [button1, button2] = target.querySelectorAll('button'); - flushSync(() => button1.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button1.click(); await tick(); - flushSync(); assert.htmlEqual( target.innerHTML, @@ -29,12 +25,8 @@ export default test({ ` ); - flushSync(() => button2.click()); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + button2.click(); await tick(); - flushSync(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js index c09d448f9cd7..837dd976e2fb 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js @@ -1,14 +1,11 @@ -import { flushSync, settled, tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { test } from '../../test'; export default test({ html: `

      loading...

      `, async test({ assert, target }) { - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, @@ -39,10 +36,7 @@ export default test({ ` ); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); + await tick(); assert.htmlEqual( target.innerHTML, From e912f885bd8d27aeb0d1e8ffc2d72c83b66059c5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 14:54:09 -0400 Subject: [PATCH 403/582] fix test --- packages/svelte/tests/runtime-legacy/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 7744c2821bb6..25e89e7db8f3 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -427,7 +427,7 @@ async function run_test_variant( if (config.test) { flushSync(); - if (variant === 'hydrate') { + if (variant === 'hydrate' && cwd.includes('async-')) { // wait for pending boundaries to render await Promise.resolve(); } From 0430c76da34cd30d6fd2540f77d4dce753ee1747 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 15:14:57 -0400 Subject: [PATCH 404/582] disallow `flushSync()` inside effects --- .../docs/98-reference/.generated/client-errors.md | 10 ++++++++++ packages/svelte/messages/client-errors/errors.md | 8 ++++++++ packages/svelte/src/internal/client/errors.js | 15 +++++++++++++++ packages/svelte/src/internal/client/runtime.js | 4 ++++ packages/svelte/src/legacy/legacy-client.js | 6 ++++-- packages/svelte/tests/signals/test.ts | 11 ++++++----- 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index cd68ae704ba6..d894bbb27045 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -74,6 +74,16 @@ Effect cannot be created inside a `$derived` value that was not itself created i Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops ``` +### flush_sync_in_effect + +``` +Cannot use `flushSync` inside an effect +``` + +The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect. + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ### hydration_failed ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index f9e86dcd503d..8a632abe3c01 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -48,6 +48,14 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops +## flush_sync_in_effect + +> Cannot use `flushSync` inside an effect + +The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect. + +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ## hydration_failed > Failed to hydrate the application diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 063595884ee0..07e3e1974c18 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -334,4 +334,19 @@ export function state_unsafe_mutation() { } else { throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); } +} + +/** + * Cannot use `flushSync` inside an effect + * @returns {never} + */ +export function flush_sync_in_effect() { + if (DEV) { + const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/flush_sync_in_effect`); + } } \ No newline at end of file diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 846a4e764800..ae3cc36c34f6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -706,6 +706,10 @@ export function process_effects(batch, root) { * @returns {T} */ export function flushSync(fn) { + if (async_mode_flag && active_effect !== null) { + e.flush_sync_in_effect(); + } + var result; const batch = Batch.ensure(); diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 45c478ecab1e..4ff1e619d5cd 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -10,6 +10,7 @@ import * as w from '../internal/client/warnings.js'; import { DEV } from 'esm-env'; import { FILENAME } from '../constants.js'; import { component_context, dev_current_component_function } from '../internal/client/context.js'; +import { async_mode_flag } from '../internal/flags/index.js'; /** * Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component. @@ -119,8 +120,9 @@ class Svelte4Component { recover: options.recover }); - // We don't flushSync for custom element wrappers or if the user doesn't want it - if (!options?.props?.$$host || options.sync === false) { + // We don't flushSync for custom element wrappers or if the user doesn't want it, + // or if we're in async mode since `flushSync()` will fail + if (!async_mode_flag && (!options?.props?.$$host || options.sync === false)) { flushSync(); } diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 0e952db7eced..e21755705faa 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -6,7 +6,8 @@ import { effect, effect_root, render_effect, - user_effect + user_effect, + user_pre_effect } from '../../src/internal/client/reactivity/effects'; import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; @@ -1079,17 +1080,17 @@ describe('signals', () => { test('nested effects depend on state of upper effects', () => { const logs: number[] = []; - user_effect(() => { + user_pre_effect(() => { const raw = state(0); const proxied = proxy({ current: 0 }); // We need those separate, else one working and rerunning the effect // could mask the other one not rerunning - user_effect(() => { + user_pre_effect(() => { logs.push($.get(raw)); }); - user_effect(() => { + user_pre_effect(() => { logs.push(proxied.current); }); @@ -1097,7 +1098,7 @@ describe('signals', () => { // together with the reading effects flushSync(); - user_effect(() => { + user_pre_effect(() => { $.untrack(() => { set(raw, $.get(raw) + 1); proxied.current += 1; From b4007802645320a2f4cb9fb634799434294a2a2c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 15:21:39 -0400 Subject: [PATCH 405/582] regenerate --- packages/svelte/src/internal/client/errors.js | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 07e3e1974c18..9afbb67c8265 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -182,6 +182,21 @@ export function effect_update_depth_exceeded() { } } +/** + * Cannot use `flushSync` inside an effect + * @returns {never} + */ +export function flush_sync_in_effect() { + if (DEV) { + const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/flush_sync_in_effect`); + } +} + /** * Failed to hydrate the application * @returns {never} @@ -334,19 +349,4 @@ export function state_unsafe_mutation() { } else { throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); } -} - -/** - * Cannot use `flushSync` inside an effect - * @returns {never} - */ -export function flush_sync_in_effect() { - if (DEV) { - const error = new Error(`flush_sync_in_effect\nCannot use \`flushSync\` inside an effect\nhttps://svelte.dev/e/flush_sync_in_effect`); - - error.name = 'Svelte error'; - throw error; - } else { - throw new Error(`https://svelte.dev/e/flush_sync_in_effect`); - } } \ No newline at end of file From 951d8e6e69b5e8e4ecf585f5384053d1ef3b2b4d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 15:37:27 -0400 Subject: [PATCH 406/582] handle errors in block expressions --- .../svelte/src/internal/client/dom/blocks/async.js | 13 +++++++++---- .../async-block-reject-during-init/_config.js | 10 ++++++++++ .../async-block-reject-during-init/main.svelte | 8 ++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index c3828fdb2517..b11ad02789f1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,5 +1,6 @@ /** @import { Effect, TemplateNode, Value } from '#client' */ import { DESTROYED } from '#client/constants'; +import { invoke_error_boundary } from '../../error-handling.js'; import { async_derived } from '../../reactivity/deriveds.js'; import { active_effect } from '../../runtime.js'; import { capture, get_pending_boundary } from './boundary.js'; @@ -9,7 +10,7 @@ import { capture, get_pending_boundary } from './boundary.js'; * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export function async(node, expressions, fn) { +export async function async(node, expressions, fn) { // TODO handle hydration var parent = /** @type {Effect} */ (active_effect); @@ -19,12 +20,16 @@ export function async(node, expressions, fn) { boundary.increment(); - Promise.all(expressions.map((fn) => async_derived(fn))).then((result) => { + try { + const result = await Promise.all(expressions.map((fn) => async_derived(fn))); + if ((parent.f & DESTROYED) !== 0) return; restore(); fn(node, ...result); - + } catch (error) { + invoke_error_boundary(error, parent); + } finally { boundary.decrement(); - }); + } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js new file mode 100644 index 000000000000..e2718a35d27c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, 'loading'); + await tick(); + assert.htmlEqual(target.innerHTML, 'nope'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte new file mode 100644 index 000000000000..412da7268ed1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte @@ -0,0 +1,8 @@ + + {#if await Promise.reject(new Error('nope'))} + hi + {/if} + + {#snippet pending()}loading{/snippet} + {#snippet failed(e)}{e.message}{/snippet} + From fdd009b60d7efe478b499c3730220052a6ded7d1 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 25 Jun 2025 22:04:07 +0200 Subject: [PATCH 407/582] make validate_each_keys async-aware --- .../3-transform/client/visitors/EachBlock.js | 29 +++++++----- .../samples/async-each-keyed/_config.js | 44 +++++++++++++++++++ .../samples/async-each-keyed/main.svelte | 13 ++++++ 3 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte 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 f5758893b2d5..d61d9f6ede0f 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 @@ -312,14 +312,7 @@ export function EachBlock(node, context) { declarations.push(b.let(node.index, index)); } - if (dev && node.metadata.keyed) { - context.state.init.push( - b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) - ); - } - const { has_await } = node.metadata.expression; - const thunk = b.thunk(collection, has_await); const render_args = [b.id('$$anchor'), item]; @@ -342,20 +335,34 @@ export function EachBlock(node, context) { } if (has_await) { + const statements = [b.stmt(b.call('$.each', ...args))]; + if (dev && node.metadata.keyed) { + statements.unshift( + b.stmt( + b.call( + '$.validate_each_keys', + b.thunk(b.call('$.get', b.id('$$collection'))), + key_function + ) + ) + ); + } 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))]) - ) + b.arrow([context.state.node, b.id('$$collection')], b.block(statements)) ) ) ); } else { + if (dev && node.metadata.keyed) { + context.state.init.push( + b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) + ); + } context.state.init.push(b.stmt(b.call('$.each', ...args))); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js new file mode 100644 index 000000000000..7a9c0760bb98 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js @@ -0,0 +1,44 @@ +import { tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + compileOptions: { + dev: true + }, + html: `

      pending

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

      a

      b

      c

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

      a

      b

      c

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

      d

      e

      f

      g

      '); + + d = deferred(); + component.promise = d.promise; + d.resolve(['d', 'e', 'f', 'd']); + await tick(); + assert.fail('should not allow duplicate keys'); + }, + + runtime_error: 'each_key_duplicate' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte new file mode 100644 index 000000000000..07e4f17c53cc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte @@ -0,0 +1,13 @@ + + + + {#each await promise as item (item)} +

      {item}

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

      pending

      + {/snippet} +
      From 5c0a4a02a5f4429a2687d9d4c627a4638b332d88 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 16:51:40 -0400 Subject: [PATCH 408/582] for unowned deriveds, throw errors lazily --- .../svelte/src/internal/client/error-handling.js | 14 ++++++++++---- packages/svelte/src/internal/client/runtime.js | 6 +++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 594ef72d2532..b88f99041b33 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -1,17 +1,23 @@ -/** @import { Effect } from '#client' */ +/** @import { Derived, Effect } from '#client' */ /** @import { Boundary } from './dom/blocks/boundary.js' */ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; -import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; +import { ASYNC_ERROR, BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; -import { active_effect } from './runtime.js'; +import { active_effect, active_reaction } from './runtime.js'; /** * @param {unknown} error */ export function handle_error(error) { - var effect = /** @type {Effect} */ (active_effect); + var effect = active_effect; + + // for unowned deriveds, don't throw until we read the value + if (effect === null) { + /** @type {Derived} */ (active_reaction).f |= ASYNC_ERROR; + return error; + } if (DEV && error instanceof Error) { // adjust_error(error, effect); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index ae3cc36c34f6..b1a59de3d6d7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -369,9 +369,13 @@ export function update_reaction(reaction) { } } + if ((reaction.f & ASYNC_ERROR) !== 0) { + reaction.f ^= ASYNC_ERROR; + } + return result; } catch (error) { - handle_error(error); + return handle_error(error); } finally { reaction.f ^= REACTION_IS_UPDATING; new_deps = previous_deps; From a0dba347707110442423ab10223900f0aa6f315b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 16:54:47 -0400 Subject: [PATCH 409/582] rename ASYNC_ERROR -> ERROR_VALUE, and avoid conflicts with other flags now that it's used with deriveds as well as sources --- packages/svelte/src/internal/client/constants.js | 2 +- packages/svelte/src/internal/client/error-handling.js | 4 ++-- .../svelte/src/internal/client/reactivity/deriveds.js | 8 ++++---- packages/svelte/src/internal/client/runtime.js | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 0ee27f4c70e5..34d0bf987ff9 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -27,7 +27,7 @@ export const EFFECT_PRESERVED = 1 << 23; // effects with this flag should not be export const REACTION_IS_UPDATING = 1 << 24; export const EFFECT_ASYNC = 1 << 25; -export const ASYNC_ERROR = 1; +export const ERROR_VALUE = 1 << 26; export const STATE_SYMBOL = Symbol('$state'); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index b88f99041b33..b1df1a50b7f3 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -3,7 +3,7 @@ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; -import { ASYNC_ERROR, BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; +import { ERROR_VALUE, BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect, active_reaction } from './runtime.js'; @@ -15,7 +15,7 @@ export function handle_error(error) { // for unowned deriveds, don't throw until we read the value if (effect === null) { - /** @type {Derived} */ (active_reaction).f |= ASYNC_ERROR; + /** @type {Derived} */ (active_reaction).f |= ERROR_VALUE; return error; } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index a98ea1cff828..6a7a6bd24031 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -2,7 +2,7 @@ /** @import { Batch } from './batch.js'; */ import { DEV } from 'esm-env'; import { - ASYNC_ERROR, + ERROR_VALUE, CLEAN, DERIVED, DESTROYED, @@ -155,14 +155,14 @@ export function async_derived(fn, location) { if (error) { if (error !== STALE_REACTION) { - signal.f |= ASYNC_ERROR; + signal.f |= ERROR_VALUE; // @ts-expect-error the error is the wrong type, but we don't care internal_set(signal, error); } } else { - if ((signal.f & ASYNC_ERROR) !== 0) { - signal.f ^= ASYNC_ERROR; + if ((signal.f & ERROR_VALUE) !== 0) { + signal.f ^= ERROR_VALUE; } internal_set(signal, value); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b1a59de3d6d7..894e2e25a117 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { EFFECT_ASYNC, RENDER_EFFECT, STALE_REACTION, - ASYNC_ERROR + ERROR_VALUE } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -369,8 +369,8 @@ export function update_reaction(reaction) { } } - if ((reaction.f & ASYNC_ERROR) !== 0) { - reaction.f ^= ASYNC_ERROR; + if ((reaction.f & ERROR_VALUE) !== 0) { + reaction.f ^= ERROR_VALUE; } return result; @@ -921,7 +921,7 @@ export function get(signal) { return batch_deriveds.get(derived); } - if ((signal.f & ASYNC_ERROR) !== 0) { + if ((signal.f & ERROR_VALUE) !== 0) { throw signal.v; } From da5b74a1804f21a5f204abb65ae866704ac481be Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 16:56:16 -0400 Subject: [PATCH 410/582] invoke boundary directly --- packages/svelte/src/internal/client/dom/blocks/async.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index b11ad02789f1..4e9f7c2b93b6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,6 +1,5 @@ /** @import { Effect, TemplateNode, Value } from '#client' */ import { DESTROYED } from '#client/constants'; -import { invoke_error_boundary } from '../../error-handling.js'; import { async_derived } from '../../reactivity/deriveds.js'; import { active_effect } from '../../runtime.js'; import { capture, get_pending_boundary } from './boundary.js'; @@ -28,7 +27,7 @@ export async function async(node, expressions, fn) { restore(); fn(node, ...result); } catch (error) { - invoke_error_boundary(error, parent); + boundary.error(error); } finally { boundary.decrement(); } From c80d165032f976879746db7e5dbbdccb7987e7fd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 19:29:22 -0400 Subject: [PATCH 411/582] local effect pending --- .../client/visitors/CallExpression.js | 2 +- .../src/internal/client/dom/blocks/async.js | 4 +- .../internal/client/dom/blocks/boundary.js | 49 ++++++++++++++----- packages/svelte/src/internal/client/index.js | 12 +---- .../src/internal/client/reactivity/batch.js | 13 ----- .../internal/client/reactivity/deriveds.js | 10 ++-- .../src/internal/client/reactivity/sources.js | 3 -- 7 files changed, 49 insertions(+), 44 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 532af08fd12c..af3553861c09 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -64,7 +64,7 @@ export function CallExpression(node, context) { ); case '$effect.pending': - return b.call('$.get', b.id('$.pending')); + return b.call(b.id('$.pending')); case '$inspect': case '$inspect().with': diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 4e9f7c2b93b6..339c05ded7b9 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -17,7 +17,7 @@ export async function async(node, expressions, fn) { var restore = capture(); var boundary = get_pending_boundary(); - boundary.increment(); + boundary.update_pending_count(1); try { const result = await Promise.all(expressions.map((fn) => async_derived(fn))); @@ -29,6 +29,6 @@ export async function async(node, expressions, fn) { } catch (error) { boundary.error(error); } finally { - boundary.decrement(); + boundary.update_pending_count(-1); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index a6dfc46057d0..8cf54490f47b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -7,6 +7,7 @@ import { block, branch, destroy_effect, pause_effect } from '../../reactivity/ef import { active_effect, active_reaction, + get, set_active_effect, set_active_reaction } from '../../runtime.js'; @@ -24,6 +25,8 @@ 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 { Batch } from '../../reactivity/batch.js'; +import { source, update } from '../../reactivity/sources.js'; +import { tag } from '../../dev/tracing.js'; /** * @typedef {{ @@ -82,6 +85,8 @@ export class Boundary { #pending_count = 0; #is_creating_fallback = false; + effect_pending = source(0); + /** * @param {TemplateNode} node * @param {BoundaryProps} props @@ -98,6 +103,10 @@ export class Boundary { this.pending = !!this.#props.pending; + if (DEV) { + tag(this.effect_pending, '$effect.pending()'); + } + this.#effect = block(() => { /** @type {Effect} */ (active_effect).b = this; @@ -210,19 +219,26 @@ export class Boundary { } } - increment() { - this.#pending_count++; - } + /** @param {1 | -1} d */ + #update_pending_count(d) { + this.#pending_count += d; - decrement() { - if (--this.#pending_count === 0) { + if (this.#pending_count === 0) { this.commit(); + } + } - if (this.#main_effect !== null) { - // TODO do we also need to `resume_effect` here? - // schedule_effect(this.#main_effect); - } + /** @param {1 | -1} d */ + update_pending_count(d) { + if (this.has_pending_snippet()) { + this.#update_pending_count(d); + } else if (this.parent) { + this.parent.#update_pending_count(d); } + + queueMicrotask(() => { + update(this.effect_pending, d); + }); } /** @param {unknown} error */ @@ -373,10 +389,10 @@ export function capture(track = true) { export function suspend() { let boundary = get_pending_boundary(); - boundary.increment(); + boundary.update_pending_count(-1); return function unsuspend() { - boundary.decrement(); + boundary.update_pending_count(-1); }; } @@ -401,3 +417,14 @@ function exit() { set_active_reaction(null); set_component_context(null); } + +export function pending() { + // TODO throw helpful error if called outside an effect + const boundary = /** @type {Effect} */ (active_effect).b; + + if (boundary === null) { + return 0; // TODO eventually we will need this to be global + } + + return get(boundary.effect_pending); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c300f00b3d43..8f3b86536d67 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -115,15 +115,7 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { - mutable_source, - mutate, - pending, - set, - state, - update, - update_pre -} from './reactivity/sources.js'; +export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js'; export { prop, rest_props, @@ -143,7 +135,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, save, suspend } from './dom/blocks/boundary.js'; +export { boundary, pending, save, suspend } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d48d225a561f..02ae6f8e27ba 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,8 +10,6 @@ import { set_signal_status, update_effect } from '../runtime.js'; -import { raf } from '../timing.js'; -import { internal_set, pending } from './sources.js'; /** @type {Set} */ const batches = new Set(); @@ -19,11 +17,6 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; -/** Update `$effect.pending()` */ -function update_pending() { - internal_set(pending, batches.size > 0); -} - /** @type {Map | null} */ export let batch_deriveds = null; @@ -239,8 +232,6 @@ export class Batch { } this.#callbacks.clear(); - - raf.tick(update_pending); } increment() { @@ -295,10 +286,6 @@ export class Batch { static ensure() { if (current_batch === null) { - if (batches.size === 0) { - raf.tick(update_pending); - } - const batch = (current_batch = new Batch()); batches.add(current_batch); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6a7a6bd24031..92fd7b18e6f5 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -31,7 +31,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 { get_pending_boundary } from '../dom/blocks/boundary.js'; +import { Boundary, get_pending_boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; @@ -105,7 +105,7 @@ export function async_derived(fn, location) { throw new Error('TODO cannot create unowned async derived'); } - let boundary = get_pending_boundary(); + var boundary = /** @type {Boundary} */ (parent.b); var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); @@ -135,7 +135,8 @@ export function async_derived(fn, location) { var pending = boundary.pending; if (should_suspend) { - (pending ? boundary : batch).increment(); + boundary.update_pending_count(1); + if (!pending) batch.increment(); } /** @@ -148,7 +149,8 @@ export function async_derived(fn, location) { from_async_derived = null; if (should_suspend) { - (pending ? boundary : batch).decrement(); + boundary.update_pending_count(-1); + if (!pending) batch.decrement(); } if (!pending) batch.restore(); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2300baed91c3..48c8ecf575bf 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -43,9 +43,6 @@ export let inspect_effects = new Set(); /** @type {Map} */ export const old_values = new Map(); -/** Internal representation of `$effect.pending()` */ -export let pending = source(false); - /** * @param {Set} v */ From ee21d9f3b8dc372b40fb4b71bbf0e26154beff50 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 19:32:41 -0400 Subject: [PATCH 412/582] update test --- .../samples/async-top-level/_config.js | 20 +++++-------------- .../samples/async-top-level/main.svelte | 6 ++++-- 2 files changed, 9 insertions(+), 17 deletions(-) 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 108ee7bef092..b2200201c611 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,24 +1,14 @@ 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 - }; - }, + html: `

      pending

      `, async test({ assert, target }) { - d.resolve('hello'); + const [hello] = target.querySelectorAll('button'); + + hello.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

      hello

      '); + 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 index 718a256b8676..78ad3ba04a18 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte @@ -1,11 +1,13 @@ + + - + {#snippet pending()}

      pending

      From 8cd5635c88d24222d009e555d1047ad5e13f67f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 19:42:42 -0400 Subject: [PATCH 413/582] fix --- 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 8cf54490f47b..e24d76800ef3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -389,7 +389,7 @@ export function capture(track = true) { export function suspend() { let boundary = get_pending_boundary(); - boundary.update_pending_count(-1); + boundary.update_pending_count(1); return function unsuspend() { boundary.update_pending_count(-1); From 8300327408b237d1c1d44be9090af826a311e07a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 19:42:48 -0400 Subject: [PATCH 414/582] fix --- playgrounds/sandbox/run.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index b24f70c8b51c..639b75502044 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -97,7 +97,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { filename: input, generate, runes: argv.values.runes, - fragments: 'tree' + fragments: 'tree', + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}.tree.js`; From c04a13b8d300293147d788e4a1e64931c67f687d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 25 Jun 2025 22:26:46 -0400 Subject: [PATCH 415/582] fix weird bug in tests --- .../svelte/src/internal/client/reactivity/batch.js | 10 ++++++++++ packages/svelte/tests/runtime-legacy/shared.ts | 3 +++ 2 files changed, 13 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 02ae6f8e27ba..e51fab23f98e 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -302,3 +302,13 @@ export class Batch { return current_batch; } } + +/** + * Forcibly remove all current batches + * TODO investigate why we need this in tests + */ +export function clear() { + for (const batch of batches) { + batch.remove(); + } +} diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 25e89e7db8f3..4ccd602afc12 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -11,6 +11,7 @@ import { assert_html_equal, assert_html_equal_with_options } from '../html_equal import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; import { suite_with_variants, type BaseTest } from '../suite.js'; +import { clear } from '../../src/internal/client/reactivity/batch.js'; type Assert = typeof import('vitest').assert & { htmlEqual(a: string, b: string, description?: string): void; @@ -521,6 +522,8 @@ async function run_test_variant( console.log = console_log; console.warn = console_warn; console.error = console_error; + + clear(); } } From 5c1ff997b47921aa7c222e9c4d8d4db1ce673ba9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 07:51:12 -0400 Subject: [PATCH 416/582] delete old changeset that somehow got left over here --- .changeset/fair-laws-appear.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/fair-laws-appear.md diff --git a/.changeset/fair-laws-appear.md b/.changeset/fair-laws-appear.md deleted file mode 100644 index 9a1149ff279d..000000000000 --- a/.changeset/fair-laws-appear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: match class and style directives against attribute selector From 042598cf331949192d9853e1112386da5f41f7cb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 08:42:13 -0400 Subject: [PATCH 417/582] Update .changeset/eleven-weeks-dance.md --- .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 eec83c3c2c52..91245df0eb6c 100644 --- a/.changeset/eleven-weeks-dance.md +++ b/.changeset/eleven-weeks-dance.md @@ -2,4 +2,4 @@ 'svelte': minor --- -feat: support `await` in components +feat: support `await` in components when using the `experimental.async` compiler option From 758c39dc8749be54d432e1b218ea1388e81d984f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 08:43:26 -0400 Subject: [PATCH 418/582] update error details --- documentation/docs/98-reference/.generated/client-errors.md | 2 ++ packages/svelte/messages/client-errors/errors.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index d894bbb27045..d6a1ed4aafae 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -126,6 +126,8 @@ The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression ``` +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ### state_descriptors_fixed ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 8a632abe3c01..a745022831d2 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -84,6 +84,8 @@ This restriction only applies when using the `experimental.async` option, which > `setContext` must be called when a component first initializes, not in a subsequent effect or after an `await` expression +This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. + ## state_descriptors_fixed > Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. From 163009dd1b7f8ee6a9026b6e8a4776cf885e34ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 09:06:40 -0400 Subject: [PATCH 419/582] unused --- .../compiler/phases/3-transform/client/transform-client.js | 3 +-- .../svelte/src/compiler/phases/3-transform/client/types.d.ts | 4 ---- .../compiler/phases/3-transform/client/visitors/Fragment.js | 3 +-- 3 files changed, 2 insertions(+), 8 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 0f2b0e2f3311..fcb9b4748453 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 @@ -157,8 +157,7 @@ export function client_component(analysis, options) { legacy_reactive_statements: new Map(), metadata: { namespace: options.namespace, - bound_contenteditable: false, - async: [] + bound_contenteditable: 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 802606bd46bb..9ca16e65494d 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 @@ -58,10 +58,6 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly metadata: { namespace: Namespace; bound_contenteditable: 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 86f430327b9a..122cae450102 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 @@ -69,8 +69,7 @@ export function Fragment(node, context) { transform: { ...context.state.transform }, metadata: { namespace, - bound_contenteditable: context.state.metadata.bound_contenteditable, - async: [] + bound_contenteditable: context.state.metadata.bound_contenteditable } }; From 2961124ba688efc09f319c3f2fabea43c8067d31 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 09:07:54 -0400 Subject: [PATCH 420/582] simplify --- .../phases/3-transform/client/visitors/CallExpression.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index af3553861c09..7b0f4d4bc19f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -64,7 +64,7 @@ export function CallExpression(node, context) { ); case '$effect.pending': - return b.call(b.id('$.pending')); + return b.call('$.pending'); case '$inspect': case '$inspect().with': From f388568c123158c6cec1efd2909614ddc8696bda Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 09:19:31 -0400 Subject: [PATCH 421/582] tweak --- .../phases/3-transform/client/visitors/IfBlock.js | 15 ++++----------- packages/svelte/src/compiler/utils/builders.js | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) 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 4bd0e1893244..8e7c8c27994e 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 @@ -21,8 +21,8 @@ export function IfBlock(node, context) { if (node.alternate) { const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); - alternate_id = context.state.scope.generate('alternate'); - statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate))); + alternate_id = b.id(context.state.scope.generate('alternate')); + statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate))); } const { has_await } = node.metadata.expression; @@ -38,15 +38,8 @@ export function IfBlock(node, context) { b.if( test, b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), - alternate_id - ? b.stmt( - b.call( - b.id('$$render'), - b.id(alternate_id), - node.alternate ? b.literal(false) : undefined - ) - ) - : undefined + alternate_id && + b.stmt(b.call('$$render', alternate_id, node.alternate && b.literal(false))) ) ]) ) diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 61c0f0d24b19..931b11e2ba64 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -101,7 +101,7 @@ export function labeled(name, body) { /** * @param {string | ESTree.Expression} callee - * @param {...(ESTree.Expression | ESTree.SpreadElement | false | undefined)} args + * @param {...(ESTree.Expression | ESTree.SpreadElement | false | undefined | null)} args * @returns {ESTree.CallExpression} */ export function call(callee, ...args) { From f34f28e54606bf448eb03d4082fa4c7234d2ef41 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 09:22:06 -0400 Subject: [PATCH 422/582] tweak --- .../phases/3-transform/client/visitors/IfBlock.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 8e7c8c27994e..f31369a5551b 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 @@ -13,9 +13,9 @@ export function IfBlock(node, context) { const statements = []; const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); - const consequent_id = context.state.scope.generate('consequent'); + const consequent_id = b.id(context.state.scope.generate('consequent')); - statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent))); + statements.push(b.var(consequent_id, b.arrow([b.id('$$anchor')], consequent))); let alternate_id; @@ -37,9 +37,8 @@ export function IfBlock(node, context) { b.block([ b.if( test, - b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), - alternate_id && - b.stmt(b.call('$$render', alternate_id, node.alternate && b.literal(false))) + b.stmt(b.call('$$render', consequent_id)), + alternate_id && b.stmt(b.call('$$render', alternate_id, b.literal(false))) ) ]) ) From 1e2e57ff51179932bd2c4ff6e94746ecaf9d27a2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 09:33:09 -0400 Subject: [PATCH 423/582] tweak --- .../3-transform/client/visitors/KeyBlock.js | 41 +++++++------------ .../src/internal/client/dom/blocks/key.js | 2 +- packages/svelte/src/internal/client/index.js | 2 +- 3 files changed, 16 insertions(+), 29 deletions(-) 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 c5b1d9def3a3..f211f64d7c36 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 @@ -11,35 +11,22 @@ import { build_expression } from './shared/utils.js'; export function KeyBlock(node, context) { context.state.template.push_comment(); - const key = build_expression(context, node.expression, node.metadata.expression); + const { has_await } = node.metadata.expression; + + const expression = build_expression(context, node.expression, node.metadata.expression); + const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression); const body = /** @type {Expression} */ (context.visit(node.fragment)); - if (node.metadata.expression.has_await) { - 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))) + let call = b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)); + + if (has_await) { + call = b.call( + '$.async', + context.state.node, + b.array([b.thunk(expression, true)]), + b.arrow([context.state.node, b.id('$$key')], b.block([b.stmt(call)])) ); } + + context.state.init.push(b.stmt(call)); } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 0023764e1bd9..5e3c42019f33 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -15,7 +15,7 @@ import { current_batch } from '../../reactivity/batch.js'; * @param {(anchor: Node) => TemplateNode | void} render_fn * @returns {void} */ -export function key_block(node, get_key, render_fn) { +export function key(node, get_key, render_fn) { if (hydrating) { hydrate_next(); } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 8f3b86536d67..a8c04e9d4c4d 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -13,7 +13,7 @@ export { async } from './dom/blocks/async.js'; export { validate_snippet_args } from './dev/validation.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'; +export { key } from './dom/blocks/key.js'; export { css_props } from './dom/blocks/css-props.js'; export { index, each } from './dom/blocks/each.js'; export { html } from './dom/blocks/html.js'; From a01249ee21b2489bd6f307c2b7aa8c0115f1f88f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 09:34:27 -0400 Subject: [PATCH 424/582] tweak --- .../phases/3-transform/client/visitors/RegularElement.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 81f7229703ed..49d4f278cc63 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 @@ -475,19 +475,19 @@ export function build_class_directives_object( ) { let properties = []; let has_call_or_state = false; - let has_async = false; + let has_await = false; for (const d of class_directives) { const expression = /** @type Expression */ (context.visit(d.expression)); properties.push(b.init(d.name, expression)); has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_async ||= d.metadata.expression.has_await; + has_await ||= d.metadata.expression.has_await; } const directives = b.object(properties); - return has_call_or_state || has_async - ? get_expression_id(has_async ? async_expressions : expressions, directives) + return has_call_or_state || has_await + ? get_expression_id(has_await ? async_expressions : expressions, directives) : directives; } From 2c2557aaa82934754a9ca97f510449f351a67a53 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 09:45:12 -0400 Subject: [PATCH 425/582] tidy up --- .../3-transform/client/visitors/RenderTag.js | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) 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 e741634c8986..b7187173e255 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 @@ -13,10 +13,7 @@ import { get_expression_id, build_expression } from './shared/utils.js'; export function RenderTag(node, context) { context.state.template.push_comment(); - const expression = unwrap_optional(node.expression); - - const callee = expression.callee; - const raw_args = expression.arguments; + const call = unwrap_optional(node.expression); /** @type {Expression[]} */ let args = []; @@ -27,18 +24,16 @@ export function RenderTag(node, context) { /** @type {MemoizedExpression[]} */ const async_expressions = []; - for (let i = 0; i < raw_args.length; i++) { - let expression = build_expression( - context, - /** @type {Expression} */ (raw_args[i]), - node.metadata.arguments[i] - ); - const { has_call, has_await } = node.metadata.arguments[i]; + for (let i = 0; i < call.arguments.length; i++) { + const arg = /** @type {Expression} */ (call.arguments[i]); + const metadata = node.metadata.arguments[i]; + + let expression = build_expression(context, arg, metadata); - if (has_await || has_call) { + if (metadata.has_await || metadata.has_call) { expression = b.call( '$.get', - get_expression_id(has_await ? async_expressions : expressions, expression) + get_expression_id(metadata.has_await ? async_expressions : expressions, expression) ); } @@ -50,13 +45,13 @@ export function RenderTag(node, context) { }); /** @type {Statement[]} */ - const statements = expressions.map((memo, i) => + const statements = expressions.map((memo) => b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) ); let snippet_function = build_expression( context, - /** @type {Expression} */ (callee), + /** @type {Expression} */ (call.callee), node.metadata.expression ); From 2fd762863acafc314656334a26d420aec267f89a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 11:32:04 -0400 Subject: [PATCH 426/582] handle errors in async block expressions --- .../svelte/src/internal/client/dom/blocks/async.js | 9 ++++++--- .../async-error-in-block-expression/_config.js | 11 +++++++++++ .../async-error-in-block-expression/main.svelte | 8 ++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 339c05ded7b9..a68736281d88 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,7 +1,7 @@ /** @import { Effect, TemplateNode, Value } from '#client' */ import { DESTROYED } from '#client/constants'; import { async_derived } from '../../reactivity/deriveds.js'; -import { active_effect } from '../../runtime.js'; +import { active_effect, get } from '../../runtime.js'; import { capture, get_pending_boundary } from './boundary.js'; /** @@ -20,12 +20,15 @@ export async function async(node, expressions, fn) { boundary.update_pending_count(1); try { - const result = await Promise.all(expressions.map((fn) => async_derived(fn))); + const deriveds = await Promise.all(expressions.map((fn) => async_derived(fn))); + + // get deriveds eagerly to avoid creating blocks if they reject + for (const d of deriveds) get(d); if ((parent.f & DESTROYED) !== 0) return; restore(); - fn(node, ...result); + fn(node, ...deriveds); } catch (error) { boundary.error(error); } finally { diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js new file mode 100644 index 000000000000..2679785cff3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'oops'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte new file mode 100644 index 000000000000..a49a5c9540d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte @@ -0,0 +1,8 @@ + + {#each (await Promise.reject(new Error('oops'))) as x} + hi + {/each} + + {#snippet pending()}loading{/snippet} + {#snippet failed()}oops{/snippet} + From 5694c0692ec2aa1d33d8e2ae34ba5d1028ef737a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 11:50:36 -0400 Subject: [PATCH 427/582] tweak --- .../client/visitors/RegularElement.js | 16 ++++++++-------- .../client/visitors/shared/element.js | 18 ++++-------------- 2 files changed, 12 insertions(+), 22 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 49d4f278cc63..1c4a75fb160c 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 @@ -462,16 +462,16 @@ function setup_select_synchronization(value_binding, context) { /** * @param {AST.ClassDirective[]} class_directives + * @param {ComponentContext} context * @param {MemoizedExpression[]} async_expressions * @param {MemoizedExpression[]} expressions - * @param {ComponentContext} context * @return {ObjectExpression | Identifier} */ export function build_class_directives_object( class_directives, - async_expressions, - expressions, - context + context, + async_expressions = context.state.async_expressions, + expressions = context.state.expressions ) { let properties = []; let has_call_or_state = false; @@ -493,16 +493,16 @@ export function build_class_directives_object( /** * @param {AST.StyleDirective[]} style_directives + * @param {ComponentContext} context * @param {MemoizedExpression[]} async_expressions * @param {MemoizedExpression[]} expressions - * @param {ComponentContext} context * @return {ObjectExpression | ArrayExpression}} */ export function build_style_directives_object( style_directives, - async_expressions, - expressions, - context + context, + async_expressions = context.state.async_expressions, + expressions = context.state.expressions ) { let normal_properties = []; let important_properties = []; 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 30f11e3ff62b..869dd169561f 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 @@ -72,7 +72,7 @@ export function build_attribute_effect( b.prop( 'init', b.array([b.id('$.CLASS')]), - build_class_directives_object(class_directives, async_expressions, expressions, context) + build_class_directives_object(class_directives, context, async_expressions, expressions) ) ); } @@ -82,7 +82,7 @@ export function build_attribute_effect( b.prop( 'init', b.array([b.id('$.STYLE')]), - build_style_directives_object(style_directives, async_expressions, expressions, context) + build_style_directives_object(style_directives, context, async_expressions, expressions) ) ); } @@ -180,12 +180,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c let next; if (class_directives.length) { - next = build_class_directives_object( - class_directives, - context.state.async_expressions, - context.state.expressions, - context - ); + next = build_class_directives_object(class_directives, context); has_state ||= class_directives.some((d) => d.metadata.expression.has_state); if (has_state) { @@ -258,12 +253,7 @@ export function build_set_style(node_id, attribute, style_directives, context) { let next; if (style_directives.length) { - next = build_style_directives_object( - style_directives, - context.state.async_expressions, - context.state.expressions, - context - ); + next = build_style_directives_object(style_directives, context); has_state ||= style_directives.some((d) => d.metadata.expression.has_state); if (has_state) { From 591aeb0cbc18ffb383faee8766a9c274a0b84ee1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 12:14:55 -0400 Subject: [PATCH 428/582] groundwork for async attribute_effect --- .../client/visitors/shared/element.js | 3 ++- .../client/dom/elements/attributes.js | 27 ++++++++++++++++--- .../src/internal/client/reactivity/effects.js | 6 ++--- 3 files changed, 28 insertions(+), 8 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 869dd169561f..b1b1a8fae4f6 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 @@ -96,8 +96,9 @@ export function build_attribute_effect( expressions.map(({ id }) => id), b.object(values) ), - // TODO need to handle async expressions too expressions.length > 0 && b.array(expressions.map(({ expression }) => b.thunk(expression))), + async_expressions.length > 0 && + b.array(async_expressions.map(({ expression }) => b.thunk(expression))), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 2d3d6a921dc1..83035cc6a180 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -1,4 +1,4 @@ -/** @import { Effect } from '#client' */ +/** @import { Effect, Value } from '#client' */ import { DEV } from 'esm-env'; import { hydrating, set_hydrating } from '../hydration.js'; import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; @@ -462,20 +462,39 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal /** * @param {Element & ElementCSSInlineStyle} element * @param {(...expressions: any) => Record} fn - * @param {Array<() => any>} thunks + * @param {Array<() => any>} sync + * @param {Array<() => Promise>} async * @param {string} [css_hash] * @param {boolean} [skip_warning] */ export function attribute_effect( element, fn, - thunks = [], + sync = [], + async = [], css_hash, skip_warning = false, d = derived ) { - const deriveds = thunks.map(d); + const deriveds = sync.map(d); + create_attribute_effect(element, fn, deriveds, css_hash, skip_warning); +} + +/** + * @param {Element & ElementCSSInlineStyle} element + * @param {(...expressions: any) => Record} fn + * @param {Value[]} deriveds + * @param {string} [css_hash] + * @param {boolean} [skip_warning] + */ +export function create_attribute_effect( + element, + fn, + deriveds = [], + css_hash, + skip_warning = false +) { /** @type {Record | undefined} */ var prev = undefined; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 15f16a069134..1bef2174769a 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -341,10 +341,10 @@ export function render_effect(fn, flags = 0) { * @param {(fn: () => T) => Derived} d */ export function template_effect(fn, sync = [], async = [], d = derived) { - var batch = current_batch; - var parent = /** @type {Effect} */ (active_effect); - if (async.length > 0) { + var batch = current_batch; + var parent = /** @type {Effect} */ (active_effect); + var restore = capture(); Promise.all(async.map((expression) => async_derived(expression))).then((result) => { From 7144f11b5ba2fec9ab212ad626df8f21322a4005 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 16:01:41 -0400 Subject: [PATCH 429/582] dry out --- .../client/visitors/shared/utils.js | 3 +- .../src/internal/client/dom/blocks/async.js | 35 +++----- .../client/dom/elements/attributes.js | 87 ++++++++----------- .../src/internal/client/reactivity/async.js | 50 +++++++++++ .../src/internal/client/reactivity/effects.js | 48 ++-------- 5 files changed, 104 insertions(+), 119 deletions(-) create mode 100644 packages/svelte/src/internal/client/reactivity/async.js 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 db173ba78d2c..92f6c2a57839 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 @@ -155,8 +155,7 @@ export function build_render_statement(state) { : b.block(state.update) ), 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') + async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))) ) ); } diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index a68736281d88..2eac6c55e034 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,37 +1,24 @@ -/** @import { Effect, TemplateNode, Value } from '#client' */ -import { DESTROYED } from '#client/constants'; -import { async_derived } from '../../reactivity/deriveds.js'; -import { active_effect, get } from '../../runtime.js'; -import { capture, get_pending_boundary } from './boundary.js'; +/** @import { TemplateNode, Value } from '#client' */ +import { flatten } from '../../reactivity/async.js'; +import { get } from '../../runtime.js'; +import { get_pending_boundary } from './boundary.js'; /** * @param {TemplateNode} node * @param {Array<() => Promise>} expressions * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn */ -export async function async(node, expressions, fn) { - // TODO handle hydration - - var parent = /** @type {Effect} */ (active_effect); - - var restore = capture(); +export function async(node, expressions, fn) { var boundary = get_pending_boundary(); + // TODO why is this necessary? doesn't it happen inside `async_derived` inside `flatten`? boundary.update_pending_count(1); - try { - const deriveds = await Promise.all(expressions.map((fn) => async_derived(fn))); - - // get deriveds eagerly to avoid creating blocks if they reject - for (const d of deriveds) get(d); - - if ((parent.f & DESTROYED) !== 0) return; + flatten([], expressions, (values) => { + // get values eagerly to avoid creating blocks if they reject + for (const d of values) get(d); - restore(); - fn(node, ...deriveds); - } catch (error) { - boundary.error(error); - } finally { + fn(node, ...values); boundary.update_pending_count(-1); - } + }); } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 83035cc6a180..fa3f98283832 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -23,6 +23,7 @@ import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; import { block, branch, destroy_effect } from '../../reactivity/effects.js'; import { derived } from '../../reactivity/deriveds.js'; import { init_select, select_option } from './bindings/select.js'; +import { flatten } from '../../reactivity/async.js'; export const CLASS = Symbol('class'); export const STYLE = Symbol('style'); @@ -473,72 +474,54 @@ export function attribute_effect( sync = [], async = [], css_hash, - skip_warning = false, - d = derived -) { - const deriveds = sync.map(d); - - create_attribute_effect(element, fn, deriveds, css_hash, skip_warning); -} - -/** - * @param {Element & ElementCSSInlineStyle} element - * @param {(...expressions: any) => Record} fn - * @param {Value[]} deriveds - * @param {string} [css_hash] - * @param {boolean} [skip_warning] - */ -export function create_attribute_effect( - element, - fn, - deriveds = [], - css_hash, skip_warning = false ) { - /** @type {Record | undefined} */ - var prev = undefined; + flatten(sync, async, (values) => { + /** @type {Record | undefined} */ + var prev = undefined; - /** @type {Record} */ - var effects = {}; + /** @type {Record} */ + var effects = {}; - var is_select = element.nodeName === 'SELECT'; - var inited = false; + var is_select = element.nodeName === 'SELECT'; + var inited = false; - block(() => { - var next = fn(...deriveds.map(get)); - /** @type {Record} */ - var current = set_attributes(element, prev, next, css_hash, skip_warning); + block(() => { + var next = fn(...values.map(get)); + /** @type {Record} */ + var current = set_attributes(element, prev, next, css_hash, skip_warning); - if (inited && is_select && 'value' in next) { - select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); - } + if (inited && is_select && 'value' in next) { + select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); + } - for (let symbol of Object.getOwnPropertySymbols(effects)) { - if (!next[symbol]) destroy_effect(effects[symbol]); - } + for (let symbol of Object.getOwnPropertySymbols(effects)) { + if (!next[symbol]) destroy_effect(effects[symbol]); + } - for (let symbol of Object.getOwnPropertySymbols(next)) { - var n = next[symbol]; + for (let symbol of Object.getOwnPropertySymbols(next)) { + var n = next[symbol]; - if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) { - if (effects[symbol]) destroy_effect(effects[symbol]); - effects[symbol] = branch(() => attach(element, () => n)); + if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) { + if (effects[symbol]) destroy_effect(effects[symbol]); + effects[symbol] = branch(() => attach(element, () => n)); + } + + current[symbol] = n; } - current[symbol] = n; + prev = current; + }); + + if (is_select) { + init_select( + /** @type {HTMLSelectElement} */ (element), + () => /** @type {Record} */ (prev).value + ); } - prev = current; + inited = true; }); - - if (is_select) { - init_select( - /** @type {HTMLSelectElement} */ (element), - () => /** @type {Record} */ (prev).value - ); - } - - inited = true; } /** diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js new file mode 100644 index 000000000000..2708d9139a4d --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -0,0 +1,50 @@ +/** @import { Effect, Value } from '#client' */ + +import { DESTROYED } from '#client/constants'; +import { is_runes } from '../context.js'; +import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; +import { invoke_error_boundary } from '../error-handling.js'; +import { active_effect } from '../runtime.js'; +import { current_batch } from './batch.js'; +import { async_derived, derived, derived_safe_equal } from './deriveds.js'; + +/** + * + * @param {Array<() => any>} sync + * @param {Array<() => Promise>} async + * @param {(values: Value[]) => any} fn + */ +export function flatten(sync, async, fn) { + const d = is_runes() ? derived : derived_safe_equal; + + if (async.length > 0) { + var batch = current_batch; + var parent = /** @type {Effect} */ (active_effect); + + var restore = capture(); + + var boundary = get_pending_boundary(); + + Promise.all(async.map((expression) => async_derived(expression))) + .then((result) => { + if ((parent.f & DESTROYED) !== 0) return; + + batch?.restore(); + + restore(); + + try { + fn([...sync.map(d), ...result]); + } catch (error) { + invoke_error_boundary(error, parent); + } + + batch?.flush(); + }) + .catch((error) => { + boundary.error(error); + }); + } else { + fn(sync.map(d)); + } +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 1bef2174769a..fdb136d503e1 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, Value } from '#client' */ +/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */ import { check_dirtiness, active_effect, @@ -38,11 +38,9 @@ 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 { async_derived, derived } from './deriveds.js'; -import { capture } from '../dom/blocks/boundary.js'; import { component_context, dev_current_component_function } from '../context.js'; -import { Batch, current_batch } from './batch.js'; -import { invoke_error_boundary } from '../error-handling.js'; +import { Batch } from './batch.js'; +import { flatten } from './async.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -338,43 +336,11 @@ export function render_effect(fn, flags = 0) { * @param {(...expressions: any) => void | (() => void)} fn * @param {Array<() => any>} sync * @param {Array<() => Promise>} async - * @param {(fn: () => T) => Derived} d */ -export function template_effect(fn, sync = [], async = [], d = derived) { - if (async.length > 0) { - var batch = current_batch; - var parent = /** @type {Effect} */ (active_effect); - - var restore = capture(); - - Promise.all(async.map((expression) => async_derived(expression))).then((result) => { - if ((parent.f & DESTROYED) !== 0) return; - - // TODO probably need to do this in async.js as well - batch?.restore(); - - restore(); - - try { - create_template_effect(fn, [...sync.map(d), ...result]); - } catch (error) { - invoke_error_boundary(error, parent); - } - - batch?.flush(); - }); - } else { - create_template_effect(fn, sync.map(d)); - } -} - -/** - * @param {(...expressions: any) => void | (() => void)} fn - * @param {Value[]} deriveds - */ -function create_template_effect(fn, deriveds) { - var effect = () => fn(...deriveds.map(get)); - create_effect(RENDER_EFFECT, effect, true); +export function template_effect(fn, sync = [], async = []) { + flatten(sync, async, (values) => { + create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); + }); } /** From d7a99b65486acfb8b96795a6d31e81ac2197b742 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 16:35:18 -0400 Subject: [PATCH 430/582] fix async directives --- .../client/visitors/RegularElement.js | 29 +++++++++++-------- .../client/visitors/shared/element.js | 18 +++++++++--- .../samples/async-class-directive/_config.js | 20 +++++++++++++ .../samples/async-class-directive/main.svelte | 11 +++++++ 4 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte 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 1c4a75fb160c..d1348a9e5ef6 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 @@ -496,7 +496,7 @@ export function build_class_directives_object( * @param {ComponentContext} context * @param {MemoizedExpression[]} async_expressions * @param {MemoizedExpression[]} expressions - * @return {ObjectExpression | ArrayExpression}} + * @return {ObjectExpression | ArrayExpression | Identifier}} */ export function build_style_directives_object( style_directives, @@ -506,28 +506,33 @@ export function build_style_directives_object( ) { let normal_properties = []; let important_properties = []; + let has_call_or_state = false; + let has_await = false; - for (const directive of style_directives) { + for (const d of style_directives) { const expression = - directive.value === true - ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) - : build_attribute_value(directive.value, context, (value, metadata) => - metadata.has_call - ? get_expression_id(metadata.has_await ? async_expressions : expressions, value) - : value - ).value; - const property = b.init(directive.name, expression); + d.value === true + ? build_getter({ name: d.name, type: 'Identifier' }, context.state) + : build_attribute_value(d.value, context).value; + const property = b.init(d.name, expression); - if (directive.modifiers.includes('important')) { + if (d.modifiers.includes('important')) { important_properties.push(property); } else { normal_properties.push(property); } + + has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; + has_await ||= d.metadata.expression.has_await; } - return important_properties.length + const directives = important_properties.length ? b.array([b.object(normal_properties), b.object(important_properties)]) : b.object(normal_properties); + + return has_call_or_state || has_await + ? get_expression_id(has_await ? async_expressions : expressions, directives) + : directives; } /** 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 b1b1a8fae4f6..868cfe29464b 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 @@ -87,18 +87,24 @@ export function build_attribute_effect( ); } + const all = [...expressions, ...async_expressions]; + + for (let i = 0; i < all.length; i += 1) { + all[i].id.name = `$${i}`; + } + context.state.init.push( b.stmt( b.call( '$.attribute_effect', element_id, b.arrow( - expressions.map(({ id }) => id), + all.map(({ id }) => id), b.object(values) ), expressions.length > 0 && b.array(expressions.map(({ expression }) => b.thunk(expression))), async_expressions.length > 0 && - b.array(async_expressions.map(({ expression }) => b.thunk(expression))), + b.array(async_expressions.map(({ expression }) => b.thunk(expression, true))), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), @@ -182,7 +188,9 @@ export function build_set_class(element, node_id, attribute, class_directives, c if (class_directives.length) { next = build_class_directives_object(class_directives, context); - has_state ||= class_directives.some((d) => d.metadata.expression.has_state); + has_state ||= class_directives.some( + (d) => d.metadata.expression.has_state || d.metadata.expression.has_await + ); if (has_state) { previous_id = b.id(context.state.scope.generate('classes')); @@ -255,7 +263,9 @@ export function build_set_style(node_id, attribute, style_directives, context) { if (style_directives.length) { next = build_style_directives_object(style_directives, context); - has_state ||= style_directives.some((d) => d.metadata.expression.has_state); + has_state ||= style_directives.some( + (d) => d.metadata.expression.has_state || d.metadata.expression.has_await + ); if (has_state) { previous_id = b.id(context.state.scope.generate('styles')); diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js new file mode 100644 index 000000000000..3186ed2069da --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +
      one
      +
      two
      +
      red
      +
      blue
      + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte new file mode 100644 index 000000000000..f0f27e483005 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte @@ -0,0 +1,11 @@ + +
      one
      +
      two
      + +
      red
      +
      blue
      + + {#snippet pending()} + loading + {/snippet} +
      From 650144aa75e47f9f614104b3b7b2ef7979037de7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 16:43:34 -0400 Subject: [PATCH 431/582] tidy up --- .../client/visitors/RegularElement.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 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 d1348a9e5ef6..5496a051ac32 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 @@ -1,4 +1,4 @@ -/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */ +/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Property } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, MemoizedExpression } from '../types' */ /** @import { Scope } from '../../../scope' */ @@ -504,8 +504,9 @@ export function build_style_directives_object( async_expressions = context.state.async_expressions, expressions = context.state.expressions ) { - let normal_properties = []; - let important_properties = []; + const normal = b.object([]); + const important = b.object([]); + let has_call_or_state = false; let has_await = false; @@ -514,21 +515,15 @@ export function build_style_directives_object( d.value === true ? build_getter({ name: d.name, type: 'Identifier' }, context.state) : build_attribute_value(d.value, context).value; - const property = b.init(d.name, expression); - if (d.modifiers.includes('important')) { - important_properties.push(property); - } else { - normal_properties.push(property); - } + const object = d.modifiers.includes('important') ? important : normal; + object.properties.push(b.init(d.name, expression)); has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; has_await ||= d.metadata.expression.has_await; } - const directives = important_properties.length - ? b.array([b.object(normal_properties), b.object(important_properties)]) - : b.object(normal_properties); + const directives = important.properties.length ? b.array([normal, important]) : normal; return has_call_or_state || has_await ? get_expression_id(has_await ? async_expressions : expressions, directives) From 334adc0897a7463c02819d91f574eae008e611cd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 18:40:17 -0400 Subject: [PATCH 432/582] initialize option values before initing select values --- .../client/visitors/RegularElement.js | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 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 b0f285eb413d..18214c83429a 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 @@ -199,16 +199,16 @@ export function RegularElement(node, context) { const node_id = context.state.node; + /** If true, needs `__value` for inputs */ + const needs_special_value_handling = + node.name === 'option' || + node.name === 'select' || + bindings.has('group') || + bindings.has('checked'); + if (has_spread) { build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id); } else { - /** If true, needs `__value` for inputs */ - const needs_special_value_handling = - node.name === 'option' || - node.name === 'select' || - bindings.has('group') || - bindings.has('checked'); - for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { if (is_event_attribute(attribute)) { visit_event_attribute(attribute, context); @@ -216,7 +216,6 @@ export function RegularElement(node, context) { } if (needs_special_value_handling && attribute.name === 'value') { - build_element_special_value_attribute(node.name, node_id, attribute, context); continue; } @@ -391,6 +390,21 @@ export function RegularElement(node, context) { context.state.update.push(b.stmt(b.assignment('=', dir, dir))); } + if (!has_spread && needs_special_value_handling) { + for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { + if (attribute.name === 'value') { + build_element_special_value_attribute( + node.name, + node_id, + attribute, + context, + context.state + ); + break; + } + } + } + context.state.template.pop_element(); } @@ -612,9 +626,9 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co * @param {Identifier} node_id * @param {AST.Attribute} attribute * @param {ComponentContext} context + * @param {ComponentClientTransformState} state */ -function build_element_special_value_attribute(element, node_id, attribute, context) { - const state = context.state; +function build_element_special_value_attribute(element, node_id, attribute, context, state) { const is_select_with_value = // attribute.metadata.dynamic would give false negatives because even if the value does not change, // the inner options could still change, so we need to always treat it as reactive From d89f3184ebb58257306796454bba2880429e1941 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 18:51:13 -0400 Subject: [PATCH 433/582] simplify init_select --- .../client/visitors/RegularElement.js | 2 +- .../internal/client/dom/elements/attributes.js | 17 +++++++++++------ .../client/dom/elements/bindings/select.js | 9 +-------- 3 files changed, 13 insertions(+), 15 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 18214c83429a..b13284c35458 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 @@ -666,7 +666,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont ); if (is_select_with_value) { - state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value)))); + state.init.push(b.stmt(b.call('$.init_select', node_id))); } if (has_state) { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 2d3d6a921dc1..cc44171e21c3 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -6,7 +6,7 @@ import { create_event, delegate } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; import { LOADING_ATTR_SYMBOL } from '#client/constants'; -import { queue_idle_task } from '../task.js'; +import { queue_idle_task, queue_micro_task } from '../task.js'; import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js'; import { active_effect, @@ -20,7 +20,7 @@ import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; import { set_style } from './style.js'; import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; -import { block, branch, destroy_effect } from '../../reactivity/effects.js'; +import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js'; import { derived } from '../../reactivity/deriveds.js'; import { init_select, select_option } from './bindings/select.js'; @@ -513,10 +513,15 @@ export function attribute_effect( }); if (is_select) { - init_select( - /** @type {HTMLSelectElement} */ (element), - () => /** @type {Record} */ (prev).value - ); + var select = /** @type {HTMLSelectElement} */ (element); + + if (!inited) { + effect(() => { + select_option(select, /** @type {Record} */ (prev).value); + }); + } + + init_select(select); } inited = true; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index e3263c65afae..5363df0d44c2 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -53,16 +53,9 @@ export function select_option(select, value, mounting) { * inside an `#each` block. * @template V * @param {HTMLSelectElement} select - * @param {() => V} [get_value] */ -export function init_select(select, get_value) { - let mounting = true; +export function init_select(select) { effect(() => { - if (get_value) { - select_option(select, untrack(get_value), mounting); - } - mounting = false; - var observer = new MutationObserver(() => { // @ts-ignore var value = select.__value; From 58918e38e997a5f0f7b33120ab7bdb0d0543f6ef Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 19:03:35 -0400 Subject: [PATCH 434/582] simplify --- .../phases/3-transform/client/visitors/RegularElement.js | 8 ++++---- .../svelte/src/internal/client/dom/elements/attributes.js | 8 +++----- .../src/internal/client/dom/elements/bindings/select.js | 5 ++--- 3 files changed, 9 insertions(+), 12 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 b13284c35458..aec8e9ef312a 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 @@ -665,10 +665,6 @@ function build_element_special_value_attribute(element, node_id, attribute, cont : inner_assignment ); - if (is_select_with_value) { - state.init.push(b.stmt(b.call('$.init_select', node_id))); - } - if (has_state) { const id = b.id(state.scope.generate(`${node_id.name}_value`)); @@ -682,4 +678,8 @@ function build_element_special_value_attribute(element, node_id, attribute, cont } else { state.init.push(update); } + + if (is_select_with_value) { + state.init.push(b.stmt(b.call('$.init_select', node_id))); + } } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index cc44171e21c3..1296d1d536cb 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -515,11 +515,9 @@ export function attribute_effect( if (is_select) { var select = /** @type {HTMLSelectElement} */ (element); - if (!inited) { - effect(() => { - select_option(select, /** @type {Record} */ (prev).value); - }); - } + queue_micro_task(() => { + select_option(select, /** @type {Record} */ (prev).value); + }); init_select(select); } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index 5363df0d44c2..c4f425533084 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -1,9 +1,9 @@ import { effect } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; -import { untrack } from '../../../runtime.js'; import { is } from '../../../proxy.js'; import { is_array } from '../../../../shared/utils.js'; import * as w from '../../../warnings.js'; +import { queue_micro_task } from '../../task.js'; /** * Selects the correct option(s) (depending on whether this is a multiple select) @@ -51,11 +51,10 @@ export function select_option(select, value, mounting) { * current selection to the dom when it changes. Such * changes could for example occur when options are * inside an `#each` block. - * @template V * @param {HTMLSelectElement} select */ export function init_select(select) { - effect(() => { + queue_micro_task(() => { var observer = new MutationObserver(() => { // @ts-ignore var value = select.__value; From 38d458a01ba31e01d6d68e566eadc40ce9d989bb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 19:13:51 -0400 Subject: [PATCH 435/582] tweak --- .../client/dom/elements/attributes.js | 5 +- .../client/dom/elements/bindings/select.js | 46 +++++++++---------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 1296d1d536cb..b14ecb42a1ae 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -515,11 +515,10 @@ export function attribute_effect( if (is_select) { var select = /** @type {HTMLSelectElement} */ (element); - queue_micro_task(() => { + effect(() => { select_option(select, /** @type {Record} */ (prev).value); + init_select(select); }); - - init_select(select); } inited = true; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index c4f425533084..ff7c59f36ce7 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -1,4 +1,4 @@ -import { effect } from '../../../reactivity/effects.js'; +import { effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; import { is } from '../../../proxy.js'; import { is_array } from '../../../../shared/utils.js'; @@ -54,29 +54,26 @@ export function select_option(select, value, mounting) { * @param {HTMLSelectElement} select */ export function init_select(select) { - queue_micro_task(() => { - var observer = new MutationObserver(() => { - // @ts-ignore - var value = select.__value; - select_option(select, value); - // Deliberately don't update the potential binding value, - // the model should be preserved unless explicitly changed - }); - - observer.observe(select, { - // Listen to option element changes - childList: true, - subtree: true, // because of - // Listen to option element value attribute changes - // (doesn't get notified of select value changes, - // because that property is not reflected as an attribute) - attributes: true, - attributeFilter: ['value'] - }); - - return () => { - observer.disconnect(); - }; + var observer = new MutationObserver(() => { + // @ts-ignore + select_option(select, select.__value); + // Deliberately don't update the potential binding value, + // the model should be preserved unless explicitly changed + }); + + observer.observe(select, { + // Listen to option element changes + childList: true, + subtree: true, // because of + // Listen to option element value attribute changes + // (doesn't get notified of select value changes, + // because that property is not reflected as an attribute) + attributes: true, + attributeFilter: ['value'] + }); + + teardown(() => { + observer.disconnect(); }); } @@ -128,7 +125,6 @@ export function bind_select_value(select, get, set = get) { mounting = false; }); - // don't pass get_value, we already initialize it in the effect above init_select(select); } From 39b7f416719bf160272767d7b0dbd7ec725fc135 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 19:14:54 -0400 Subject: [PATCH 436/582] tidy up --- packages/svelte/src/internal/client/dom/elements/attributes.js | 2 +- .../svelte/src/internal/client/dom/elements/bindings/select.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index b14ecb42a1ae..5db685cf3e90 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -6,7 +6,7 @@ import { create_event, delegate } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; import { LOADING_ATTR_SYMBOL } from '#client/constants'; -import { queue_idle_task, queue_micro_task } from '../task.js'; +import { queue_idle_task } from '../task.js'; import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js'; import { active_effect, diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index ff7c59f36ce7..5e89686d8654 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -3,7 +3,6 @@ import { listen_to_event_and_reset_event } from './shared.js'; import { is } from '../../../proxy.js'; import { is_array } from '../../../../shared/utils.js'; import * as w from '../../../warnings.js'; -import { queue_micro_task } from '../../task.js'; /** * Selects the correct option(s) (depending on whether this is a multiple select) From 5615fd34e88979c427e81665c8a317ec63d73448 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 19:17:11 -0400 Subject: [PATCH 437/582] tweak --- .../3-transform/client/visitors/RegularElement.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 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 aec8e9ef312a..9a9bd80975af 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 @@ -393,13 +393,7 @@ export function RegularElement(node, context) { if (!has_spread && needs_special_value_handling) { for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { if (attribute.name === 'value') { - build_element_special_value_attribute( - node.name, - node_id, - attribute, - context, - context.state - ); + build_element_special_value_attribute(node.name, node_id, attribute, context); break; } } @@ -626,9 +620,9 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co * @param {Identifier} node_id * @param {AST.Attribute} attribute * @param {ComponentContext} context - * @param {ComponentClientTransformState} state */ -function build_element_special_value_attribute(element, node_id, attribute, context, state) { +function build_element_special_value_attribute(element, node_id, attribute, context) { + const state = context.state; const is_select_with_value = // attribute.metadata.dynamic would give false negatives because even if the value does not change, // the inner options could still change, so we need to always treat it as reactive From b459bb093592a0a6a346ad44fb5295eeccfa3fc9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 19:26:10 -0400 Subject: [PATCH 438/582] on second thoughts just simplify it here --- .../3-transform/client/visitors/RegularElement.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 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 9a9bd80975af..ae8680f5940c 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 @@ -22,12 +22,7 @@ import { build_set_style } from './shared/element.js'; import { process_children } from './shared/fragment.js'; -import { - build_render_statement, - build_template_chunk, - get_expression_id, - memoize_expression -} from './shared/utils.js'; +import { build_render_statement, build_template_chunk, get_expression_id } from './shared/utils.js'; import { visit_event_attribute } from './shared/events.js'; /** @@ -629,12 +624,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont element === 'select' && attribute.value !== true && !is_text_attribute(attribute); const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call - ? // if is a select with value we will also invoke `init_select` which need a reference before the template effect so we memoize separately - is_select_with_value - ? memoize_expression(state, value) - : get_expression_id(state.expressions, value) - : value + metadata.has_call ? get_expression_id(state.expressions, value) : value ); const evaluated = context.state.scope.evaluate(value); From 3070b9aa4fa388e91fd9ae75d814827acfa8d28b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 19:30:51 -0400 Subject: [PATCH 439/582] tidy --- packages/svelte/src/internal/client/dom/elements/attributes.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 9b21d22a4e9c..d0316f11b2c4 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -1,4 +1,4 @@ -/** @import { Effect, Value } from '#client' */ +/** @import { Effect } from '#client' */ import { DEV } from 'esm-env'; import { hydrating, set_hydrating } from '../hydration.js'; import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; @@ -21,7 +21,6 @@ import { set_class } from './class.js'; import { set_style } from './style.js'; import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js'; -import { derived } from '../../reactivity/deriveds.js'; import { init_select, select_option } from './bindings/select.js'; import { flatten } from '../../reactivity/async.js'; From a018796154e39089fca906e193e105c0ade1b907 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 20:56:11 -0400 Subject: [PATCH 440/582] handle awaits in `` --- .../client/visitors/SlotElement.js | 54 ++++++++++++++----- .../samples/async-slot/Child.svelte | 1 + .../samples/async-slot/_config.js | 13 +++++ .../samples/async-slot/main.svelte | 13 +++++ 4 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-slot/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index 401cfde42832..19d485c01a98 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -1,9 +1,10 @@ -/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */ +/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext } from '../types' */ +/** @import { ComponentContext, MemoizedExpression } from '../types' */ import * as b from '#compiler/builders'; +import { create_derived } from '../utils.js'; import { build_attribute_value } from './shared/element.js'; -import { memoize_expression } from './shared/utils.js'; +import { get_expression_id, memoize_expression } from './shared/utils.js'; /** * @param {AST.SlotElement} node @@ -22,7 +23,11 @@ export function SlotElement(node, context) { /** @type {ExpressionStatement[]} */ const lets = []; - let is_default = true; + /** @type {MemoizedExpression[]} */ + const expressions = []; + + /** @type {MemoizedExpression[]} */ + const async_expressions = []; let name = b.literal('default'); @@ -33,12 +38,17 @@ export function SlotElement(node, context) { const { value, has_state } = build_attribute_value( attribute.value, context, - (value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value) + (value, metadata) => + metadata.has_call || metadata.has_await + ? b.call( + '$.get', + get_expression_id(metadata.has_await ? async_expressions : expressions, value) + ) + : value ); if (attribute.name === 'name') { name = /** @type {Literal} */ (value); - is_default = false; } else if (attribute.name !== 'slot') { if (has_state) { props.push(b.get(attribute.name, [b.return(value)])); @@ -54,6 +64,11 @@ export function SlotElement(node, context) { // Let bindings first, they can be used on attributes context.state.init.push(...lets); + /** @type {Statement[]} */ + const statements = expressions.map((memo) => + b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ); + const props_expression = spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads); @@ -62,14 +77,25 @@ export function SlotElement(node, context) { ? b.null : b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))); - const slot = b.call( - '$.slot', - context.state.node, - b.id('$$props'), - name, - props_expression, - fallback + statements.push( + b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback)) ); - context.state.init.push(b.stmt(slot)); + 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/tests/runtime-runes/samples/async-slot/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte new file mode 100644 index 000000000000..c32f869f63c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js new file mode 100644 index 000000000000..3d54d242598c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` +

      loading...

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

      hello

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

      {message}

      +
      + + {#snippet pending()} +

      loading...

      + {/snippet} +
      From 94d74f26e3190139cd0f0482216c23d9f4f1422a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 20:56:29 -0400 Subject: [PATCH 441/582] unused --- .../phases/3-transform/client/visitors/SlotElement.js | 2 +- .../phases/3-transform/client/visitors/shared/utils.js | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index 19d485c01a98..c1d235c25bae 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -4,7 +4,7 @@ import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { build_attribute_value } from './shared/element.js'; -import { get_expression_id, memoize_expression } from './shared/utils.js'; +import { get_expression_id } from './shared/utils.js'; /** * @param {AST.SlotElement} node 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 8d079ef73cdd..a30451fa95e0 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 @@ -10,16 +10,6 @@ import is_reference from 'is-reference'; import { dev, is_ignored, locator } from '../../../../../state.js'; import { build_getter, create_derived } from '../../utils.js'; -/** - * @param {ComponentClientTransformState} state - * @param {Expression} value - */ -export function memoize_expression(state, value) { - const id = b.id(state.scope.generate('expression')); - state.init.push(b.const(id, create_derived(state, b.thunk(value)))); - return b.call('$.get', id); -} - /** * * @param {MemoizedExpression[]} expressions From 0c2412964dbaa4004216e626651821e6a9e31dc0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 21:00:20 -0400 Subject: [PATCH 442/582] tidy up --- .../phases/3-transform/client/visitors/SlotElement.js | 4 ++++ .../phases/3-transform/client/visitors/shared/utils.js | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index c1d235c25bae..072eca95f6fb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -61,6 +61,10 @@ export function SlotElement(node, context) { } } + [...async_expressions, ...expressions].forEach((memo, i) => { + memo.id.name = `$${i}`; + }); + // Let bindings first, they can be used on attributes context.state.init.push(...lets); 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 a30451fa95e0..8f684bfed468 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 @@ -16,8 +16,7 @@ import { build_getter, create_derived } from '../../utils.js'; * @param {Expression} expression */ export function get_expression_id(expressions, expression) { - // TODO tidy this up - const id = b.id(`$${expressions.length}`); + const id = b.id(`#`); // filled in later expressions.push({ id, expression }); return id; From 12fffb9c1c61c23c3a79486c2717c405c6a6c47f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 21:02:54 -0400 Subject: [PATCH 443/582] tidy up --- .../phases/3-transform/client/visitors/shared/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 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 8f684bfed468..bbffefc152e7 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,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context, MemoizedExpression } from '../../types' */ import { walk } from 'zimmerframe'; @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator } from '../../../../../state.js'; -import { build_getter, create_derived } from '../../utils.js'; +import { build_getter } from '../../utils.js'; /** * From 2b4410007dabcefc09aa24a2862476e6152f8810 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 21:27:23 -0400 Subject: [PATCH 444/582] dry out --- .../3-transform/client/transform-client.js | 6 +- .../phases/3-transform/client/types.d.ts | 12 +--- .../3-transform/client/visitors/Fragment.js | 5 +- .../client/visitors/RegularElement.js | 31 +++------- .../3-transform/client/visitors/RenderTag.js | 27 +++------ .../client/visitors/SlotElement.js | 27 +++------ .../client/visitors/SvelteElement.js | 7 +-- .../client/visitors/shared/component.js | 40 ++++--------- .../client/visitors/shared/element.js | 52 ++++++----------- .../client/visitors/shared/utils.js | 56 +++++++++++-------- 10 files changed, 99 insertions(+), 164 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 fcb9b4748453..85fe359e20ee 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 @@ -59,6 +59,7 @@ import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UseDirective } from './visitors/UseDirective.js'; import { AttachTag } from './visitors/AttachTag.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; +import { Memoizer } from './visitors/shared/utils.js'; /** @type {Visitors} */ const visitors = { @@ -170,10 +171,9 @@ export function client_component(analysis, options) { // these are set inside the `Fragment` visitor, and cannot be used until then 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) + template: /** @type {any} */ (null), + memoizer: /** @type {any} */ (null) }; const module = /** @type {ESTree.Program} */ ( 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 9ca16e65494d..cf5c942268cc 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 @@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; import type { ComponentAnalysis } from '../../types.js'; import type { Template } from './transform-template/template.js'; +import type { Memoizer } from './visitors/shared/utils.js'; export interface ClientTransformState extends TransformState { /** @@ -49,10 +50,8 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly update: Statement[]; /** 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 }>; - /** Expressions used inside the render effect */ - readonly async_expressions: Array<{ id: Identifier; expression: Expression }>; + /** Memoized expressions */ + readonly memoizer: Memoizer; /** The HTML template string */ readonly template: Template; readonly metadata: { @@ -87,8 +86,3 @@ 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 122cae450102..0b10c02ffbe1 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 @@ -6,7 +6,7 @@ import * as b from '#compiler/builders'; import { clean_nodes, infer_namespace } from '../../utils.js'; import { transform_template } from '../transform-template/index.js'; import { process_children } from './shared/fragment.js'; -import { build_render_statement } from './shared/utils.js'; +import { build_render_statement, Memoizer } from './shared/utils.js'; import { Template } from '../transform-template/template.js'; /** @@ -62,9 +62,8 @@ export function Fragment(node, context) { ...context.state, init: [], update: [], - expressions: [], - async_expressions: [], after_update: [], + memoizer: new Memoizer(), template: new Template(), transform: { ...context.state.transform }, metadata: { 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 ad11af92161f..0426ddc3b452 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 @@ -22,7 +22,7 @@ import { build_set_style } from './shared/element.js'; import { process_children } from './shared/fragment.js'; -import { build_render_statement, build_template_chunk, get_expression_id } from './shared/utils.js'; +import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js'; import { visit_event_attribute } from './shared/events.js'; /** @@ -255,10 +255,7 @@ export function RegularElement(node, context) { context, (value, metadata) => metadata.has_call || metadata.has_await - ? get_expression_id( - metadata.has_await ? context.state.async_expressions : context.state.expressions, - value - ) + ? context.state.memoizer.add(value, metadata.has_await) : value ); @@ -465,15 +462,13 @@ function setup_select_synchronization(value_binding, context) { /** * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context - * @param {MemoizedExpression[]} async_expressions - * @param {MemoizedExpression[]} expressions + * @param {Memoizer} memoizer * @return {ObjectExpression | Identifier} */ export function build_class_directives_object( class_directives, context, - async_expressions = context.state.async_expressions, - expressions = context.state.expressions + memoizer = context.state.memoizer ) { let properties = []; let has_call_or_state = false; @@ -488,23 +483,19 @@ export function build_class_directives_object( const directives = b.object(properties); - return has_call_or_state || has_await - ? get_expression_id(has_await ? async_expressions : expressions, directives) - : directives; + return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives; } /** * @param {AST.StyleDirective[]} style_directives * @param {ComponentContext} context - * @param {MemoizedExpression[]} async_expressions - * @param {MemoizedExpression[]} expressions + * @param {Memoizer} memoizer * @return {ObjectExpression | ArrayExpression | Identifier}} */ export function build_style_directives_object( style_directives, context, - async_expressions = context.state.async_expressions, - expressions = context.state.expressions + memoizer = context.state.memoizer ) { const normal = b.object([]); const important = b.object([]); @@ -527,9 +518,7 @@ export function build_style_directives_object( const directives = important.properties.length ? b.array([normal, important]) : normal; - return has_call_or_state || has_await - ? get_expression_id(has_await ? async_expressions : expressions, directives) - : directives; + return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives; } /** @@ -651,9 +640,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont element === 'select' && attribute.value !== true && !is_text_attribute(attribute); const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await - ? get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value) - : value + metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value ); const evaluated = context.state.scope.evaluate(value); 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 b7187173e255..5255693fe36d 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,10 +1,10 @@ /** @import { Expression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext, MemoizedExpression } from '../types' */ +/** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; -import { get_expression_id, build_expression } from './shared/utils.js'; +import { build_expression, Memoizer } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -18,11 +18,7 @@ export function RenderTag(node, context) { /** @type {Expression[]} */ let args = []; - /** @type {MemoizedExpression[]} */ - const expressions = []; - - /** @type {MemoizedExpression[]} */ - const async_expressions = []; + const memoizer = new Memoizer(); for (let i = 0; i < call.arguments.length; i++) { const arg = /** @type {Expression} */ (call.arguments[i]); @@ -31,21 +27,16 @@ export function RenderTag(node, context) { let expression = build_expression(context, arg, metadata); if (metadata.has_await || metadata.has_call) { - expression = b.call( - '$.get', - get_expression_id(metadata.has_await ? async_expressions : expressions, expression) - ); + expression = b.call('$.get', memoizer.add(expression, metadata.has_await)); } args.push(b.thunk(expression)); } - [...async_expressions, ...expressions].forEach((memo, i) => { - memo.id.name = `$${i}`; - }); + memoizer.apply(); /** @type {Statement[]} */ - const statements = expressions.map((memo) => + const statements = memoizer.sync.map((memo) => b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) ); @@ -76,15 +67,15 @@ export function RenderTag(node, context) { ); } - if (async_expressions.length > 0) { + if (memoizer.async.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.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), b.arrow( - [context.state.node, ...async_expressions.map((memo) => memo.id)], + [context.state.node, ...memoizer.async.map((memo) => memo.id)], b.block(statements) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index 072eca95f6fb..70de454c0e94 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -1,10 +1,10 @@ /** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext, MemoizedExpression } from '../types' */ +/** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { build_attribute_value } from './shared/element.js'; -import { get_expression_id } from './shared/utils.js'; +import { Memoizer } from './shared/utils.js'; /** * @param {AST.SlotElement} node @@ -23,11 +23,7 @@ export function SlotElement(node, context) { /** @type {ExpressionStatement[]} */ const lets = []; - /** @type {MemoizedExpression[]} */ - const expressions = []; - - /** @type {MemoizedExpression[]} */ - const async_expressions = []; + const memoizer = new Memoizer(); let name = b.literal('default'); @@ -40,10 +36,7 @@ export function SlotElement(node, context) { context, (value, metadata) => metadata.has_call || metadata.has_await - ? b.call( - '$.get', - get_expression_id(metadata.has_await ? async_expressions : expressions, value) - ) + ? b.call('$.get', memoizer.add(value, metadata.has_await)) : value ); @@ -61,15 +54,13 @@ export function SlotElement(node, context) { } } - [...async_expressions, ...expressions].forEach((memo, i) => { - memo.id.name = `$${i}`; - }); + memoizer.apply(); // Let bindings first, they can be used on attributes context.state.init.push(...lets); /** @type {Statement[]} */ - const statements = expressions.map((memo) => + const statements = memoizer.sync.map((memo) => b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) ); @@ -85,15 +76,15 @@ export function SlotElement(node, context) { b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback)) ); - if (async_expressions.length > 0) { + if (memoizer.async.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.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), b.arrow( - [context.state.node, ...async_expressions.map((memo) => memo.id)], + [context.state.node, ...memoizer.async.map((memo) => memo.id)], b.block(statements) ) ) 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 0534895fe19b..2062519bb64b 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 @@ -10,7 +10,7 @@ import { build_attribute_effect, build_set_class } from './shared/element.js'; -import { build_render_statement } from './shared/utils.js'; +import { build_render_statement, Memoizer } from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -46,9 +46,8 @@ export function SvelteElement(node, context) { node: element_id, init: [], update: [], - expressions: [], - async_expressions: [], - after_update: [] + after_update: [], + memoizer: new Memoizer() } }; 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 d14a60da672b..77535cf16e22 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,10 +1,10 @@ /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext, MemoizedExpression } from '../../types.js' */ +/** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_bind_this, get_expression_id, validate_binding } from '../shared/utils.js'; +import { build_bind_this, Memoizer, 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'; @@ -44,11 +44,7 @@ export function build_component(node, component_name, context) { /** @type {Record} */ const events = {}; - /** @type {MemoizedExpression[]} */ - const expressions = []; - - /** @type {MemoizedExpression[]} */ - const async_expressions = []; + const memoizer = new Memoizer(); /** @type {Property[]} */ const custom_css_props = []; @@ -139,13 +135,7 @@ export function build_component(node, component_name, context) { props_and_spreads.push( b.thunk( attribute.metadata.expression.has_await || attribute.metadata.expression.has_call - ? b.call( - '$.get', - get_expression_id( - attribute.metadata.expression.has_await ? async_expressions : expressions, - expression - ) - ) + ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await)) : expression ) ); @@ -160,10 +150,7 @@ export function build_component(node, component_name, context) { build_attribute_value(attribute.value, context, (value, metadata) => { // TODO put the derived in the local block return metadata.has_call || metadata.has_await - ? b.call( - '$.get', - get_expression_id(metadata.has_await ? async_expressions : expressions, value) - ) + ? b.call('$.get', memoizer.add(value, metadata.has_await)) : value; }).value ) @@ -199,10 +186,7 @@ export function build_component(node, component_name, context) { }); return should_wrap_in_derived - ? b.call( - '$.get', - get_expression_id(metadata.has_await ? async_expressions : expressions, value) - ) + ? b.call('$.get', memoizer.add(value, metadata.has_await)) : value; } ); @@ -465,7 +449,7 @@ export function build_component(node, component_name, context) { const statements = [ ...snippet_declarations, - ...expressions.map((memo) => + ...memoizer.sync.map((memo) => b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) ) ]; @@ -515,17 +499,15 @@ export function build_component(node, component_name, context) { statements.push(b.stmt(fn(anchor))); } - [...async_expressions, ...expressions].forEach((memo, i) => { - memo.id.name = `$${i}`; - }); + memoizer.apply(); - if (async_expressions.length > 0) { + if (memoizer.async.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)) + b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), + b.arrow([b.id('$$anchor'), ...memoizer.async.map(({ id }) => id)], b.block(statements)) ) ); } 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 868cfe29464b..8da489409bc5 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 @@ -1,13 +1,13 @@ -/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */ +/** @import { Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ -/** @import { ComponentContext, MemoizedExpression } from '../../types' */ +/** @import { ComponentContext } from '../../types' */ import { escape_html } from '../../../../../../escaping.js'; import { normalize_attribute } from '../../../../../../utils.js'; import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; -import { build_expression, build_template_chunk, get_expression_id } from './utils.js'; +import { build_expression, build_template_chunk, Memoizer } from './utils.js'; /** * @param {Array} attributes @@ -28,18 +28,12 @@ export function build_attribute_effect( /** @type {ObjectExpression['properties']} */ const values = []; - /** @type {MemoizedExpression[]} */ - const async_expressions = []; - - /** @type {MemoizedExpression[]} */ - const expressions = []; + const memoizer = new Memoizer(); for (const attribute of attributes) { if (attribute.type === 'Attribute') { const { value } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await - ? get_expression_id(metadata.has_await ? async_expressions : expressions, value) - : value + metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value ); if ( @@ -57,10 +51,7 @@ export function build_attribute_effect( let value = /** @type {Expression} */ (context.visit(attribute)); if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) { - value = get_expression_id( - attribute.metadata.expression.has_await ? async_expressions : expressions, - value - ); + value = memoizer.add(value, attribute.metadata.expression.has_await); } values.push(b.spread(value)); @@ -72,7 +63,7 @@ export function build_attribute_effect( b.prop( 'init', b.array([b.id('$.CLASS')]), - build_class_directives_object(class_directives, context, async_expressions, expressions) + build_class_directives_object(class_directives, context, memoizer) ) ); } @@ -82,16 +73,12 @@ export function build_attribute_effect( b.prop( 'init', b.array([b.id('$.STYLE')]), - build_style_directives_object(style_directives, context, async_expressions, expressions) + build_style_directives_object(style_directives, context, memoizer) ) ); } - const all = [...expressions, ...async_expressions]; - - for (let i = 0; i < all.length; i += 1) { - all[i].id.name = `$${i}`; - } + const all = memoizer.apply(); context.state.init.push( b.stmt( @@ -102,9 +89,10 @@ export function build_attribute_effect( all.map(({ id }) => id), b.object(values) ), - expressions.length > 0 && b.array(expressions.map(({ expression }) => b.thunk(expression))), - async_expressions.length > 0 && - b.array(async_expressions.map(({ expression }) => b.thunk(expression, true))), + memoizer.sync.length > 0 && + b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), + memoizer.async.length > 0 && + b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), @@ -170,10 +158,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c } return metadata.has_call || metadata.has_await - ? get_expression_id( - metadata.has_await ? context.state.async_expressions : context.state.expressions, - value - ) + ? context.state.memoizer.add(value, metadata.has_await) : value; }); @@ -244,12 +229,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c */ export function build_set_style(node_id, attribute, style_directives, context) { let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call - ? get_expression_id( - metadata.has_await ? context.state.async_expressions : context.state.expressions, - value - ) - : value + metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value ); /** @type {Identifier | undefined} */ @@ -258,7 +238,7 @@ export function build_set_style(node_id, attribute, style_directives, context) { /** @type {ObjectExpression | Identifier | undefined} */ let prev; - /** @type {ArrayExpression | ObjectExpression | undefined} */ + /** @type {Expression | undefined} */ let next; if (style_directives.length) { 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 bbffefc152e7..ed9b8ad8a4a9 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 { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ -/** @import { ComponentClientTransformState, ComponentContext, Context, MemoizedExpression } from '../../types' */ +/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -10,16 +10,34 @@ import is_reference from 'is-reference'; import { dev, is_ignored, locator } from '../../../../../state.js'; import { build_getter } from '../../utils.js'; -/** - * - * @param {MemoizedExpression[]} expressions - * @param {Expression} expression - */ -export function get_expression_id(expressions, expression) { - const id = b.id(`#`); // filled in later - expressions.push({ id, expression }); +export class Memoizer { + /** @type {Array<{ id: Identifier, expression: Expression }>} */ + sync = []; + + /** @type {Array<{ id: Identifier, expression: Expression }>} */ + async = []; + + /** + * @param {Expression} expression + * @param {boolean} has_await + */ + add(expression, has_await) { + const id = b.id(`#`); // filled in later + + (has_await ? this.async : this.sync).push({ id, expression }); - return id; + return id; + } + + apply() { + const all = [...this.async, ...this.sync]; + + all.forEach((memo, i) => { + memo.id.name = `$${i}`; + }); + + return all; + } } /** @@ -34,9 +52,7 @@ export function build_template_chunk( context, state = context.state, memoize = (value, metadata) => - metadata.has_call || metadata.has_await - ? get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value) - : value + metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value ) { /** @type {Expression[]} */ const expressions = []; @@ -125,14 +141,9 @@ export function build_template_chunk( * @param {ComponentClientTransformState} state */ export function build_render_statement(state) { - const sync = state.expressions; - const async = state.async_expressions; - - const all = [...sync, ...async]; + const { memoizer } = state; - for (let i = 0; i < all.length; i += 1) { - all[i].id.name = `$${i}`; - } + const all = state.memoizer.apply(); return b.stmt( b.call( @@ -143,8 +154,9 @@ export function build_render_statement(state) { ? state.update[0].expression : b.block(state.update) ), - all.length > 0 && b.array(sync.map(({ expression }) => b.thunk(expression))), - async.length > 0 && b.array(async.map(({ expression }) => b.thunk(expression, true))) + all.length > 0 && b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), + memoizer.async.length > 0 && + b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))) ) ); } From 25855163bf7d915492f62d3d0e5370ed74e65132 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 21:54:34 -0400 Subject: [PATCH 445/582] dry out --- .../3-transform/client/visitors/RenderTag.js | 16 +++---- .../client/visitors/SlotElement.js | 16 +++---- .../client/visitors/shared/component.js | 16 +++---- .../client/visitors/shared/element.js | 13 ++---- .../client/visitors/shared/utils.js | 43 +++++++++++++------ 5 files changed, 53 insertions(+), 51 deletions(-) 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 5255693fe36d..528f867e01e4 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 @@ -3,7 +3,6 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { create_derived } from '../utils.js'; import { build_expression, Memoizer } from './shared/utils.js'; /** @@ -36,9 +35,7 @@ export function RenderTag(node, context) { memoizer.apply(); /** @type {Statement[]} */ - const statements = memoizer.sync.map((memo) => - b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) - ); + const statements = memoizer.deriveds(); let snippet_function = build_expression( context, @@ -67,17 +64,16 @@ export function RenderTag(node, context) { ); } - if (memoizer.async.length > 0) { + const async_values = memoizer.async_values(); + + if (async_values) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), - b.arrow( - [context.state.node, ...memoizer.async.map((memo) => memo.id)], - b.block(statements) - ) + async_values, + b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements)) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index 70de454c0e94..fce445c6267c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -2,7 +2,6 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { create_derived } from '../utils.js'; import { build_attribute_value } from './shared/element.js'; import { Memoizer } from './shared/utils.js'; @@ -60,9 +59,7 @@ export function SlotElement(node, context) { context.state.init.push(...lets); /** @type {Statement[]} */ - const statements = memoizer.sync.map((memo) => - b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) - ); + const statements = memoizer.deriveds(); const props_expression = spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads); @@ -76,17 +73,16 @@ export function SlotElement(node, context) { b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback)) ); - if (memoizer.async.length > 0) { + const async_values = memoizer.async_values(); + + if (async_values) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), - b.arrow( - [context.state.node, ...memoizer.async.map((memo) => memo.id)], - b.block(statements) - ) + async_values, + b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements)) ) ) ); 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 77535cf16e22..582b57a7ada1 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 @@ -8,7 +8,6 @@ import { build_bind_this, Memoizer, 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 @@ -447,12 +446,7 @@ export function build_component(node, component_name, context) { }; } - const statements = [ - ...snippet_declarations, - ...memoizer.sync.map((memo) => - b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) - ) - ]; + const statements = [...snippet_declarations, ...memoizer.deriveds()]; if (is_component_dynamic) { const prev = fn; @@ -501,13 +495,15 @@ export function build_component(node, component_name, context) { memoizer.apply(); - if (memoizer.async.length > 0) { + const async_values = memoizer.async_values(); + + if (async_values) { return b.stmt( b.call( '$.async', anchor, - b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), - b.arrow([b.id('$$anchor'), ...memoizer.async.map(({ id }) => id)], b.block(statements)) + async_values, + b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements)) ) ); } 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 8da489409bc5..d63658b48183 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 @@ -78,21 +78,16 @@ export function build_attribute_effect( ); } - const all = memoizer.apply(); + memoizer.apply(); context.state.init.push( b.stmt( b.call( '$.attribute_effect', element_id, - b.arrow( - all.map(({ id }) => id), - b.object(values) - ), - memoizer.sync.length > 0 && - b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), - memoizer.async.length > 0 && - b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), + b.arrow(memoizer.all_ids(), b.object(values)), + memoizer.sync_values(), + memoizer.async_values(), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), 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 ed9b8ad8a4a9..f032f49a4816 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 @@ -12,10 +12,10 @@ import { build_getter } from '../../utils.js'; export class Memoizer { /** @type {Array<{ id: Identifier, expression: Expression }>} */ - sync = []; + #sync = []; /** @type {Array<{ id: Identifier, expression: Expression }>} */ - async = []; + #async = []; /** * @param {Expression} expression @@ -24,19 +24,39 @@ export class Memoizer { add(expression, has_await) { const id = b.id(`#`); // filled in later - (has_await ? this.async : this.sync).push({ id, expression }); + (has_await ? this.#async : this.#sync).push({ id, expression }); return id; } apply() { - const all = [...this.async, ...this.sync]; - - all.forEach((memo, i) => { + [...this.#async, ...this.#sync].forEach((memo, i) => { memo.id.name = `$${i}`; }); + } + + all_ids() { + return [...this.#async, ...this.#sync].map((memo) => memo.id); + } + + async_ids() { + return this.#async.map((memo) => memo.id); + } + + async_values() { + if (this.#async.length === 0) return; + return b.array(this.#async.map((memo) => b.thunk(memo.expression, true))); + } + + deriveds(runes = true) { + return this.#sync.map((memo) => + b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression))) + ); + } - return all; + sync_values() { + if (this.#sync.length === 0) return; + return b.array(this.#sync.map((memo) => b.thunk(memo.expression))); } } @@ -143,20 +163,19 @@ export function build_template_chunk( export function build_render_statement(state) { const { memoizer } = state; - const all = state.memoizer.apply(); + state.memoizer.apply(); return b.stmt( b.call( '$.template_effect', b.arrow( - all.map(({ id }) => id), + memoizer.all_ids(), state.update.length === 1 && state.update[0].type === 'ExpressionStatement' ? state.update[0].expression : b.block(state.update) ), - all.length > 0 && b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), - memoizer.async.length > 0 && - b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))) + memoizer.sync_values(), + memoizer.async_values() ) ); } From 3eee9d5f9d19d30b2580d258e44dcf110f2dd872 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 26 Jun 2025 21:59:38 -0400 Subject: [PATCH 446/582] Revert "dry out" This reverts commit 25855163bf7d915492f62d3d0e5370ed74e65132. --- .../3-transform/client/visitors/RenderTag.js | 16 ++++--- .../client/visitors/SlotElement.js | 16 ++++--- .../client/visitors/shared/component.js | 16 ++++--- .../client/visitors/shared/element.js | 13 ++++-- .../client/visitors/shared/utils.js | 43 ++++++------------- 5 files changed, 51 insertions(+), 53 deletions(-) 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 528f867e01e4..5255693fe36d 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 @@ -3,6 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; +import { create_derived } from '../utils.js'; import { build_expression, Memoizer } from './shared/utils.js'; /** @@ -35,7 +36,9 @@ export function RenderTag(node, context) { memoizer.apply(); /** @type {Statement[]} */ - const statements = memoizer.deriveds(); + const statements = memoizer.sync.map((memo) => + b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ); let snippet_function = build_expression( context, @@ -64,16 +67,17 @@ export function RenderTag(node, context) { ); } - const async_values = memoizer.async_values(); - - if (async_values) { + if (memoizer.async.length > 0) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - async_values, - b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements)) + b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), + b.arrow( + [context.state.node, ...memoizer.async.map((memo) => memo.id)], + b.block(statements) + ) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index fce445c6267c..70de454c0e94 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { create_derived } from '../utils.js'; import { build_attribute_value } from './shared/element.js'; import { Memoizer } from './shared/utils.js'; @@ -59,7 +60,9 @@ export function SlotElement(node, context) { context.state.init.push(...lets); /** @type {Statement[]} */ - const statements = memoizer.deriveds(); + const statements = memoizer.sync.map((memo) => + b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ); const props_expression = spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads); @@ -73,16 +76,17 @@ export function SlotElement(node, context) { b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback)) ); - const async_values = memoizer.async_values(); - - if (async_values) { + if (memoizer.async.length > 0) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - async_values, - b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements)) + b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), + b.arrow( + [context.state.node, ...memoizer.async.map((memo) => memo.id)], + b.block(statements) + ) ) ) ); 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 582b57a7ada1..77535cf16e22 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 @@ -8,6 +8,7 @@ import { build_bind_this, Memoizer, 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 @@ -446,7 +447,12 @@ export function build_component(node, component_name, context) { }; } - const statements = [...snippet_declarations, ...memoizer.deriveds()]; + const statements = [ + ...snippet_declarations, + ...memoizer.sync.map((memo) => + b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ) + ]; if (is_component_dynamic) { const prev = fn; @@ -495,15 +501,13 @@ export function build_component(node, component_name, context) { memoizer.apply(); - const async_values = memoizer.async_values(); - - if (async_values) { + if (memoizer.async.length > 0) { return b.stmt( b.call( '$.async', anchor, - async_values, - b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements)) + b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), + b.arrow([b.id('$$anchor'), ...memoizer.async.map(({ id }) => id)], b.block(statements)) ) ); } 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 d63658b48183..8da489409bc5 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 @@ -78,16 +78,21 @@ export function build_attribute_effect( ); } - memoizer.apply(); + const all = memoizer.apply(); context.state.init.push( b.stmt( b.call( '$.attribute_effect', element_id, - b.arrow(memoizer.all_ids(), b.object(values)), - memoizer.sync_values(), - memoizer.async_values(), + b.arrow( + all.map(({ id }) => id), + b.object(values) + ), + memoizer.sync.length > 0 && + b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), + memoizer.async.length > 0 && + b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), 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 f032f49a4816..ed9b8ad8a4a9 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 @@ -12,10 +12,10 @@ import { build_getter } from '../../utils.js'; export class Memoizer { /** @type {Array<{ id: Identifier, expression: Expression }>} */ - #sync = []; + sync = []; /** @type {Array<{ id: Identifier, expression: Expression }>} */ - #async = []; + async = []; /** * @param {Expression} expression @@ -24,39 +24,19 @@ export class Memoizer { add(expression, has_await) { const id = b.id(`#`); // filled in later - (has_await ? this.#async : this.#sync).push({ id, expression }); + (has_await ? this.async : this.sync).push({ id, expression }); return id; } apply() { - [...this.#async, ...this.#sync].forEach((memo, i) => { + const all = [...this.async, ...this.sync]; + + all.forEach((memo, i) => { memo.id.name = `$${i}`; }); - } - - all_ids() { - return [...this.#async, ...this.#sync].map((memo) => memo.id); - } - - async_ids() { - return this.#async.map((memo) => memo.id); - } - - async_values() { - if (this.#async.length === 0) return; - return b.array(this.#async.map((memo) => b.thunk(memo.expression, true))); - } - - deriveds(runes = true) { - return this.#sync.map((memo) => - b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression))) - ); - } - sync_values() { - if (this.#sync.length === 0) return; - return b.array(this.#sync.map((memo) => b.thunk(memo.expression))); + return all; } } @@ -163,19 +143,20 @@ export function build_template_chunk( export function build_render_statement(state) { const { memoizer } = state; - state.memoizer.apply(); + const all = state.memoizer.apply(); return b.stmt( b.call( '$.template_effect', b.arrow( - memoizer.all_ids(), + all.map(({ id }) => id), state.update.length === 1 && state.update[0].type === 'ExpressionStatement' ? state.update[0].expression : b.block(state.update) ), - memoizer.sync_values(), - memoizer.async_values() + all.length > 0 && b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), + memoizer.async.length > 0 && + b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))) ) ); } From 7d6b60391ef83061cdfed287bdd63f6775477b60 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 09:43:48 -0400 Subject: [PATCH 447/582] dry out --- .../phases/3-transform/client/visitors/RenderTag.js | 6 ++++-- .../phases/3-transform/client/visitors/SlotElement.js | 6 ++++-- .../3-transform/client/visitors/shared/component.js | 6 ++++-- .../phases/3-transform/client/visitors/shared/element.js | 3 +-- .../phases/3-transform/client/visitors/shared/utils.js | 8 ++++++-- 5 files changed, 19 insertions(+), 10 deletions(-) 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 5255693fe36d..83604380ca6a 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 @@ -67,13 +67,15 @@ export function RenderTag(node, context) { ); } - if (memoizer.async.length > 0) { + const async_values = memoizer.async_values(); + + if (async_values) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), + memoizer.async_values(), b.arrow( [context.state.node, ...memoizer.async.map((memo) => memo.id)], b.block(statements) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index 70de454c0e94..1f5a13ffaf09 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -76,13 +76,15 @@ export function SlotElement(node, context) { b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback)) ); - if (memoizer.async.length > 0) { + const async_values = memoizer.async_values(); + + if (async_values) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - b.array(memoizer.async.map((memo) => b.thunk(memo.expression, true))), + async_values, b.arrow( [context.state.node, ...memoizer.async.map((memo) => memo.id)], b.block(statements) 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 77535cf16e22..96a9b776b601 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 @@ -501,12 +501,14 @@ export function build_component(node, component_name, context) { memoizer.apply(); - if (memoizer.async.length > 0) { + const async_values = memoizer.async_values(); + + if (async_values) { return b.stmt( b.call( '$.async', anchor, - b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), + async_values, b.arrow([b.id('$$anchor'), ...memoizer.async.map(({ id }) => id)], b.block(statements)) ) ); 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 8da489409bc5..40c19c2b643c 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 @@ -91,8 +91,7 @@ export function build_attribute_effect( ), memoizer.sync.length > 0 && b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), - memoizer.async.length > 0 && - b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))), + memoizer.async_values(), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), 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 ed9b8ad8a4a9..45b368034cfe 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 @@ -38,6 +38,11 @@ export class Memoizer { return all; } + + async_values() { + if (this.async.length === 0) return; + return b.array(this.async.map((memo) => b.thunk(memo.expression, true))); + } } /** @@ -155,8 +160,7 @@ export function build_render_statement(state) { : b.block(state.update) ), all.length > 0 && b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), - memoizer.async.length > 0 && - b.array(memoizer.async.map(({ expression }) => b.thunk(expression, true))) + memoizer.async_values() ) ); } From 7bbb64060a64837db02ee999ed45eb225512dc19 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 09:45:15 -0400 Subject: [PATCH 448/582] dry out --- .../compiler/phases/3-transform/client/visitors/RenderTag.js | 5 +---- .../phases/3-transform/client/visitors/SlotElement.js | 5 +---- .../phases/3-transform/client/visitors/shared/component.js | 2 +- .../phases/3-transform/client/visitors/shared/utils.js | 4 ++++ 4 files changed, 7 insertions(+), 9 deletions(-) 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 83604380ca6a..623311955185 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 @@ -76,10 +76,7 @@ export function RenderTag(node, context) { '$.async', context.state.node, memoizer.async_values(), - b.arrow( - [context.state.node, ...memoizer.async.map((memo) => memo.id)], - b.block(statements) - ) + b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements)) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index 1f5a13ffaf09..fcc4952fd7fd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -85,10 +85,7 @@ export function SlotElement(node, context) { '$.async', context.state.node, async_values, - b.arrow( - [context.state.node, ...memoizer.async.map((memo) => memo.id)], - b.block(statements) - ) + b.arrow([context.state.node, ...memoizer.async_ids()], b.block(statements)) ) ) ); 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 96a9b776b601..24ce0c1996c3 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 @@ -509,7 +509,7 @@ export function build_component(node, component_name, context) { '$.async', anchor, async_values, - b.arrow([b.id('$$anchor'), ...memoizer.async.map(({ id }) => id)], b.block(statements)) + b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements)) ) ); } 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 45b368034cfe..c82d2d6438fc 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 @@ -39,6 +39,10 @@ export class Memoizer { return all; } + async_ids() { + return this.async.map((memo) => memo.id); + } + async_values() { if (this.async.length === 0) return; return b.array(this.async.map((memo) => b.thunk(memo.expression, true))); From a1a289eccaa35c19a65f538e3d929ae1a126fb7c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 09:46:20 -0400 Subject: [PATCH 449/582] use let for block-scoped stuff --- .../compiler/phases/3-transform/client/visitors/RenderTag.js | 2 +- .../compiler/phases/3-transform/client/visitors/SlotElement.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 623311955185..1e30fe6e8083 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 @@ -37,7 +37,7 @@ export function RenderTag(node, context) { /** @type {Statement[]} */ const statements = memoizer.sync.map((memo) => - b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) + b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) ); let snippet_function = build_expression( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index fcc4952fd7fd..bd2edb3e92b6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -61,7 +61,7 @@ export function SlotElement(node, context) { /** @type {Statement[]} */ const statements = memoizer.sync.map((memo) => - b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) + b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) ); const props_expression = From 97f81102bd62afe4a329dd776d3c114f7ae9f5a9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 09:48:28 -0400 Subject: [PATCH 450/582] dry out --- .../phases/3-transform/client/visitors/RenderTag.js | 4 +--- .../phases/3-transform/client/visitors/SlotElement.js | 4 +--- .../phases/3-transform/client/visitors/shared/component.js | 7 +------ .../phases/3-transform/client/visitors/shared/utils.js | 6 ++++++ 4 files changed, 9 insertions(+), 12 deletions(-) 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 1e30fe6e8083..95aa888081d7 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 @@ -36,9 +36,7 @@ export function RenderTag(node, context) { memoizer.apply(); /** @type {Statement[]} */ - const statements = memoizer.sync.map((memo) => - b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) - ); + const statements = memoizer.deriveds(context.state.analysis.runes); let snippet_function = build_expression( context, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index bd2edb3e92b6..992891c2fa6e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -60,9 +60,7 @@ export function SlotElement(node, context) { context.state.init.push(...lets); /** @type {Statement[]} */ - const statements = memoizer.sync.map((memo) => - b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) - ); + const statements = memoizer.deriveds(context.state.analysis.runes); const props_expression = spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads); 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 24ce0c1996c3..5c12d06755c2 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 @@ -447,12 +447,7 @@ export function build_component(node, component_name, context) { }; } - const statements = [ - ...snippet_declarations, - ...memoizer.sync.map((memo) => - b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) - ) - ]; + const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)]; if (is_component_dynamic) { const prev = fn; 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 c82d2d6438fc..d2f4a959c143 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 @@ -39,6 +39,12 @@ export class Memoizer { return all; } + deriveds(runes = true) { + return this.sync.map((memo) => + b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression))) + ); + } + async_ids() { return this.async.map((memo) => memo.id); } From 53137a133651f79edb6a34aea14ac868c270ca38 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 09:49:51 -0400 Subject: [PATCH 451/582] dry out --- .../phases/3-transform/client/visitors/shared/element.js | 3 +-- .../phases/3-transform/client/visitors/shared/utils.js | 7 ++++++- 2 files changed, 7 insertions(+), 3 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 40c19c2b643c..5b5dc73da106 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 @@ -89,8 +89,7 @@ export function build_attribute_effect( all.map(({ id }) => id), b.object(values) ), - memoizer.sync.length > 0 && - b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), + memoizer.sync_values(), memoizer.async_values(), element.metadata.scoped && context.state.analysis.css.hash !== '' && 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 d2f4a959c143..ebd864ca4a41 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 @@ -53,6 +53,11 @@ export class Memoizer { if (this.async.length === 0) return; return b.array(this.async.map((memo) => b.thunk(memo.expression, true))); } + + sync_values() { + if (this.sync.length === 0) return; + return b.array(this.sync.map((memo) => b.thunk(memo.expression))); + } } /** @@ -169,7 +174,7 @@ export function build_render_statement(state) { ? state.update[0].expression : b.block(state.update) ), - all.length > 0 && b.array(memoizer.sync.map(({ expression }) => b.thunk(expression))), + memoizer.sync_values(), memoizer.async_values() ) ); From b3bcdaf33352c19b75b2be6ab87e043a768ba695 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 09:52:02 -0400 Subject: [PATCH 452/582] tidy up --- .../client/visitors/shared/element.js | 7 ++--- .../client/visitors/shared/utils.js | 31 +++++++++---------- 2 files changed, 16 insertions(+), 22 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 5b5dc73da106..9143a570255b 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 @@ -78,17 +78,14 @@ export function build_attribute_effect( ); } - const all = memoizer.apply(); + const ids = memoizer.apply(); context.state.init.push( b.stmt( b.call( '$.attribute_effect', element_id, - b.arrow( - all.map(({ id }) => id), - b.object(values) - ), + b.arrow(ids, b.object(values)), memoizer.sync_values(), memoizer.async_values(), element.metadata.scoped && 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 ebd864ca4a41..4716ae1a4ce7 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 @@ -12,51 +12,48 @@ import { build_getter } from '../../utils.js'; export class Memoizer { /** @type {Array<{ id: Identifier, expression: Expression }>} */ - sync = []; + #sync = []; /** @type {Array<{ id: Identifier, expression: Expression }>} */ - async = []; + #async = []; /** * @param {Expression} expression * @param {boolean} has_await */ add(expression, has_await) { - const id = b.id(`#`); // filled in later + const id = b.id('#'); // filled in later - (has_await ? this.async : this.sync).push({ id, expression }); + (has_await ? this.#async : this.#sync).push({ id, expression }); return id; } apply() { - const all = [...this.async, ...this.sync]; - - all.forEach((memo, i) => { + return [...this.#async, ...this.#sync].map((memo, i) => { memo.id.name = `$${i}`; + return memo.id; }); - - return all; } deriveds(runes = true) { - return this.sync.map((memo) => + return this.#sync.map((memo) => b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression))) ); } async_ids() { - return this.async.map((memo) => memo.id); + return this.#async.map((memo) => memo.id); } async_values() { - if (this.async.length === 0) return; - return b.array(this.async.map((memo) => b.thunk(memo.expression, true))); + if (this.#async.length === 0) return; + return b.array(this.#async.map((memo) => b.thunk(memo.expression, true))); } sync_values() { - if (this.sync.length === 0) return; - return b.array(this.sync.map((memo) => b.thunk(memo.expression))); + if (this.#sync.length === 0) return; + return b.array(this.#sync.map((memo) => b.thunk(memo.expression))); } } @@ -163,13 +160,13 @@ export function build_template_chunk( export function build_render_statement(state) { const { memoizer } = state; - const all = state.memoizer.apply(); + const ids = state.memoizer.apply(); return b.stmt( b.call( '$.template_effect', b.arrow( - all.map(({ id }) => id), + ids, state.update.length === 1 && state.update[0].type === 'ExpressionStatement' ? state.update[0].expression : b.block(state.update) From aa14305d3b29634d07301e7b66dc8b9536834f6c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 17:12:44 -0400 Subject: [PATCH 453/582] only wrap awaits in `$.save` when necessary --- .../src/compiler/phases/2-analyze/index.js | 20 +++++- .../compiler/phases/2-analyze/utils/awaits.js | 70 +++++++++++++++++++ .../2-analyze/visitors/AwaitExpression.js | 27 ++----- packages/svelte/src/compiler/phases/nodes.js | 3 +- packages/svelte/src/compiler/types/index.d.ts | 3 + .../svelte/src/compiler/utils/builders.js | 15 +++- 6 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index e0140342f2f7..bf8a9f4736e8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,5 +1,5 @@ /** @import { Expression, Node, Program } from 'estree' */ -/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ +/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions, ExpressionMetadata } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ import { walk } from 'zimmerframe'; @@ -76,6 +76,10 @@ import { UseDirective } from './visitors/UseDirective.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import is_reference from 'is-reference'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; +import { find_last_await, is_last_evaluated_expression } from './utils/awaits.js'; + +/** @type {Array} */ +const metadata_stack = []; /** * @type {Visitors} @@ -127,9 +131,23 @@ const visitors = { ignore_map.set(node, structuredClone(ignore_stack)); + metadata_stack.push(state.expression); + const scope = state.scopes.get(node); next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state); + metadata_stack.pop(); + + // if this node set `state.expression`, now that we've visited it we can determine + // which `await` expressions need to be wrapped in `$.save(...)` + if (state.expression && metadata_stack[metadata_stack.length - 1] === null) { + for (const { path, node } of state.expression.awaits) { + if (!is_last_evaluated_expression(path, node)) { + state.analysis.context_preserving_awaits.add(node); + } + } + } + if (ignores.length > 0) { pop_ignore(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js b/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js new file mode 100644 index 000000000000..c8923056513b --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js @@ -0,0 +1,70 @@ +/** @import { Expression, Property, SpreadElement } from 'estree' */ +/** @import { AST } from '#compiler' */ + +/** + * + * @param {AST.SvelteNode[]} path + * @param {Expression | SpreadElement | Property} node + */ +export function is_last_evaluated_expression(path, node) { + let i = path.length; + + while (i--) { + const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]); + + // @ts-expect-error we could probably use a neater/more robust mechanism + if (parent.metadata) { + return true; + } + + switch (parent.type) { + case 'ArrayExpression': + if (node !== parent.elements.at(-1)) return false; + break; + + case 'AssignmentExpression': + case 'BinaryExpression': + case 'LogicalExpression': + if (node === parent.left) return false; // TODO is this right for assignment expressions? + break; + + case 'CallExpression': + case 'NewExpression': + if (node !== parent.arguments.at(-1)) return false; + break; + + case 'ConditionalExpression': + if (node === parent.test) return false; + break; + + case 'MemberExpression': + if (parent.computed && node === parent.object) return false; + break; + + case 'ObjectExpression': + if (node !== parent.properties.at(-1)) return false; + break; + + case 'Property': + if (node === parent.key) return false; + break; + + case 'SequenceExpression': + if (node !== parent.expressions.at(-1)) return false; + break; + + case 'TaggedTemplateExpression': + if (node !== parent.quasi.expressions.at(-1)) return false; + break; + + case 'TemplateLiteral': + if (node !== parent.expressions.at(-1)) return false; + break; + + default: + return false; + } + + node = parent; + } +} 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 4f50d447f7d6..bd96e99d884a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -8,28 +8,17 @@ import * as e from '../../../errors.js'; */ export function AwaitExpression(node, context) { const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; + + if (tla) { + context.state.analysis.context_preserving_awaits.add(node); + } + let suspend = tla; - let preserve_context = tla; if (context.state.expression) { + context.state.expression.awaits.push({ node, path: context.path.slice() }); context.state.expression.has_await = true; suspend = true; - - // wrap the expression in `(await $.save(...)).restore()` 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]; - - // 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 - preserve_context = true; - } } if (suspend) { @@ -42,9 +31,5 @@ export function AwaitExpression(node, context) { } } - if (preserve_context) { - context.state.analysis.context_preserving_awaits.add(node); - } - context.next(); } diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 4874554ff0fb..7f3cc67fb43f 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -67,7 +67,8 @@ export function create_expression_metadata() { has_call: false, has_member_expression: false, has_assignment: false, - has_await: false + has_await: false, + awaits: [] }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index c4f41b724ac2..b9d13fde7d85 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -5,6 +5,7 @@ import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js'; import type { StateCreationRuneName } from '../../utils.js'; import type { AssignmentExpression, + AwaitExpression, CallExpression, PrivateIdentifier, PropertyDefinition @@ -298,6 +299,8 @@ export interface ExpressionMetadata { has_member_expression: boolean; /** True if the expression includes an assignment or an update */ has_assignment: boolean; + /** An array of await expressions, so we can figure out which ones need context preservation */ + awaits: Array<{ node: AwaitExpression; path: AST.SvelteNode[] }>; } export interface StateField { diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 931b11e2ba64..b1da7946fecf 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -1,4 +1,5 @@ /** @import * as ESTree from 'estree' */ +import { walk } from 'zimmerframe'; import { regex_is_valid_identifier } from '../phases/patterns.js'; import { sanitize_template_string } from './sanitize_template_string.js'; @@ -432,8 +433,20 @@ export function thunk(expression, async = false) { * @returns {ESTree.Expression} */ export function unthunk(expression) { + // optimize `async () => await x()`, but not `async () => await x(await y)` if (expression.async && expression.body.type === 'AwaitExpression') { - return unthunk(arrow(expression.params, expression.body.argument)); + let has_await = false; + + walk(expression.body.argument, null, { + AwaitExpression(node, context) { + has_await = true; + context.stop(); + } + }); + + if (!has_await) { + return unthunk(arrow(expression.params, expression.body.argument)); + } } if ( From 1c66278bc9920275830ac21c5554cbfa4024bcb5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 27 Jun 2025 17:13:54 -0400 Subject: [PATCH 454/582] oops --- packages/svelte/src/compiler/phases/2-analyze/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index bf8a9f4736e8..09ce8b7d24be 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -76,7 +76,7 @@ import { UseDirective } from './visitors/UseDirective.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import is_reference from 'is-reference'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; -import { find_last_await, is_last_evaluated_expression } from './utils/awaits.js'; +import { is_last_evaluated_expression } from './utils/awaits.js'; /** @type {Array} */ const metadata_stack = []; From 4d05b1bbb55bcb975f356bf7c204e3f17e79b087 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 16:03:13 -0400 Subject: [PATCH 455/582] remove TODO comment (just checked) --- packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js b/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js index c8923056513b..e9e5cdc8baae 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js +++ b/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js @@ -25,7 +25,7 @@ export function is_last_evaluated_expression(path, node) { case 'AssignmentExpression': case 'BinaryExpression': case 'LogicalExpression': - if (node === parent.left) return false; // TODO is this right for assignment expressions? + if (node === parent.left) return false; break; case 'CallExpression': From 73fb7a1904a3f48b110490cb858fc38c024004e4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 16:26:37 -0400 Subject: [PATCH 456/582] oops, leftover --- packages/svelte/src/internal/client/runtime.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 1663f2b24f6f..40a0f35e9f33 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -306,11 +306,6 @@ export function update_reaction(reaction) { reaction.ac = null; } - if (reaction.ac !== null) { - reaction.ac.abort(STALE_REACTION); - reaction.ac = null; - } - try { reaction.f |= REACTION_IS_UPDATING; var result = /** @type {Function} */ (0, reaction.fn)(); From 80de2ca1ef87b065c4ae9f70f57b319e124001c9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 16:29:14 -0400 Subject: [PATCH 457/582] simplify --- .../src/compiler/phases/2-analyze/index.js | 22 +----- .../compiler/phases/2-analyze/utils/awaits.js | 70 ----------------- .../2-analyze/visitors/AwaitExpression.js | 11 +-- .../3-transform/client/transform-client.js | 3 +- .../phases/3-transform/client/types.d.ts | 4 +- .../client/visitors/AwaitExpression.js | 78 ++++++++++++++++++- packages/svelte/src/compiler/phases/nodes.js | 3 +- .../svelte/src/compiler/phases/types.d.ts | 3 - packages/svelte/src/compiler/types/index.d.ts | 2 - 9 files changed, 86 insertions(+), 110 deletions(-) delete mode 100644 packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index bc079ac6185d..404cd1a537ca 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -77,10 +77,6 @@ import { UseDirective } from './visitors/UseDirective.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import is_reference from 'is-reference'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; -import { is_last_evaluated_expression } from './utils/awaits.js'; - -/** @type {Array} */ -const metadata_stack = []; /** * @type {Visitors} @@ -132,23 +128,9 @@ const visitors = { ignore_map.set(node, structuredClone(ignore_stack)); - metadata_stack.push(state.expression); - const scope = state.scopes.get(node); next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state); - metadata_stack.pop(); - - // if this node set `state.expression`, now that we've visited it we can determine - // which `await` expressions need to be wrapped in `$.save(...)` - if (state.expression && metadata_stack[metadata_stack.length - 1] === null) { - for (const { path, node } of state.expression.awaits) { - if (!is_last_evaluated_expression(path, node)) { - state.analysis.context_preserving_awaits.add(node); - } - } - } - if (ignores.length > 0) { pop_ignore(); } @@ -291,7 +273,6 @@ export function analyze_module(source, options) { immutable: true, tracing: false, async_deriveds: new Set(), - context_preserving_awaits: new Set(), comments, classes: new Map() }; @@ -538,8 +519,7 @@ export function analyze_component(root, source, options) { undefined_exports: new Map(), snippet_renderers: new Map(), snippets: new Set(), - async_deriveds: new Set(), - context_preserving_awaits: new Set() + async_deriveds: new Set() }; if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js b/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js deleted file mode 100644 index e9e5cdc8baae..000000000000 --- a/packages/svelte/src/compiler/phases/2-analyze/utils/awaits.js +++ /dev/null @@ -1,70 +0,0 @@ -/** @import { Expression, Property, SpreadElement } from 'estree' */ -/** @import { AST } from '#compiler' */ - -/** - * - * @param {AST.SvelteNode[]} path - * @param {Expression | SpreadElement | Property} node - */ -export function is_last_evaluated_expression(path, node) { - let i = path.length; - - while (i--) { - const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]); - - // @ts-expect-error we could probably use a neater/more robust mechanism - if (parent.metadata) { - return true; - } - - switch (parent.type) { - case 'ArrayExpression': - if (node !== parent.elements.at(-1)) return false; - break; - - case 'AssignmentExpression': - case 'BinaryExpression': - case 'LogicalExpression': - if (node === parent.left) return false; - break; - - case 'CallExpression': - case 'NewExpression': - if (node !== parent.arguments.at(-1)) return false; - break; - - case 'ConditionalExpression': - if (node === parent.test) return false; - break; - - case 'MemberExpression': - if (parent.computed && node === parent.object) return false; - break; - - case 'ObjectExpression': - if (node !== parent.properties.at(-1)) return false; - break; - - case 'Property': - if (node === parent.key) return false; - break; - - case 'SequenceExpression': - if (node !== parent.expressions.at(-1)) return false; - break; - - case 'TaggedTemplateExpression': - if (node !== parent.quasi.expressions.at(-1)) return false; - break; - - case 'TemplateLiteral': - if (node !== parent.expressions.at(-1)) return false; - break; - - default: - return false; - } - - node = parent; - } -} 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 bd96e99d884a..af7d0307e9dc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -7,20 +7,15 @@ import * as e from '../../../errors.js'; * @param {Context} context */ export function AwaitExpression(node, context) { - const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; - - if (tla) { - context.state.analysis.context_preserving_awaits.add(node); - } - - let suspend = tla; + let suspend = context.state.ast_type === 'instance' && context.state.function_depth === 1; if (context.state.expression) { - context.state.expression.awaits.push({ node, path: context.path.slice() }); context.state.expression.has_await = true; suspend = true; } + // disallow top-level `await` or `await` in template expressions + // unless a) in runes mode and b) opted into `experimental.async` if (suspend) { if (!context.state.options.experimental.async) { e.experimental_async(node); 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 dd093ca4288b..a424d9c65c7e 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 @@ -702,7 +702,8 @@ export function client_module(analysis, options) { scopes: analysis.module.scopes, state_fields: new Map(), transform: {}, - in_constructor: false + in_constructor: false, + is_instance: false }; const module = /** @type {ESTree.Program} */ ( 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 cf5c942268cc..4b099eed52d9 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 @@ -21,6 +21,9 @@ export interface ClientTransformState extends TransformState { */ readonly in_constructor: boolean; + /** `true` if we're transforming the contents of ` + + + + + - {#each await promise as item (item)} -

      {item}

      - {/each} +
      + {#each await deferred.promise as item (item)} +

      {item}

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

      pending

      From 2629561a9c599a631306d0784a42218d51ee59ab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 3 Jul 2025 11:40:44 -0400 Subject: [PATCH 495/582] failing test --- .../runtime-runes/samples/async-reactivity-loss/_config.js | 2 +- .../runtime-runes/samples/async-reactivity-loss/main.svelte | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 index 934accd9b3b3..f8a7cfd479af 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -10,7 +10,7 @@ export default test({ async test({ assert, target, warnings }) { await tick(); - assert.htmlEqual(target.innerHTML, '

      3

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

      3

      3

      '); assert.equal( warnings[0], 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 index 488fc25f324d..bdb1b095c9bc 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -12,6 +12,7 @@

      {await a_plus_b()}

      +

      {await a + await b}

      {#snippet pending()}

      pending

      From 9c13fef7e50639c8c628168ffb2e4265f41d21ab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 3 Jul 2025 13:08:19 -0400 Subject: [PATCH 496/582] unused --- .../phases/3-transform/client/visitors/AwaitExpression.js | 2 -- 1 file changed, 2 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 c15709e48dc9..59809a178ec2 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,9 +1,7 @@ -/** @import { AST } from '#compiler' */ /** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */ /** @import { Context } from '../types' */ import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; -import { get_rune } from '../../../scope.js'; /** * @param {AwaitExpression} node From 70c9f4be08bad0a2ca1bda1200df13cd0c631f3e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 3 Jul 2025 14:18:36 -0400 Subject: [PATCH 497/582] fix test --- .../tests/runtime-runes/samples/async-each-keyed/_config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js index bef2b1546fa7..0d13cbc3cbc3 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js @@ -32,6 +32,7 @@ export default test({ assert.htmlEqual(div.innerHTML, '

      d

      e

      f

      g

      '); reset.click(); + await tick(); three.click(); await tick(); assert.fail('should not allow duplicate keys'); From f043519924d221e1b36e650cf0432ceefe636ef9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 3 Jul 2025 14:36:43 -0400 Subject: [PATCH 498/582] fix --- eslint.config.js | 3 +- .../client/visitors/AwaitExpression.js | 30 ++++++++++++------- .../internal/client/reactivity/deriveds.js | 8 ++++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index d7044fc9f1ec..41d98fa428ff 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -49,12 +49,13 @@ export default [ }, rules: { '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/prefer-promise-reject-errors': 'error', '@typescript-eslint/require-await': 'error', 'no-console': 'error', 'lube/svelte-naming-convention': ['error', { fixSameNames: true }], // eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off 'object-shorthand': 'off', + // eslint is being a dummy here too + '@typescript-eslint/prefer-promise-reject-errors': 'off', 'no-var': 'off', // TODO: enable these rules and run `pnpm lint:fix` 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 59809a178ec2..e03c35c8a251 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,6 +1,6 @@ /** @import { AwaitExpression, Expression, Property, SpreadElement } from 'estree' */ /** @import { Context } from '../types' */ -import { dev } from '../../../../state.js'; +import { dev, is_ignored } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; /** @@ -8,18 +8,26 @@ import * as b from '../../../../utils/builders.js'; * @param {Context} context */ export function AwaitExpression(node, context) { - const save = - // preserve context if this is a top-level await in ` + +

      {value}

      diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js new file mode 100644 index 000000000000..73c9b50a6962 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js @@ -0,0 +1,40 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, hello] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + toggle.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + hello.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

      condition is true

      +

      hello

      + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte new file mode 100644 index 000000000000..d111ce6fe3db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte @@ -0,0 +1,20 @@ + + + + + + + {#if condition} +

      condition is {condition}

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

      pending

      + {/snippet} +
      From d39e60bf0ae58569b4c99f4d6ced21fdf161ad27 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 5 Jul 2025 16:10:38 -0400 Subject: [PATCH 514/582] rename from_async_derived -> current_async_derived --- .../svelte/src/internal/client/dom/blocks/boundary.js | 6 +++--- .../svelte/src/internal/client/reactivity/deriveds.js | 10 +++++----- packages/svelte/src/internal/client/runtime.js | 9 +++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 9b6c80706776..1b3ace58e512 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -22,7 +22,7 @@ import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import { DEV } from 'esm-env'; -import { from_async_derived, set_from_async_derived } from '../../reactivity/deriveds.js'; +import { current_async_effect, set_from_async_derived } from '../../reactivity/deriveds.js'; import { Batch } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; @@ -397,7 +397,7 @@ export function capture(track = true) { var previous_component_context = component_context; if (DEV && !track) { - var was_from_async_derived = from_async_derived; + var previous_async_effect = current_async_effect; } return function restore() { @@ -408,7 +408,7 @@ export function capture(track = true) { } if (DEV) { - set_from_async_derived(track ? null : was_from_async_derived); + set_from_async_derived(track ? null : previous_async_effect); } // prevent the active effect from outstaying its welcome diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index ef72751a8581..b51f14f7d863 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -36,11 +36,11 @@ import { UNINITIALIZED } from '../../../constants.js'; import { current_batch } from './batch.js'; /** @type {Effect | null} */ -export let from_async_derived = null; +export let current_async_effect = null; /** @param {Effect | null} v */ export function set_from_async_derived(v) { - from_async_derived = v; + current_async_effect = v; } export const recent_async_deriveds = new Set(); @@ -115,7 +115,7 @@ export function async_derived(fn, location) { var should_suspend = !active_reaction; render_effect(() => { - if (DEV) from_async_derived = active_effect; + if (DEV) current_async_effect = active_effect; try { var p = fn(); @@ -123,7 +123,7 @@ export function async_derived(fn, location) { p = Promise.reject(error); } - if (DEV) from_async_derived = null; + if (DEV) current_async_effect = null; promise = prev === null @@ -150,7 +150,7 @@ export function async_derived(fn, location) { const handler = (value, error = undefined) => { prev = null; - from_async_derived = null; + current_async_effect = null; if (!pending) batch.restore(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 061870daa2bf..5b4eaa80d5b7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -33,7 +33,7 @@ import { internal_set, old_values } from './reactivity/sources.js'; import { destroy_derived_effects, execute_derived, - from_async_derived, + current_async_effect, recent_async_deriveds, update_derived } from './reactivity/deriveds.js'; @@ -869,9 +869,10 @@ export function get(signal) { } 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 (current_async_effect) { + var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; + var was_read = + current_async_effect.deps !== null && current_async_effect.deps.includes(signal); if (!tracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label)); From 54e39252a59dbcaba074c2a51460fcd31cffe238 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 5 Jul 2025 16:14:22 -0400 Subject: [PATCH 515/582] tweak --- 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 5b4eaa80d5b7..75c6bda2dc0e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -871,8 +871,7 @@ export function get(signal) { if (DEV) { if (current_async_effect) { var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; - var was_read = - current_async_effect.deps !== null && current_async_effect.deps.includes(signal); + var was_read = current_async_effect.deps?.includes(signal); if (!tracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label)); From f991d9d4377c9d01b36871b4e94e22ac9626219a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 5 Jul 2025 16:20:45 -0400 Subject: [PATCH 516/582] remove TODO - this method is only called when pending snippet exists --- .../internal/client/dom/blocks/boundary.js | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 1b3ace58e512..69a5bd45ad0f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -196,22 +196,15 @@ export class Boundary { } #show_pending_snippet() { - const pending = this.#props.pending; + const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending); - if (pending !== undefined) { - // TODO can this be false? - if (this.#main_effect !== null) { - this.#offscreen_fragment = document.createDocumentFragment(); - move_effect(this.#main_effect, this.#offscreen_fragment); - } + if (this.#main_effect !== null) { + this.#offscreen_fragment = document.createDocumentFragment(); + move_effect(this.#main_effect, this.#offscreen_fragment); + } - if (this.#pending_effect === null) { - this.#pending_effect = branch(() => pending(this.#anchor)); - } - } else if (this.parent) { - throw new Error('TODO show pending snippet on parent'); - } else { - throw new Error('no pending snippet to show'); + if (this.#pending_effect === null) { + this.#pending_effect = branch(() => pending(this.#anchor)); } } From f0bb8ddd58dd96b269b32c01471c5bb430796b11 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 6 Jul 2025 07:45:23 -0400 Subject: [PATCH 517/582] use error boundary for test - vitest does some weird error swallowing afaict --- .../tests/runtime-runes/samples/async-each-keyed/_config.js | 5 ++--- .../tests/runtime-runes/samples/async-each-keyed/main.svelte | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js index 0d13cbc3cbc3..43d3a0f8760d 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js @@ -35,8 +35,7 @@ export default test({ await tick(); three.click(); await tick(); - assert.fail('should not allow duplicate keys'); - }, - runtime_error: 'each_key_duplicate' + assert.include(target.innerHTML, '

      each_key_duplicate'); + } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte index 081e17fbfcf2..e2f826378017 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte @@ -14,6 +14,10 @@ {/each}

    + {#snippet failed(e)} +

    {e.message}

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

    pending

    {/snippet} From 5e1ec58eff890c6aec65a8fb56ef37dbcbf9a2f7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 6 Jul 2025 07:58:17 -0400 Subject: [PATCH 518/582] flush less often --- packages/svelte/src/internal/client/reactivity/async.js | 2 +- packages/svelte/src/internal/client/reactivity/batch.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 2708d9139a4d..1240eee291eb 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -39,7 +39,7 @@ export function flatten(sync, async, fn) { invoke_error_boundary(error, parent); } - batch?.flush(); + batch?.deactivate(); }) .catch((error) => { boundary.error(error); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index c64f3ab98c66..a60399830d60 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -228,6 +228,10 @@ export class Batch { current_batch = this; } + deactivate() { + current_batch = null; + } + flush() { if (queued_root_effects.length > 0) { flush_queued_root_effects(); From aa5af759d9215640fec8e21461596698bfe840ca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 6 Jul 2025 07:58:36 -0400 Subject: [PATCH 519/582] restore -> activate --- packages/svelte/src/internal/client/reactivity/async.js | 2 +- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- packages/svelte/src/internal/client/reactivity/deriveds.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 1240eee291eb..c4ff5eebf86a 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -29,7 +29,7 @@ export function flatten(sync, async, fn) { .then((result) => { if ((parent.f & DESTROYED) !== 0) return; - batch?.restore(); + batch?.activate(); restore(); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a60399830d60..4e8015bde6c0 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -224,7 +224,7 @@ export class Batch { this.#current.set(source, source.v); } - restore() { + activate() { current_batch = this; } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b51f14f7d863..5253d00f562d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -152,7 +152,7 @@ export function async_derived(fn, location) { current_async_effect = null; - if (!pending) batch.restore(); + if (!pending) batch.activate(); if (error) { if (error !== STALE_REACTION) { From c83374c3caef253a507d50034ec689312d90c398 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 6 Jul 2025 08:08:36 -0400 Subject: [PATCH 520/582] remove TODO --- packages/svelte/src/internal/client/dom/blocks/async.js | 1 - 1 file changed, 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 2eac6c55e034..9e8a4ed0d314 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -11,7 +11,6 @@ import { get_pending_boundary } from './boundary.js'; export function async(node, expressions, fn) { var boundary = get_pending_boundary(); - // TODO why is this necessary? doesn't it happen inside `async_derived` inside `flatten`? boundary.update_pending_count(1); flatten([], expressions, (values) => { From ec21b741c2d240dac9726c83c52766b2d17a7576 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 6 Jul 2025 08:46:15 -0400 Subject: [PATCH 521/582] move batch-related code into batch.js --- packages/svelte/src/index-client.js | 2 +- .../src/internal/client/dom/blocks/await.js | 3 +- packages/svelte/src/internal/client/index.js | 3 +- .../src/internal/client/reactivity/batch.js | 270 +++++++++++++++++- .../src/internal/client/reactivity/effects.js | 3 +- .../src/internal/client/reactivity/sources.js | 3 +- .../svelte/src/internal/client/runtime.js | 265 +---------------- packages/svelte/src/legacy/legacy-client.js | 3 +- packages/svelte/tests/store/test.ts | 9 +- 9 files changed, 288 insertions(+), 273 deletions(-) diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 942a7b6bce08..464bef85b783 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -238,7 +238,7 @@ function init_update_callbacks(context) { return (l.u ??= { a: [], b: [], m: [] }); } -export { flushSync } from './internal/client/runtime.js'; +export { flushSync } from './internal/client/reactivity/batch.js'; export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack, settled } from './internal/client/runtime.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 325224fff237..4f68db57b1bb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -3,7 +3,7 @@ import { DEV } from 'esm-env'; import { is_promise } from '../../../shared/utils.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; -import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js'; +import { set_active_effect, set_active_reaction } from '../../runtime.js'; import { hydrate_next, hydrate_node, @@ -22,6 +22,7 @@ import { set_dev_current_component_function, set_dev_stack } from '../../context.js'; +import { flushSync } from '../../reactivity/batch.js'; const PENDING = 0; const THEN = 1; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 25ef7886040d..04bad60c762e 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -98,7 +98,7 @@ export { props_id, with_script } from './dom/template.js'; -export { suspend } from './reactivity/batch.js'; +export { flushSync as flush, suspend } from './reactivity/batch.js'; export { async_derived, user_derived as derived, @@ -142,7 +142,6 @@ export { get, safe_get, invalidate_inner_signals, - flushSync as flush, tick, untrack, exclude_from_object, diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 4e8015bde6c0..6ea395936bb4 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,17 +1,36 @@ /** @import { Derived, Effect, Source } from '#client' */ -import { CLEAN, DIRTY } from '#client/constants'; -import { deferred } from '../../shared/utils.js'; +import { + BLOCK_EFFECT, + BRANCH_EFFECT, + CLEAN, + DESTROYED, + DIRTY, + EFFECT, + EFFECT_ASYNC, + INERT, + RENDER_EFFECT, + ROOT_EFFECT +} from '#client/constants'; +import { async_mode_flag } from '../../flags/index.js'; +import { deferred, define_property } from '../../shared/utils.js'; import { get_pending_boundary } from '../dom/blocks/boundary.js'; import { - flush_queued_effects, - flush_queued_root_effects, - process_effects, + active_effect, + check_dirtiness, + dev_effect_stack, + is_updating_effect, queued_root_effects, - schedule_effect, + set_is_updating_effect, set_queued_root_effects, set_signal_status, update_effect } from '../runtime.js'; +import * as e from '../errors.js'; +import { flush_tasks } from '../dom/task.js'; +import { DEV } from 'esm-env'; +import { invoke_error_boundary } from '../error-handling.js'; +import { old_values } from './sources.js'; +import { unlink_effect } from './effects.js'; /** @type {Set} */ const batches = new Set(); @@ -22,6 +41,9 @@ export let current_batch = null; /** @type {Map | null} */ export let batch_deriveds = null; +/** @type {Effect | null} */ +let last_scheduled_effect = null; + /** TODO handy for debugging, but we should probably eventually delete it */ let uid = 1; @@ -329,6 +351,242 @@ export class Batch { } } +/** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * @template [T=void] + * @param {(() => T) | undefined} [fn] + * @returns {T} + */ +export function flushSync(fn) { + if (async_mode_flag && active_effect !== null) { + e.flush_sync_in_effect(); + } + + var result; + + const batch = Batch.ensure(); + + if (fn) { + flush_queued_root_effects(); + + result = fn(); + } + + while (true) { + flush_tasks(); + + if (queued_root_effects.length === 0) { + if (batch === current_batch) { + batch.flush(); + } + + // this would be reset in `flush_queued_root_effects` but since we are early returning here, + // we need to reset it here as well in case the first time there's 0 queued root effects + last_scheduled_effect = null; + + if (DEV) { + dev_effect_stack.length = 0; + } + + return /** @type {T} */ (result); + } + + flush_queued_root_effects(); + } +} + +function log_effect_stack() { + // eslint-disable-next-line no-console + console.error( + 'Last ten effects were: ', + dev_effect_stack.slice(-10).map((d) => d.fn) + ); + dev_effect_stack.length = 0; +} + +function infinite_loop_guard() { + try { + e.effect_update_depth_exceeded(); + } catch (error) { + if (DEV) { + // stack is garbage, ignore. Instead add a console.error message. + define_property(error, 'stack', { + value: '' + }); + } + // Try and handle the error so it can be caught at a boundary, that's + // if there's an effect available from when it was last scheduled + if (last_scheduled_effect !== null) { + if (DEV) { + try { + invoke_error_boundary(error, last_scheduled_effect); + } catch (e) { + // Only log the effect stack if the error is re-thrown + log_effect_stack(); + throw e; + } + } else { + invoke_error_boundary(error, last_scheduled_effect); + } + } else { + if (DEV) { + log_effect_stack(); + } + throw error; + } + } +} + +export function flush_queued_root_effects() { + var was_updating_effect = is_updating_effect; + var batch = /** @type {Batch} */ (current_batch); + + try { + var flush_count = 0; + set_is_updating_effect(true); + + while (queued_root_effects.length > 0) { + if (flush_count++ > 1000) { + infinite_loop_guard(); + } + + batch.process(queued_root_effects); + + old_values.clear(); + } + } finally { + set_is_updating_effect(was_updating_effect); + + last_scheduled_effect = null; + if (DEV) { + dev_effect_stack.length = 0; + } + } +} + +/** + * @param {Array} effects + * @returns {void} + */ +export function flush_queued_effects(effects) { + var length = effects.length; + if (length === 0) return; + + for (var i = 0; i < length; i++) { + var effect = effects[i]; + + if ((effect.f & (DESTROYED | INERT)) === 0) { + if (check_dirtiness(effect)) { + update_effect(effect); + + // Effects with no dependencies or teardown do not get added to the effect tree. + // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we + // don't know if we need to keep them until they are executed. Doing the check + // here (rather than in `update_effect`) allows us to skip the work for + // immediate effects. + if (effect.deps === null && effect.first === null && effect.nodes_start === null) { + if (effect.teardown === null) { + // remove this effect from the graph + unlink_effect(effect); + } else { + // keep the effect in the graph, but free up some memory + effect.fn = null; + } + } + } + } + } +} + +/** + * @param {Effect} signal + * @returns {void} + */ +export function schedule_effect(signal) { + var effect = (last_scheduled_effect = signal); + + while (effect.parent !== null) { + effect = effect.parent; + var flags = effect.f; + + if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { + if ((flags & CLEAN) === 0) return; + effect.f ^= CLEAN; + } + } + + queued_root_effects.push(effect); +} + +/** + * + * This function both runs render effects and collects user effects in topological order + * from the starting effect passed in. Effects will be collected when they match the filtered + * bitwise flag passed in only. The collected effects array will be populated with all the user + * effects to be flushed. + * + * @param {Batch} batch + * @param {Effect} root + */ +export function process_effects(batch, root) { + root.f ^= CLEAN; + + var effect = root.first; + + while (effect !== null) { + var flags = effect.f; + var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; + var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; + + var skip = is_skippable_branch || (flags & INERT) !== 0 || batch.skipped_effects.has(effect); + + if (!skip && effect.fn !== null) { + if ((flags & EFFECT_ASYNC) !== 0) { + const boundary = effect.b; + + if (check_dirtiness(effect)) { + var effects = boundary?.pending ? batch.boundary_async_effects : batch.async_effects; + effects.push(effect); + } + } else if ((flags & BLOCK_EFFECT) !== 0) { + if (check_dirtiness(effect)) { + update_effect(effect); + } + } else if (is_branch) { + effect.f ^= CLEAN; + } else if ((flags & RENDER_EFFECT) !== 0) { + // we need to branch here because in legacy mode we run render effects + // before running block effects + if (async_mode_flag) { + batch.render_effects.push(effect); + } else { + if (check_dirtiness(effect)) { + update_effect(effect); + } + } + } else if ((flags & EFFECT) !== 0) { + batch.effects.push(effect); + } + + var child = effect.first; + + if (child !== null) { + effect = child; + continue; + } + } + + var parent = effect.parent; + effect = effect.next; + + while (effect === null && parent !== null) { + effect = parent.next; + parent = parent.parent; + } + } +} + export function suspend() { var boundary = get_pending_boundary(); var batch = /** @type {Batch} */ (current_batch); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index b7fe3d86f1cd..a6b3c8f91a32 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -7,7 +7,6 @@ import { get, is_destroying_effect, remove_reactions, - schedule_effect, set_active_reaction, set_is_destroying_effect, set_signal_status, @@ -40,7 +39,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js'; -import { Batch } from './batch.js'; +import { Batch, schedule_effect } from './batch.js'; import { flatten } from './async.js'; /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index aa0e3660bd22..fec8fc6b42d8 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -5,7 +5,6 @@ import { active_effect, untracked_writes, get, - schedule_effect, set_untracked_writes, set_signal_status, untrack, @@ -34,7 +33,7 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; -import { Batch } from './batch.js'; +import { Batch, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 75c6bda2dc0e..94e329cb72f3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,21 +1,18 @@ /** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */ import { DEV } from 'esm-env'; -import { define_property, get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'; +import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'; import { destroy_block_effect_children, destroy_effect_children, - execute_effect_teardown, - unlink_effect + execute_effect_teardown } from './reactivity/effects.js'; import { - EFFECT, DIRTY, MAYBE_DIRTY, CLEAN, DERIVED, UNOWNED, DESTROYED, - INERT, BRANCH_EFFECT, STATE_SYMBOL, BLOCK_EFFECT, @@ -23,12 +20,9 @@ import { DISCONNECTED, REACTION_IS_UPDATING, EFFECT_IS_UPDATING, - EFFECT_ASYNC, - RENDER_EFFECT, STALE_REACTION, ERROR_VALUE } from './constants.js'; -import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; import { destroy_derived_effects, @@ -37,7 +31,6 @@ import { recent_async_deriveds, update_derived } from './reactivity/deriveds.js'; -import * as e from './errors.js'; import { async_mode_flag, tracing_mode_flag } from '../flags/index.js'; import { tracing_expressions, get_stack } from './dev/tracing.js'; import { @@ -50,13 +43,15 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; -import { handle_error, invoke_error_boundary } from './error-handling.js'; +import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; +import { handle_error } from './error-handling.js'; -/** @type {Effect | null} */ -let last_scheduled_effect = null; +export let is_updating_effect = false; -let is_updating_effect = false; +/** @param {boolean} value */ +export function set_is_updating_effect(value) { + is_updating_effect = value; +} export let is_destroying_effect = false; @@ -65,8 +60,6 @@ export function set_is_destroying_effect(value) { is_destroying_effect = value; } -// Handle effect queues - /** @type {Effect[]} */ export let queued_root_effects = []; @@ -76,8 +69,7 @@ export function set_queued_root_effects(v) { } /** @type {Effect[]} Stack of effects, dev only */ -let dev_effect_stack = []; -// Handle signal reactivity tree dependencies and reactions +export let dev_effect_stack = []; /** @type {null | Reaction} */ export let active_reaction = null; @@ -522,242 +514,6 @@ export function update_effect(effect) { } } -function log_effect_stack() { - // eslint-disable-next-line no-console - console.error( - 'Last ten effects were: ', - dev_effect_stack.slice(-10).map((d) => d.fn) - ); - dev_effect_stack = []; -} - -function infinite_loop_guard() { - try { - e.effect_update_depth_exceeded(); - } catch (error) { - if (DEV) { - // stack is garbage, ignore. Instead add a console.error message. - define_property(error, 'stack', { - value: '' - }); - } - // Try and handle the error so it can be caught at a boundary, that's - // if there's an effect available from when it was last scheduled - if (last_scheduled_effect !== null) { - if (DEV) { - try { - invoke_error_boundary(error, last_scheduled_effect); - } catch (e) { - // Only log the effect stack if the error is re-thrown - log_effect_stack(); - throw e; - } - } else { - invoke_error_boundary(error, last_scheduled_effect); - } - } else { - if (DEV) { - log_effect_stack(); - } - throw error; - } - } -} - -export function flush_queued_root_effects() { - var was_updating_effect = is_updating_effect; - var batch = /** @type {Batch} */ (current_batch); - - try { - var flush_count = 0; - is_updating_effect = true; - - while (queued_root_effects.length > 0) { - if (flush_count++ > 1000) { - infinite_loop_guard(); - } - - batch.process(queued_root_effects); - - old_values.clear(); - } - } finally { - is_updating_effect = was_updating_effect; - - last_scheduled_effect = null; - if (DEV) { - dev_effect_stack = []; - } - } -} - -/** - * @param {Array} effects - * @returns {void} - */ -export function flush_queued_effects(effects) { - var length = effects.length; - if (length === 0) return; - - for (var i = 0; i < length; i++) { - var effect = effects[i]; - - if ((effect.f & (DESTROYED | INERT)) === 0) { - if (check_dirtiness(effect)) { - update_effect(effect); - - // Effects with no dependencies or teardown do not get added to the effect tree. - // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we - // don't know if we need to keep them until they are executed. Doing the check - // here (rather than in `update_effect`) allows us to skip the work for - // immediate effects. - if (effect.deps === null && effect.first === null && effect.nodes_start === null) { - if (effect.teardown === null) { - // remove this effect from the graph - unlink_effect(effect); - } else { - // keep the effect in the graph, but free up some memory - effect.fn = null; - } - } - } - } - } -} - -/** - * @param {Effect} signal - * @returns {void} - */ -export function schedule_effect(signal) { - var effect = (last_scheduled_effect = signal); - - while (effect.parent !== null) { - effect = effect.parent; - var flags = effect.f; - - if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { - if ((flags & CLEAN) === 0) return; - effect.f ^= CLEAN; - } - } - - queued_root_effects.push(effect); -} - -/** - * - * This function both runs render effects and collects user effects in topological order - * from the starting effect passed in. Effects will be collected when they match the filtered - * bitwise flag passed in only. The collected effects array will be populated with all the user - * effects to be flushed. - * - * @param {Batch} batch - * @param {Effect} root - */ -export function process_effects(batch, root) { - root.f ^= CLEAN; - - var effect = root.first; - - while (effect !== null) { - var flags = effect.f; - var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; - var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - - var skip = is_skippable_branch || (flags & INERT) !== 0 || batch.skipped_effects.has(effect); - - if (!skip && effect.fn !== null) { - if ((flags & EFFECT_ASYNC) !== 0) { - const boundary = effect.b; - - if (check_dirtiness(effect)) { - var effects = boundary?.pending ? batch.boundary_async_effects : batch.async_effects; - effects.push(effect); - } - } else if ((flags & BLOCK_EFFECT) !== 0) { - if (check_dirtiness(effect)) { - update_effect(effect); - } - } else if (is_branch) { - effect.f ^= CLEAN; - } else if ((flags & RENDER_EFFECT) !== 0) { - // we need to branch here because in legacy mode we run render effects - // before running block effects - if (async_mode_flag) { - batch.render_effects.push(effect); - } else { - if (check_dirtiness(effect)) { - update_effect(effect); - } - } - } else if ((flags & EFFECT) !== 0) { - batch.effects.push(effect); - } - - var child = effect.first; - - if (child !== null) { - effect = child; - continue; - } - } - - var parent = effect.parent; - effect = effect.next; - - while (effect === null && parent !== null) { - effect = parent.next; - parent = parent.parent; - } - } -} - -/** - * Synchronously flush any pending updates. - * Returns void if no callback is provided, otherwise returns the result of calling the callback. - * @template [T=void] - * @param {(() => T) | undefined} [fn] - * @returns {T} - */ -export function flushSync(fn) { - if (async_mode_flag && active_effect !== null) { - e.flush_sync_in_effect(); - } - - var result; - - const batch = Batch.ensure(); - - if (fn) { - flush_queued_root_effects(); - - result = fn(); - } - - while (true) { - flush_tasks(); - - if (queued_root_effects.length === 0) { - if (batch === current_batch) { - batch.flush(); - } - - // this would be reset in `flush_queued_root_effects` but since we are early returning here, - // we need to reset it here as well in case the first time there's 0 queued root effects - last_scheduled_effect = null; - - if (DEV) { - dev_effect_stack = []; - } - - return /** @type {T} */ (result); - } - - flush_queued_root_effects(); - } -} - /** * Returns a promise that resolves once any pending state changes have been applied. * @returns {Promise} @@ -768,6 +524,7 @@ export async function tick() { } await Promise.resolve(); + // By calling flushSync we guarantee that any pending state changes are applied after one tick. // TODO look into whether we can make flushing subsequent updates synchronously in the future. flushSync(); diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 4ff1e619d5cd..6397cffe9f7a 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -3,7 +3,8 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; -import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js'; +import { active_effect, get, set_signal_status } from '../internal/client/runtime.js'; +import { flushSync } from '../internal/client/reactivity/batch.js'; import { lifecycle_outside_component } from '../internal/shared/errors.js'; import { define_property, is_array } from '../internal/shared/utils.js'; import * as w from '../internal/client/warnings.js'; diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts index 77cecca7e525..ecb22c1be6f6 100644 --- a/packages/svelte/tests/store/test.ts +++ b/packages/svelte/tests/store/test.ts @@ -11,6 +11,7 @@ import { } from 'svelte/store'; import { source, set } from '../../src/internal/client/reactivity/sources'; import * as $ from '../../src/internal/client/runtime'; +import { flushSync } from '../../src/internal/client/reactivity/batch'; import { effect_root, render_effect } from 'svelte/internal/client'; describe('writable', () => { @@ -602,7 +603,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); unsubscribe(); @@ -625,7 +626,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); store.set(2); @@ -654,11 +655,11 @@ describe('fromStore', () => { assert.deepEqual(log, [0]); store.set(1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); count.current = 2; - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1, 2]); assert.equal(get(store), 2); From bf9e1098825de8b4efd76b8d3eb8f1c8d5f4bc35 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 09:33:36 -0400 Subject: [PATCH 522/582] make flush_queued_root_effects a method of batch --- .../src/internal/client/reactivity/batch.js | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6ea395936bb4..f0583b9155a8 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -256,13 +256,13 @@ export class Batch { flush() { if (queued_root_effects.length > 0) { - flush_queued_root_effects(); + this.flush_effects(); } else { this.#commit(); } if (current_batch !== this) { - // this can happen if a `flushSync` occurred during `flush_queued_root_effects`, + // this can happen if a `flushSync` occurred during `this.flush_effects()`, // which is permitted in legacy mode despite being a terrible idea return; } @@ -274,6 +274,32 @@ export class Batch { current_batch = null; } + flush_effects() { + var was_updating_effect = is_updating_effect; + + try { + var flush_count = 0; + set_is_updating_effect(true); + + while (queued_root_effects.length > 0) { + if (flush_count++ > 1000) { + infinite_loop_guard(); + } + + this.process(queued_root_effects); + + old_values.clear(); + } + } finally { + set_is_updating_effect(was_updating_effect); + + last_scheduled_effect = null; + if (DEV) { + dev_effect_stack.length = 0; + } + } + } + #commit() { for (const fn of this.#callbacks) { fn(); @@ -368,7 +394,7 @@ export function flushSync(fn) { const batch = Batch.ensure(); if (fn) { - flush_queued_root_effects(); + batch.flush_effects(); result = fn(); } @@ -381,7 +407,7 @@ export function flushSync(fn) { batch.flush(); } - // this would be reset in `flush_queued_root_effects` but since we are early returning here, + // this would be reset in `batch.flush_effects()` but since we are early returning here, // we need to reset it here as well in case the first time there's 0 queued root effects last_scheduled_effect = null; @@ -392,7 +418,7 @@ export function flushSync(fn) { return /** @type {T} */ (result); } - flush_queued_root_effects(); + batch.flush_effects(); } } @@ -438,33 +464,6 @@ function infinite_loop_guard() { } } -export function flush_queued_root_effects() { - var was_updating_effect = is_updating_effect; - var batch = /** @type {Batch} */ (current_batch); - - try { - var flush_count = 0; - set_is_updating_effect(true); - - while (queued_root_effects.length > 0) { - if (flush_count++ > 1000) { - infinite_loop_guard(); - } - - batch.process(queued_root_effects); - - old_values.clear(); - } - } finally { - set_is_updating_effect(was_updating_effect); - - last_scheduled_effect = null; - if (DEV) { - dev_effect_stack.length = 0; - } - } -} - /** * @param {Array} effects * @returns {void} From 0c5b4d86c4b450c275d8e6c256f15fa3038f4ba4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 09:53:37 -0400 Subject: [PATCH 523/582] make process_effects a method of batch --- .../src/internal/client/reactivity/batch.js | 131 +++++++++--------- 1 file changed, 62 insertions(+), 69 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index f0583b9155a8..4badc9326a40 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -142,7 +142,7 @@ export class Batch { } for (const root of root_effects) { - process_effects(this, root); + this.process_root(root); } if (this.async_effects.length === 0 && this.#pending === 0) { @@ -234,6 +234,67 @@ export class Batch { this.boundary_async_effects = []; } + /** + * @param {Effect} root + */ + process_root(root) { + root.f ^= CLEAN; + + var effect = root.first; + + while (effect !== null) { + var flags = effect.f; + var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; + var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; + + var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect); + + if (!skip && effect.fn !== null) { + if ((flags & EFFECT_ASYNC) !== 0) { + const boundary = effect.b; + + if (check_dirtiness(effect)) { + var effects = boundary?.pending ? this.boundary_async_effects : this.async_effects; + effects.push(effect); + } + } else if ((flags & BLOCK_EFFECT) !== 0) { + if (check_dirtiness(effect)) { + update_effect(effect); + } + } else if (is_branch) { + effect.f ^= CLEAN; + } else if ((flags & RENDER_EFFECT) !== 0) { + // we need to branch here because in legacy mode we run render effects + // before running block effects + if (async_mode_flag) { + this.render_effects.push(effect); + } else { + if (check_dirtiness(effect)) { + update_effect(effect); + } + } + } else if ((flags & EFFECT) !== 0) { + this.effects.push(effect); + } + + var child = effect.first; + + if (child !== null) { + effect = child; + continue; + } + } + + var parent = effect.parent; + effect = effect.next; + + while (effect === null && parent !== null) { + effect = parent.next; + parent = parent.parent; + } + } + } + /** * @param {Source} source * @param {any} value @@ -518,74 +579,6 @@ export function schedule_effect(signal) { queued_root_effects.push(effect); } -/** - * - * This function both runs render effects and collects user effects in topological order - * from the starting effect passed in. Effects will be collected when they match the filtered - * bitwise flag passed in only. The collected effects array will be populated with all the user - * effects to be flushed. - * - * @param {Batch} batch - * @param {Effect} root - */ -export function process_effects(batch, root) { - root.f ^= CLEAN; - - var effect = root.first; - - while (effect !== null) { - var flags = effect.f; - var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; - var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - - var skip = is_skippable_branch || (flags & INERT) !== 0 || batch.skipped_effects.has(effect); - - if (!skip && effect.fn !== null) { - if ((flags & EFFECT_ASYNC) !== 0) { - const boundary = effect.b; - - if (check_dirtiness(effect)) { - var effects = boundary?.pending ? batch.boundary_async_effects : batch.async_effects; - effects.push(effect); - } - } else if ((flags & BLOCK_EFFECT) !== 0) { - if (check_dirtiness(effect)) { - update_effect(effect); - } - } else if (is_branch) { - effect.f ^= CLEAN; - } else if ((flags & RENDER_EFFECT) !== 0) { - // we need to branch here because in legacy mode we run render effects - // before running block effects - if (async_mode_flag) { - batch.render_effects.push(effect); - } else { - if (check_dirtiness(effect)) { - update_effect(effect); - } - } - } else if ((flags & EFFECT) !== 0) { - batch.effects.push(effect); - } - - var child = effect.first; - - if (child !== null) { - effect = child; - continue; - } - } - - var parent = effect.parent; - effect = effect.next; - - while (effect === null && parent !== null) { - effect = parent.next; - parent = parent.parent; - } - } -} - export function suspend() { var boundary = get_pending_boundary(); var batch = /** @type {Batch} */ (current_batch); From c7c9404d5ec8d6f089811f771287ee97980d762e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 09:54:31 -0400 Subject: [PATCH 524/582] make stuff private --- .../src/internal/client/reactivity/batch.js | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 4badc9326a40..14d49ba4e50b 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -84,16 +84,16 @@ export class Batch { #deferred = null; /** @type {Effect[]} */ - async_effects = []; + #async_effects = []; /** @type {Effect[]} */ - boundary_async_effects = []; + #boundary_async_effects = []; /** @type {Effect[]} */ - render_effects = []; + #render_effects = []; /** @type {Effect[]} */ - effects = []; + #effects = []; /** * A set of branches that still exist, but will be destroyed when this batch @@ -145,7 +145,7 @@ export class Batch { this.process_root(root); } - if (this.async_effects.length === 0 && this.#pending === 0) { + if (this.#async_effects.length === 0 && this.#pending === 0) { var merged = false; // if there are older batches with overlapping @@ -162,19 +162,19 @@ export class Batch { batch.#current.set(source, value); } - for (const e of this.render_effects) { + for (const e of this.#render_effects) { set_signal_status(e, CLEAN); // TODO use sets instead of arrays - if (!batch.render_effects.includes(e)) { - batch.render_effects.push(e); + if (!batch.#render_effects.includes(e)) { + batch.#render_effects.push(e); } } - for (const e of this.effects) { + for (const e of this.#effects) { set_signal_status(e, CLEAN); // TODO use sets instead of arrays - if (!batch.effects.includes(e)) { - batch.effects.push(e); + if (!batch.#effects.includes(e)) { + batch.#effects.push(e); } } @@ -192,11 +192,11 @@ export class Batch { } if (!merged) { - var render_effects = this.render_effects; - var effects = this.effects; + var render_effects = this.#render_effects; + var effects = this.#effects; - this.render_effects = []; - this.effects = []; + this.#render_effects = []; + this.#effects = []; this.#commit(); @@ -206,8 +206,8 @@ export class Batch { this.#deferred?.resolve(); } } else { - for (const e of this.render_effects) set_signal_status(e, CLEAN); - for (const e of this.effects) set_signal_status(e, CLEAN); + for (const e of this.#render_effects) set_signal_status(e, CLEAN); + for (const e of this.#effects) set_signal_status(e, CLEAN); } if (current_values) { @@ -222,16 +222,16 @@ export class Batch { batch_deriveds = null; } - for (const effect of this.async_effects) { + for (const effect of this.#async_effects) { update_effect(effect); } - for (const effect of this.boundary_async_effects) { + for (const effect of this.#boundary_async_effects) { update_effect(effect); } - this.async_effects = []; - this.boundary_async_effects = []; + this.#async_effects = []; + this.#boundary_async_effects = []; } /** @@ -254,7 +254,7 @@ export class Batch { const boundary = effect.b; if (check_dirtiness(effect)) { - var effects = boundary?.pending ? this.boundary_async_effects : this.async_effects; + var effects = boundary?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); } } else if ((flags & BLOCK_EFFECT) !== 0) { @@ -267,14 +267,14 @@ export class Batch { // we need to branch here because in legacy mode we run render effects // before running block effects if (async_mode_flag) { - this.render_effects.push(effect); + this.#render_effects.push(effect); } else { if (check_dirtiness(effect)) { update_effect(effect); } } } else if ((flags & EFFECT) !== 0) { - this.effects.push(effect); + this.#effects.push(effect); } var child = effect.first; @@ -377,18 +377,18 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { - for (const e of this.render_effects) { + for (const e of this.#render_effects) { set_signal_status(e, DIRTY); schedule_effect(e); } - for (const e of this.effects) { + for (const e of this.#effects) { set_signal_status(e, DIRTY); schedule_effect(e); } - this.render_effects = []; - this.effects = []; + this.#render_effects = []; + this.#effects = []; this.flush(); } From 497ef135c747e2ab1a579463ecdf83a7375367a7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 09:59:44 -0400 Subject: [PATCH 525/582] unused --- .../src/internal/client/reactivity/batch.js | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 14d49ba4e50b..5febc61fbf3c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -106,7 +106,7 @@ export class Batch { * * @param {Effect[]} root_effects */ - process(root_effects) { + #process(root_effects) { set_queued_root_effects([]); /** @type {Map | null} */ @@ -142,7 +142,7 @@ export class Batch { } for (const root of root_effects) { - this.process_root(root); + this.#process_root(root); } if (this.#async_effects.length === 0 && this.#pending === 0) { @@ -237,7 +237,7 @@ export class Batch { /** * @param {Effect} root */ - process_root(root) { + #process_root(root) { root.f ^= CLEAN; var effect = root.first; @@ -347,7 +347,7 @@ export class Batch { infinite_loop_guard(); } - this.process(queued_root_effects); + this.#process(queued_root_effects); old_values.clear(); } @@ -399,22 +399,6 @@ export class Batch { this.#callbacks.add(fn); } - /** @param {Effect} effect */ - skips(effect) { - /** @type {Effect | null} */ - var e = effect; - - while (e !== null) { - if (this.skipped_effects.has(e)) { - return true; - } - - e = e.parent; - } - - return false; - } - settled() { return (this.#deferred ??= deferred()).promise; } From d7659d500ffeee12d69d4552ad19ba69f4e6feb9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 11:36:24 -0400 Subject: [PATCH 526/582] regenerate --- packages/svelte/types/index.d.ts | 56 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index e068601f4cfa..72eb871db628 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -434,6 +434,11 @@ declare module 'svelte' { * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => void): void; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; /** * Create a snippet programmatically * */ @@ -443,34 +448,6 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; - /** - * Synchronously flush any pending updates. - * Returns void if no callback is provided, otherwise returns the result of calling the callback. - * */ - export function flushSync(fn?: (() => T) | undefined): T; - /** - * Returns a promise that resolves once any pending state changes have been applied. - * */ - export function tick(): Promise; - /** - * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, - * have resolved and the DOM has been updated - * */ - export function settled(): Promise; - /** - * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), - * any state read inside `fn` will not be treated as a dependency. - * - * ```ts - * $effect(() => { - * // this will run when `data` changes, but not when `time` changes - * save(data, { - * timestamp: untrack(() => time) - * }); - * }); - * ``` - * */ - export function untrack(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -544,6 +521,29 @@ declare module 'svelte' { export function unmount(component: Record, options?: { outro?: boolean; } | undefined): Promise; + /** + * Returns a promise that resolves once any pending state changes have been applied. + * */ + export function tick(): Promise; + /** + * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, + * have resolved and the DOM has been updated + * */ + export function settled(): Promise; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), + * any state read inside `fn` will not be treated as a dependency. + * + * ```ts + * $effect(() => { + * // this will run when `data` changes, but not when `time` changes + * save(data, { + * timestamp: untrack(() => time) + * }); + * }); + * ``` + * */ + export function untrack(fn: () => T): T; type Getters = { [K in keyof T]: () => T[K]; }; From 52a95a0d7c157401e46306dafe9be719cbbbdea0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 14:52:57 -0400 Subject: [PATCH 527/582] update test --- .../tests/runtime-runes/samples/effect-cleanup/_config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index 53e938d63f40..416f61d23a9b 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -16,7 +16,7 @@ export default test({ // it works differently: https://github.com/sveltejs/svelte/pull/15564 assert.deepEqual( logs, - async_mode ? ['init 0', 'cleanup 2', null, 'init 2', 'cleanup 4', null, 'init 4'] : ['init 0'] + async_mode ? ['init 0', 'cleanup 0', null, 'init 2', 'cleanup 2', null, 'init 4'] : ['init 0'] ); } }); From 6169405638afa0c4734f0c048938162b7f89fb02 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 20:05:11 -0400 Subject: [PATCH 528/582] more JSDoc --- .../src/internal/client/reactivity/batch.js | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9db6c6a276bc..c05b1c0d2b9c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -85,16 +85,32 @@ export class Batch { */ #deferred = null; - /** @type {Effect[]} */ + /** + * Async effects (created inside `async_derived`) encountered during processing. + * These run after the rest of the batch has updated, since they should + * always have the latest values + * @type {Effect[]} + */ #async_effects = []; - /** @type {Effect[]} */ + /** + * The same as `#async_effects`, but for effects inside a newly-created + * `` — these do not prevent the batch from committing + * @type {Effect[]} + */ #boundary_async_effects = []; - /** @type {Effect[]} */ + /** + * Template effects and `$effect.pre` effects, which run when + * a batch is committed + * @type {Effect[]} + */ #render_effects = []; - /** @type {Effect[]} */ + /** + * The same as `#render_effects`, but for `$effect` (which runs after) + * @type {Effect[]} + */ #effects = []; /** From 76b8d52de22c20b7b2510fec12fdc8291a67863d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 21:10:55 -0400 Subject: [PATCH 529/582] add more JSDoc --- .../src/internal/client/reactivity/batch.js | 35 +++++++++++-------- .../svelte/src/internal/client/runtime.js | 8 ----- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index c05b1c0d2b9c..5da776bdc7ad 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -20,9 +20,7 @@ import { check_dirtiness, dev_effect_stack, is_updating_effect, - queued_root_effects, set_is_updating_effect, - set_queued_root_effects, set_signal_status, update_effect, write_version @@ -40,9 +38,17 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; -/** @type {Map | null} */ +/** + * When time travelling, we re-evaluate deriveds based on the temporary + * values of their dependencies rather than their actual values, and cache + * the results in this map rather than on the deriveds themselves + * @type {Map | null} + */ export let batch_deriveds = null; +/** @type {Effect[]} */ +let queued_root_effects = []; + /** @type {Effect | null} */ let last_scheduled_effect = null; @@ -125,20 +131,15 @@ export class Batch { * @param {Effect[]} root_effects */ #process(root_effects) { - set_queued_root_effects([]); + queued_root_effects = []; /** @type {Map | null} */ var current_values = null; - var time_travelling = false; - for (const batch of batches) { - if (batch !== this) { - time_travelling = true; - break; - } - } - - if (time_travelling) { + // if there are multiple batches, we are 'time travelling' — + // we need to undo the changes belonging to any batch + // other than the current one + if (batches.size > 1) { current_values = new Map(); batch_deriveds = new Map(); @@ -253,6 +254,8 @@ export class Batch { } /** + * Traverse the effect tree, executing effects or stashing + * them for later execution as appropriate * @param {Effect} root */ #process_root(root) { @@ -314,6 +317,8 @@ export class Batch { } /** + * Associate a change to a given source with the current + * batch, noting its previous and current values * @param {Source} source * @param {any} value */ @@ -366,7 +371,6 @@ export class Batch { } this.#process(queued_root_effects); - old_values.clear(); } } finally { @@ -379,6 +383,9 @@ export class Batch { } } + /** + * Append and remove branches to/from the DOM + */ #commit() { for (const fn of this.#callbacks) { fn(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 093d875d601d..1f5621674ffa 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -61,14 +61,6 @@ export function set_is_destroying_effect(value) { is_destroying_effect = value; } -/** @type {Effect[]} */ -export let queued_root_effects = []; - -/** @param {Effect[]} v */ -export function set_queued_root_effects(v) { - queued_root_effects = v; -} - /** @type {Effect[]} Stack of effects, dev only */ export let dev_effect_stack = []; From 3f3734b3fc296305630b44e65f6bee5363fbc783 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 21:16:26 -0400 Subject: [PATCH 530/582] branch and block effects do not also need to be render effects --- packages/svelte/src/internal/client/reactivity/batch.js | 6 +++--- packages/svelte/src/internal/client/reactivity/effects.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 5da776bdc7ad..d55e38872303 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -271,7 +271,9 @@ export class Batch { var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect); if (!skip && effect.fn !== null) { - if ((flags & EFFECT_ASYNC) !== 0) { + if (is_branch) { + effect.f ^= CLEAN; + } else if ((flags & EFFECT_ASYNC) !== 0) { const boundary = effect.b; if (check_dirtiness(effect)) { @@ -282,8 +284,6 @@ export class Batch { if (check_dirtiness(effect)) { update_effect(effect); } - } else if (is_branch) { - effect.f ^= CLEAN; } else if ((flags & RENDER_EFFECT) !== 0) { // we need to branch here because in legacy mode we run render effects // before running block effects diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index cedfed1ea90c..747ef8e82010 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -355,7 +355,7 @@ export function template_effect(fn, sync = [], async = []) { * @param {number} flags */ export function block(fn, flags = 0) { - var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); + var effect = create_effect(BLOCK_EFFECT | flags, fn, true); if (DEV) { effect.dev_stack = dev_stack; } @@ -367,7 +367,7 @@ export function block(fn, flags = 0) { * @param {boolean} [push] */ export function branch(fn, push = true) { - return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push); + return create_effect(BRANCH_EFFECT, fn, true, push); } /** From ee3a02aa504f75ab7bdd02288b88af5b4c0eaa32 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 21:20:22 -0400 Subject: [PATCH 531/582] tidy up --- .../svelte/src/internal/client/reactivity/deriveds.js | 7 +++---- .../svelte/src/internal/client/reactivity/effects.js | 11 ++++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 1fd23f6d3f67..aece8f427c8e 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -6,7 +6,6 @@ import { CLEAN, DERIVED, DIRTY, - EFFECT_ASYNC, EFFECT_PRESERVED, MAYBE_DIRTY, STALE_REACTION, @@ -26,7 +25,7 @@ import { import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; -import { destroy_effect, render_effect } from './effects.js'; +import { async_effect, 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'; @@ -114,7 +113,7 @@ export function async_derived(fn, location) { // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; - render_effect(() => { + async_effect(() => { if (DEV) current_async_effect = active_effect; try { @@ -187,7 +186,7 @@ export function async_derived(fn, location) { }; promise.then(handler, (e) => handler(null, e || 'unknown')); - }, EFFECT_ASYNC | EFFECT_PRESERVED); + }); return new Promise((fulfil) => { /** @param {Promise} p */ diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 747ef8e82010..a91b739d5f62 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -33,7 +33,8 @@ import { EFFECT_PRESERVED, BOUNDARY_EFFECT, STALE_REACTION, - USER_EFFECT + USER_EFFECT, + EFFECT_ASYNC } from '#client/constants'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; @@ -331,6 +332,14 @@ export function legacy_pre_effect_reset() { }); } +/** + * @param {() => void | (() => void)} fn + * @returns {Effect} + */ +export function async_effect(fn) { + return create_effect(EFFECT_ASYNC | EFFECT_PRESERVED, fn, true); +} + /** * @param {() => void | (() => void)} fn * @returns {Effect} From 8b8f0590168cfcc88c55c1eb38272a1f361a9457 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 21:35:34 -0400 Subject: [PATCH 532/582] simplify --- .../src/internal/client/reactivity/batch.js | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d55e38872303..aaedbb7ac700 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -273,29 +273,17 @@ export class Batch { if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; - } else if ((flags & EFFECT_ASYNC) !== 0) { - const boundary = effect.b; - - if (check_dirtiness(effect)) { - var effects = boundary?.pending ? this.#boundary_async_effects : this.#async_effects; + } else if ((flags & EFFECT) !== 0) { + this.#effects.push(effect); + } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { + this.#render_effects.push(effect); + } else if (check_dirtiness(effect)) { + if ((flags & EFFECT_ASYNC) !== 0) { + var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); - } - } else if ((flags & BLOCK_EFFECT) !== 0) { - if (check_dirtiness(effect)) { - update_effect(effect); - } - } else if ((flags & RENDER_EFFECT) !== 0) { - // we need to branch here because in legacy mode we run render effects - // before running block effects - if (async_mode_flag) { - this.#render_effects.push(effect); } else { - if (check_dirtiness(effect)) { - update_effect(effect); - } + update_effect(effect); } - } else if ((flags & EFFECT) !== 0) { - this.#effects.push(effect); } var child = effect.first; From fa1deacd0e1952da8ca8e9d38cf2b86000c56d5d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 21:36:56 -0400 Subject: [PATCH 533/582] unused --- packages/svelte/src/internal/client/reactivity/batch.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index aaedbb7ac700..ddfccc2dadc5 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,6 +1,5 @@ /** @import { Derived, Effect, Source } from '#client' */ import { - BLOCK_EFFECT, BRANCH_EFFECT, CLEAN, DESTROYED, From 6f12c0901e47f57c36899ebdee744ff5004c58a4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 21:40:29 -0400 Subject: [PATCH 534/582] move code where it belongs --- .../svelte/src/internal/client/reactivity/batch.js | 10 ++++++---- packages/svelte/src/internal/client/runtime.js | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ddfccc2dadc5..3e4fc78bdc0e 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -17,7 +17,6 @@ import { get_pending_boundary } from '../dom/blocks/boundary.js'; import { active_effect, check_dirtiness, - dev_effect_stack, is_updating_effect, set_is_updating_effect, set_signal_status, @@ -45,6 +44,9 @@ export let current_batch = null; */ export let batch_deriveds = null; +/** @type {Effect[]} Stack of effects, dev only */ +export let dev_effect_stack = []; + /** @type {Effect[]} */ let queued_root_effects = []; @@ -365,7 +367,7 @@ export class Batch { last_scheduled_effect = null; if (DEV) { - dev_effect_stack.length = 0; + dev_effect_stack = []; } } } @@ -469,7 +471,7 @@ export function flushSync(fn) { last_scheduled_effect = null; if (DEV) { - dev_effect_stack.length = 0; + dev_effect_stack = []; } return /** @type {T} */ (result); @@ -485,7 +487,7 @@ function log_effect_stack() { 'Last ten effects were: ', dev_effect_stack.slice(-10).map((d) => d.fn) ); - dev_effect_stack.length = 0; + dev_effect_stack = []; } function infinite_loop_guard() { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 1f5621674ffa..c1bec75ae89e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -43,7 +43,13 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; +import { + Batch, + batch_deriveds, + dev_effect_stack, + flushSync, + schedule_effect +} from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; @@ -61,9 +67,6 @@ export function set_is_destroying_effect(value) { is_destroying_effect = value; } -/** @type {Effect[]} Stack of effects, dev only */ -export let dev_effect_stack = []; - /** @type {null | Reaction} */ export let active_reaction = null; From d36705216978ab93cf021eb124eefc5f95a8fc68 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 22:18:27 -0400 Subject: [PATCH 535/582] remove, for now --- packages/svelte/src/internal/client/reactivity/batch.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 3e4fc78bdc0e..7be2434f7dd7 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -53,12 +53,7 @@ let queued_root_effects = []; /** @type {Effect | null} */ let last_scheduled_effect = null; -/** TODO handy for debugging, but we should probably eventually delete it */ -let uid = 1; - export class Batch { - id = uid++; - /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` From c79553bec79586b8672a1fa140e3e6df4b3b898c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 23:07:40 -0400 Subject: [PATCH 536/582] fix --- packages/svelte/src/internal/client/error-handling.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index b1df1a50b7f3..c9a888658c8a 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -30,8 +30,7 @@ export function handle_error(error) { throw error; } - // @ts-expect-error - effect.fn(error); + /** @type {Boundary} */ (effect.b).error(error); } else { // otherwise we bubble up the effect tree ourselves invoke_error_boundary(error, effect); From 6c2064b195f7dfa4c8ec61f0b7533cfd2f7f7ab3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 23:20:19 -0400 Subject: [PATCH 537/582] only apply error adjustments when error escapes boundaries --- .../src/internal/client/error-handling.js | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index c9a888658c8a..8e93923c466a 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -7,6 +7,8 @@ import { ERROR_VALUE, BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect, active_reaction } from './runtime.js'; +const adjustments = new WeakMap(); + /** * @param {unknown} error */ @@ -19,14 +21,18 @@ export function handle_error(error) { return error; } - if (DEV && error instanceof Error) { - // adjust_error(error, effect); + if (DEV && error instanceof Error && !adjustments.has(error)) { + adjustments.set(error, get_adjustments(error, effect)); } if ((effect.f & EFFECT_RAN) === 0) { // if the error occurred while creating this subtree, we let it // bubble up until it hits a boundary that can handle it if ((effect.f & BOUNDARY_EFFECT) === 0) { + if (!effect.parent && error instanceof Error) { + apply_adjustments(error); + } + throw error; } @@ -53,6 +59,10 @@ export function invoke_error_boundary(error, effect) { effect = effect.parent; } + if (error instanceof Error) { + apply_adjustments(error); + } + throw error; } @@ -64,7 +74,7 @@ const adjusted_errors = new WeakSet(); * @param {Error} error * @param {Effect} effect */ -function adjust_error(error, effect) { +function get_adjustments(error, effect) { if (adjusted_errors.has(error)) return; adjusted_errors.add(error); @@ -83,17 +93,28 @@ function adjust_error(error, effect) { context = context.p; } - define_property(error, 'message', { - value: error.message + `\n${component_stack}\n` - }); + return { + message: error.message + `\n${component_stack}\n`, + stack: error.stack + ?.split('\n') + .filter((line) => !line.includes('svelte/src/internal')) + .join('\n') + }; +} + +/** + * @param {Error} error + */ +function apply_adjustments(error) { + const adjusted = adjustments.get(error); + + if (adjusted) { + define_property(error, 'message', { + value: adjusted.message + }); - if (error.stack) { - // Filter out internal modules define_property(error, 'stack', { - value: error.stack - .split('\n') - .filter((line) => !line.includes('svelte/src/internal')) - .join('\n') + value: adjusted.stack }); } } From 179980e96541265cf05ba8dfd72ca826d6b5de73 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Jul 2025 07:18:05 -0400 Subject: [PATCH 538/582] remove EFFECT_IS_UPDATING --- packages/svelte/src/internal/client/constants.js | 9 ++++----- packages/svelte/src/internal/client/runtime.js | 11 +---------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 44ee7a45c882..0716a2e4d06c 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -18,14 +18,13 @@ export const EFFECT_TRANSPARENT = 1 << 16; export const INSPECT_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; export const EFFECT_PRESERVED = 1 << 19; -export const EFFECT_IS_UPDATING = 1 << 20; -export const USER_EFFECT = 1 << 21; +export const USER_EFFECT = 1 << 20; // Flags used for async -export const REACTION_IS_UPDATING = 1 << 22; -export const EFFECT_ASYNC = 1 << 23; +export const REACTION_IS_UPDATING = 1 << 21; +export const EFFECT_ASYNC = 1 << 22; -export const ERROR_VALUE = 1 << 24; +export const ERROR_VALUE = 1 << 23; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c1bec75ae89e..fcd6fb2cc4a6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -19,7 +19,6 @@ import { ROOT_EFFECT, DISCONNECTED, REACTION_IS_UPDATING, - EFFECT_IS_UPDATING, STALE_REACTION, ERROR_VALUE } from './constants.js'; @@ -94,7 +93,7 @@ export let source_ownership = null; /** @param {Value} value */ export function push_reaction_value(value) { - if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { + if (active_reaction !== null && (!async_mode_flag || (active_reaction.f & DERIVED) !== 0)) { if (source_ownership === null) { source_ownership = { reaction: active_reaction, sources: [value] }; } else { @@ -287,10 +286,6 @@ export function update_reaction(reaction) { untracking = false; read_version++; - if (!async_mode_flag || (reaction.f & DERIVED) !== 0) { - reaction.f |= EFFECT_IS_UPDATING; - } - if (reaction.ac !== null) { reaction.ac.abort(STALE_REACTION); reaction.ac = null; @@ -381,10 +376,6 @@ export function update_reaction(reaction) { source_ownership = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; - - if (!async_mode_flag || (reaction.f & DERIVED) !== 0) { - reaction.f ^= EFFECT_IS_UPDATING; - } } } From 2019451444da03eae60998b5f2efbf89e4fa1a61 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Jul 2025 07:23:32 -0400 Subject: [PATCH 539/582] is_dirty is a better name than check_dirtiness --- packages/svelte/src/internal/client/reactivity/batch.js | 6 +++--- packages/svelte/src/internal/client/reactivity/effects.js | 6 +++--- packages/svelte/src/internal/client/reactivity/sources.js | 5 +++-- packages/svelte/src/internal/client/runtime.js | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 7be2434f7dd7..a320331bf675 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -16,7 +16,7 @@ import { deferred, define_property } from '../../shared/utils.js'; import { get_pending_boundary } from '../dom/blocks/boundary.js'; import { active_effect, - check_dirtiness, + is_dirty, is_updating_effect, set_is_updating_effect, set_signal_status, @@ -273,7 +273,7 @@ export class Batch { this.#effects.push(effect); } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { this.#render_effects.push(effect); - } else if (check_dirtiness(effect)) { + } else if (is_dirty(effect)) { if ((flags & EFFECT_ASYNC) !== 0) { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); @@ -530,7 +530,7 @@ function flush_queued_effects(effects) { var effect = effects[i]; if ((effect.f & (DESTROYED | INERT)) === 0) { - if (check_dirtiness(effect)) { + if (is_dirty(effect)) { var wv = write_version; update_effect(effect); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index a91b739d5f62..92065f44d036 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -1,6 +1,6 @@ /** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */ import { - check_dirtiness, + is_dirty, active_effect, active_reaction, update_effect, @@ -323,7 +323,7 @@ export function legacy_pre_effect_reset() { set_signal_status(effect, MAYBE_DIRTY); } - if (check_dirtiness(effect)) { + if (is_dirty(effect)) { update_effect(effect); } @@ -614,7 +614,7 @@ function resume_children(effect, local) { effect.f ^= INERT; // If a dependency of this effect changed while it was paused, - // schedule the effect to update. we don't use `check_dirtiness` + // schedule the effect to update. we don't use `is_dirty` // here because we don't want to eagerly recompute a derived like // `{#if foo}{foo.bar()}{/if}` if `foo` is now `undefined if ((effect.f & CLEAN) === 0) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index fec8fc6b42d8..75a65b167802 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -11,7 +11,7 @@ import { increment_write_version, update_effect, source_ownership, - check_dirtiness, + is_dirty, untracking, is_destroying_effect, push_reaction_value @@ -222,7 +222,8 @@ export function internal_set(source, value) { if ((effect.f & CLEAN) !== 0) { set_signal_status(effect, MAYBE_DIRTY); } - if (check_dirtiness(effect)) { + + if (is_dirty(effect)) { update_effect(effect); } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fcd6fb2cc4a6..e10115c6feae 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -155,7 +155,7 @@ export function increment_write_version() { * @param {Reaction} reaction * @returns {boolean} */ -export function check_dirtiness(reaction) { +export function is_dirty(reaction) { var flags = reaction.f; if ((flags & DIRTY) !== 0) { @@ -208,7 +208,7 @@ export function check_dirtiness(reaction) { for (i = 0; i < length; i++) { dependency = dependencies[i]; - if (check_dirtiness(/** @type {Derived} */ (dependency))) { + if (is_dirty(/** @type {Derived} */ (dependency))) { update_derived(/** @type {Derived} */ (dependency)); } @@ -607,7 +607,7 @@ export function get(signal) { if (is_derived && !is_destroying_effect && batch_deriveds === null) { derived = /** @type {Derived} */ (signal); - if (check_dirtiness(derived)) { + if (is_dirty(derived)) { update_derived(derived); } } From f6358d53c985d8baf9fc0f62a7f418e51d6110dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Jul 2025 09:01:24 -0400 Subject: [PATCH 540/582] duplicates are rare and harmless --- .../svelte/src/internal/client/reactivity/batch.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a320331bf675..f341252ffc40 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -179,18 +179,12 @@ export class Batch { for (const e of this.#render_effects) { set_signal_status(e, CLEAN); - // TODO use sets instead of arrays - if (!batch.#render_effects.includes(e)) { - batch.#render_effects.push(e); - } + batch.#render_effects.push(e); } for (const e of this.#effects) { set_signal_status(e, CLEAN); - // TODO use sets instead of arrays - if (!batch.#effects.includes(e)) { - batch.#effects.push(e); - } + batch.#effects.push(e); } for (const e of this.skipped_effects) { From 11f2c4868a935314843d85d2a93ca64b0c2bab1e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Jul 2025 09:16:53 -0400 Subject: [PATCH 541/582] apparently we no longer need the merging logic? we can simplify and fix stuff by removing it --- .../src/internal/client/reactivity/batch.js | 60 ++++--------------- .../async-unresolved-promise/_config.js | 32 ++++++++++ .../async-unresolved-promise/main.svelte | 19 ++++++ 3 files changed, 62 insertions(+), 49 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index f341252ffc40..e73618c02669 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -160,61 +160,23 @@ export class Batch { this.#process_root(root); } + // if we didn't start any new async work, and no async work + // is outstanding from a previous flush, commit if (this.#async_effects.length === 0 && this.#pending === 0) { - var merged = false; + var render_effects = this.#render_effects; + var effects = this.#effects; - // if there are older batches with overlapping - // state, we can't commit this batch. instead, - // we merge it into the older batches - for (const batch of batches) { - if (batch === this) break; - - for (const [source] of batch.#current) { - if (this.#current.has(source)) { - merged = true; - - for (const [source, value] of this.#current) { - batch.#current.set(source, value); - } - - for (const e of this.#render_effects) { - set_signal_status(e, CLEAN); - batch.#render_effects.push(e); - } - - for (const e of this.#effects) { - set_signal_status(e, CLEAN); - batch.#effects.push(e); - } - - for (const e of this.skipped_effects) { - batch.skipped_effects.add(e); - } - - for (const fn of this.#callbacks) { - batch.#callbacks.add(fn); - } - - break; - } - } - } - - if (!merged) { - var render_effects = this.#render_effects; - var effects = this.#effects; - - this.#render_effects = []; - this.#effects = []; + this.#render_effects = []; + this.#effects = []; - this.#commit(); + this.#commit(); - flush_queued_effects(render_effects); - flush_queued_effects(effects); + flush_queued_effects(render_effects); + flush_queued_effects(effects); - this.#deferred?.resolve(); - } + this.#deferred?.resolve(); } else { + // otherwise mark effects clean so they get scheduled on the next run for (const e of this.#render_effects) set_signal_status(e, CLEAN); for (const e of this.#effects) set_signal_status(e, CLEAN); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js new file mode 100644 index 000000000000..e9ccbba2b66b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + +

    0

    + ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

    2

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte new file mode 100644 index 000000000000..e0619a1fe4d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte @@ -0,0 +1,19 @@ + + + + + + {#if count % 2} +

    {await new Promise(() => {})}

    + {:else} +

    {await count}

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

    loading...

    + {/snippet} +
    From 39a7f08ead3057bbe5a84a22a8b9612441b9f18d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Jul 2025 10:20:49 -0400 Subject: [PATCH 542/582] tidy --- .../svelte/src/internal/client/reactivity/deriveds.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index aece8f427c8e..58a22194d71d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -124,13 +124,8 @@ export function async_derived(fn, location) { if (DEV) current_async_effect = null; - promise = - prev === null - ? Promise.resolve(p) - : prev.then( - () => p, - () => p - ); + var r = () => p; + promise = prev?.then(r, r) ?? Promise.resolve(p); prev = promise; From 60eaa2862e646c5d6cd2324536d02f493240fd18 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Jul 2025 10:50:08 -0400 Subject: [PATCH 543/582] don't commit stale batches --- .../src/internal/client/reactivity/batch.js | 12 ++++- .../internal/client/reactivity/deriveds.js | 7 ++- .../samples/async-stale-derived/_config.js | 52 +++++++++++++++++++ .../samples/async-stale-derived/main.svelte | 26 ++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e73618c02669..59edfda5f916 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -87,6 +87,12 @@ export class Batch { */ #deferred = null; + /** + * True if an async effect inside this batch resolved and + * its parent branch was already deleted + */ + #neutered = false; + /** * Async effects (created inside `async_derived`) encountered during processing. * These run after the rest of the batch has updated, since they should @@ -278,10 +284,14 @@ export class Batch { current_batch = null; } + neuter() { + this.#neutered = true; + } + flush() { if (queued_root_effects.length > 0) { this.flush_effects(); - } else { + } else if (!this.#neutered) { this.#commit(); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 58a22194d71d..2012bb81ac77 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -9,7 +9,8 @@ import { EFFECT_PRESERVED, MAYBE_DIRTY, STALE_REACTION, - UNOWNED + UNOWNED, + DESTROYED } from '#client/constants'; import { active_reaction, @@ -144,6 +145,10 @@ export function async_derived(fn, location) { const handler = (value, error = undefined) => { prev = null; + if ((parent.f & DESTROYED) !== 0) { + batch.neuter(); + } + current_async_effect = null; if (!pending) batch.activate(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js new file mode 100644 index 000000000000..884b27d865ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment, shift] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

    0

    + ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

    2

    + ` + ); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

    delayed: 3

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte new file mode 100644 index 000000000000..eeefffbee6c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte @@ -0,0 +1,26 @@ + + + + + + + {#if count % 2} +

    delayed: {await push()}

    + {:else} +

    {await count}

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

    loading...

    + {/snippet} +
    From c57e673c846d6bf8da1bcca253c5227dc67abbb1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 8 Jul 2025 12:19:02 -0400 Subject: [PATCH 544/582] add skipped failing test --- .../samples/async-stale-derived-2/_config.js | 58 +++++++++++++++++++ .../samples/async-stale-derived-2/main.svelte | 34 +++++++++++ 2 files changed, 92 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js new file mode 100644 index 000000000000..8276c5be419b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js @@ -0,0 +1,58 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO this one is tricky + + async test({ assert, target }) { + const [increment, a, b] = target.querySelectorAll('button'); + + a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

    a: 0

    + ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

    a: 0

    + ` + ); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

    b: 0

    + let count = $state(0); + + let a = []; + let b = []; + + function push(deferreds, value) { + const deferred = Promise.withResolvers(); + deferreds.push({ deferred, value }); + return deferred.promise; + } + + + + + + + + {#if count % 2 === 0} +

    a: {await push(a, count)}

    + {:else} +

    b: {await push(b, count)}

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

    loading...

    + {/snippet} +
    From 6afb9c6028e19100829d7157c48c168cef32990b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 09:58:17 -0400 Subject: [PATCH 545/582] partial merge --- .changeset/light-rivers-jump.md | 5 - .github/workflows/ecosystem-ci-trigger.yml | 43 ++++--- packages/svelte/CHANGELOG.md | 8 ++ packages/svelte/package.json | 2 +- .../svelte/src/internal/client/context.js | 9 +- .../client/dom/elements/transitions.js | 11 +- packages/svelte/src/internal/client/proxy.js | 16 +-- .../src/internal/client/reactivity/props.js | 8 -- .../src/internal/client/reactivity/sources.js | 8 ++ .../svelte/src/internal/client/runtime.js | 22 ++-- .../svelte/src/internal/client/types.d.ts | 2 - packages/svelte/src/motion/spring.js | 12 +- packages/svelte/src/motion/tweened.js | 6 +- .../src/reactivity/create-subscriber.js | 3 +- packages/svelte/src/reactivity/map.js | 28 +++-- packages/svelte/src/reactivity/set.js | 21 +++- .../src/reactivity/url-search-params.js | 3 +- packages/svelte/src/reactivity/utils.js | 7 -- packages/svelte/src/version.js | 2 +- .../side-effect-derived-map/_config.js | 33 +++++- .../side-effect-derived-map/main.svelte | 106 +++++++++++++++--- .../side-effect-derived-set/_config.js | 12 +- .../side-effect-derived-set/main.svelte | 51 ++++++--- .../side-effect-derived-spring/_config.js | 23 ++++ .../side-effect-derived-spring/main.svelte | 26 +++++ .../side-effect-derived-tween/_config.js | 23 ++++ .../side-effect-derived-tween/main.svelte | 26 +++++ 27 files changed, 386 insertions(+), 130 deletions(-) delete mode 100644 .changeset/light-rivers-jump.md delete mode 100644 packages/svelte/src/reactivity/utils.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte diff --git a/.changeset/light-rivers-jump.md b/.changeset/light-rivers-jump.md deleted file mode 100644 index 2454d5715602..000000000000 --- a/.changeset/light-rivers-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: re-evaluate derived props during teardown diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 71df3242e8f1..7c6b74037092 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -8,9 +8,17 @@ jobs: trigger: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') + permissions: + issues: write # to add / delete reactions + pull-requests: read # to read PR data + actions: read # to check workflow status + contents: read # to clone the repo steps: - - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - - uses: actions/github-script@v6 + - name: monitor action permissions + uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - name: check user authorization # user needs triage permission + uses: actions/github-script@v7 + id: check-permissions with: script: | const user = context.payload.sender.login @@ -29,7 +37,7 @@ jobs: } if (hasTriagePermission) { - console.log('Allowed') + console.log('User is allowed. Adding +1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -37,16 +45,18 @@ jobs: content: '+1', }) } else { - console.log('Not allowed') + console.log('User is not allowed. Adding -1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: '-1', }) - throw new Error('not allowed') + throw new Error('User does not have the necessary permissions.') } - - uses: actions/github-script@v6 + + - name: Get PR Data + uses: actions/github-script@v7 id: get-pr-data with: script: | @@ -59,21 +69,27 @@ jobs: return { num: context.issue.number, branchName: pr.head.ref, + commit: pr.head.sha, repo: pr.head.repo.full_name } - - id: generate-token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0 + + - name: Generate Token + id: generate-token + uses: actions/create-github-app-token@v2 with: - app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} - private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} - repository: '${{ github.repository_owner }}/svelte-ecosystem-ci' - - uses: actions/github-script@v6 + app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} + private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} + repositories: | + svelte + svelte-ecosystem-ci + + - name: Trigger Downstream Workflow + uses: actions/github-script@v7 id: trigger env: COMMENT: ${{ github.event.comment.body }} with: github-token: ${{ steps.generate-token.outputs.token }} - result-encoding: string script: | const comment = process.env.COMMENT.trim() const prData = ${{ steps.get-pr-data.outputs.result }} @@ -89,6 +105,7 @@ jobs: prNumber: '' + prData.num, branchName: prData.branchName, repo: prData.repo, + commit: prData.commit, suite: suite === '' ? '-' : suite } }) diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 89ae38084029..19aa1466c069 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.35.5 + +### Patch Changes + +- fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction ([#16325](https://github.com/sveltejs/svelte/pull/16325)) + +- fix: re-evaluate derived props during teardown ([#16278](https://github.com/sveltejs/svelte/pull/16278)) + ## 5.35.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7c4fd88617c1..378fad72caf5 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.35.4", + "version": "5.35.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 93c14e6c051b..18d906890de8 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -146,20 +146,15 @@ export function getAllContexts() { * @returns {void} */ export function push(props, runes = false, fn) { - var ctx = (component_context = { + component_context = { p: component_context, c: null, - d: false, e: null, m: false, s: props, x: null, l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null - }); - - teardown(() => { - /** @type {ComponentContext} */ (ctx).d = true; - }); + }; if (DEV) { // component function diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 38100e982cce..00fad9ffdb58 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -209,21 +209,14 @@ export function transition(flags, element, get_fn, get_params) { var outro; function get_options() { - var previous_reaction = active_reaction; - var previous_effect = active_effect; - set_active_reaction(null); - set_active_effect(null); - try { + return without_reactive_context(() => { // If a transition is still ongoing, we use the existing options rather than generating // new ones. This ensures that reversible transitions reverse smoothly, rather than // jumping to a new spot because (for example) a different `duration` was used return (current_options ??= get_fn()(element, get_params?.() ?? /** @type {P} */ ({}), { direction })); - } finally { - set_active_reaction(previous_reaction); - set_active_effect(previous_effect); - } + }); } /** @type {TransitionManager} */ diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index d9063aee3436..97c8da9d33d6 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -8,7 +8,7 @@ import { is_array, object_prototype } from '../shared/utils.js'; -import { state as source, set } from './reactivity/sources.js'; +import { state as source, set, increment } from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; @@ -118,7 +118,7 @@ export function proxy(value) { if (prop in target) { const s = with_parent(() => source(UNINITIALIZED, stack)); sources.set(prop, s); - update_version(version); + increment(version); if (DEV) { tag(s, get_label(path, prop)); @@ -136,7 +136,7 @@ export function proxy(value) { } } set(s, UNINITIALIZED); - update_version(version); + increment(version); } return true; @@ -304,7 +304,7 @@ export function proxy(value) { } } - update_version(version); + increment(version); } return true; @@ -343,14 +343,6 @@ function get_label(path, prop) { return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`; } -/** - * @param {Source} signal - * @param {1 | -1} [d] - */ -function update_version(signal, d = 1) { - set(signal, signal.v + d); -} - /** * @param {any} value */ diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 03daad5251f2..f39d45bb049c 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -268,14 +268,6 @@ export function spread_props(...props) { return new Proxy({ props }, spread_props_handler); } -/** - * @param {Derived} current_value - * @returns {boolean} - */ -function has_destroyed_component_ctx(current_value) { - return current_value.ctx?.d ?? false; -} - /** * This function is responsible for synchronizing a possibly bound prop with the inner component state. * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 75a65b167802..4185fa1954e0 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -264,6 +264,14 @@ export function update_pre(source, d = 1) { return set(source, d === 1 ? ++value : --value); } +/** + * Silently (without using `get`) increment a source + * @param {Source} source + */ +export function increment(source) { + set(source, source.v + 1); +} + /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e10115c6feae..d62df4fb5b7e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -133,6 +133,8 @@ export let write_version = 1; /** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */ let read_version = 0; +export let update_version = read_version; + // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; @@ -237,17 +239,17 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) var reactions = signal.reactions; if (reactions === null) return; + if ( + !async_mode_flag && + source_ownership?.reaction === active_reaction && + source_ownership.sources.includes(signal) + ) { + return; + } + for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; - if ( - !async_mode_flag && - source_ownership?.reaction === active_reaction && - source_ownership.sources.includes(signal) - ) { - continue; - } - if ((reaction.f & DERIVED) !== 0) { schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); } else if (effect === reaction) { @@ -271,6 +273,7 @@ export function update_reaction(reaction) { var previous_reaction_sources = source_ownership; var previous_component_context = component_context; var previous_untracking = untracking; + var previous_update_version = update_version; var flags = reaction.f; @@ -284,7 +287,7 @@ export function update_reaction(reaction) { source_ownership = null; set_component_context(reaction.ctx); untracking = false; - read_version++; + update_version = ++read_version; if (reaction.ac !== null) { reaction.ac.abort(STALE_REACTION); @@ -376,6 +379,7 @@ export function update_reaction(reaction) { source_ownership = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; + update_version = previous_update_version; } } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index eb386ce583fb..916169e9ffbf 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -14,8 +14,6 @@ export type ComponentContext = { p: null | ComponentContext; /** context */ c: null | Map; - /** destroyed */ - d: boolean; /** deferred effects */ e: null | Array<{ fn: () => void | (() => void); diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 0f3bc6fb9f87..44be1a501b91 100644 --- a/packages/svelte/src/motion/spring.js +++ b/packages/svelte/src/motion/spring.js @@ -5,7 +5,7 @@ import { writable } from '../store/shared/index.js'; import { loop } from '../internal/client/loop.js'; import { raf } from '../internal/client/timing.js'; import { is_date } from './utils.js'; -import { set, source } from '../internal/client/reactivity/sources.js'; +import { set, state } from '../internal/client/reactivity/sources.js'; import { render_effect } from '../internal/client/reactivity/effects.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; @@ -170,9 +170,9 @@ export function spring(value, opts = {}) { * @since 5.8.0 */ export class Spring { - #stiffness = source(0.15); - #damping = source(0.8); - #precision = source(0.01); + #stiffness = state(0.15); + #damping = state(0.8); + #precision = state(0.01); #current; #target; @@ -194,8 +194,8 @@ export class Spring { * @param {SpringOpts} [options] */ constructor(value, options = {}) { - this.#current = DEV ? tag(source(value), 'Spring.current') : source(value); - this.#target = DEV ? tag(source(value), 'Spring.target') : source(value); + this.#current = DEV ? tag(state(value), 'Spring.current') : state(value); + this.#target = DEV ? tag(state(value), 'Spring.target') : state(value); if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1); if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1); diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index 09bd06c325d5..437c22ec3b2b 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -6,7 +6,7 @@ import { raf } from '../internal/client/timing.js'; import { loop } from '../internal/client/loop.js'; import { linear } from '../easing/index.js'; import { is_date } from './utils.js'; -import { set, source } from '../internal/client/reactivity/sources.js'; +import { set, state } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get, render_effect } from 'svelte/internal/client'; import { DEV } from 'esm-env'; @@ -191,8 +191,8 @@ export class Tween { * @param {TweenedOptions} options */ constructor(value, options = {}) { - this.#current = source(value); - this.#target = source(value); + this.#current = state(value); + this.#target = state(value); this.#defaults = options; if (DEV) { diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index df36064e9644..dcbc5df9fe24 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -1,8 +1,7 @@ import { get, tick, untrack } from '../internal/client/runtime.js'; import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; -import { source } from '../internal/client/reactivity/sources.js'; +import { source, increment } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; -import { increment } from './utils.js'; import { DEV } from 'esm-env'; /** diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index cd2fac163fc6..014b5e7c7ca1 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -1,9 +1,8 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { set, source, state } from '../internal/client/reactivity/sources.js'; +import { set, source, state, increment } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; -import { increment } from './utils.js'; +import { get, update_version } from '../internal/client/runtime.js'; /** * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object. @@ -56,6 +55,7 @@ export class SvelteMap extends Map { #sources = new Map(); #version = state(0); #size = state(0); + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -79,6 +79,19 @@ export class SvelteMap extends Map { } } + /** + * If the source is being created inside the same reaction as the SvelteMap instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + return update_version === this.#update_version ? state(value) : source(value); + } + /** @param {K} key */ has(key) { var sources = this.#sources; @@ -87,7 +100,7 @@ export class SvelteMap extends Map { if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -123,7 +136,7 @@ export class SvelteMap extends Map { if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -154,7 +167,7 @@ export class SvelteMap extends Map { var version = this.#version; if (s === undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -219,8 +232,7 @@ export class SvelteMap extends Map { if (this.#size.v !== sources.size) { for (var key of super.keys()) { if (!sources.has(key)) { - var s = source(0); - + var s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); } diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 8a656c2bc14a..d7c2deeaae86 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -1,9 +1,8 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { source, set, state } from '../internal/client/reactivity/sources.js'; +import { source, set, state, increment } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; -import { increment } from './utils.js'; +import { get, update_version } from '../internal/client/runtime.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; @@ -50,6 +49,7 @@ export class SvelteSet extends Set { #sources = new Map(); #version = state(0); #size = state(0); + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -75,6 +75,19 @@ export class SvelteSet extends Set { if (!inited) this.#init(); } + /** + * If the source is being created inside the same reaction as the SvelteSet instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + return update_version === this.#update_version ? state(value) : source(value); + } + // We init as part of the first instance so that we can treeshake this class #init() { inited = true; @@ -116,7 +129,7 @@ export class SvelteSet extends Set { return false; } - s = source(true); + s = this.#source(true); if (DEV) { tag(s, `SvelteSet has(${label(value)})`); diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 389da7cdb67a..2381e118755d 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -1,9 +1,8 @@ import { DEV } from 'esm-env'; -import { state } from '../internal/client/reactivity/sources.js'; +import { state, increment } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { get_current_url } from './url.js'; -import { increment } from './utils.js'; export const REPLACE = Symbol(); diff --git a/packages/svelte/src/reactivity/utils.js b/packages/svelte/src/reactivity/utils.js deleted file mode 100644 index cd55e0e0baac..000000000000 --- a/packages/svelte/src/reactivity/utils.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @import { Source } from '#client' */ -import { set } from '../internal/client/reactivity/sources.js'; - -/** @param {Source} source */ -export function increment(source) { - set(source, source.v + 1); -} diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c4111b9e8d41..eb68753d7129 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.4'; +export const VERSION = '5.35.5'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js index 5ad6f57e311f..c10dc7fb555f 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js @@ -8,7 +8,8 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4, button5, button6, button7, button8] = + target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +20,35 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); + + assert.throws(() => { + button5?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button6?.click(); + flushSync(); + }); + + assert.throws(() => { + button7?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button8?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte index bdd5ccb75c91..c37f37ceb6b7 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte @@ -1,27 +1,101 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throw_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has} + {throw_has} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has} + {works_has} {/if} + +{#if outside_get} + {throw_get} +{/if} + +{#if inside_get} + {works_get} +{/if} + + +{#if outside_values} + {throw_values} +{/if} + +{#if inside_values} + {works_values} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js index 5ad6f57e311f..5cf066fb8a0c 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js @@ -8,7 +8,7 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4] = target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +19,15 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte index 8564f6e7c48e..1d6735ba64b4 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte @@ -1,27 +1,52 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has_delete} + {throws_has_delete} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has_delete} + {works_has_delete} {/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js new file mode 100644 index 000000000000..5ad6f57e311f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte new file mode 100644 index 000000000000..b0818deca960 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js new file mode 100644 index 000000000000..5ad6f57e311f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte new file mode 100644 index 000000000000..bd007f2b500d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file From 16f960eba7bd4ecdfac07420c5f2864193ef2973 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 12:08:08 -0400 Subject: [PATCH 546/582] WIP --- .../svelte/src/internal/client/context.js | 51 ++++++++----------- .../src/internal/client/reactivity/effects.js | 1 + 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 18d906890de8..4e9eac6b017e 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -169,37 +169,30 @@ export function push(props, runes = false, fn) { * @returns {T} */ export function pop(component) { - const context_stack_item = component_context; - if (context_stack_item !== null) { - if (component !== undefined) { - context_stack_item.x = component; - } - const component_effects = context_stack_item.e; - if (component_effects !== null) { - var previous_effect = active_effect; - var previous_reaction = active_reaction; - context_stack_item.e = null; - try { - for (var i = 0; i < component_effects.length; i++) { - var component_effect = component_effects[i]; - set_active_effect(component_effect.effect); - set_active_reaction(component_effect.reaction); - create_user_effect(component_effect.fn); - } - } finally { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - } - } - component_context = context_stack_item.p; - if (DEV) { - dev_current_component_function = context_stack_item.p?.function ?? null; + var context = /** @type {ComponentContext} */ (component_context); + var effects = context.e; + + if (effects !== null) { + context.e = null; + + for (var effect of effects) { + create_user_effect(effect.fn); } - context_stack_item.m = true; } - // Micro-optimization: Don't set .a above to the empty object - // so it can be garbage-collected when the return here is unused - return component || /** @type {T} */ ({}); + + if (component !== undefined) { + context.x = component; + } + + context.m = true; + + component_context = context.p; + + if (DEV) { + dev_current_component_function = context.p?.function ?? null; + } + + return component ?? /** @type {T} */ ({}); } /** @returns {boolean} */ diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 92065f44d036..f5d0d76aa865 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -186,6 +186,7 @@ export function user_effect(fn) { // Non-nested `$effect(...)` in a component should be deferred // until the component is mounted var defer = + active_reaction === null && active_effect !== null && (active_effect.f & BRANCH_EFFECT) !== 0 && component_context !== null && From 99fa11b4fce68b4aa7d22de9070240baded87805 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 12:10:10 -0400 Subject: [PATCH 547/582] WIP --- packages/svelte/src/internal/client/context.js | 4 ++-- packages/svelte/src/internal/client/reactivity/effects.js | 6 +----- packages/svelte/src/internal/client/types.d.ts | 6 +----- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 4e9eac6b017e..b07ae27e5130 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -175,8 +175,8 @@ export function pop(component) { if (effects !== null) { context.e = null; - for (var effect of effects) { - create_user_effect(effect.fn); + for (var fn of effects) { + create_user_effect(fn); } } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index f5d0d76aa865..6ef7c539a93a 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -200,11 +200,7 @@ export function user_effect(fn) { if (defer) { var context = /** @type {ComponentContext} */ (component_context); - (context.e ??= []).push({ - fn, - effect: active_effect, - reaction: active_reaction - }); + (context.e ??= []).push(fn); } else { return create_user_effect(fn); } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 916169e9ffbf..a7ef638ea3bd 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -15,11 +15,7 @@ export type ComponentContext = { /** context */ c: null | Map; /** deferred effects */ - e: null | Array<{ - fn: () => void | (() => void); - effect: null | Effect; - reaction: null | Reaction; - }>; + e: null | Array<() => void | (() => void)>; /** mounted */ m: boolean; /** From 0244319608f3baba923cc8586429f0e57b9dc0c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 12:12:01 -0400 Subject: [PATCH 548/582] WIP --- packages/svelte/src/internal/client/reactivity/effects.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 6ef7c539a93a..e77a66d295e6 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -189,8 +189,7 @@ export function user_effect(fn) { active_reaction === null && active_effect !== null && (active_effect.f & BRANCH_EFFECT) !== 0 && - component_context !== null && - !component_context.m; + (active_effect.f & EFFECT_RAN) === 0; if (DEV) { define_property(fn, 'name', { From ab48be10466c5d9191258237bbf6ff5c3a90817b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 12:24:41 -0400 Subject: [PATCH 549/582] tweak --- .../src/internal/client/reactivity/effects.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index e77a66d295e6..5c07d852fbd8 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -183,24 +183,23 @@ export function teardown(fn) { export function user_effect(fn) { validate_effect('$effect'); - // Non-nested `$effect(...)` in a component should be deferred - // until the component is mounted - var defer = - active_reaction === null && - active_effect !== null && - (active_effect.f & BRANCH_EFFECT) !== 0 && - (active_effect.f & EFFECT_RAN) === 0; - if (DEV) { define_property(fn, 'name', { value: '$effect' }); } + // Non-nested `$effect(...)` in a component should be deferred + // until the component is mounted + var flags = /** @type {Effect} */ (active_effect).f; + var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0; + if (defer) { + // Top-level `$effect(...)` in an unmounted component — defer until mount var context = /** @type {ComponentContext} */ (component_context); (context.e ??= []).push(fn); } else { + // Everything else — create immediately return create_user_effect(fn); } } From f4055960c562ca6cfd0b594eb95b7e2d8c571abf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 12:30:27 -0400 Subject: [PATCH 550/582] tidy up --- .../svelte/src/internal/client/context.js | 21 +++++++------------ .../svelte/src/internal/client/types.d.ts | 2 -- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index b07ae27e5130..9f948de18ff8 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,17 +1,12 @@ -/** @import { ComponentContext, DevStackEntry } from '#client' */ - +/** @import { ComponentContext, DevStackEntry, Effect } from '#client' */ import { DEV } from 'esm-env'; import { lifecycle_outside_component } from '../shared/errors.js'; import * as e from './errors.js'; -import { - active_effect, - active_reaction, - set_active_effect, - set_active_reaction -} from './runtime.js'; -import { create_user_effect, teardown } from './reactivity/effects.js'; +import { active_effect, active_reaction } from './runtime.js'; +import { create_user_effect } from './reactivity/effects.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; +import { BRANCH_EFFECT, EFFECT_RAN } from './constants.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -105,7 +100,10 @@ export function setContext(key, context) { const context_map = get_or_init_context_map('setContext'); if (async_mode_flag) { - if (/** @type {ComponentContext} */ (component_context).m) { + var flags = /** @type {Effect} */ (active_effect).f; + var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0; + + if (!valid) { e.set_context_after_init(); } } @@ -150,7 +148,6 @@ export function push(props, runes = false, fn) { p: component_context, c: null, e: null, - m: false, s: props, x: null, l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null @@ -184,8 +181,6 @@ export function pop(component) { context.x = component; } - context.m = true; - component_context = context.p; if (DEV) { diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index a7ef638ea3bd..d24218c4d3b0 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -16,8 +16,6 @@ export type ComponentContext = { c: null | Map; /** deferred effects */ e: null | Array<() => void | (() => void)>; - /** mounted */ - m: boolean; /** * props — needed for legacy mode lifecycle functions, and for `createEventDispatcher` * @deprecated remove in 6.0 From c419ab0eec9f48b4aaca048b0bcc08d7ea2ac78d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 16:23:32 -0400 Subject: [PATCH 551/582] dont update derived status when time-travelling --- .../src/internal/client/reactivity/deriveds.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 2012bb81ac77..c65980405451 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -33,7 +33,7 @@ import { tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { current_batch } from './batch.js'; +import { batch_deriveds, current_batch } from './batch.js'; /** @type {Effect | null} */ export let current_async_effect = null; @@ -328,8 +328,12 @@ export function update_derived(derived) { // cleanup function, or it will cache a stale value if (is_destroying_effect) return; - var status = - (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; + if (batch_deriveds !== null) { + batch_deriveds.set(derived, derived.v); + } else { + var status = + (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; - set_signal_status(derived, status); + set_signal_status(derived, status); + } } From af7d4849413df9655eb3b21b2d6a193d5dfaa008 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 16:31:53 -0400 Subject: [PATCH 552/582] tidy up --- packages/svelte/src/internal/client/runtime.js | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8bdd2061e3f0..f5e164896f5e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -603,10 +603,7 @@ export function get(signal) { } } - // if this is a derived, we may need to update it, but - // not if `batch_deriveds` is not null (meaning we're - // currently time travelling)) - if (is_derived && !is_destroying_effect && batch_deriveds === null) { + if (is_derived && !is_destroying_effect) { derived = /** @type {Derived} */ (signal); if (is_dirty(derived)) { @@ -684,19 +681,6 @@ export function get(signal) { } } - // if we're time travelling, we don't want to update the - // intrinsic value of the derived — we want to compute it - // once and stash it for the duration of batch processing - if (is_derived && batch_deriveds !== null) { - derived = /** @type {Derived} */ (signal); - - if (!batch_deriveds.has(derived)) { - batch_deriveds.set(derived, execute_derived(derived)); - } - - return batch_deriveds.get(derived); - } - if ((signal.f & ERROR_VALUE) !== 0) { throw signal.v; } From 3566d218c2f63d9e299912aad22417838818a566 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 16:33:24 -0400 Subject: [PATCH 553/582] tidy up --- .../svelte/src/internal/client/reactivity/sources.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 522af2675c9c..66cb673d0434 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -275,10 +275,9 @@ export function increment(source) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY - * @param {boolean} partial should skip async/block effects * @returns {void} */ -export function mark_reactions(signal, status, partial = false) { +export function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; @@ -298,17 +297,13 @@ export function mark_reactions(signal, status, partial = false) { continue; } - if (partial && (flags & (EFFECT_ASYNC | BLOCK_EFFECT)) !== 0) { - continue; - } - if (status === DIRTY || (flags & DIRTY) === 0) { // don't make a DIRTY signal MAYBE_DIRTY set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY, partial); + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); } else { schedule_effect(/** @type {Effect} */ (reaction)); } From bfa9f04e492dee69ee0d20271f527119bef5a179 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 16:43:10 -0400 Subject: [PATCH 554/582] tag async deriveds --- .../3-transform/client/visitors/VariableDeclaration.js | 7 +++++-- packages/svelte/src/internal/client/dev/tracing.js | 4 ++-- .../svelte/src/internal/client/reactivity/deriveds.js | 9 ++++++++- 3 files changed, 15 insertions(+), 5 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 d32f17b97a15..acf3bd6f44b6 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 @@ -208,13 +208,16 @@ export function VariableDeclaration(node, context) { if (is_async) { const location = dev && is_ignored(init, 'await_waterfall') && locate_node(init); - const call = b.call( + let call = b.call( '$.async_derived', b.thunk(expression, true), location ? b.literal(location) : undefined ); - declarations.push(b.declarator(declarator.id, b.call(b.await(b.call('$.save', call))))); + call = b.call(b.await(b.call('$.save', call))); + if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); + + declarations.push(b.declarator(declarator.id, call)); } else { if (rune === '$derived') expression = b.thunk(expression); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 5834f5bffd0e..f5dce1cb2963 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -2,7 +2,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; import { define_property } from '../../shared/utils.js'; -import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; +import { DERIVED, EFFECT_ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { effect_tracking } from '../reactivity/effects.js'; import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js'; @@ -26,7 +26,7 @@ function log_entry(signal, entry) { return; } - const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state'; + const type = (signal.f & (DERIVED | EFFECT_ASYNC)) !== 0 ? '$derived' : '$state'; const current_reaction = /** @type {Reaction} */ (active_reaction); const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0; const style = dirty diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c65980405451..c202dd10d1b1 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -10,7 +10,8 @@ import { MAYBE_DIRTY, STALE_REACTION, UNOWNED, - DESTROYED + DESTROYED, + EFFECT_ASYNC } from '#client/constants'; import { active_reaction, @@ -188,6 +189,12 @@ export function async_derived(fn, location) { promise.then(handler, (e) => handler(null, e || 'unknown')); }); + if (DEV) { + // add a flag that lets this be printed as a derived + // when using `$inspect.trace()` + signal.f |= EFFECT_ASYNC; + } + return new Promise((fulfil) => { /** @param {Promise} p */ function next(p) { From 67ad3bcb0bba87580d9b7bdb19d6e5fc093cf4f7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 21:04:46 -0400 Subject: [PATCH 555/582] tweak --- packages/svelte/src/internal/client/runtime.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index f5e164896f5e..b5f6822207ee 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -603,14 +603,6 @@ export function get(signal) { } } - if (is_derived && !is_destroying_effect) { - derived = /** @type {Derived} */ (signal); - - if (is_dirty(derived)) { - update_derived(derived); - } - } - if (DEV) { if (current_async_effect) { var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; @@ -679,6 +671,16 @@ export function get(signal) { return value; } + } else if (is_derived) { + derived = /** @type {Derived} */ (signal); + + if (batch_deriveds?.has(derived)) { + return batch_deriveds.get(derived); + } + + if (is_dirty(derived)) { + update_derived(derived); + } } if ((signal.f & ERROR_VALUE) !== 0) { From 7a43163fff4d2674d59b5f21d85d2ca38b12ce70 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Jul 2025 11:42:22 -0400 Subject: [PATCH 556/582] bail out of secondary flushes --- .../svelte/src/internal/client/reactivity/batch.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 59edfda5f916..3a9a434b5487 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,5 +1,6 @@ /** @import { Derived, Effect, Source } from '#client' */ import { + BLOCK_EFFECT, BRANCH_EFFECT, CLEAN, DESTROYED, @@ -53,6 +54,8 @@ let queued_root_effects = []; /** @type {Effect | null} */ let last_scheduled_effect = null; +let is_flushing = false; + export class Batch { /** * The current values of any sources that are updated in this batch @@ -310,6 +313,7 @@ export class Batch { flush_effects() { var was_updating_effect = is_updating_effect; + is_flushing = true; try { var flush_count = 0; @@ -324,6 +328,7 @@ export class Batch { old_values.clear(); } } finally { + is_flushing = false; set_is_updating_effect(was_updating_effect); last_scheduled_effect = null; @@ -541,6 +546,12 @@ export function schedule_effect(signal) { effect = effect.parent; var flags = effect.f; + // if the effect is being scheduled because a parent (each/await/etc) block + // updated an internal source, bail out or we'll cause a second flush + if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) { + return; + } + if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) { if ((flags & CLEAN) === 0) return; effect.f ^= CLEAN; From 7d853cf27ba6f5f8d60d5244748669ec7139a00a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Jul 2025 17:21:15 -0400 Subject: [PATCH 557/582] re-run blocks on subsequent flushes --- .../src/internal/client/reactivity/batch.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 3a9a434b5487..28714ca9c999 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -124,6 +124,13 @@ export class Batch { */ #effects = []; + /** + * Block effects, which may need to re-run on subsequent flushes + * in order to update internal sources (e.g. each block items) + * @type {Effect[]} + */ + #block_effects = []; + /** * A set of branches that still exist, but will be destroyed when this batch * is committed — we skip over these during `process` @@ -177,6 +184,7 @@ export class Batch { this.#render_effects = []; this.#effects = []; + this.#block_effects = []; this.#commit(); @@ -188,6 +196,7 @@ export class Batch { // otherwise mark effects clean so they get scheduled on the next run for (const e of this.#render_effects) set_signal_status(e, CLEAN); for (const e of this.#effects) set_signal_status(e, CLEAN); + for (const e of this.#block_effects) set_signal_status(e, CLEAN); } if (current_values) { @@ -243,6 +252,7 @@ export class Batch { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); } else { + if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); update_effect(effect); } } @@ -367,6 +377,11 @@ export class Batch { schedule_effect(e); } + for (const e of this.#block_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + this.#render_effects = []; this.#effects = []; From eaf4d0d808bb1dd979050a3e38fb95fe47be2c74 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Jul 2025 17:51:33 -0400 Subject: [PATCH 558/582] add test --- .../samples/async-block-rerun/_config.js | 53 +++++++++++++++++++ .../samples/async-block-rerun/main.svelte | 45 ++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js new file mode 100644 index 000000000000..18175de4dcb6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js @@ -0,0 +1,53 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [override, release, resolve] = target.querySelectorAll('button'); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

    before

    +

    before

    + ` + ); + + override.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

    during

    +

    during

    + ` + ); + + release.click(); + await tick(); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

    after

    +

    after

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte new file mode 100644 index 000000000000..256ad68f4af7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte @@ -0,0 +1,45 @@ + + + + + + + + + + {#each await indirect() as entry} +

    {entry}

    + {/each} + + {#each current as entry} +

    {entry}

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

    pending...

    + {/snippet} +
    From c2e6b28a550af3026f998afaacf1a1dff6e232b6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 11 Jul 2025 23:13:19 -0400 Subject: [PATCH 559/582] fix --- .../svelte/src/internal/client/reactivity/batch.js | 8 +++++--- .../svelte/src/internal/client/reactivity/deriveds.js | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 28714ca9c999..9d709a421113 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -304,7 +304,7 @@ export class Batch { flush() { if (queued_root_effects.length > 0) { this.flush_effects(); - } else if (!this.#neutered) { + } else { this.#commit(); } @@ -352,8 +352,10 @@ export class Batch { * Append and remove branches to/from the DOM */ #commit() { - for (const fn of this.#callbacks) { - fn(); + if (!this.#neutered) { + for (const fn of this.#callbacks) { + fn(); + } } this.#callbacks.clear(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index c202dd10d1b1..0580918c9c0d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -146,10 +146,6 @@ export function async_derived(fn, location) { const handler = (value, error = undefined) => { prev = null; - if ((parent.f & DESTROYED) !== 0) { - batch.neuter(); - } - current_async_effect = null; if (!pending) batch.activate(); @@ -187,6 +183,12 @@ export function async_derived(fn, location) { }; promise.then(handler, (e) => handler(null, e || 'unknown')); + + if (batch) { + return () => { + queueMicrotask(() => batch.neuter()); + }; + } }); if (DEV) { From 12188f37f67d816c7e3cd9b9123ba9b564b3bb60 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 12 Jul 2025 12:00:50 -0400 Subject: [PATCH 560/582] add tests, one failing --- .../_config.js | 27 +++++++++++++++++ .../main.svelte | 28 ++++++++++++++++++ .../_config.js | 27 +++++++++++++++++ .../main.svelte | 29 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js new file mode 100644 index 000000000000..a29c99860dab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

    false

    +

    1

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte new file mode 100644 index 000000000000..a93eb7dc254c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte @@ -0,0 +1,28 @@ + + + + + + + {#if count % 2 === 0} +

    true

    +

    {await push()}

    + {:else} +

    false

    +

    {await push()}

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

    loading...

    + {/snippet} +
    diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js new file mode 100644 index 000000000000..a29c99860dab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

    false

    +

    1

    + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte new file mode 100644 index 000000000000..1ad6cb84decc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte @@ -0,0 +1,29 @@ + + + + + + + + {#if count % 2 === 0} +

    true

    + {#each await push() as count}

    {count}

    {/each} + {:else} +

    false

    + {#each await push() as count}

    {count}

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

    loading...

    + {/snippet} +
    From 4d8432a0f2324970b509fe01c6eb57d7767ca826 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 12 Jul 2025 12:29:23 -0400 Subject: [PATCH 561/582] fix --- .../svelte/src/internal/client/dom/blocks/async.js | 11 +++++++---- .../svelte/src/internal/client/reactivity/async.js | 7 ++++--- .../async-block-reject-each-during-init/_config.js | 11 ++++++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 9e8a4ed0d314..82f107ab29a1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -14,10 +14,13 @@ export function async(node, expressions, fn) { boundary.update_pending_count(1); flatten([], expressions, (values) => { - // get values eagerly to avoid creating blocks if they reject - for (const d of values) get(d); + try { + // get values eagerly to avoid creating blocks if they reject + for (const d of values) get(d); - fn(node, ...values); - boundary.update_pending_count(-1); + fn(node, ...values); + } finally { + boundary.update_pending_count(-1); + } }); } diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index c4ff5eebf86a..cd40bf046201 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -27,8 +27,6 @@ export function flatten(sync, async, fn) { Promise.all(async.map((expression) => async_derived(expression))) .then((result) => { - if ((parent.f & DESTROYED) !== 0) return; - batch?.activate(); restore(); @@ -36,7 +34,10 @@ export function flatten(sync, async, fn) { try { fn([...sync.map(d), ...result]); } catch (error) { - invoke_error_boundary(error, parent); + // ignore errors in blocks that have already been destroyed + if ((parent.f & DESTROYED) === 0) { + invoke_error_boundary(error, parent); + } } batch?.deactivate(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js index a29c99860dab..cd89439a7220 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -2,23 +2,24 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - async test({ assert, target }) { - const [increment, shift] = target.querySelectorAll('button'); + async test({ assert, target, errors }) { + const [increment, resolve, reject] = target.querySelectorAll('button'); increment.click(); await tick(); - shift.click(); + reject.click(); await tick(); - shift.click(); + resolve.click(); await tick(); assert.htmlEqual( target.innerHTML, ` - + +

    false

    1

    ` From 6c6233f14010c6eaf0342ac515b16024d99abe89 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 12 Jul 2025 13:29:56 -0400 Subject: [PATCH 562/582] flesh out await_waterfall message --- .../.generated/client-warnings.md | 23 +++++++++++++++++-- .../messages/client-warnings/warnings.md | 23 +++++++++++++++++-- .../client/visitors/VariableDeclaration.js | 2 +- .../internal/client/reactivity/deriveds.js | 2 +- .../svelte/src/internal/client/warnings.js | 7 +++--- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 60cb02a1eeb2..1c75faef5377 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -71,10 +71,29 @@ let total = $derived(await sum(a, b)); ### await_waterfall ``` -An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app +An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app ``` -TODO +In a case like this... + +```js +let a = $derived(await one()); +let b = $derived(await two()); +``` + +...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary. + +(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.) + +You can solve this by creating the promises first and _then_ awaiting them: + +```js +let aPromise = $derived(one()); +let bPromise = $derived(two()); + +let a = $derived(await aPromise); +let b = $derived(await bPromise); +``` ### binding_property_non_reactive diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index e4390318eb53..498c19a54756 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -64,9 +64,28 @@ let total = $derived(await sum(a, b)); ## await_waterfall -> An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app +> An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app -TODO +In a case like this... + +```js +let a = $derived(await one()); +let b = $derived(await two()); +``` + +...the second `$derived` will not be created until the first one has resolved. Since `await two()` does not depend on the value of `a`, this delay, often described as a 'waterfall', is unnecessary. + +(Note that if the values of `await one()` and `await two()` subsequently change, they can do so concurrently — the waterfall only occurs when the deriveds are first created.) + +You can solve this by creating the promises first and _then_ awaiting them: + +```js +let aPromise = $derived(one()); +let bPromise = $derived(two()); + +let a = $derived(await aPromise); +let b = $derived(await bPromise); +``` ## binding_property_non_reactive 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 acf3bd6f44b6..19a7de57159d 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 @@ -207,7 +207,7 @@ export function VariableDeclaration(node, context) { ); if (is_async) { - const location = dev && is_ignored(init, 'await_waterfall') && locate_node(init); + const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init); let call = b.call( '$.async_derived', b.thunk(expression, true), diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 0580918c9c0d..ecddcd671ea2 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -169,7 +169,7 @@ export function async_derived(fn, location) { setTimeout(() => { if (recent_async_deriveds.has(signal)) { - w.await_waterfall(location); + w.await_waterfall(/** @type {string} */ (signal.label), location); recent_async_deriveds.delete(signal); } }); diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index 902b471d8b56..dfd50a8722c9 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -31,12 +31,13 @@ export function await_reactivity_loss(name) { } /** - * An async value (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app + * An async derived, `%name%` (%location%) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app + * @param {string} name * @param {string} location */ -export function await_waterfall(location) { +export function await_waterfall(name, location) { if (DEV) { - 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); + console.warn(`%c[svelte] await_waterfall\n%cAn async derived, \`${name}\` (${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 796db26626402f8e921a256d05c921e90b370446 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 12:55:25 -0400 Subject: [PATCH 563/582] tidy up --- .../svelte/scripts/process-messages/templates/client-errors.js | 1 - packages/svelte/src/internal/client/errors.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/scripts/process-messages/templates/client-errors.js b/packages/svelte/scripts/process-messages/templates/client-errors.js index b9532f0929cd..ef749b4ba3c9 100644 --- a/packages/svelte/scripts/process-messages/templates/client-errors.js +++ b/packages/svelte/scripts/process-messages/templates/client-errors.js @@ -1,5 +1,4 @@ import { DEV } from 'esm-env'; -export * from '../shared/errors.js'; export * from '../shared/errors.js'; diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index d3618db3d554..a491dc683d9e 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -423,4 +423,4 @@ export function state_unsafe_mutation() { } else { throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); } -} +} \ No newline at end of file From ffc1f6bd5e8f4da52c5087013227cf6d06c1c644 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 13:51:00 -0400 Subject: [PATCH 564/582] dry out --- .../3-transform/client/visitors/EachBlock.js | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 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 789465ac16bc..225a4f617c50 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 @@ -313,7 +313,8 @@ export function EachBlock(node, context) { } const { has_await } = node.metadata.expression; - const thunk = b.thunk(collection, has_await); + const get_collection = b.thunk(collection, has_await); + const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection; const render_args = [b.id('$$anchor'), item]; if (uses_index || collection_id) render_args.push(index); @@ -323,7 +324,7 @@ export function EachBlock(node, context) { const args = [ context.state.node, b.literal(flags), - has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : thunk, + thunk, key_function, b.arrow(render_args, b.block(declarations.concat(block.body))) ]; @@ -334,36 +335,25 @@ export function EachBlock(node, context) { ); } + const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')]; + + if (dev && node.metadata.keyed) { + statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function))); + } + if (has_await) { - const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')]; - if (dev && node.metadata.keyed) { - statements.unshift( - b.stmt( - b.call( - '$.validate_each_keys', - b.thunk(b.call('$.get', b.id('$$collection'))), - key_function - ) - ) - ); - } context.state.init.push( b.stmt( b.call( '$.async', context.state.node, - b.array([thunk]), + b.array([get_collection]), b.arrow([context.state.node, b.id('$$collection')], b.block(statements)) ) ) ); } else { - if (dev && node.metadata.keyed) { - context.state.init.push( - b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) - ); - } - context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each')); + context.state.init.push(...statements); } } From a7e0c847828e3e117b7fc2a893998a02cb343525 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 14:09:16 -0400 Subject: [PATCH 565/582] unused --- .../compiler/phases/3-transform/client/transform-client.js | 1 - .../phases/3-transform/server/visitors/SvelteBoundary.js | 2 +- packages/svelte/src/internal/client/constants.js | 2 +- packages/svelte/src/internal/client/dev/debug.js | 4 ++-- packages/svelte/src/internal/client/dev/tracing.js | 4 ++-- packages/svelte/src/internal/client/reactivity/batch.js | 4 ++-- packages/svelte/src/internal/client/reactivity/deriveds.js | 4 ++-- packages/svelte/src/internal/client/reactivity/effects.js | 4 ++-- packages/svelte/src/internal/client/reactivity/sources.js | 4 ++-- 9 files changed, 14 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 e5e51024eeb7..c42d1b95d88d 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 @@ -59,7 +59,6 @@ import { UpdateExpression } from './visitors/UpdateExpression.js'; import { UseDirective } from './visitors/UseDirective.js'; import { AttachTag } from './visitors/AttachTag.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; -import { Memoizer } from './visitors/shared/utils.js'; /** @type {Visitors} */ const visitors = { 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 a2942ffc3c8c..6e814d6384c6 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,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../../internal/server/hydration.js'; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 0716a2e4d06c..50a7a21ae80f 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -22,7 +22,7 @@ export const USER_EFFECT = 1 << 20; // Flags used for async export const REACTION_IS_UPDATING = 1 << 21; -export const EFFECT_ASYNC = 1 << 22; +export const ASYNC = 1 << 22; export const ERROR_VALUE = 1 << 23; diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index fdaa02350c01..c47080ed2f1e 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -7,7 +7,7 @@ import { CLEAN, DERIVED, EFFECT, - EFFECT_ASYNC, + ASYNC, MAYBE_DIRTY, RENDER_EFFECT, ROOT_EFFECT @@ -40,7 +40,7 @@ export function log_effect_tree(effect, depth = 0) { label = 'boundary'; } else if ((flags & BLOCK_EFFECT) !== 0) { label = 'block'; - } else if ((flags & EFFECT_ASYNC) !== 0) { + } else if ((flags & ASYNC) !== 0) { label = 'async'; } else if ((flags & BRANCH_EFFECT) !== 0) { label = 'branch'; diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index f5dce1cb2963..128942ceb293 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -2,7 +2,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; import { define_property } from '../../shared/utils.js'; -import { DERIVED, EFFECT_ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; +import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { effect_tracking } from '../reactivity/effects.js'; import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js'; @@ -26,7 +26,7 @@ function log_entry(signal, entry) { return; } - const type = (signal.f & (DERIVED | EFFECT_ASYNC)) !== 0 ? '$derived' : '$state'; + const type = (signal.f & (DERIVED | ASYNC)) !== 0 ? '$derived' : '$state'; const current_reaction = /** @type {Reaction} */ (active_reaction); const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0; const style = dirty diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9d709a421113..1e93bed01e91 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -6,7 +6,7 @@ import { DESTROYED, DIRTY, EFFECT, - EFFECT_ASYNC, + ASYNC, INERT, RENDER_EFFECT, ROOT_EFFECT, @@ -248,7 +248,7 @@ export class Batch { } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { this.#render_effects.push(effect); } else if (is_dirty(effect)) { - if ((flags & EFFECT_ASYNC) !== 0) { + if ((flags & ASYNC) !== 0) { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); } else { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index ecddcd671ea2..0a0bda4431ca 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -11,7 +11,7 @@ import { STALE_REACTION, UNOWNED, DESTROYED, - EFFECT_ASYNC + ASYNC } from '#client/constants'; import { active_reaction, @@ -194,7 +194,7 @@ export function async_derived(fn, location) { if (DEV) { // add a flag that lets this be printed as a derived // when using `$inspect.trace()` - signal.f |= EFFECT_ASYNC; + signal.f |= ASYNC; } return new Promise((fulfil) => { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 5c07d852fbd8..953f892d65e6 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -34,7 +34,7 @@ import { BOUNDARY_EFFECT, STALE_REACTION, USER_EFFECT, - EFFECT_ASYNC + ASYNC } from '#client/constants'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; @@ -332,7 +332,7 @@ export function legacy_pre_effect_reset() { * @returns {Effect} */ export function async_effect(fn) { - return create_effect(EFFECT_ASYNC | EFFECT_PRESERVED, fn, true); + return create_effect(ASYNC | EFFECT_PRESERVED, fn, true); } /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 66cb673d0434..c95257dd2412 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -27,7 +27,7 @@ import { MAYBE_DIRTY, BLOCK_EFFECT, ROOT_EFFECT, - EFFECT_ASYNC + ASYNC } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) { // to ensure we error if state is set inside an inspect effect (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && is_runes() && - (active_reaction.f & (DERIVED | BLOCK_EFFECT | EFFECT_ASYNC | INSPECT_EFFECT)) !== 0 && + (active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | INSPECT_EFFECT)) !== 0 && !current_sources?.includes(source) ) { e.state_unsafe_mutation(); From 8f47fa811946f28c95e002aae43bd37df1bcd3d7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 14:13:38 -0400 Subject: [PATCH 566/582] tweak --- .../src/internal/client/reactivity/async.js | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index cd40bf046201..7e067ff49070 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -17,35 +17,35 @@ import { async_derived, derived, derived_safe_equal } from './deriveds.js'; export function flatten(sync, async, fn) { const d = is_runes() ? derived : derived_safe_equal; - if (async.length > 0) { - var batch = current_batch; - var parent = /** @type {Effect} */ (active_effect); + if (async.length === 0) { + fn(sync.map(d)); + return; + } - var restore = capture(); + var batch = current_batch; + var parent = /** @type {Effect} */ (active_effect); - var boundary = get_pending_boundary(); + var restore = capture(); + var boundary = get_pending_boundary(); - Promise.all(async.map((expression) => async_derived(expression))) - .then((result) => { - batch?.activate(); + Promise.all(async.map((expression) => async_derived(expression))) + .then((result) => { + batch?.activate(); - restore(); + restore(); - try { - fn([...sync.map(d), ...result]); - } catch (error) { - // ignore errors in blocks that have already been destroyed - if ((parent.f & DESTROYED) === 0) { - invoke_error_boundary(error, parent); - } + try { + fn([...sync.map(d), ...result]); + } catch (error) { + // ignore errors in blocks that have already been destroyed + if ((parent.f & DESTROYED) === 0) { + invoke_error_boundary(error, parent); } + } - batch?.deactivate(); - }) - .catch((error) => { - boundary.error(error); - }); - } else { - fn(sync.map(d)); - } + batch?.deactivate(); + }) + .catch((error) => { + boundary.error(error); + }); } From 65b038119557a2c2e6ea8b159fc76e9f936a8e2e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 14:19:46 -0400 Subject: [PATCH 567/582] tidy up --- .../internal/client/dom/blocks/boundary.js | 33 ++++++++----------- 1 file changed, 13 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 69a5bd45ad0f..f270e3563aae 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -49,7 +49,6 @@ export function boundary(node, props, children) { } export class Boundary { - inert = false; pending = false; /** @type {Boundary | null} */ @@ -85,9 +84,7 @@ export class Boundary { #pending_count = 0; #is_creating_fallback = false; - /** - * @type {Source | null} - */ + /** @type {Source | null} */ #effect_pending = null; #effect_pending_subscriber = createSubscriber(() => { @@ -208,27 +205,23 @@ export class Boundary { } } - commit() { - this.pending = false; - - if (this.#pending_effect) { - pause_effect(this.#pending_effect, () => { - this.#pending_effect = null; - }); - } - - if (this.#offscreen_fragment) { - this.#anchor.before(this.#offscreen_fragment); - this.#offscreen_fragment = null; - } - } - /** @param {1 | -1} d */ #update_pending_count(d) { this.#pending_count += d; if (this.#pending_count === 0) { - this.commit(); + this.pending = false; + + if (this.#pending_effect) { + pause_effect(this.#pending_effect, () => { + this.#pending_effect = null; + }); + } + + if (this.#offscreen_fragment) { + this.#anchor.before(this.#offscreen_fragment); + this.#offscreen_fragment = null; + } } } From c67119280c897261ca0255c31e14f6d7827420a6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 14:21:37 -0400 Subject: [PATCH 568/582] TODO --- packages/svelte/src/internal/client/dom/blocks/boundary.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index f270e3563aae..b2ec72c5297b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -398,6 +398,7 @@ export function capture(track = true) { } // prevent the active effect from outstaying its welcome + // TODO this feels brittle queue_micro_task(exit); }; } From 51215957efb77c5fd6e8c647488aa7577438f7ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 14:24:40 -0400 Subject: [PATCH 569/582] tweak --- .../svelte/src/internal/client/dom/blocks/each.js | 12 ++++++------ 1 file changed, 6 insertions(+), 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 a6e4f9b6c6c8..43c75e2a3769 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -67,18 +67,18 @@ export function index(_, i) { * Pause multiple effects simultaneously, and coordinate their * subsequent destruction. Used in each blocks * @param {EachState} state - * @param {EachItem[]} to_destroy + * @param {EachItem[]} items * @param {null | Node} controlled_anchor */ -function pause_effects(state, to_destroy, controlled_anchor) { +function pause_effects(state, items, controlled_anchor) { var items_map = state.items; /** @type {TransitionManager[]} */ var transitions = []; - var length = to_destroy.length; + var length = items.length; for (var i = 0; i < length; i++) { - pause_children(to_destroy[i].e, transitions, true); + pause_children(items[i].e, transitions, true); } var is_controlled = length > 0 && transitions.length === 0 && controlled_anchor !== null; @@ -91,12 +91,12 @@ function pause_effects(state, to_destroy, controlled_anchor) { clear_text_content(parent_node); parent_node.append(/** @type {Element} */ (controlled_anchor)); items_map.clear(); - link(state, to_destroy[0].prev, to_destroy[length - 1].next); + link(state, items[0].prev, items[length - 1].next); } run_out_transitions(transitions, () => { for (var i = 0; i < length; i++) { - var item = to_destroy[i]; + var item = items[i]; if (!is_controlled) { items_map.delete(item.k); link(state, item.prev, item.next); From d291a7968df606cda97319cebc5d389323add847 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 14:34:10 -0400 Subject: [PATCH 570/582] tidy up --- packages/svelte/src/internal/client/reactivity/batch.js | 4 ++-- packages/svelte/src/internal/client/reactivity/deriveds.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 1e93bed01e91..ee14e19b0711 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -173,7 +173,7 @@ export class Batch { } for (const root of root_effects) { - this.#process_root(root); + this.#traverse_effect_tree(root); } // if we didn't start any new async work, and no async work @@ -228,7 +228,7 @@ export class Batch { * them for later execution as appropriate * @param {Effect} root */ - #process_root(root) { + #traverse_effect_tree(root) { root.f ^= CLEAN; var effect = root.first; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 0a0bda4431ca..f5957690e29c 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -10,7 +10,6 @@ import { MAYBE_DIRTY, STALE_REACTION, UNOWNED, - DESTROYED, ASYNC } from '#client/constants'; import { @@ -27,7 +26,7 @@ import { import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; -import { async_effect, destroy_effect, render_effect } from './effects.js'; +import { async_effect, 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'; From 6de770c7fd1b47caa8ebc539df05af7dbdd86405 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 14:39:33 -0400 Subject: [PATCH 571/582] remove TODO --- packages/svelte/src/internal/client/reactivity/effects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 953f892d65e6..090c8328e76c 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -141,7 +141,7 @@ function create_effect(type, fn, sync, push = true) { effect.first === null && effect.nodes_start === null && effect.teardown === null && - (effect.f & (EFFECT_PRESERVED | BOUNDARY_EFFECT)) === 0; // TODO think we can remove `| BOUNDARY_EFFECT` once the relevant PR is merged + (effect.f & EFFECT_PRESERVED) === 0; if (!inert && push) { if (parent !== null) { From 892bb28fcf69ce3ab76abfda32f30acfa24a4ea9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 15:04:48 -0400 Subject: [PATCH 572/582] unused export --- 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 c95257dd2412..b0bffad59566 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -277,7 +277,7 @@ export function increment(source) { * @param {number} status should be DIRTY or MAYBE_DIRTY * @returns {void} */ -export function mark_reactions(signal, status) { +function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; From 6d9801d16f5595e4a40f4e40882fd90268cef29c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 15:06:38 -0400 Subject: [PATCH 573/582] add optimisation back --- packages/svelte/src/internal/client/reactivity/sources.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index b0bffad59566..125f21bb81a1 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -288,6 +288,9 @@ function mark_reactions(signal, status) { 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; From b2c0b7986f9e33e6e27af60ea1b6968552053fba Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 15:09:36 -0400 Subject: [PATCH 574/582] revert unneeded changes --- .../src/internal/client/reactivity/sources.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 125f21bb81a1..29c657bdd038 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -300,15 +300,15 @@ function mark_reactions(signal, status) { continue; } - if (status === DIRTY || (flags & DIRTY) === 0) { - // don't make a DIRTY signal MAYBE_DIRTY - set_signal_status(reaction, status); - } + set_signal_status(reaction, status); - if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else { - schedule_effect(/** @type {Effect} */ (reaction)); + // 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); + } else { + schedule_effect(/** @type {Effect} */ (reaction)); + } } } } From 8c05ee97ff702bc3f2b30fd3943875bdce587e40 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 15:34:57 -0400 Subject: [PATCH 575/582] revert --- packages/svelte/tests/css/samples/class-directive/input.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/css/samples/class-directive/input.svelte b/packages/svelte/tests/css/samples/class-directive/input.svelte index 60e1f531713d..70075f89d49d 100644 --- a/packages/svelte/tests/css/samples/class-directive/input.svelte +++ b/packages/svelte/tests/css/samples/class-directive/input.svelte @@ -6,4 +6,4 @@ .second { color: green } .third { color: green } .forth { color: red } - + \ No newline at end of file From 27b46a98cee621752ee99ce1d3a3aad9673c0e9a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 17:40:48 -0400 Subject: [PATCH 576/582] update some tests --- .../samples/async-abort-signal/_config.js | 2 +- .../_config.js | 2 +- .../samples/async-derived-module/_config.js | 2 +- .../samples/async-each/_config.js | 55 +++++++++++------- .../samples/async-each/main.svelte | 8 ++- .../samples/async-expression/_config.js | 2 +- .../samples/async-html-tag/_config.js | 57 ++++++++++++------- .../samples/async-html-tag/main.svelte | 8 ++- .../runtime-runes/samples/async-if/_config.js | 12 ---- .../samples/async-key/_config.js | 54 +++++++++--------- .../samples/async-key/main.svelte | 8 ++- 11 files changed, 122 insertions(+), 88 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js index b721f4dd62c0..a947a91ab881 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js @@ -2,7 +2,7 @@ import { settled, tick } from 'svelte'; import { test } from '../../test'; export default test({ - async test({ assert, target, logs, variant }) { + async test({ assert, target, logs }) { const [reset, resolve] = target.querySelectorAll('button'); reset.click(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js index cd89439a7220..c5dae7fee294 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -2,7 +2,7 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - async test({ assert, target, errors }) { + async test({ assert, target }) { const [increment, resolve, reject] = target.querySelectorAll('button'); increment.click(); 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 b16ef652aee2..7a20ecda9221 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 @@ -1,4 +1,4 @@ -import { flushSync, settled, tick } from 'svelte'; +import { flushSync, tick } from 'svelte'; import { deferred } from '../../../../src/internal/shared/utils.js'; import { test } from '../../test'; 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 9dde2beb3926..50aa055130ba 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -1,33 +1,48 @@ 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(); + html: ` + + + +

    pending

    + `, - return { - promise: d.promise - }; - }, + async test({ assert, target }) { + const [reset, abc, defg] = target.querySelectorAll('button'); - async test({ assert, target, component }) { - d.resolve(['a', 'b', 'c']); + abc.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    a

    b

    c

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

    a

    b

    c

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

    a

    b

    c

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

    a

    b

    c

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

    d

    e

    f

    g

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

    d

    e

    f

    g

    ` + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte index 9b59d57b055a..8e4412811a45 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte @@ -1,9 +1,13 @@ + + + + - {#each await promise as item} + {#each await deferred.promise as item}

    {item}

    {/each} 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 d626569ba250..3a66ea709f01 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 { flushSync, tick } from 'svelte'; +import { tick } from 'svelte'; import { test } from '../../test'; export default test({ 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 22b8b2a1c462..f5b1f3d2c478 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 @@ -1,32 +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(); + html: ` + + + +

    pending

    + `, - return { - promise: d.promise - }; - }, + async test({ assert, target }) { + const [reset, hello, goodbye] = target.querySelectorAll('button'); - async test({ assert, target, component }) { - d.resolve('hello'); + hello.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    hello

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

    hello

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

    hello

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

    hello

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

    wheee

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

    goodbye

    + ` + ); } }); 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 index f5aa363731c2..980bb16d5c2f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte @@ -1,9 +1,13 @@ + + + + -

    {@html await promise}

    +

    {@html await deferred.promise}

    {#snippet pending()}

    pending

    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 a4bee8c9956f..3cd67952c3cb 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -1,21 +1,9 @@ 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 }) { const [reset, t, f] = target.querySelectorAll('button'); 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 bda922705464..e7e5db3dd8f9 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -1,46 +1,46 @@ 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); + html: ` + + + +

    pending

    + `, + + async test({ assert, target }) { + const [reset, one, two] = target.querySelectorAll('button'); + + const html = ` + + + +

    hello

    + `; + + one.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    hello

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

    hello

    '); + assert.htmlEqual(target.innerHTML, html); - d.resolve(1); + one.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    hello

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

    hello

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

    hello

    '); + assert.htmlEqual(target.innerHTML, html); 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 index 7cac0f854240..5fbdbd47d008 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte @@ -1,9 +1,13 @@ + + + + - {#key await promise} + {#key await deferred.promise}

    hello

    {/key} From 68f59c0ccee172d645b8a6832cf2daf1d8343343 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 17:48:53 -0400 Subject: [PATCH 577/582] more --- .../samples/async-prop/_config.js | 58 ++++++++++++------- .../samples/async-prop/main.svelte | 8 ++- .../samples/async-render-tag/_config.js | 57 ++++++++++++------ .../samples/async-render-tag/main.svelte | 8 ++- .../samples/async-svelte-element/_config.js | 57 ++++++++++++------ .../samples/async-svelte-element/main.svelte | 8 ++- 6 files changed, 132 insertions(+), 64 deletions(-) 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 ef4c453b26ce..66690c120cc9 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -1,33 +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(); + html: ` + + + +

    pending

    + `, - return { - promise: d.promise - }; - }, + async test({ assert, target }) { + const [reset, hello, again] = target.querySelectorAll('button'); - async test({ assert, target, component }) { - d.resolve('hello'); + hello.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    hello

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

    hello

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

    hello

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

    hello

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

    hello again

    '); + 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 index cb5d00b3d374..38388607ceed 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte @@ -1,11 +1,15 @@ + + + + - + {#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 index 22b8b2a1c462..6f3473f59245 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 @@ -1,32 +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(); + html: ` + + + +

    pending

    + `, - return { - promise: d.promise - }; - }, + async test({ assert, target }) { + const [reset, hello, wheee] = target.querySelectorAll('button'); - async test({ assert, target, component }) { - d.resolve('hello'); + hello.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    hello

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

    hello

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

    hello

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

    hello

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

    wheee

    '); + 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 index e98738567112..b59bc319d826 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte @@ -1,13 +1,17 @@ + + + + {#snippet hello(message)}

    {message}

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

    pending

    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 558caa629231..dc25be10c878 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 @@ -1,32 +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(); + html: ` + + + +

    pending

    + `, - return { - promise: d.promise - }; - }, + async test({ assert, target }) { + const [reset, h1, h2] = target.querySelectorAll('button'); - async test({ assert, target, component }) { - d.resolve('h1'); + h1.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    hello

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

    hello

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

    hello

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

    hello

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

    hello

    '); + 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 index 52852b549c8e..f8165784dc25 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte @@ -1,9 +1,13 @@ + + + + - hello + hello {#snippet pending()}

    pending

    From 2f227b12189bc422cb23fba95571ecfa77b35db9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 17:55:42 -0400 Subject: [PATCH 578/582] more --- .../samples/async-derived-module/_config.js | 76 +++++++++++++------ .../samples/async-derived-module/main.svelte | 10 ++- 2 files changed, 61 insertions(+), 25 deletions(-) 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 7a20ecda9221..f7d1d28fdece 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 @@ -1,24 +1,19 @@ 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(); + html: ` + + + + +

    pending

    + `, - return { - promise: d.promise, - num: 1 - }; - }, + async test({ assert, target, logs }) { + const [reset, a, b, increment] = target.querySelectorAll('button'); - async test({ assert, target, component, logs }) { - d.resolve(42); + a.click(); // TODO why is this necessary? why isn't `await tick()` enough? await Promise.resolve(); @@ -31,20 +26,55 @@ export default test({ await Promise.resolve(); flushSync(); await tick(); - assert.htmlEqual(target.innerHTML, '

    42

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

    42

    + ` + ); - component.num = 2; + increment.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    84

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

    84

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

    84

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

    84

    + ` + ); - d.resolve(43); + b.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

    86

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

    86

    + ` + ); assert.deepEqual(logs, [ 'outside boundary 1', 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 index e90bbf720ed3..2c83e1d23d1c 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte @@ -1,11 +1,17 @@ + + + + + - + {#snippet pending()}

    pending

    From 74b7f89679ca40ce1c07ae82c36807817fb4f7e4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 22:08:09 -0400 Subject: [PATCH 579/582] move some code --- .../internal/client/dom/blocks/boundary.js | 63 -------------- packages/svelte/src/internal/client/index.js | 3 +- .../src/internal/client/reactivity/async.js | 84 ++++++++++++++++++- 3 files changed, 82 insertions(+), 68 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index b2ec72c5297b..f32aea9a57dc 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -366,69 +366,6 @@ export function get_pending_boundary() { return boundary; } -/** - * Captures the current effect context so that we can restore it after - * some asynchronous work has happened if `track` is true (so that e.g. - * `await a + b` causes `b` to be registered as a dependency). - * - * If `track` is false, we just take a note of which async derived - * brought us here, so that we can emit a `async_reactivity_loss` - * warning when it's appropriate to do so. - * - * @param {boolean} track - */ -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 previous_async_effect = current_async_effect; - } - - return function restore() { - if (track) { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_component_context); - } - - if (DEV) { - set_from_async_derived(track ? null : previous_async_effect); - } - - // prevent the active effect from outstaying its welcome - // TODO this feels brittle - queue_micro_task(exit); - }; -} - -/** - * Wraps an `await` expression in such a way that the effect context that was - * active before the expression evaluated can be reapplied afterwards — - * `await a + b` becomes `(await $.save(a))() + b` - * @template T - * @param {Promise} promise - * @param {boolean} [track] - * @returns {Promise<() => T>} - */ -export async function save(promise, track = true) { - var restore = capture(track); - var value = await promise; - - return () => { - restore(); - return value; - }; -} - -function exit() { - set_active_effect(null); - set_active_reaction(null); - set_component_context(null); - if (DEV) set_from_async_derived(null); -} - export function pending() { if (active_effect === null) { e.effect_pending_outside_reaction(); diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 04bad60c762e..729367531269 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -98,6 +98,7 @@ export { props_id, with_script } from './dom/template.js'; +export { save } from './reactivity/async.js'; export { flushSync as flush, suspend } from './reactivity/batch.js'; export { async_derived, @@ -136,7 +137,7 @@ export { update_store, mark_store_binding } from './reactivity/store.js'; -export { boundary, pending, save } from './dom/blocks/boundary.js'; +export { boundary, pending } from './dom/blocks/boundary.js'; export { set_text } from './render.js'; export { get, diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 7e067ff49070..d79e93d38be7 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -1,12 +1,25 @@ /** @import { Effect, Value } from '#client' */ import { DESTROYED } from '#client/constants'; -import { is_runes } from '../context.js'; -import { capture, get_pending_boundary } from '../dom/blocks/boundary.js'; +import { DEV } from 'esm-env'; +import { component_context, is_runes, set_component_context } from '../context.js'; +import { get_pending_boundary } from '../dom/blocks/boundary.js'; import { invoke_error_boundary } from '../error-handling.js'; -import { active_effect } from '../runtime.js'; +import { + active_effect, + active_reaction, + set_active_effect, + set_active_reaction +} from '../runtime.js'; import { current_batch } from './batch.js'; -import { async_derived, derived, derived_safe_equal } from './deriveds.js'; +import { + async_derived, + current_async_effect, + derived, + derived_safe_equal, + set_from_async_derived +} from './deriveds.js'; +import { queue_micro_task } from '../dom/task.js'; /** * @@ -49,3 +62,66 @@ export function flatten(sync, async, fn) { boundary.error(error); }); } + +/** + * Captures the current effect context so that we can restore it after + * some asynchronous work has happened if `track` is true (so that e.g. + * `await a + b` causes `b` to be registered as a dependency). + * + * If `track` is false, we just take a note of which async derived + * brought us here, so that we can emit a `async_reactivity_loss` + * warning when it's appropriate to do so. + * + * @param {boolean} track + */ +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 previous_async_effect = current_async_effect; + } + + return function restore() { + if (track) { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); + } + + if (DEV) { + set_from_async_derived(track ? null : previous_async_effect); + } + + // prevent the active effect from outstaying its welcome + // TODO this feels brittle + queue_micro_task(exit); + }; +} + +/** + * Wraps an `await` expression in such a way that the effect context that was + * active before the expression evaluated can be reapplied afterwards — + * `await a + b` becomes `(await $.save(a))() + b` + * @template T + * @param {Promise} promise + * @param {boolean} [track] + * @returns {Promise<() => T>} + */ +export async function save(promise, track = true) { + var restore = capture(track); + var value = await promise; + + return () => { + restore(); + return value; + }; +} + +function exit() { + set_active_effect(null); + set_active_reaction(null); + set_component_context(null); + if (DEV) set_from_async_derived(null); +} From bd83eeb6a0754bcbfd406402abbac0f99c2fee1b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 22:09:22 -0400 Subject: [PATCH 580/582] rename --- packages/svelte/src/internal/client/reactivity/async.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index d79e93d38be7..6a8d84ba2c3c 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -96,7 +96,7 @@ export function capture(track = true) { // prevent the active effect from outstaying its welcome // TODO this feels brittle - queue_micro_task(exit); + queue_micro_task(unset_context); }; } @@ -119,7 +119,7 @@ export async function save(promise, track = true) { }; } -function exit() { +function unset_context() { set_active_effect(null); set_active_reaction(null); set_component_context(null); From db272883cf5ac99e4aa229baacd43cf52c7f0c4a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 22:18:23 -0400 Subject: [PATCH 581/582] WIP --- .../client/visitors/AwaitExpression.js | 2 +- packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/async.js | 48 ++++++++++--------- 3 files changed, 28 insertions(+), 24 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 e03c35c8a251..83edde194941 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 @@ -24,7 +24,7 @@ export function AwaitExpression(node, context) { // in dev, note which values are read inside a reactive expression, // but don't track them else if (dev && !is_ignored(node, 'await_reactivity_loss')) { - return b.call(b.await(b.call('$.save', argument, b.false))); + return b.call(b.await(b.call('$.track_reactivity_loss', argument, b.false))); } return argument === node.argument ? node : { ...node, argument }; diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 729367531269..cddb432a982b 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -98,7 +98,7 @@ export { props_id, with_script } from './dom/template.js'; -export { save } from './reactivity/async.js'; +export { save, track_reactivity_loss } from './reactivity/async.js'; export { flushSync as flush, suspend } from './reactivity/batch.js'; export { async_derived, diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 6a8d84ba2c3c..c9ffe64734c4 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -65,33 +65,21 @@ export function flatten(sync, async, fn) { /** * Captures the current effect context so that we can restore it after - * some asynchronous work has happened if `track` is true (so that e.g. - * `await a + b` causes `b` to be registered as a dependency). - * - * If `track` is false, we just take a note of which async derived - * brought us here, so that we can emit a `async_reactivity_loss` - * warning when it's appropriate to do so. - * - * @param {boolean} track + * some asynchronous work has happened (so that e.g. `await a + b` + * causes `b` to be registered as a dependency). */ -export function capture(track = true) { +function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; - if (DEV && !track) { - var previous_async_effect = current_async_effect; - } - return function restore() { - if (track) { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_component_context); - } + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_component_context); if (DEV) { - set_from_async_derived(track ? null : previous_async_effect); + set_from_async_derived(null); } // prevent the active effect from outstaying its welcome @@ -106,11 +94,10 @@ export function capture(track = true) { * `await a + b` becomes `(await $.save(a))() + b` * @template T * @param {Promise} promise - * @param {boolean} [track] * @returns {Promise<() => T>} */ -export async function save(promise, track = true) { - var restore = capture(track); +export async function save(promise) { + var restore = capture(); var value = await promise; return () => { @@ -119,6 +106,23 @@ export async function save(promise, track = true) { }; } +/** + * Reset `current_async_effect` after the `promise` resolves, so + * that we can emit `await_reactivity_loss` warnings + * @template T + * @param {Promise} promise + * @returns {Promise<() => T>} + */ +export async function track_reactivity_loss(promise) { + var previous_async_effect = current_async_effect; + var value = await promise; + + return () => { + set_from_async_derived(previous_async_effect); + return value; + }; +} + function unset_context() { set_active_effect(null); set_active_reaction(null); From ed8d73f5a44679dcc911259132e95cce3c6fd575 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 22:21:00 -0400 Subject: [PATCH 582/582] unset context synchronously --- packages/svelte/src/internal/client/reactivity/async.js | 7 ++----- packages/svelte/src/internal/client/reactivity/batch.js | 3 +++ packages/svelte/src/internal/client/reactivity/deriveds.js | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index c9ffe64734c4..0e332d2ed73f 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -57,6 +57,7 @@ export function flatten(sync, async, fn) { } batch?.deactivate(); + unset_context(); }) .catch((error) => { boundary.error(error); @@ -81,10 +82,6 @@ function capture() { if (DEV) { set_from_async_derived(null); } - - // prevent the active effect from outstaying its welcome - // TODO this feels brittle - queue_micro_task(unset_context); }; } @@ -123,7 +120,7 @@ export async function track_reactivity_loss(promise) { }; } -function unset_context() { +export function unset_context() { set_active_effect(null); set_active_reaction(null); set_component_context(null); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ee14e19b0711..1126946ce9b1 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -30,6 +30,7 @@ import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; import { old_values } from './sources.js'; import { unlink_effect } from './effects.js'; +import { unset_context } from './async.js'; /** @type {Set} */ const batches = new Set(); @@ -589,6 +590,8 @@ export function suspend() { return function unsuspend() { boundary.update_pending_count(-1); if (!pending) batch.decrement(); + + unset_context(); }; } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index f5957690e29c..fa6a9e02a1a6 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -34,6 +34,7 @@ import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; import { batch_deriveds, current_batch } from './batch.js'; +import { unset_context } from './async.js'; /** @type {Effect | null} */ export let current_async_effect = null; @@ -179,6 +180,8 @@ export function async_derived(fn, location) { boundary.update_pending_count(-1); if (!pending) batch.decrement(); } + + unset_context(); }; promise.then(handler, (e) => handler(null, e || 'unknown'));