From cc3a9c86132d6439c46038f36c1d7bfb04262d16 Mon Sep 17 00:00:00 2001 From: Hampus Date: Thu, 3 Sep 2026 23:21:32 +0200 Subject: [PATCH] fix(app): jitter gateway reconnects and recover status nagbar (#2439) --- .../features/app/hooks/useConnectionNotice.ts | 6 +++++ .../gateway/transport/GatewaySocket.ts | 9 ++++++-- .../src/features/user/state/StatusPage.ts | 22 +++++++++---------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/fluxer_app/src/features/app/hooks/useConnectionNotice.ts b/fluxer_app/src/features/app/hooks/useConnectionNotice.ts index 2e820f143..8a9efbb27 100644 --- a/fluxer_app/src/features/app/hooks/useConnectionNotice.ts +++ b/fluxer_app/src/features/app/hooks/useConnectionNotice.ts @@ -86,6 +86,12 @@ export function useConnectionNotice(): ConnectionNotice | null { }, BOOT_NOTICE_DELAY_MS); return () => window.clearTimeout(timer); }, [booting]); + useEffect(() => { + if (!reconnecting) { + return; + } + StatusPage.refreshForConnectionIssue(); + }, [reconnecting]); if (Nagbar.forceHideConnectionNotice) { return null; } diff --git a/fluxer_app/src/features/gateway/transport/GatewaySocket.ts b/fluxer_app/src/features/gateway/transport/GatewaySocket.ts index 3bf3fe8a1..90c545245 100644 --- a/fluxer_app/src/features/gateway/transport/GatewaySocket.ts +++ b/fluxer_app/src/features/gateway/transport/GatewaySocket.ts @@ -32,6 +32,7 @@ const GATEWAY_TIMEOUTS = { ResumeWindow: 180000, MinReconnect: 1000, MaxReconnect: 60000, + ReconnectSpread: 2000, Hello: 20000, } as const; export const GatewayState = { @@ -1034,7 +1035,7 @@ export class GatewaySocket extends EventEmitter { } const allowImmediate = options.allowImmediate ?? true; const wasImmediate = allowImmediate && this.shouldReconnectImmediately; - const delay = wasImmediate ? 0 : this.nextReconnectDelay(); + const delay = wasImmediate ? this.reconnectSpread() : this.nextReconnectDelay(); this.shouldReconnectImmediately = false; this.log.info(`Scheduling reconnect in ${delay}ms${wasImmediate ? ' (immediate)' : ''}`); this.reconnectTimeoutId = window.setTimeout(() => { @@ -1047,12 +1048,16 @@ export class GatewaySocket extends EventEmitter { }, delay); } + private reconnectSpread(): number { + return Math.floor(Math.random() * GATEWAY_TIMEOUTS.ReconnectSpread); + } + private nextReconnectDelay(): number { const now = Date.now(); const elapsed = now - this.lastReconnectAt; if (elapsed < GATEWAY_TIMEOUTS.MinReconnect) { this.log.debug(`Last reconnect ${elapsed}ms ago, enforcing minimum delay (${GATEWAY_TIMEOUTS.MinReconnect}ms)`); - return GATEWAY_TIMEOUTS.MinReconnect; + return GATEWAY_TIMEOUTS.MinReconnect + this.reconnectSpread(); } this.lastReconnectAt = now; const delay = this.reconnectBackoff.next(); diff --git a/fluxer_app/src/features/user/state/StatusPage.ts b/fluxer_app/src/features/user/state/StatusPage.ts index 8f11a4d41..2b172fcca 100644 --- a/fluxer_app/src/features/user/state/StatusPage.ts +++ b/fluxer_app/src/features/user/state/StatusPage.ts @@ -90,7 +90,11 @@ const POLL_ACTIVE_MAINTENANCE_JITTER_MS = 15 * 1000; const POLL_AFTER_MAINTENANCE_START_MS = 5 * 1000; const POLL_MIN_DELAY_MS = 10 * 1000; const POLL_RESUME_STALE_MS = 30 * 1000; -const STATUS_PAGE_FETCH_OPTIONS: RequestInit = {cache: 'no-store'}; +const STATUS_PAGE_FETCH_TIMEOUT_MS = 10 * 1000; + +function statusPageFetchOptions(): RequestInit { + return {cache: 'no-store', signal: AbortSignal.timeout(STATUS_PAGE_FETCH_TIMEOUT_MS)}; +} export function computePollDelay(scheduledMaintenance: StatusPageMaintenance | null): number { if (scheduledMaintenance?.status === 'in_progress') { @@ -256,12 +260,8 @@ export class StatusPage { private async fetchIncidents(): Promise { try { - const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/summary.json`, STATUS_PAGE_FETCH_OPTIONS); + const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/summary.json`, statusPageFetchOptions()); if (!response.ok) { - runInAction(() => { - this.incident = null; - this.scheduledMaintenance = null; - }); return; } const data: InstatusSummary = await response.json(); @@ -296,16 +296,12 @@ export class StatusPage { }); } catch { logger.warn('Failed to fetch status page'); - runInAction(() => { - this.incident = null; - this.scheduledMaintenance = null; - }); } } private async fetchComponentMaintenances(): Promise> { try { - const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/components.json`, STATUS_PAGE_FETCH_OPTIONS); + const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/components.json`, statusPageFetchOptions()); if (!response.ok) { return []; } @@ -321,6 +317,10 @@ export class StatusPage { this.incident = null; } + refreshForConnectionIssue(): void { + this.refreshIfStale(); + } + private async refreshAndReschedule(): Promise { try { await this.checkIncidents();