/** * 临时显示资源的共享租约,按不透明 owner 对象+key 复用进行中的加载和 URL。 * 不拥有上传/授权/原字节持久化,调用者取得租约前必须确认权限并保留字节;没有 idle URL 缓存。 * 最后一个租约释放就 abort/清理来源;迟到 load 返回的资源也会被释放,不能跨退役 owner 重新采用。 */ import { fault } from '../canvas-core/values.ts'; /** 加载器返回的临时 URL 与可选清理回调(如 revokeObjectURL),不是永久资源身份。 */ export type DisplaySource = { readonly url: string; release?(): void }; /** 每个消费者独立 ready/release,共享来源不意味着一个 view 卸载可以释放其它 view 的租约。 */ export type DisplayLease = { readonly ready: Promise; release(): void }; /** owner/key 的内存条目,controller 控制共享加载,leases 精确记录当前借用者,retired 防迟到结果。 */ type Entry = { readonly key: string; readonly owner: object; readonly controller: AbortController; readonly leases: Set<() => void>; promise: Promise; source?: DisplaySource; retired: boolean }; /** Ephemeral display ownership only. The caller checks authorization and pins * durable bytes before acquiring. Scope objects are never serialized or reused * across project engines/auth epochs. There is no idle URL retention. */ /** 创建纯显示生命周期容器,owners 两层 Map 用对象身份+字符串 key 索引共享条目。 * owner 应绑定 engine/auth epoch;本模块不解释账号、资源 ID、URL 权限或实际文件 pin。 * 没有容量上限/空闲保留,最后 lease 释放即清理;永久撤销 owner 则另记到 WeakSet。 */ export function createDisplayCache(options: { onError?: (error: unknown) => void } = {}) { // owners 仅保留活条目;retired WeakSet 禁止退役 owner 再 acquire,但不强持有其内存。 const owners = new Map>(), retired = new WeakSet(); let closed = false; /** 同步调用来源 release,隔离其抛错和 onError 的抛错;onError 只报告清理失败。 * load 的失败通过各 lease.ready 传出,不在这里自动记录或重试;release 合同也不是异步清理任务。 */ function dispose(source: DisplaySource) { try { source.release?.(); } catch (error) { try { options.onError?.(error); } catch { /* Reporting cannot interrupt cleanup. */ } } } /** 幂等退休共享条目,仅删除仍指向自己的表项,避免旧任务删掉同 key 的新 entry。 */ function retire(entry: Entry) { if (entry.retired) return; entry.retired = true; const entries = owners.get(entry.owner); if (entries?.get(entry.key) === entry) { entries.delete(entry.key); if (!entries.size) owners.delete(entry.owner); } entry.controller.abort(); if (entry.source) { dispose(entry.source); entry.source = undefined; } } /** 永久撤销一个 owner 的所有租约;逐租约拒绝尚未完成 ready,再清理来源/请求。 */ function releaseOwner(owner: object) { retired.add(owner); for (const entry of [...(owners.get(owner)?.values() ?? [])]) { for (const release of [...entry.leases]) release(); retire(entry); } } return Object.freeze({ /** 同 owner/key 复用进行中或已加载来源,命中时忽略本次传入的 load,不逐次重验权限。 * 每个调用者独立 ready/release;释放已兑现的 ready 不会改变 Promise 的成功结果,但 URL 可能随最后释放失效。 * 不自动接收 AbortSignal,取消通过 lease.release;单个释放不取消仍有其它消费者的共享加载。 */ acquire(owner: object, key: string, load: (signal: AbortSignal) => Promise): DisplayLease { if (closed || retired.has(owner)) throw fault('display_scope_closed'); if (!owner || typeof owner !== 'object' || typeof key !== 'string' || !key) throw fault('invalid_display_key'); let entries = owners.get(owner); if (!entries) { entries = new Map(); owners.set(owner, entries); } let entry = entries.get(key); // 先登记 created 再微任务调用外部 load,使同步异常也有清理句柄,不重复启动相同 key 加载。 if (!entry) { const created: Entry = { owner, key, controller: new AbortController(), leases: new Set(), retired: false, promise: undefined as unknown as Promise }; entry = created; entries.set(key, created); created.promise = Promise.resolve().then(() => { if (created.retired) throw fault('display_released'); return load(created.controller.signal); }).then(source => { // 这里只检查 URL 为非空字符串及可选 release 为函数,不验证 URL 协议/可访问性或图片内容。 if (!source || typeof source.url !== 'string' || !source.url || source.release !== undefined && typeof source.release !== 'function') { if (typeof source?.release === 'function') dispose(source); throw fault('invalid_display_source'); } // 即使 load 不响应 abort,迟到返回也只能 dispose,不允许挂回已无消费者的缓存。 if (created.retired || !created.leases.size) { dispose(source); throw fault('display_released'); } created.source = source; return source; }, error => { retire(created); throw error; }); } const shared = entry; let released = false, rejectReady: (error: unknown) => void = () => {}; /** 只释放本租约;未结束 ready 用 display_released 拒绝,最后一个租约才退休共享 entry。 */ const release = () => { if (released) return; released = true; shared.leases.delete(release); rejectReady(fault('display_released')); if (!shared.leases.size) retire(shared); }; shared.leases.add(release); // 映射共享 promise 到各 lease.ready,失败逐个归还;校验 source 时抛错也沿这条失败路径清理。 // 缓存不吞 ready 的拒绝,消费者需接住取消/加载错误;释放后迟到成功不能再次 resolve 本租约。 const ready = new Promise((resolve, reject) => { rejectReady = reject; shared.promise.then(source => { if (!released) resolve(source.url); }, error => { if (!released) { reject(error); release(); } }); }); return Object.freeze({ ready, release }); }, releaseOwner, /** 幂等关闭整个缓存并退休全部 owner,不删除文件或撤销已确认上传。 */ close() { if (closed) return; closed = true; for (const owner of [...owners.keys()]) releaseOwner(owner); }, }); } /** 公共 API 由工厂推导;view 只借用 lease,不直接拥有该 workspace 级缓存。 */ export type DisplayCache = ReturnType;