Files
crawler-plugin/frontend-vue/src/shared/progress-response-cache.ts
T
huangzd1997 83b93eb39b task-82: 为进度响应 Map 增加 TTL 清理与最大条目数
新增纯 TS 进度响应缓存 progress-response-cache:写入后 ttlMs 过期、
条目数超 maxEntries 驱逐最旧、读取/枚举/清理惰性移除过期条目,时钟
可注入以便测试 TTL 边界。set 对非法 key fail-fast 抛错,时钟故障时
零部分写入,clear 释放全部条目。useTaskProgressLoop 新增可选
progressCache 选项,每轮响应快照写入有界缓存(默认不启用)。
2026-08-30 22:42:08 +08:00

142 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.
/**
* 进度响应 MapTask 82)。
*
* 为前端任务轮询的进度响应快照提供带 TTL 与最大条目数的内存缓存:
* - 条目在写入后 ttlMs 毫秒过期,读取/枚举/清理时惰性移除;
* - 条目数超过 maxEntries 时驱逐最旧条目(首次写入顺序),缓存始终有界;
* - 时钟可注入(now),测试可用虚拟时钟验证 TTL 边界,不依赖真实时间。
*
* 纯 TS 无副作用模块;set 对非法 key 抛出「key 必须是正整数」错误(fail-fast),
* 时钟抛错时 set 不留下任何部分写入;读取路径(get/has/delete)对非法 key 宽容。
* 供 useTaskProgressLoop 的可选缓存选项复用。
*/
export interface ProgressResponseCacheOptions {
/** 条目过期时间(毫秒),必须为正数 */
ttlMs: number
/** 缓存最大条目数,必须为正数 */
maxEntries: number
/** 时钟来源;默认 Date.now(),测试可注入虚拟时钟 */
now?: () => number
}
interface Entry<V> {
value: V
createdAt: number
}
export function createProgressResponseCache<V>(options: ProgressResponseCacheOptions) {
const ttlMs = options.ttlMs
const maxEntries = options.maxEntries
const now = options.now ?? Date.now
if (!(ttlMs > 0)) {
throw new Error('ttlMs 必须为正数: ' + ttlMs)
}
if (!(maxEntries > 0)) {
throw new Error('maxEntries 必须为正数: ' + maxEntries)
}
const map = new Map<number, Entry<V>>()
function validateKey(key: number): boolean {
return Number.isFinite(key) && key > 0 && Number.isInteger(key)
}
function isExpired(entry: Entry<V>, at: number): boolean {
return at - entry.createdAt >= ttlMs
}
function purgeExpired(): number {
const at = now()
let removed = 0
for (const [key, entry] of map) {
if (isExpired(entry, at)) {
map.delete(key)
removed += 1
}
}
return removed
}
function evictIfNeeded(at: number) {
while (map.size > maxEntries) {
const oldest = map.keys().next().value as number | undefined
if (oldest == null) break
map.delete(oldest)
}
}
function set(key: number, value: V) {
if (!validateKey(key)) {
throw new Error('key 必须是正整数: ' + key)
}
const createdAt = now()
if (map.has(key)) {
map.set(key, { value, createdAt })
} else {
map.set(key, { value, createdAt })
evictIfNeeded(createdAt)
}
}
function get(key: number): V | undefined {
if (!validateKey(key)) return undefined
const at = now()
const entry = map.get(key)
if (!entry) return undefined
if (isExpired(entry, at)) {
map.delete(key)
return undefined
}
return entry.value
}
function has(key: number): boolean {
if (!validateKey(key)) return false
const at = now()
const entry = map.get(key)
if (!entry) return false
if (isExpired(entry, at)) {
map.delete(key)
return false
}
return true
}
function deleteEntry(key: number): boolean {
if (!validateKey(key)) return false
return map.delete(key)
}
function clear() {
map.clear()
}
function entries(): Array<[number, V]> {
const at = now()
const result: Array<[number, V]> = []
for (const [key, entry] of map) {
if (isExpired(entry, at)) {
map.delete(key)
continue
}
result.push([key, entry.value])
}
return result
}
return {
get size() {
return map.size
},
set,
get,
has,
delete: deleteEntry,
clear,
entries,
purgeExpired,
}
}
export type ProgressResponseCache<V> = ReturnType<typeof createProgressResponseCache<V>>