type TimerKind = 'timeout' | 'interval' type TimerCancel = () => void interface TimerEntry { id: number kind: TimerKind category: string cancel?: TimerCancel } const timerBuckets = new Map>() function bucketOf(category: string) { let bucket = timerBuckets.get(category) if (!bucket) { bucket = new Map() timerBuckets.set(category, bucket) } return bucket } function removeTimer(category: string, id: number) { const bucket = timerBuckets.get(category) if (!bucket) return bucket.delete(id) if (!bucket.size) timerBuckets.delete(category) } export function setCategorizedTimeout(category: string, handler: () => void, delayMs: number) { const id = window.setTimeout(() => { removeTimer(category, id) handler() }, delayMs) bucketOf(category).set(id, { id, kind: 'timeout', category }) return id } export function setCategorizedInterval(category: string, handler: () => void, delayMs: number) { const id = window.setInterval(handler, delayMs) bucketOf(category).set(id, { id, kind: 'interval', category }) return id } export function clearCategorizedTimer(category: string, id: number | null | undefined) { if (id == null) return const bucket = timerBuckets.get(category) const entry = bucket?.get(id) if (entry?.kind === 'interval') window.clearInterval(id) else window.clearTimeout(id) entry?.cancel?.() removeTimer(category, id) } export function clearCategorizedTimers(category: string) { const bucket = timerBuckets.get(category) if (!bucket) return for (const entry of bucket.values()) { if (entry.kind === 'interval') window.clearInterval(entry.id) else window.clearTimeout(entry.id) entry.cancel?.() } timerBuckets.delete(category) } export function sleepWithCategory(category: string, delayMs: number) { return new Promise((resolve) => { const id = window.setTimeout(() => { removeTimer(category, id) resolve() }, delayMs) bucketOf(category).set(id, { id, kind: 'timeout', category, cancel: resolve }) }) } export function clearTimerScope(scope: string) { const prefix = `${scope}:` for (const category of Array.from(timerBuckets.keys())) { if (category === scope || category.startsWith(prefix)) { clearCategorizedTimers(category) } } } export function createCategorizedTimers(scope: string) { const key = (category: string) => `${scope}:${category}` return { setTimeout(category: string, handler: () => void, delayMs: number) { return setCategorizedTimeout(key(category), handler, delayMs) }, setInterval(category: string, handler: () => void, delayMs: number) { return setCategorizedInterval(key(category), handler, delayMs) }, clearTimer(category: string, id: number | null | undefined) { clearCategorizedTimer(key(category), id) }, clearCategory(category: string) { clearCategorizedTimers(key(category)) }, clearScope() { clearTimerScope(scope) }, sleep(category: string, delayMs: number) { return sleepWithCategory(key(category), delayMs) }, } } export type CategorizedTimers = ReturnType export function getCategorizedTimerStats() { return Array.from(timerBuckets.entries()).map(([category, bucket]) => ({ category, count: bucket.size, })) }