web-font/backend/temp_cleaner.ts
崮生(子虚) 5a7c6a9913 fix: LLRT readdir 不兼容 withFileTypes 导致临时字体清理失效
根因:LLRT 的 fs.readdir 返回字符串数组而非 Dirent 对象,
entry.isFile() 抛 TypeError 被 catch 静默吞掉,
导致 evictIfNeeded 和 temp_cleaner 从未执行(线上堆积 108 个文件 992M)。

修复:
- node.ts: readdir 改为先拿文件名再 stat 判断
- temp_cleaner.ts: catch 块加日志不再静默吞错误

同时包含:
- font-config.json homepage 链接修复(删除 3 个 404,修正 2 个)
- FontDetail 字符预览改为可实时编辑
- 添加 scripts/bench.mjs 压测脚本
2026-08-01 08:03:33 +08:00

91 lines
3.0 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.

/**
* 临时字体保留机制 —— 定时扫描 font/temp删除超过保留时限且最近无人使用的字体。
*
* "最近使用" = 最后一次被 subset/font-meta/font-detail 请求的时间。
* 使用记录存在内存 Map 中(进程级),重启后从文件 mtime 重新起步。
*
* 清理周期 = 保留时限的一半(最少 5 分钟),避免过于频繁的扫描。
*/
import { readdir, stat, unlink, path_join } from "./interface";
import { tempRetentionSeconds } from "./config";
/** 临时字体目录 */
const TEMP_DIR = "font/temp";
/** 字体最后使用时间戳key = 文件名(不含目录前缀) */
const lastUsedMap = new Map<string, number>();
/**
* 记录字体被使用subset/meta/detail 请求时调用)。
* 仅记录 font/temp 下的文件,其他目录无需跟踪。
*/
export function markFontUsed(fontPath: string): void {
/** 仅临时字体需要跟踪 */
if (!fontPath.startsWith(TEMP_DIR + "/")) return;
const name = fontPath.split("/").pop()!;
lastUsedMap.set(name, Date.now());
}
/**
* 执行一次清理扫描。
* 遍历 font/temp 中的字体文件,删除:
* (now - max(最后使用时间, 文件 mtime)) > 保留时限
*/
async function cleanOnce(): Promise<void> {
const now = Date.now();
const retentionMs = tempRetentionSeconds * 1000;
let entries: Array<{ name: string; isFile: () => boolean }>;
try {
entries = await readdir(TEMP_DIR);
} catch (err) {
console.log("[temp-cleaner] readdir 失败:", err);
return;
}
let cleaned = 0;
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!/\.(ttf|otf|woff|woff2)$/i.test(entry.name)) continue;
const filePath = path_join(TEMP_DIR, entry.name);
try {
const s = await stat(filePath);
/** 取"最后使用时间"和"文件修改时间"的较大值作为活跃判定基准 */
const lastUsed = lastUsedMap.get(entry.name) ?? 0;
const lastActive = Math.max(lastUsed, s.mtimeMs);
if (now - lastActive > retentionMs) {
await unlink(filePath);
lastUsedMap.delete(entry.name);
cleaned++;
}
} catch (err) {
console.log("[temp-cleaner] 处理文件失败:", entry.name, err);
}
}
if (cleaned > 0) {
console.log(`[temp-cleaner] 本次清理 ${cleaned} 个过期临时字体`);
}
}
/** 清理周期:保留时限的一半,最少 5 分钟 */
const CLEAN_INTERVAL = Math.max(tempRetentionSeconds * 500, 300_000);
/**
* 启动定时清理器。
* 首次延迟 1 分钟执行(避免启动峰),之后按周期循环。
*/
export function startTempCleaner(): void {
const intervalSec = Math.round(CLEAN_INTERVAL / 1000);
console.log(`[temp-cleaner] 启动,保留时限 ${tempRetentionSeconds}s清理周期 ${intervalSec}s`);
/** 首次延迟 60 秒 */
setTimeout(() => {
cleanOnce().catch(() => {});
/** 后续按周期循环 */
setInterval(() => {
cleanOnce().catch(() => {});
}, CLEAN_INTERVAL);
}, 60_000);
}