ripple
Version:
Ripple is an elegant TypeScript UI framework
1,361 lines (1,251 loc) • 36.2 kB
JavaScript
/** @import { AppendIntoAnchor, Block, Tracked } from '#client' */
import { IS_CONTROLLED, IS_INDEXED, LOCAL_ITEMS, ROOT_CONTROLLED } from '../../../constants.js';
import {
create_block as allocate_block,
branch,
destroy_block,
destroy_block_children,
get_first_node,
get_last_node,
own_anchor,
render,
} from './blocks.js';
import {
BLOCK_HAS_RUN,
BRANCH_BLOCK,
DESTROYED,
FOR_BLOCK,
ITEM_BLOCK,
TRACKED_ARRAY,
} from './constants.js';
import { hydrate_first_child, hydrate_next, hydrate_node, hydrating } from './hydration.js';
import { get_last_child, next_sibling, resolve_anchor } from './operations.js';
import { append } from './template.js';
import { active_block, run_block, run_branch, set, set_tracking, tracked } from './runtime.js';
import { array_from, is_array } from '@tsrx/core/runtime/language-helpers';
import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
/**
* @template V
* @param {Node | AppendIntoAnchor} anchor
* @param {V} value
* @param {number} index
* @param {any} key the key the item was matched by (keyed lists)
* @param {ListState} state the list's state: how items render
* @returns {Block}
*/
function create_item(anchor, value, index, key, state) {
var block = /** @type {Block} */ (active_block);
var render_fn = state.r;
var map_item = state.m;
var tracked_index = state.x ? tracked(index, block) : undefined;
var tracked_value = state.y
? tracked(map_item === undefined ? value : map_item(value), block)
: value;
var item_state = {
start: null,
end: null,
i: tracked_index,
v: tracked_value,
// The item as the collection holds it; a keyed diff compares against it.
r: value,
// The key the item was matched by: fixed for the block's whole life, so
// the body reads it as a plain value (a compiled `@if (outer === item.key)`
// depends on the selector alone, not on the item).
k: key,
// The render state of a body that carries its render block on the item
// block itself (see `item`), or null.
p: null,
};
// The item renders in place: allocated, linked and run through
// `run_branch`, with none of the first-run bookkeeping a block that may
// re-run needs (a rerun is `run_item`'s, through `run_block`).
var b = allocate_block(BRANCH_BLOCK, run_item, item_state);
// The item's tracked value and index are owned by the item block itself.
if (tracked_value !== value) {
/** @type {Tracked} */ (tracked_value).b = b;
}
if (tracked_index !== undefined) {
/** @type {Tracked} */ (tracked_index).b = b;
}
run_branch(b, render_fn, anchor, tracked_value, tracked_index, key);
b.f ^= BLOCK_HAS_RUN;
return b;
}
/**
* A matched item at its new index: its tracked index and, for a computed key,
* its tracked value follow the collection.
* @param {ListState} state
* @param {Block} block
* @param {any} value
* @param {number} index
*/
function patch_item(state, block, value, index) {
if (state.x) {
update_index(block, index);
}
if (state.k !== undefined) {
update_value(block, value, state.m, state.l);
}
}
/**
* The insertion point of a list: a node the items go before, or, for a
* controlled list (the sole content of its parent), an append-into sentinel so
* items append to the parent with no anchor node in the DOM.
* @typedef {Element | Text | AppendIntoAnchor} ListAnchor
*/
/**
* @param {Element} parent
* @returns {AppendIntoAnchor}
*/
function controlled_anchor(parent) {
return { parent, into: true, tail: false };
}
/**
* A re-run of an item that carries its render block (see `item`): only the
* body's update function, tracked. The first run is `run_branch`'s, in
* `create_item`; an item block with no render block of its own never re-runs.
* @param {{ i: Tracked | undefined, v: any, k: any, p: any }} state
*/
function run_item(state) {
set_tracking(true);
/** @type {(prev: any) => void} */ (
/** @type {ListState} */ (/** @type {Block} */ (/** @type {Block} */ (active_block).p).s).u
)(state.p);
}
/**
* The compiled body of a list item whose updates all live in one render block
* calls this where it would have created that block: the item block carries
* the render block instead. The update function is the list's (passed to
* `for`/`for_keyed`, the same for every item) and `prev` its state; it runs
* tracked here, so its dependencies are the item block's own, and a change
* re-runs it alone through `run_item`. One block, one run, and one state
* object less per item.
* @param {any} prev
*/
export function item(prev) {
var block = /** @type {Block} */ (active_block);
block.s.p = prev;
block.f |= ITEM_BLOCK;
set_tracking(true);
/** @type {(prev: any) => void} */ (
/** @type {ListState} */ (/** @type {Block} */ (block.p).s).u
)(prev);
set_tracking(false);
}
/**
* @param {ListAnchor} anchor
* @param {(anchor: Node) => void} render_empty
* @returns {Block}
*/
function create_empty(anchor, render_empty) {
return branch(() => {
render_empty(/** @type {Node} */ (anchor));
});
}
/**
* @param {Block} block
* @param {ChildNode | AppendIntoAnchor} anchor
* @returns {void}
*/
function move(block, anchor) {
// Fast path: a normal item records its own range. Only an optimized single
// control-flow / component root item (DOM rendered through a descendant
// block, `s.start` null) needs the descent via `get_first_node`/`get_last_node`.
var s = block.s;
var node = s.start;
var end;
if (node === null) {
node = get_first_node(block);
if (node === null) {
return;
}
end = get_last_node(block);
} else {
end = s.end;
}
// A controlled list appends into its parent (no anchor node); anything
// else inserts before its anchor.
/** @type {Node | null} */
var parent = null;
/** @type {ChildNode | null} */
var before = null;
if (/** @type {AppendIntoAnchor} */ (anchor).into === true) {
parent = /** @type {AppendIntoAnchor} */ (anchor).parent;
} else {
before = /** @type {ChildNode} */ (anchor);
}
if (node === end) {
insert_node(parent, before, node);
return;
}
while (node !== null) {
var next_node = /** @type {Node} */ (next_sibling(node));
insert_node(parent, before, node);
node = next_node;
if (node === end) {
insert_node(parent, before, /** @type {Node} */ (end));
break;
}
}
}
/**
* @param {Node | null} parent
* @param {ChildNode | null} before
* @param {Node} node
*/
function insert_node(parent, before, node) {
if (parent !== null) {
parent.appendChild(node);
} else {
/** @type {ChildNode} */ (before).before(node);
}
}
/**
* Resolve the insertion anchor for a block at `index`: the first real DOM node
* at or after `index`, or `fallback` when every remaining block renders nothing.
* Scanning forward keeps insertions correct even when an optimized item renders
* no DOM (e.g. a single `@if` that is currently false), which previously relied
* on a synthesized `<!>` wrapper as a stable position marker.
* @param {Block[]} blocks
* @param {number} index
* @param {number} length
* @param {ListAnchor} fallback
* @returns {ChildNode | AppendIntoAnchor}
*/
function block_start(blocks, index, length, fallback) {
if (index >= length) {
return fallback;
}
// Fast path: a normal item records its own boundary, so this is the same
// single property read the pre-#1307 code did — no descent, no scan.
var first = blocks[index].s.start;
if (first !== null) {
return first;
}
for (var k = index; k < length; k++) {
var node = get_first_node(blocks[k]);
if (node !== null) {
return /** @type {ChildNode} */ (node);
}
}
return fallback;
}
/**
* @template V
* @param {V[] | Iterable<V>} collection
* @returns {V[]}
*/
function collection_to_array(collection) {
var array = is_array(collection) ? collection : collection == null ? [] : array_from(collection);
// If we are working with a tracked array, then we need to get a copy of
// the elements, as the array itself is proxied, and not useful in diffing
if (TRACKED_ARRAY in array) {
array = array_from(array);
}
return array;
}
/**
* The state of a list block. The reconciled list (`array`, `blocks`, `keys`,
* `empty`) and the inputs the list is re-run with both live here, so a list
* allocates no closures.
*/
class ListState {
/** @type {any[]} */
array = [];
/** @type {Block[]} */
blocks = [];
/** @type {any[] | null} null until the first reconcile */
keys = null;
/** @type {Block | null} */
empty = null;
/**
* @param {ListAnchor} anchor
* @param {() => any} get_collection
* @param {(anchor: Node, value: any, index?: any) => Block} render_fn
* @param {boolean} is_controlled
* @param {boolean} is_indexed
* @param {((item: any) => any) | undefined} get_key
* @param {((anchor: Node) => void) | undefined} render_empty
* @param {((item: any) => any) | undefined} map_item
* @param {((prev: any) => void) | undefined} update_fn the update function
* of an item body that carries its render block (see `item`)
* @param {boolean} is_local items are held as they are (`LOCAL_ITEMS`)
* @param {boolean} is_keyed each item holds a tracked of its value
*/
constructor(
anchor,
get_collection,
render_fn,
is_controlled,
is_indexed,
get_key,
render_empty,
map_item,
update_fn,
is_local,
is_keyed,
) {
this.a = anchor;
this.g = get_collection;
this.r = render_fn;
this.c = is_controlled;
this.x = is_indexed;
this.k = get_key;
this.e = render_empty;
this.m = map_item;
this.u = update_fn;
this.l = is_local;
this.y = is_keyed;
}
}
/**
* @param {ListState} state
*/
function run_for(state) {
var block = /** @type {Block} */ (active_block);
var array = collection_to_array(state.g());
// Items render untracked, as in a branch; the list tracks only its collection.
set_tracking(false);
reconcile_by_ref(state.a, block, array);
set_tracking(true);
// Re-anchor a hydrated list: the hydrated anchor is the block's start
// marker; later inserts and end moves must go before the cursor, which now
// sits after the hydrated items.
if (HYDRATION && hydrating) {
state.a = /** @type {Element | Text} */ (hydrate_node);
}
}
/**
* @param {ListState} state
*/
function run_for_keyed(state) {
var block = /** @type {Block} */ (active_block);
var array = collection_to_array(state.g());
set_tracking(false);
reconcile_by_key(state.a, block, array);
set_tracking(true);
if (HYDRATION && hydrating) {
state.a = /** @type {Element | Text} */ (hydrate_node);
}
}
/**
* @template V
* @param {Element | AppendIntoAnchor} node
* @param {() => V[] | Iterable<V>} get_collection
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
* @param {number} flags
* @param {(anchor: Node) => void} [render_empty]
* @param {(prev: any) => void} [update_fn] for an item body that carries its
* render block: its update function (see `item`)
* @returns {void}
*/
export function for_block(node, get_collection, render_fn, flags, render_empty, update_fn) {
var is_controlled = (flags & IS_CONTROLLED) !== 0;
var is_indexed = (flags & IS_INDEXED) !== 0;
var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
// A root-controlled list receives the component's `__anchor`, which may be
// an append-into sentinel; moves and end insertions need a real node.
var anchor = /** @type {ListAnchor} */ (root_controlled ? resolve_anchor(node) : node);
/** @type {Node | undefined} */
var boundary;
if (is_controlled) {
if (HYDRATION && hydrating) {
// The cursor sits on the list's element; the items are its children.
hydrate_first_child();
} else {
anchor = controlled_anchor(/** @type {Element} */ (node));
}
}
if (HYDRATION && hydrating) {
if (root_controlled) {
boundary = /** @type {Node} */ (hydrate_node);
}
hydrate_next();
}
render(
run_for,
new ListState(
anchor,
get_collection,
render_fn,
is_controlled,
is_indexed,
undefined,
render_empty,
undefined,
update_fn,
false,
false,
),
FOR_BLOCK,
);
if (!is_controlled) own_anchor(node, /** @type {Node} */ (anchor));
if (HYDRATION && hydrating && root_controlled) {
// The original `node`: for a sentinel, `hydrate_append` performs the
// cursor advance that stands in for the eliminated sibling navigation.
append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
}
}
/**
* @template V
* @template K
* @param {Element | AppendIntoAnchor} node
* @param {() => V[] | Iterable<V>} get_collection
* @param {(anchor: Node, value: V | Tracked, index?: any) => Block} render_fn
* @param {number} flags
* @param {(item: V) => K} [get_key] omitted for identity keys (`key item`)
* @param {(anchor: Node) => void} [render_empty]
* @param {(item: V) => any} [map_item] for a destructuring pattern with a rest
* element or a default: destructures the item once, natively, into an object
* of the pattern's names, which is what the item's tracked then holds
* @param {(prev: any) => void} [update_fn] for an item body that carries its
* render block: its update function (see `item`)
* @returns {void}
*/
export function for_block_keyed(
node,
get_collection,
render_fn,
flags,
get_key,
render_empty,
map_item,
update_fn,
) {
var is_controlled = (flags & IS_CONTROLLED) !== 0;
var is_indexed = (flags & IS_INDEXED) !== 0;
var root_controlled = (flags & ROOT_CONTROLLED) !== 0;
var anchor = /** @type {ListAnchor} */ (root_controlled ? resolve_anchor(node) : node);
/** @type {Node | undefined} */
var boundary;
if (is_controlled) {
var parent_node = /** @type {Element} */ (node);
if (HYDRATION && hydrating) {
hydrate_first_child();
anchor = /** @type {Element | Text} */ (get_last_child(parent_node));
} else {
anchor = controlled_anchor(parent_node);
}
}
if (HYDRATION && hydrating) {
if (root_controlled) {
boundary = /** @type {Node} */ (hydrate_node);
}
hydrate_next();
}
render(
run_for_keyed,
new ListState(
anchor,
get_collection,
render_fn,
is_controlled,
is_indexed,
get_key,
render_empty,
map_item,
update_fn,
(flags & LOCAL_ITEMS) !== 0,
(flags & LOCAL_ITEMS) === 0,
),
FOR_BLOCK,
);
if (!is_controlled) own_anchor(node, /** @type {Node} */ (anchor));
if (HYDRATION && hydrating && root_controlled) {
// The original `node`: for a sentinel, `hydrate_append` performs the
// cursor advance that stands in for the eliminated sibling navigation.
append(/** @type {ChildNode} */ (node), /** @type {Node} */ (boundary));
}
}
/**
* @template V
* @param {ListAnchor} anchor
* @param {Block} block
* @param {V[]} array
* @returns {void}
*/
function reconcile_fast_clear(anchor, block, array) {
var state = block.s;
var into = /** @type {AppendIntoAnchor} */ (anchor).into === true;
var parent_node = /** @type {Element} */ (
into ? /** @type {AppendIntoAnchor} */ (anchor).parent : /** @type {Node} */ (anchor).parentNode
);
parent_node.textContent = '';
destroy_block_children(block);
if (!into) parent_node.append(/** @type {Node} */ (anchor));
state.array = array;
state.blocks = [];
state.empty = null;
}
/**
* @param {Block} block
* @param {number} index
* @returns {void}
*/
function update_index(block, index) {
set(block.s.i, index);
}
/**
* @param {Block} block
* @param {any} value
* @param {((item: any) => any) | undefined} map_item
* @param {boolean} is_local
* @returns {void}
*/
function update_value(block, value, map_item, is_local) {
var state = block.s;
if (state.r !== value) {
state.r = value;
var next = map_item === undefined ? value : map_item(value);
if (is_local) {
// Held as it is, read by the item block's update function alone
// (`LOCAL_ITEMS`) off its state's `$item` slot: the block re-runs here.
state.v = next;
state.p.$item = next;
run_block(block);
} else {
set(state.v, next);
}
}
}
/**
* @template V
* @param {ListAnchor} anchor
* @param {Block} block
* @param {V[]} b
* @returns {void}
*
* The first run only creates items, so it lives in this small function and
* the diff below is compiled only once a list actually changes.
*/
function reconcile_by_key(anchor, block, b) {
var b_length = b.length;
var state = /** @type {ListState} */ (block.s);
var get_key = state.k;
if (state.keys === null && b_length > 0) {
var b_blocks = Array(b_length);
// Identity keys (`key item`) are the items themselves; the list keeps a
// copy, as the array it rendered may later be mutated in place.
var b_keys = get_key === undefined ? b.slice() : Array(b_length);
// One loop, no `map` callback machinery: most lists are short.
for (var j = 0; j < b_length; j++) {
var value = b[j];
if (get_key !== undefined) {
b_keys[j] = get_key(value);
}
b_blocks[j] = create_item(anchor, value, j, b_keys[j], state);
}
state.array = b;
state.blocks = b_blocks;
state.keys = b_keys;
return;
}
reconcile_by_key_diff(anchor, block, b);
}
/**
* Keyed diff for every run after the first (or a first run with an empty
* list). See {@link reconcile_by_key}.
* @template V
* @param {ListAnchor} anchor
* @param {Block} block
* @param {V[]} b
* @returns {void}
*/
function reconcile_by_key_diff(anchor, block, b) {
var state = /** @type {ListState} */ (block.s);
var is_controlled = state.c;
var is_indexed = state.x;
var get_key = state.k;
var render_empty = state.e;
// Variables used in conditional branches - declare with initial values
/** @type {number} */
var a_left = 0;
/** @type {number} */
var b_left = 0;
/** @type {Int32Array} */
var sources = new Int32Array(0);
/** @type {boolean} */
var moved = false;
/** @type {number} */
var pos = 0;
/** @type {number} */
var patched = 0;
/** @type {number} */
var i = 0;
var a_blocks = state.blocks;
var a_length = a_blocks.length;
var b_length = b.length;
var j = 0;
if (b_length === 0) {
if (a_length > 0) {
if (is_controlled) {
reconcile_fast_clear(anchor, block, b);
} else {
for (; j < a_length; j++) {
destroy_block(state.blocks[j]);
}
state.array = b;
state.blocks = [];
state.keys = [];
}
}
if (render_empty && state.empty === null) {
state.empty = create_empty(anchor, render_empty);
}
return;
}
if (state.empty !== null) {
destroy_block(state.empty);
state.empty = null;
}
// Fast-path for clear
if (is_controlled && b_length === 0) {
if (a_length > 0) {
reconcile_fast_clear(anchor, block, b);
}
return;
}
// Identity keys are the items themselves (copied, as the rendered array
// may later be mutated in place), so a matched key is also the item's
// current tracked value: only computed keys need `update_value`.
var b_keys = get_key === undefined ? b.slice() : b.map(get_key);
// A same-length run rewrites the block array in place: matched ends are
// already at their index, and the middle reads the old blocks from a copy
// taken before its first write.
var in_place = a_length === b_length;
var b_blocks = in_place ? a_blocks : Array(b_length);
// Fast-path for create
if (a_length === 0) {
for (; j < b_length; j++) {
b_blocks[j] = create_item(anchor, b[j], j, b_keys[j], state);
}
state.array = b;
state.blocks = b_blocks;
state.keys = b_keys;
return;
}
// Set by every run that leaves items behind.
var a_keys = /** @type {any[]} */ (state.keys);
var a_start = 0;
var b_start = 0;
var a_end = a_length - 1;
var b_end = b_length - 1;
var b_val;
var b_block;
if (get_key === undefined && !is_indexed) {
// Identity keys without an index: a matched item needs no update, so
// the unchanged prefix and suffix are skipped with plain compares.
while (a_start <= a_end && b_start <= b_end && a_keys[a_start] === b_keys[b_start]) {
if (!in_place) {
b_blocks[b_start] = a_blocks[a_start];
}
a_start++;
b_start++;
}
while (a_start <= a_end && b_start <= b_end && a_keys[a_end] === b_keys[b_end]) {
if (!in_place) {
b_blocks[b_end] = a_blocks[a_end];
}
a_end--;
b_end--;
}
}
// Match from both ends first, including the two end-crossing cases: an old
// item that moved to the far end of the new list, and a run whose ends were
// exchanged (a reversal, or a swap of two items). Those complete with plain
// moves; only what is left afterwards needs the map and LIS below.
while (a_start <= a_end && b_start <= b_end) {
if (a_keys[a_start] === b_keys[b_start]) {
b_block = b_blocks[b_start] = a_blocks[a_start];
patch_item(state, b_block, b[b_start], b_start);
a_start++;
b_start++;
continue;
}
if (a_keys[a_end] === b_keys[b_end]) {
b_block = b_blocks[b_end] = a_blocks[a_end];
patch_item(state, b_block, b[b_end], b_end);
a_end--;
b_end--;
continue;
}
if (a_start === a_end || a_blocks[a_start].s.start === null) {
break;
}
if (in_place) {
// An end-crossing move overwrites an old block before it is read.
a_blocks = a_blocks.slice();
in_place = false;
}
if (a_keys[a_end] === b_keys[b_start]) {
// Last old item is the next new one: move it in front of the old run.
b_block = b_blocks[b_start] = a_blocks[a_end];
patch_item(state, b_block, b[b_start], b_start);
move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start));
a_end--;
b_start++;
continue;
}
if (a_keys[a_start] === b_keys[b_end]) {
// First old item is the last new one: move it behind the old run.
b_block = b_blocks[b_end] = a_blocks[a_start];
patch_item(state, b_block, b[b_end], b_end);
move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor));
a_start++;
b_end--;
continue;
}
break;
}
var fast_path_removal = false;
if (a_start > a_end) {
if (b_start <= b_end) {
var target_node = block_start(b_blocks, b_end + 1, b_length, anchor);
while (b_start <= b_end) {
b_blocks[b_start] = create_item(target_node, b[b_start], b_start, b_keys[b_start], state);
b_start++;
}
}
} else if (b_start > b_end) {
while (a_start <= a_end) {
destroy_block(a_blocks[a_start++]);
}
} else {
if (in_place) {
a_blocks = a_blocks.slice();
in_place = false;
}
a_left = a_end - a_start + 1;
b_left = b_end - b_start + 1;
sources = new Int32Array(b_left + 1);
moved = false;
pos = 0;
patched = 0;
i = 0;
fast_path_removal = is_controlled && a_left === a_length;
// When sizes are small, just loop them through
if (b_length < 4 || (a_left | b_left) < 32) {
for (i = a_start; i <= a_end; ++i) {
if (patched < b_left) {
for (j = b_start; j <= b_end; j++) {
if (a_keys[i] === b_keys[j]) {
sources[j - b_start] = i + 1;
if (fast_path_removal) {
fast_path_removal = false;
while (a_start < i) {
destroy_block(a_blocks[a_start++]);
}
}
if (pos > j) {
moved = true;
} else {
pos = j;
}
b_block = b_blocks[j] = a_blocks[i];
patch_item(state, b_block, b[j], j);
++patched;
break;
}
}
if (!fast_path_removal && j > b_end) {
destroy_block(a_blocks[i]);
}
} else if (!fast_path_removal) {
destroy_block(a_blocks[i]);
}
}
} else {
var map = new Map();
for (i = b_start; i <= b_end; ++i) {
map.set(b_keys[i], i);
}
for (i = a_start; i <= a_end; ++i) {
if (patched < b_left) {
j = map.get(a_keys[i]);
if (j !== undefined) {
if (fast_path_removal) {
fast_path_removal = false;
while (i > a_start) {
destroy_block(a_blocks[a_start++]);
}
}
sources[j - b_start] = i + 1;
if (pos > j) {
moved = true;
} else {
pos = j;
}
b_block = b_blocks[j] = a_blocks[i];
patch_item(state, b_block, b[j], j);
++patched;
} else if (!fast_path_removal) {
destroy_block(a_blocks[i]);
}
} else if (!fast_path_removal) {
destroy_block(a_blocks[i]);
}
}
}
}
if (fast_path_removal) {
reconcile_fast_clear(anchor, block, []);
reconcile_by_key(anchor, block, b);
return;
} else if (moved) {
var next_pos = 0;
var seq = lis_algorithm(sources);
j = seq.length - 1;
// When most surviving items have to move anyway, re-lay the whole range
// in order (see `relay`).
if ((patched - seq.length) * 3 > b_left * 2) {
relay(anchor, a_blocks, a_start, a_end, b, b_blocks, b_start, b_end, sources, b_keys, state);
state.array = b;
state.blocks = b_blocks;
state.keys = b_keys;
return;
}
for (i = b_left - 1; i >= 0; i--) {
if (sources[i] === 0) {
pos = i + b_start;
b_val = b[pos];
next_pos = pos + 1;
var target = block_start(b_blocks, next_pos, b_length, anchor);
b_blocks[pos] = create_item(target, b_val, pos, b_keys[pos], state);
} else if (j < 0 || i !== seq[j]) {
pos = i + b_start;
next_pos = pos + 1;
var target = block_start(b_blocks, next_pos, b_length, anchor);
move(b_blocks[pos], target);
} else {
j--;
}
}
} else if (patched !== b_left) {
for (i = b_left - 1; i >= 0; i--) {
if (sources[i] === 0) {
pos = i + b_start;
b_val = b[pos];
next_pos = pos + 1;
var target = block_start(b_blocks, next_pos, b_length, anchor);
b_blocks[pos] = create_item(target, b_val, pos, b_keys[pos], state);
}
}
}
state.array = b;
state.blocks = b_blocks;
state.keys = b_keys;
}
/**
* Re-lays the unmatched range `b_start..b_end` in its new order, walking it
* front to back: `cursor` is the first node of the range that is not in place
* yet, so a block already there costs a compare and a sibling read, and every
* other block (or a new item) is inserted before `cursor`. Inserting each item
* toward the front is what the reverse fast path does too; appending every
* item at the end of the range instead cost about twice as much in Blink's
* reorder floor (500 rows: 0.55 ms against 0.29), and several times more
* right after a layout flush, so this holds even when most items move.
* @template V
* @param {ListAnchor} anchor
* @param {Block[]} a_blocks the old blocks; destroyed ones are skipped
* @param {number} a_start
* @param {number} a_end
* @param {V[]} b
* @param {Block[]} b_blocks matched blocks at their new index, holes for new items
* @param {number} b_start
* @param {number} b_end
* @param {Int32Array} sources zero at the index of a new item
* @param {any[] | undefined} b_keys the new items' keys (a keyed list)
* @param {ListState} state
*/
function relay(
anchor,
a_blocks,
a_start,
a_end,
b,
b_blocks,
b_start,
b_end,
sources,
b_keys,
state,
) {
var b_length = b.length;
var end_target = block_start(b_blocks, b_end + 1, b_length, anchor);
/** @type {ChildNode | AppendIntoAnchor | null} */
var cursor = null;
// The range starts at the first surviving old block.
for (var i = a_start; i <= a_end && cursor === null; i++) {
var old_block = a_blocks[i];
if ((old_block.f & DESTROYED) === 0) {
cursor = /** @type {ChildNode | null} */ (get_first_node(old_block));
}
}
if (cursor === null) {
cursor = end_target;
}
for (var pos = b_start; pos <= b_end; pos++) {
if (sources[pos - b_start] === 0) {
b_blocks[pos] = create_item(
cursor,
b[pos],
pos,
b_keys === undefined ? undefined : b_keys[pos],
state,
);
continue;
}
var block = b_blocks[pos];
var first = get_first_node(block);
if (first === null) {
// Renders nothing: it has no place to take.
continue;
}
if (first !== cursor) {
move(block, cursor);
continue;
}
var after = next_sibling(/** @type {Node} */ (get_last_node(block)));
cursor = after === null ? end_target : /** @type {ChildNode} */ (after);
}
}
/**
* @template V
* @param {ListAnchor} anchor
* @param {Block} block
* @param {V[]} b
* @returns {void}
*/
function reconcile_by_ref(anchor, block, b) {
var state = /** @type {ListState} */ (block.s);
var is_controlled = state.c;
var render_empty = state.e;
var a_length = state.array.length;
var b_length = b.length;
var j = 0;
if (b_length === 0) {
if (a_length > 0) {
if (is_controlled) {
reconcile_fast_clear(anchor, block, b);
} else {
for (; j < a_length; j++) {
destroy_block(state.blocks[j]);
}
state.array = b;
state.blocks = [];
}
}
if (render_empty && state.empty === null) {
state.empty = create_empty(anchor, render_empty);
}
return;
}
if (state.empty !== null) {
destroy_block(state.empty);
state.empty = null;
}
// Fast-path for clear
if (is_controlled && b_length === 0) {
if (a_length > 0) {
reconcile_fast_clear(anchor, block, b);
}
return;
}
var b_blocks = Array(b_length);
// Fast-path for create
if (a_length === 0) {
for (; j < b_length; j++) {
b_blocks[j] = create_item(anchor, b[j], j, undefined, state);
}
state.array = b;
state.blocks = b_blocks;
return;
}
reconcile_by_ref_diff(anchor, block, b, b_blocks);
}
/**
* Diffs a non-empty list against its previous run. Separate from the empty and
* create paths so a first render (a mount, a hydration) never compiles it.
* @template V
* @param {ListAnchor} anchor
* @param {Block} block
* @param {V[]} b
* @param {Block[]} b_blocks
* @returns {void}
*/
function reconcile_by_ref_diff(anchor, block, b, b_blocks) {
var state = /** @type {ListState} */ (block.s);
var is_controlled = state.c;
// Variables used in conditional branches - declare with initial values
/** @type {number} */
var a_left = 0;
/** @type {number} */
var b_left = 0;
/** @type {Int32Array} */
var sources = new Int32Array(0);
/** @type {boolean} */
var moved = false;
/** @type {number} */
var pos = 0;
/** @type {number} */
var patched = 0;
/** @type {number} */
var i = 0;
var a = state.array;
var a_length = a.length;
var b_length = b.length;
var j = 0;
var a_blocks = state.blocks;
var a_start = 0;
var b_start = 0;
var a_end = a_length - 1;
var b_end = b_length - 1;
var b_val;
var b_block;
// Match from both ends first, including the two end-crossing cases: an old
// item that moved to the far end of the new list, and a run whose ends were
// exchanged (a reversal, or a swap of two items). Those complete with plain
// moves; only what is left afterwards needs the map and LIS below.
while (a_start <= a_end && b_start <= b_end) {
if (a[a_start] === b[b_start]) {
b_block = b_blocks[b_start] = a_blocks[a_start];
patch_item(state, b_block, b[b_start], b_start);
a_start++;
b_start++;
continue;
}
if (a[a_end] === b[b_end]) {
b_block = b_blocks[b_end] = a_blocks[a_end];
patch_item(state, b_block, b[b_end], b_end);
a_end--;
b_end--;
continue;
}
if (a_start === a_end || a_blocks[a_start].s.start === null) {
break;
}
if (a[a_end] === b[b_start]) {
// Last old item is the next new one: move it in front of the old run.
b_block = b_blocks[b_start] = a_blocks[a_end];
patch_item(state, b_block, b[b_start], b_start);
move(b_block, /** @type {ChildNode} */ (a_blocks[a_start].s.start));
a_end--;
b_start++;
continue;
}
if (a[a_start] === b[b_end]) {
// First old item is the last new one: move it behind the old run.
b_block = b_blocks[b_end] = a_blocks[a_start];
patch_item(state, b_block, b[b_end], b_end);
move(b_block, block_start(b_blocks, b_end + 1, b_length, anchor));
a_start++;
b_end--;
continue;
}
break;
}
var fast_path_removal = false;
if (a_start > a_end) {
if (b_start <= b_end) {
var target_node = block_start(b_blocks, b_end + 1, b_length, anchor);
while (b_start <= b_end) {
b_blocks[b_start] = create_item(target_node, b[b_start], b_start, undefined, state);
b_start++;
}
}
} else if (b_start > b_end) {
while (a_start <= a_end) {
destroy_block(a_blocks[a_start++]);
}
} else {
a_left = a_end - a_start + 1;
b_left = b_end - b_start + 1;
sources = new Int32Array(b_left + 1);
moved = false;
pos = 0;
patched = 0;
i = 0;
fast_path_removal = is_controlled && a_left === a_length;
// When sizes are small, just loop them through
if (b_length < 4 || (a_left | b_left) < 32) {
for (i = a_start; i <= a_end; ++i) {
if (patched < b_left) {
for (j = b_start; j <= b_end; j++) {
if (a[i] === b[j]) {
sources[j - b_start] = i + 1;
if (fast_path_removal) {
fast_path_removal = false;
while (a_start < i) {
destroy_block(a_blocks[a_start++]);
}
}
if (pos > j) {
moved = true;
} else {
pos = j;
}
b_block = b_blocks[j] = a_blocks[i];
patch_item(state, b_block, b[j], j);
++patched;
break;
}
}
if (!fast_path_removal && j > b_end) {
destroy_block(a_blocks[i]);
}
} else if (!fast_path_removal) {
destroy_block(a_blocks[i]);
}
}
} else {
var map = new Map();
for (i = b_start; i <= b_end; ++i) {
map.set(b[i], i);
}
for (i = a_start; i <= a_end; ++i) {
if (patched < b_left) {
j = map.get(a[i]);
if (j !== undefined) {
if (fast_path_removal) {
fast_path_removal = false;
while (i > a_start) {
destroy_block(a_blocks[a_start++]);
}
}
sources[j - b_start] = i + 1;
if (pos > j) {
moved = true;
} else {
pos = j;
}
b_block = b_blocks[j] = a_blocks[i];
patch_item(state, b_block, b[j], j);
++patched;
} else if (!fast_path_removal) {
destroy_block(a_blocks[i]);
}
} else if (!fast_path_removal) {
destroy_block(a_blocks[i]);
}
}
}
}
if (fast_path_removal) {
reconcile_fast_clear(anchor, block, []);
reconcile_by_ref(anchor, block, b);
return;
} else if (moved) {
var next_pos = 0;
var seq = lis_algorithm(sources);
j = seq.length - 1;
// When most surviving items have to move anyway, re-lay the whole range
// in order (see `relay`).
if ((patched - seq.length) * 3 > b_left * 2) {
relay(
anchor,
a_blocks,
a_start,
a_end,
b,
b_blocks,
b_start,
b_end,
sources,
undefined,
state,
);
state.array = b;
state.blocks = b_blocks;
return;
}
for (i = b_left - 1; i >= 0; i--) {
if (sources[i] === 0) {
pos = i + b_start;
b_val = b[pos];
next_pos = pos + 1;
var target = block_start(b_blocks, next_pos, b_length, anchor);
b_blocks[pos] = create_item(target, b_val, pos, undefined, state);
} else if (j < 0 || i !== seq[j]) {
pos = i + b_start;
next_pos = pos + 1;
var target = block_start(b_blocks, next_pos, b_length, anchor);
move(b_blocks[pos], target);
} else {
j--;
}
}
} else if (patched !== b_left) {
for (i = b_left - 1; i >= 0; i--) {
if (sources[i] === 0) {
pos = i + b_start;
b_val = b[pos];
next_pos = pos + 1;
var target = block_start(b_blocks, next_pos, b_length, anchor);
b_blocks[pos] = create_item(target, b_val, pos, undefined, state);
}
}
}
state.array = b;
state.blocks = b_blocks;
}
/** @type {Int32Array} */
let result;
/** @type {Int32Array} */
let p;
let max_len = 0;
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
/**
* @param {Int32Array} arr
* @returns {Int32Array}
*/
function lis_algorithm(arr) {
let arrI = 0;
let i = 0;
let j = 0;
let k = 0;
let u = 0;
let v = 0;
let c = 0;
var len = arr.length;
if (len > max_len) {
max_len = len;
result = new Int32Array(len);
p = new Int32Array(len);
}
for (; i < len; ++i) {
arrI = arr[i];
if (arrI !== 0) {
j = result[k];
if (arr[j] < arrI) {
p[i] = j;
result[++k] = i;
continue;
}
u = 0;
v = k;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = k + 1;
var seq = new Int32Array(u);
v = result[u - 1];
while (u-- > 0) {
seq[u] = v;
v = p[v];
result[u] = 0;
}
return seq;
}