|
9 | 9 | OAUTH_SCOPE_VERSION, |
10 | 10 | OAUTH_SCOPES, |
11 | 11 | TOKEN_REFRESH_BUFFER_MS, |
| 12 | + TOKEN_REFRESH_FORCE_MS, |
12 | 13 | } from "@shared/constants/oauth"; |
13 | 14 | import { ANALYTICS_EVENTS } from "@shared/types/analytics"; |
14 | 15 | import type { CloudRegion } from "@shared/types/oauth"; |
@@ -98,6 +99,75 @@ interface AuthState { |
98 | 99 |
|
99 | 100 | let refreshTimeoutId: number | null = null; |
100 | 101 |
|
| 102 | +function isTokenExpiringSoon(tokenExpiry: number | null): boolean { |
| 103 | + return ( |
| 104 | + tokenExpiry != null && tokenExpiry - Date.now() <= TOKEN_REFRESH_FORCE_MS |
| 105 | + ); |
| 106 | +} |
| 107 | + |
| 108 | +async function attemptRefreshWithActivityCheck( |
| 109 | + getState: () => AuthState, |
| 110 | +): Promise<void> { |
| 111 | + try { |
| 112 | + // If the token is about to expire, skip the activity check and refresh immediately |
| 113 | + if (isTokenExpiringSoon(getState().tokenExpiry)) { |
| 114 | + log.warn( |
| 115 | + "Token expiring imminently, forcing refresh despite active sessions", |
| 116 | + ); |
| 117 | + await getState().refreshAccessToken(); |
| 118 | + return; |
| 119 | + } |
| 120 | + |
| 121 | + // Refresh if there are no active sessions |
| 122 | + const hasActive = await trpcVanilla.agent.hasActiveSessions.query(); |
| 123 | + if (!hasActive) { |
| 124 | + await getState().refreshAccessToken(); |
| 125 | + return; |
| 126 | + } |
| 127 | + |
| 128 | + await new Promise<void>((resolve, reject) => { |
| 129 | + let settled = false; |
| 130 | + const settle = (reason: string, fn: () => Promise<void>) => { |
| 131 | + if (settled) return; |
| 132 | + settled = true; |
| 133 | + subscription.unsubscribe(); |
| 134 | + window.clearInterval(expiryCheckId); |
| 135 | + log.info(`Settling activity wait: ${reason}`); |
| 136 | + fn().then(resolve).catch(reject); |
| 137 | + }; |
| 138 | + |
| 139 | + // Subscribe to the idle event |
| 140 | + const subscription = trpcVanilla.agent.onSessionsIdle.subscribe( |
| 141 | + undefined, |
| 142 | + { |
| 143 | + onData: () => |
| 144 | + settle("sessions idle", () => getState().refreshAccessToken()), |
| 145 | + onError: (error) => { |
| 146 | + log.warn( |
| 147 | + "Sessions idle subscription failed, refreshing anyway", |
| 148 | + error, |
| 149 | + ); |
| 150 | + settle("subscription error", () => getState().refreshAccessToken()); |
| 151 | + }, |
| 152 | + }, |
| 153 | + ); |
| 154 | + |
| 155 | + // Safety net: if the token is about to expire while we wait, force refresh |
| 156 | + const expiryCheckId = window.setInterval(() => { |
| 157 | + if (isTokenExpiringSoon(getState().tokenExpiry)) { |
| 158 | + settle("token expiring imminently", () => |
| 159 | + getState().refreshAccessToken(), |
| 160 | + ); |
| 161 | + } |
| 162 | + }, TOKEN_REFRESH_FORCE_MS / 2); |
| 163 | + }); |
| 164 | + } catch (error) { |
| 165 | + // IPC call failed — refresh anyway (better than letting the token expire) |
| 166 | + log.warn("Activity check failed, refreshing token anyway", error); |
| 167 | + await getState().refreshAccessToken(); |
| 168 | + } |
| 169 | +} |
| 170 | + |
101 | 171 | export const useAuthStore = create<AuthState>()( |
102 | 172 | subscribeWithSelector( |
103 | 173 | persist( |
@@ -399,18 +469,14 @@ export const useAuthStore = create<AuthState>()( |
399 | 469 |
|
400 | 470 | if (timeUntilRefresh > 0) { |
401 | 471 | refreshTimeoutId = window.setTimeout(() => { |
402 | | - get() |
403 | | - .refreshAccessToken() |
404 | | - .catch((error) => { |
405 | | - log.error("Proactive token refresh failed:", error); |
406 | | - }); |
| 472 | + attemptRefreshWithActivityCheck(get).catch((error) => { |
| 473 | + log.error("Proactive token refresh failed:", error); |
| 474 | + }); |
407 | 475 | }, timeUntilRefresh); |
408 | 476 | } else { |
409 | | - get() |
410 | | - .refreshAccessToken() |
411 | | - .catch((error) => { |
412 | | - log.error("Immediate token refresh failed:", error); |
413 | | - }); |
| 477 | + attemptRefreshWithActivityCheck(get).catch((error) => { |
| 478 | + log.error("Immediate token refresh failed:", error); |
| 479 | + }); |
414 | 480 | } |
415 | 481 | }, |
416 | 482 |
|
|
0 commit comments