web-font/backend/routes/subset.ts
崮生(子虚) 7d0243e6f5 fix(v1.9.2): subsetCache 改用 Date.now() 启动时戳,修复 LLRT process.uptime 报错
process.uptime() 在 LLRT 运行时不存在(typeof === "undefined"),调用抛
TypeError: not a function,导致容器启动即崩溃(v1.9.1 修完 __filename 后暴露)。

改用模块加载时取一次 Date.now():Node/LLRT 均可用,进程重启即变化,
开发态「重启即重置内存缓存」语义不变,生产态靠 PACKAGE_VERSION。

另增 dev:backend:llrt / serve:llrt 本地命令,用 LLRT 跑后端提前暴露兼容问题。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:32:33 +08:00

109 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { fontSubset } from "../font_util/font";
import type { FontEditor } from "../../vendor/fonteditor-core/lib/ttf/font.js";
import { parseUrl, jsonResponse, stats, subsetCache, findFontPath, readFontBuffer } from "../shared";
/**
* 进程启动时戳(模块加载时取一次,进程重启即变化)
*
* 用 Date.now() 而非 process.uptime():后者在 LLRT 运行时不存在typeof === "undefined"
* 调用会抛 TypeError: not a function导致容器启动即崩溃。
* Date.now() 在 Node 与 LLRT 中均可用,且模块只加载一次,取到的就是稳定的启动时戳。
*/
declare const PACKAGE_VERSION: string;
/** 进程启动时刻,重启进程即变化 —— 用于开发态「重启即重置内存缓存」 */
const PROCESS_START_TIME = Date.now();
/**
* 子集化版本指纹
*
* 纳入 subsetCache 的 key让旧缓存条目在以下两种场景自动失效无需手动清缓存
* - 生产:发版 bump package.json 的 version构建期由 tsdown define 注入),旧缓存自然过期
* - 开发pnpm dev 重启进程时 PROCESS_START_TIME 变化,内存缓存整体重置
*
* 杜绝「子集化代码已修但缓存返回旧错误结果」的陷阱。
*/
const SUBSET_CACHE_KEY = `${PACKAGE_VERSION}:${PROCESS_START_TIME}`;
/** GET /api?font=...&text=... — 字体裁剪 */
export async function handleFontSubset(req: Request, res: Response) {
const url = parseUrl(req);
const params = new URLSearchParams(url.search);
const font = params.get("font") || "";
const text = params.get("text") || "";
if (text.length === 0) {
return { req, res };
}
const fontPath = await findFontPath(font);
if (!fontPath) {
return {
req,
res: new Response(`Font not found: ${font}`, {
status: 404,
headers: { "Content-Type": "text/plain; charset=utf-8" },
}),
};
}
/** 默认 ttf兼容性最好 */
const outTypeParam = params.get("outType") || "";
const outType = (outTypeParam === "woff2" || outTypeParam === "ttf") ? outTypeParam : "ttf";
/** 查询裁剪结果缓存 */
/** 版本指纹纳入 key代码变更后旧缓存自动失效 */
const cacheKey = `${SUBSET_CACHE_KEY}:${fontPath}:${outType}:${text}`;
stats.subsetRequests++;
stats.totalChars += text.length;
const cached = subsetCache.get(cacheKey);
if (cached) {
stats.subsetCacheHits++;
const contentTypes: Record<string, string> = { ttf: "font/ttf", woff2: "font/woff2" };
return {
req,
res: new Response(cached, {
status: 200,
headers: {
"Content-Type": contentTypes[outType] || "font/ttf",
"Cache-Control": "public, max-age=86400",
"X-Cache": "HIT",
},
}),
};
}
const fontType = fontPath.split(".").pop() as FontEditor.FontType;
let oldFontBuffer: ArrayBuffer;
try {
oldFontBuffer = await readFontBuffer(fontPath);
} catch {
return {
req,
res: new Response(`Font read error: ${font}`, {
status: 500,
headers: { "Content-Type": "text/plain; charset=utf-8" },
}),
};
}
const newFont = await fontSubset(oldFontBuffer, text, {
outType: outType,
sourceType: fontType,
});
/** 写入裁剪结果缓存 */
subsetCache.set(cacheKey, newFont as ArrayBuffer);
const contentTypes: Record<string, string> = { ttf: "font/ttf", woff2: "font/woff2" };
return {
req,
res: new Response(newFont, {
status: 200,
headers: {
"Content-Type": contentTypes[outType] || "font/ttf",
"Cache-Control": "public, max-age=86400",
"X-Cache": "MISS",
},
}),
};
}