"use client"; import * as React from "react"; import type { ToolcraftRendererPipelinePassCacheInput, ToolcraftRendererPipelinePassHandle, ToolcraftRendererPipelinePassResult, } from "../../rendering"; import { useToolcraftPipeline } from "./use-toolcraft-pipeline"; type AnyPassHandle = ToolcraftRendererPipelinePassHandle; export type ToolcraftPipelinePassState = | Readonly<{ status: "pending"; }> | Readonly<{ result: Result; status: "success"; }> | Readonly<{ error: unknown; status: "error"; }>; type PipelinePassRequest = Readonly<{ cacheInput: ToolcraftRendererPipelinePassCacheInput; pass: Handle; }>; type ResolvedPipelinePass = Readonly<{ request: PipelinePassRequest; state: ToolcraftPipelinePassState< ToolcraftRendererPipelinePassResult >; }>; function isPromiseLike(value: unknown): value is PromiseLike { return ( (typeof value === "object" && value !== null) || typeof value === "function" ) && "then" in value; } function sameValueZero(left: unknown, right: unknown): boolean { return left === right || (left !== left && right !== right); } function hasEqualCacheInput(left: unknown, right: unknown): boolean { if (sameValueZero(left, right)) { return true; } if ( typeof left !== "object" || left === null || typeof right !== "object" || right === null ) { return false; } const leftKeys = Reflect.ownKeys(left); const rightKeys = Reflect.ownKeys(right); return ( leftKeys.length === rightKeys.length && leftKeys.every( (key) => Object.hasOwn(right, key) && sameValueZero( (left as Record)[key], (right as Record)[key], ), ) ); } function useStableCacheInput(cacheInput: Input): Input { const stableInput = React.useRef(cacheInput); if (!hasEqualCacheInput(stableInput.current, cacheInput)) { stableInput.current = cacheInput; } return stableInput.current; } export function useToolcraftPipelinePass( pass: Handle, cacheInput: NoInfer>, compute: () => | PromiseLike>> | NoInfer>, ): ToolcraftPipelinePassState> { const pipeline = useToolcraftPipeline(); const stableCacheInput = useStableCacheInput(cacheInput); const computeRef = React.useRef(compute); React.useLayoutEffect(() => { computeRef.current = compute; }, [compute]); const request = React.useMemo>( () => ({ cacheInput: stableCacheInput, pass }), [pass, stableCacheInput], ); const [resolved, setResolved] = React.useState< ResolvedPipelinePass | undefined >(); React.useLayoutEffect(() => { let active = true; let hasSynchronousResult = false; let synchronousResult: ToolcraftRendererPipelinePassResult; const execute = () => { const result = computeRef.current(); if (!isPromiseLike(result)) { hasSynchronousResult = true; synchronousResult = result; } return result; }; let result: Promise>; try { result = pipeline ? pipeline.runPass(request.pass, request.cacheInput, execute) : Promise.resolve(execute()); } catch (error) { setResolved({ request, state: { error, status: "error" }, }); return () => { active = false; }; } if (hasSynchronousResult) { setResolved({ request, state: { result: synchronousResult!, status: "success", }, }); } void result.then( (value) => { if (active && !hasSynchronousResult) { setResolved({ request, state: { result: value, status: "success", }, }); } }, (error: unknown) => { if (active) { setResolved({ request, state: { error, status: "error" }, }); } }, ); return () => { active = false; }; }, [pipeline, request]); if (resolved?.request === request) { return resolved.state; } return { status: "pending" }; }