mirror of
https://github.com/2234839/web-font.git
synced 2026-09-04 23:02:27 +08:00
根因: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 压测脚本
91 lines
3.0 KiB
TypeScript
91 lines
3.0 KiB
TypeScript
/**
|
||
* 临时字体保留机制 —— 定时扫描 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);
|
||
}
|