From 9af99d5d06e704972aeac740ea8176ab6f4b1150 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Mon, 18 May 2026 17:30:53 +0000 Subject: [PATCH] Integrate finalizeInstance into commit/remove pipeline via disposeFiber --- src/host/reconcile.ts | 4 +- test/commit-mutations.test.ts | 134 ++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/src/host/reconcile.ts b/src/host/reconcile.ts index c2c3ea0..af04583 100644 --- a/src/host/reconcile.ts +++ b/src/host/reconcile.ts @@ -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( 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, ctx); } // Build new children array and determine placement actions diff --git a/test/commit-mutations.test.ts b/test/commit-mutations.test.ts index 1335bfe..43e9ab9 100644 --- a/test/commit-mutations.test.ts +++ b/test/commit-mutations.test.ts @@ -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>; @@ -380,4 +381,137 @@ describe("commitMutations direct", () => { const childIdx = ops.indexOf("commitUpdate(child-inst)"); expect(parentIdx).toBeLessThan(childIdx); }); +}); + +describe("finalizeInstance during commit/remove pipeline", () => { + function makeFiber(key: string | undefined, tag: string, instance?: string): Fiber { + 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 = { + name: "test", + createRootContext: () => ({}), + createInstance: () => "inst", + createTextInstance: () => "text", + appendChild: () => {}, + removeChild: () => {}, + finalizeInstance: (instance) => { + finalized.push(instance); + }, + }; + + const classification: ChildClassification = { + matched: [], + added: [], + removed: [removedFiber], + moves: new Map(), + }; + + const commitCtx: CommitContext = { + host: host as HostConfig, + 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 = { + name: "test", + createRootContext: () => ({}), + createInstance: () => "inst", + createTextInstance: () => "text", + appendChild: () => {}, + removeChild: () => {}, + }; + + const classification: ChildClassification = { + matched: [], + added: [], + removed: [removedFiber], + moves: new Map(), + }; + + const commitCtx: CommitContext = { + host: host as HostConfig, + 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 = { + 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); + }); }); \ No newline at end of file