From 939bb170e46f95224790c7a7aeb23beea390192b Mon Sep 17 00:00:00 2001 From: James Date: Fri, 4 Sep 2026 19:12:58 -0400 Subject: [PATCH] fix(producer): isolate font cache temporary writes --- .../src/services/fontCompression.test.ts | 72 ++++++++++++++++++- .../producer/src/services/fontCompression.ts | 18 ++++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/packages/producer/src/services/fontCompression.test.ts b/packages/producer/src/services/fontCompression.test.ts index 37934e95bb..07af822d41 100644 --- a/packages/producer/src/services/fontCompression.test.ts +++ b/packages/producer/src/services/fontCompression.test.ts @@ -1,5 +1,13 @@ -import { describe, expect, it } from "bun:test"; -import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { describe, expect, it, spyOn } from "bun:test"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { compressToWoff2, fontToDataUri } from "./fontCompression.js"; @@ -43,6 +51,66 @@ describe("compressToWoff2", () => { }); describe("fontToDataUri", () => { + it("preserves a pre-existing temporary file and still populates the cache", async () => { + const cacheDir = mkdtempSync(join(tmpdir(), "hf-local-font-cache-")); + const raw = Buffer.from("stable-font-content"); + const compressed = Buffer.from("compressed-font-content"); + const compressImpl = async () => compressed; + const clock = spyOn(Date, "now").mockReturnValue(1234567890); + try { + const first = await fontToDataUri(raw, "ttf", { cacheDir, compressImpl }); + const [cacheName] = readdirSync(cacheDir); + if (!cacheName) throw new Error("Expected a cached font"); + rmSync(join(cacheDir, cacheName)); + const existingName = `${cacheName}.tmp-${process.pid}-${Date.now()}`; + writeFileSync(join(cacheDir, existingName), "another writer's file"); + + expect(await fontToDataUri(raw, "ttf", { cacheDir, compressImpl })).toBe(first); + expect(readFileSync(join(cacheDir, existingName), "utf8")).toBe("another writer's file"); + expect(readFileSync(join(cacheDir, cacheName))).toEqual(compressed); + expect(readdirSync(cacheDir).sort()).toEqual([cacheName, existingName].sort()); + } finally { + clock.mockRestore(); + rmSync(cacheDir, { recursive: true, force: true }); + } + }); + + it("cleans up its temporary files when publishing the cache fails", async () => { + const cacheDir = mkdtempSync(join(tmpdir(), "hf-local-font-cache-")); + const raw = Buffer.from("stable-font-content"); + const compressImpl = async () => Buffer.from("compressed-font-content"); + try { + const first = await fontToDataUri(raw, "ttf", { cacheDir, compressImpl }); + const [cacheName] = readdirSync(cacheDir); + if (!cacheName) throw new Error("Expected a cached font"); + rmSync(join(cacheDir, cacheName)); + mkdirSync(join(cacheDir, cacheName)); + + expect(await fontToDataUri(raw, "ttf", { cacheDir, compressImpl })).toBe(first); + expect(readdirSync(cacheDir)).toEqual([cacheName]); + } finally { + rmSync(cacheDir, { recursive: true, force: true }); + } + }); + + it("returns compressed data when the cache directory cannot be created", async () => { + const root = mkdtempSync(join(tmpdir(), "hf-local-font-cache-")); + const cacheDir = join(root, "not-a-directory"); + writeFileSync(cacheDir, "keep"); + try { + const compressed = Buffer.from("compressed-font-content"); + const uri = await fontToDataUri(Buffer.from("font"), "ttf", { + cacheDir, + compressImpl: async () => compressed, + }); + expect(uri).toBe(`data:font/woff2;base64,${compressed.toString("base64")}`); + expect(readFileSync(cacheDir, "utf8")).toBe("keep"); + expect(readdirSync(root)).toEqual(["not-a-directory"]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it("reuses cached compression across calls", async () => { const cacheDir = mkdtempSync(join(tmpdir(), "hf-local-font-cache-")); const raw = Buffer.from("stable-font-content"); diff --git a/packages/producer/src/services/fontCompression.ts b/packages/producer/src/services/fontCompression.ts index c968a1a68e..ad4c5cfe11 100644 --- a/packages/producer/src/services/fontCompression.ts +++ b/packages/producer/src/services/fontCompression.ts @@ -1,7 +1,15 @@ // @ts-expect-error -- wawoff2 ships no type declarations; ambient .d.ts only visible to producer's own tsconfig import wawoff2 from "wawoff2"; import { createHash } from "node:crypto"; -import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; import { homedir, tmpdir } from "node:os"; import { dirname, join } from "node:path"; @@ -57,9 +65,13 @@ function readCachedCompression(path: string): Buffer | null { } function cacheCompression(path: string, compressed: Buffer): void { - const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}`; + let tempDir: string | undefined; try { mkdirSync(dirname(path), { recursive: true }); + // Keep the write on the cache filesystem for an atomic rename, inside a + // private directory that this invocation owns. + tempDir = mkdtempSync(join(dirname(path), ".compression-")); + const tmpPath = join(tempDir, "font.woff2"); writeFileSync(tmpPath, compressed, { flag: "wx", mode: 0o644 }); renameSync(tmpPath, path); } catch { @@ -67,7 +79,7 @@ function cacheCompression(path: string, compressed: Buffer): void { // read-only. Compression still succeeded, so rendering can continue. } finally { try { - rmSync(tmpPath, { force: true }); + if (tempDir) rmSync(tempDir, { recursive: true, force: true }); } catch { // Best-effort cleanup only. }