fix(web): use cached session lists on session detail

This commit is contained in:
i-trytoohard 2026-05-16 20:48:14 +05:30
parent 406b26e837
commit 408cb4571b
2 changed files with 37 additions and 27 deletions

View File

@ -114,7 +114,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return {
ok: true,
status: 200,
@ -122,7 +122,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?project=my-app&orchestratorOnly=true&fresh=true") {
if (url === "/api/sessions?project=my-app&orchestratorOnly=true") {
return {
ok: true,
status: 200,
@ -156,12 +156,12 @@ describe("SessionPage project polling", () => {
expect.any(Object),
);
expect(fetch).toHaveBeenCalledWith(
"/api/sessions?fresh=true",
"/api/sessions",
expect.any(Object),
);
expect(fetch).toHaveBeenCalledWith(
"/api/sessions?project=my-app&orchestratorOnly=true&fresh=true",
"/api/sessions?project=my-app&orchestratorOnly=true",
expect.any(Object),
);
@ -169,7 +169,7 @@ describe("SessionPage project polling", () => {
vi
.mocked(fetch)
.mock.calls.filter(
([url]) => url === "/api/sessions?project=my-app&orchestratorOnly=true&fresh=true",
([url]) => url === "/api/sessions?project=my-app&orchestratorOnly=true",
),
).toHaveLength(1);
@ -182,13 +182,16 @@ describe("SessionPage project polling", () => {
vi
.mocked(fetch)
.mock.calls.filter(
([url]) => url === "/api/sessions?project=my-app&orchestratorOnly=true&fresh=true",
([url]) => url === "/api/sessions?project=my-app&orchestratorOnly=true",
),
).toHaveLength(1);
expect(
vi.mocked(fetch).mock.calls.filter(([url]) => url === "/api/sessions?fresh=true"),
vi.mocked(fetch).mock.calls.filter(([url]) => url === "/api/sessions"),
).toHaveLength(3);
expect(
vi.mocked(fetch).mock.calls.filter(([url]) => String(url).includes("fresh=true")),
).toHaveLength(0);
});
it("does not deadlock project polling after a cached worker poll is skipped", async () => {
@ -219,7 +222,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return {
ok: true,
status: 200,
@ -227,7 +230,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?project=my-app&orchestratorOnly=true&fresh=true") {
if (url === "/api/sessions?project=my-app&orchestratorOnly=true") {
return {
ok: true,
status: 200,
@ -235,7 +238,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?project=my-app&fresh=true") {
if (url === "/api/sessions?project=my-app") {
return {
ok: true,
status: 200,
@ -257,7 +260,7 @@ describe("SessionPage project polling", () => {
await flushAsyncWork();
expect(fetch).toHaveBeenCalledWith(
"/api/sessions?project=my-app&fresh=true",
"/api/sessions?project=my-app",
expect.any(Object),
);
});
@ -355,7 +358,7 @@ describe("SessionPage project polling", () => {
});
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return Promise.resolve({
ok: true,
status: 200,
@ -418,7 +421,7 @@ describe("SessionPage project polling", () => {
return Promise.reject(new DOMException("Aborted", "AbortError"));
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return Promise.resolve({
ok: true,
status: 200,
@ -477,7 +480,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return {
ok: true,
status: 200,
@ -485,7 +488,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?project=my-app&orchestratorOnly=true&fresh=true") {
if (url === "/api/sessions?project=my-app&orchestratorOnly=true") {
return {
ok: true,
status: 200,
@ -507,7 +510,7 @@ describe("SessionPage project polling", () => {
await flushAsyncWork();
expect(fetchMock.mock.calls.filter(([url]) => url === "/api/projects")).toHaveLength(2);
expect(fetchMock.mock.calls.filter(([url]) => url === "/api/sessions?fresh=true")).toHaveLength(
expect(fetchMock.mock.calls.filter(([url]) => url === "/api/sessions")).toHaveLength(
2,
);
});
@ -536,7 +539,7 @@ describe("SessionPage project polling", () => {
} as Response);
}
if (url === "/api/sessions?project=my-app&orchestratorOnly=true&fresh=true") {
if (url === "/api/sessions?project=my-app&orchestratorOnly=true") {
return Promise.resolve({
ok: true,
status: 200,
@ -544,7 +547,7 @@ describe("SessionPage project polling", () => {
} as Response);
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return new Promise<Response>((_, reject) => {
init?.signal?.addEventListener(
"abort",
@ -597,7 +600,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return {
ok: true,
status: 200,
@ -605,7 +608,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?project=my-app&orchestratorOnly=true&fresh=true") {
if (url === "/api/sessions?project=my-app&orchestratorOnly=true") {
return {
ok: true,
status: 200,
@ -649,7 +652,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return {
ok: true,
status: 200,
@ -657,7 +660,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?project=broken-app&orchestratorOnly=true&fresh=true") {
if (url === "/api/sessions?project=broken-app&orchestratorOnly=true") {
return {
ok: true,
status: 200,
@ -704,7 +707,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?fresh=true") {
if (url === "/api/sessions") {
return {
ok: true,
status: 200,
@ -712,7 +715,7 @@ describe("SessionPage project polling", () => {
} as Response;
}
if (url === "/api/sessions?project=other-app&orchestratorOnly=true&fresh=true") {
if (url === "/api/sessions?project=other-app&orchestratorOnly=true") {
return {
ok: true,
status: 200,

View File

@ -616,9 +616,13 @@ export default function SessionPage() {
const controller = new AbortController();
projectSessionsFetchControllerRef.current = controller;
try {
// Use the cached list path here. The session-detail sidebar refreshes on
// mount and every poll tick; forcing `fresh=true` from this page defeats
// sessionManager.listCached() and fans out tmux/ps probes while the page's
// own session request is trying to load.
const query = isOrchestrator
? `/api/sessions?project=${encodeURIComponent(projectId)}&fresh=true`
: `/api/sessions?project=${encodeURIComponent(projectId)}&orchestratorOnly=true&fresh=true`;
? `/api/sessions?project=${encodeURIComponent(projectId)}`
: `/api/sessions?project=${encodeURIComponent(projectId)}&orchestratorOnly=true`;
const body = await fetchJsonWithTimeout<ProjectSessionsBody>(query, {
signal: controller.signal,
timeoutMs: PROJECT_SIDEBAR_FETCH_TIMEOUT_MS,
@ -676,10 +680,13 @@ export default function SessionPage() {
const controller = new AbortController();
sidebarFetchControllerRef.current = controller;
try {
// This poll runs frequently and is fed by mux patches for freshness; do
// not force the uncached `fresh=true` list path from the session-detail
// page while the critical `/api/sessions/:id` request is loading.
const body = await fetchJsonWithTimeout<{
sessions?: DashboardSession[];
orchestrators?: DashboardOrchestratorLink[];
} | null>("/api/sessions?fresh=true", {
} | null>("/api/sessions", {
signal: controller.signal,
timeoutMs: PROJECT_SIDEBAR_FETCH_TIMEOUT_MS,
timeoutMessage: `Sidebar sessions request timed out after ${PROJECT_SIDEBAR_FETCH_TIMEOUT_MS}ms`,