docs(runtime): explain public recovery and engine shutdown contracts
This commit is contained in:
@@ -1260,10 +1260,13 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
// 对外只暴露冻结的控制面;根快照由 editor 读取,异步执行器和持久化内部表不直接交给界面修改。
|
||||
return Object.freeze({
|
||||
editor, sessions, imports,
|
||||
/** 渐进加载控制面:accept 合入同一流的前缀,complete 提交完整结果,resume 重新读取权威模型。 */
|
||||
/** 渐进加载控制面:accept 合入同一流前缀,complete 提交完整结果,resume 重新读取权威模型。
|
||||
* getState 返回当前根中的状态引用;accept 同步发布候选,complete/resume 成功路径另等本地保存。 */
|
||||
loading:Object.freeze({getState:()=>editor.getSnapshot().runtime.loading,
|
||||
/** 同一时间共享一个恢复读取。v3+ 等旧请求确认后先 hold 新发送,再读服务端,
|
||||
* 避免读取期间又发出写入造成模型包含范围不明;finally 释放本次读取的暂停租约。 */
|
||||
* 避免读取期间又发出写入造成模型包含范围不明;finally 释放本次读取的暂停租约。
|
||||
* 等所有 request.phase=resolved,不只等 HTTP 回调;已 applied 但尚未协调的请求仍会阻止刷新。
|
||||
* 不暂停本地编辑,completeLoading 会叠回期间未决意图;此读取不重启导入或替用户解决冲突。 */
|
||||
resume():Promise<Result>{
|
||||
if(loadingRead)return loadingRead;
|
||||
const task=(async()=>{assertWritable(false);if(!editor.getSnapshot().runtime.loading)return{status:'applied' as const,revision:editor.getSnapshot().revision,operationIds:[]};if(!options.persistence.read)throw fault('canonical_reader_unavailable');const controller=new AbortController();loadingReadController=controller;let releaseRead:(()=>void)|undefined;
|
||||
@@ -1275,7 +1278,8 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
})();loadingRead=task.finally(()=>{if(loadingRead===wrapped)loadingRead=undefined;});const wrapped=loadingRead;return wrapped;
|
||||
},
|
||||
/** 校验来源前缀只能合法扩展,再合并期间已确认的修改并重放 pending;
|
||||
* 原始读取 nextRead 与 working 分开保存,本地编辑不冒充服务端新增内容。 */
|
||||
* 原始读取 nextRead 与 working 分开保存,本地编辑不冒充服务端新增内容。
|
||||
* acceptingLoading 仅覆盖本次同步 reconcile,finally 撤销;返回 applied 仍须后台保存,并不等待持久回执。 */
|
||||
accept(document:Document,versions:ReadonlyMap<Id,number>,state:LoadingState):Result{
|
||||
try{assertWritable(false);const root=editor.getSnapshot(),previous=root.runtime;if(!previous.loading)throw fault('loading_not_active');const source=previous.loadingRead;validatePrefixExtension(source?.document??previous.acknowledged,source?.versions??previous.versions,previous.loading,document,versions,state,options.registry);const nextRead=Object.freeze({document:createDocument(document.projectId,[...document.canvases.values()],[...document.elements.values()],document.tombstones),versions:readMap(versions)}),proposal=mergeLoadingRead(previous,nextRead,options.registry),acknowledged=proposal.document,working=replayPending(acknowledged,previous.ledger,options.registry);acceptingLoading=true;return editor.reconcile(working,Object.freeze({...previous,loading:freeze(clone(state)),...(state.version>=3?{loadingRead:nextRead}:{}),versions:readMap(proposal.versions),acknowledged}),root.rootRevision);}catch(error){return reject(error);}finally{acceptingLoading=false;}
|
||||
},
|
||||
@@ -1286,7 +1290,9 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
collectResources,collectImportSources,inventoryLocalFiles,collectOrphanFiles,
|
||||
getResourcePins:()=>resourceRetentionPins(editor.getSnapshot(),options.registry,displayPins,admissionPins(editor.getSnapshot())),
|
||||
/** 暂停调度/生成轮询/导入,排空在途协调并保存,再读取服务端模型生成审阅会话。
|
||||
* prepare 本身不替用户丢弃编辑或改写原请求;发现冲突只返回诊断,发布发生在 apply/resolve。 */
|
||||
* 不提前发布恢复候选;在途已准入任务仍可先完成并更新当前根,排空后捕获固定底稿。
|
||||
* 暂停、保存、读服务端并验证已生效回执后尝试条件重放;重放冲突以诊断返回会话,不提前改文档。
|
||||
* prepare 期间仍保存原 working/history/request 事实,用户取舍引起的新发布发生在 apply/resolve。 */
|
||||
async prepareRecovery(): Promise<RecoverySession> {
|
||||
assertWritable(false);
|
||||
if(!options.persistence.read)throw fault('canonical_reader_unavailable');
|
||||
@@ -1306,6 +1312,7 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
await persist();assertCurrent();
|
||||
// 从已知 applied 回执推导可采用的候选证据;先保存现有根,不将未验证的候选提前发布。
|
||||
const staged=stageAppliedRecovery(editor.getSnapshot(),{persistence:options.persistence,runs:options.runs,assets:options.assets,now:runClock.now()});
|
||||
// staged 只在私有候选中解决已验证 applied 证据;它保持底稿 rootRevision,用于后续乐观发布。
|
||||
const root=staged.snapshot,previous=root.runtime;
|
||||
// 读取后的根 revision 必须仍一致;后续计划不能建立在与当前状态不同的旧底稿上。
|
||||
const loaded=await network.run(controller.signal,()=>options.persistence.read!(controller.signal));assertCurrent();
|
||||
@@ -1320,21 +1327,24 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
let committed:ConflictPlan|'refresh'|undefined,committedIds:readonly Id[]=[];
|
||||
// 只接受本会话实际生成的 plan 对象,拒绝调用方拼接或跨会话重用的方案。
|
||||
const plans=new WeakSet<ConflictPlan>();
|
||||
/** 已发布后先过保存屏障,再恢复调度/轮询并释放会话。
|
||||
* 保存失败继续持有暂停;再次调用可重试屏障,不重复发布相同恢复操作。 */
|
||||
/** 发布成功后:persist→scheduler.resume→通知新操作/受影响 run→释放三类 hold。
|
||||
* persist 或 resume 失败都保留已提交标记及会话,可再试 finish 而不重复发布恢复操作。
|
||||
* 返回 applied 表示本地恢复发布/保存完成,不等于替代操作已获服务端 ACK 或生成任务结束。 */
|
||||
const finish=async()=>{if(previous.loading&&!editor.getSnapshot().runtime.loading)loadingCompletionRevision=editor.getSnapshot().rootRevision;await persist();assertCurrent();await scheduler.resume();assertCurrent();scheduler.notify(committedIds);runTracker.notify(staged.runIds);active.release();return{status:'applied' as const,revision:editor.getSnapshot().revision,operationIds:committedIds};};
|
||||
// 返回会话后,未发布可直接退出;已经发布则必须完成保存,cancel 不等于回滚已发布恢复。
|
||||
active.cancel=async()=>{if(recovery!==active)return;if(committed){await finish();return;}active.release();};
|
||||
return Object.freeze({baseRootRevision:root.rootRevision,canonical,acknowledgedRequestIds:staged.requestIds,
|
||||
pendingOperationIds:Object.freeze([...previous.ledger.operations.values()].filter(record=>record.phase!=='resolved').map(record=>record.intent.id)),
|
||||
...(conflict?{conflict}:{}),
|
||||
/** 在固定根上纯计算明确取舍的恢复方案,分配新操作身份并保存可审计备份。 */
|
||||
/** 在固定底稿上计算恢复候选并分配新操作 ID,生成含审计备份的 plan;不发布/写磁盘。
|
||||
* WeakSet 只登记本会话生成的对象身份,拒绝外部拼造;createId 的分配计数不会因放弃 plan 回滚。 */
|
||||
plan(choices:ReadonlyMap<Id,RecoveryChoice>){
|
||||
assertCurrent();if(committed)throw fault('recovery_already_committed');if(editor.getSnapshot().rootRevision!==root.rootRevision)throw fault('stale_plan');
|
||||
const plan=planConflictRecovery(root,canonical,choices,options.registry,createId,options.historyLimit??100);plans.add(plan);return plan;
|
||||
},
|
||||
/** 采用本会话方案:重查策略/生成输出适用性,发布文档、历史、账本、审计及资源 pin。
|
||||
* 同一方案已发布时只重试 finish;不能接着换另一方案覆盖第一次发布。 */
|
||||
* 同一方案已发布时只重试 finish;不能接着换另一方案覆盖第一次发布。
|
||||
* 策略拒绝返回 rejected 并保留会话;通过后 reconcile 按原 rootRevision 同时发布 plan 文档/历史/操作和 runtime。 */
|
||||
async resolve(plan:ConflictPlan):Promise<Result>{
|
||||
assertCurrent();if(!plans.has(plan))throw fault('invalid_recovery_plan');if(committed&&committed!==plan)throw fault('recovery_already_committed');
|
||||
if(!committed){
|
||||
@@ -1360,7 +1370,7 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
if(result.status==='rejected')return result;
|
||||
committed='refresh';
|
||||
}
|
||||
// Persistence failure retains the hold; apply/cancel can retry this barrier without republishing.
|
||||
// 首次发布后记 committed=refresh;保存失败时 apply/cancel 复用 finish,不再发布第二份刷新。
|
||||
return finish();
|
||||
},
|
||||
// 使用更新后的 cancel 语义,已提交会话仍需先完成可靠保存。
|
||||
@@ -1368,7 +1378,8 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
});
|
||||
}catch(error){active.release();throw error;}
|
||||
},
|
||||
/** 读取当前项目画布的模型目录,经过共享网络门;不创建生成任务。 */
|
||||
/** 读取当前项目画布的模型目录,经过共享网络门;不创建生成任务或更新 Root。
|
||||
* 只查画布存在/服务可用及网络门身份,不要求无恢复会话或完整模型;目录返回仍由端口解释。 */
|
||||
models(boardId: Id, signal: AbortSignal) {
|
||||
if(closing||closed||!options.runs)return Promise.reject(fault('run_interpreter_unavailable'));
|
||||
if(!editor.getSnapshot().document.canvases.has(boardId))return Promise.reject(fault('canvas_missing'));
|
||||
@@ -1385,7 +1396,9 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
}catch(error){return reject(error);}
|
||||
},
|
||||
/** 用户明确采用冲突任务的一份输出。校验目标代次/任务成员/输出身份并防重复,
|
||||
* 通过普通数据编辑取得 history;若内容本已相同,仍登记空 changes 的 run.apply 控制动作。 */
|
||||
* 通过普通数据编辑取得 history;若内容本已相同,仍登记空 changes 的 run.apply 控制动作。
|
||||
* 文本采用 text;图片采用 src/assetId,不把输出复制成新的正在执行任务或重复上传任务。
|
||||
* applied 只表示本地接纳,服务应用回执仍需版本/目标身份核验,appliedOutputIds 在回执协调后更新。 */
|
||||
applyRunOutput(runId:Id,target:RunApplyInput['target'],outputId:Id):Result {
|
||||
try{
|
||||
assertWritable();if(!options.runs)throw fault('run_interpreter_unavailable');if(applying)throw fault('reentrant_dispatch');
|
||||
@@ -1405,7 +1418,9 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
}catch(error){return reject(error);}finally{applying=undefined;}
|
||||
},
|
||||
/** 登记生成启动意图,真正受理/执行由服务完成。只允许当前已知的支持类型节点,
|
||||
* 启动依赖所有旧未决操作,保证服务端生成所见输入先包含当前编辑;不在此直接请求模型。 */
|
||||
* 启动依赖所有旧未决操作,保证服务端生成所见输入先包含当前编辑;不在此直接请求模型。
|
||||
* 校验通用输入与目标类型/编辑能力,不在这里刷新模型目录或证明远端模型此刻可用。
|
||||
* 未决 run.start 按目标 ID 保守占用;已有任务用 ID+incarnation/传播目标及 hasMore/pendingPage 判忙。 */
|
||||
startRun(input: RunInput): Result {
|
||||
try {
|
||||
assertWritable();
|
||||
@@ -1428,7 +1443,8 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
}catch(error){return reject(error);}
|
||||
},
|
||||
/** 用新的权威模型刷新 ACK,再条件重放本地未决修改;未确认请求存在时拒绝模糊刷新。
|
||||
* 与 prepareRecovery 不同,此入口只尝试安全重放,冲突时保留文档并登记诊断。 */
|
||||
* 与 prepareRecovery 不同,此入口只尝试安全重放,冲突时保留文档并登记诊断。
|
||||
* 不自行读取网络;调用方提供文档/版本。同步 applied 不等待 persist;loading 中转 completeLoading 的同类同步路径。 */
|
||||
refresh(document: Document, versions: ReadonlyMap<Id, number>): Result {
|
||||
if(editor.getSnapshot().runtime.loading)return completeLoading(document,versions,{fresh:true});
|
||||
const root = editor.getSnapshot(), previous = root.runtime;
|
||||
@@ -1451,7 +1467,9 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
/** 查询图片暂存能力;渐进加载仅 v6 已完整画布开放,真正事务还需经过 editor 策略校验。 */
|
||||
canStageFiles(canvasId:Id){try{assertWritable(false);if(!options.assets)throw fault('asset_interpreter_unavailable');const snapshot=editor.getSnapshot();if(!snapshot.document.canvases.has(canvasId))throw fault('canvas_not_loaded',[canvasId]);if(snapshot.runtime.loading&&(snapshot.runtime.loading.version!==6||!loadingQueryCoverage(snapshot).completeCanvasIds.includes(canvasId)))throw fault('loading_scope_incomplete',[canvasId]);const permission=options.policy?.({type:'changes.apply',changes:[]},snapshot.document);if(permission&&!permission.enabled)throw fault(permission.code,permission.targets);return{enabled:true as const};}catch(error){const result=reject(error);return{enabled:false as const,code:result.code,targets:result.targets};}},
|
||||
/** 同步图片加入入口:校验字节/身份,暂存 Blob,再由 callback 在同一事务创建资源引用。
|
||||
* reducer 同根建立上传意图和资源记录,后续异步摘要/上传;事务失败清理未被根采用的字节。 */
|
||||
* reducer 同根建立上传意图和资源记录,后续异步摘要/上传;事务失败清理未被根采用的字节。
|
||||
* callback 须同步使用 handles/tx 创建引用,不能 await 后继续提交;这是本地编辑接纳,不等上传完成。
|
||||
* 文件名/MIME/长度在此筛选,实际图片解码与摘要另行验证;空净事务撤回本次待写 Blob。 */
|
||||
stageFiles(files: readonly StagedFile[], callback: (resources: readonly ResourceHandle[], transaction: Transaction) => unknown) {
|
||||
assertWritable(false);if(editor.getSnapshot().runtime.loading&&editor.getSnapshot().runtime.loading?.version!==6)throw fault('model_loading');
|
||||
if (staged.length||adopting) throw fault('reentrant_dispatch');
|
||||
@@ -1473,7 +1491,9 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
} finally { staged = []; stagedDefaultSizes.clear();for (const resource of resources) if (!editor.getSnapshot().runtime.resources.has(resource.id)) fileWrites.delete(resource.id); }
|
||||
},
|
||||
/** 复用同工作区已存在的图片:先走服务授权/取字节,再用同步事务建立本项目绑定。
|
||||
* 这条路径不重复创建上传任务;采用失败则还原原来的 adoptedFiles 暂存值。 */
|
||||
* 这条路径不重复创建上传任务;采用失败或无净操作则还原原 adoptedFiles 暂存值。
|
||||
* await 授权后重查写入身份与取消,再设置同步 adopting 上下文;授权成功本身不发布元素/绑定。
|
||||
* Promise 结束于本地事务结果,已接纳字节随后随相应 Root/depot cut 保存,不在此单独等待 durable 回执。 */
|
||||
async adoptResource(id:Id,callback:(resource:ResourceHandle&{readonly asset:StoredAsset},transaction:Transaction)=>unknown,settings:{signal?:AbortSignal}={}):Promise<Result>{
|
||||
assertWritable(false);if(!depot||!options.assets?.authority||!options.storage.assetDepot)throw fault('asset_depot_unavailable');if(editor.getSnapshot().runtime.loading&&editor.getSnapshot().runtime.loading?.version!==6)throw fault('model_loading');
|
||||
if(staged.length||adopting)throw fault('reentrant_dispatch');
|
||||
@@ -1494,7 +1514,8 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
adoptionTasks.set(controller,task);void task.finally(()=>adoptionTasks.delete(controller)).catch(()=>{});return task;
|
||||
},
|
||||
/** 借出图片显示句柄:先 pin 防清理,再取得共享显示 lease,调用方用完必须 release。
|
||||
* 外部 signal、项目关闭和失败路径也会释放;Blob URL 由缓存的最后一个使用者撤销。 */
|
||||
* 外部 signal、项目关闭和失败路径也会释放;Blob URL 由缓存的最后一个使用者撤销。
|
||||
* pin 是本项目瞬态计数,不另存到 Root;共享缓存 key 命中可复用 URL,新句柄仍独立检查身份/取消。 */
|
||||
async displayResource(id: Id, settings: { signal?: AbortSignal } = {}) {
|
||||
assertDisplayIdentity();
|
||||
if(collectionReservations.has(id))throw fault('resource_collection_in_progress',[id]);
|
||||
@@ -1524,7 +1545,8 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
// 缓存键带授权/摘要/操作身份或退休代次,同 ID 换来源不会误用旧 URL。
|
||||
const key=JSON.stringify(binding?['shared-asset',id,binding.authority,binding.digest]:retained?['asset',id,retained.operationId,retained.retirement?.purgedAt??null]:orphan?['orphan',id,orphan.digest]:source?['source',id,source.sha256??null]:['unmanaged',id]);
|
||||
/** 加载回调先找待保存/本地字节,再尝试共享仓库,最后使用服务端显示 URL。
|
||||
* 孤立文件必须再次核对内容证据;所有 await 后复查项目身份,迟到 URL 不交给旧用户。 */
|
||||
* 孤立文件重算摘要并核长度/MIME;普通资源/导入源不在这条显示路径统一重新哈希。
|
||||
* 在字节采纳及最终句柄交付前检查身份/取消;depot.read 不是新的跨项目 adoption 授权。 */
|
||||
lease=displayCache.acquire(displayOwner,key,async signal=>{
|
||||
let bytes=adoptedFiles.get(id)?.bytes??fileWrites.get(id)??await options.storage.readFile(leader,id);
|
||||
if(!bytes&&binding&&depot)bytes=await depot.read(leader,binding,signal);
|
||||
@@ -1545,7 +1567,9 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
}catch(error){release();assertDisplayIdentity();if(settings.signal?.aborted)throw fault('display_aborted');throw error;}
|
||||
},
|
||||
/** 用户主动按原始图片比例调整大小,形成独立编辑事务;与创建时默认尺寸补写区别在历史语义。
|
||||
* 要求资源确属目标、代次未变且无手势,事务后核对实际几何,策略约束不满足则整次拒绝。 */
|
||||
* 要求资源确属目标、代次未变且无手势,在事务草稿中核对实际几何,约束不满足则整次拒绝。
|
||||
* 只从本项目 resources 的已解码/服务尺寸取比例,不对共享 binding 自动发起解码或下载。
|
||||
* 和默认 grant 补写不同,这次是用户显式几何编辑,由普通 transaction 形成独立历史语义。 */
|
||||
fitImage(target:Target,resourceId:Id):Result{
|
||||
try{assertWritable();if(sessions.getSnapshot().gesture)throw fault('gesture_in_progress');const root=editor.getSnapshot(),element=root.document.elements.get(target.id),resource=root.runtime.resources.get(resourceId),size=resource?.image?.version===1&&resource.image.status==='ready'?resource.image:resource?.stored;
|
||||
if(!element||element.incarnation!==target.incarnation||element.structure!=='node'||options.registry.resolve(element)!=='known'||!options.registry.components.get(element.componentId)?.resourceFields?.includes('assetId')||element.data.assetId!==resourceId)throw fault('target_changed');if(!size||!validImageDimensions(size))throw fault('image_dimensions_unavailable');
|
||||
@@ -1561,10 +1585,13 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
/** 订阅本地耐久性变化,返回取消订阅;已关闭时不再登记监听。 */
|
||||
subscribeDurability(listener:()=>void){if(closed)return()=>{};durabilityListeners.add(listener);return()=>{durabilityListeners.delete(listener);};},
|
||||
/** 显式重试先解决恢复会话和保存屏障,再重置可重试摘要/解码/尺寸授权及来源验证。
|
||||
* 最后恢复调度与生成追尾;复用原逻辑事实和冻结请求,不把失败重试变成重复创建。 */
|
||||
* 最后恢复调度与生成追尾;复用原逻辑事实和冻结请求,不把失败重试变成重复创建。
|
||||
* 只重置约定的可重试状态:不复活 revoked grant、不重启所有已终结导入,也不等待未来服务任务结束。 */
|
||||
retry: async () => { if(recovery)await recovery.cancel();assertWritable(false);await persist();retryDefaultSizes();const runtime=editor.getSnapshot().runtime,ids=[...runtime.resources.values()].filter(resource=>!resource.digest&&runtime.ledger.operations.get(resource.operationId)?.blockers.some(code=>digestFailures.has(code))).map(resource=>resource.operationId);if(ids.length){mutate(ledger=>{for(const id of ids)ledger.block(id,['waiting_digest']);});prepareDigests();}if(options.images){editor.updateRuntime(previous=>{const resources=new Map(previous.resources);let changed=false;for(const [id,resource]of resources)if(resource.image?.version===1&&resource.image.status==='failed'){resources.set(id,freeze({...resource,image:{version:1 as const,status:'pending' as const}}));changed=true;}return changed?Object.freeze({...previous,resources:readMap(resources)}):previous;});prepareImages();}for(const job of editor.getSnapshot().runtime.imports?.values()??[])if(sourceNeedsSeal(job)){await importLoads.get(job.id);await resetImportReader(job.id);await loadImport(job.id);}await scheduler.resume();await runTracker.retry(); },
|
||||
/** 排空当前执行链及其派生保存,根变化时再次检查直到稳定。
|
||||
* 不等待未来轮询时刻或服务端生成终结;暂停任务和未到期重试也不等于已经同步成功。 */
|
||||
* 不等待未来轮询时刻或服务端生成终结;暂停任务和未到期重试也不等于已经同步成功。
|
||||
* 不主动取得 hold:期间新增本地根/任务可延长等待;以根 revision、pending 和任务表重新检查稳定性。
|
||||
* adoption/copy 的拒绝用 allSettled 等收尾,其它链路仍可能抛错,settle 不是所有领域错误的汇总报告。 */
|
||||
async settle() {
|
||||
let revision:number;
|
||||
do {
|
||||
@@ -1574,21 +1601,26 @@ export async function openProjectEngine(options: EngineOptions) {
|
||||
await scheduler.settle();await runTracker.settle();await persist();await scheduler.settle();await persist();
|
||||
} while(revision!==editor.getSnapshot().rootRevision||pending.size||writing||adoptionTasks.size||copyPreparations.size||importLoads.size||verificationTasks.size||digestTasks.size||imageTasks.size||sizingTask);
|
||||
},
|
||||
/** 关闭项目所有者:先阻止新准入,中止并排空执行器,再归还显示资源,最后可靠保存与释放领导权。
|
||||
* 保存失败时保留 leader/根与字节材料,允许再次 close 重试;视图 detach 不应调用本方法。 */
|
||||
/** 关闭项目所有者:先 closing、请求中止后台工作,等待列出的本地任务,再释放显示/保存根/归还 leader。
|
||||
* scheduler/runTracker.stop 仅请求 abort 并阻止迟到采纳,这里未另外 await 它们的传输 Promise 全部结束。
|
||||
* 末尾 persist 失败时保留 leader/根/待写字节与 closing 状态,后续 close 可重试;不是把实例恢复可编辑。
|
||||
* 只有 closed 后重复调用立即返回,没有额外 close 单飞 Promise;宿主负责串行关闭,视图 detach 不应调用。 */
|
||||
async close() {
|
||||
if (closed) return;
|
||||
// 关闭标志先发布,再逐类取消/等待;共享 display/depot 只有本项目自己创建时才负责关闭整个实例。
|
||||
closing = true;notifyDurability();loadingReadController?.abort();await loadingRead?.catch(()=>{});unsubscribeSizing();orphanController?.abort();await Promise.allSettled([collection,sourceCollection,orphanCollection].filter(Boolean));digestLifecycle.abort();for(const {controller}of verificationTasks.values())controller.abort();for(const controller of preparationControllers.values())controller.abort();for(const controller of copyPreparations.keys())controller.abort();await Promise.allSettled([...copyPreparations.values()]);for(const controller of adoptionTasks.keys())controller.abort();await Promise.allSettled([...adoptionTasks.values()]); sessions.dispose(); scheduler.stop(); runTracker.stop();
|
||||
// runner close 保留已提交前缀;等所有载入/校验/摘要/解码结束后才能释放读取器和底层存储。
|
||||
// 先等 runner.close、装配/验证,再关闭 proof reader,接着等摘要/解码及尺寸 timer。
|
||||
// runner 关闭保留已经提交内容;上述步骤完成才进入最后根保存,不等待服务器生成任务本身终结。
|
||||
await Promise.all([...importRunners.values()].map(runner=>runner.close()));while(importLoads.size)await Promise.all([...importLoads.values()]);while(verificationTasks.size)await Promise.allSettled([...verificationTasks.values()].map(task=>task.promise));for(const {reader}of proofReaders.values())reader.close();proofReaders.clear();while(digestTasks.size)await Promise.allSettled([...digestTasks.values()]);while(imageTasks.size)await Promise.allSettled([...imageTasks.values()]);while(sizingTask)await sizingTask;
|
||||
recovery?.controller.abort();recovery?.release();
|
||||
// 释放本项目所有显示句柄和 cache owner;其它项目仍可继续使用注入的共享缓存。
|
||||
for (const release of [...displays]) release();displayCache.releaseOwner(displayOwner);if(!options.displays)displayCache.close();
|
||||
// Failure keeps the leader/recovery material available for an explicit retry of close.
|
||||
// 只有 persist 成功才取消根订阅、dispose editor、释放自建 depot 和 leader,再标 closed。
|
||||
// 注入的 storage 连接及共享池由宿主/Workspace 管,engine 不在这里调用 storage.close。
|
||||
await persist(); unsubscribe(); editor.dispose();if(!options.assetDepot)depot?.close();adoptedFiles.clear(); await leader.release(); closed = true;notifyDurability();durabilityListeners.clear();
|
||||
},
|
||||
});
|
||||
// 工厂打开失败时释放已取得的领导权;成功返回后的关闭责任交给实例 close。
|
||||
// 工厂 try 内失败至少归还已取得的 leader,并传播异常;不是已返回实例 close 的完整清理路径。
|
||||
// 成功返回后由宿主调用实例 close 管理项目所有权,视图挂载/卸载不重建这个 leader。
|
||||
} catch (error) { await leader.release(); throw error; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user