Skip to content

Commit 90eea04

Browse files
Skip healthy env reconnects after browser resume (pingdotgg#2677)
1 parent f92e1e1 commit 90eea04

3 files changed

Lines changed: 59 additions & 2 deletions

File tree

apps/web/src/environments/runtime/service.threadSubscriptions.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ describe("retainThreadDetailSubscription", () => {
176176
},
177177
})),
178178
},
179+
isHeartbeatFresh: vi.fn(() => true),
179180
orchestration: {
180181
subscribeThread: mockSubscribeThread,
181182
},
@@ -382,7 +383,7 @@ describe("retainThreadDetailSubscription", () => {
382383
await resetEnvironmentServiceForTests();
383384
});
384385

385-
it("reconnects environment streams when the browser resumes from the background", async () => {
386+
it("keeps healthy environment streams connected when the browser resumes from the background", async () => {
386387
let visibilityState: DocumentVisibilityState = "visible";
387388
const documentTarget = new EventTarget();
388389
const windowTarget = new EventTarget();
@@ -408,6 +409,57 @@ describe("retainThreadDetailSubscription", () => {
408409
documentTarget.dispatchEvent(new Event("visibilitychange"));
409410
expect(mockConnectionReconnects[0]).not.toHaveBeenCalled();
410411

412+
visibilityState = "visible";
413+
documentTarget.dispatchEvent(new Event("visibilitychange"));
414+
expect(mockConnectionReconnects[0]).not.toHaveBeenCalled();
415+
416+
stop();
417+
await resetEnvironmentServiceForTests();
418+
});
419+
420+
it("reconnects stale environment streams when the browser resumes from the background", async () => {
421+
let visibilityState: DocumentVisibilityState = "visible";
422+
const documentTarget = new EventTarget();
423+
const windowTarget = new EventTarget();
424+
vi.stubGlobal("document", {
425+
addEventListener: documentTarget.addEventListener.bind(documentTarget),
426+
removeEventListener: documentTarget.removeEventListener.bind(documentTarget),
427+
get visibilityState() {
428+
return visibilityState;
429+
},
430+
});
431+
vi.stubGlobal("window", {
432+
addEventListener: windowTarget.addEventListener.bind(windowTarget),
433+
removeEventListener: windowTarget.removeEventListener.bind(windowTarget),
434+
});
435+
mockCreateWsRpcClient.mockReturnValue({
436+
server: {
437+
getConfig: vi.fn(async () => ({
438+
environment: {
439+
environmentId: EnvironmentId.make("env-remote"),
440+
label: "Remote env",
441+
platform: { os: "darwin", arch: "arm64" },
442+
serverVersion: "0.0.0-test",
443+
capabilities: { repositoryIdentity: true },
444+
},
445+
})),
446+
},
447+
isHeartbeatFresh: vi.fn(() => false),
448+
orchestration: {
449+
subscribeThread: mockSubscribeThread,
450+
},
451+
});
452+
453+
const { resetEnvironmentServiceForTests, startEnvironmentConnectionService } =
454+
await import("./service");
455+
456+
const stop = startEnvironmentConnectionService(new QueryClient());
457+
expect(mockConnectionReconnects).toHaveLength(1);
458+
459+
visibilityState = "hidden";
460+
documentTarget.dispatchEvent(new Event("visibilitychange"));
461+
expect(mockConnectionReconnects[0]).not.toHaveBeenCalled();
462+
411463
visibilityState = "visible";
412464
documentTarget.dispatchEvent(new Event("visibilitychange"));
413465
expect(mockConnectionReconnects[0]).toHaveBeenCalledTimes(1);

apps/web/src/environments/runtime/service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1482,9 +1482,12 @@ function reconnectEnvironmentConnectionsAfterBrowserResume(reason: string): void
14821482
if (now - lastBrowserResumeReconnectAt < BROWSER_RESUME_RECONNECT_COOLDOWN_MS) {
14831483
return;
14841484
}
1485-
lastBrowserResumeReconnectAt = now;
14861485

14871486
for (const connection of environmentConnections.values()) {
1487+
if (connection.client.isHeartbeatFresh()) {
1488+
continue;
1489+
}
1490+
lastBrowserResumeReconnectAt = now;
14881491
void connection.reconnect().catch((error) => {
14891492
console.warn("Environment reconnect after browser resume failed", {
14901493
environmentId: connection.environmentId,

apps/web/src/rpc/wsRpcClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ interface GitRunStackedActionOptions {
5656
export interface WsRpcClient {
5757
readonly dispose: () => Promise<void>;
5858
readonly reconnect: () => Promise<void>;
59+
readonly isHeartbeatFresh: () => boolean;
5960
readonly terminal: {
6061
readonly open: RpcUnaryMethod<typeof WS_METHODS.terminalOpen>;
6162
readonly write: RpcUnaryMethod<typeof WS_METHODS.terminalWrite>;
@@ -158,6 +159,7 @@ export function createWsRpcClient(transport: WsTransport): WsRpcClient {
158159
resetWsReconnectBackoff();
159160
await transport.reconnect();
160161
},
162+
isHeartbeatFresh: () => transport.isHeartbeatFresh(),
161163
terminal: {
162164
open: (input) => transport.request((client) => client[WS_METHODS.terminalOpen](input)),
163165
write: (input) => transport.request((client) => client[WS_METHODS.terminalWrite](input)),

0 commit comments

Comments
 (0)