/** * Per-actuator leading+trailing debounce so a dragged slider doesn't fire a * device command on every pointermove - devices don't need mouse-move-rate * updates, and most BLE stacks choke on a command flood. */ const THROTTLE_MS = 75; interface ThrottleEntry { timer: ReturnType | null; pendingValue: number | null; pendingDuration: number | undefined; lastSentAt: number; } const entries = new Map(); export function throttledSend( key: string, send: (value: number, durationMs?: number) => void | Promise, value: number, durationMs?: number, ): void { let entry = entries.get(key); if (!entry) { entry = { timer: null, pendingValue: null, pendingDuration: undefined, lastSentAt: 0 }; entries.set(key, entry); } entry.pendingValue = value; entry.pendingDuration = durationMs; const flush = (): void => { const v = entry!.pendingValue; entry!.pendingValue = null; entry!.timer = null; entry!.lastSentAt = Date.now(); if (v !== null) void send(v, entry!.pendingDuration); }; const elapsed = Date.now() - entry.lastSentAt; if (elapsed >= THROTTLE_MS) { if (entry.timer) clearTimeout(entry.timer); flush(); } else if (!entry.timer) { entry.timer = setTimeout(flush, THROTTLE_MS - elapsed); } } export function clearThrottle(key: string): void { const entry = entries.get(key); if (entry?.timer) clearTimeout(entry.timer); entries.delete(key); }