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
+20 -6
View File
@@ -23,7 +23,10 @@ export type WorkspaceOptions = {
};
/** Workspace owns persistent projects; a view only borrows one and never closes it. */
/** 创建工作区容器,不立刻打开所有项目,只有 open/openPartial 才取得项目 engine。 */
/** 建立 workspace 共享池和 projectId→打开 Promise 表,不在构造时 claim 项目存储或恢复文档。
* 注入的 digests/images/storage 也由本容器在最终 close 时关闭,跨 workspace 共享它们需宿主另行管理所有权。
* options 仍由闭包读取,身份/端口应保持稳定;公开标量与 assetDepot scope 并非每次随外部 options 重写。
* getProjects 捕获只读表成员,但 Promise 兑现的 engine 仍是业务 API,不是不可操作的纯数据快照。 */
export function createWorkspaceRuntime(options: WorkspaceOptions) {
const digests=options.digests??createDigestPool();
const displays=createDisplayCache({onError:options.onError});
@@ -33,7 +36,9 @@ export function createWorkspaceRuntime(options: WorkspaceOptions) {
/** 准入同时要求工作区仍接受任务和宿主身份仍有效,关闭一开始就让旧调用失效。 */
const current = () => accepting && !closed && options.isCurrentIdentity();
const assetDepot=options.storage.assetDepot?createAssetDepot({scope:options,port:options.storage.assetDepot,current,digest:(bytes,signal)=>digests.digest(bytes,signal)}):undefined;
/** 取得/创建共享项目;打开失败只移除同一个 Promise,迟到失败不能删掉后来重开的实例。 */
/** current 通过后按 document.projectId 复用在途/已打开 Promise,避免同 workspace 同项目重复 claim leader。
* 命中时不以新 document/versions/loading 替换初始参数;open/openPartial 随后再协调新输入。
* 打开拒绝仅移除仍对应自己的 Promise,准入有效时下次可重试;adapters 的同步抛错直接传出。 */
function acquire(document:Document,versions:ReadonlyMap<Id,number>,loading?:LoadingState){
if(!current())return Promise.reject(fault('identity_changed'));
const existing=projects.get(document.projectId);if(existing)return existing;
@@ -46,20 +51,29 @@ export function createWorkspaceRuntime(options: WorkspaceOptions) {
assetDepot,
/** 查看项目打开句柄的只读表,不授予外部关闭/替换内部 ownership 的能力。 */
getProjects: () => readMap(projects),
/** 完整源与已有 engine 协调:同捕获完成走 loading.complete,旧版本不覆盖较新的本地基线。 */
/** 取得 engine 后复查身份:同 sourceToken 的 loading 用 complete,否则尝试普通 refresh。
* 任一传入版本低于当前已知值就跳过整个 refresh;其余一致性由 engine 校验,不在这里合并版本向量。
* complete 拒绝总会抛错;refresh 拒绝仅在原 engine 仍 loading 时抛错,非 loading 可保留原 engine 返回。
* 因此返回 engine 表示复用成功,不等于传入 document 已被采纳或服务器写入已完成。 */
open(document: Document, versions: ReadonlyMap<Id, number>,settings:{sourceToken?:string}={}) {
return acquire(document,versions).then(async engine=>{if(!current())throw fault('identity_changed');const loading=engine.loading.getState();if(loading&&settings.sourceToken===loading.sourceToken){const result=await engine.loading.complete(document,versions,{sourceToken:settings.sourceToken});if(result.status==='rejected')throw fault(result.code,result.targets);}else{const previous=engine.getSnapshot().runtime.versions;if(![...versions].some(([id,value])=>(previous.get(id)??0)>value)){const result=engine.refresh(document,versions);if(result.status==='rejected'&&loading)throw fault(result.code,result.targets);}}return engine;});
},
/** 接入部分源并复用项目 engine,仅相同 sourceToken 的后续块继续 accept。 */
/** 新项目以 partial 文档/loading 打开;已有项目只在当前 loading.sourceToken 匹配时 accept。
* 不同 token 或已无 loading 时直接返回原 engine,不替换其文档/加载任务;匹配但 accept 拒绝则抛错。 */
openPartial(document:Document,versions:ReadonlyMap<Id,number>,loading:LoadingState){
return acquire(document,versions,loading).then(engine=>{if(!current())throw fault('identity_changed');const previous=engine.loading.getState();if(previous?.sourceToken===loading.sourceToken){const result=engine.loading.accept(document,versions,loading);if(result.status==='rejected')throw fault(result.code,result.targets);}return engine;});
},
/** view 离开后工作区仍可补完项目加载;await 前后都检查身份,错误交宿主报告。 */
/** view 离开后仍可 resume 已打开项目的背景加载;等待 opening 前后检查 current。
* loading.resume 的在途身份/结果围栏由 engine 负责,此方法在其返回后不额外再查 current。
* 错误仅在当前身份仍有效时报告 onError,随后原样拒绝;不吞错或清除项目句柄。 */
async resumeLoading(projectId:Id){
try{if(!current())throw fault('identity_changed');const opening=projects.get(projectId);if(!opening)throw fault('project_not_open');const engine=await opening;if(!current())throw fault('identity_changed');const result=await engine.loading.resume();if(result.status==='rejected')throw fault(result.code,result.targets);return result;}
catch(error){if(current()){try{options.onError?.(error);}catch{}}throw error;}
},
/** 幂等/可重试关闭:复用当前 closing Promise,全部 engine 成功后才释放池和 storage。 */
/** 先 accepting=false 永久停止此实例新准入,再单飞等待全部在途打开/engine 关闭。
* 打开失败视为无需关闭;engine 关闭失败聚合抛错并保留项目/共享池,后续 close 可重试但不重新 accepting。
* 项目全成功后才依次同步关闭共享端口;这段不是原子事务,注入端口 close 应可安全重复,抛错也允许重试。
* 全部收尾成功才清 projects/置 closed;此接口不证明外部服务或服务器生成任务也已停止。 */
close() {
accepting = false; // Fence commands/transports before awaiting pending opens or storage.
if (closed) return Promise.resolve();