47 lines
4.3 KiB
TypeScript
47 lines
4.3 KiB
TypeScript
/**
|
||
* 页面打开画布的协议分流入口。先读取 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}:{}) };
|
||
}
|