docs(canvas): explain compute workers and display resource lifetimes
This commit is contained in:
@@ -1,17 +1,29 @@
|
||||
/**
|
||||
* 临时显示资源的共享租约,按不透明 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<string>; release(): void };
|
||||
/** owner/key 的内存条目,controller 控制共享加载,leases 精确记录当前借用者,retired 防迟到结果。 */
|
||||
type Entry = { readonly key: string; readonly owner: object; readonly controller: AbortController; readonly leases: Set<() => void>; promise: Promise<DisplaySource>; 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.
|
||||
*/
|
||||
/** 创建纯显示生命周期容器;owner 应绑定 engine/auth epoch,不能序列化或跨账号复用。 */
|
||||
export function createDisplayCache(options: { onError?: (error: unknown) => void } = {}) {
|
||||
// owners 仅保留活条目;retired WeakSet 禁止退役 owner 再 acquire,但不强持有其内存。
|
||||
const owners = new Map<object, Map<string, Entry>>(), retired = new WeakSet<object>();
|
||||
let closed = false;
|
||||
/** 统一调用来源 release 并隔离错误,清理/日志失败不阻止释放其它 URL。 */
|
||||
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;
|
||||
@@ -20,6 +32,7 @@ export function createDisplayCache(options: { onError?: (error: unknown) => void
|
||||
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() ?? [])]) {
|
||||
@@ -28,11 +41,13 @@ export function createDisplayCache(options: { onError?: (error: unknown) => void
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
/** 借用显示来源:同 owner/key 合并加载,但每个调用者可独立取消自己的等待。 */
|
||||
acquire(owner: object, key: string, load: (signal: AbortSignal) => Promise<DisplaySource>): 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<DisplaySource> };
|
||||
entry = created; entries.set(key, created);
|
||||
@@ -41,18 +56,21 @@ export function createDisplayCache(options: { onError?: (error: unknown) => void
|
||||
return load(created.controller.signal);
|
||||
}).then(source => {
|
||||
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 映射成每租约的独立 ready;来源失败时本租约也自动归还,不泄漏计数。
|
||||
const ready = new Promise<string>((resolve, reject) => {
|
||||
rejectReady = reject;
|
||||
shared.promise.then(source => { if (!released) resolve(source.url); }, error => { if (!released) { reject(error); release(); } });
|
||||
@@ -60,7 +78,9 @@ export function createDisplayCache(options: { onError?: (error: unknown) => void
|
||||
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<typeof createDisplayCache>;
|
||||
|
||||
Reference in New Issue
Block a user