130 lines
3.6 KiB
TypeScript
130 lines
3.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
OptimizationCancelledError,
|
|
OptimizerClient,
|
|
} from "../../src/optimization/optimizer-client";
|
|
import type {
|
|
OptimizationRequest,
|
|
OptimizationResult,
|
|
} from "../../src/optimization/optimization.types";
|
|
|
|
class TestWorker extends EventTarget {
|
|
static instances: TestWorker[] = [];
|
|
|
|
readonly messages: unknown[] = [];
|
|
terminateCount = 0;
|
|
|
|
constructor() {
|
|
super();
|
|
TestWorker.instances.push(this);
|
|
}
|
|
|
|
postMessage(message: unknown): void {
|
|
this.messages.push(message);
|
|
}
|
|
|
|
terminate(): void {
|
|
this.terminateCount += 1;
|
|
}
|
|
|
|
respond(response: OptimizationResult): void {
|
|
this.dispatchEvent(new MessageEvent("message", { data: response }));
|
|
}
|
|
}
|
|
|
|
function resultFor(worker: TestWorker, source = "<svg/>"): OptimizationResult {
|
|
const request = worker.messages[0] as OptimizationRequest;
|
|
return {
|
|
type: "result",
|
|
jobId: request.jobId,
|
|
source,
|
|
profile: request.profile,
|
|
optionalPlugins: request.optionalPlugins,
|
|
inputBytes: source.length,
|
|
outputBytes: source.length,
|
|
elapsedMs: 1,
|
|
};
|
|
}
|
|
|
|
describe("OptimizerClient cancellation", () => {
|
|
beforeEach(() => {
|
|
TestWorker.instances = [];
|
|
vi.useFakeTimers();
|
|
vi.stubGlobal("Worker", TestWorker);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("rejects a cancelled job immediately and cleans up exactly once", async () => {
|
|
const client = new OptimizerClient();
|
|
const pending = client.optimize("<svg/>", "conservative");
|
|
const rejected = expect(pending).rejects.toBeInstanceOf(
|
|
OptimizationCancelledError,
|
|
);
|
|
const worker = TestWorker.instances[0]!;
|
|
|
|
expect(vi.getTimerCount()).toBe(1);
|
|
client.cancel();
|
|
client.cancel();
|
|
|
|
await rejected;
|
|
expect(worker.terminateCount).toBe(1);
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
|
|
worker.respond(resultFor(worker, '<svg id="late"/>'));
|
|
expect(worker.terminateCount).toBe(1);
|
|
});
|
|
|
|
it("promptly rejects a superseded job while the replacement can finish", async () => {
|
|
const client = new OptimizerClient();
|
|
const first = client.optimize('<svg id="first"/>', "conservative");
|
|
const firstRejected = expect(first).rejects.toBeInstanceOf(
|
|
OptimizationCancelledError,
|
|
);
|
|
const firstWorker = TestWorker.instances[0]!;
|
|
|
|
const second = client.optimize('<svg id="second"/>', "standard");
|
|
const secondWorker = TestWorker.instances[1]!;
|
|
await firstRejected;
|
|
|
|
expect(firstWorker.terminateCount).toBe(1);
|
|
expect(secondWorker.terminateCount).toBe(0);
|
|
expect(vi.getTimerCount()).toBe(1);
|
|
|
|
const expected = resultFor(secondWorker, '<svg id="optimized"/>');
|
|
secondWorker.respond(expected);
|
|
await expect(second).resolves.toEqual(expected);
|
|
expect(secondWorker.terminateCount).toBe(1);
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
|
|
client.cancel();
|
|
expect(secondWorker.terminateCount).toBe(1);
|
|
});
|
|
|
|
it("uses the same race-safe cleanup for AbortSignal cancellation", async () => {
|
|
const controller = new AbortController();
|
|
const client = new OptimizerClient();
|
|
const pending = client.optimize(
|
|
"<svg/>",
|
|
"aggressive",
|
|
[],
|
|
controller.signal,
|
|
);
|
|
const rejected = expect(pending).rejects.toBeInstanceOf(
|
|
OptimizationCancelledError,
|
|
);
|
|
const worker = TestWorker.instances[0]!;
|
|
|
|
controller.abort();
|
|
await rejected;
|
|
|
|
expect(worker.terminateCount).toBe(1);
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
client.cancel();
|
|
expect(worker.terminateCount).toBe(1);
|
|
});
|
|
});
|