66 lines
6.0 KiB
TypeScript
66 lines
6.0 KiB
TypeScript
/**
|
||
* 本地文件回收前的完整保留视图与发布协调。
|
||
* 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)});
|
||
}
|