fix(image-pipeline): bound source bitmap cache lifecycle

This commit is contained in:
Matthias
2026-04-04 11:37:26 +02:00
parent 77f8736579
commit c0534e04e0
2 changed files with 148 additions and 5 deletions

View File

@@ -167,4 +167,90 @@ describe("loadSourceBitmap", () => {
expect(succeedingResponse.blob).toHaveBeenCalledTimes(1);
expect(createImageBitmap).toHaveBeenCalledTimes(1);
});
it("evicts the least-recently-used bitmap once the cache limit is exceeded", async () => {
const subject = (await importSubject()) as typeof import("@/lib/image-pipeline/source-loader") & {
SOURCE_BITMAP_CACHE_MAX_ENTRIES?: number;
};
const { SOURCE_BITMAP_CACHE_MAX_ENTRIES, loadSourceBitmap } = subject;
expect(SOURCE_BITMAP_CACHE_MAX_ENTRIES).toBeTypeOf("number");
vi.stubGlobal(
"fetch",
vi.fn().mockImplementation(async (input: string | URL | Request) => ({
ok: true,
status: 200,
blob: vi.fn().mockResolvedValue(new Blob([String(input)])),
})),
);
const urls = Array.from(
{ length: SOURCE_BITMAP_CACHE_MAX_ENTRIES! + 1 },
(_, index) => `https://cdn.example.com/source-${index}.png`,
);
for (const url of urls) {
await expect(loadSourceBitmap(url)).resolves.toBe(bitmap);
}
expect(fetch).toHaveBeenCalledTimes(urls.length);
await expect(loadSourceBitmap(urls[urls.length - 1])).resolves.toBe(bitmap);
expect(fetch).toHaveBeenCalledTimes(urls.length);
await expect(loadSourceBitmap(urls[0])).resolves.toBe(bitmap);
expect(fetch).toHaveBeenCalledTimes(urls.length + 1);
});
it("closes evicted bitmaps and disposes the remaining cache explicitly", async () => {
const subject = (await importSubject()) as typeof import("@/lib/image-pipeline/source-loader") & {
SOURCE_BITMAP_CACHE_MAX_ENTRIES?: number;
clearSourceBitmapCache?: () => void;
};
const { SOURCE_BITMAP_CACHE_MAX_ENTRIES, clearSourceBitmapCache, loadSourceBitmap } = subject;
expect(SOURCE_BITMAP_CACHE_MAX_ENTRIES).toBeGreaterThanOrEqual(2);
expect(clearSourceBitmapCache).toBeTypeOf("function");
const bitmaps = Array.from(
{ length: SOURCE_BITMAP_CACHE_MAX_ENTRIES! + 1 },
() => ({ close: vi.fn() }) as unknown as ImageBitmap,
);
let bitmapIndex = 0;
vi.stubGlobal(
"createImageBitmap",
vi.fn().mockImplementation(async () => bitmaps[bitmapIndex++]),
);
vi.stubGlobal(
"fetch",
vi.fn().mockImplementation(async (input: string | URL | Request) => ({
ok: true,
status: 200,
blob: vi.fn().mockResolvedValue(new Blob([String(input)])),
})),
);
const urls = Array.from(
{ length: SOURCE_BITMAP_CACHE_MAX_ENTRIES! + 1 },
(_, index) => `https://cdn.example.com/source-${index}.png`,
);
for (const [index, url] of urls.entries()) {
await expect(loadSourceBitmap(url)).resolves.toBe(bitmaps[index]);
}
expect(bitmaps[0].close).toHaveBeenCalledTimes(1);
for (const bitmap of bitmaps.slice(1)) {
expect(bitmap.close).not.toHaveBeenCalled();
}
clearSourceBitmapCache!();
expect(bitmaps[0].close).toHaveBeenCalledTimes(1);
for (const bitmap of bitmaps.slice(1)) {
expect(bitmap.close).toHaveBeenCalledTimes(1);
}
});
});