Implement commitMutations for insert/move/remove effects in tree order

Adds commitMutations function to reconcile.ts that processes fiber effects
in correct order: removes (reverse), inserts+moves (left-to-right with
insertBefore), updates (top-down). Integrates key-based reconciliation
pipeline into render() via reconcileNode, resolveUNode, and
createFiberForInsert.
This commit is contained in:
2026-05-18 17:17:28 +00:00
parent 9e5b901efc
commit 1e0abb0900
5 changed files with 656 additions and 13 deletions

View File

@@ -2,7 +2,8 @@ import type { UNode, UElement, URoot, ComponentFn, UComponent } from "../core/sc
import { isURoot, isUPrimitive } from "../core/schema.js";
import { Context } from "../core/context.js";
import type { Fiber } from "./fiber.js";
import { reconcileProps, commitEffects } from "./reconcile.js";
import { reconcileProps, reconcileChildren, commitMutations } from "./reconcile.js";
import type { CommitContext } from "./reconcile.js";
export interface HostConfig<TTag extends string, Instance, RootCtx> {
name: string;
@@ -116,6 +117,130 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
return fiber;
}
function createFiberForInsert(node: UNode, parentFiber: Fiber<Instance>): Fiber<Instance> {
if (isUPrimitive(node)) {
const text = node === null ? "" : String(node);
const t = host.createTextInstance(text, ctx, parentFiber.instance);
host.emit?.("instance.create", `text_${Date.now()}`, { kind: "text", value: text });
return {
instance: t,
tag: "#text",
props: { text },
key: undefined,
children: [],
parent: parentFiber,
effect: null,
signalDisposers: [],
prevProps: null,
};
}
const el = node as UElement;
if (typeof el.type === "function") {
const component = el.type as ComponentFn;
const out = component({ ...el.props, children: el.children });
host.emit?.("component.invoke", `comp_${Date.now()}`, { type: (component as unknown as UComponent).displayName ?? "anonymous" });
return createFiberForInsert(out, parentFiber);
}
const tag = el.type as TTag;
const inst = host.createInstance(tag, el.props as Record<string, unknown>, ctx, parentFiber.instance);
host.emit?.("instance.create", `${tag}_${Date.now()}`, { kind: "element", tag, props: el.props });
const fiber: Fiber<Instance> = {
instance: inst,
tag,
props: el.props as Record<string, unknown>,
key: el.key,
children: [],
parent: parentFiber,
effect: null,
signalDisposers: [],
prevProps: null,
};
for (const child of el.children) {
const childFiber = createFiberForInsert(child, fiber);
if (childFiber) {
host.appendChild(inst, childFiber.instance, ctx);
fiber.children.push(childFiber);
}
}
return fiber;
}
function resolveUNode(node: UNode): UNode[] {
if (node == null || node === false) return [];
if (isUPrimitive(node)) return [node];
if (isURoot(node)) {
const result: UNode[] = [];
for (const child of (node as URoot).children) {
result.push(...resolveUNode(child));
}
return result;
}
const el = node as UElement;
if (typeof el.type === "function") {
const component = el.type as ComponentFn;
const out = component({ ...el.props, children: el.children });
return resolveUNode(out);
}
return [node];
}
function reconcileNode(fiber: Fiber<Instance>, nextNode: UNode): void {
if (isUPrimitive(nextNode)) {
if (fiber.tag === "#text") {
reconcileProps(fiber, nextNode, host as HostConfig<string, Instance, unknown>, ctx as unknown);
}
return;
}
if (isURoot(nextNode)) {
const rootChildren = (nextNode as URoot).children;
const classification = reconcileChildren(
fiber.children,
rootChildren,
);
for (const m of classification.matched) {
reconcileProps(m.oldFiber, m.newChild, host as HostConfig<string, Instance, unknown>, ctx as unknown);
}
const commitCtx: CommitContext<Instance> = {
host: host as HostConfig<string, Instance, unknown>,
ctx: ctx as unknown,
createFiber: (node, parent) => createFiberForInsert(node, parent),
};
commitMutations(fiber, classification, commitCtx);
return;
}
const el = nextNode as UElement;
if (typeof el.type === "function") {
const component = el.type as ComponentFn;
const out = component({ ...el.props, children: el.children });
reconcileNode(fiber, out);
return;
}
if (fiber.tag !== el.type) return;
reconcileProps(fiber, el, host as HostConfig<string, Instance, unknown>, ctx as unknown);
const classification = reconcileChildren(fiber.children, el.children);
for (const m of classification.matched) {
reconcileProps(m.oldFiber, m.newChild, host as HostConfig<string, Instance, unknown>, ctx as unknown);
}
const commitCtx: CommitContext<Instance> = {
host: host as HostConfig<string, Instance, unknown>,
ctx: ctx as unknown,
createFiber: (node, parent) => createFiberForInsert(node, parent),
};
commitMutations(fiber, classification, commitCtx);
}
return {
host,
ctx,
@@ -125,13 +250,23 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
render(node: UNode) {
if (this.rootFiber) {
const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
for (let i = 0; i < payloadChildren.length; i++) {
const childFiber = this.rootFiber.children[i];
if (childFiber) {
reconcileProps(childFiber, payloadChildren[i]!, host as HostConfig<string, Instance, unknown>, ctx as unknown);
}
const resolvedChildren: UNode[] = [];
for (const child of payloadChildren) {
resolvedChildren.push(...resolveUNode(child));
}
commitEffects(this.rootFiber, host as HostConfig<string, Instance, unknown>, ctx as unknown);
const classification = reconcileChildren(
this.rootFiber.children,
resolvedChildren,
);
for (const m of classification.matched) {
reconcileNode(m.oldFiber, m.newChild);
}
const commitCtx: CommitContext<Instance> = {
host: host as HostConfig<string, Instance, unknown>,
ctx: ctx as unknown,
createFiber: (node, parent) => createFiberForInsert(node, parent),
};
commitMutations(this.rootFiber, classification, commitCtx);
host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length });
return;
}

View File

@@ -4,6 +4,12 @@ import type { HostConfig } from "./config.js";
import type { UNode, UElement } from "../core/schema.js";
import { isUPrimitive, isURoot, isUElement } from "../core/schema.js";
export interface CommitContext<I> {
host: HostConfig<string, I, unknown>;
ctx: unknown;
createFiber: (node: UNode, parentFiber: Fiber<I>) => Fiber<I>;
}
interface PendingUpdate {
fiber: Fiber<unknown>;
nextNode: UNode;
@@ -320,4 +326,125 @@ export function reconcileChildren<I>(
}
return { matched, added, removed, moves };
}
export function commitMutations<I>(
parentFiber: Fiber<I>,
classification: ChildClassification<I>,
commitCtx: CommitContext<I>,
): void {
const { host, ctx, createFiber } = commitCtx;
const parentInst = parentFiber.instance;
const movedSet = new Set<Fiber<I>>(classification.moves.values());
// Map matched index (position in classification.matched[]) for quick lookup
const matchedArrayIndex = new Map<MatchedChild<I>, number>();
for (let i = 0; i < classification.matched.length; i++) {
matchedArrayIndex.set(classification.matched[i]!, i);
}
// Build maps keyed by new-children position
const matchedByNewIndex = new Map<number, MatchedChild<I>>();
for (const m of classification.matched) {
matchedByNewIndex.set(m.index, m);
}
const addedByIndex = new Map<number, UElement>();
for (const { newChild, index } of classification.added) {
addedByIndex.set(index, newChild);
}
// Collect all positions in new-children order
const allIndices = new Set<number>();
for (const m of classification.matched) allIndices.add(m.index);
for (const a of classification.added) allIndices.add(a.index);
const sortedIndices = [...allIndices].sort((a, b) => a - b);
// Phase 1: Removes — reverse order (children before parents, bottom-up)
for (let i = classification.removed.length - 1; i >= 0; i--) {
const fiber = classification.removed[i]!;
host.removeChild?.(parentInst as never, fiber.instance as never, ctx as never);
}
// Build new children array and determine placement actions
const newChildren: Fiber<I>[] = [];
type Placement = { fiber: Fiber<I>; beforeFiber: Fiber<I> | null; kind: "insert" | "move" };
const placements: Placement[] = [];
// Create fibers for added children first (so they're available for before-lookup)
const addedFibers = new Map<number, Fiber<I>>();
for (const { index } of classification.added) {
const newChild = addedByIndex.get(index)!;
const fiber = createFiber(newChild, parentFiber);
addedFibers.set(index, fiber);
}
// Build the final children array
for (const idx of sortedIndices) {
const addedChild = addedByIndex.get(idx);
if (addedChild !== undefined) {
const fiber = addedFibers.get(idx)!;
newChildren.push(fiber);
// Find before: next fiber in newChildren that is "staying" (not moved, not just inserted)
const before = findNextStayingFiber(sortedIndices, matchedByNewIndex, addedByIndex, movedSet, idx);
placements.push({ fiber, beforeFiber: before, kind: "insert" });
continue;
}
const match = matchedByNewIndex.get(idx);
if (match !== undefined) {
const fiber = match.oldFiber;
newChildren.push(fiber);
const mIdx = matchedArrayIndex.get(match)!;
if (classification.moves.has(mIdx)) {
const before = findNextStayingFiber(sortedIndices, matchedByNewIndex, addedByIndex, movedSet, idx);
placements.push({ fiber, beforeFiber: before, kind: "move" });
}
}
}
// Phase 2: Inserts + Moves — left-to-right
for (const { fiber, beforeFiber } of placements) {
if (host.insertBefore && beforeFiber) {
host.insertBefore(
parentInst as never,
fiber.instance as never,
beforeFiber.instance as never,
ctx as never,
);
} else {
host.appendChild(parentInst as never, fiber.instance as never, ctx as never);
}
}
// Update fiber tree
parentFiber.children = newChildren;
for (const fiber of classification.removed) {
fiber.parent = null;
}
// Phase 3: Updates — top-down (parent before child) via commitEffects
commitEffects(parentFiber, host as HostConfig<string, I, unknown>, ctx);
}
function findNextStayingFiber<I>(
sortedIndices: number[],
matchedByNewIndex: Map<number, MatchedChild<I>>,
addedByIndex: Map<number, UElement>,
movedSet: Set<Fiber<I>>,
currentIdx: number,
): Fiber<I> | null {
const posInSorted = sortedIndices.indexOf(currentIdx);
for (let p = posInSorted + 1; p < sortedIndices.length; p++) {
const nextIdx = sortedIndices[p]!;
const isAdded = addedByIndex.has(nextIdx);
const match = matchedByNewIndex.get(nextIdx);
if (!isAdded && match !== undefined) {
const isMoved = movedSet.has(match.oldFiber);
if (!isMoved) {
return match.oldFiber;
}
}
}
return null;
}

View File

@@ -21,8 +21,8 @@ export type { HostConfig, Root } from "./host/config.js";
export type { Fiber, Effect } from "./host/fiber.js";
export { scheduleUpdate, flushUpdates, reconcileProps, commitEffects, wireSignalToFiber, resetUpdateQueue, reconcileChildren, longestIncreasingSubsequence } from "./host/reconcile.js";
export type { MatchedChild, ChildClassification } from "./host/reconcile.js";
export { scheduleUpdate, flushUpdates, reconcileProps, commitEffects, commitMutations, wireSignalToFiber, resetUpdateQueue, reconcileChildren, longestIncreasingSubsequence } from "./host/reconcile.js";
export type { MatchedChild, ChildClassification, CommitContext } from "./host/reconcile.js";
export { TransformRegistry, childCtx, matchesSchema, ctx as transformCtx } from "./transform/registry.js";
export type { TransformContext, TransformFn, TransformRule } from "./transform/registry.js";