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'); });