fix: pause hidden session catalog polling

This commit is contained in:
Shakker
2026-07-18 07:39:16 +01:00
parent 42133d0c3c
commit b5d9573dca
2 changed files with 69 additions and 4 deletions

View File

@@ -175,12 +175,11 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarBase {
override updated() {
this.syncSessionsScrollObserver();
const snapshot = this.context?.gateway.snapshot;
if (this.context) {
this.synchronizeSessionCatalogAgent(this.expandedAgentId());
}
if (
!sessionCatalogListClient(snapshot, this.connected) ||
!this.visibleSessionCatalogClient() ||
this.sessionCatalogLive.timer ||
this.sessionCatalogLive.requestGeneration === this.sessionCatalogGeneration
) {
@@ -189,6 +188,13 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarBase {
void this.refreshSessionCatalogs();
}
private visibleSessionCatalogClient(): GatewayBrowserClient | null {
if (document.visibilityState === "hidden") {
return null;
}
return sessionCatalogListClient(this.context?.gateway.snapshot, this.connected);
}
private synchronizeSessionCatalogAgent(agentId: string) {
if (agentId === this.sessionCatalogAgentId) {
return;
@@ -265,7 +271,9 @@ export abstract class AppSidebarSessionDataElement extends AppSidebarBase {
}
private async refreshSessionCatalogs() {
const client = sessionCatalogListClient(this.context?.gateway.snapshot, this.connected);
// Hidden pages resume through the coalesced activation handler. Starting
// here without a timer makes catalog state updates poll at request latency.
const client = this.visibleSessionCatalogClient();
if (!client) {
return;
}

View File

@@ -1,7 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import type { SessionsCatalogListResult } from "../../../../packages/gateway-protocol/src/index.ts";
import type { GatewayBrowserClient } from "../../api/gateway.ts";
import type { ApplicationGatewaySnapshot } from "../../app/context.ts";
import { catalogPage, createGatewayHarness, createSessions, mountSidebar } from "../app-sidebar.ts";
import {
catalogPage,
createGatewayHarness,
createSessions,
deferred,
mountSidebar,
} from "../app-sidebar.ts";
import "../../components/app-sidebar.ts";
describe("AppSidebar session catalog pagination", () => {
@@ -44,6 +51,56 @@ describe("AppSidebar session catalog pagination", () => {
}
});
it("stays paused when an in-flight catalog refresh finishes in a hidden tab", async () => {
vi.useFakeTimers();
let visibility: DocumentVisibilityState = "visible";
const visibilitySpy = vi
.spyOn(document, "visibilityState", "get")
.mockImplementation(() => visibility);
try {
const pending = deferred<SessionsCatalogListResult>();
const request = vi
.fn()
.mockReturnValueOnce(pending.promise)
.mockResolvedValue(catalogPage([]));
const gateway = createGatewayHarness({ request } as unknown as GatewayBrowserClient);
gateway.publish({
hello: {
features: { methods: ["sessions.catalog.list"] },
} as ApplicationGatewaySnapshot["hello"],
});
const { sidebar } = await mountSidebar(
gateway.gateway,
createSessions("main", ["agent:main:main"]),
);
sidebar.connected = true;
await sidebar.updateComplete;
await vi.advanceTimersByTimeAsync(0);
expect(request).toHaveBeenCalledTimes(1);
visibility = "hidden";
document.dispatchEvent(new Event("visibilitychange"));
pending.resolve(catalogPage([]));
await vi.advanceTimersByTimeAsync(0);
await sidebar.updateComplete;
await vi.advanceTimersByTimeAsync(60_000);
expect(request).toHaveBeenCalledTimes(1);
visibility = "visible";
document.dispatchEvent(new Event("visibilitychange"));
globalThis.dispatchEvent(new Event("focus"));
await vi.advanceTimersByTimeAsync(49);
expect(request).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
expect(request).toHaveBeenCalledTimes(2);
await vi.advanceTimersByTimeAsync(0);
expect(request).toHaveBeenCalledTimes(2);
} finally {
visibilitySpy.mockRestore();
vi.useRealTimers();
}
});
it("does not poll an older Gateway that does not advertise session catalogs", async () => {
vi.useFakeTimers();
try {