Skip to content
Merged
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
71 changes: 69 additions & 2 deletions apps/app/src/hooks/queries/environment-queries.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
// @vitest-environment jsdom

import { cleanup, renderHook, waitFor } from "@testing-library/react";
import type { ThreadPullRequest } from "@bb/domain";
import { QueryClient } from "@tanstack/react-query";
import type { Environment, ThreadPullRequest } from "@bb/domain";
import type { EnvironmentPullRequestResponse } from "@bb/server-contract";
import { sdk } from "@/lib/sdk";
import { createQueryClientTestHarness } from "@/test/queryClientTestHarness";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { environmentPullRequestQueryKey } from "./query-keys";
import {
environmentPullRequestQueryKey,
environmentQueryKey,
} from "./query-keys";
import {
buildEnvironmentFilePreview,
getEnvironmentPullRequestRefetchInterval,
getEnvironmentPullRequestStaleTime,
resolveEnvironmentQueryMount,
useEnvironmentPullRequest,
} from "./environment-queries";
import { resolveGitDiffTabStatus } from "@/components/secondary-panel/gitDiffTabEligibility";

vi.mock("@/lib/sdk", () => ({
sdk: { environments: { pullRequest: vi.fn() } },
Expand Down Expand Up @@ -232,3 +238,64 @@ describe("buildEnvironmentFilePreview", () => {
expect(preview.url).toBe(`data:image/png;base64,${pngBase64}`);
});
});

const CACHED_ENVIRONMENT: Environment = {
id: "env-1",
name: "Workspace",
projectId: "project-1",
hostId: "host-1",
path: "/tmp/workspace",
managed: true,
isGitRepo: true,
isWorktree: false,
workspaceProvisionType: "unmanaged",
branchName: "main",
baseBranch: "main",
defaultBranch: "main",
mergeBaseBranch: null,
status: "ready",
createdAt: 1,
updatedAt: 1,
};

describe("resolveEnvironmentQueryMount", () => {
it("enables a cached environment while bootstrap is still in flight and does not refetch", () => {
const queryClient = new QueryClient();
queryClient.setQueryData(environmentQueryKey("env-1"), CACHED_ENVIRONMENT);
expect(
resolveEnvironmentQueryMount({
bootstrapSettled: false,
environmentId: "env-1",
queryClient,
}),
).toEqual({ enabled: true, refetchOnMount: false });
expect(
resolveGitDiffTabStatus({
environmentId: "env-1",
environmentIsGitRepo: CACHED_ENVIRONMENT.isGitRepo,
environmentLoadFailed: false,
hasResolvedThread: true,
}),
).toBe("eligible");
});

it("does not start an environment read until bootstrap when the cache is empty", () => {
expect(
resolveEnvironmentQueryMount({
bootstrapSettled: false,
environmentId: "env-1",
queryClient: new QueryClient(),
}),
).toEqual({ enabled: false, refetchOnMount: false });
});

it("enables after bootstrap settles so a cold open can fetch", () => {
expect(
resolveEnvironmentQueryMount({
bootstrapSettled: true,
environmentId: "env-1",
queryClient: new QueryClient(),
}),
).toEqual({ enabled: true, refetchOnMount: true });
});
});
21 changes: 20 additions & 1 deletion apps/app/src/hooks/queries/environment-queries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery } from "@tanstack/react-query";
import { useQuery, type QueryClient } from "@tanstack/react-query";
import type {
Environment,
ThreadPullRequest,
Expand Down Expand Up @@ -49,9 +49,27 @@ import {
} from "./query-policies";

interface EnvironmentQueryOptions extends QueryOptions {
refetchOnMount?: boolean | "always";
staleTime?: number;
}

export function resolveEnvironmentQueryMount(args: {
bootstrapSettled: boolean;
environmentId: string | null | undefined;
queryClient: QueryClient;
}): { enabled: boolean; refetchOnMount: boolean } {
const hasCachedEnvironment =
Boolean(args.environmentId) &&
args.queryClient.getQueryData(environmentQueryKey(args.environmentId)) !==
undefined;
return {
enabled:
Boolean(args.environmentId) &&
(hasCachedEnvironment || args.bootstrapSettled),
refetchOnMount: args.bootstrapSettled,
};
}

interface BranchQueryOptions extends QueryOptions {
limit?: number;
query?: string;
Expand Down Expand Up @@ -95,6 +113,7 @@ export function useEnvironment(
signal,
}),
enabled,
refetchOnMount: options?.refetchOnMount,
staleTime: options?.staleTime,
});
}
Expand Down
28 changes: 27 additions & 1 deletion apps/app/src/hooks/queries/host-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { QueryClient } from "@tanstack/react-query";
import { describe, expect, it } from "vitest";
import type { Host } from "@bb/domain";
import { selectPrimaryHost } from "./host-queries";
import { resolveHostsQueryMount, selectPrimaryHost } from "./host-queries";
import { hostsQueryKey } from "./query-keys";

function host(overrides: Partial<Host> & Pick<Host, "id">): Host {
return {
Expand Down Expand Up @@ -41,3 +43,27 @@ describe("selectPrimaryHost", () => {
expect(selectPrimaryHost([], null)).toBeNull();
});
});

