Files
Endoleap/src/lib/canvas-runtime/resource-retention.ts
T

66 lines
6.0 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.
/**
* 本地文件回收前的完整保留视图与发布协调。
* working、ACK、history/ledger、导入/run、解码、临时准备和显示句柄分别保留资源;任何未知引用都保守阻止删除。
* 退休标记不等于已删字节:重新 pin 会撤销软退休,已永久清理的本地独有字节不能假装仍可恢复。
*/
import {importNeedsSeal} from './imports.ts';
import type {Id,Registry} from '../canvas-core/types.ts';
import {fault,readMap,readSet,freeze} from '../canvas-core/values.ts';
import type {EngineSnapshot,RuntimeState} from './contracts.ts';
import {createResourcePinScan} from './resources.ts';
/** Complete local-byte protection for one project scope; no DOM/viewport filtering. */
/** 派生 resource ID → 保留理由 Set,同时扫描 working 与 acknowledged。
* 复用同一 history/ledger 解释器,但 ACK 只额外合入 element: 理由;两次扫描完整性仍都参与结果。
* displays 是资源 ID→租用数,temporary/resourcePins 是 owner→资源 ID 列表,不是同方向的表。
* 返回捕获的只读表/集合;不写 runtime、不增减持久引用计数,也不盘点文件是否实际存在。 */
export function resourceRetentionPins(snapshot:EngineSnapshot,registry:Registry,displays:ReadonlyMap<Id,number>=new Map(),temporary:ReadonlyMap<Id,readonly Id[]>=new Map()) {
const {runtime}=snapshot;
const scan=createResourcePinScan(snapshot.history,runtime.ledger,registry);
const current=scan(snapshot.document,runtime.resourcePins);
const base=scan(runtime.acknowledged);
const pins=new Map<Id,Set<string>>();
/** 合并 resource ID→owner 理由集,删除一个理由不影响其它 owner 的保留权。 */
const add=(id:Id,owner:string)=>{let owners=pins.get(id);if(!owners){owners=new Set();pins.set(id,owners);}owners.add(owner);};
for(const [id,owners]of current.pins)for(const owner of owners)add(id,owner);
for(const [id,owners]of base.pins)for(const owner of owners)if(owner.startsWith('element:'))add(id,`acknowledged:${owner}`);
// Sources and server run envelopes are not all interpreted resource schemas yet.
// Pin the whole known local depot rather than claiming opaque task data contains no references.
// 未结束/未封存导入和 opaque run 保留已知资源仓库,不假设尚未解释的 envelope 没有引用。
for(const job of runtime.imports?.values()??[])if(!['completed','partial','cancelled'].includes(job.status)||importNeedsSeal(job)){if(job.source)add(job.source.fileId,`import-source:${job.id}`);for(const [id,resource]of runtime.resources)if(resource.retirement?.purgedAt===undefined)add(id,`import:${job.id}`);}
for(const run of runtime.runs.values())for(const [id,resource]of runtime.resources)if(resource.retirement?.purgedAt===undefined)add(id,`run:${run.id}`);
// imports 只在非终态或仍需 seal 时 pin,runs 则不按 status 过滤;它们保留已知且未 purge 的资源。
// 这种保守 pin 不会自行把 complete 改为 false,也无法列出未知仓库 ID;全局/孤儿回收还需更强覆盖门槛。
// 等待解码或没有解释器的状态也 pin;显示 URL 租用数与临时任务另外合并。
for(const resource of runtime.resources.values())if(resource.image&&(resource.image.version!==1||resource.image.status==='pending'))add(resource.id,resource.image.version!==1?'image-decoder-unavailable':'image-decode');
for(const [owner,ids]of temporary)for(const id of ids)add(id,owner);
for(const [id,count]of displays)if(count>0)add(id,'display');
// sizing 未知版本也降低覆盖完整性,即使本轮没有形成额外 pin;complete 不是文件可读/可删证明。
return Object.freeze({complete:current.complete&&base.complete&&![...runtime.resources.values()].some(resource=>resource.image&&resource.image.version!==1||resource.sizing&&resource.sizing.version!==1),pins:readMap(new Map([...pins].map(([id,owners])=>[id,readSet(owners)])))});
}
/** Runs inside the editor's single publication boundary, including reconcile and metadata commits. */
/** 由 engine 的 Root 发布钩子调用:检查全局 reserved 集合,清除失效的软退休标记。
* reserved 非空时,任一保留 ID 相交或扫描不完整都会拒绝发布;它本身不获取/释放 reservation。
* 只按 next 检查保留状态,不比较 previous 来证明退休时钟单调,也不执行磁盘删除或等待保留期。 */
export function prepareResourceRetention(next:EngineSnapshot,registry:Registry,displays:ReadonlyMap<Id,number>,reserved:ReadonlySet<Id>,temporary:ReadonlyMap<Id,readonly Id[]>=new Map()):RuntimeState {
// 没有待回收工作/软标记时快速复用 runtime,避免每次普通编辑重扫整个资源集合。
if(!reserved.size&&![...next.runtime.resources.values()].some(resource=>resource.retirement&&(resource.retirement.purgedAt===undefined||!resource.stored)))return next.runtime;
const pins=resourceRetentionPins(next,registry,displays,temporary);
if(reserved.size&&(!pins.complete||[...reserved].some(id=>pins.pins.has(id))))throw fault('resource_collection_in_progress',[...reserved]);
// 没有标记需清除时沿用 runtime;首次真正修改才复制 resources 表,其余资源记录继续共享。
let resources=next.runtime.resources;
for(const [id,resource]of next.runtime.resources){
if(!resource.retirement)continue;
// 本地独有且已删字节不能重新引用;有 stored 远端副本则可由显示/读取端口恢复。
if(resource.retirement.purgedAt!==undefined){
if(!resource.stored&&pins.pins.has(id))throw fault('resource_bytes_unavailable',[id]);
// 重新引用或覆盖失去完整性都清掉软退休,下一次无引用必须重新开始保留期。
}else if(!pins.complete||pins.pins.has(id)){
if(resources===next.runtime.resources)resources=new Map(resources);
const {retirement:_,...kept}=resource;(resources as Map<Id,typeof resource>).set(id,freeze(kept));
}
}
return resources===next.runtime.resources?next.runtime:Object.freeze({...next.runtime,resources:readMap(resources)});
}