chore: centralise branch management (#16977)

* WIP

* WIP

* WIP

* WIP

* WIP

* fix hydration

* simplify

* all tests passing

* key blocks

* snippets

* fix

* tidy up

* WIP await

* tidy up

* fix

* neaten up

* unused

* tweak

* elements

* changeset

* fix

* preserve newer batches

* add comment

* add comment

* no longer necessary apparently?

* move legacy logic to key block
pull/16984/head
Rich Harris 1 week ago committed by GitHub
parent 9a488d6b25
commit b8627e511d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: centralise branch management

@ -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,

@ -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);
}

@ -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<V>)} get_input
* @param {(() => any)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source<V>) => 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;
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);
/** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED;
var branches = new BranchManager(node);
/** @type {Effect | null} */
var pending_effect;
block(() => {
var input = get_input();
var destroyed = false;
/** @type {Effect | null} */
var then_effect;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
// @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight
let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE);
/** @type {Effect | null} */
var catch_effect;
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
set_hydrate_node(skip_nodes());
set_hydrating(false);
}
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);
if (is_promise(input)) {
var restore = capture();
var resolved = false;
/**
* @param {AwaitState} state
* @param {boolean} restore
* @param {() => void} fn
*/
function update(state, restore) {
const resolve = (fn) => {
if (destroyed) return;
resolved = true;
restore();
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);
}
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);
}
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));
}
fn();
} 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);
unset_context();
// 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 effect = block(() => {
if (input === (input = get_input())) return;
/** 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);
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
}
};
if (is_promise(input)) {
var promise = input;
resolved = false;
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);
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);
(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_source.v;
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;
}
}

@ -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);
}

@ -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<Batch, Key>} */
#batches = new Map();
/** @type {Map<Key, Effect>} */
#onscreen = new Map();
/** @type {Map<Key, Branch>} */
#offscreen = new Map();
/**
* Whether to pause (i.e. outro) on change, or destroy immediately.
* This is necessary for `<svelte:element>`
*/
#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();
}
}
}

@ -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;
}
}
var defer = should_defer_append();
var target = anchor;
branches.anchor = 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);
batch.add_callback(commit);
} else {
commit();
}
if (mismatch) {
// continue in hydration mode
set_hydrating(true);
branches.ensure(condition, fn);
}
};
block(() => {
has_branch = false;
fn(set_branch);
var has_branch = false;
fn((fn, flag = true) => {
has_branch = true;
update_branch(flag, fn);
});
if (!has_branch) {
update_branch(null, null);
update_branch(false, null);
}
}, flags);
if (hydrating) {
anchor = hydrate_node;
}
}

@ -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();
var key = get_key();
if (defer) {
offscreen_fragment = document.createDocumentFragment();
offscreen_fragment.append((target = create_text()));
// key blocks in Svelte <5 had stupid semantics
if (legacy && key !== null && typeof key === 'object') {
key = /** @type {V} */ ({});
}
pending_effect = branch(() => render_fn(target));
if (defer) {
/** @type {Batch} */ (current_batch).add_callback(commit);
} else {
commit();
}
}
branches.ensure(key, render_fn);
});
if (hydrating) {
anchor = hydrate_node;
}
}

@ -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;
}
}
/**

@ -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;
}
}

@ -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;
if (next_tag === null) {
branches.ensure(null, null);
set_should_intro(true);
return;
}
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 (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 && next_tag !== current_tag) {
effect = branch(() => {
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);
});
}
tag = next_tag;
if (tag) current_tag = tag;
set_current_each_item(previous_each_item);
if (hydrating) {
set_hydrate_node(anchor);
}
});
// 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);

@ -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<void>} fn
* @param {TemplateNode} anchor
* @param {(target: TemplateNode) => Promise<void>} 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);

@ -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;
}
}

@ -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;

Loading…
Cancel
Save