perf: refactor sse handleing

This commit is contained in:
d-kimsuon
2025-09-18 20:42:44 +09:00
parent a90ef520dd
commit eb5a8ddeeb
38 changed files with 727 additions and 597 deletions

View File

@@ -0,0 +1,33 @@
"use client";
import { useQueryClient } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import type { FC, PropsWithChildren } from "react";
import { projectDetailQuery, sessionDetailQuery } from "../../lib/api/queries";
import { useServerEventListener } from "../../lib/sse/hook/useServerEventListener";
import { aliveTasksAtom } from "../projects/[projectId]/sessions/[sessionId]/store/aliveTasksAtom";
export const SSEEventListeners: FC<PropsWithChildren> = ({ children }) => {
const queryClient = useQueryClient();
const setAliveTasks = useSetAtom(aliveTasksAtom);
useServerEventListener("sessionListChanged", async (event) => {
// invalidate session list
await queryClient.invalidateQueries({
queryKey: projectDetailQuery(event.projectId).queryKey,
});
});
useServerEventListener("sessionChanged", async (event) => {
// invalidate session detail
await queryClient.invalidateQueries({
queryKey: sessionDetailQuery(event.projectId, event.sessionId).queryKey,
});
});
useServerEventListener("taskChanged", async (event) => {
setAliveTasks(event.aliveTasks);
});
return <>{children}</>;
};

View File

@@ -1,13 +0,0 @@
"use client";
import { useServerEvents } from "@/hooks/useServerEvents";
interface ServerEventsProviderProps {
children: React.ReactNode;
}
export function ServerEventsProvider({ children }: ServerEventsProviderProps) {
useServerEvents();
return <>{children}</>;
}

View File

@@ -5,21 +5,15 @@ import {
} from "@tanstack/react-query";
import { useCallback } from "react";
import { honoClient } from "../../lib/api/client";
import { configQuery } from "../../lib/api/queries";
import type { Config } from "../../server/config/config";
export const configQueryConfig = {
queryKey: ["config"],
queryFn: async () => {
const response = await honoClient.api.config.$get();
return await response.json();
},
} as const;
export const useConfig = () => {
const queryClient = useQueryClient();
const { data } = useSuspenseQuery({
...configQueryConfig,
queryKey: configQuery.queryKey,
queryFn: configQuery.queryFn,
});
const updateConfigMutation = useMutation({
mutationFn: async (config: Config) => {
@@ -30,7 +24,7 @@ export const useConfig = () => {
},
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: configQueryConfig.queryKey,
queryKey: configQuery.queryKey,
});
},
});

View File

