/** * 应用级Canvas运行时装配点。一次安装模型registry与React renderer目录,按账户/工作区创建runtime, * 按项目root安装Foundry服务适配。组件只装配依赖,业务根、history、ledger和持久化仍由下层实例管理。 * 真实宿主卸载才关闭项目;单个画布页面切换只借还视图,不在这里每次重建engine。 */ "use client"; import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from "react"; import { defaultCanvasRenderers } from "@/components/canvas/canvas-renderers"; import { createReactRenderers, type CanvasRuntimeExtension } from "@/lib/canvas-bindings/react-flow"; import { notify } from "@/lib/notifications"; import { errorText } from "@/lib/api"; import { createRegistry, builtinComponents } from "@/lib/canvas-core/index"; import { createRuntimeHost, createWorkspaceRuntime, type RuntimeHost } from "@/lib/canvas-runtime/index"; import { createBrowserDigests, createBrowserImageDecoder, createBrowserStorage } from "@/lib/canvas-runtime/browser"; import { createFoundryAssets, createFoundryPersistence, createFoundryTransport, createFoundryRuns,createFoundryModelLoader } from "@/lib/canvas-service/index"; /** 应用共享RuntimeHost引用,未装配时为null;renderer Context另有内置显示目录默认值。 */ const Context = createContext(null); /** 模型定义与显示实现一起在宿主初始化捕获,子页面用此目录保持一致解释。 */ const Renderers=createContext(defaultCanvasRenderers); /** 读取宿主安装的React renderer集合,不为每个节点重新构造类型表。 */ export function useCanvasRenderers(){return useContext(Renderers);} /** extensions仅在useState惰性初始化时捕获;后续prop换引用不会热替换已打开项目的schema/renderer。 */ export function CanvasRuntimeHost({ children,extensions=[] }: { children: ReactNode;extensions?:readonly CanvasRuntimeExtension[] }) { // 惰性初始化只装配对象/工厂,不在 render 直接打开项目;实际账户绑定和持久化租约由后续调用取得。 const [{host,renderers}] = useState(() => { // 先组合内置与扩展模型,再按精确版本/结构验证renderer,冲突在打开项目之前失败。 const registry = createRegistry([...builtinComponents(),...extensions.flatMap(extension=>extension.components??[])]),renderers=createReactRenderers([...defaultCanvasRenderers.definitions,...extensions.flatMap(extension=>extension.renderers??[])],registry); const host=createRuntimeHost({ registry, /** 每身份作用域绑定transport,current护栏由host提供,流读取也要防换账户后的迟到结果。 */ modelLoader(accountId,workspaceId,_authEpoch,current){return createFoundryModelLoader({workspaceId,registry,transport:createFoundryTransport({accountId,isCurrentIdentity:current})});}, /** 工作区持有浏览器存储、摘要/解码池;root项目共享其身份范围,各自再装配业务端口。 */ workspace(accountId, workspaceId, authEpoch, isCurrentIdentity) { return createWorkspaceRuntime({ accountId, workspaceId, authEpoch, isCurrentIdentity, registry, storage: createBrowserStorage(),digests:createBrowserDigests(),images:createBrowserImageDecoder(), /** 一项目一组协议适配器;summary策略只压缩已协调回执,不改变网络请求或原始操作身份。 */ adapters(rootId, current) { const transport = createFoundryTransport({ accountId, isCurrentIdentity: current }); return { runs: createFoundryRuns({ workspaceId, rootId, registry, transport }), persistence: createFoundryPersistence({ workspaceId, rootId, registry, transport, resolvedReceiptPolicy:'summary' }), assets: createFoundryAssets({ workspaceId, transport, project: process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap", origin: process.env.NEXT_PUBLIC_FOUNDRY_API_ORIGIN }) }; }, // 同工作区错误用稳定通知id更新,不创建另一份任务状态;错误恢复仍须runtime明确操作。 onError: error => notify({ id: `canvas-runtime:${accountId}:${workspaceId}`, title: "画布同步需要处理", description: errorText(error), level: "error" }), }); }, onError: error => notify({ id: "canvas-host-close", title: "本地修改尚未保存成功", description: errorText(error), level: "error" }), }); return {host,renderers}; }); // effect重放的代次令牌只保护宿主清理,不是账户authEpoch,也不进入持久模型。 const lifetime = useRef(0); /** cleanup延到微任务检查代次:Strict Mode同步重设effect会递增代次,旧cleanup不关闭新挂载宿主。 * 真正卸载才发起host.close,保存失败由宿主错误回调/日志报告,不在此丢弃根或强行释放leader。 */ useEffect(() => { const generation = ++lifetime.current; // cleanup 不等待 close 完成,也不主动重试;异步失败经 host 的错误通道和此处 catch 报告。 // Strict Mode replays effects synchronously; only a real detach retires the host. return () => { queueMicrotask(() => { if (lifetime.current === generation) void host.close().catch(error => console.error("[canvas-host]", error.message)); }); }; }, [host]); return {children}; } /** 需要runtime的页面必须在根Provider内,缺失装配明确抛错而不偷建一个临时host。 */ export function useCanvasRuntimeHost() { const host = useContext(Context); if (!host) throw new Error("CanvasRuntimeHost is missing"); return host; }