import { createThumbnailRequestKey, startOwnedThumbnailGeneration, ThumbnailRequestGeneration, thumbnailIdsFromRequestKey, } from '../src/lib/thumbnailRequests.ts'; import { AsyncAdoptionScope } from '../src/lib/asyncOwnership.ts'; const assertEquals = (actual: T, expected: T, message: string) => { if (actual !== expected) { throw new Error(`${message}: expected ${expected}, got ${actual}`); } }; const deferred = () => { let resolve!: (value: T) => void; let reject!: (reason: unknown) => void; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; }; Deno.test('thumbnail request keys are stable and deduplicate ids', () => { const first = createThumbnailRequestKey(['bravo', 'alpha', 'bravo']); const second = createThumbnailRequestKey(['alpha', 'bravo']); assertEquals(first, second, 'equivalent id sets should have one effect key'); assertEquals( JSON.stringify(thumbnailIdsFromRequestKey(first)), JSON.stringify(['alpha', 'bravo']), 'the effect should receive each id once in stable order', ); }); Deno.test('constructing a thumbnail generation does not start a request', async () => { let starts = 0; const generation = new ThumbnailRequestGeneration( () => { starts += 1; return Promise.resolve('url'); }, () => {}, ); assertEquals(starts, 0, 'request work must wait for the owning effect to start it'); await generation.dispose(); await generation.start(['game']); assertEquals(starts, 0, 'a disposed generation must not start later work'); }); Deno.test('a thumbnail generation cannot start after window admission closes', async () => { const scope = new AsyncAdoptionScope(); await scope.disposeOwned(); let starts = 0; const generation = new ThumbnailRequestGeneration( () => { starts += 1; return Promise.resolve('url'); }, () => {}, ); const cleanup = startOwnedThumbnailGeneration(scope, generation, ['game']); cleanup(); await scope.drain(); assertEquals(starts, 0, 'closed window admission must prevent the first invoke'); }); Deno.test('an unmounted thumbnail generation suppresses its in-flight result', async () => { const result = deferred(); const published: string[] = []; const generation = new ThumbnailRequestGeneration( () => result.promise, (id, url) => published.push(`${id}:${url}`), ); const loading = generation.start(['game']); const disposal = generation.dispose(); result.resolve('thumbnail-url'); await loading; await disposal; assertEquals(published.length, 0, 'an in-flight result must not publish after unmount'); }); Deno.test('a replaced thumbnail generation cannot overwrite the current generation', async () => { const abandonedResult = deferred(); const published: string[] = []; const abandoned = new ThumbnailRequestGeneration( () => abandonedResult.promise, (_id, url) => published.push(url), ); const abandonedLoading = abandoned.start(['game']); const abandonedDisposal = abandoned.dispose(); const current = new ThumbnailRequestGeneration( () => Promise.resolve('current-url'), (_id, url) => published.push(url), ); await current.start(['game']); abandonedResult.resolve('stale-url'); await abandonedLoading; await abandonedDisposal; assertEquals( JSON.stringify(published), JSON.stringify(['current-url']), 'only the current effect generation may publish', ); }); Deno.test('an active failed thumbnail request publishes the empty fallback', async () => { const failure = new Error('thumbnail unavailable'); const published: string[] = []; const generation = new ThumbnailRequestGeneration( () => Promise.reject(failure), (id, url) => published.push(`${id}:${url}`), ); await generation.start(['game']); assertEquals( JSON.stringify(published), JSON.stringify(['game:']), 'active failures should retain the placeholder-cover behavior', ); });