Merge branch 'feat/host-finalize-instance'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>>;
|
||||
@@ -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<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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user