2026-03-11 20:09:10 +09:00
|
|
|
const MAX_PROCESSED_ENTRY_COUNT = 10_000
|
2026-03-14 13:42:24 +09:00
|
|
|
const PROCESSED_COMMAND_TTL_MS = 30_000
|
2026-03-11 20:09:10 +09:00
|
|
|
|
2026-03-26 19:44:55 +09:00
|
|
|
function pruneExpiredEntries(entries: Map<string, number>, now: number): void {
|
|
|
|
|
for (const [commandKey, expiresAt] of entries) {
|
|
|
|
|
if (expiresAt <= now) {
|
|
|
|
|
entries.delete(commandKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-14 13:42:24 +09:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 19:44:55 +09:00
|
|
|
function trimProcessedEntries(entries: Map<string, number>): void {
|
2026-03-11 20:09:10 +09:00
|
|
|
if (entries.size <= MAX_PROCESSED_ENTRY_COUNT) {
|
2026-03-26 19:44:55 +09:00
|
|
|
return
|
2026-03-11 20:09:10 +09:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 19:44:55 +09:00
|
|
|
const targetSize = Math.floor(entries.size / 2)
|
|
|
|
|
for (const commandKey of entries.keys()) {
|
|
|
|
|
if (entries.size <= targetSize) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries.delete(commandKey)
|
|
|
|
|
}
|
2026-03-11 20:09:10 +09:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 19:44:55 +09:00
|
|
|
function removeSessionEntries(entries: Map<string, number>, sessionID: string): void {
|
2026-03-11 20:09:10 +09:00
|
|
|
const sessionPrefix = `${sessionID}:`
|
2026-03-26 19:44:55 +09:00
|
|
|
for (const entry of entries.keys()) {
|
|
|
|
|
if (entry.startsWith(sessionPrefix)) {
|
|
|
|
|
entries.delete(entry)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 20:09:10 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ProcessedCommandStore {
|
|
|
|
|
has(commandKey: string): boolean
|
2026-03-16 13:53:12 +09:00
|
|
|
add(commandKey: string, ttlMs?: number): void
|
2026-03-11 20:09:10 +09:00
|
|
|
cleanupSession(sessionID: string): void
|
|
|
|
|
clear(): void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createProcessedCommandStore(): ProcessedCommandStore {
|
2026-03-14 13:42:24 +09:00
|
|
|
let entries = new Map<string, number>()
|
2026-03-11 20:09:10 +09:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
has(commandKey: string): boolean {
|
2026-03-26 19:44:55 +09:00
|
|
|
const expiresAt = entries.get(commandKey)
|
|
|
|
|
if (expiresAt === undefined) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (expiresAt <= Date.now()) {
|
|
|
|
|
entries.delete(commandKey)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
2026-03-11 20:09:10 +09:00
|
|
|
},
|
2026-03-16 13:53:12 +09:00
|
|
|
add(commandKey: string, ttlMs = PROCESSED_COMMAND_TTL_MS): void {
|
2026-03-14 13:42:24 +09:00
|
|
|
const now = Date.now()
|
2026-03-26 19:44:55 +09:00
|
|
|
pruneExpiredEntries(entries, now)
|
2026-03-14 13:42:24 +09:00
|
|
|
entries.delete(commandKey)
|
2026-03-16 13:53:12 +09:00
|
|
|
entries.set(commandKey, now + ttlMs)
|
2026-03-26 19:44:55 +09:00
|
|
|
trimProcessedEntries(entries)
|
2026-03-11 20:09:10 +09:00
|
|
|
},
|
|
|
|
|
cleanupSession(sessionID: string): void {
|
2026-03-26 19:44:55 +09:00
|
|
|
removeSessionEntries(entries, sessionID)
|
2026-03-11 20:09:10 +09:00
|
|
|
},
|
|
|
|
|
clear(): void {
|
|
|
|
|
entries.clear()
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|