docs(runtime): explain host and workspace ownership lifecycles

This commit is contained in:
2026-11-12 13:27:00 +08:00
parent 896e6a0656
commit 3e4833470a
2 changed files with 36 additions and 12 deletions
+16 -6
View File
@@ -11,7 +11,10 @@ import { createScopedLoader, type LoadPort, type ScopedLoader } from './loader.t
import type { CanonicalReadUpdate } from './contracts.ts';
/** Host lifetime matches the signed-in application, independently of navigation. */
/** 宿主注入 workspace/loader 工厂;current 闭包绑定精确账号与 epoch,作为异步结果准入围栏。 */
/** 一个应用级宿主:workspaces/readers 按当前账号下的 workspaceId 复用,retired 单独保留待关闭实例。
* registry 由注入方按不可变定义合同共享,本函数不冻结或复制它;也不保存 token/密码等认证凭证。
* current 闭包绑定账号+authEpoch,拒绝旧身份结果;业务/存储实际副作用仍需 adapters 自行遵守该围栏。
* 后台退休关闭可与新身份运行并行;Host 不为 closeRetired 建单飞锁,workspace.close 应支持重复/并发调用。 */
export function createRuntimeHost(options: { registry: Registry; workspace(accountId: Id, workspaceId: Id, authEpoch: number, current: () => boolean): WorkspaceRuntime; modelLoader?(accountId:Id,workspaceId:Id,authEpoch:number,current:()=>boolean):LoadPort<CanonicalReadUpdate>; onError?: (error: unknown) => void; onObserverError?: (error: unknown) => void }) {
let accountId: Id | null = null, authEpoch = 0, disposed = false;
// active 表只含当前身份,retired 持有尚未成功关闭的旧 runtime,不能因换账号就丢掉待保存内容。
@@ -25,7 +28,8 @@ export function createRuntimeHost(options: { registry: Registry; workspace(accou
};
/** 同步撤销当前身份:先换 epoch,再关闭读取句柄/移走 workspace;实际存储收尾另行等待。 */
function retire() { authEpoch++; accountId = null; for (const reader of readers.values()) reader.close();readers.clear();for (const runtime of workspaces.values()) retired.add(runtime); workspaces.clear(); }
/** 账号变化才退休旧作用域;后台关闭错误走 onError,报告失败不能改变新账号的身份事实。 */
/** 相同账号复用 epoch,换账号先 retire 再同步启用新账号,不等待旧 workspace 关闭。
* 旧关闭失败通过 onError 报告且保留 retired,后续身份退休/close 可重试;异常报告也被隔离。 */
function activate(account: Id) {
if (disposed) throw fault('host_disposed');
if (accountId !== account) {
@@ -42,7 +46,10 @@ export function createRuntimeHost(options: { registry: Registry; workspace(accou
identity: () => Object.freeze({ accountId, authEpoch }),
/** 返回当前 workspace 表的安全只读快照,外部不能手动增删运行实例。 */
getWorkspaces: () => readMap(workspaces),
/** 通过共享 scoped loader 读 canonical 源;增量可产生只读预览,terminal 完整后才返回最终模型。 */
/** 只读加载不创建 workspace engine/storage;每 workspace 复用 scoped loader、每订阅维护独立 preview/seen。
* 导航代次属于订阅者,授权 epoch 才参与共享身份;已完成读取不作长期缓存(maxCacheEntries=0)。
* ready 后核对身份与终结完整标志,再返回 ready 模型或其它终态;不是在 Host 再做全量 Document/schema 校验。
* preview/observer 出错只停止此订阅预览并上报 onObserverError,不取消其它订阅或替换共享读取事实。 */
async loadModel(account:Id,workspace:Id,canvas:Id,settings:{signal?:AbortSignal;navigationGeneration?:number;onPreview?:(preview:LiveModelPreview)=>void}={}) {
activate(account);
const readEpoch=authEpoch;
@@ -60,7 +67,8 @@ export function createRuntimeHost(options: { registry: Registry; workspace(accou
};
// key 纳入账号、workspace、格式、查询与 authEpoch;view 导航代次不替代授权代次。
const handle=reader.acquire({accountId:account,workspaceId:workspace,projectId:null,canvasId:canvas,viewKey:'canonical-project',format:'canonical-model-v1',queryFingerprint:'full-project',source:'foundry',authEpoch,navigationGeneration:settings.navigationGeneration??0},{signal:settings.signal,onUpdate});
// ready 后再查身份和 terminal/complete;finally 始终释放本订阅,不能泄漏 reader 引用。
// handle 成功取得后,ready 成功/拒绝都在 finally 释放订阅并 close preview;不关闭共享 reader/其它订阅。
// 端口返回模型的领域合法性仍由 canonical adapter/预览解释器和后续 engine 准入负责。
try{const snapshot=await handle.ready;if(disposed||accountId!==account||authEpoch!==readEpoch)throw fault('identity_changed');const last=snapshot.chunks.at(-1);if(!last||!last.complete||last.value.status==='receiving')throw fault('incomplete_load');return last.value.status==='ready'?Object.freeze({...last.value,sourceToken:last.snapshotToken}):last.value;}finally{handle.release();preview?.close();}
},
/** 按当前账号激活并懒创建 workspace;导航到同工作区不会重新创建资源池或项目。 */
@@ -74,9 +82,11 @@ export function createRuntimeHost(options: { registry: Registry; workspace(accou
}
return runtime;
},
/** 退出身份但保留 host 可再次绑定;返回旧 runtime 收尾 Promise,让宿主处理未保存错误。 */
/** 同步清账号并递增 epoch,关闭读句柄,再等待退休 runtime;不永久 disposed,之后可重新绑定。
* 不等待 close 成功也已使旧 current 失效;失败不会恢复旧身份或删除退休实例。 */
retireIdentity() { retire(); return closeRetired(); },
/** 永久关闭 host 并退休全部作用域;失败仍可再次调用 close 重试退休集合。 */
/** 永久 disposed 后再退休/等待关闭,后续 workspace/bind/load 准入拒绝。
* 失败可再次 close 重试;每次都会再次 retire/推进 epoch,返回值不是共享的单飞关闭 Promise。 */
close() { disposed = true; retire(); return closeRetired(); },
});
}