@@ -1,13 +1,15 @@
import { QueryClient } from "@tanstack/react-query";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Toaster } from "../components/ui/sonner";
import { QueryClientProviderWrapper } from "../lib/api/QueryClientProviderWrapper";
import { SSEProvider } from "../lib/sse/components/SSEProvider";
import { RootErrorBoundary } from "./components/RootErrorBoundary";
import { ServerEventsProvider } from "./components/ServerEventsProvider";
import "./globals.css";
import { QueryClient } from "@tanstack/react-query";
import { configQueryConfig } from "./hooks/useConfig";
import { configQuery } from "../lib/api/queries";
import { SSEEventListeners } from "./components/SSEEventListeners";
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
@@ -35,7 +37,8 @@ export default async function RootLayout({
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
...configQueryConfig,
queryKey: configQuery.queryKey,
queryFn: configQuery.queryFn,
});
return (
@@ -45,7 +48,9 @@ export default async function RootLayout({
>
<RootErrorBoundary>
<QueryClientProviderWrapper>
<ServerEventsProvider>{children}</ServerEventsProvider>
<SSEProvider>
<SSEEventListeners>{children}</SSEEventListeners>
</SSEProvider>
</QueryClientProviderWrapper>
</RootErrorBoundary>
<Toaster position="top-right" />

View File

@@ -25,8 +25,9 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { projectDetailQuery } from "../../../../lib/api/queries";
import { useConfig } from "../../../hooks/useConfig";
import { projectQueryConfig, useProject } from "../hooks/useProject";
import { useProject } from "../hooks/useProject";
import { firstCommandToTitle } from "../services/firstCommandToTitle";
import { NewChatModal } from "./newChat/NewChatModal";
@@ -41,7 +42,7 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
// biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when config changed
useEffect(() => {
void queryClient.invalidateQueries({
queryKey: projectQueryConfig(projectId).queryKey,
queryKey: projectDetailQuery(projectId).queryKey,
});
}, [config.hideNoUserMessageSession, config.unifySameTitleSession]);

View File

@@ -15,7 +15,7 @@ import {
Collapsible,
CollapsibleContent,
} from "../../../../../components/ui/collapsible";
import { honoClient } from "../../../../../lib/api/client";
import { claudeCommandsQuery } from "../../../../../lib/api/queries";
import { cn } from "../../../../../lib/utils";
type CommandCompletionProps = {
@@ -40,18 +40,8 @@ export const CommandCompletion = forwardRef<
// コマンドリストを取得
const { data: commandData } = useQuery({
queryKey: ["claude-commands", projectId],
queryFn: async () => {
const response = await honoClient.api.projects[":projectId"][
"claude-commands"
].$get({
param: { projectId },
});
if (!response.ok) {
throw new Error("Failed to fetch commands");
}
return response.json();
},
queryKey: claudeCommandsQuery(projectId).queryKey,
queryFn: claudeCommandsQuery(projectId).queryFn,
staleTime: 1000 * 60 * 5, // 5分間キャッシュ
});

View File

@@ -1,21 +1,10 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { honoClient } from "../../../../lib/api/client";
export const projectQueryConfig = (projectId: string) =>
({
queryKey: ["projects", projectId],
queryFn: async () => {
const response = await honoClient.api.projects[":projectId"].$get({
param: { projectId },
});
return await response.json();
},
}) as const;
import { projectDetailQuery } from "../../../../lib/api/queries";
export const useProject = (projectId: string) => {
return useSuspenseQuery({
...projectQueryConfig(projectId),
queryKey: projectDetailQuery(projectId).queryKey,
queryFn: projectDetailQuery(projectId).queryFn,
refetchOnReconnect: true,
});
};

View File

@@ -3,8 +3,8 @@ import {
HydrationBoundary,
QueryClient,
} from "@tanstack/react-query";
import { projectDetailQuery } from "../../../lib/api/queries";
import { ProjectPageContent } from "./components/ProjectPage";
import { projectQueryConfig } from "./hooks/useProject";
interface ProjectPageProps {
params: Promise<{ projectId: string }>;
@@ -16,7 +16,8 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
...projectQueryConfig(projectId),
queryKey: projectDetailQuery(projectId).queryKey,
queryFn: projectDetailQuery(projectId).queryFn,
});
return (

View File

@@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { RefreshCwIcon } from "lucide-react";
import type { FC } from "react";
import { Button } from "@/components/ui/button";
import { honoClient } from "@/lib/api/client";
import { mcpListQuery } from "../../../../../../../lib/api/queries";
export const McpTab: FC = () => {
const queryClient = useQueryClient();
@@ -14,18 +14,12 @@ export const McpTab: FC = () => {
isLoading,
error,
} = useQuery({
queryKey: ["mcp", "list"],
queryFn: async () => {
const response = await honoClient.api.mcp.list.$get();
if (!response.ok) {
throw new Error("Failed to fetch MCP servers");
}
return response.json();
},
queryKey: mcpListQuery.queryKey,
queryFn: mcpListQuery.queryFn,
});
const handleReload = () => {
queryClient.invalidateQueries({ queryKey: ["mcp", "list"] });
queryClient.invalidateQueries({ queryKey: mcpListQuery.queryKey });
};
return (

View File

@@ -1,24 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useMemo } from "react";
import { honoClient } from "../../../../../../lib/api/client";
import { aliveTasksQuery } from "../../../../../../lib/api/queries";
import { aliveTasksAtom } from "../store/aliveTasksAtom";
export const useAliveTask = (sessionId: string) => {
const [aliveTasks, setAliveTasks] = useAtom(aliveTasksAtom);
useQuery({
queryKey: ["aliveTasks"],
queryKey: aliveTasksQuery.queryKey,
queryFn: async () => {
const response = await honoClient.api.tasks.alive.$get({});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json();
setAliveTasks(data.aliveTasks);
return response.json();
const { aliveTasks } = await aliveTasksQuery.queryFn();
setAliveTasks(aliveTasks);
return aliveTasks;
},
refetchOnReconnect: true,
});

View File

@@ -1,42 +1,22 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { honoClient } from "@/lib/api/client";
import {
gitBranchesQuery,
gitCommitsQuery,
} from "../../../../../../lib/api/queries";
export const useGitBranches = (projectId: string) => {
return useQuery({
queryKey: ["git", "branches", projectId],
queryFn: async () => {
const response = await honoClient.api.projects[
":projectId"
].git.branches.$get({
param: { projectId },
});
if (!response.ok) {
throw new Error(`Failed to fetch branches: ${response.statusText}`);
}
return response.json();
},
queryKey: gitBranchesQuery(projectId).queryKey,
queryFn: gitBranchesQuery(projectId).queryFn,
staleTime: 30000, // 30 seconds
});
};
export const useGitCommits = (projectId: string) => {
return useQuery({
queryKey: ["git", "commits", projectId],
queryFn: async () => {
const response = await honoClient.api.projects[
":projectId"
].git.commits.$get({
param: { projectId },
});
if (!response.ok) {
throw new Error(`Failed to fetch commits: ${response.statusText}`);
}
return response.json();
},
queryKey: gitCommitsQuery(projectId).queryKey,
queryFn: gitCommitsQuery(projectId).queryFn,
staleTime: 30000, // 30 seconds
});
};

View File

@@ -1,25 +1,9 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { honoClient } from "../../../../../../lib/api/client";
export const sessionQueryConfig = (projectId: string, sessionId: string) =>
({
queryKey: ["sessions", sessionId],
queryFn: async () => {
const response = await honoClient.api.projects[":projectId"].sessions[
":sessionId"
].$get({
param: {
projectId,
sessionId,
},
});
return response.json();
},
}) as const;
import { sessionDetailQuery } from "../../../../../../lib/api/queries";
export const useSessionQuery = (projectId: string, sessionId: string) => {
return useSuspenseQuery({
...sessionQueryConfig(projectId, sessionId),
queryKey: sessionDetailQuery(projectId, sessionId).queryKey,
queryFn: sessionDetailQuery(projectId, sessionId).queryFn,
});
};

View File

@@ -1,8 +1,10 @@
import { QueryClient } from "@tanstack/react-query";
import type { Metadata } from "next";
import { projectQueryConfig } from "../../hooks/useProject";
import {
projectDetailQuery,
sessionDetailQuery,
} from "../../../../../lib/api/queries";
import { SessionPageContent } from "./components/SessionPageContent";
import { sessionQueryConfig } from "./hooks/useSessionQuery";
type PageParams = {
projectId: string;
@@ -19,11 +21,12 @@ export async function generateMetadata({
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
...sessionQueryConfig(projectId, sessionId),
...sessionDetailQuery(projectId, sessionId),
});
await queryClient.prefetchQuery({
...projectQueryConfig(projectId),
queryKey: projectDetailQuery(projectId).queryKey,
queryFn: projectDetailQuery(projectId).queryFn,
});
return {

View File

@@ -14,7 +14,9 @@ import {
import { useProjects } from "../hooks/useProjects";
export const ProjectList: FC = () => {
const { data: projects } = useProjects();
const {
data: { projects },
} = useProjects();
if (projects.length === 0) {
<Card>

View File

@@ -1,18 +1,9 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { honoClient } from "../../../lib/api/client";
export const projetsQueryConfig = {
queryKey: ["projects"],
queryFn: async () => {
const response = await honoClient.api.projects.$get();
const { projects } = await response.json();
return projects;
},
} as const;
import { projectListQuery } from "../../../lib/api/queries";
export const useProjects = () => {
return useSuspenseQuery({
queryKey: projetsQueryConfig.queryKey,
queryFn: projetsQueryConfig.queryFn,
queryKey: projectListQuery.queryKey,
queryFn: projectListQuery.queryFn,
});
};

View File

@@ -1,7 +1,7 @@
import { QueryClient } from "@tanstack/react-query";
import { HistoryIcon } from "lucide-react";
import { projectListQuery } from "../../lib/api/queries";
import { ProjectList } from "./components/ProjectList";
import { projetsQueryConfig } from "./hooks/useProjects";
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
@@ -10,8 +10,8 @@ export default async function ProjectsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: projetsQueryConfig.queryKey,
queryFn: projetsQueryConfig.queryFn,
queryKey: projectListQuery.queryKey,
queryFn: projectListQuery.queryFn,
});
return (