import { jwtDecode } from "jwt-decode"; import { ActivityMonitor } from "../activity-monitor/activity-monitor"; import { TokenManager } from "../token-manager/token-manager"; const CHECK_INTERVAL_MS = 30_000; const REFRESH_THRESHOLD_S = 120; const ACTIVITY_WINDOW_MS = 15 * 60 * 1000; export interface SessionRefreshTimer { start: () => void; stop: () => void; } export class SessionRefreshTimerImpl implements SessionRefreshTimer { private intervalId: ReturnType | null = null; private isRefreshing = false; constructor( private readonly tokenManager: TokenManager, private readonly activityMonitor: ActivityMonitor, ) {} start = () => { this.intervalId = setInterval(this.checkAndRefresh, CHECK_INTERVAL_MS); }; stop = () => { if (this.intervalId !== null) { clearInterval(this.intervalId); this.intervalId = null; } }; private getTokenExpiry = (): number => { try { const { exp } = jwtDecode(this.tokenManager.getToken()) as { exp?: number }; return exp ?? 0; } catch { return 0; } }; private checkAndRefresh = async () => { const now = Date.now(); // Skip if the user hasn't interacted in the last ACTIVITY_WINDOW_MS. // Avoids keeping sessions alive for unattended tabs. if (now - this.activityMonitor.getLastActivityTimestamp() > ACTIVITY_WINDOW_MS) return; // Skip if a refresh is already in flight (this interval can fire again before the previous awaits). if (this.isRefreshing) return; const exp = this.getTokenExpiry(); // Skip if the token can't be decoded (e.g. not yet initialized). if (exp === 0) return; // exp is in seconds, now is in milliseconds — compare in the same unit. if (exp - now / 1000 <= REFRESH_THRESHOLD_S) { this.isRefreshing = true; try { await this.tokenManager.refreshToken(); } catch { // tokenManager.refreshToken() publishes refreshTokenFailed via broker on failure } finally { this.isRefreshing = false; } } }; } export const createSessionRefreshTimer = ( tokenManager: TokenManager, activityMonitor: ActivityMonitor, ): SessionRefreshTimer => new SessionRefreshTimerImpl(tokenManager, activityMonitor);