fix(image-pipeline): dedupe in-flight preview requests
This commit is contained in:
@@ -40,6 +40,10 @@ type WorkerMessage =
|
||||
| {
|
||||
kind: "preview" | "full";
|
||||
requestId: number;
|
||||
payload?: {
|
||||
previewWidth?: number;
|
||||
includeHistogram?: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
kind: "cancel";
|
||||
@@ -65,10 +69,39 @@ class FakeWorker {
|
||||
}
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((innerResolve, innerReject) => {
|
||||
resolve = innerResolve;
|
||||
reject = innerReject;
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
resolve,
|
||||
reject,
|
||||
};
|
||||
}
|
||||
|
||||
describe("worker-client fallbacks", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
vi.stubGlobal(
|
||||
"ImageData",
|
||||
class ImageData {
|
||||
data: Uint8ClampedArray;
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(data: Uint8ClampedArray, width: number, height: number) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
},
|
||||
);
|
||||
previewRendererMocks.renderPreview.mockReset();
|
||||
bridgeMocks.renderFull.mockReset();
|
||||
previewRendererMocks.renderPreview.mockResolvedValue({
|
||||
@@ -185,4 +218,191 @@ describe("worker-client fallbacks", () => {
|
||||
expect(previewResult.width).toBe(16);
|
||||
expect(fullResult.format).toBe("png");
|
||||
});
|
||||
|
||||
it("shares one worker preview execution across identical requests", async () => {
|
||||
const workerMessages: WorkerMessage[] = [];
|
||||
FakeWorker.behavior = (worker, message) => {
|
||||
workerMessages.push(message);
|
||||
if (message.kind !== "preview") {
|
||||
return;
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
worker.onmessage?.({
|
||||
data: {
|
||||
kind: "preview-result",
|
||||
requestId: message.requestId,
|
||||
payload: {
|
||||
width: 8,
|
||||
height: 4,
|
||||
histogram: emptyHistogram(),
|
||||
pixels: new Uint8ClampedArray(8 * 4 * 4).buffer,
|
||||
},
|
||||
},
|
||||
} as MessageEvent);
|
||||
});
|
||||
};
|
||||
vi.stubGlobal("Worker", FakeWorker as unknown as typeof Worker);
|
||||
|
||||
const { renderPreviewWithWorkerFallback } = await import("@/lib/image-pipeline/worker-client");
|
||||
|
||||
const request = {
|
||||
sourceUrl: "https://cdn.example.com/source.png",
|
||||
steps: [],
|
||||
previewWidth: 128,
|
||||
includeHistogram: true,
|
||||
} as const;
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
renderPreviewWithWorkerFallback(request),
|
||||
renderPreviewWithWorkerFallback(request),
|
||||
]);
|
||||
|
||||
expect(workerMessages.filter((message) => message.kind === "preview")).toHaveLength(1);
|
||||
expect(previewRendererMocks.renderPreview).not.toHaveBeenCalled();
|
||||
expect(first.width).toBe(8);
|
||||
expect(second.width).toBe(8);
|
||||
});
|
||||
|
||||
it("creates separate preview executions when width or histogram settings differ", async () => {
|
||||
const workerMessages: WorkerMessage[] = [];
|
||||
FakeWorker.behavior = (worker, message) => {
|
||||
workerMessages.push(message);
|
||||
if (message.kind !== "preview") {
|
||||
return;
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
worker.onmessage?.({
|
||||
data: {
|
||||
kind: "preview-result",
|
||||
requestId: message.requestId,
|
||||
payload: {
|
||||
width: message.payload?.previewWidth ?? 1,
|
||||
height: 4,
|
||||
histogram: emptyHistogram(),
|
||||
pixels: new Uint8ClampedArray((message.payload?.previewWidth ?? 1) * 4 * 4).buffer,
|
||||
},
|
||||
},
|
||||
} as MessageEvent);
|
||||
});
|
||||
};
|
||||
vi.stubGlobal("Worker", FakeWorker as unknown as typeof Worker);
|
||||
|
||||
const { renderPreviewWithWorkerFallback } = await import("@/lib/image-pipeline/worker-client");
|
||||
|
||||
await Promise.all([
|
||||
renderPreviewWithWorkerFallback({
|
||||
sourceUrl: "https://cdn.example.com/source.png",
|
||||
steps: [],
|
||||
previewWidth: 128,
|
||||
includeHistogram: false,
|
||||
}),
|
||||
renderPreviewWithWorkerFallback({
|
||||
sourceUrl: "https://cdn.example.com/source.png",
|
||||
steps: [],
|
||||
previewWidth: 256,
|
||||
includeHistogram: false,
|
||||
}),
|
||||
renderPreviewWithWorkerFallback({
|
||||
sourceUrl: "https://cdn.example.com/source.png",
|
||||
steps: [],
|
||||
previewWidth: 128,
|
||||
includeHistogram: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(workerMessages.filter((message) => message.kind === "preview")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("removes aborted subscribers without canceling surviving identical preview consumers", async () => {
|
||||
const workerMessages: WorkerMessage[] = [];
|
||||
const previewStarted = createDeferred<void>();
|
||||
|
||||
FakeWorker.behavior = (worker, message) => {
|
||||
workerMessages.push(message);
|
||||
if (message.kind !== "preview") {
|
||||
return;
|
||||
}
|
||||
|
||||
previewStarted.resolve();
|
||||
queueMicrotask(() => {
|
||||
worker.onmessage?.({
|
||||
data: {
|
||||
kind: "preview-result",
|
||||
requestId: message.requestId,
|
||||
payload: {
|
||||
width: 8,
|
||||
height: 4,
|
||||
histogram: emptyHistogram(),
|
||||
pixels: new Uint8ClampedArray(8 * 4 * 4).buffer,
|
||||
},
|
||||
},
|
||||
} as MessageEvent);
|
||||
});
|
||||
};
|
||||
vi.stubGlobal("Worker", FakeWorker as unknown as typeof Worker);
|
||||
|
||||
const { renderPreviewWithWorkerFallback } = await import("@/lib/image-pipeline/worker-client");
|
||||
|
||||
const firstController = new AbortController();
|
||||
const secondController = new AbortController();
|
||||
const request = {
|
||||
sourceUrl: "https://cdn.example.com/source.png",
|
||||
steps: [],
|
||||
previewWidth: 128,
|
||||
includeHistogram: true,
|
||||
} as const;
|
||||
|
||||
const firstPromise = renderPreviewWithWorkerFallback({
|
||||
...request,
|
||||
signal: firstController.signal,
|
||||
});
|
||||
const secondPromise = renderPreviewWithWorkerFallback({
|
||||
...request,
|
||||
signal: secondController.signal,
|
||||
});
|
||||
|
||||
await previewStarted.promise;
|
||||
firstController.abort();
|
||||
|
||||
await expect(firstPromise).rejects.toMatchObject({ name: "AbortError" });
|
||||
await expect(secondPromise).resolves.toMatchObject({ width: 8, height: 4 });
|
||||
expect(workerMessages.filter((message) => message.kind === "preview")).toHaveLength(1);
|
||||
expect(workerMessages.filter((message) => message.kind === "cancel")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("shares one fallback preview execution across identical requests", async () => {
|
||||
vi.stubGlobal("Worker", undefined);
|
||||
const deferred = createDeferred<Awaited<ReturnType<typeof previewRendererMocks.renderPreview>>>();
|
||||
previewRendererMocks.renderPreview.mockReturnValueOnce(deferred.promise);
|
||||
|
||||
const { renderPreviewWithWorkerFallback } = await import("@/lib/image-pipeline/worker-client");
|
||||
|
||||
const firstPromise = renderPreviewWithWorkerFallback({
|
||||
sourceUrl: "https://cdn.example.com/source.png",
|
||||
steps: [],
|
||||
previewWidth: 128,
|
||||
includeHistogram: false,
|
||||
});
|
||||
const secondPromise = renderPreviewWithWorkerFallback({
|
||||
sourceUrl: "https://cdn.example.com/source.png",
|
||||
steps: [],
|
||||
previewWidth: 128,
|
||||
includeHistogram: false,
|
||||
});
|
||||
|
||||
expect(previewRendererMocks.renderPreview).toHaveBeenCalledTimes(1);
|
||||
|
||||
deferred.resolve({
|
||||
width: 16,
|
||||
height: 16,
|
||||
imageData: { data: new Uint8ClampedArray(16 * 16 * 4) },
|
||||
histogram: emptyHistogram(),
|
||||
});
|
||||
|
||||
const [first, second] = await Promise.all([firstPromise, secondPromise]);
|
||||
expect(first.width).toBe(16);
|
||||
expect(second.width).toBe(16);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user