docs(tests): explain model streams previews and scoped read caches
This commit is contained in:
@@ -1,24 +1,35 @@
|
||||
/**
|
||||
* 资源显示租约缓存测试。按 owner 和资源键共享进行中的读取/URL,最后租约释放即清理,不保留空闲 URL。
|
||||
* 每个等待者可独立取消;旧读取迟到结果应清理自身,不能替换同键的新代次或破坏其他 owner。
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createDisplayCache } from '../../src/lib/canvas-runtime/display-cache.ts';
|
||||
// 手动 gate 可返回成功显示源或抛错,让取消和迟到清理时序可控。
|
||||
const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; };
|
||||
// 推进事件循环等待 load/清理执行,不做真实磁盘或浏览器图片加载。
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
// 两个租约共享一次 load 和同一 URL,release 幂等;最后释放清理后再 acquire 必须重新 load,关闭清理新源。
|
||||
test('concurrent display leases share one source until the last idempotent release; there is no idle URL retention', async () => {
|
||||
const cache = createDisplayCache(), owner = {}; let loads = 0, disposed = 0;
|
||||
// 可观测显示源工厂:每次真实读取产生新 URL,源 release 计数代表底层资源释放。
|
||||
const load = async () => ({ url: `blob:${++loads}`, release: () => disposed++ });
|
||||
const a = cache.acquire(owner, 'asset', load), b = cache.acquire(owner, 'asset', load);
|
||||
assert.deepEqual(await Promise.all([a.ready, b.ready]), ['blob:1', 'blob:1']); assert.equal(loads, 1);
|
||||
a.release(); a.release(); assert.equal(disposed, 0); b.release(); assert.equal(disposed, 1);
|
||||
const c = cache.acquire(owner, 'asset', load); assert.equal(await c.ready, 'blob:2'); c.release(); cache.close(); assert.equal(disposed, 2);
|
||||
});
|
||||
// 取消一个等待租约只拒绝它自己的 ready,共享 AbortSignal 仍有效;最后一个 reader 释放才 abort 并清理。
|
||||
test('cancelling one waiting lease does not abort a shared load or another reader', async () => {
|
||||
const cache = createDisplayCache(), owner = {}, gate = deferred(); let signal, disposed = 0;
|
||||
// 捕获共享读取的 AbortSignal 并挂起,区分释放单个租约与终止整个底层读取。
|
||||
const load = async value => { signal = value; return gate.promise; };
|
||||
const a = cache.acquire(owner, 'asset', load), b = cache.acquire(owner, 'asset', load); await tick();
|
||||
const cancelled = assert.rejects(a.ready, { code: 'display_released' }); a.release(); await cancelled; assert.equal(signal.aborted, false);
|
||||
gate.resolve({ url: 'blob:shared', release: () => disposed++ }); assert.equal(await b.ready, 'blob:shared'); b.release(); assert.equal(signal.aborted, true); assert.equal(disposed, 1);
|
||||
});
|
||||
// 最后旧租约释放即 abort,随后同键可新建;旧源迟到只释放自身,不能替换/逐出新缓存条目。
|
||||
test('last release aborts a load; a late source cannot replace or evict a newly acquired entry', async () => {
|
||||
const cache = createDisplayCache(), owner = {}, gate = deferred(); let signal, oldDisposed = 0, newDisposed = 0;
|
||||
const old = cache.acquire(owner, 'asset', async value => { signal = value; return gate.promise; }); await tick();
|
||||
@@ -27,12 +38,14 @@ test('last release aborts a load; a late source cannot replace or evict a newly
|
||||
gate.resolve({ url: 'blob:old', release: () => oldDisposed++ }); await tick(); assert.equal(oldDisposed, 1); assert.equal(newDisposed, 0);
|
||||
const peer = cache.acquire(owner, 'asset', async () => { throw Error('unexpected duplicate read'); }); assert.equal(await peer.ready, 'blob:new'); peer.release(); next.release(); assert.equal(newDisposed, 1);
|
||||
});
|
||||
// 读取失败保留原错误码并移除失败条目,新 acquire 可重新读取成功,不永久缓存错误。
|
||||
test('read failure retains its cause and removes the failed cache entry for a fresh retry', async () => {
|
||||
const cache = createDisplayCache(), owner = {};
|
||||
const a = cache.acquire(owner, 'asset', async () => { throw Object.assign(Error('disk_failed'), { code: 'disk_failed' }); });
|
||||
await assert.rejects(a.ready, { code: 'disk_failed' });
|
||||
const b = cache.acquire(owner, 'asset', async () => ({ url: '/remote' })); assert.equal(await b.ready, '/remote'); b.release(); cache.close();
|
||||
});
|
||||
// 同资源 ID 不同 owner 各有源;releaseOwner 只关闭自己的作用域并禁止再次领取,不影响另一个 owner 的共享租约。
|
||||
test('project owner scopes isolate identical IDs and retired owners cannot reacquire', async () => {
|
||||
const cache = createDisplayCache(), a = {}, b = {}; let disposed = 0;
|
||||
const first = cache.acquire(a, 'same-id', async () => ({ url: 'blob:a', release: () => disposed++ }));
|
||||
@@ -41,6 +54,7 @@ test('project owner scopes isolate identical IDs and retired owners cannot reacq
|
||||
assert.throws(() => cache.acquire(a, 'same-id', async () => ({ url: '/forbidden' })), { code: 'display_scope_closed' });
|
||||
const peer = cache.acquire(b, 'same-id', async () => { throw Error('unexpected'); }); assert.equal(await peer.ready, 'blob:b'); peer.release(); second.release(); assert.equal(disposed, 2);
|
||||
});
|
||||
// close 立即拒绝挂起 ready,迟到源仍需释放;关闭后新 acquire 拒绝,多次 close 安全。
|
||||
test('cache close rejects waiting readers immediately and disposes late data without reopening a scope', async () => {
|
||||
const cache = createDisplayCache(), gate = deferred(); let disposed = 0;
|
||||
const lease = cache.acquire({}, 'asset', () => gate.promise); await tick();
|
||||
@@ -48,6 +62,7 @@ test('cache close rejects waiting readers immediately and disposes late data wit
|
||||
gate.resolve({ url: 'blob:late', release: () => disposed++ }); await tick(); assert.equal(disposed, 1);
|
||||
assert.throws(() => cache.acquire({}, 'asset', async () => ({ url: '/late' })), { code: 'display_scope_closed' }); cache.close();
|
||||
});
|
||||
// 空 URL 等非法源需清理已分配资源;某个 release 抛错通过 onError 报告,不能阻止其他 owner 的清理。
|
||||
test('invalid sources clean their allocation and cleanup errors do not interrupt other owners', async () => {
|
||||
const errors = [], cache = createDisplayCache({ onError: error => errors.push(error) }); let disposed = 0;
|
||||
const invalid = cache.acquire({}, 'invalid', async () => ({ url: '', release: () => disposed++ })); await assert.rejects(invalid.ready, { code: 'invalid_display_source' }); assert.equal(disposed, 1);
|
||||
|
||||
Reference in New Issue
Block a user