authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-13 22:29:36+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-13 22:29:36+02:00
log90eea047a967ea2edae543e966627ec0a394ef81
treed5bb69ce2d3f08077a1c855fdc818eeb56a58000
parentf92e1e1b06ec5ba78ce66849cf288a3a45e54bb0
signature Signed by PGP key B5690EEEBB952194

Skip healthy env reconnects after browser resume (#2677)


3 files changed, 59 insertions(+), 2 deletions(-)

apps/web/src/environments/runtime/service.threadSubscriptions.test.ts+53-1
......@@ -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+4-1
......@@ -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+2
......@@ -56,6 +56,7 @@ interface GitRunStackedActionOptions {
5656export 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)),