From 5dfd5d29a9aa30c027ba98552a16ec1511fc4051 Mon Sep 17 00:00:00 2001 From: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:00:02 -0700 Subject: [PATCH] fix(relay): recover cloud session refresh failures --- .../relay-auth-coordinator-recovery.test.ts | 26 +++++++++++++++++++ .../runtime/relay/relay-auth-coordinator.ts | 6 ++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts b/src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts index 30f8424bcb3..21e7f9aadec 100644 --- a/src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts +++ b/src/main/runtime/relay/relay-auth-coordinator-recovery.test.ts @@ -39,6 +39,32 @@ describe('RelayAuthCoordinator transient recovery', () => { expect(statuses.at(-1)).toBe('registered') }) + it('retries when cloud-session refresh fails before identity can be read', async () => { + vi.useFakeTimers() + const broker = { closeNow: vi.fn() } + const readContext = vi + .fn() + .mockRejectedValueOnce(new Error('temporary cloud session refresh failure')) + .mockResolvedValueOnce(context) + const openBroker = vi.fn().mockResolvedValue(broker) + const coordinator = new RelayAuthCoordinator({ + readContext, + openBroker, + onStatus: vi.fn(), + random: () => 0.5 + }) + + coordinator.reconcile() + await vi.advanceTimersByTimeAsync(0) + expect(readContext).toHaveBeenCalledOnce() + expect(openBroker).not.toHaveBeenCalled() + + await vi.advanceTimersByTimeAsync(501) + expect(readContext).toHaveBeenCalledTimes(2) + expect(openBroker).toHaveBeenCalledOnce() + expect(coordinator.getActiveBroker()).toBe(broker) + }) + it('backs a sustained outage off to the five-minute jitter cap', async () => { vi.useFakeTimers() const openBroker = vi.fn().mockRejectedValue(new Error('temporary control open failure')) diff --git a/src/main/runtime/relay/relay-auth-coordinator.ts b/src/main/runtime/relay/relay-auth-coordinator.ts index 4754db8d546..7f97e33dc41 100644 --- a/src/main/runtime/relay/relay-auth-coordinator.ts +++ b/src/main/runtime/relay/relay-auth-coordinator.ts @@ -112,7 +112,7 @@ export class RelayAuthCoordinator { } private async reconcileEpoch(epoch: number, expectedIdentityKey?: string): Promise { - let retryIdentityKey: string | null = null + let retryIdentityKey: string | undefined try { const context = await this.options.readContext() if (!this.isEpochCurrent(epoch)) { @@ -182,14 +182,14 @@ export class RelayAuthCoordinator { } catch (error) { if (this.isEpochCurrent(epoch)) { this.options.onStatus('offline') - if (retryIdentityKey && shouldRetryRelayConnectionError(error)) { + if (shouldRetryRelayConnectionError(error)) { this.scheduleRetry(epoch, retryIdentityKey) } } } } - private scheduleRetry(epoch: number, expectedIdentityKey: string): void { + private scheduleRetry(epoch: number, expectedIdentityKey?: string): void { if (this.retryTimer || !this.isEpochCurrent(epoch)) { return }