describe("resolveHostsQueryMount", () => {
it("enables a cached host list while bootstrap is still in flight and does not refetch", () => {
const queryClient = new QueryClient();
queryClient.setQueryData(hostsQueryKey(), [host({ id: "host-1" })]);
expect(
resolveHostsQueryMount({
bootstrapSettled: false,
environmentId: "env-1",
queryClient,
}),
).toEqual({ enabled: true, refetchOnMount: false });
});

it("does not start a hosts read until bootstrap when the cache is empty", () => {
expect(
resolveHostsQueryMount({
bootstrapSettled: false,
environmentId: "env-1",
queryClient: new QueryClient(),
}),
).toEqual({ enabled: false, refetchOnMount: false });
});
});
29 changes: 27 additions & 2 deletions apps/app/src/hooks/queries/host-queries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useMemo } from "react";
import { keepPreviousData, skipToken, useQuery } from "@tanstack/react-query";
import {
keepPreviousData,
skipToken,
useQuery,
type QueryClient,
} from "@tanstack/react-query";
import type { Host } from "@bb/domain";
import type { HostDirectoryListing } from "@bb/server-contract";
import { sdk } from "@/lib/sdk";
Expand All @@ -12,14 +17,34 @@ import {
} from "./query-keys";
import type { QueryOptions } from "./query-helpers";

export function useHosts(options?: QueryOptions) {
interface HostListQueryOptions extends QueryOptions {
refetchOnMount?: boolean | "always";
}

export function resolveHostsQueryMount(args: {
bootstrapSettled: boolean;
environmentId: string | null | undefined;
queryClient: QueryClient;
}): { enabled: boolean; refetchOnMount: boolean } {
const hasEnvironment =
args.environmentId !== null && args.environmentId !== undefined;
const hasCachedHosts =
args.queryClient.getQueryData(hostsQueryKey()) !== undefined;
return {
enabled: hasEnvironment && (hasCachedHosts || args.bootstrapSettled),
refetchOnMount: args.bootstrapSettled,
};
}

export function useHosts(options?: HostListQueryOptions) {
const enabled = options?.enabled ?? true;
useHostListRealtimeSubscription({ enabled });

return useQuery<Host[]>({
queryKey: hostsQueryKey(),
queryFn: ({ signal }) => sdk.hosts.list({ signal }),
enabled,
refetchOnMount: options?.refetchOnMount,
staleTime: 60_000,
});
}
Expand Down
24 changes: 18 additions & 6 deletions apps/app/src/views/thread-detail/ThreadDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useState,
type ReactNode,
} from "react";
import { useQueryClient } from "@tanstack/react-query";
import { nanoid } from "nanoid";
import { useSystemProviderInfo } from "@/hooks/queries/system-queries";
import { useNavigate } from "react-router-dom";
Expand Down Expand Up @@ -58,6 +59,7 @@ import { useUpdateEnvironment } from "../../hooks/mutations/environment-mutation
import {
useEnvironment,
getEnvironmentPullRequestFromResponse,
resolveEnvironmentQueryMount,
useEnvironmentPullRequest,
useEnvironmentWorkStatus,
} from "../../hooks/queries/environment-queries";
Expand Down Expand Up @@ -97,7 +99,7 @@ import { assertNever } from "@bb/thread-view";
import { useCreateThreadInWorktree } from "@/hooks/useCreateThreadInWorktree";
import { useHostDaemon } from "@/hooks/useHostDaemon";
import { useLocalOpenTargets } from "@/hooks/useLocalOpenTargets";
import { useHosts } from "@/hooks/queries/host-queries";
import { resolveHostsQueryMount, useHosts } from "@/hooks/queries/host-queries";
import { useSystemConfig } from "@/hooks/queries/system-queries";
import { useConnectionAwareQueryState } from "@/hooks/queries/connection-aware-query-state";
import {
Expand Down Expand Up @@ -513,6 +515,7 @@ function ThreadDetailViewInternal(props: ThreadRoutePathArgs) {
const { isFocused, navigateInPane, onRequestClose, isBoundedPane } =
usePaneContext();
const navigate = useNavigate();
const queryClient = useQueryClient();
useFixedPanelTabsStorageMaintenance();
const systemConfigQuery = useSystemConfig();
const threadDetailBootstrapQuery = useThreadDetailBootstrap(threadId);
Expand All @@ -526,8 +529,14 @@ function ThreadDetailViewInternal(props: ThreadRoutePathArgs) {
} = useThread(threadId, {
bootstrap: threadDetailBootstrapQuery,
});
const environmentQueryMount = resolveEnvironmentQueryMount({
bootstrapSettled: hasThreadDetailBootstrapSettled,
environmentId: thread?.environmentId,
queryClient,
});
const environmentQuery = useEnvironment(thread?.environmentId, {
enabled: hasThreadDetailBootstrapSettled,
enabled: environmentQueryMount.enabled,
refetchOnMount: environmentQueryMount.refetchOnMount,
staleTime: 5_000,
});
const environment = environmentQuery.data;
Expand Down Expand Up @@ -903,11 +912,14 @@ function ThreadDetailViewInternal(props: ThreadRoutePathArgs) {
terminalsListQuery.data,
updateFixedPanelTabsState,
]);
const hostsQueryMount = resolveHostsQueryMount({
bootstrapSettled: hasThreadDetailBootstrapSettled,
environmentId: thread?.environmentId,
queryClient,
});
const hostsQuery = useHosts({
enabled:
hasThreadDetailBootstrapSettled &&
thread?.environmentId !== null &&
thread?.environmentId !== undefined,
enabled: hostsQueryMount.enabled,
refetchOnMount: hostsQueryMount.refetchOnMount,
});
const connectedHostIds = useMemo(
() =>
Expand Down