mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-21 05:51:46 +10:00
refactor(stylesheet): move Semantic CSS to the browser (#3329)
This commit is contained in:
@@ -131,10 +131,10 @@ describe("createApp", () => {
|
||||
it("uses the transport address for public PDF fallback despite rotated forwarding headers", async () => {
|
||||
const { createApp } = await import("./app");
|
||||
const app = createApp();
|
||||
const first = new Request("http://localhost:3001/api/resumes/jane/resume/pdf?reason=render-data-hash", {
|
||||
const first = new Request("http://localhost:3001/api/resumes/jane/resume/pdf", {
|
||||
headers: { "x-forwarded-for": "198.51.100.1" },
|
||||
});
|
||||
const rotated = new Request("http://localhost:3001/api/resumes/jane/resume/pdf?reason=render-data-hash", {
|
||||
const rotated = new Request("http://localhost:3001/api/resumes/jane/resume/pdf", {
|
||||
headers: { "x-forwarded-for": "198.51.100.2" },
|
||||
});
|
||||
const env = transportEnv("203.0.113.9");
|
||||
@@ -159,8 +159,8 @@ describe("createApp", () => {
|
||||
const unknownRpcRequest = new Request("http://localhost:3001/api/rpc", {
|
||||
headers: { "cf-connecting-ip": "198.51.100.2" },
|
||||
});
|
||||
const trustedOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume/style-projection");
|
||||
const unknownOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume/style-projection");
|
||||
const trustedOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume");
|
||||
const unknownOpenApiRequest = new Request("http://localhost:3001/api/openapi/resumes/jane/resume");
|
||||
|
||||
await app.fetch(trustedRpcRequest, transportEnv("203.0.113.9"));
|
||||
await app.fetch(unknownRpcRequest);
|
||||
|
||||
@@ -6,16 +6,6 @@ const mocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("@reactive-resume/api/features/resume/public-pdf", () => ({
|
||||
createPublicResumePdf: mocks.createPublicResumePdf,
|
||||
PUBLIC_RESUME_PDF_MISMATCH_REASONS: [
|
||||
"missing-projection",
|
||||
"format-version",
|
||||
"language-version",
|
||||
"semantic-tree-version",
|
||||
"registry-fingerprint",
|
||||
"adapter-fingerprint",
|
||||
"render-data-hash",
|
||||
"invalid-projection",
|
||||
],
|
||||
}));
|
||||
|
||||
const { handlePublicResumePdf } = await import("./public-resume-pdf");
|
||||
@@ -24,18 +14,15 @@ const trustedClient = "203.0.113.9";
|
||||
describe("handlePublicResumePdf", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it("returns the authorized fallback PDF with strict mismatch metadata and cache policy", async () => {
|
||||
it("returns the authorized on-demand PDF without forwarding compatibility metadata", async () => {
|
||||
const body = new File(["%PDF"], "Ada_Lovelace.pdf", { type: "text/plain" });
|
||||
mocks.createPublicResumePdf.mockResolvedValueOnce({
|
||||
body,
|
||||
filename: "Ada_Lovelace.pdf",
|
||||
});
|
||||
const registry = "0".repeat(64);
|
||||
const adapter = "1".repeat(64);
|
||||
const request = new Request(
|
||||
`https://example.com/api/resumes/jane/resume/pdf?reason=render-data-hash®istryFingerprint=${registry}&adapterFingerprint=${adapter}`,
|
||||
{ headers: { "x-forwarded-for": "203.0.113.7" } },
|
||||
);
|
||||
const request = new Request("https://example.com/api/resumes/jane/resume/pdf?ignored=true", {
|
||||
headers: { "x-forwarded-for": "203.0.113.7" },
|
||||
});
|
||||
|
||||
const response = await handlePublicResumePdf(request, "jane", "resume", trustedClient);
|
||||
|
||||
@@ -50,13 +37,10 @@ describe("handlePublicResumePdf", () => {
|
||||
slug: "resume",
|
||||
requestHeaders: request.headers,
|
||||
trustedClient,
|
||||
mismatchReason: "render-data-hash",
|
||||
clientRegistryFingerprint: registry,
|
||||
clientAdapterFingerprint: adapter,
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults a missing mismatch reason and keeps password/private responses uncacheable", async () => {
|
||||
it("keeps password and private responses uncacheable", async () => {
|
||||
mocks.createPublicResumePdf.mockResolvedValueOnce({
|
||||
body: new File(["%PDF"], "resume.pdf", { type: "application/pdf" }),
|
||||
filename: "resume.pdf",
|
||||
@@ -66,13 +50,15 @@ describe("handlePublicResumePdf", () => {
|
||||
const response = await handlePublicResumePdf(request, "jane", "resume", trustedClient);
|
||||
|
||||
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
expect(mocks.createPublicResumePdf).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ mismatchReason: "missing-projection" }),
|
||||
);
|
||||
expect(mocks.createPublicResumePdf).toHaveBeenCalledWith({
|
||||
username: "jane",
|
||||
slug: "resume",
|
||||
requestHeaders: request.headers,
|
||||
trustedClient,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ code: "BAD_REQUEST" }, 400],
|
||||
[{ code: "NEED_PASSWORD" }, 401],
|
||||
[{ code: "NOT_FOUND" }, 404],
|
||||
[{ code: "RATE_LIMIT_EXCEEDED" }, 429],
|
||||
@@ -90,20 +76,4 @@ describe("handlePublicResumePdf", () => {
|
||||
expect(response.status).toBe(status);
|
||||
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
});
|
||||
|
||||
it.each(["?reason=private-source", "?registryFingerprint=unsafe", "?adapterFingerprint=unsafe"])(
|
||||
"rejects invalid fallback metadata before the API service",
|
||||
async (search) => {
|
||||
const response = await handlePublicResumePdf(
|
||||
new Request(`https://example.com/api/resumes/jane/resume/pdf${search}`),
|
||||
"jane",
|
||||
"resume",
|
||||
trustedClient,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
||||
expect(mocks.createPublicResumePdf).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,57 +1,28 @@
|
||||
import type { PublicResumePdfMismatchReason } from "@reactive-resume/api/features/resume/public-pdf";
|
||||
import {
|
||||
createPublicResumePdf,
|
||||
PUBLIC_RESUME_PDF_MISMATCH_REASONS,
|
||||
} from "@reactive-resume/api/features/resume/public-pdf";
|
||||
import { createPublicResumePdf } from "@reactive-resume/api/features/resume/public-pdf";
|
||||
|
||||
const noStoreResponse = (body: string, status: number) =>
|
||||
new Response(body, { status, headers: { "Cache-Control": "private, no-store" } });
|
||||
|
||||
const errorStatus = (error: unknown): number => {
|
||||
const code = typeof error === "object" && error && "code" in error ? (error as { code?: unknown }).code : undefined;
|
||||
if (code === "BAD_REQUEST") return 400;
|
||||
if (code === "NEED_PASSWORD") return 401;
|
||||
if (code === "NOT_FOUND") return 404;
|
||||
if (code === "RATE_LIMIT_EXCEEDED") return 429;
|
||||
return 500;
|
||||
};
|
||||
|
||||
const fingerprint = (value: string | null): string | undefined => {
|
||||
if (value === null) return;
|
||||
return /^[a-f0-9]{64}$/.test(value) ? value : undefined;
|
||||
};
|
||||
|
||||
export async function handlePublicResumePdf(
|
||||
request: Request,
|
||||
username: string,
|
||||
slug: string,
|
||||
trustedClient = "unknown",
|
||||
): Promise<Response> {
|
||||
const searchParams = new URL(request.url).searchParams;
|
||||
const rawReason = searchParams.get("reason") ?? "missing-projection";
|
||||
if (!PUBLIC_RESUME_PDF_MISMATCH_REASONS.includes(rawReason as PublicResumePdfMismatchReason)) {
|
||||
return noStoreResponse("Invalid fallback metadata", 400);
|
||||
}
|
||||
const rawRegistryFingerprint = searchParams.get("registryFingerprint");
|
||||
const rawAdapterFingerprint = searchParams.get("adapterFingerprint");
|
||||
const clientRegistryFingerprint = fingerprint(rawRegistryFingerprint);
|
||||
const clientAdapterFingerprint = fingerprint(rawAdapterFingerprint);
|
||||
if (
|
||||
(rawRegistryFingerprint !== null && clientRegistryFingerprint === undefined) ||
|
||||
(rawAdapterFingerprint !== null && clientAdapterFingerprint === undefined)
|
||||
) {
|
||||
return noStoreResponse("Invalid fallback metadata", 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createPublicResumePdf({
|
||||
username,
|
||||
slug,
|
||||
requestHeaders: request.headers,
|
||||
trustedClient,
|
||||
mismatchReason: rawReason as PublicResumePdfMismatchReason,
|
||||
...(clientRegistryFingerprint ? { clientRegistryFingerprint } : {}),
|
||||
...(clientAdapterFingerprint ? { clientAdapterFingerprint } : {}),
|
||||
});
|
||||
|
||||
return new Response(result.body, {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { pathToFileURL } from "node:url";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { env } from "@reactive-resume/env/server";
|
||||
import { createApp } from "./http/app";
|
||||
import { stylesheetPreflightRunner } from "./services/stylesheet-preflight";
|
||||
import { runStartupChecks } from "./startup/checks";
|
||||
|
||||
export { createApp } from "./http/app";
|
||||
@@ -32,10 +31,6 @@ async function main() {
|
||||
console.info(`🚀 Up and running on http://localhost:${info.port}`);
|
||||
},
|
||||
);
|
||||
|
||||
// Load the heavy PDF preflight runtime once, now, so the first semantic-CSS edit
|
||||
// does not pay (and time out on) the cold worker start.
|
||||
stylesheetPreflightRunner.warmup();
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { RPCHandler } from "@orpc/server/fetch";
|
||||
import { BatchHandlerPlugin, RequestHeadersPlugin, StrictGetMethodPlugin } from "@orpc/server/plugins";
|
||||
import router from "@reactive-resume/api/routers";
|
||||
import { mergeResponseHeaders } from "../http/headers";
|
||||
import { stylesheetPreflightRunner } from "../services/stylesheet-preflight";
|
||||
import { getRequestLocale } from "./locale";
|
||||
|
||||
const rpcHandler = new RPCHandler(router, {
|
||||
@@ -24,7 +23,6 @@ export async function handleRpc(request: Request, trustedClient = "unknown") {
|
||||
reqHeaders: request.headers,
|
||||
resHeaders,
|
||||
trustedClient,
|
||||
stylesheetPreflightRunner,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
|
||||
import { createStylesheetPreflightRunner, STYLESHEET_PREFLIGHT_LIMITS } from "./stylesheet-preflight";
|
||||
|
||||
const validStylesheet = {
|
||||
languageVersion: 1,
|
||||
text: "@version 1;",
|
||||
} as const;
|
||||
|
||||
const input = {
|
||||
data: defaultResumeData,
|
||||
template: defaultResumeData.metadata.template,
|
||||
stylesheet: validStylesheet,
|
||||
} as const;
|
||||
|
||||
// Runners now own a long-lived, reused worker; destroy them so no worker thread
|
||||
// outlives its test.
|
||||
const runners: Array<{ destroy(): Promise<void> }> = [];
|
||||
const track = <T extends { destroy(): Promise<void> }>(runner: T): T => {
|
||||
runners.push(runner);
|
||||
return runner;
|
||||
};
|
||||
afterEach(async () => {
|
||||
await Promise.all(runners.splice(0).map((runner) => runner.destroy()));
|
||||
});
|
||||
|
||||
const memoryExhaustionWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
const retained = [];
|
||||
while (true) {
|
||||
const batch = Array.from({ length: 100_000 }, (_, index) => ({ batch: retained.length, index }));
|
||||
retained.push(batch);
|
||||
}
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const failedWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent('throw new Error("sensitive worker details");')}`,
|
||||
);
|
||||
|
||||
// Counts how many preflights this single worker instance served so a reuse test
|
||||
// can prove the worker is not respawned per request.
|
||||
const reuseCountingWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
let served = 0;
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
served += 1;
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: { ok: true, pageCount: 1, byteCount: served, diagnostics: [] },
|
||||
});
|
||||
});
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const delayedSuccessfulWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
setTimeout(() => {
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: {
|
||||
ok: true,
|
||||
pageCount: 1,
|
||||
byteCount: Number(message.input.data.basics.name),
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const delayedReadyWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
setTimeout(() => {
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: { ok: true, pageCount: 1, byteCount: 1, diagnostics: [] },
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
setTimeout(() => parentPort.postMessage({ type: "ready" }), 50);
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const neverCompletesWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
setInterval(() => {}, 1_000);
|
||||
`)}`,
|
||||
);
|
||||
|
||||
const synchronousFailureWorker = new URL("https://example.com/stylesheet-preflight.mjs");
|
||||
|
||||
const numberedInput = (number: number) => ({
|
||||
...input,
|
||||
data: {
|
||||
...input.data,
|
||||
basics: {
|
||||
...input.data.basics,
|
||||
name: String(number),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const invalidInput = () => {
|
||||
const data = structuredClone(defaultResumeData);
|
||||
data.customSections = [
|
||||
{
|
||||
id: "custom-experience",
|
||||
type: "experience",
|
||||
title: "Experience",
|
||||
icon: "",
|
||||
columns: 1,
|
||||
hidden: false,
|
||||
keepTogether: false,
|
||||
startOnNewPage: false,
|
||||
items: [{ id: "summary-shaped-item", hidden: false, content: "<p>Missing company</p>" }],
|
||||
} as never,
|
||||
];
|
||||
return { ...input, data };
|
||||
};
|
||||
|
||||
describe("stylesheet PDF preflight worker", () => {
|
||||
it("keeps the production resource policy fixed and immutable", () => {
|
||||
expect(STYLESHEET_PREFLIGHT_LIMITS).toEqual({
|
||||
timeoutMs: 30_000,
|
||||
maxPages: 20,
|
||||
maxBytes: 10_000_000,
|
||||
maxPageWidthPt: 2_000,
|
||||
maxPageHeightPt: 20_000,
|
||||
maxPageAreaPt2: 20_000_000,
|
||||
maxOldGenerationMb: 256,
|
||||
maxConcurrentWorkers: 1,
|
||||
maxQueuedRequests: 32,
|
||||
});
|
||||
expect(Object.isFrozen(STYLESHEET_PREFLIGHT_LIMITS)).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts a bounded candidate render in an isolated worker", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 15_000 }));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
ok: true,
|
||||
pageCount: 1,
|
||||
byteCount: expect.any(Number),
|
||||
}),
|
||||
);
|
||||
if (!result.ok) throw new Error(`Expected successful preflight, received ${result.code}.`);
|
||||
expect(result.byteCount).toBeGreaterThan(0);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
}, 45_000);
|
||||
|
||||
it("reuses one warm worker across sequential requests instead of cold-starting each one", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({}, reuseCountingWorker));
|
||||
|
||||
const first = await runner.run(input);
|
||||
const second = await runner.run(input);
|
||||
const third = await runner.run(input);
|
||||
|
||||
// A single reused worker increments its per-instance counter; a per-request
|
||||
// worker would report byteCount 1 every time.
|
||||
expect([first, second, third].map((result) => (result.ok ? result.byteCount : -1))).toEqual([1, 2, 3]);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves structured resume-data failures across the worker boundary", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 15_000 }));
|
||||
|
||||
const result = runner.run(invalidInput());
|
||||
|
||||
await expect(result).rejects.toMatchObject({
|
||||
name: "ZodError",
|
||||
issues: expect.arrayContaining([expect.objectContaining({ path: ["customSections", 0, "items", 0, "company"] })]),
|
||||
});
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
}, 20_000);
|
||||
|
||||
it("terminates a worker when the render exceeds its deadline", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 1 }, neverCompletesWorker));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_TIMEOUT" }));
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
|
||||
it("starts the authored render deadline after the worker runtime is ready", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 20 }, delayedReadyWorker));
|
||||
|
||||
await expect(runner.run(input)).resolves.toEqual(expect.objectContaining({ ok: true, pageCount: 1 }));
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
});
|
||||
|
||||
it("returns deterministic output byte and page limit codes", async () => {
|
||||
const byteRunner = track(createStylesheetPreflightRunner({ maxBytes: 16 }));
|
||||
const pageRunner = track(createStylesheetPreflightRunner({ maxPages: 0 }));
|
||||
|
||||
await expect(byteRunner.run(input)).resolves.toEqual(
|
||||
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_BYTE_LIMIT" }),
|
||||
);
|
||||
await expect(pageRunner.run(input)).resolves.toEqual(
|
||||
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_PAGE_LIMIT" }),
|
||||
);
|
||||
expect(byteRunner.activeWorkerCount).toBe(0);
|
||||
expect(pageRunner.activeWorkerCount).toBe(0);
|
||||
}, 30_000);
|
||||
|
||||
it("maps worker heap exhaustion to a controlled memory-limit result", async () => {
|
||||
const runner = track(
|
||||
createStylesheetPreflightRunner({ maxOldGenerationMb: 8, timeoutMs: 10_000 }, memoryExhaustionWorker),
|
||||
);
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_MEMORY_LIMIT" }));
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
}, 15_000);
|
||||
|
||||
it("does not expose internal errors from a failed worker bootstrap", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({}, failedWorker));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed.",
|
||||
diagnostics: [],
|
||||
});
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
|
||||
it("surfaces sanitized render failures from inside the worker catch path", async () => {
|
||||
const throwingWorker = new URL(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
import { parentPort } from "node:worker_threads";
|
||||
parentPort.postMessage({ type: "ready" });
|
||||
parentPort.on("message", (message) => {
|
||||
if (message?.type !== "preflight") return;
|
||||
parentPort.postMessage({
|
||||
type: "result",
|
||||
requestId: message.requestId,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed. (Error: Canvas is already closed)",
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
`)}`,
|
||||
);
|
||||
const runner = track(createStylesheetPreflightRunner({ timeoutMs: 5_000 }, throwingWorker));
|
||||
|
||||
const result = await runner.run(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight worker failed. (Error: Canvas is already closed)",
|
||||
diagnostics: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds concurrent workers and queued requests without charging queue time to the worker deadline", async () => {
|
||||
const runner = track(
|
||||
createStylesheetPreflightRunner(
|
||||
{
|
||||
timeoutMs: 500,
|
||||
maxConcurrentWorkers: 1,
|
||||
maxQueuedRequests: 2,
|
||||
},
|
||||
delayedSuccessfulWorker,
|
||||
),
|
||||
);
|
||||
const completionOrder: number[] = [];
|
||||
const accepted = [1, 2, 3].map((number) =>
|
||||
runner.run(numberedInput(number)).then((result) => {
|
||||
if (result.ok) completionOrder.push(result.byteCount);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
const rejected = runner.run(numberedInput(4));
|
||||
|
||||
expect(runner.activeWorkerCount).toBe(1);
|
||||
expect(runner.queuedPreflightCount).toBe(2);
|
||||
await expect(rejected).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: "The PDF preflight queue is full.",
|
||||
}),
|
||||
);
|
||||
const results = await Promise.all(accepted);
|
||||
|
||||
expect(results.every((result) => result.ok)).toBe(true);
|
||||
expect(completionOrder).toEqual([1, 2, 3]);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
}, 5_000);
|
||||
|
||||
it("does not leak a slot when the worker constructor throws synchronously", async () => {
|
||||
const runner = track(createStylesheetPreflightRunner({}, synchronousFailureWorker));
|
||||
|
||||
await expect(runner.run(input)).resolves.toEqual(
|
||||
expect.objectContaining({ ok: false, code: "STYLESHEET_PREFLIGHT_WORKER_FAILED" }),
|
||||
);
|
||||
expect(runner.activeWorkerCount).toBe(0);
|
||||
expect(runner.queuedPreflightCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,362 +0,0 @@
|
||||
import type {
|
||||
PdfPreflightFailure,
|
||||
PdfPreflightResult,
|
||||
StylesheetPreflightInput,
|
||||
StylesheetPreflightRunner,
|
||||
} from "@reactive-resume/pdf/server";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { STYLESHEET_PREFLIGHT_LIMITS } from "@reactive-resume/pdf/preflight-reference";
|
||||
|
||||
export { STYLESHEET_PREFLIGHT_LIMITS } from "@reactive-resume/pdf/preflight-reference";
|
||||
|
||||
type StylesheetPreflightLimits = {
|
||||
timeoutMs: number;
|
||||
maxPages: number;
|
||||
maxBytes: number;
|
||||
maxPageWidthPt: number;
|
||||
maxPageHeightPt: number;
|
||||
maxPageAreaPt2: number;
|
||||
maxOldGenerationMb: number;
|
||||
maxConcurrentWorkers: number;
|
||||
maxQueuedRequests: number;
|
||||
};
|
||||
|
||||
const SOURCE_WORKER_LOADER_HEAP_MB = 256;
|
||||
// The worker is warmed once and reused, so the one-time cold load (which is
|
||||
// super-linear in available CPU — measured ~9s at 0.5 vCPU, ~53s at 0.35, ~93s at
|
||||
// 0.25) is paid at startup, not per request. This ceiling must exceed that cold
|
||||
// load or warmup is killed mid-bootstrap and never completes on a throttled box;
|
||||
// it only bounds a genuinely stuck bootstrap and is off the per-edit path.
|
||||
const WORKER_READINESS_TIMEOUT_MS = 120_000;
|
||||
|
||||
type SerializedPreflightCause = {
|
||||
name: string;
|
||||
message: string;
|
||||
issues: readonly unknown[];
|
||||
};
|
||||
|
||||
type StylesheetPreflightWorkerMessage =
|
||||
| { type: "ready" }
|
||||
| { type: "load_error"; message: string }
|
||||
| { type: "result"; requestId: number; result: PdfPreflightResult }
|
||||
| { type: "preflight_error"; requestId: number; cause: SerializedPreflightCause };
|
||||
|
||||
export type NodeStylesheetPreflightRunner = StylesheetPreflightRunner & {
|
||||
readonly activeWorkerCount: number;
|
||||
readonly queuedPreflightCount: number;
|
||||
warmup(): void;
|
||||
destroy(): Promise<void>;
|
||||
};
|
||||
|
||||
const failure = (code: PdfPreflightFailure["code"], message: string): PdfPreflightFailure => ({
|
||||
ok: false,
|
||||
code,
|
||||
message,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const workerFailure = (error: Error): PdfPreflightFailure => {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
return code === "ERR_WORKER_OUT_OF_MEMORY" || /heap out of memory/i.test(error.message)
|
||||
? failure("STYLESHEET_PREFLIGHT_MEMORY_LIMIT", "The PDF preflight worker exceeded its memory limit.")
|
||||
: failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed.");
|
||||
};
|
||||
|
||||
const sourceWorkerExecArgv = () => {
|
||||
const importIndex = process.execArgv.findIndex(
|
||||
(argument, index, arguments_) =>
|
||||
(argument === "--import" && arguments_[index + 1]?.includes("tsx")) ||
|
||||
(argument.startsWith("--import=") && argument.includes("tsx")),
|
||||
);
|
||||
const inherited = process.execArgv[importIndex];
|
||||
if (inherited?.startsWith("--import=")) return [inherited];
|
||||
if (inherited === "--import") return [inherited, process.execArgv[importIndex + 1] as string];
|
||||
return ["--import", import.meta.resolve("tsx")];
|
||||
};
|
||||
|
||||
const workerLocation = () => {
|
||||
const source = import.meta.url.endsWith(".ts");
|
||||
return {
|
||||
source,
|
||||
url: source
|
||||
? new URL("../workers/stylesheet-preflight.ts", import.meta.url)
|
||||
: new URL("./stylesheet-preflight-worker.mjs", import.meta.url),
|
||||
// Production must not inherit parent execArgv (e.g. --import/--input-type); source workers need tsx.
|
||||
execArgv: source ? sourceWorkerExecArgv() : [],
|
||||
...(source
|
||||
? {
|
||||
env: {
|
||||
...process.env,
|
||||
TSX_TSCONFIG_PATH: fileURLToPath(new URL("../../tsconfig.json", import.meta.url)),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
type PendingRequest = {
|
||||
resolve: (result: PdfPreflightResult) => void;
|
||||
reject: (cause: unknown) => void;
|
||||
renderTimer?: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
type Readiness = {
|
||||
promise: Promise<Worker>;
|
||||
resolve: (worker: Worker) => void;
|
||||
reject: (error: Error) => void;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
export function createStylesheetPreflightRunner(
|
||||
overrides: Partial<StylesheetPreflightLimits> = {},
|
||||
testWorkerUrl?: URL,
|
||||
): NodeStylesheetPreflightRunner {
|
||||
const limits = Object.freeze({ ...STYLESHEET_PREFLIGHT_LIMITS, ...overrides });
|
||||
|
||||
let worker: Worker | undefined;
|
||||
let ready = false;
|
||||
let readiness: Readiness | undefined;
|
||||
let destroyed = false;
|
||||
let requestSeq = 0;
|
||||
|
||||
const pending = new Map<number, PendingRequest>();
|
||||
// ponytail: process-local, bounded admission; upgrade to a pooled/distributed queue only for multi-process coordination.
|
||||
const queue: Array<{
|
||||
input: StylesheetPreflightInput;
|
||||
resolve: PendingRequest["resolve"];
|
||||
reject: PendingRequest["reject"];
|
||||
}> = [];
|
||||
|
||||
const teardownWorker = () => {
|
||||
const dead = worker;
|
||||
worker = undefined;
|
||||
ready = false;
|
||||
if (readiness) {
|
||||
clearTimeout(readiness.timer);
|
||||
readiness.reject(new Error("Stylesheet preflight worker did not become ready."));
|
||||
readiness = undefined;
|
||||
}
|
||||
if (!dead) return;
|
||||
dead.off("message", onMessage);
|
||||
dead.off("error", onError);
|
||||
dead.off("exit", onExit);
|
||||
void dead.terminate().catch(() => undefined);
|
||||
};
|
||||
|
||||
const clearPending = (requestId: number): PendingRequest | undefined => {
|
||||
const entry = pending.get(requestId);
|
||||
if (!entry) return undefined;
|
||||
if (entry.renderTimer) clearTimeout(entry.renderTimer);
|
||||
pending.delete(requestId);
|
||||
return entry;
|
||||
};
|
||||
|
||||
const drainQueue = () => {
|
||||
while (!destroyed && pending.size < limits.maxConcurrentWorkers && queue.length > 0) {
|
||||
const next = queue.shift();
|
||||
if (!next) return;
|
||||
dispatch(next.input, next.resolve, next.reject);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveRequest = (requestId: number, result: PdfPreflightResult) => {
|
||||
const entry = clearPending(requestId);
|
||||
if (!entry) return;
|
||||
entry.resolve(result);
|
||||
drainQueue();
|
||||
};
|
||||
|
||||
const rejectRequest = (requestId: number, cause: unknown) => {
|
||||
const entry = clearPending(requestId);
|
||||
if (!entry) return;
|
||||
entry.reject(cause);
|
||||
drainQueue();
|
||||
};
|
||||
|
||||
// A crashed/exited/timed-out worker is torn down and its in-flight requests are
|
||||
// failed; the next dispatch (including any queued requests) spawns a fresh one,
|
||||
// so a poisoned render never lingers across requests.
|
||||
const failWorker = (result: PdfPreflightFailure) => {
|
||||
teardownWorker();
|
||||
const stale = [...pending.keys()];
|
||||
for (const requestId of stale) resolveRequest(requestId, result);
|
||||
drainQueue();
|
||||
};
|
||||
|
||||
function onMessage(message: StylesheetPreflightWorkerMessage) {
|
||||
if (message.type === "ready") {
|
||||
if (readiness) {
|
||||
clearTimeout(readiness.timer);
|
||||
ready = true;
|
||||
const resolveReady = readiness.resolve;
|
||||
const readyWorker = worker;
|
||||
readiness = undefined;
|
||||
if (readyWorker) resolveReady(readyWorker);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.type === "load_error") {
|
||||
failWorker(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", message.message));
|
||||
return;
|
||||
}
|
||||
if (message.type === "result") {
|
||||
resolveRequest(message.requestId, message.result);
|
||||
return;
|
||||
}
|
||||
if (message.type === "preflight_error") {
|
||||
rejectRequest(
|
||||
message.requestId,
|
||||
Object.assign(new Error(message.cause.message), { name: message.cause.name, issues: message.cause.issues }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onError(error: Error) {
|
||||
console.warn("[stylesheet-preflight] worker error:", error.message);
|
||||
failWorker(workerFailure(error));
|
||||
}
|
||||
|
||||
function onExit(code: number) {
|
||||
if (destroyed || (!worker && pending.size === 0)) return;
|
||||
console.warn(`[stylesheet-preflight] worker exited unexpectedly (code ${code})`);
|
||||
failWorker(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker failed."));
|
||||
}
|
||||
|
||||
const startWorker = (): Promise<Worker> => {
|
||||
const location = testWorkerUrl ? { source: false, url: testWorkerUrl, execArgv: [] as string[] } : workerLocation();
|
||||
let spawned: Worker;
|
||||
try {
|
||||
spawned = new Worker(location.url, {
|
||||
name: "stylesheet-preflight",
|
||||
resourceLimits: {
|
||||
// The source-only tsx compiler heap is outside the production render budget.
|
||||
maxOldGenerationSizeMb: limits.maxOldGenerationMb + (location.source ? SOURCE_WORKER_LOADER_HEAP_MB : 0),
|
||||
},
|
||||
execArgv: location.execArgv,
|
||||
...("env" in location ? { env: location.env } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject(error instanceof Error ? error : new Error("Failed to start PDF preflight worker."));
|
||||
}
|
||||
|
||||
worker = spawned;
|
||||
ready = false;
|
||||
// The idle reused worker must not keep the process (or a test run) alive; active
|
||||
// requests stay alive through their ref'd readiness/render timers.
|
||||
spawned.unref();
|
||||
spawned.on("message", onMessage);
|
||||
spawned.once("error", onError);
|
||||
spawned.once("exit", onExit);
|
||||
|
||||
let resolve!: (value: Worker) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
const promise = new Promise<Worker>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
console.warn(`[stylesheet-preflight] worker did not become ready within ${WORKER_READINESS_TIMEOUT_MS}ms`);
|
||||
teardownWorker();
|
||||
}, WORKER_READINESS_TIMEOUT_MS);
|
||||
readiness = { promise, resolve, reject, timer };
|
||||
return promise;
|
||||
};
|
||||
|
||||
const getReadyWorker = (): Promise<Worker> => {
|
||||
if (destroyed) return Promise.reject(new Error("Stylesheet preflight worker was terminated."));
|
||||
if (worker && ready) return Promise.resolve(worker);
|
||||
if (readiness) return readiness.promise;
|
||||
return startWorker();
|
||||
};
|
||||
|
||||
// One retry: a worker that failed to become ready is torn down; a second
|
||||
// attempt spawns a fresh worker before the request gives up.
|
||||
const waitUntilReady = async (): Promise<Worker> => {
|
||||
try {
|
||||
return await getReadyWorker();
|
||||
} catch {
|
||||
return await getReadyWorker();
|
||||
}
|
||||
};
|
||||
|
||||
function dispatch(
|
||||
input: StylesheetPreflightInput,
|
||||
resolve: PendingRequest["resolve"],
|
||||
reject: PendingRequest["reject"],
|
||||
) {
|
||||
const requestId = ++requestSeq;
|
||||
pending.set(requestId, { resolve, reject });
|
||||
void waitUntilReady()
|
||||
.then((readyWorker) => {
|
||||
const entry = pending.get(requestId);
|
||||
if (!entry) return;
|
||||
entry.renderTimer = setTimeout(() => {
|
||||
// A stuck render poisons the reused worker: tear it down and respawn — but
|
||||
// only the worker this request actually ran on, so a stale timer can never
|
||||
// kill a newer worker already serving other requests.
|
||||
clearPending(requestId);
|
||||
if (worker === readyWorker) teardownWorker();
|
||||
resolve(failure("STYLESHEET_PREFLIGHT_TIMEOUT", "The PDF preflight exceeded its deadline."));
|
||||
drainQueue();
|
||||
}, limits.timeoutMs);
|
||||
readyWorker.postMessage({ type: "preflight", requestId, input, limits });
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
resolveRequest(
|
||||
requestId,
|
||||
workerFailure(error instanceof Error ? error : new Error("Failed to start PDF preflight worker.")),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
get activeWorkerCount() {
|
||||
return pending.size;
|
||||
},
|
||||
get queuedPreflightCount() {
|
||||
return queue.length;
|
||||
},
|
||||
|
||||
warmup() {
|
||||
void waitUntilReady().catch(() => undefined);
|
||||
},
|
||||
|
||||
run(input: StylesheetPreflightInput): Promise<PdfPreflightResult> {
|
||||
return new Promise<PdfPreflightResult>((resolve, reject) => {
|
||||
if (destroyed) {
|
||||
resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."));
|
||||
return;
|
||||
}
|
||||
if (pending.size < limits.maxConcurrentWorkers) {
|
||||
dispatch(input, resolve, reject);
|
||||
return;
|
||||
}
|
||||
if (queue.length >= limits.maxQueuedRequests) {
|
||||
resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight queue is full."));
|
||||
return;
|
||||
}
|
||||
queue.push({ input, resolve, reject });
|
||||
});
|
||||
},
|
||||
|
||||
async destroy() {
|
||||
destroyed = true;
|
||||
const dead = worker;
|
||||
teardownWorker();
|
||||
await dead?.terminate().catch(() => undefined);
|
||||
for (const requestId of [...pending.keys()]) {
|
||||
resolveRequest(
|
||||
requestId,
|
||||
failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."),
|
||||
);
|
||||
}
|
||||
while (queue.length > 0) {
|
||||
const next = queue.shift();
|
||||
next?.resolve(failure("STYLESHEET_PREFLIGHT_WORKER_FAILED", "The PDF preflight worker was terminated."));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const stylesheetPreflightRunner = createStylesheetPreflightRunner();
|
||||
@@ -1,78 +0,0 @@
|
||||
import type {
|
||||
PdfPreflightPageLimits,
|
||||
PdfPreflightResult,
|
||||
RenderPreflightPdfResult,
|
||||
} from "@reactive-resume/pdf/preflight";
|
||||
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
|
||||
|
||||
type StylesheetPreflightInspectionLimits = PdfPreflightPageLimits & {
|
||||
maxPages: number;
|
||||
maxBytes: number;
|
||||
};
|
||||
|
||||
type PdfLoadingTask = {
|
||||
promise: PromiseLike<{ numPages: number }>;
|
||||
destroy(): Promise<void>;
|
||||
};
|
||||
|
||||
type LoadPdf = (options: { data: Uint8Array }) => PdfLoadingTask;
|
||||
|
||||
export async function inspectPreflightPdf(
|
||||
rendered: Extract<RenderPreflightPdfResult, { ok: true }>,
|
||||
limits: StylesheetPreflightInspectionLimits,
|
||||
loadPdf: LoadPdf = getDocument,
|
||||
): Promise<PdfPreflightResult> {
|
||||
if (rendered.bytes.byteLength > limits.maxBytes) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_BYTE_LIMIT",
|
||||
message: "The rendered PDF exceeds the preflight byte limit.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
const byteCount = rendered.bytes.byteLength;
|
||||
let loadingTask: PdfLoadingTask;
|
||||
try {
|
||||
loadingTask = loadPdf({ data: rendered.bytes });
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let document: { numPages: number };
|
||||
try {
|
||||
document = await loadingTask.promise;
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
if (document.numPages > limits.maxPages) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PAGE_LIMIT",
|
||||
message: "The rendered PDF exceeds the preflight page limit.",
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
pageCount: document.numPages,
|
||||
byteCount,
|
||||
diagnostics: rendered.diagnostics,
|
||||
};
|
||||
} finally {
|
||||
await loadingTask.destroy().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { inspectPreflightPdf } from "./stylesheet-preflight-inspection";
|
||||
|
||||
const warning = {
|
||||
code: "EXTREME_VALUE",
|
||||
severity: "warning",
|
||||
message: "The authored value is unusually large.",
|
||||
range: {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 1, column: 10, offset: 9 },
|
||||
},
|
||||
} as const;
|
||||
|
||||
const rendered = {
|
||||
ok: true as const,
|
||||
bytes: new TextEncoder().encode("%PDF-1.7"),
|
||||
diagnostics: [warning],
|
||||
};
|
||||
|
||||
const limits = {
|
||||
maxPages: 20,
|
||||
maxBytes: 10_000_000,
|
||||
maxPageWidthPt: 2_000,
|
||||
maxPageHeightPt: 20_000,
|
||||
maxPageAreaPt2: 20_000_000,
|
||||
};
|
||||
|
||||
describe("stylesheet preflight PDF inspection", () => {
|
||||
it("preserves compiler warnings and hides parser exceptions", async () => {
|
||||
const destroy = vi.fn(async () => undefined);
|
||||
|
||||
const result = await inspectPreflightPdf(rendered, limits, () => ({
|
||||
promise: Promise.reject(new Error("sensitive parser details")),
|
||||
destroy,
|
||||
}));
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: [warning],
|
||||
});
|
||||
expect(destroy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves compiler warnings when PDF.js fails before returning a loading task", async () => {
|
||||
const result = await inspectPreflightPdf(rendered, limits, () => {
|
||||
throw new Error("sensitive parser setup details");
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_PARSE_FAILED",
|
||||
message: "PDF inspection failed.",
|
||||
diagnostics: [warning],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
import type {
|
||||
PdfPreflightPageLimits,
|
||||
PdfPreflightResult,
|
||||
StylesheetPreflightInput,
|
||||
} from "@reactive-resume/pdf/preflight";
|
||||
import { parentPort } from "node:worker_threads";
|
||||
import * as React from "react";
|
||||
import { inspectPreflightPdf } from "./stylesheet-preflight-inspection";
|
||||
|
||||
(globalThis as typeof globalThis & { React: typeof React }).React = React;
|
||||
|
||||
type StylesheetPreflightWorkerLimits = PdfPreflightPageLimits & {
|
||||
maxPages: number;
|
||||
maxBytes: number;
|
||||
};
|
||||
|
||||
// The worker is long-lived and reused across requests: the heavy PDF runtime is
|
||||
// loaded once at startup and each preflight arrives as a message (input can no
|
||||
// longer come from `workerData`, which is fixed at construction time).
|
||||
type StylesheetPreflightWorkerRequest = {
|
||||
type: "preflight";
|
||||
requestId: number;
|
||||
input: StylesheetPreflightInput;
|
||||
limits: StylesheetPreflightWorkerLimits;
|
||||
};
|
||||
|
||||
type SerializedPreflightCause = {
|
||||
name: string;
|
||||
message: string;
|
||||
issues: readonly unknown[];
|
||||
};
|
||||
|
||||
const sanitizeWorkerCause = (cause: unknown): string => {
|
||||
if (!(cause instanceof Error)) return "The PDF preflight worker failed.";
|
||||
const detail = cause.message.replace(/\s+/g, " ").trim().slice(0, 200);
|
||||
if (!detail) return "The PDF preflight worker failed.";
|
||||
return `The PDF preflight worker failed. (${cause.name}: ${detail})`;
|
||||
};
|
||||
|
||||
const serializeZodCause = (cause: unknown): SerializedPreflightCause | undefined => {
|
||||
if (!(cause instanceof Error) || cause.name !== "ZodError" || !("issues" in cause) || !Array.isArray(cause.issues)) {
|
||||
return;
|
||||
}
|
||||
return { name: cause.name, message: cause.message, issues: cause.issues };
|
||||
};
|
||||
|
||||
// Load the heavy PDF runtime once. A rejection here (missing/broken dependency in
|
||||
// a pruned production install) is reported explicitly instead of becoming an
|
||||
// unhandled rejection that silently kills the worker and surfaces as an opaque
|
||||
// runner-side failure.
|
||||
const initialization = import("@reactive-resume/pdf/preflight");
|
||||
|
||||
void initialization.then(
|
||||
() => parentPort?.postMessage({ type: "ready" }),
|
||||
(cause: unknown) => {
|
||||
console.error("[stylesheet-preflight] worker runtime failed to load", cause);
|
||||
parentPort?.postMessage({ type: "load_error", message: sanitizeWorkerCause(cause) });
|
||||
},
|
||||
);
|
||||
|
||||
const handle = async (request: StylesheetPreflightWorkerRequest): Promise<void> => {
|
||||
try {
|
||||
const { renderPreflightPdf } = await initialization;
|
||||
const rendered = await renderPreflightPdf(request.input, request.limits);
|
||||
const result: PdfPreflightResult = rendered.ok ? await inspectPreflightPdf(rendered, request.limits) : rendered;
|
||||
parentPort?.postMessage({ type: "result", requestId: request.requestId, result });
|
||||
} catch (cause) {
|
||||
const serializedCause = serializeZodCause(cause);
|
||||
if (serializedCause) {
|
||||
parentPort?.postMessage({ type: "preflight_error", requestId: request.requestId, cause: serializedCause });
|
||||
return;
|
||||
}
|
||||
console.error("[stylesheet-preflight]", cause);
|
||||
parentPort?.postMessage({
|
||||
type: "result",
|
||||
requestId: request.requestId,
|
||||
result: {
|
||||
ok: false,
|
||||
code: "STYLESHEET_PREFLIGHT_WORKER_FAILED",
|
||||
message: sanitizeWorkerCause(cause),
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
parentPort?.on("message", (message: StylesheetPreflightWorkerRequest) => {
|
||||
if (message.type !== "preflight") return;
|
||||
void handle(message);
|
||||
});
|
||||
Reference in New Issue
Block a user