Cache Keepalive: skip + disarm if the idle timer fired >5s late (sleep/throttle guard)
| @@ -37,6 +37,11 @@ const NONE_KEY = '__none__'; | |||
| 37 | // user message can pre-empt the keepalive. | 37 | // user message can pre-empt the keepalive. |
| 38 | const EXPIRED_GRACE_MS = 3000; | 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 | const defaultSettings = { | 45 | const defaultSettings = { |
| 41 | enabled: false, | 46 | enabled: false, |
| 42 | timeoutSeconds: 295, | 47 | timeoutSeconds: 295, |
| @@ -52,6 +57,9 @@ let keepaliveActive = false; | |||
| 52 | // history already present) we have no way to know whether anything is still cached | 57 | // history already present) we have no way to know whether anything is still cached |
| 53 | // server-side — so pinging before then would be pointless. Resets on page reload. | 58 | // server-side — so pinging before then would be pointless. Resets on page reload. |
| 54 | let armed = false; | 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 | // Per-profile "time since last message", keyed by connection profile id (or NONE_KEY). | 63 | // Per-profile "time since last message", keyed by connection profile id (or NONE_KEY). |
| 56 | // In-memory only: on reload every profile is treated as freshly active. | 64 | // In-memory only: on reload every profile is treated as freshly active. |
| 57 | const lastActivity = {}; | 65 | const lastActivity = {}; |
| @@ -102,12 +110,15 @@ function clearIdleTimer() { | |||
| 102 | function scheduleIdleTimer() { | 110 | function scheduleIdleTimer() { |
| 103 | clearIdleTimer(); | 111 | clearIdleTimer(); |
| 104 | if (!armed || !isKeepaliveEnabledForActive()) { | 112 | if (!armed || !isKeepaliveEnabledForActive()) { |
| 113 | scheduledFireTime = null; | ||
| 105 | return; | 114 | return; |
| 106 | } | 115 | } |
| 107 | const timeoutMs = Math.max(60, Number(extension_settings[MODULE].timeoutSeconds) || 295) * 1000; | 116 | const timeoutMs = Math.max(60, Number(extension_settings[MODULE].timeoutSeconds) || 295) * 1000; |
| 108 | const last = lastActivity[getActiveProfileKey()] ?? Date.now(); | 117 | const last = lastActivity[getActiveProfileKey()] ?? Date.now(); |
| 109 | const remaining = timeoutMs - (Date.now() - last); | 118 | const remaining = timeoutMs - (Date.now() - last); |
| 110 | idleTimer = setTimeout(fireKeepalive, remaining > 0 ? remaining : EXPIRED_GRACE_MS); | 119 | const delay = 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 | return; | 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 | // Never start while any generation is in progress, or while a previous | 177 | // Never start while any generation is in progress, or while a previous |
| 155 | // keepalive is still running. Reschedule and bail. | 178 | // keepalive is still running. Reschedule and bail. |
| 156 | if (keepaliveActive || isGenerating() || streamingProcessor) { | 179 | if (keepaliveActive || isGenerating() || streamingProcessor) { |