/** * 从即将持久化的精确 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;displays:ReadonlyMap;temporary:ReadonlyMap;adoptedFiles?:ReadonlyMap}):Promise{ 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}; }