Coordinate scoped model reads with bounded streaming state and identity fences

This commit is contained in:
2026-11-09 01:26:01 +08:00
parent 03c3a0dc66
commit 8219209284
13 changed files with 396 additions and 16 deletions
+7 -6
View File
@@ -1,12 +1,11 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { api, errorText } from "@/lib/api";
import { readDemoBoards } from "@/lib/demo-workspace";
import { matchesRef, type Board } from "@/lib/workspace";
import { loadCanvasEntry } from "@/lib/canvas-entry";
import { createFoundryTransport } from "@/lib/canvas-service/foundry";
import type { ProjectEngine } from "@/lib/canvas-runtime/workspace";
import { useCanvasRuntimeHost, useCanvasRenderers } from "./canvas-runtime-host";
import type { CanvasSnapshot } from "@/lib/canvas-sync";
@@ -27,6 +26,7 @@ export default function GraphPage({ workspaceId, boardId }: { workspaceId: strin
const [loaded, setLoaded] = useState<Loaded | null>(null);
const [error, setError] = useState("");
const [retry, setRetry] = useState(0);
const navigationGeneration=useRef(0);
useEffect(() => {
setLoaded(null); setError("");
if (demo) {
@@ -37,13 +37,14 @@ export default function GraphPage({ workspaceId, boardId }: { workspaceId: strin
}
let active = true;
const controller = new AbortController();
loadCanvasEntry({ request: api, workspaceRef: workspaceId, boardRef: boardId, registry: runtimeHost.registry, signal: controller.signal,
transport(accountId) { return createFoundryTransport({ accountId, isCurrentIdentity: runtimeHost.bindAccount(accountId) }); },
const startingIdentity=runtimeHost.identity();let currentIdentity:(()=>boolean)|undefined;
loadCanvasEntry({ request: api, workspaceRef: workspaceId, boardRef: boardId, registry: runtimeHost.registry, signal: controller.signal,navigationGeneration:++navigationGeneration.current,
readModel:input=>{const identity=runtimeHost.identity();if(identity.authEpoch!==startingIdentity.authEpoch&&identity.accountId!==input.accountId)throw new Error('identity_changed');currentIdentity=runtimeHost.bindAccount(input.accountId);return runtimeHost.loadModel(input.accountId,input.workspaceId,input.canvasId,{signal:input.signal,navigationGeneration:input.navigationGeneration});},
}).then(async result => {
if (!active) return;
if (!active||currentIdentity&&!currentIdentity()) return;
if (result.mode === 'legacy') { setLoaded({ board: result.board, snapshot: result.snapshot, route }); return; }
const engine = await runtimeHost.workspace(result.profile.id, result.board.workspace_id).open(result.model.document, result.model.versions);
if (active) setLoaded({ board: result.board, engine, route });
if (active&&(!currentIdentity||currentIdentity())) setLoaded({ board: result.board, engine, route });
})
.catch((error) => { if (active) { setError(errorText(error)); if (error.message === "invalid_session") router.replace("/login"); } });
return () => { active = false; controller.abort(); };