132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 1 | import assert from 'node:assert/strict'; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 2 | import { test } from 'node:test'; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 3 | import { createAvatarLoader, type AvatarCache } from '../src/lib/avatar-cache.ts'; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 4 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 5 | function memoryCache(): AvatarCache & { entries: Map<string, Response> } { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 6 | const entries = new Map<string, Response>(); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 7 | return { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 8 | entries, |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 9 | async match(key) { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 10 | return entries.get(key)?.clone(); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 11 | }, |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 12 | async put(key, response) { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 13 | entries.set(key, response.clone()); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 14 | } |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 15 | }; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 16 | } |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 17 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 18 | function image(body = 'avatar', status = 200): Response { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 19 | return new Response(body, { status, headers: { 'Content-Type': 'image/png' } }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 20 | } |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 21 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 22 | test('avatar cache coalesces simultaneous misses and persists results', async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 23 | const store = memoryCache(); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 24 | let loads = 0; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 25 | let release: ((response: Response) => void) | undefined; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 26 | let signalLoadStarted: (() => void) | undefined; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 27 | const loadStarted = new Promise<void>((resolve) => (signalLoadStarted = resolve)); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 28 | const loader = createAvatarLoader(async () => store, async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 29 | loads += 1; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 30 | signalLoadStarted?.(); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 31 | return new Promise((resolve) => (release = resolve)); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 32 | }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 33 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 34 | const first = loader('/api/media?mxc=avatar', 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 35 | const second = loader('/api/media?mxc=avatar', 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 36 | await loadStarted; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 37 | assert.equal(loads, 1); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 38 | release?.(image()); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 39 | assert.equal(await (await first).text(), 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 40 | assert.equal(await (await second).text(), 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 41 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 42 | const nextWorker = createAvatarLoader(async () => store, async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 43 | loads += 1; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 44 | return image('unexpected'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 45 | }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 46 | assert.equal(await (await nextWorker('/api/media?mxc=avatar', 'avatar')).text(), 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 47 | assert.equal(loads, 1); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 48 | }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 49 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 50 | test('avatar cache retries unsuccessful responses', async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 51 | const store = memoryCache(); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 52 | let loads = 0; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 53 | const loader = createAvatarLoader(async () => store, async () => ++loads === 1 ? image('missing', 404) : image()); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 54 | assert.equal((await loader('/avatar', 'avatar')).status, 404); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 55 | assert.equal(await (await loader('/avatar', 'avatar')).text(), 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 56 | assert.equal(loads, 2); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 57 | }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 58 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 59 | test('avatar cache read failures still return the image', async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 60 | let loads = 0; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 61 | const cache: AvatarCache = { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 62 | async match() { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 63 | throw new Error('read failed'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 64 | }, |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 65 | async put() {} |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 66 | }; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 67 | const loader = createAvatarLoader(async () => cache, async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 68 | loads += 1; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 69 | return image(); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 70 | }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 71 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 72 | assert.equal(await (await loader('/avatar', 'avatar')).text(), 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 73 | assert.equal(loads, 1); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 74 | }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 75 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 76 | test('avatar cache write failures still return the image', async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 77 | let loads = 0; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 78 | const cache: AvatarCache = { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 79 | async match() { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 80 | return undefined; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 81 | }, |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 82 | async put() { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 83 | throw new Error('write failed'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 84 | } |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 85 | }; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 86 | const loader = createAvatarLoader(async () => cache, async () => { |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 87 | loads += 1; |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 88 | return image(); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 89 | }); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 90 | |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 91 | assert.equal(await (await loader('/avatar', 'avatar')).text(), 'avatar'); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 92 | assert.equal(loads, 1); |
132eeceeb362 | atagen | 2026-10-25 03:34 UTC | 93 | }); |