Files
Endoleap/src/lib/canvas-entry.ts
T

47 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 页面打开画布的协议分流入口。先读取 canonical 模型,再决定使用新版 engine 或旧版 snapshot 编辑器,
* 防止已迁移项目误走旧快照写入;迁移提示只说明当前可迁移,不能直接作为冻结迁移请求使用。
*/
import type {LiveModelPreview} from './canvas-runtime/model-preview.ts';
import type { Profile } from './api.ts';
import type { CanvasRequest } from './canvas-sync.ts';
import { decodeSnapshot } from './canvas-sync.ts';
import { CANVAS_ACCEPT } from './canvas-v2.ts';
import { matchesRef, type Board } from './workspace.ts';
import type { Registry } from './canvas-core/types.ts';
import { loadFoundryModel, type ModelLoad, type ServiceTransport } from './canvas-service/foundry.ts';
/** Read canonical first: a migrated project must never be opened through snapshot writers. */
/** 并行解析画布引用与账户,再按实际工作区/画布ID读取模型。可注入共享readModel以取得渐进预览,
* 或注入基础transport做完整读取;ready 返回headless,其它允许回退的情形才读取旧snapshot。 */
export async function loadCanvasEntry(options: { request: CanvasRequest; workspaceRef: string; boardRef: string; registry: Registry; transport?(accountId: string): ServiceTransport; readModel?(input:{accountId:string;workspaceId:string;canvasId:string;signal?:AbortSignal;navigationGeneration:number;onPreview?:(preview:LiveModelPreview)=>void}):Promise<ModelLoad>;onPreview?:(preview:LiveModelPreview,board:Board,profile:Profile)=>void;signal?: AbortSignal;navigationGeneration?:number }) {
const path = `workspaces/${encodeURIComponent(options.workspaceRef)}/materials/${encodeURIComponent(options.boardRef)}`;
// slug/路由ref先解析成真实ID,画布kind、引用和工作区/账户信息必须匹配再选择服务作用域。
// 两个读取共享取消信号,但任一拒绝不会自动取消另一个;这里不分配页面状态或编辑租约。
const [board, profile] = await Promise.all([options.request<Board>(path, { signal: options.signal }), options.request<Profile>('profile', { signal: options.signal })]);
options.signal?.throwIfAborted();
if (board.kind !== 'board' || !matchesRef(board, options.boardRef) || !board.workspace_id || !profile.id) throw new Error('invalid_reference');
if(!options.readModel&&!options.transport)throw new Error('model_loader_unavailable');
let migration:Extract<ModelLoad,{status:'migration-required'}>|undefined;
try {
// 预览回调携带同次读取的board/profile,navigationGeneration由共享loader隔离导航结果。
// 注入的 readModel 优先于 transport;入口本身不去重请求,也不保存或撤销已发出的预览。
const model = options.readModel?await options.readModel({accountId:profile.id,workspaceId:board.workspace_id,canvasId:board.id,signal:options.signal,navigationGeneration:options.navigationGeneration??0,...(options.onPreview?{onPreview:(preview:LiveModelPreview)=>options.onPreview!(preview,board,profile)}:{})}):await loadFoundryModel(options.transport!(profile.id), board.workspace_id, board.id, options.registry, options.signal);
options.signal?.throwIfAborted();
// 新模型一旦ready立即走新版,不能因旧数据端点仍存在就继续让旧writer写它。
if (model.status === 'ready') return { mode: 'headless' as const, board, profile, model };
// Existing projects keep their current editor until the user explicitly migrates them.
// This read describes eligibility, not a frozen write request: the migration
// action must drain legacy writers and refresh source versions before sending.
migration=model;
} catch (error) {
options.signal?.throwIfAborted();
// 仅明确not_found允许旧端点回退,身份/权限/协议/网络错误继续抛出,避免掩盖新模型故障。
if ((error as { code?: string }).code !== 'not_found') throw error;
}
// 旧请求显式协商v2快照;调用页面仍负责对当前导航/身份校验,不在这里安装页面状态。
const snapshot = decodeSnapshot(await options.request(path + '/canvas', { signal: options.signal, headers: { Accept: CANVAS_ACCEPT } }));
// 此回退路径在 await 后没有再次 throwIfAborted;宿主仍须核对导航/身份后才能采用结果。
return { mode: 'legacy' as const, board, profile, snapshot,...(migration?{migration}:{}) };
}