From 74c483c69de5b59995e0c8454a108f548a188c75 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 14 Jan 2025 23:46:51 +0000 Subject: [PATCH 001/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] =?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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] =?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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] . --- 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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/589] 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')); From 5d7bb5dcb1219a2d0744aff494fe82472a3fc55f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 11:02:05 -0400 Subject: [PATCH 583/589] remove unused argument --- .../phases/3-transform/client/visitors/AwaitExpression.js | 2 +- 1 file changed, 1 insertion(+), 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 83edde194941..a65abd7c1ad7 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('$.track_reactivity_loss', argument, b.false))); + return b.call(b.await(b.call('$.track_reactivity_loss', argument))); } return argument === node.argument ? node : { ...node, argument }; From 3e7d3b7862e3c6d0f2ed0cdebf55891a71b38688 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 11:39:24 -0400 Subject: [PATCH 584/589] Apply suggestions from code review Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- packages/svelte/src/compiler/phases/2-analyze/index.js | 2 +- packages/svelte/src/internal/client/dom/blocks/boundary.js | 1 - packages/svelte/src/internal/client/reactivity/async.js | 1 - packages/svelte/src/internal/client/reactivity/effects.js | 1 - 4 files changed, 1 insertion(+), 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 78a4c66f2ad9..258b59018a6c 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, ExpressionMetadata } from '#compiler' */ +/** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ import { walk } from 'zimmerframe'; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index f32aea9a57dc..7084481bd5bb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -22,7 +22,6 @@ 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 { 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'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 0e332d2ed73f..c200f10dba8f 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -19,7 +19,6 @@ import { derived_safe_equal, set_from_async_derived } from './deriveds.js'; -import { queue_micro_task } from '../dom/task.js'; /** * diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 090c8328e76c..c4edd2bf8d95 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -31,7 +31,6 @@ import { HEAD_EFFECT, MAYBE_DIRTY, EFFECT_PRESERVED, - BOUNDARY_EFFECT, STALE_REACTION, USER_EFFECT, ASYNC From b44bd67fa5feac64ce2c84188e38569e50330362 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 11:45:09 -0400 Subject: [PATCH 585/589] add comment --- .../svelte/src/internal/client/dom/blocks/boundary.js | 8 +++++++- 1 file changed, 7 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 7084481bd5bb..5ed0515210ec 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -83,7 +83,13 @@ export class Boundary { #pending_count = 0; #is_creating_fallback = false; - /** @type {Source | null} */ + /** + * A source containing the number of pending async deriveds/expressions. + * Only created if `$effect.pending()` is used inside the boundary, + * otherwise updating the source results in needless `Batch.ensure()` + * calls followed by no-op flushes + * @type {Source | null} + */ #effect_pending = null; #effect_pending_subscriber = createSubscriber(() => { From b82447343b60ace6626499484a18bb42d3eee038 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 11:49:49 -0400 Subject: [PATCH 586/589] add comment --- .../src/internal/client/dom/elements/bindings/input.js | 5 +++++ 1 file changed, 5 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 4fd2ee0a4b02..569d1179e602 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -65,6 +65,11 @@ export function bind_value(input, get, set = get) { var value = get(); if (input === document.activeElement) { + // Never rewrite the contents of a focused input. We can get here if, for example, + // an update is deferred because of async work depending on the input: + // + // + //

    {await find(query)}

    return; } From 36612abdd8389e0b28f58294601c8ef555fbace3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 11:58:24 -0400 Subject: [PATCH 587/589] use queue_micro_task in createSubscriber --- packages/svelte/src/reactivity/create-subscriber.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index dcbc5df9fe24..afcea9c5b40a 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -3,6 +3,7 @@ import { effect_tracking, render_effect } from '../internal/client/reactivity/ef import { source, increment } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; import { DEV } from 'esm-env'; +import { queue_micro_task } from '../internal/client/dom/task.js'; /** * Returns a `subscribe` function that, if called in an effect (including expressions in the template), @@ -68,7 +69,7 @@ export function createSubscriber(start) { subscribers += 1; return () => { - queueMicrotask(() => { + queue_micro_task(() => { // Only count down after a microtask, 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. From 0752833dbd625b01092845ba557f7389db006742 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 15:48:37 -0400 Subject: [PATCH 588/589] Update packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitExpression.js Co-authored-by: Elliott Johnson --- .../phases/3-transform/client/visitors/AwaitExpression.js | 8 +++----- 1 file changed, 3 insertions(+), 5 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 a65abd7c1ad7..386e013f9e14 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,11 +15,9 @@ export function AwaitExpression(node, context) { // preserve context for // a) top-level await and // b) awaits that precede other expressions in template or `$derived(...)` - if (tla || is_reactive_expression(context)) { - if (tla || !is_last_evaluated_expression(context, node)) { - return b.call(b.await(b.call('$.save', argument))); - } - } +if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) { + return b.call(b.await(b.call('$.save', argument))); +} // in dev, note which values are read inside a reactive expression, // but don't track them From c6e8850deb9bdd9c447fdcac91490537850c1dfc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 15:53:42 -0400 Subject: [PATCH 589/589] prettier --- .../phases/3-transform/client/visitors/AwaitExpression.js | 6 +++--- 1 file changed, 3 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 386e013f9e14..803d317ad40e 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,9 +15,9 @@ export function AwaitExpression(node, context) { // preserve context for // a) top-level await and // b) awaits that precede other expressions in template or `$derived(...)` -if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) { - return b.call(b.await(b.call('$.save', argument))); -} + if (tla || (is_reactive_expression(context) && !is_last_evaluated_expression(context, node))) { + return b.call(b.await(b.call('$.save', argument))); + } // in dev, note which values are read inside a reactive expression, // but don't track them pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy