import { effect } from "@preact/signals-core"; import type { Fiber, Effect } from "./fiber.js"; import type { HostConfig } from "./config.js"; import type { UNode, UElement } from "../core/schema.js"; import { isUPrimitive, isURoot, isUElement } from "../core/schema.js"; interface PendingUpdate { fiber: Fiber; nextNode: UNode; } let pendingUpdates: PendingUpdate[] = []; let flushScheduled = false; export function scheduleUpdate( fiber: Fiber, nextNode: UNode, host: HostConfig, ctx: unknown, ): void { pendingUpdates.push({ fiber: fiber as Fiber, nextNode }); if (!flushScheduled) { flushScheduled = true; queueMicrotask(() => flushUpdates(host as HostConfig, ctx)); } } export function flushUpdates( host: HostConfig, ctx: unknown, ): void { flushScheduled = false; const updates = pendingUpdates.splice(0); for (const { fiber, nextNode } of updates) { reconcileProps(fiber, nextNode, host, ctx); } const seen = new Set>(); for (const { fiber } of updates) { let root: Fiber | null = fiber; while (root.parent) root = root.parent; if (!seen.has(root)) { seen.add(root); commitEffects(root, host, ctx); } } } export function reconcileProps( fiber: Fiber, nextNode: UNode, host: HostConfig, ctx: unknown, ): void { if (isUPrimitive(nextNode)) { if (fiber.tag === "#text") { const text = nextNode === null ? "" : String(nextNode); if (fiber.props.text !== text) { const nextProps = { text }; const payload = host.prepareUpdate?.( fiber.instance, fiber.tag as never, fiber.props, nextProps, ctx as never, ); if (payload !== null && payload !== undefined) { fiber.effect = { type: "update", payload }; fiber.prevProps = { ...fiber.props }; fiber.props = nextProps; } } } return; } if (isURoot(nextNode)) { const rootChildren = nextNode.children; const count = Math.min(fiber.children.length, rootChildren.length); for (let i = 0; i < count; i++) { reconcileProps(fiber.children[i]!, rootChildren[i]!, host, ctx); } return; } const el = nextNode as UElement; if (typeof el.type === "function") { const component = el.type as (props: Record) => UNode; const out = component({ ...el.props, children: el.children }); reconcileProps(fiber, out, host, ctx); return; } if (fiber.tag !== "#text" && fiber.tag !== el.type) return; const nextProps = el.props as Record; const payload = host.prepareUpdate?.( fiber.instance, fiber.tag as never, fiber.props, nextProps, ctx as never, ); if (payload !== null && payload !== undefined) { fiber.effect = { type: "update", payload }; fiber.prevProps = { ...fiber.props }; fiber.props = nextProps; } const count = Math.min(fiber.children.length, el.children.length); for (let i = 0; i < count; i++) { reconcileProps(fiber.children[i]!, el.children[i]!, host, ctx); } } export function commitEffects( fiber: Fiber, host: HostConfig, ctx: unknown, ): void { if (fiber.effect?.type === "update") { const updateEffect = fiber.effect as Effect & { type: "update"; payload: unknown }; host.commitUpdate?.( fiber.instance, updateEffect.payload, fiber.tag as never, fiber.prevProps ?? {}, fiber.props, ctx as never, ); } for (const child of fiber.children) { commitEffects(child, host, ctx); } fiber.effect = null; } export function wireSignalToFiber( fiber: Fiber, signalGetter: () => UNode, host: HostConfig, ctx: unknown, ): void { const disposer = effect(() => { const nextNode = signalGetter(); scheduleUpdate(fiber, nextNode, host, ctx); }); fiber.signalDisposers.push(disposer); } export function resetUpdateQueue(): void { pendingUpdates = []; flushScheduled = false; } export interface MatchedChild { oldFiber: Fiber; newChild: UElement; index: number; } export interface ChildClassification { matched: MatchedChild[]; added: { newChild: UElement; index: number }[]; removed: Fiber[]; } export function reconcileChildren( oldFibers: Fiber[], newChildren: UNode[], ): ChildClassification { const matched: MatchedChild[] = []; const added: { newChild: UElement; index: number }[] = []; const removed: Fiber[] = []; const oldKeyMap = new Map>(); const displacedOldFibers: Fiber[] = []; for (const fiber of oldFibers) { if (fiber.key !== undefined) { if (oldKeyMap.has(fiber.key)) { console.warn(`Duplicate key "${fiber.key}" among old children; last-wins`); displacedOldFibers.push(oldKeyMap.get(fiber.key)!); } oldKeyMap.set(fiber.key, fiber); } } const matchedOldKeys = new Set(); const unkeyedOldUsed = new Set(); let unkeyedOldCursor = 0; const seenNewKeys = new Map(); for (let i = 0; i < newChildren.length; i++) { const child = newChildren[i]!; if (!isUElement(child)) continue; if (child.key !== undefined) { const prevIdx = seenNewKeys.get(child.key); if (prevIdx !== undefined) { console.warn(`Duplicate key "${child.key}" among new children; last-wins`); const prevMatch = matched.findIndex((m) => m.newChild.key === child.key && m.index === prevIdx); if (prevMatch !== -1) { const oldFiber = matched[prevMatch]!.oldFiber; removed.push(oldFiber); matched.splice(prevMatch, 1); } } seenNewKeys.set(child.key, i); const oldFiber = oldKeyMap.get(child.key); if (oldFiber !== undefined) { matchedOldKeys.add(child.key); if (oldFiber.tag === child.type) { matched.push({ oldFiber, newChild: child, index: i }); } else { removed.push(oldFiber); added.push({ newChild: child, index: i }); } } else { added.push({ newChild: child, index: i }); } } else { let matchedOld: Fiber | null = null; let matchedOldIdx = -1; for (let j = unkeyedOldCursor; j < oldFibers.length; j++) { if (!unkeyedOldUsed.has(j) && oldFibers[j]!.key === undefined) { matchedOld = oldFibers[j]!; matchedOldIdx = j; break; } } if (matchedOld !== null) { unkeyedOldUsed.add(matchedOldIdx); unkeyedOldCursor = matchedOldIdx + 1; if (matchedOld.tag === child.type) { matched.push({ oldFiber: matchedOld, newChild: child, index: i }); } else { removed.push(matchedOld); added.push({ newChild: child, index: i }); } } else { added.push({ newChild: child, index: i }); } } } for (let i = 0; i < oldFibers.length; i++) { const fiber = oldFibers[i]!; if (fiber.key !== undefined) { if (!matchedOldKeys.has(fiber.key)) { removed.push(fiber); } } else { if (!unkeyedOldUsed.has(i)) { removed.push(fiber); } } } for (const displaced of displacedOldFibers) { if (matchedOldKeys.has(displaced.key!)) { removed.push(displaced); } } return { matched, added, removed }; }