Merge branch 'feat/host-finalize-instance'

This commit is contained in:
2026-05-18 17:31:05 +00:00
2 changed files with 137 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
import { effect } from "@preact/signals-core";
import { Value } from "@alkdev/typebox/value";
import type { Fiber, Effect } from "./fiber.js";
import type { Fiber, Effect, HostLike } from "./fiber.js";
import { disposeFiber } 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";
@@ -373,6 +374,7 @@ export function commitMutations<I>(
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);
disposeFiber(fiber, host as HostLike<I, unknown>, ctx);
}
// Build new children array and determine placement actions

View File

@@ -5,6 +5,7 @@ import type { HostConfig } from "../src/host/config.js";
import { commitMutations, reconcileChildren } from "../src/host/reconcile.js";
import type { CommitContext, ChildClassification } from "../src/host/reconcile.js";
import type { Fiber } from "../src/host/fiber.js";
import { disposeFiber } from "../src/host/fiber.js";
function makeHost(): {
host: HostConfig<string, string, Record<string, unknown>>;
@@ -381,3 +382,136 @@ describe("commitMutations direct", () => {
expect(parentIdx).toBeLessThan(childIdx);
});
});
describe("finalizeInstance during commit/remove pipeline", () => {
function makeFiber(key: string | undefined, tag: string, instance?: string): Fiber<string> {
return {
instance: instance ?? `inst-${tag}-${key ?? "nokey"}`,
tag,
props: {},
key,
children: [],
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
cachedNode: null,
disposed: false,
};
}
it("host implementing finalizeInstance receives calls during disposal via commitMutations", () => {
const finalized: string[] = [];
const parentFiber = makeFiber(undefined, "div", "parent-inst");
const removedFiber = makeFiber("b", "span", "b-inst");
removedFiber.parent = parentFiber;
const host: HostConfig<string, string, unknown> = {
name: "test",
createRootContext: () => ({}),
createInstance: () => "inst",
createTextInstance: () => "text",
appendChild: () => {},
removeChild: () => {},
finalizeInstance: (instance) => {
finalized.push(instance);
},
};
const classification: ChildClassification<string> = {
matched: [],
added: [],
removed: [removedFiber],
moves: new Map(),
};
const commitCtx: CommitContext<string> = {
host: host as HostConfig<string, string, unknown>,
ctx: {},
createFiber: () => makeFiber("x", "x", "x"),
};
commitMutations(parentFiber, classification, commitCtx);
expect(finalized).toContain("b-inst");
expect(removedFiber.disposed).toBe(true);
});
it("finalizeInstance called bottom-up: children before parents", () => {
const finalized: string[] = [];
const parentFiber = makeFiber(undefined, "div", "parent-inst");
const childFiber = makeFiber(undefined, "span", "child-inst");
childFiber.parent = parentFiber;
parentFiber.children = [childFiber];
disposeFiber(parentFiber, {
finalizeInstance: (instance) => {
finalized.push(instance);
},
}, {});
expect(finalized).toEqual(["child-inst", "parent-inst"]);
});
it("finalizeInstance is optional — hosts without it still work", () => {
const parentFiber = makeFiber(undefined, "div", "parent-inst");
const removedFiber = makeFiber("b", "span", "b-inst");
removedFiber.parent = parentFiber;
const host: HostConfig<string, string, unknown> = {
name: "test",
createRootContext: () => ({}),
createInstance: () => "inst",
createTextInstance: () => "text",
appendChild: () => {},
removeChild: () => {},
};
const classification: ChildClassification<string> = {
matched: [],
added: [],
removed: [removedFiber],
moves: new Map(),
};
const commitCtx: CommitContext<string> = {
host: host as HostConfig<string, string, unknown>,
ctx: {},
createFiber: () => makeFiber("x", "x", "x"),
};
expect(() => commitMutations(parentFiber, classification, commitCtx)).not.toThrow();
expect(removedFiber.disposed).toBe(true);
});
it("disposeFiber calls finalizeInstance and clears signal disposer", () => {
const disposed: string[] = [];
const fiber: Fiber<string> = {
instance: "inst-1",
tag: "div",
props: {},
key: undefined,
children: [],
parent: null,
effect: null,
signalDisposers: [() => disposed.push("signal-a"), () => disposed.push("signal-b")],
prevProps: null,
cachedNode: null,
disposed: false,
};
const finalized: string[] = [];
disposeFiber(fiber, {
finalizeInstance: (instance) => {
finalized.push(instance);
},
}, {});
expect(finalized).toEqual(["inst-1"]);
expect(disposed).toEqual(["signal-a", "signal-b"]);
expect(fiber.disposed).toBe(true);
});
});