const REORDER = [];
function rowsOf(n) {
const out = [];
for (let i = 0; i < n; i += 1) out.push({ id: i });
return out;
}
function reorderRow(item) {
const node = document.createElement('i');
const label = document.createTextNode('');
node.appendChild(label);
bindText(label, () => String(item().id));
return node;
}
function eachIntoCursor(start, end, listGetter, keyOf, render) {
let mounted = new Map();
onCleanup(() => mounted.forEach((entry) => entry.dispose()));
effect(() => {
const items = [...(listGetter() ?? [])];
const parent = end.parentNode;
batch(() => {
const keys = [];
const live = new Set();
for (const item of items) {
const key = keyOf(item, keys.length);
if (live.has(key)) {
throw new Error('Duplicate key in a list. Keys must be unique.');
}
live.add(key);
keys.push(key);
}
for (const [key, entry] of mounted) {
if (!live.has(key)) {
for (const node of entry.nodes) node.remove();
entry.dispose();
mounted.delete(key);
}
}
const next = new Map();
let cursor = start.nextSibling;
for (let i = 0; i < items.length; i += 1) {
const item = items[i];
const key = keys[i];
let entry = mounted.get(key);
if (entry === undefined) {
const [get, set] = signal(item);
const [rendered, dispose] = owned(() => render(get));
const nodes = rendered.nodeType === 11 ? [...rendered.childNodes] : [rendered];
entry = { nodes, set, dispose };
} else {
entry.set(item);
}
next.set(key, entry);
if (cursor !== entry.nodes[0]) {
for (const node of entry.nodes) parent.insertBefore(node, cursor);
} else {
cursor = entry.nodes[entry.nodes.length - 1].nextSibling;
}
}
mounted = next;
});
});
}
const SHAPES = [
{
name: 'swap two rows',
of: (base) => {
const out = base.slice();
const a = out[1];
out[1] = out[out.length - 2];
out[out.length - 2] = a;
return out;
},
},
{
name: 'move the last row to the front',
of: (base) => [base[base.length - 1], ...base.slice(0, base.length - 1)],
},
{
name: 'remove one, add one, swap two',
of: (base) => {
const out = base.slice();
out.splice(4, 1);
const a = out[1];
out[1] = out[out.length - 2];
out[out.length - 2] = a;
out.push({ id: -1 });
return out;
},
},
{
name: 'reverse the whole list',
of: (base) => base.slice().reverse(),
},
];
const SIZES = [100, 1000, 5000];
function orderDigest(host) {
let hash = 0x811c9dc5;
const push = (text) => {
for (let i = 0; i < text.length; i += 1) {
hash ^= text.charCodeAt(i);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
};
const visit = (node) => {
if (node.kind === 'text') {
push(node.nodeValue);
push('|');
}
for (const child of node.childNodes) visit(child);
};
visit(host);
return hash;
}
function runReorderArm(name, reconcile) {
for (const size of SIZES) {
const base = rowsOf(size);
const [list, setList] = signal(base);
const host = document.createElement('div');
const region = anchors();
const start = region.firstChild;
const end = region.lastChild;
host.appendChild(region);
reconcile(start, end, list, (item) => item.id, reorderRow);
for (const shape of SHAPES) {
setList(base);
resetCounts();
setList(shape.of(base));
const counts = snapshot();
REORDER.push(
'RESULT\t' +
name +
'\t' +
shape.name +
' at N=' +
size +
'\t' +
[
'moves=' + counts.crossings.insertBefore,
'removals=' + counts.crossings.removeChild,
'rows=' + host.childNodes.filter((n) => n.kind === 'element').length,
'digest=' + orderDigest(host),
].join(',')
);
}
}
}
runReorderArm('lis', eachInto);
runReorderArm('cursor', eachIntoCursor);
REORDER.join('\n');