Implement disposeFiber for fiber tree disposal

Add disposeFiber() function that performs bottom-up teardown of fiber
subtrees: recursively disposes children before parents, calls
host.finalizeInstance for per-instance cleanup, invokes signal disposers,
and clears fiber state. Idempotent via disposed flag. Does NOT call
host.removeChild (that's the commit phase's job).

- Add disposeFiber + HostLike to src/host/fiber.ts
- Add finalizeInstance to HostConfig interface
- Add disposed boolean to Fiber interface
- Export disposeFiber and HostLike from barrel
- Add 7 tests for disposeFiber (3-level tree, idempotency, signal cleanup, etc.)
This commit is contained in:
2026-05-18 17:22:05 +00:00
parent 1e0abb0900
commit 95995f4602
7 changed files with 279 additions and 3 deletions

View File

@@ -30,6 +30,7 @@ export interface HostConfig<TTag extends string, Instance, RootCtx> {
ctx: RootCtx,
): void;
emit?(type: string, id: string, payload: unknown): void;
finalizeInstance?(instance: Instance, ctx: RootCtx): void;
}
export interface Root<TTag extends string, Instance, RootCtx> {
@@ -70,6 +71,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null,
signalDisposers: [],
prevProps: null,
disposed: false,
};
if (parentFiber) parentFiber.children.push(fiber);
return fiber;
@@ -106,6 +108,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null,
signalDisposers: [],
prevProps: null,
disposed: false,
};
for (const child of el.children) {
@@ -132,6 +135,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null,
signalDisposers: [],
prevProps: null,
disposed: false,
};
}
@@ -158,6 +162,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null,
signalDisposers: [],
prevProps: null,
disposed: false,
};
for (const child of el.children) {
@@ -281,6 +286,7 @@ export function createRoot<TTag extends string, Instance, RootCtx>(
effect: null,
signalDisposers: [],
prevProps: null,
disposed: false,
};
const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
for (const child of payloadChildren) {

View File

@@ -8,10 +8,34 @@ export interface Fiber<I> {
effect: Effect<I> | null;
signalDisposers: (() => void)[];
prevProps: Record<string, unknown> | null;
disposed: boolean;
}
export type Effect<I> =
| { type: "update"; payload: unknown }
| { type: "insert"; before: Fiber<I> | null }
| { type: "move"; before: Fiber<I> | null }
| { type: "remove" };
| { type: "remove" };
export interface HostLike<I, Ctx> {
finalizeInstance?(instance: I, ctx: Ctx): void;
}
export function disposeFiber<I, Ctx>(fiber: Fiber<I>, host: HostLike<I, Ctx>, ctx: Ctx): void {
if (fiber.disposed) return;
for (const child of fiber.children) {
disposeFiber(child, host, ctx);
}
host.finalizeInstance?.(fiber.instance, ctx);
for (const disposer of fiber.signalDisposers) {
disposer();
}
fiber.signalDisposers = [];
fiber.parent = null;
fiber.effect = null;
fiber.disposed = true;
}

View File

@@ -19,7 +19,8 @@ export { ValuePointer, selectNode, setNode } from "./core/pointer.js";
export { createRoot as createHostRoot } from "./host/config.js";
export type { HostConfig, Root } from "./host/config.js";
export type { Fiber, Effect } from "./host/fiber.js";
export { disposeFiber } from "./host/fiber.js";
export type { Fiber, Effect, HostLike } from "./host/fiber.js";
export { scheduleUpdate, flushUpdates, reconcileProps, commitEffects, commitMutations, wireSignalToFiber, resetUpdateQueue, reconcileChildren, longestIncreasingSubsequence } from "./host/reconcile.js";
export type { MatchedChild, ChildClassification, CommitContext } from "./host/reconcile.js";