|
|
|
|
@ -1,7 +1,6 @@
|
|
|
|
|
/** @import { Effect, EffectNodes, Source, TemplateNode } from '#client' */
|
|
|
|
|
/** @import { Effect, Source, TemplateNode } from '#client' */
|
|
|
|
|
/** @import { Batch } from '../../reactivity/batch.js' */
|
|
|
|
|
import { DESTROYED, DESTROYING } from '#client/constants';
|
|
|
|
|
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../../constants.js';
|
|
|
|
|
import { DESTROYED, DESTROYING, HEAD_EFFECT } from '#client/constants';
|
|
|
|
|
import { capture } from '../../reactivity/async.js';
|
|
|
|
|
import { current_batch } from '../../reactivity/batch.js';
|
|
|
|
|
import {
|
|
|
|
|
@ -9,31 +8,63 @@ import {
|
|
|
|
|
branch,
|
|
|
|
|
destroy_effect,
|
|
|
|
|
move_effect,
|
|
|
|
|
remove_effect_dom,
|
|
|
|
|
render_effect
|
|
|
|
|
} from '../../reactivity/effects.js';
|
|
|
|
|
import { set, source } from '../../reactivity/sources.js';
|
|
|
|
|
import { active_effect, get, set_active_effect, untrack } from '../../runtime.js';
|
|
|
|
|
import { active_effect, get, untrack } from '../../runtime.js';
|
|
|
|
|
import {
|
|
|
|
|
hydrate_next,
|
|
|
|
|
hydrate_node,
|
|
|
|
|
hydrating,
|
|
|
|
|
set_hydrate_node,
|
|
|
|
|
set_hydrating
|
|
|
|
|
set_hydrating,
|
|
|
|
|
skip_nodes
|
|
|
|
|
} from '../hydration.js';
|
|
|
|
|
import { create_text, get_next_sibling, should_defer_append } from '../operations.js';
|
|
|
|
|
import { queue_micro_task } from '../task.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @typedef {{ anchor: TemplateNode }} Outlet
|
|
|
|
|
* @typedef {{ outlets: Source<Array<Outlet>>, pending: Set<(outlet: Outlet) => void> }} OutletEntry
|
|
|
|
|
* @typedef {{ key: any, effect: Effect, fragment: DocumentFragment | null }} PortalBranch
|
|
|
|
|
* @typedef {{ anchor: TemplateNode, outlet?: Outlet }} PortalTarget
|
|
|
|
|
* A branch of a `{#portal ...}` block, rendered into a single target.
|
|
|
|
|
* While the branch is offscreen (its insertion is deferred until a batch
|
|
|
|
|
* commits), `fragment` contains its DOM, otherwise it is `null`.
|
|
|
|
|
* @typedef {{ effect: Effect, fragment: DocumentFragment | null }} PortalBranch
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Represents a `{@portal ...}` outlet.
|
|
|
|
|
* - `anchor` is the node portaled content is inserted before
|
|
|
|
|
* - `claim` is only used during hydration — it is the position of the last
|
|
|
|
|
* server-rendered portal content that was claimed by a `{#portal ...}` block
|
|
|
|
|
* - `items` tracks the content of the portals currently rendered into this outlet
|
|
|
|
|
* (ordered by portal creation order), so that content of portals that are created
|
|
|
|
|
* (or committed) out of order can be inserted at the right position
|
|
|
|
|
* @typedef {{
|
|
|
|
|
* anchor: TemplateNode,
|
|
|
|
|
* claim: TemplateNode | null,
|
|
|
|
|
* items: Array<{ seq: number, branch: PortalBranch }>
|
|
|
|
|
* }} Outlet
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* All known outlets (and portals waiting for outlets) for a given key.
|
|
|
|
|
* `pending` is only used during hydration — it contains render functions of
|
|
|
|
|
* `{#portal ...}` blocks that were created before their outlet, so that the
|
|
|
|
|
* outlet can have them claim their server-rendered content upon initialisation.
|
|
|
|
|
* @typedef {{
|
|
|
|
|
* outlets: Source<Outlet[]>,
|
|
|
|
|
* pending: Set<(outlet: Outlet) => void>
|
|
|
|
|
* }} OutletEntry
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** @type {Map<any, OutletEntry>} */
|
|
|
|
|
const outlet_map = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Monotonically increasing sequence number, used to keep content of multiple
|
|
|
|
|
* portals targeting the same outlet in creation order
|
|
|
|
|
*/
|
|
|
|
|
let portal_seq = 0;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {any} key
|
|
|
|
|
* @returns {OutletEntry}
|
|
|
|
|
@ -49,6 +80,74 @@ function get_outlet_entry(key) {
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Run `fn` now (during hydration, where synchronous timing is required for
|
|
|
|
|
* claiming server-rendered content), or in a microtask otherwise. The latter
|
|
|
|
|
* ensures that outlet (un)registrations - which can happen while a batch is
|
|
|
|
|
* being committed - do not interfere with the commit by scheduling portal
|
|
|
|
|
* updates (which would happen in a new batch) at the wrong moment.
|
|
|
|
|
* @param {() => void} fn
|
|
|
|
|
*/
|
|
|
|
|
function run_outside_batch(fn) {
|
|
|
|
|
if (hydrating) {
|
|
|
|
|
fn();
|
|
|
|
|
} else {
|
|
|
|
|
// TODO this is a hack to get around a (I think) general batch.js bug
|
|
|
|
|
// where setting state while flushing (render) effects can mess with
|
|
|
|
|
// #commit() of the earlier batch that runs afterwards, where roots
|
|
|
|
|
// would not be scheduled for other batches anymore because scheduling
|
|
|
|
|
// an effect might reach a branch that is already unclean, so scheduling
|
|
|
|
|
// thinks "oh we already have this root scheduled" (wrong because not in the context of that batch).
|
|
|
|
|
queue_micro_task(fn);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the node before which the content of a portal with the given
|
|
|
|
|
* sequence number must be inserted, so that the contents of multiple portals
|
|
|
|
|
* appear in the order in which the portals were created
|
|
|
|
|
* @param {Outlet} outlet
|
|
|
|
|
* @param {number} seq
|
|
|
|
|
* @returns {TemplateNode}
|
|
|
|
|
*/
|
|
|
|
|
function get_insertion_anchor(outlet, seq) {
|
|
|
|
|
var items = outlet.items;
|
|
|
|
|
|
|
|
|
|
// prune branches that were destroyed or moved offscreen
|
|
|
|
|
for (var i = items.length - 1; i >= 0; i -= 1) {
|
|
|
|
|
var { effect, fragment } = items[i].branch;
|
|
|
|
|
|
|
|
|
|
if ((effect.f & (DESTROYED | DESTROYING)) !== 0 || effect.nodes === null || fragment !== null) {
|
|
|
|
|
items.splice(i, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (var item of items) {
|
|
|
|
|
if (item.seq > seq) {
|
|
|
|
|
return /** @type {TemplateNode} */ (item.branch.effect.nodes?.start);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return outlet.anchor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Registers a rendered portal branch with an outlet, keeping `outlet.items` ordered
|
|
|
|
|
* @param {Outlet} outlet
|
|
|
|
|
* @param {number} seq
|
|
|
|
|
* @param {PortalBranch} branch
|
|
|
|
|
*/
|
|
|
|
|
function register_branch(outlet, seq, branch) {
|
|
|
|
|
var items = outlet.items;
|
|
|
|
|
var index = items.findIndex((item) => item.seq > seq);
|
|
|
|
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
items.push({ seq, branch });
|
|
|
|
|
} else {
|
|
|
|
|
items.splice(index, 0, { seq, branch });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {TemplateNode} node
|
|
|
|
|
* @param {() => any} get_id
|
|
|
|
|
@ -58,347 +157,435 @@ export function portal_outlet(node, get_id) {
|
|
|
|
|
var anchor = node;
|
|
|
|
|
|
|
|
|
|
if (hydrating) {
|
|
|
|
|
// `node` is the `<!--[-->` comment — advance to the `<!--portal:N-->` marker.
|
|
|
|
|
// Server-rendered content of `{#portal ...}` blocks comes right after it
|
|
|
|
|
// and is claimed by the corresponding blocks during hydration
|
|
|
|
|
anchor = hydrate_next();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @type {Outlet} */
|
|
|
|
|
var outlet = { anchor };
|
|
|
|
|
var outlet = { anchor, claim: hydrating ? anchor : null, items: [] };
|
|
|
|
|
|
|
|
|
|
// TODO this should be a block effect so it runs during traversal. The way it's right now
|
|
|
|
|
// it means that a #portal block with async work will have that async work not coordinated
|
|
|
|
|
// if it's instantiated through this @portal for the first time.
|
|
|
|
|
render_effect(() => {
|
|
|
|
|
const id = get_id();
|
|
|
|
|
if (id == null) return;
|
|
|
|
|
|
|
|
|
|
const entry = get_outlet_entry(id);
|
|
|
|
|
const outlets = entry.outlets;
|
|
|
|
|
|
|
|
|
|
set(
|
|
|
|
|
outlets,
|
|
|
|
|
untrack(() => [...get(outlets), outlet])
|
|
|
|
|
);
|
|
|
|
|
var registered = false;
|
|
|
|
|
var cancelled = false;
|
|
|
|
|
|
|
|
|
|
for (const render of entry.pending) {
|
|
|
|
|
render(outlet);
|
|
|
|
|
}
|
|
|
|
|
const register = () => {
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
registered = true;
|
|
|
|
|
|
|
|
|
|
set(
|
|
|
|
|
entry.outlets,
|
|
|
|
|
untrack(() => [...get(entry.outlets), outlet])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// during hydration, portals that were created before this outlet claim
|
|
|
|
|
// their server-rendered content now, while the hydration position is known
|
|
|
|
|
for (const render of entry.pending) {
|
|
|
|
|
render(outlet);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const unregister = () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
if (!registered) return;
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
set(
|
|
|
|
|
outlets,
|
|
|
|
|
untrack(() => get(outlets).filter((item) => item !== outlet))
|
|
|
|
|
entry.outlets,
|
|
|
|
|
untrack(() => get(entry.outlets).filter((o) => o !== outlet))
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
run_outside_batch(register);
|
|
|
|
|
|
|
|
|
|
return () => run_outside_batch(unregister);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (hydrating) {
|
|
|
|
|
let depth = 1;
|
|
|
|
|
while (anchor !== null && depth > 0) {
|
|
|
|
|
// TODO we have similar logic in other places, consolidate?
|
|
|
|
|
anchor = /** @type {TemplateNode} */ (get_next_sibling(anchor));
|
|
|
|
|
if (anchor?.nodeType === 8) {
|
|
|
|
|
var comment = /** @type {Comment} */ (anchor).data;
|
|
|
|
|
if (comment === HYDRATION_START || comment === HYDRATION_START_ELSE) depth += 1;
|
|
|
|
|
else if (comment[0] === HYDRATION_END) depth -= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
set_hydrate_node(anchor);
|
|
|
|
|
// move past the (claimed) server-rendered portal contents to the
|
|
|
|
|
// closing `<!--]-->` comment, which becomes the outlet anchor.
|
|
|
|
|
// Portals that appear later in the markup claim their server-rendered
|
|
|
|
|
// content through `outlet.claim`.
|
|
|
|
|
var close = skip_nodes(false);
|
|
|
|
|
|
|
|
|
|
outlet.anchor = close;
|
|
|
|
|
|
|
|
|
|
set_hydrate_node(close);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {() => any} get_target
|
|
|
|
|
* @param {(anchor: TemplateNode) => void} content
|
|
|
|
|
* @returns {void | (() => void)}
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
export function portal(get_target, content) {
|
|
|
|
|
// Portal targets are reconciled at batch boundaries. A target can disappear in one
|
|
|
|
|
// pending batch and reappear in a later one, so effects are kept offscreen until we
|
|
|
|
|
// know whether they are committed or discarded (similar to BranchManager).
|
|
|
|
|
/** @type {Map<any, PortalBranch>} */
|
|
|
|
|
let onscreen = new Map();
|
|
|
|
|
/** @type {Map<any, PortalBranch>} */
|
|
|
|
|
let offscreen = new Map();
|
|
|
|
|
/** @type {Map<Batch, Map<any, PortalTarget>>} */
|
|
|
|
|
let pending = new Map();
|
|
|
|
|
/** @type {{ entry: OutletEntry, render: (outlet: Outlet) => void } | null} */
|
|
|
|
|
let unrendered = null;
|
|
|
|
|
|
|
|
|
|
/** @param {Map<any, PortalBranch>} portals */
|
|
|
|
|
function destroy_portals(portals) {
|
|
|
|
|
for (const portal of portals.values()) {
|
|
|
|
|
destroy_effect(portal.effect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
portals.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clear_unrendered() {
|
|
|
|
|
if (unrendered === null) return;
|
|
|
|
|
var seq = portal_seq++;
|
|
|
|
|
|
|
|
|
|
const { entry, render } = unrendered;
|
|
|
|
|
entry.pending.delete(render);
|
|
|
|
|
unrendered = null;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* Branches that are currently in the DOM, keyed by target
|
|
|
|
|
* (an {@link Outlet}, or an element in case of `{#portal some_dom_node}`).
|
|
|
|
|
* Since a portal renders into every outlet with a matching key,
|
|
|
|
|
* there can be multiple branches at any given time
|
|
|
|
|
* @type {Map<Outlet | Element, PortalBranch>}
|
|
|
|
|
*/
|
|
|
|
|
var onscreen = new Map();
|
|
|
|
|
|
|
|
|
|
/** @param {OutletEntry} entry */
|
|
|
|
|
function set_unrendered(entry) {
|
|
|
|
|
/** @type {(outlet: Outlet) => void} */
|
|
|
|
|
const render = (outlet) => {
|
|
|
|
|
const prev_context = capture();
|
|
|
|
|
portal_context(false);
|
|
|
|
|
/**
|
|
|
|
|
* Branches that are rendered into a `DocumentFragment` because their
|
|
|
|
|
* insertion is deferred until their batch commits, keyed by target
|
|
|
|
|
* @type {Map<Outlet | Element, PortalBranch>}
|
|
|
|
|
*/
|
|
|
|
|
var offscreen = new Map();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const portal = create_portal(outlet, { anchor: outlet.anchor, outlet }, false);
|
|
|
|
|
onscreen.set(outlet, portal);
|
|
|
|
|
} finally {
|
|
|
|
|
prev_context(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
/**
|
|
|
|
|
* The target (outlet key or element) and resolved targets each in-flight
|
|
|
|
|
* batch wants this portal to render into. An entry is only removed once
|
|
|
|
|
* its batch commits or is discarded
|
|
|
|
|
* @type {Map<Batch, { target: any, targets: Set<Outlet | Element> }>}
|
|
|
|
|
*/
|
|
|
|
|
var batches = new Map();
|
|
|
|
|
|
|
|
|
|
entry.pending.add(render);
|
|
|
|
|
unrendered = { entry, render };
|
|
|
|
|
}
|
|
|
|
|
/** @type {Effect} */
|
|
|
|
|
var self;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {any} key
|
|
|
|
|
* @param {PortalTarget} target
|
|
|
|
|
* @param {boolean} offscreen
|
|
|
|
|
* @returns {PortalBranch}
|
|
|
|
|
* Creates a branch for the given target, either directly in the DOM
|
|
|
|
|
* (claiming server-rendered content, if it exists) or offscreen
|
|
|
|
|
* @param {Outlet | Element} target
|
|
|
|
|
* @param {boolean} offscreen_render
|
|
|
|
|
*/
|
|
|
|
|
function create_portal(key, target, offscreen) {
|
|
|
|
|
/** @type {DocumentFragment | null} */
|
|
|
|
|
let fragment = null;
|
|
|
|
|
let anchor = target.anchor;
|
|
|
|
|
let previous_hydrating = false;
|
|
|
|
|
let previous_hydrate_node = null;
|
|
|
|
|
|
|
|
|
|
if (offscreen) {
|
|
|
|
|
fragment = document.createDocumentFragment();
|
|
|
|
|
anchor = create_text();
|
|
|
|
|
fragment.append(anchor);
|
|
|
|
|
|
|
|
|
|
if (hydrating) {
|
|
|
|
|
// Offscreen branches are new client work, there's no SSR content to claim.
|
|
|
|
|
previous_hydrating = true;
|
|
|
|
|
set_hydrating(false);
|
|
|
|
|
}
|
|
|
|
|
} else if (hydrating) {
|
|
|
|
|
if (target.outlet !== undefined) {
|
|
|
|
|
// An outlet was discovered before this matching portal block. Preserve
|
|
|
|
|
// the global hydration cursor while hydrating from the outlet's own anchor.
|
|
|
|
|
previous_hydrating = true;
|
|
|
|
|
previous_hydrate_node = hydrate_node;
|
|
|
|
|
set_hydrate_node((anchor = /** @type {TemplateNode} */ (get_next_sibling(anchor))));
|
|
|
|
|
function create_branch(target, offscreen_render) {
|
|
|
|
|
var outlet = target instanceof Element ? null : target;
|
|
|
|
|
|
|
|
|
|
/** @type {PortalBranch} */
|
|
|
|
|
var portal_branch = { effect: /** @type {any} */ (null), fragment: null };
|
|
|
|
|
|
|
|
|
|
if (hydrating && outlet !== null && outlet.claim !== null && !offscreen_render) {
|
|
|
|
|
// claim the server-rendered content, which sits after the previously
|
|
|
|
|
// claimed content (or after the `<!--portal:N-->` marker)
|
|
|
|
|
var previous_hydrate_node = hydrate_node;
|
|
|
|
|
var start = /** @type {TemplateNode} */ (get_next_sibling(outlet.claim));
|
|
|
|
|
|
|
|
|
|
set_hydrate_node(start);
|
|
|
|
|
|
|
|
|
|
var effect = branch(() => content(start));
|
|
|
|
|
|
|
|
|
|
// the trailing `<!---->` of this portal's server-rendered chunk
|
|
|
|
|
// becomes the branch's personal anchor
|
|
|
|
|
var anchor = hydrate_node;
|
|
|
|
|
|
|
|
|
|
if (effect.nodes === null) {
|
|
|
|
|
effect.nodes = { start, end: anchor, a: null, t: null };
|
|
|
|
|
} else {
|
|
|
|
|
// This is a DOM portal, they are not SSR'd, so temporarily disable hydration to avoid claiming the wrong nodes.
|
|
|
|
|
previous_hydrating = true;
|
|
|
|
|
set_hydrating(false);
|
|
|
|
|
// make sure the whole chunk (including boundary comments) belongs
|
|
|
|
|
// to the branch, so that it is moved/removed in its entirety
|
|
|
|
|
effect.nodes.start = start;
|
|
|
|
|
effect.nodes.end = anchor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @type {PortalBranch} */
|
|
|
|
|
const portal = {
|
|
|
|
|
key,
|
|
|
|
|
effect: branch(() => {
|
|
|
|
|
content(anchor);
|
|
|
|
|
return () => {
|
|
|
|
|
// The parent block will traverse all nodes in the current context, and then state that
|
|
|
|
|
// child effects (like this one) don't need to traverse the nodes anymore because they
|
|
|
|
|
// were already removed by the parent. That's not true in this case because the nodes
|
|
|
|
|
// are somewhere else, so remove them "manually" here.
|
|
|
|
|
const nodes = /** @type {EffectNodes} */ (portal.effect.nodes);
|
|
|
|
|
remove_effect_dom(nodes.start, /** @type {TemplateNode} */ (nodes.end));
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
fragment
|
|
|
|
|
};
|
|
|
|
|
outlet.claim = anchor;
|
|
|
|
|
|
|
|
|
|
if (previous_hydrate_node !== null) {
|
|
|
|
|
target.anchor = hydrate_node;
|
|
|
|
|
// Future portal instances for this outlet must insert after the hydrated content,
|
|
|
|
|
// not after the original outlet marker.
|
|
|
|
|
/** @type {Outlet} */ (target.outlet).anchor = hydrate_node;
|
|
|
|
|
set_hydrate_node(previous_hydrate_node);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (previous_hydrating) {
|
|
|
|
|
set_hydrating(true);
|
|
|
|
|
}
|
|
|
|
|
portal_branch.effect = effect;
|
|
|
|
|
onscreen.set(target, portal_branch);
|
|
|
|
|
register_branch(outlet, seq, portal_branch);
|
|
|
|
|
} else {
|
|
|
|
|
// render from scratch — content is created before a personal anchor,
|
|
|
|
|
// which is part of the branch so it travels with it
|
|
|
|
|
var was_hydrating = hydrating;
|
|
|
|
|
if (was_hydrating) set_hydrating(false);
|
|
|
|
|
|
|
|
|
|
return portal;
|
|
|
|
|
}
|
|
|
|
|
var personal_anchor = create_text();
|
|
|
|
|
|
|
|
|
|
/** @returns {Set<any>} */
|
|
|
|
|
function get_future_keys() {
|
|
|
|
|
// Pending newer batches determine whether an offscreen branch should be preserved.
|
|
|
|
|
// This mirrors BranchManager's ability to keep a branch alive across discarded work.
|
|
|
|
|
const keys = new Set();
|
|
|
|
|
try {
|
|
|
|
|
if (offscreen_render) {
|
|
|
|
|
var fragment = document.createDocumentFragment();
|
|
|
|
|
fragment.append(personal_anchor);
|
|
|
|
|
portal_branch.fragment = fragment;
|
|
|
|
|
} else if (target instanceof Element) {
|
|
|
|
|
target.appendChild(personal_anchor);
|
|
|
|
|
} else {
|
|
|
|
|
get_insertion_anchor(/** @type {Outlet} */ (outlet), seq).before(personal_anchor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const targets of pending.values()) {
|
|
|
|
|
for (const key of targets.keys()) {
|
|
|
|
|
keys.add(key);
|
|
|
|
|
portal_branch.effect = branch(() => content(personal_anchor));
|
|
|
|
|
} finally {
|
|
|
|
|
if (was_hydrating) set_hydrating(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return keys;
|
|
|
|
|
}
|
|
|
|
|
if (portal_branch.effect.nodes === null) {
|
|
|
|
|
portal_branch.effect.nodes = {
|
|
|
|
|
start: personal_anchor,
|
|
|
|
|
end: personal_anchor,
|
|
|
|
|
a: null,
|
|
|
|
|
t: null
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
// include the personal anchor in the effect's node range
|
|
|
|
|
portal_branch.effect.nodes.end = personal_anchor;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {Batch} batch */
|
|
|
|
|
function discard(batch) {
|
|
|
|
|
pending.delete(batch);
|
|
|
|
|
const future_keys = get_future_keys();
|
|
|
|
|
if (offscreen_render) {
|
|
|
|
|
offscreen.set(target, portal_branch);
|
|
|
|
|
} else {
|
|
|
|
|
onscreen.set(target, portal_branch);
|
|
|
|
|
|
|
|
|
|
for (const [key, portal] of offscreen) {
|
|
|
|
|
if (!future_keys.has(key)) {
|
|
|
|
|
destroy_effect(portal.effect);
|
|
|
|
|
offscreen.delete(key);
|
|
|
|
|
if (outlet !== null) {
|
|
|
|
|
register_branch(outlet, seq, portal_branch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// portaled DOM lives at the outlet, outside the node range of ancestor
|
|
|
|
|
// effects — this flag ensures it is removed when the branch is destroyed,
|
|
|
|
|
// even if an ancestor was already removed from the DOM
|
|
|
|
|
portal_branch.effect.f |= HEAD_EFFECT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @param {Batch} batch */
|
|
|
|
|
function commit(batch) {
|
|
|
|
|
if ((effect.f & DESTROYED) !== 0) return;
|
|
|
|
|
/**
|
|
|
|
|
* Brings the DOM in line with the given target selection — inserts wanted
|
|
|
|
|
* offscreen branches, and removes deselected onscreen branches (moving them
|
|
|
|
|
* offscreen if another in-flight batch still needs them)
|
|
|
|
|
* @param {Set<Outlet | Element>} targets
|
|
|
|
|
*/
|
|
|
|
|
function apply(targets) {
|
|
|
|
|
// move offscreen branches that were selected into the DOM
|
|
|
|
|
for (var target of targets) {
|
|
|
|
|
var portal_branch = offscreen.get(target);
|
|
|
|
|
|
|
|
|
|
if (portal_branch !== undefined) {
|
|
|
|
|
offscreen.delete(target);
|
|
|
|
|
onscreen.set(target, portal_branch);
|
|
|
|
|
|
|
|
|
|
const targets = pending.get(batch);
|
|
|
|
|
if (targets === undefined) return;
|
|
|
|
|
var fragment = /** @type {DocumentFragment} */ (portal_branch.fragment);
|
|
|
|
|
portal_branch.fragment = null;
|
|
|
|
|
|
|
|
|
|
for (const [b] of pending) {
|
|
|
|
|
pending.delete(b);
|
|
|
|
|
// keep values for newer batches
|
|
|
|
|
if (b === batch) break;
|
|
|
|
|
if (target instanceof Element) {
|
|
|
|
|
target.appendChild(fragment);
|
|
|
|
|
} else {
|
|
|
|
|
get_insertion_anchor(target, seq).before(fragment);
|
|
|
|
|
register_branch(target, seq, portal_branch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const future_keys = get_future_keys();
|
|
|
|
|
// remove onscreen branches that were deselected...
|
|
|
|
|
for (const [t, b] of onscreen) {
|
|
|
|
|
if (targets.has(t)) continue;
|
|
|
|
|
|
|
|
|
|
if (is_wanted(t)) {
|
|
|
|
|
// ...unless another in-flight batch still needs the branch, in
|
|
|
|
|
// which case it is moved offscreen rather than destroyed
|
|
|
|
|
var f = document.createDocumentFragment();
|
|
|
|
|
move_effect(b.effect, f);
|
|
|
|
|
b.fragment = f;
|
|
|
|
|
offscreen.set(t, b);
|
|
|
|
|
} else {
|
|
|
|
|
destroy_effect(b.effect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Newly selected targets were rendered into a fragment if this batch was deferred,
|
|
|
|
|
// move them into the real outlet/element now that the batch committed.
|
|
|
|
|
for (const [key, target] of targets) {
|
|
|
|
|
const portal = offscreen.get(key);
|
|
|
|
|
onscreen.delete(t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (portal !== undefined) {
|
|
|
|
|
/** @type {TemplateNode} */ (portal.fragment?.lastChild).remove();
|
|
|
|
|
target.anchor.before(/** @type {DocumentFragment} */ (portal.fragment));
|
|
|
|
|
portal.fragment = null;
|
|
|
|
|
offscreen.delete(key);
|
|
|
|
|
onscreen.set(key, portal);
|
|
|
|
|
// destroy offscreen branches that no batch needs anymore
|
|
|
|
|
for (const [t, b] of offscreen) {
|
|
|
|
|
if (!targets.has(t) && !is_wanted(t)) {
|
|
|
|
|
destroy_effect(b.effect);
|
|
|
|
|
offscreen.delete(t);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* True if an in-flight batch wants this portal to render into `target`
|
|
|
|
|
* @param {Outlet | Element} target
|
|
|
|
|
*/
|
|
|
|
|
function is_wanted(target) {
|
|
|
|
|
for (const { targets } of batches.values()) {
|
|
|
|
|
if (targets.has(target)) return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [key, portal] of onscreen) {
|
|
|
|
|
if (targets.has(key)) continue;
|
|
|
|
|
/**
|
|
|
|
|
* @param {Batch} batch
|
|
|
|
|
*/
|
|
|
|
|
function commit(batch) {
|
|
|
|
|
// if this batch was made obsolete, or the portal was destroyed, bail
|
|
|
|
|
if (!batches.has(batch) || (self.f & (DESTROYED | DESTROYING)) !== 0) return;
|
|
|
|
|
|
|
|
|
|
var { target } = /** @type {{ target: any, targets: Set<Outlet | Element> }} */ (
|
|
|
|
|
batches.get(batch)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onscreen.delete(key);
|
|
|
|
|
remove(batch);
|
|
|
|
|
|
|
|
|
|
if (future_keys.has(key)) {
|
|
|
|
|
// A newer pending batch wants this branch again. Move it out of the DOM instead
|
|
|
|
|
// of destroying it so the later batch can commit without recreating it.
|
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
|
move_effect(portal.effect, fragment);
|
|
|
|
|
fragment.append(create_text());
|
|
|
|
|
// The outlets for this batch's key may have changed since the batch last
|
|
|
|
|
// ran (an outlet can be (un)registered by another batch, without this
|
|
|
|
|
// block necessarily re-running within this batch), so the target
|
|
|
|
|
// selection is computed from the now-committed state
|
|
|
|
|
/** @type {Set<Outlet | Element>} */
|
|
|
|
|
var targets = new Set();
|
|
|
|
|
|
|
|
|
|
portal.fragment = fragment;
|
|
|
|
|
offscreen.set(key, portal);
|
|
|
|
|
if (target != null) {
|
|
|
|
|
if (target instanceof Element) {
|
|
|
|
|
targets.add(target);
|
|
|
|
|
} else {
|
|
|
|
|
destroy_effect(portal.effect);
|
|
|
|
|
for (var outlet of get_outlet_entry(target).outlets.v) {
|
|
|
|
|
targets.add(outlet);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [key, portal] of offscreen) {
|
|
|
|
|
if (targets.has(key) || future_keys.has(key)) continue;
|
|
|
|
|
apply(targets);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Removes the bookkeeping of a batch that committed or was discarded,
|
|
|
|
|
* and of any batches that are no longer in-flight
|
|
|
|
|
* @param {Batch} batch
|
|
|
|
|
*/
|
|
|
|
|
function remove(batch) {
|
|
|
|
|
batches.delete(batch);
|
|
|
|
|
|
|
|
|
|
destroy_effect(portal.effect);
|
|
|
|
|
offscreen.delete(key);
|
|
|
|
|
for (const b of batches.keys()) {
|
|
|
|
|
if (!b.linked) batches.delete(b);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @type {Effect} */
|
|
|
|
|
let effect;
|
|
|
|
|
/**
|
|
|
|
|
* @param {Batch} batch
|
|
|
|
|
*/
|
|
|
|
|
function discard(batch) {
|
|
|
|
|
if ((self.f & (DESTROYED | DESTROYING)) !== 0) return;
|
|
|
|
|
|
|
|
|
|
/** @type {ReturnType<typeof capture>} */
|
|
|
|
|
let portal_context;
|
|
|
|
|
remove(batch);
|
|
|
|
|
|
|
|
|
|
block(() => {
|
|
|
|
|
effect = /** @type {Effect} */ (active_effect);
|
|
|
|
|
portal_context = capture();
|
|
|
|
|
|
|
|
|
|
const target = get_target();
|
|
|
|
|
/** @type {Map<any, PortalTarget>} */
|
|
|
|
|
const targets = new Map();
|
|
|
|
|
|
|
|
|
|
if (target instanceof Element) {
|
|
|
|
|
// Our rendering logic always prepends elements to the anchor. To not confuse users,
|
|
|
|
|
// adjust the anchor such that the content is portaled _into_ the target.
|
|
|
|
|
let anchor = /** @type {TemplateNode} */ (target.firstChild);
|
|
|
|
|
if (!anchor) {
|
|
|
|
|
target.appendChild((anchor = document.createTextNode('')));
|
|
|
|
|
for (const [t, b] of offscreen) {
|
|
|
|
|
if (!is_wanted(t)) {
|
|
|
|
|
destroy_effect(b.effect);
|
|
|
|
|
offscreen.delete(t);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
block(() => {
|
|
|
|
|
self = /** @type {Effect} */ (active_effect);
|
|
|
|
|
|
|
|
|
|
targets.set(target, { anchor });
|
|
|
|
|
} else if (target != null) {
|
|
|
|
|
const entry = get_outlet_entry(target);
|
|
|
|
|
const outlets_source = entry.outlets;
|
|
|
|
|
const outlets = get(outlets_source);
|
|
|
|
|
var target = get_target();
|
|
|
|
|
var batch = /** @type {Batch} */ (current_batch);
|
|
|
|
|
var defer = should_defer_append();
|
|
|
|
|
|
|
|
|
|
// We are adding pending portals to the entry, so that outlets can render them when they are discovered.
|
|
|
|
|
set_unrendered(entry);
|
|
|
|
|
/** @type {Set<Outlet | Element>} */
|
|
|
|
|
var targets = new Set();
|
|
|
|
|
|
|
|
|
|
for (const outlet of outlets) {
|
|
|
|
|
targets.set(outlet, {
|
|
|
|
|
anchor: outlet.anchor,
|
|
|
|
|
outlet
|
|
|
|
|
});
|
|
|
|
|
/** @type {(() => void) | undefined} */
|
|
|
|
|
var teardown;
|
|
|
|
|
|
|
|
|
|
/** the outlet key this run targets, if any */
|
|
|
|
|
var key = target != null && !(target instanceof Element) ? target : null;
|
|
|
|
|
|
|
|
|
|
// Register dependencies on the outlets of the current key, but also on those
|
|
|
|
|
// of keys other in-flight batches are interested in. Otherwise, if this run
|
|
|
|
|
// switches to a different key, changes to the other keys' outlets would no
|
|
|
|
|
// longer re-run this block within those batches
|
|
|
|
|
/** @type {Set<any>} */
|
|
|
|
|
var keys = new Set();
|
|
|
|
|
|
|
|
|
|
if (key != null) keys.add(key);
|
|
|
|
|
|
|
|
|
|
for (const info of batches.values()) {
|
|
|
|
|
if (info.target != null && !(info.target instanceof Element)) {
|
|
|
|
|
keys.add(info.target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const batch = /** @type {Batch} */ (current_batch);
|
|
|
|
|
const defer = should_defer_append();
|
|
|
|
|
for (const k of keys) {
|
|
|
|
|
var outlets = get(get_outlet_entry(k).outlets);
|
|
|
|
|
|
|
|
|
|
// Ensure every target requested by this batch has a branch, but do not destroy
|
|
|
|
|
// branches that are absent from this batch until commit/discard tells us whether
|
|
|
|
|
// this batch actually wins.
|
|
|
|
|
for (const [key, target] of targets) {
|
|
|
|
|
let portal = onscreen.get(key) ?? offscreen.get(key);
|
|
|
|
|
if (k === key) {
|
|
|
|
|
for (var outlet of outlets) {
|
|
|
|
|
targets.add(outlet);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (portal !== undefined) {
|
|
|
|
|
if (defer) batch.unskip_effect(portal.effect);
|
|
|
|
|
} else {
|
|
|
|
|
portal = create_portal(key, target, defer && !(hydrating && target.outlet !== undefined));
|
|
|
|
|
(portal.fragment !== null ? offscreen : onscreen).set(key, portal);
|
|
|
|
|
if (target != null) {
|
|
|
|
|
if (target instanceof Element) {
|
|
|
|
|
targets.add(target);
|
|
|
|
|
} else if (hydrating) {
|
|
|
|
|
// an outlet with our key may appear later during this hydration
|
|
|
|
|
// pass (`{#portal ...}` before `{@portal ...}` in the markup).
|
|
|
|
|
// Register a callback so it can have us claim our server-rendered
|
|
|
|
|
// content at its position
|
|
|
|
|
var entry = get_outlet_entry(target);
|
|
|
|
|
var restore = capture();
|
|
|
|
|
|
|
|
|
|
/** @param {Outlet} o */
|
|
|
|
|
var render = (o) => {
|
|
|
|
|
if ((self.f & (DESTROYED | DESTROYING)) !== 0) return;
|
|
|
|
|
|
|
|
|
|
var previous = capture();
|
|
|
|
|
restore(false);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!onscreen.has(o) && !offscreen.has(o)) {
|
|
|
|
|
create_branch(o, false);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
previous(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
entry.pending.add(render);
|
|
|
|
|
teardown = () => entry.pending.delete(render);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pending.set(batch, targets);
|
|
|
|
|
for (var t of targets) {
|
|
|
|
|
if (!onscreen.has(t) && !offscreen.has(t)) {
|
|
|
|
|
create_branch(t, defer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
batches.set(batch, { target, targets });
|
|
|
|
|
|
|
|
|
|
// even if this batch's changes are applied right away, we need to know
|
|
|
|
|
// when the batch is done, so that its bookkeeping can be removed
|
|
|
|
|
batch.oncommit(commit);
|
|
|
|
|
batch.ondiscard(discard);
|
|
|
|
|
|
|
|
|
|
if (defer) {
|
|
|
|
|
for (const [key, portal] of onscreen) {
|
|
|
|
|
if (targets.has(key)) {
|
|
|
|
|
batch.unskip_effect(portal.effect);
|
|
|
|
|
for (const [t, b] of onscreen) {
|
|
|
|
|
if (targets.has(t)) {
|
|
|
|
|
batch.unskip_effect(b.effect);
|
|
|
|
|
} else {
|
|
|
|
|
batch.skip_effect(portal.effect);
|
|
|
|
|
batch.skip_effect(b.effect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [key, portal] of offscreen) {
|
|
|
|
|
if (targets.has(key)) {
|
|
|
|
|
batch.unskip_effect(portal.effect);
|
|
|
|
|
for (const [t, b] of offscreen) {
|
|
|
|
|
if (targets.has(t)) {
|
|
|
|
|
batch.unskip_effect(b.effect);
|
|
|
|
|
} else {
|
|
|
|
|
batch.skip_effect(portal.effect);
|
|
|
|
|
batch.skip_effect(b.effect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
batch.oncommit(commit);
|
|
|
|
|
batch.ondiscard(discard);
|
|
|
|
|
} else {
|
|
|
|
|
commit(batch);
|
|
|
|
|
apply(targets);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
clear_unrendered();
|
|
|
|
|
|
|
|
|
|
if (/** @type {Effect} */ (effect).f & DESTROYING) {
|
|
|
|
|
destroy_portals(onscreen);
|
|
|
|
|
destroy_portals(offscreen);
|
|
|
|
|
pending.clear();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return teardown;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|