mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-19 21:11:45 +10:00
fix(web): stop custom styles from sticking on Checking (#3283)
Concurrent compile requests from editor intelligence were rejecting in-flight edit compiles as stale, and the store swallowed that rejection without leaving compiling. Resolve all compile results and surface compile failures as an error status so styles can apply again. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor Agent
parent
186c400ab7
commit
4a8f87ab8f
@@ -741,6 +741,97 @@ describe("stylesheet store runtime", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not leave Checking stuck when compile rejects for the current edit", async () => {
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: () => Promise.reject(new Error("Discarded stale stylesheet compiler result.")),
|
||||
preflight: vi.fn(),
|
||||
mutate: vi.fn(),
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("edited source");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().status).not.toBe("compiling");
|
||||
expect(runtime.store.getState().status).toBe("error");
|
||||
});
|
||||
|
||||
it("finishes an edit when refreshIntelligence interleaves through the shared compile client", async () => {
|
||||
const { createCompileWorkerClient } = await import("./worker-client");
|
||||
const listeners = new Map<string, Set<EventListener>>();
|
||||
const fake = {
|
||||
postMessage: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
addEventListener: vi.fn((type: string, listener: EventListener) => {
|
||||
const bucket = listeners.get(type) ?? new Set();
|
||||
bucket.add(listener);
|
||||
listeners.set(type, bucket);
|
||||
}),
|
||||
removeEventListener: vi.fn((type: string, listener: EventListener) => {
|
||||
listeners.get(type)?.delete(listener);
|
||||
}),
|
||||
emit(data: unknown) {
|
||||
for (const listener of listeners.get("message") ?? []) {
|
||||
(listener as (event: MessageEvent) => void)(new MessageEvent("message", { data }));
|
||||
}
|
||||
},
|
||||
};
|
||||
const client = createCompileWorkerClient(() => fake);
|
||||
const runtime = createStylesheetStoreRuntime({
|
||||
resumeId: "resume-1",
|
||||
initial,
|
||||
resumeData: defaultResumeData,
|
||||
debounceMs: 0,
|
||||
compile: client.compile,
|
||||
preflight: async ({ editGeneration }) => ({
|
||||
type: "preflight_result",
|
||||
requestId: editGeneration,
|
||||
editGeneration,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [], pdf: new ArrayBuffer(1) },
|
||||
}),
|
||||
mutate: async ({ editGeneration }) => ({
|
||||
stylesheet: stylesheet("edited source"),
|
||||
revision: 4,
|
||||
renderDataVersion: 7,
|
||||
editGeneration,
|
||||
diagnostics: [],
|
||||
}),
|
||||
});
|
||||
|
||||
runtime.store.getState().setSourceText("edited source");
|
||||
await vi.runAllTimersAsync();
|
||||
expect(runtime.store.getState().status).toBe("compiling");
|
||||
expect(fake.postMessage).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Mobile Design remount / accordion reopen refreshes intelligence while the edit is compiling.
|
||||
runtime.store.getState().refreshIntelligence();
|
||||
expect(fake.postMessage).toHaveBeenCalledTimes(2);
|
||||
|
||||
fake.emit({
|
||||
type: "compile_result",
|
||||
requestId: 1,
|
||||
editGeneration: 1,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
colorTokens: [],
|
||||
});
|
||||
fake.emit({
|
||||
type: "compile_result",
|
||||
requestId: 2,
|
||||
editGeneration: 1,
|
||||
program: { languageVersion: 1, rules: [] },
|
||||
diagnostics: [],
|
||||
colorTokens: [],
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(runtime.store.getState().status).toBe("applied");
|
||||
expect(runtime.store.getState().applied.text).toBe("edited source");
|
||||
});
|
||||
|
||||
it("terminates both worker clients and clears the store on cleanup", () => {
|
||||
const destroy = vi.fn();
|
||||
let mutationSignal: AbortSignal | undefined;
|
||||
|
||||
@@ -349,6 +349,9 @@ export function createStylesheetStoreRuntime(options: CreateStylesheetStoreRunti
|
||||
compileInput(resumeData, source, candidate.generation, editorMetadata.semanticTree),
|
||||
);
|
||||
} catch {
|
||||
if (candidateValidationEpoch !== validationEpoch) return;
|
||||
if (destroyed || candidate.generation !== store.getState().editGeneration) return;
|
||||
patch({ status: "error" });
|
||||
return;
|
||||
}
|
||||
if (candidateValidationEpoch !== validationEpoch) return;
|
||||
|
||||
@@ -5,20 +5,33 @@ import { createCompileWorkerClient, createPreflightWorkerClient } from "./worker
|
||||
type Listener = (event: MessageEvent) => void;
|
||||
|
||||
function worker() {
|
||||
const listeners = new Set<Listener>();
|
||||
const listeners = new Map<string, Set<EventListener>>();
|
||||
return {
|
||||
postMessage: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
addEventListener: vi.fn((_type: string, listener: Listener) => listeners.add(listener)),
|
||||
removeEventListener: vi.fn((_type: string, listener: Listener) => listeners.delete(listener)),
|
||||
addEventListener: vi.fn((type: string, listener: EventListener) => {
|
||||
const bucket = listeners.get(type) ?? new Set();
|
||||
bucket.add(listener);
|
||||
listeners.set(type, bucket);
|
||||
}),
|
||||
removeEventListener: vi.fn((type: string, listener: EventListener) => {
|
||||
listeners.get(type)?.delete(listener);
|
||||
}),
|
||||
emit(data: unknown) {
|
||||
for (const listener of listeners) listener(new MessageEvent("message", { data }));
|
||||
for (const listener of listeners.get("message") ?? []) {
|
||||
(listener as Listener)(new MessageEvent("message", { data }));
|
||||
}
|
||||
},
|
||||
emitError(event: ErrorEvent) {
|
||||
for (const listener of listeners.get("error") ?? []) {
|
||||
listener(event);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("stylesheet worker clients", () => {
|
||||
it("rejects stale compiler results by request id", async () => {
|
||||
it("resolves older compiler results so callers can generation-check without aborting", async () => {
|
||||
const fake = worker();
|
||||
const client = createCompileWorkerClient(() => fake);
|
||||
const first = client.compile({ editGeneration: 1 } as never);
|
||||
@@ -27,8 +40,18 @@ describe("stylesheet worker clients", () => {
|
||||
fake.emit({ type: "compile_result", requestId: 1, editGeneration: 1, program: null, diagnostics: [] });
|
||||
fake.emit({ type: "compile_result", requestId: 2, editGeneration: 2, program: null, diagnostics: [] });
|
||||
|
||||
await expect(first).rejects.toThrow("stale");
|
||||
await expect(second).resolves.toMatchObject({ requestId: 2 });
|
||||
await expect(first).resolves.toMatchObject({ requestId: 1, editGeneration: 1 });
|
||||
await expect(second).resolves.toMatchObject({ requestId: 2, editGeneration: 2 });
|
||||
});
|
||||
|
||||
it("rejects pending compiles when the worker reports an error", async () => {
|
||||
const fake = worker();
|
||||
const client = createCompileWorkerClient(() => fake);
|
||||
const pending = client.compile({ editGeneration: 1 } as never);
|
||||
|
||||
fake.emitError({ message: "Failed to load compiler worker" } as ErrorEvent);
|
||||
|
||||
await expect(pending).rejects.toThrow("Failed to load compiler worker");
|
||||
});
|
||||
|
||||
it("terminates and recreates a timed-out preflight worker", async () => {
|
||||
@@ -68,7 +91,9 @@ describe("stylesheet worker clients", () => {
|
||||
client.warmup();
|
||||
|
||||
expect(createWorker).toHaveBeenCalledOnce();
|
||||
expect(fake.addEventListener).toHaveBeenCalledOnce();
|
||||
expect(fake.addEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(fake.addEventListener).toHaveBeenCalledWith("message", expect.any(Function));
|
||||
expect(fake.addEventListener).toHaveBeenCalledWith("error", expect.any(Function));
|
||||
expect(fake.postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -10,12 +10,15 @@ import type {
|
||||
} from "./protocol";
|
||||
|
||||
type WorkerListener = (event: MessageEvent<unknown>) => void;
|
||||
type WorkerErrorListener = (event: ErrorEvent) => void;
|
||||
|
||||
export type StylesheetWorker = {
|
||||
postMessage(message: unknown, transfer?: Transferable[]): void;
|
||||
terminate(): void;
|
||||
addEventListener(type: "message", listener: WorkerListener): void;
|
||||
addEventListener(type: "error", listener: WorkerErrorListener): void;
|
||||
removeEventListener(type: "message", listener: WorkerListener): void;
|
||||
removeEventListener(type: "error", listener: WorkerErrorListener): void;
|
||||
};
|
||||
|
||||
type Pending<T> = {
|
||||
@@ -34,13 +37,17 @@ export function createCompileWorkerClient(createWorker: () => StylesheetWorker)
|
||||
const request = pending.get(response.requestId);
|
||||
if (!request) return;
|
||||
pending.delete(response.requestId);
|
||||
if (response.requestId !== latestRequestId) {
|
||||
request.reject(new Error("Discarded stale stylesheet compiler result."));
|
||||
return;
|
||||
}
|
||||
// Resolve every in-flight compile. Callers already generation-check; rejecting "stale"
|
||||
// results aborts the edit pipeline and can leave the editor stuck on Checking.
|
||||
request.resolve(response);
|
||||
};
|
||||
const onError: WorkerErrorListener = (event) => {
|
||||
const error = new Error(event.message || "Stylesheet compiler worker failed to load.");
|
||||
for (const request of pending.values()) request.reject(error);
|
||||
pending.clear();
|
||||
};
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.addEventListener("error", onError);
|
||||
|
||||
return {
|
||||
compile(input: CompileWorkerInput): Promise<CompileWorkerResponse> {
|
||||
@@ -53,6 +60,7 @@ export function createCompileWorkerClient(createWorker: () => StylesheetWorker)
|
||||
},
|
||||
destroy() {
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
worker.terminate();
|
||||
for (const request of pending.values()) request.reject(new Error("Stylesheet compiler worker was terminated."));
|
||||
pending.clear();
|
||||
@@ -120,10 +128,14 @@ export function createPreflightWorkerClient(
|
||||
pending.delete(response.requestId);
|
||||
request.resolve(response);
|
||||
};
|
||||
const onError: WorkerErrorListener = () => {
|
||||
terminate();
|
||||
};
|
||||
|
||||
const terminate = () => {
|
||||
if (!worker) return;
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
worker.terminate();
|
||||
worker = undefined;
|
||||
ready = false;
|
||||
@@ -140,6 +152,7 @@ export function createPreflightWorkerClient(
|
||||
if (readiness) return readiness.promise;
|
||||
worker = createWorker();
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.addEventListener("error", onError);
|
||||
let resolve!: (value: StylesheetWorker) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const promise = new Promise<StylesheetWorker>((resolvePromise, rejectPromise) => {
|
||||
|
||||
Reference in New Issue
Block a user