Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions web-admin/src/features/projects/project-query-options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
V1DeploymentStatus,
type V1GetProjectResponse,
} from "@rilldata/web-admin/client";
import { RUNTIME_ACCESS_TOKEN_DEFAULT_TTL } from "@rilldata/web-common/runtime-client/constants";
import { describe, expect, it } from "vitest";
import { baseGetProjectQueryOptions } from "./project-query-options";

const refetchInterval = baseGetProjectQueryOptions.refetchInterval;

function poll(data: V1GetProjectResponse | undefined) {
if (typeof refetchInterval !== "function") {
throw new Error("expected refetchInterval to be a function");
}
return refetchInterval({
state: { data },
} as unknown as Parameters<typeof refetchInterval>[0]);
}

describe("baseGetProjectQueryOptions.refetchInterval", () => {
it("polls while a loaded project is hibernating (no deployment)", () => {
expect(poll({ project: { name: "p" } })).toBe(2000);
});

it("polls quickly while the deployment is pending", () => {
expect(
poll({
project: { name: "p" },
deployment: { status: V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING },
}),
).toBe(1000);
});

it("refetches the JWT proactively while running", () => {
expect(
poll({
project: { name: "p" },
deployment: { status: V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING },
}),
).toBe(RUNTIME_ACCESS_TOKEN_DEFAULT_TTL / 2);
});

it("does not poll when there is no data (initial load or error)", () => {
expect(poll(undefined)).toBe(false);
});
});
14 changes: 11 additions & 3 deletions web-admin/src/features/projects/project-query-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import type { CreateQueryOptions } from "@tanstack/svelte-query";
const PollTimeWhenProjectDeploymentPending = 1000;
const PollTimeWhenProjectDeploymentError = 5000;
const PollTimeWhenProjectDeploymentOk = RUNTIME_ACCESS_TOKEN_DEFAULT_TTL / 2; // Proactively refetch the JWT before it expires
const PollTimeWhenProjectHibernating = 2000;

export const baseGetProjectQueryOptions: Partial<
CreateQueryOptions<V1GetProjectResponse, RpcStatus>
> = {
gcTime: Math.min(RUNTIME_ACCESS_TOKEN_DEFAULT_TTL, 1000 * 60 * 5), // Make sure we don't keep a stale JWT in the cache
refetchInterval: (query) => {
const status = query.state.data?.deployment?.status;
switch (status) {
const data = query.state.data;
switch (data?.deployment?.status) {
case V1DeploymentStatus.DEPLOYMENT_STATUS_PENDING:
case V1DeploymentStatus.DEPLOYMENT_STATUS_UPDATING:
case V1DeploymentStatus.DEPLOYMENT_STATUS_STOPPING:
Expand All @@ -26,7 +27,14 @@ export const baseGetProjectQueryOptions: Partial<
case V1DeploymentStatus.DEPLOYMENT_STATUS_RUNNING:
return PollTimeWhenProjectDeploymentOk;
default:
return false;
// A loaded project with no deployment is hibernating. Keep polling so the
// layout auto-progresses once a deployment appears, whether the wake was
// initiated from this tab or elsewhere. Without this, a wake gets stuck on
// the hibernating/"Waking..." screen if the post-wake refetch raced ahead of
// the backend's visible state and polling never re-armed.
return data?.project && !data.deployment
? PollTimeWhenProjectHibernating
: false;
}
},
refetchIntervalInBackground: true, // Keep polling while the tab is hidden (e.g. deploy loader)
Expand Down
Loading