Wire signal-driven updates: scheduleUpdate, flushUpdates, reconcileProps, commitEffects, wireSignalToFiber, re-renderable render()

This commit is contained in:
2026-05-18 16:50:08 +00:00
parent 87ec672641
commit 24d0134ae4
4 changed files with 472 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ 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";
export interface HostConfig<TTag extends string, Instance, RootCtx> {
name: string;
@@ -122,6 +123,19 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
context: rootContext,
rootFiber: null,
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);
}
}
commitEffects(this.rootFiber, host as HostConfig<string, Instance, unknown>, ctx as unknown);
host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length });
return;
}
const root: Fiber<Instance> = {
instance: undefined as unknown as Instance,
tag: "#root",

157
src/host/reconcile.ts Normal file
View File

@@ -0,0 +1,157 @@
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 } from "../core/schema.js";
interface PendingUpdate {
fiber: Fiber<unknown>;
nextNode: UNode;
}
let pendingUpdates: PendingUpdate[] = [];
let flushScheduled = false;
export function scheduleUpdate<I>(
fiber: Fiber<I>,
nextNode: UNode,
host: HostConfig<string, I, unknown>,
ctx: unknown,
): void {
pendingUpdates.push({ fiber: fiber as Fiber<unknown>, nextNode });
if (!flushScheduled) {
flushScheduled = true;
queueMicrotask(() => flushUpdates(host as HostConfig<string, unknown, unknown>, ctx));
}
}
export function flushUpdates(
host: HostConfig<string, unknown, unknown>,
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<Fiber<unknown>>();
for (const { fiber } of updates) {
let root: Fiber<unknown> | null = fiber;
while (root.parent) root = root.parent;
if (!seen.has(root)) {
seen.add(root);
commitEffects(root, host, ctx);
}
}
}
export function reconcileProps<I>(
fiber: Fiber<I>,
nextNode: UNode,
host: HostConfig<string, I, unknown>,
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<string, unknown>) => 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<string, unknown>;
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<I>(
fiber: Fiber<I>,
host: HostConfig<string, I, unknown>,
ctx: unknown,
): void {
if (fiber.effect?.type === "update") {
const updateEffect = fiber.effect as Effect<I> & { 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<I>(
fiber: Fiber<I>,
signalGetter: () => UNode,
host: HostConfig<string, I, unknown>,
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;
}

View File

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