diff --git a/.changeset/yellow-shrimps-provide.md b/.changeset/yellow-shrimps-provide.md new file mode 100644 index 0000000000..a29385660a --- /dev/null +++ b/.changeset/yellow-shrimps-provide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: centralise branch management 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 cd3fb7a64d..0dd4ae03b9 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 @@ -385,7 +385,9 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); - component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + component_block.body.push( + b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true))) + ); } else { component_block.body.push( ...state.instance_level_snippets, 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 bee4fcaab4..8d6a2fac88 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 @@ -178,7 +178,11 @@ export function Fragment(node, context) { } if (has_await) { - return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]); + return b.block([ + b.stmt( + b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true)) + ) + ]); } else { return b.block(body); } diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index e7917fbd9e..bac01e4c33 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -1,12 +1,9 @@ -/** @import { Effect, Source, TemplateNode } from '#client' */ -import { DEV } from 'esm-env'; +/** @import { Source, TemplateNode } from '#client' */ import { is_promise } from '../../../shared/utils.js'; -import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { block } from '../../reactivity/effects.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; -import { set_active_effect, set_active_reaction } from '../../runtime.js'; import { hydrate_next, - hydrate_node, hydrating, skip_nodes, set_hydrate_node, @@ -14,15 +11,10 @@ import { } from '../hydration.js'; import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; -import { - component_context, - dev_stack, - is_runes, - set_component_context, - set_dev_current_component_function, - set_dev_stack -} from '../../context.js'; +import { is_runes } from '../../context.js'; import { flushSync, is_flushing_sync } from '../../reactivity/batch.js'; +import { BranchManager } from './branches.js'; +import { capture, unset_context } from '../../reactivity/async.js'; const PENDING = 0; const THEN = 1; @@ -33,7 +25,7 @@ const CATCH = 2; /** * @template V * @param {TemplateNode} node - * @param {(() => Promise)} get_input + * @param {(() => any)} get_input * @param {null | ((anchor: Node) => void)} pending_fn * @param {null | ((anchor: Node, value: Source) => void)} then_fn * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn @@ -44,149 +36,94 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { hydrate_next(); } - var anchor = node; var runes = is_runes(); - var active_component_context = component_context; - - /** @type {any} */ - var component_function = DEV ? component_context?.function : null; - var dev_original_stack = DEV ? dev_stack : null; - - /** @type {V | Promise | typeof UNINITIALIZED} */ - var input = UNINITIALIZED; - - /** @type {Effect | null} */ - var pending_effect; - - /** @type {Effect | null} */ - var then_effect; - - /** @type {Effect | null} */ - var catch_effect; - - var input_source = runes - ? source(/** @type {V} */ (undefined)) - : mutable_source(/** @type {V} */ (undefined), false, false); - var error_source = runes ? source(undefined) : mutable_source(undefined, false, false); - var resolved = false; - /** - * @param {AwaitState} state - * @param {boolean} restore - */ - function update(state, restore) { - resolved = true; - - if (restore) { - set_active_effect(effect); - set_active_reaction(effect); // TODO do we need both? - set_component_context(active_component_context); - if (DEV) { - set_dev_current_component_function(component_function); - set_dev_stack(dev_original_stack); - } - } - - try { - if (state === PENDING && pending_fn) { - if (pending_effect) resume_effect(pending_effect); - else pending_effect = branch(() => pending_fn(anchor)); - } - - if (state === THEN && then_fn) { - if (then_effect) resume_effect(then_effect); - else then_effect = branch(() => then_fn(anchor, input_source)); - } - - if (state === CATCH && catch_fn) { - if (catch_effect) resume_effect(catch_effect); - else catch_effect = branch(() => catch_fn(anchor, error_source)); - } - - if (state !== PENDING && pending_effect) { - pause_effect(pending_effect, () => (pending_effect = null)); - } - - if (state !== THEN && then_effect) { - pause_effect(then_effect, () => (then_effect = null)); - } - - if (state !== CATCH && catch_effect) { - pause_effect(catch_effect, () => (catch_effect = null)); - } - } finally { - if (restore) { - if (DEV) { - set_dev_current_component_function(null); - set_dev_stack(null); - } - set_component_context(null); - set_active_reaction(null); - set_active_effect(null); + var v = /** @type {V} */ (UNINITIALIZED); + var value = runes ? source(v) : mutable_source(v, false, false); + var error = runes ? source(v) : mutable_source(v, false, false); - // without this, the DOM does not update until two ticks after the promise - // resolves, which is unexpected behaviour (and somewhat irksome to test) - if (!is_flushing_sync) flushSync(); - } - } - } + var branches = new BranchManager(node); - var effect = block(() => { - if (input === (input = get_input())) return; + block(() => { + var input = get_input(); + var destroyed = false; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ - // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight - let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE); + // @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight + let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE); if (mismatch) { // Hydration mismatch: remove everything inside the anchor and start fresh - anchor = skip_nodes(); - - set_hydrate_node(anchor); + set_hydrate_node(skip_nodes()); set_hydrating(false); - mismatch = true; } if (is_promise(input)) { - var promise = input; + var restore = capture(); + var resolved = false; + + /** + * @param {() => void} fn + */ + const resolve = (fn) => { + if (destroyed) return; + + resolved = true; + restore(); + + if (hydrating) { + // `restore()` could set `hydrating` to `true`, which we very much + // don't want — we want to restore everything _except_ this + set_hydrating(false); + } - resolved = false; + try { + fn(); + } finally { + unset_context(); - promise.then( - (value) => { - if (promise !== input) return; - // we technically could use `set` here since it's on the next microtick - // but let's use internal_set for consistency and just to be safe - internal_set(input_source, value); - update(THEN, true); + // without this, the DOM does not update until two ticks after the promise + // resolves, which is unexpected behaviour (and somewhat irksome to test) + if (!is_flushing_sync) flushSync(); + } + }; + + input.then( + (v) => { + resolve(() => { + internal_set(value, v); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, value))); + }); }, - (error) => { - if (promise !== input) return; - // we technically could use `set` here since it's on the next microtick - // but let's use internal_set for consistency and just to be safe - internal_set(error_source, error); - update(CATCH, true); - if (!catch_fn) { - // Rethrow the error if no catch block exists - throw error_source.v; - } + (e) => { + resolve(() => { + internal_set(error, e); + branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error))); + + if (!catch_fn) { + // Rethrow the error if no catch block exists + throw error.v; + } + }); } ); if (hydrating) { - if (pending_fn) { - pending_effect = branch(() => pending_fn(anchor)); - } + branches.ensure(PENDING, pending_fn); } else { // Wait a microtask before checking if we should show the pending state as - // the promise might have resolved by the next microtask. + // the promise might have resolved by then queue_micro_task(() => { - if (!resolved) update(PENDING, true); + if (!resolved) { + resolve(() => { + branches.ensure(PENDING, pending_fn); + }); + } }); } } else { - internal_set(input_source, input); - update(THEN, false); + internal_set(value, input); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, value))); } if (mismatch) { @@ -194,11 +131,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { set_hydrating(true); } - // Set the input to something else, in order to disable the promise callbacks - return () => (input = UNINITIALIZED); + return () => { + destroyed = true; + }; }); - - if (hydrating) { - anchor = hydrate_node; - } } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ca6437e384..026ffb36fc 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -8,7 +8,13 @@ import { import { HYDRATION_START_ELSE } from '../../../../constants.js'; import { component_context, set_component_context } from '../../context.js'; import { handle_error, invoke_error_boundary } from '../../error-handling.js'; -import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; +import { + block, + branch, + destroy_effect, + move_effect, + pause_effect +} from '../../reactivity/effects.js'; import { active_effect, active_reaction, @@ -425,24 +431,6 @@ export class Boundary { } } -/** - * - * @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 get_boundary() { return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b); } diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js new file mode 100644 index 0000000000..827f9f44fa --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -0,0 +1,185 @@ +/** @import { Effect, TemplateNode } from '#client' */ +import { is_runes } from '../../context.js'; +import { Batch, current_batch } from '../../reactivity/batch.js'; +import { + branch, + destroy_effect, + move_effect, + pause_effect, + resume_effect +} from '../../reactivity/effects.js'; +import { set_should_intro, should_intro } from '../../render.js'; +import { hydrate_node, hydrating } from '../hydration.js'; +import { create_text, should_defer_append } from '../operations.js'; + +/** + * @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch + */ + +/** + * @template Key + */ +export class BranchManager { + /** @type {TemplateNode} */ + anchor; + + /** @type {Map} */ + #batches = new Map(); + + /** @type {Map} */ + #onscreen = new Map(); + + /** @type {Map} */ + #offscreen = new Map(); + + /** + * Whether to pause (i.e. outro) on change, or destroy immediately. + * This is necessary for `` + */ + #transition = true; + + /** + * @param {TemplateNode} anchor + * @param {boolean} transition + */ + constructor(anchor, transition = true) { + this.anchor = anchor; + this.#transition = transition; + } + + #commit = () => { + var batch = /** @type {Batch} */ (current_batch); + + // if this batch was made obsolete, bail + if (!this.#batches.has(batch)) return; + + var key = /** @type {Key} */ (this.#batches.get(batch)); + + var onscreen = this.#onscreen.get(key); + + if (onscreen) { + // effect is already in the DOM — abort any current outro + resume_effect(onscreen); + } else { + // effect is currently offscreen. put it in the DOM + var offscreen = this.#offscreen.get(key); + + if (offscreen) { + this.#onscreen.set(key, offscreen.effect); + this.#offscreen.delete(key); + + // remove the anchor... + /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove(); + + // ...and append the fragment + this.anchor.before(offscreen.fragment); + onscreen = offscreen.effect; + } + } + + for (const [b, k] of this.#batches) { + this.#batches.delete(b); + + if (b === batch) { + // keep values for newer batches + break; + } + + const offscreen = this.#offscreen.get(k); + + if (offscreen) { + // for older batches, destroy offscreen effects + // as they will never be committed + destroy_effect(offscreen.effect); + this.#offscreen.delete(k); + } + } + + // outro/destroy all onscreen effects... + for (const [k, effect] of this.#onscreen) { + // ...except the one that was just committed + if (k === key) continue; + + const on_destroy = () => { + const keys = Array.from(this.#batches.values()); + + if (keys.includes(k)) { + // keep the effect offscreen, as another batch will need it + var fragment = document.createDocumentFragment(); + move_effect(effect, fragment); + + fragment.append(create_text()); // TODO can we avoid this? + + this.#offscreen.set(k, { effect, fragment }); + } else { + destroy_effect(effect); + } + + this.#onscreen.delete(k); + }; + + if (this.#transition || !onscreen) { + pause_effect(effect, on_destroy, false); + } else { + on_destroy(); + } + } + }; + + /** + * + * @param {any} key + * @param {null | ((target: TemplateNode) => void)} fn + */ + ensure(key, fn) { + var batch = /** @type {Batch} */ (current_batch); + var defer = should_defer_append(); + + if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) { + if (defer) { + var fragment = document.createDocumentFragment(); + var target = create_text(); + + fragment.append(target); + + this.#offscreen.set(key, { + effect: branch(() => fn(target)), + fragment + }); + } else { + this.#onscreen.set( + key, + branch(() => fn(this.anchor)) + ); + } + } + + this.#batches.set(batch, key); + + if (defer) { + for (const [k, effect] of this.#onscreen) { + if (k === key) { + batch.skipped_effects.delete(effect); + } else { + batch.skipped_effects.add(effect); + } + } + + for (const [k, branch] of this.#offscreen) { + if (k === key) { + batch.skipped_effects.delete(branch.effect); + } else { + batch.skipped_effects.add(branch.effect); + } + } + + batch.add_callback(this.#commit); + } else { + if (hydrating) { + this.anchor = hydrate_node; + } + + this.#commit(); + } + } +} diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 6349ab8399..7fa5ca464d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,19 +1,16 @@ -/** @import { Effect, TemplateNode } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js'; */ +/** @import { TemplateNode } from '#client' */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_next, - hydrate_node, hydrating, read_hydration_instruction, skip_nodes, set_hydrate_node, set_hydrating } from '../hydration.js'; -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 { current_batch } from '../../reactivity/batch.js'; +import { block } from '../../reactivity/effects.js'; +import { HYDRATION_START_ELSE } from '../../../../constants.js'; +import { BranchManager } from './branches.js'; // TODO reinstate https://github.com/sveltejs/svelte/pull/15250 @@ -28,122 +25,46 @@ export function if_block(node, fn, elseif = false) { hydrate_next(); } - var anchor = node; - - /** @type {Effect | null} */ - var consequent_effect = null; - - /** @type {Effect | null} */ - var alternate_effect = null; - - /** @type {typeof UNINITIALIZED | boolean | null} */ - var condition = UNINITIALIZED; - + var branches = new BranchManager(node); var flags = elseif ? EFFECT_TRANSPARENT : 0; - var has_branch = false; - - const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => { - has_branch = true; - update_branch(flag, fn); - }; - - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - function commit() { - if (offscreen_fragment !== null) { - // remove the anchor - /** @type {Text} */ (offscreen_fragment.lastChild).remove(); - - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - var active = condition ? consequent_effect : alternate_effect; - var inactive = condition ? alternate_effect : consequent_effect; - - if (active) { - resume_effect(active); - } - - if (inactive) { - pause_effect(inactive, () => { - if (condition) { - alternate_effect = null; - } else { - consequent_effect = null; - } - }); - } - } - - const update_branch = ( - /** @type {boolean | null} */ new_condition, - /** @type {null | ((anchor: Node) => void)} */ fn - ) => { - if (condition === (condition = new_condition)) return; - - /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ - let mismatch = false; - + /** + * @param {boolean} condition, + * @param {null | ((anchor: Node) => void)} fn + */ + function update_branch(condition, fn) { if (hydrating) { - const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE; + const is_else = read_hydration_instruction(node) === HYDRATION_START_ELSE; - if (!!condition === is_else) { + if (condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example - anchor = skip_nodes(); + var anchor = skip_nodes(); set_hydrate_node(anchor); - set_hydrating(false); - mismatch = true; - } - } + branches.anchor = anchor; - var defer = should_defer_append(); - var target = anchor; - - if (defer) { - offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = create_text())); - } + set_hydrating(false); + branches.ensure(condition, fn); + set_hydrating(true); - if (condition) { - consequent_effect ??= fn && branch(() => fn(target)); - } else { - alternate_effect ??= fn && branch(() => fn(target)); + return; + } } - if (defer) { - var batch = /** @type {Batch} */ (current_batch); - - 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); + branches.ensure(condition, fn); + } - batch.add_callback(commit); - } else { - commit(); - } + block(() => { + var has_branch = false; - if (mismatch) { - // continue in hydration mode - set_hydrating(true); - } - }; + fn((fn, flag = true) => { + has_branch = true; + update_branch(flag, fn); + }); - block(() => { - has_branch = false; - fn(set_branch); if (!has_branch) { - update_branch(null, null); + update_branch(false, null); } }, flags); - - if (hydrating) { - anchor = hydrate_node; - } } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 5e3c42019f..849b1c2447 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,12 +1,8 @@ -/** @import { Effect, TemplateNode } from '#client' */ -/** @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 { TemplateNode } from '#client' */ 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 { current_batch } from '../../reactivity/batch.js'; +import { block } from '../../reactivity/effects.js'; +import { hydrate_next, hydrating } from '../hydration.js'; +import { BranchManager } from './branches.js'; /** * @template V @@ -20,60 +16,18 @@ export function key(node, get_key, render_fn) { hydrate_next(); } - var anchor = node; + var branches = new BranchManager(node); - /** @type {V | typeof UNINITIALIZED} */ - var key = UNINITIALIZED; - - /** @type {Effect} */ - var effect; - - /** @type {Effect} */ - var pending_effect; - - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - var changed = is_runes() ? not_equal : safe_not_equal; - - function commit() { - if (effect) { - pause_effect(effect); - } - - if (offscreen_fragment !== null) { - // remove the anchor - /** @type {Text} */ (offscreen_fragment.lastChild).remove(); - - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - effect = pending_effect; - } + var legacy = !is_runes(); block(() => { - if (changed(key, (key = get_key()))) { - var target = anchor; - - var defer = should_defer_append(); - - if (defer) { - offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = create_text())); - } - - pending_effect = branch(() => render_fn(target)); + var key = get_key(); - if (defer) { - /** @type {Batch} */ (current_batch).add_callback(commit); - } else { - commit(); - } + // key blocks in Svelte <5 had stupid semantics + if (legacy && key !== null && typeof key === 'object') { + key = /** @type {V} */ ({}); } - }); - if (hydrating) { - anchor = hydrate_node; - } + branches.ensure(key, render_fn); + }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 32d88d4c60..0c4948aca0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,8 +1,8 @@ /** @import { Snippet } from 'svelte' */ -/** @import { Effect, TemplateNode } from '#client' */ +/** @import { TemplateNode } from '#client' */ /** @import { Getters } from '#shared' */ import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; -import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; +import { block, teardown } from '../../reactivity/effects.js'; import { dev_current_component_function, set_dev_current_component_function @@ -14,8 +14,8 @@ import * as w from '../../warnings.js'; import * as e from '../../errors.js'; import { DEV } from 'esm-env'; import { get_first_child, get_next_sibling } from '../operations.js'; -import { noop } from '../../../shared/utils.js'; import { prevent_snippet_stringification } from '../../../shared/validate.js'; +import { BranchManager } from './branches.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -25,33 +25,17 @@ import { prevent_snippet_stringification } from '../../../shared/validate.js'; * @returns {void} */ export function snippet(node, get_snippet, ...args) { - var anchor = node; - - /** @type {SnippetFn | null | undefined} */ - // @ts-ignore - var snippet = noop; - - /** @type {Effect | null} */ - var snippet_effect; + var branches = new BranchManager(node); block(() => { - if (snippet === (snippet = get_snippet())) return; - - if (snippet_effect) { - destroy_effect(snippet_effect); - snippet_effect = null; - } + const snippet = get_snippet() ?? null; if (DEV && snippet == null) { e.invalid_snippet(); } - snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args)); + branches.ensure(snippet, snippet && ((anchor) => snippet(anchor, ...args))); }, EFFECT_TRANSPARENT); - - if (hydrating) { - anchor = hydrate_node; - } } /** 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 2697722b39..134e57e627 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,8 @@ -/** @import { TemplateNode, Dom, Effect } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js'; */ +/** @import { TemplateNode, Dom } from '#client' */ import { EFFECT_TRANSPARENT } from '#client/constants'; -import { block, branch, pause_effect } from '../../reactivity/effects.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'; +import { block } from '../../reactivity/effects.js'; +import { hydrate_next, hydrating } from '../hydration.js'; +import { BranchManager } from './branches.js'; /** * @template P @@ -19,64 +17,10 @@ export function component(node, get_component, render_fn) { hydrate_next(); } - var anchor = node; - - /** @type {C} */ - var component; - - /** @type {Effect | null} */ - var effect; - - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - /** @type {Effect | null} */ - var pending_effect = null; - - function commit() { - if (effect) { - pause_effect(effect); - effect = null; - } - - if (offscreen_fragment) { - // remove the anchor - /** @type {Text} */ (offscreen_fragment.lastChild).remove(); - - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - effect = pending_effect; - pending_effect = null; - } + var branches = new BranchManager(node); block(() => { - if (component === (component = get_component())) return; - - var defer = should_defer_append(); - - if (component) { - var target = anchor; - - if (defer) { - offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = create_text())); - if (effect) { - /** @type {Batch} */ (current_batch).skipped_effects.add(effect); - } - } - pending_effect = branch(() => render_fn(target, component)); - } - - if (defer) { - /** @type {Batch} */ (current_batch).add_callback(commit); - } else { - commit(); - } + var component = get_component() ?? null; + branches.ensure(component, component && ((target) => render_fn(target, component))); }, EFFECT_TRANSPARENT); - - if (hydrating) { - anchor = hydrate_node; - } } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 231a3621b1..6533ff8921 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -8,13 +8,7 @@ import { set_hydrating } from '../hydration.js'; import { create_text, get_first_child } from '../operations.js'; -import { - block, - branch, - destroy_effect, - pause_effect, - resume_effect -} from '../../reactivity/effects.js'; +import { block, teardown } from '../../reactivity/effects.js'; import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; import { active_effect } from '../../runtime.js'; @@ -23,6 +17,7 @@ import { DEV } from 'esm-env'; import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { assign_nodes } from '../template.js'; import { is_raw_text_element } from '../../../../utils.js'; +import { BranchManager } from './branches.js'; /** * @param {Comment | Element} node @@ -42,12 +37,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio var filename = DEV && location && component_context?.function[FILENAME]; - /** @type {string | null} */ - var tag; - - /** @type {string | null} */ - var current_tag; - /** @type {null | Element} */ var element = null; @@ -58,9 +47,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node); - /** @type {Effect | null} */ - var effect; - /** * The keyed `{#each ...}` item block, if any, that this element is inside. * We track this so we can set it when changing the element, allowing any @@ -68,36 +54,24 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio */ var each_item_block = current_each_item; + var branches = new BranchManager(anchor, false); + block(() => { const next_tag = get_tag() || null; var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null; - // Assumption: Noone changes the namespace but not the tag (what would that even mean?) - if (next_tag === tag) return; - - // See explanation of `each_item_block` above - var previous_each_item = current_each_item; - set_current_each_item(each_item_block); - - if (effect) { - if (next_tag === null) { - // start outro - pause_effect(effect, () => { - effect = null; - current_tag = null; - }); - } else if (next_tag === current_tag) { - // same tag as is currently rendered — abort outro - resume_effect(effect); - } else { - // tag is changing — destroy immediately, render contents without intro transitions - destroy_effect(effect); - set_should_intro(false); - } + if (next_tag === null) { + branches.ensure(null, null); + set_should_intro(true); + return; } - if (next_tag && next_tag !== current_tag) { - effect = branch(() => { + branches.ensure(next_tag, (anchor) => { + // See explanation of `each_item_block` above + var previous_each_item = current_each_item; + set_current_each_item(each_item_block); + + if (next_tag) { element = hydrating ? /** @type {Element} */ (element) : ns @@ -149,16 +123,31 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio /** @type {Effect} */ (active_effect).nodes_end = element; anchor.before(element); - }); - } + } + + set_current_each_item(previous_each_item); + + if (hydrating) { + set_hydrate_node(anchor); + } + }); - tag = next_tag; - if (tag) current_tag = tag; + // revert to the default state after the effect has been created set_should_intro(true); - set_current_each_item(previous_each_item); + return () => { + if (next_tag) { + // if we're in this callback because we're re-running the effect, + // disable intros (unless no element is currently displayed) + set_should_intro(false); + } + }; }, EFFECT_TRANSPARENT); + teardown(() => { + set_should_intro(true); + }); + if (was_hydrating) { set_hydrating(true); set_hydrate_node(anchor); diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 45c78ff926..1d408744fd 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -1,8 +1,13 @@ -/** @import { Effect, Value } from '#client' */ - +/** @import { Effect, TemplateNode, Value } from '#client' */ import { DESTROYED } from '#client/constants'; import { DEV } from 'esm-env'; -import { component_context, is_runes, set_component_context } from '../context.js'; +import { + component_context, + dev_stack, + is_runes, + set_component_context, + set_dev_stack +} from '../context.js'; import { get_boundary } from '../dom/blocks/boundary.js'; import { invoke_error_boundary } from '../error-handling.js'; import { @@ -28,6 +33,7 @@ import { set_hydrating, skip_nodes } from '../dom/hydration.js'; +import { create_text } from '../dom/operations.js'; /** * @@ -80,7 +86,7 @@ export function flatten(sync, async, fn) { * some asynchronous work has happened (so that e.g. `await a + b` * causes `b` to be registered as a dependency). */ -function capture() { +export function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; @@ -92,6 +98,10 @@ function capture() { var previous_hydrate_node = hydrate_node; } + if (DEV) { + var previous_dev_stack = dev_stack; + } + return function restore() { set_active_effect(previous_effect); set_active_reaction(previous_reaction); @@ -105,6 +115,7 @@ function capture() { if (DEV) { set_from_async_derived(null); + set_dev_stack(previous_dev_stack); } }; } @@ -193,13 +204,18 @@ export function unset_context() { set_active_effect(null); set_active_reaction(null); set_component_context(null); - if (DEV) set_from_async_derived(null); + + if (DEV) { + set_from_async_derived(null); + set_dev_stack(null); + } } /** - * @param {() => Promise} fn + * @param {TemplateNode} anchor + * @param {(target: TemplateNode) => Promise} fn */ -export async function async_body(fn) { +export async function async_body(anchor, fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); var pending = boundary.is_pending(); @@ -218,7 +234,7 @@ export async function async_body(fn) { } try { - var promise = fn(); + var promise = fn(anchor); } finally { if (next_hydrate_node) { set_hydrate_node(next_hydrate_node); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2c9e4db911..bfbb95a8db 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -553,15 +553,16 @@ 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); run_out_transitions(transitions, () => { - destroy_effect(effect); + if (destroy) destroy_effect(effect); if (callback) callback(); }); } @@ -662,3 +663,20 @@ function resume_children(effect, local) { export function aborted(effect = /** @type {Effect} */ (active_effect)) { return (effect.f & DESTROYED) !== 0; } + +/** + * @param {Effect} effect + * @param {DocumentFragment} fragment + */ +export 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; + } +} diff --git a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js index 1725cd8f6f..9ef598de6c 100644 --- a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js @@ -1,3 +1,4 @@ +import { tick } from 'svelte'; import { test } from '../../test'; /** @@ -77,7 +78,7 @@ export default test({ const { promise, reject } = promiseWithResolver(); component.promise = promise; // wait for rendering - await Promise.resolve(); + await tick(); // remove the promise component.promise = null;