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
1 change: 0 additions & 1 deletion apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,6 @@ export function AppLayout({ children }: AppLayoutProps) {
);
const threadDetailBootstrapQuery = useThreadDetailBootstrap(threadId ?? "", {
enabled: isThreadView && Boolean(threadId),
timelinePrefetch: isThreadView && Boolean(threadId),
});
const hasThreadDetailBootstrapSettled =
threadDetailBootstrapQuery.isSuccess || threadDetailBootstrapQuery.isError;
Expand Down
22 changes: 13 additions & 9 deletions apps/app/src/hooks/queries/thread-queries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as api from "@/lib/api";
import { sdk } from "@/lib/sdk";
import { makeThreadListEntry } from "@/test/fixtures/thread-list-entries";
import { createQueryClientTestHarness } from "@/test/queryClientTestHarness";
import { createPerfPhaseLog } from "@/test/perf-phase";
import { ARCHIVED_THREADS_PAGE_SIZE } from "./archived-threads-page-size";
import {
sidebarNavigationQueryKey,
Expand Down Expand Up @@ -186,12 +187,16 @@ beforeEach(() => {
});

describe("useThreadDetailBootstrap", () => {
it("starts the timeline request before the thread bootstrap settles", async () => {
it("starts timeline, queued, and pending-interaction reads before the thread bootstrap settles", async () => {
const phase = createPerfPhaseLog();
let resolveThread:
| ((thread: ThreadWithIncludesResponse) => void)
| undefined;
const threadPromise = new Promise<ThreadWithIncludesResponse>((resolve) => {
resolveThread = resolve;
resolveThread = (thread) => {
phase.mark("bootstrap-settled");
resolve(thread);
};
});
vi.mocked(sdk.threads.get).mockReturnValue(threadPromise);
const { wrapper } = createQueryClientTestHarness();
Expand All @@ -207,13 +212,18 @@ describe("useThreadDetailBootstrap", () => {
await waitFor(() => {
expect(sdk.threads.get).toHaveBeenCalledTimes(1);
expect(sdk.threads.timeline).toHaveBeenCalledTimes(1);
expect(sdk.threads.queuedMessages.list).toHaveBeenCalledTimes(1);
expect(sdk.threads.interactions.list).toHaveBeenCalledTimes(1);
});
expect(result.result.current.isPending).toBe(true);
phase.mark("sidecar-reads-started");
expect(phase.names()).not.toContain("bootstrap-settled");

resolveThread?.(THREAD_WITH_INCLUDES);
await waitFor(() => {
expect(result.result.current.isSuccess).toBe(true);
});
phase.expectBefore("sidecar-reads-started", "bootstrap-settled");
});

it("uses the cached timeline sequence and merges a prefetched delta", async () => {
Expand Down Expand Up @@ -263,13 +273,7 @@ describe("useThreadDetailBootstrap", () => {
{ updatedAt: 1 },
);

renderHook(
() =>
useThreadDetailBootstrap("thread-1", {
timelinePrefetch: true,
}),
{ wrapper },
);
renderHook(() => useThreadDetailBootstrap("thread-1"), { wrapper });

await waitFor(() => {
expect(sdk.threads.timeline).toHaveBeenCalledWith({
Expand Down
42 changes: 26 additions & 16 deletions apps/app/src/hooks/queries/thread-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,7 @@ const THREAD_SEARCH_DEBOUNCE_MS = 150;
export const THREAD_SEARCH_LIMIT_PER_GROUP = 20;
const THREAD_SEARCH_MIN_NON_WHITESPACE_CHARS = 2;

interface ThreadDetailBootstrapQueryOptions extends QueryOptions {
timelinePrefetch?: boolean;
}
type ThreadDetailBootstrapQueryOptions = QueryOptions;

export function didThreadDetailBootstrapRefreshAfterMount(query: {
dataUpdatedAt: number;
Expand Down Expand Up @@ -663,19 +661,31 @@ export function useThreadDetailBootstrap(
queryKey: threadDetailBootstrapQueryKey(id),
queryFn: async ({ signal }) => {
const threadId = requireThreadId(id, "useThreadDetailBootstrap");
const timelinePrefetch = options?.timelinePrefetch ?? false;

if (timelinePrefetch) {
void queryClient.prefetchQuery({
queryKey: threadTimelineQueryKey(threadId),
queryFn: ({ signal: timelineSignal }) =>
fetchThreadTimeline({
queryClient,
signal: timelineSignal,
threadId,
}),
});
}
void queryClient.prefetchQuery({
queryKey: threadTimelineQueryKey(threadId),
queryFn: () =>
fetchThreadTimeline({
queryClient,
signal,
threadId,
}),
});
void queryClient.prefetchQuery({
queryKey: threadQueuedMessagesQueryKey(threadId),
queryFn: () =>
sdk.threads.queuedMessages.list({
threadId,
signal,
}),
});
void queryClient.prefetchQuery({
queryKey: threadPendingInteractionsQueryKey(threadId),
queryFn: () =>
sdk.threads.interactions.list({
threadId,
signal,
}),
});

const thread = await sdk.threads.get({
include: "environment,host",
Expand Down
33 changes: 33 additions & 0 deletions apps/app/src/test/perf-phase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { expect } from "vitest";

interface PerfPhaseEvent {
at: number;
name: string;
}

export function createPerfPhaseLog() {
const events: PerfPhaseEvent[] = [];

return {
mark(name: string) {
events.push({ at: performance.now(), name });
},
names(): string[] {
return events.map((event) => event.name);
},
expectBefore(earlier: string, later: string) {
const earlierEvent = events.find((event) => event.name === earlier);
const laterEvent = events.find((event) => event.name === later);
expect(earlierEvent, `missing phase "${earlier}"`).toEqual(
expect.objectContaining({ name: earlier }),
);
expect(laterEvent, `missing phase "${later}"`).toEqual(
expect.objectContaining({ name: later }),
);
expect(
earlierEvent!.at,
`expected "${earlier}" before "${later}"`,
).toBeLessThan(laterEvent!.at);
},
};
}