Skip to content

Commit 28adcd5

Browse files
authored
fix: Pull exponential backoff into a util (#494)
...
1 parent 3be7497 commit 28adcd5

3 files changed

Lines changed: 46 additions & 11 deletions

File tree

apps/array/src/main/services/connectivity/service.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { net } from "electron";
22
import { injectable, postConstruct } from "inversify";
3+
import { getBackoffDelay } from "../../../shared/utils/backoff.js";
34
import { logger } from "../../lib/logger.js";
45
import { TypedEventEmitter } from "../../lib/typed-event-emitter.js";
56
import {
@@ -19,7 +20,7 @@ const ONLINE_POLL_INTERVAL_MS = 3_000;
1920
export class ConnectivityService extends TypedEventEmitter<ConnectivityEvents> {
2021
private isOnline = false;
2122
private pollTimeoutId: ReturnType<typeof setTimeout> | null = null;
22-
private currentPollInterval = MIN_POLL_INTERVAL_MS;
23+
private offlinePollAttempt = 0;
2324

2425
@postConstruct()
2526
init(): void {
@@ -45,7 +46,7 @@ export class ConnectivityService extends TypedEventEmitter<ConnectivityEvents> {
4546
log.info("Connectivity status changed", { isOnline: online });
4647
this.emit(ConnectivityEvent.StatusChange, { isOnline: online });
4748

48-
this.currentPollInterval = MIN_POLL_INTERVAL_MS;
49+
this.offlinePollAttempt = 0;
4950
}
5051

5152
private async checkConnectivity(): Promise<void> {
@@ -73,7 +74,7 @@ export class ConnectivityService extends TypedEventEmitter<ConnectivityEvents> {
7374
private startPolling(): void {
7475
if (this.pollTimeoutId) return;
7576

76-
this.currentPollInterval = MIN_POLL_INTERVAL_MS;
77+
this.offlinePollAttempt = 0;
7778
this.schedulePoll();
7879
}
7980

@@ -82,7 +83,11 @@ export class ConnectivityService extends TypedEventEmitter<ConnectivityEvents> {
8283
// when offline: poll more frequently with backoff to detect recovery
8384
const interval = this.isOnline
8485
? ONLINE_POLL_INTERVAL_MS
85-
: this.currentPollInterval;
86+
: getBackoffDelay(this.offlinePollAttempt, {
87+
initialDelayMs: MIN_POLL_INTERVAL_MS,
88+
maxDelayMs: MAX_POLL_INTERVAL_MS,
89+
multiplier: 1.5,
90+
});
8691

8792
this.pollTimeoutId = setTimeout(async () => {
8893
this.pollTimeoutId = null;
@@ -91,10 +96,7 @@ export class ConnectivityService extends TypedEventEmitter<ConnectivityEvents> {
9196
await this.checkConnectivity();
9297

9398
if (!this.isOnline && wasOffline) {
94-
this.currentPollInterval = Math.min(
95-
this.currentPollInterval * 1.5,
96-
MAX_POLL_INTERVAL_MS,
97-
);
99+
this.offlinePollAttempt++;
98100
}
99101

100102
this.schedulePoll();

apps/array/src/renderer/features/auth/stores/authStore.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logger } from "@renderer/lib/logger";
55
import { queryClient } from "@renderer/lib/queryClient";
66
import { trpcVanilla } from "@renderer/trpc/client";
77
import type { CloudRegion } from "@shared/types/oauth";
8+
import { sleepWithBackoff } from "@shared/utils/backoff";
89
import { useNavigationStore } from "@stores/navigationStore";
910
import { create } from "zustand";
1011
import { persist, subscribeWithSelector } from "zustand/middleware";
@@ -176,11 +177,12 @@ export const useAuthStore = create<AuthState>()(
176177
for (let attempt = 0; attempt < REFRESH_MAX_RETRIES; attempt++) {
177178
try {
178179
if (attempt > 0) {
179-
const delay = REFRESH_INITIAL_DELAY_MS * 2 ** (attempt - 1);
180180
log.debug(
181-
`Retrying token refresh (attempt ${attempt + 1}/${REFRESH_MAX_RETRIES}) after ${delay}ms`,
181+
`Retrying token refresh (attempt ${attempt + 1}/${REFRESH_MAX_RETRIES})`,
182182
);
183-
await new Promise((resolve) => setTimeout(resolve, delay));
183+
await sleepWithBackoff(attempt - 1, {
184+
initialDelayMs: REFRESH_INITIAL_DELAY_MS,
185+
});
184186
}
185187

186188
const result = await trpcVanilla.oauth.refreshToken.mutate({
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export interface BackoffOptions {
2+
initialDelayMs: number;
3+
maxDelayMs?: number;
4+
multiplier?: number;
5+
}
6+
7+
/**
8+
* Calculate delay for exponential backoff
9+
* @param attempt - Zero-indexed attempt number (0 = first retry)
10+
* @param options - Backoff configuration
11+
* @returns Delay in milliseconds
12+
*/
13+
export function getBackoffDelay(
14+
attempt: number,
15+
options: BackoffOptions,
16+
): number {
17+
const { initialDelayMs, maxDelayMs, multiplier = 2 } = options;
18+
const delay = initialDelayMs * multiplier ** attempt;
19+
return maxDelayMs ? Math.min(delay, maxDelayMs) : delay;
20+
}
21+
22+
/**
23+
* Sleep with exponential backoff delay
24+
*/
25+
export function sleepWithBackoff(
26+
attempt: number,
27+
options: BackoffOptions,
28+
): Promise<void> {
29+
const delay = getBackoffDelay(attempt, options);
30+
return new Promise((resolve) => setTimeout(resolve, delay));
31+
}

0 commit comments

Comments
 (0)