Files
Endoleap/src/lib/canvas-runtime/depot-cut.ts
T

51 lines
4.5 KiB
TypeScript

/**
* 从即将持久化的精确 EngineSnapshot 派生共享仓库清单。
* 清单关联同一 rootRevision;不能把较新的采纳字节或旧的 pin 扫描混进这次 Root。
* 这里只准备 descriptors,实际字节校验和写入交给 storage adapter 的原子事务。
*/
import type {Registry} from '../canvas-core/types.ts';
import {fault} from '../canvas-core/values.ts';
import type {EngineSnapshot} from './contracts.ts';
import type {DepotCut,DepotWrite} from './asset-depot.ts';
import {sharedAssetRef} from './asset-depot.ts';
import type {StorageLeader,StoragePort} from './storage-port.ts';
import {resourceRetentionPins} from './resource-retention.ts';
/** Derive one project's shared-byte manifest from the exact Root being persisted. */
/** 从指定不可变 snapshot 派生共享身份/字节写入项与 pins,缺少 assetDepot 端口则拒绝。
* 会异步读取 depot/本地文件;不提交存储事务、不重算摘要,也不在 await 后自行校验 leader/宿主身份。
* 返回可变描述数组而非冻结 Root;调用方须把清单与该 snapshot 同次提交,rootRevision 不另存于返回值。
* snapshot 和各输入缓存需遵循宿主捕获约定,尤其 adoptedFiles 用 revision 防止混入较晚的采纳。 */
export async function prepareDepotCut(input:{snapshot:EngineSnapshot;registry:Registry;storage:StoragePort;leader:StorageLeader;authority:string;files:ReadonlyMap<string,Blob>;displays:ReadonlyMap<string,number>;temporary:ReadonlyMap<string,readonly string[]>;adoptedFiles?:ReadonlyMap<string,{readonly bytes:Blob;readonly rootRevision:number}>}):Promise<DepotCut>{
const {snapshot,storage,leader,authority}=input,pins=resourceRetentionPins(snapshot,input.registry,input.displays,input.temporary),writes:DepotWrite[]=[];
if(!storage.assetDepot)throw fault('asset_depot_unavailable');
// 每个 stored+digest 资源都登记身份(包括已 purge 的项目记录),但只有仍保留的才尝试补字节。
// sharedAssetRef 校验并捕获元数据,随后要求资产 ID/长度对应本地记录;未上传资源不进入共享写入清单。
for(const resource of snapshot.runtime.resources.values())if(resource.stored&&resource.digest){
const ref=sharedAssetRef({version:1,authority,digest:resource.digest,asset:resource.stored});
if(ref.asset.id!==resource.id||ref.asset.bytes!==resource.byteLength)throw fault('shared_asset_identity_conflict',[resource.id]);
let bytes:Blob|undefined;
// 仅未 purge 且有保留理由时补共享字节;仓库已有字节就不重复提供项目文件。
if(resource.retirement?.purgedAt===undefined&&pins.pins.has(resource.id)){
const cached=await storage.assetDepot.read(leader,authority,resource.id);
// The store enforces immutable identity even when bytes are already present.
// 缓存已有字节则只提交身份;否则优先项目内存文件,再读项目存储。
// 两处均无字节时仍可形成仅身份写入项;是否满足事务要求、摘要与不可变身份由 adapter 检查。
if(!cached?.bytes)bytes=input.files.get(resource.id)??await storage.readFile(leader,resource.id);
}
writes.push({ref,...(bytes?{bytes}:{})});
}
// 跨项目采纳 binding 不应与本地 upload resource 重名;延后 Root 的 bytes 不得提前提交。
for(const [id,binding]of snapshot.runtime.assetBindings??[]){
if(binding.authority!==authority||binding.asset.id!==id||snapshot.runtime.resources.has(id))throw fault('shared_asset_identity_conflict',[id]);
const pending=input.adoptedFiles?.get(id),bytes=pending&&pending.rootRevision<=snapshot.rootRevision?pending.bytes:undefined;writes.push({ref:sharedAssetRef(binding),...(bytes?{bytes}:{})});
}
// binding 不以 pins 决定是否写入,只有其暂存 bytes 的采纳 revision 不晚于本 Root 才携带字节。
// A source/run envelope may refer to resources outside this project's upload table.
// Its complete resource interpreter must be installed before global deletion is allowed.
// 引用、加载与任务解释都完整才允许全局回收;opaque run/未完导入不能假定无共享资源引用。
const complete=pins.complete&&!snapshot.runtime.loading&&![...(snapshot.runtime.imports?.values()??[])].some(job=>!['completed','partial','cancelled'].includes(job.status))&&snapshot.runtime.runs.size===0;
// complete 是共享仓库全局删除所需的覆盖声明,不是字节写入完成回执;真正删除由仓库协议决定。
return{version:1,authority,complete,pins:[...pins.pins].map(([id,owners])=>[id,[...owners]] as const),writes};
}