83b93eb39b
新增纯 TS 进度响应缓存 progress-response-cache:写入后 ttlMs 过期、 条目数超 maxEntries 驱逐最旧、读取/枚举/清理惰性移除过期条目,时钟 可注入以便测试 TTL 边界。set 对非法 key fail-fast 抛错,时钟故障时 零部分写入,clear 释放全部条目。useTaskProgressLoop 新增可选 progressCache 选项,每轮响应快照写入有界缓存(默认不启用)。
142 lines
3.6 KiB
TypeScript
142 lines
3.6 KiB
TypeScript
/**
|
||
* 进度响应 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<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>>
|