import { commitResourceFiber, createResourceFiber, renderResourceFiber, unmountResourceFiber, } from "../core/ResourceFiber"; import { UpdateScheduler } from "../core/scheduler"; import { isDevelopment } from "../core/helpers/env"; import { commitRoot, createResourceFiberRoot, setRootVersion, } from "../core/helpers/root"; import { cloneCurrentTapContext, withTapContextRoot } from "../core/context"; import { useEffect, useEffectEvent, useMemo, useRef } from "react"; import { useDevStrictMode } from "./utils/useDevStrictMode"; export namespace useTapRoot { export type Unsubscribe = () => void; export interface Root { /** * Get the current value of the root. */ getValue(): R; /** * Subscribe to the root. */ subscribe(listener: () => void): Unsubscribe; } } const useHostRoot = (render: () => R): R => render(); export const useTapRoot = (render: () => R): useTapRoot.Root => { const scheduler = useMemo( () => new UpdateScheduler(() => handleUpdate()), [], ); const queue = useMemo(() => [] as (() => boolean)[], []); const getDevStrictMode = useDevStrictMode(); const fiber = useMemo(() => { const root = createResourceFiberRoot((evaluate, apply) => { if (!scheduler.isDirty) { if (!evaluate()) return; apply(); } setRootVersion(root, root.committedVersion + root.changelog.length); queue.push(apply); scheduler.markDirty(); }); return createResourceFiber( useHostRoot, root, undefined, getDevStrictMode(), ); }, [queue, scheduler, getDevStrictMode]); const context = cloneCurrentTapContext(); const drainedCount = fiber.root.version - fiber.root.committedVersion; const render2 = withTapContextRoot(context, () => { return renderResourceFiber(fiber, [render]); }); const isMountedRef = useRef(false); const committedArgsRef = useRef([render] as const); const valueRef = useRef(render2); const subscribers = useMemo(() => new Set<() => void>(), []); const publish = (output: R) => { if (scheduler.isDirty || valueRef.current === output) return; valueRef.current = output; subscribers.forEach((listener) => listener()); }; const handleUpdate = useEffectEvent(() => { setRootVersion(fiber.root, fiber.root.committedVersion); queue.forEach((callback) => { if (isDevelopment && fiber.devStrictMode) { callback(); } callback(); }); setRootVersion( fiber.root, fiber.root.committedVersion + fiber.root.changelog.length, ); if (isDevelopment && fiber.devStrictMode) { void withTapContextRoot(fiber.root.context, () => { return renderResourceFiber(fiber, committedArgsRef.current); }); } const render = withTapContextRoot(fiber.root.context, () => { return renderResourceFiber(fiber, committedArgsRef.current); }); if (scheduler.isDirty) throw new Error("Scheduler is dirty, this should never happen"); commitRoot(fiber.root); queue.length = 0; if (isMountedRef.current) { commitResourceFiber(fiber); } publish(render); }); useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; unmountResourceFiber(fiber); }; }, [fiber]); useEffect(() => { committedArgsRef.current = [render]; commitRoot(fiber.root); queue.splice(0, drainedCount); fiber.root.context = context; commitResourceFiber(fiber); publish(render2); }); return useMemo( () => ({ getValue: () => valueRef.current, subscribe: (listener: () => void) => { subscribers.add(listener); return () => subscribers.delete(listener); }, }), [subscribers], ); };