57 lines
5.4 KiB
JavaScript
57 lines
5.4 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createDisplayCache } from '../../src/lib/canvas-runtime/display-cache.ts';
|
|
const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; };
|
|
const tick = () => new Promise(resolve => setImmediate(resolve));
|
|
|
|
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;
|
|
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);
|
|
});
|
|
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;
|
|
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);
|
|
});
|
|
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();
|
|
const cancelled = assert.rejects(old.ready, { code: 'display_released' }); old.release(); await cancelled; assert.equal(signal.aborted, true);
|
|
const next = cache.acquire(owner, 'asset', async () => ({ url: 'blob:new', release: () => newDisposed++ })); assert.equal(await next.ready, 'blob:new');
|
|
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);
|
|
});
|
|
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();
|
|
});
|
|
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++ }));
|
|
const second = cache.acquire(b, 'same-id', async () => ({ url: 'blob:b', release: () => disposed++ }));
|
|
assert.deepEqual(await Promise.all([first.ready, second.ready]), ['blob:a', 'blob:b']); cache.releaseOwner(a); assert.equal(disposed, 1);
|
|
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);
|
|
});
|
|
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();
|
|
const cancelled = assert.rejects(lease.ready, { code: 'display_released' }); cache.close(); await cancelled;
|
|
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();
|
|
});
|
|
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);
|
|
const a = cache.acquire({}, 'a', async () => ({ url: 'blob:a', release: () => { throw Error('cleanup_failed'); } }));
|
|
const b = cache.acquire({}, 'b', async () => ({ url: 'blob:b', release: () => disposed++ })); await Promise.all([a.ready, b.ready]); cache.close(); assert.equal(disposed, 2); assert.equal(errors[0].message, 'cleanup_failed');
|
|
});
|