/** * 进度响应 Map(Task 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 { value: V createdAt: number } export function createProgressResponseCache(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>() function validateKey(key: number): boolean { return Number.isFinite(key) && key > 0 && Number.isInteger(key) } function isExpired(entry: Entry, 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 = ReturnType>