fix(image-pipeline): close cleared in-flight source bitmaps
This commit is contained in:
@@ -3,6 +3,7 @@ export const SOURCE_BITMAP_CACHE_MAX_ENTRIES = 32;
|
|||||||
type CacheEntry = {
|
type CacheEntry = {
|
||||||
promise: Promise<ImageBitmap>;
|
promise: Promise<ImageBitmap>;
|
||||||
bitmap?: ImageBitmap;
|
bitmap?: ImageBitmap;
|
||||||
|
released?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageBitmapCache = new Map<string, CacheEntry>();
|
const imageBitmapCache = new Map<string, CacheEntry>();
|
||||||
@@ -29,6 +30,7 @@ function deleteCacheEntry(sourceUrl: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.released = true;
|
||||||
imageBitmapCache.delete(sourceUrl);
|
imageBitmapCache.delete(sourceUrl);
|
||||||
closeBitmap(entry.bitmap);
|
closeBitmap(entry.bitmap);
|
||||||
}
|
}
|
||||||
@@ -77,6 +79,12 @@ function getOrCreateSourceBitmapPromise(sourceUrl: string): Promise<ImageBitmap>
|
|||||||
|
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const bitmap = await createImageBitmap(blob);
|
const bitmap = await createImageBitmap(blob);
|
||||||
|
|
||||||
|
if (entry.released || imageBitmapCache.get(sourceUrl) !== entry) {
|
||||||
|
closeBitmap(bitmap);
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
entry.bitmap = bitmap;
|
entry.bitmap = bitmap;
|
||||||
evictIfNeeded(sourceUrl);
|
evictIfNeeded(sourceUrl);
|
||||||
return bitmap;
|
return bitmap;
|
||||||
|
|||||||
@@ -253,4 +253,49 @@ describe("loadSourceBitmap", () => {
|
|||||||
expect(bitmap.close).toHaveBeenCalledTimes(1);
|
expect(bitmap.close).toHaveBeenCalledTimes(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("closes a decoded bitmap that resolves after its cache entry was cleared in flight", async () => {
|
||||||
|
const subject = (await importSubject()) as typeof import("@/lib/image-pipeline/source-loader") & {
|
||||||
|
clearSourceBitmapCache?: () => void;
|
||||||
|
};
|
||||||
|
const { clearSourceBitmapCache, loadSourceBitmap } = subject;
|
||||||
|
|
||||||
|
expect(clearSourceBitmapCache).toBeTypeOf("function");
|
||||||
|
|
||||||
|
const firstBitmap = { close: vi.fn() } as unknown as ImageBitmap;
|
||||||
|
const secondBitmap = { close: vi.fn() } as unknown as ImageBitmap;
|
||||||
|
const decodeDeferred = createDeferred<ImageBitmap>();
|
||||||
|
|
||||||
|
vi.stubGlobal(
|
||||||
|
"createImageBitmap",
|
||||||
|
vi
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce(async () => await decodeDeferred.promise)
|
||||||
|
.mockResolvedValueOnce(secondBitmap),
|
||||||
|
);
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn().mockImplementation(async (input: string | URL | Request) => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
blob: vi.fn().mockResolvedValue(new Blob([String(input)])),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceUrl = "https://cdn.example.com/source-in-flight.png";
|
||||||
|
const pendingLoad = loadSourceBitmap(sourceUrl);
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(createImageBitmap).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
clearSourceBitmapCache!();
|
||||||
|
decodeDeferred.resolve(firstBitmap);
|
||||||
|
|
||||||
|
await expect(pendingLoad).resolves.toBe(firstBitmap);
|
||||||
|
expect(firstBitmap.close).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await expect(loadSourceBitmap(sourceUrl)).resolves.toBe(secondBitmap);
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user