Cache Keepalive: skip + disarm if the idle timer fired >5s late (sleep/throttle guard)
| @@ -37,6 +37,11 @@ const NONE_KEY = '__none__'; | ||
| 37 | 37 | // user message can pre-empt the keepalive. |
| 38 | 38 | const EXPIRED_GRACE_MS = 3000; |
| 39 | 39 | |
| 40 | +// If the idle timer fires more than this many ms later than it was scheduled to, assume the | |
| 41 | +// machine slept / the tab was throttled — far more wall-clock time passed than the interval, | |
| 42 | +// so the cache almost certainly lapsed. Skip the ping and go dormant rather than guess. | |
| 43 | +const MAX_TIMER_DRIFT_MS = 5000; | |
| 44 | + | |
| 40 | 45 | const defaultSettings = { |
| 41 | 46 | enabled: false, |
| 42 | 47 | timeoutSeconds: 295, |
| @@ -52,6 +57,9 @@ let keepaliveActive = false; | ||
| 52 | 57 | // history already present) we have no way to know whether anything is still cached |
| 53 | 58 | // server-side — so pinging before then would be pointless. Resets on page reload. |
| 54 | 59 | let armed = false; |
| 60 | +// Wall-clock time (Date.now() + delay) the pending idle timer is expected to fire, or null when | |
| 61 | +// none is scheduled. Used to detect a timer that fired far too late (sleep/throttle). | |
| 62 | +let scheduledFireTime = null; | |
| 55 | 63 | // Per-profile "time since last message", keyed by connection profile id (or NONE_KEY). |
| 56 | 64 | // In-memory only: on reload every profile is treated as freshly active. |
| 57 | 65 | const lastActivity = {}; |
| @@ -102,12 +110,15 @@ function clearIdleTimer() { | ||
| 102 | 110 | function scheduleIdleTimer() { |
| 103 | 111 | clearIdleTimer(); |
| 104 | 112 | if (!armed || !isKeepaliveEnabledForActive()) { |
| 113 | + scheduledFireTime = null; | |
| 105 | 114 | return; |
| 106 | 115 | } |
| 107 | 116 | const timeoutMs = Math.max(60, Number(extension_settings[MODULE].timeoutSeconds) || 295) * 1000; |
| 108 | 117 | const last = lastActivity[getActiveProfileKey()] ?? Date.now(); |
| 109 | 118 | const remaining = timeoutMs - (Date.now() - last); |
| 110 | 119 | idleTimerconst =delay setTimeout(fireKeepalive,= remaining > 0 ? remaining : EXPIRED_GRACE_MS); |
| 120 | + scheduledFireTime = Date.now() + delay; | |
| 121 | + idleTimer = setTimeout(fireKeepalive, delay); | |
| 111 | 122 | } |
| 112 | 123 | |
| 113 | 124 | /** |
| @@ -151,6 +162,18 @@ async function fireKeepalive() { | ||
| 151 | 162 | return; |
| 152 | 163 | } |
| 153 | 164 | |
| 165 | + // If the timer fired much later than scheduled (machine sleep, background-tab throttling, | |
| 166 | + // etc.), far more wall-clock time has elapsed than the idle interval, so the cache has | |
| 167 | + // almost certainly expired. Don't ping on a stale assumption — go dormant until the next | |
| 168 | + // real message re-arms keepalive, exactly like a freshly opened tab. | |
| 169 | + if (scheduledFireTime !== null && (Date.now() - scheduledFireTime) > MAX_TIMER_DRIFT_MS) { | |
| 170 | + console.debug('[Keepalive] Idle timer fired', Date.now() - scheduledFireTime, 'ms late; skipping and disarming until next activity.'); | |
| 171 | + armed = false; | |
| 172 | + clearIdleTimer(); | |
| 173 | + scheduledFireTime = null; | |
| 174 | + return; | |
| 175 | + } | |
| 176 | + | |
| 154 | 177 | // Never start while any generation is in progress, or while a previous |
| 155 | 178 | // keepalive is still running. Reschedule and bail. |
| 156 | 179 | if (keepaliveActive || isGenerating() || streamingProcessor) { |