2026-04-20 15:10:54 +02:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
2026-06-03 06:35:03 +02:00
|
|
|
import { isAxiosError } from "axios";
|
2026-04-20 15:10:54 +02:00
|
|
|
import { channelsApi, type ChannelFilters } from "@/lib/api/channels";
|
|
|
|
|
import { sessionsApi } from "@/lib/api/sessions";
|
|
|
|
|
import { messagesApi } from "@/lib/api/messages";
|
|
|
|
|
import { tasksApi } from "@/lib/api/tasks";
|
|
|
|
|
|
2026-06-03 06:35:03 +02:00
|
|
|
// A 404 on a session/message read is terminal: the session has closed and been
|
|
|
|
|
// reaped server-side, so it will never come back. Retrying (or continuing to
|
|
|
|
|
// poll) a 404'd session is exactly what produces the "404 storm" as dead
|
|
|
|
|
// session-ids accumulate. Treat 404 as final — fail fast, never retry.
|
|
|
|
|
function isNotFound(error: unknown): boolean {
|
|
|
|
|
return isAxiosError(error) && error.response?.status === 404;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function retryUnlessNotFound(failureCount: number, error: unknown): boolean {
|
|
|
|
|
if (isNotFound(error)) return false;
|
|
|
|
|
return failureCount < 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 15:10:54 +02:00
|
|
|
export const channelKeys = {
|
|
|
|
|
all: ["channels"] as const,
|
|
|
|
|
list: (filters?: ChannelFilters) => [...channelKeys.all, "list", filters] as const,
|
|
|
|
|
detail: (id: string) => [...channelKeys.all, "detail", id] as const,
|
|
|
|
|
groups: (channelId: string) => [...channelKeys.all, "groups", channelId] as const,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const sessionKeys = {
|
|
|
|
|
all: ["sessions"] as const,
|
|
|
|
|
list: (groupId: string) => [...sessionKeys.all, "list", groupId] as const,
|
|
|
|
|
detail: (id: string) => [...sessionKeys.all, "detail", id] as const,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const messageKeys = {
|
|
|
|
|
all: ["messages"] as const,
|
|
|
|
|
list: (sessionId: string) => [...messageKeys.all, "list", sessionId] as const,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fetch channel list once - manual refresh available
|
|
|
|
|
export function useChannels(filters?: ChannelFilters) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: channelKeys.list(filters),
|
|
|
|
|
queryFn: () => channelsApi.list(filters),
|
|
|
|
|
staleTime: Infinity,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useChannel(channelId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: channelKeys.detail(channelId || ""),
|
|
|
|
|
queryFn: () => channelsApi.get(channelId!),
|
|
|
|
|
enabled: !!channelId,
|
|
|
|
|
staleTime: Infinity,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch groups for a channel
|
|
|
|
|
export function useChannelGroups(channelId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: channelKeys.groups(channelId || ""),
|
|
|
|
|
queryFn: () => channelsApi.getGroups(channelId!),
|
|
|
|
|
enabled: !!channelId,
|
|
|
|
|
staleTime: Infinity,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch sessions for a group
|
|
|
|
|
export function useGroupSessions(groupId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: sessionKeys.list(groupId || ""),
|
|
|
|
|
queryFn: () => sessionsApi.listByGroup(groupId!),
|
|
|
|
|
enabled: !!groupId,
|
|
|
|
|
staleTime: Infinity,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch a single session by ID (with task links and task titles)
|
|
|
|
|
export function useSession(sessionId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: sessionKeys.detail(sessionId || ""),
|
|
|
|
|
queryFn: async () => {
|
|
|
|
|
const session = await sessionsApi.get(sessionId!);
|
|
|
|
|
// Fetch task links separately since the endpoint doesn't include them
|
|
|
|
|
try {
|
|
|
|
|
const taskLinks = await sessionsApi.getTasksForSession(sessionId!);
|
|
|
|
|
|
|
|
|
|
// Fetch task details to get titles
|
|
|
|
|
const taskLinksWithTitles = await Promise.all(
|
|
|
|
|
taskLinks.map(async (link) => {
|
|
|
|
|
try {
|
|
|
|
|
const task = await tasksApi.get(link.task_id);
|
|
|
|
|
return {
|
|
|
|
|
task_id: link.task_id,
|
|
|
|
|
task_title: task.title,
|
|
|
|
|
is_primary: link.is_primary,
|
|
|
|
|
relationship_type: link.relationship_type,
|
|
|
|
|
};
|
|
|
|
|
} catch {
|
|
|
|
|
return {
|
|
|
|
|
task_id: link.task_id,
|
|
|
|
|
task_title: null,
|
|
|
|
|
is_primary: link.is_primary,
|
|
|
|
|
relationship_type: link.relationship_type,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...session,
|
|
|
|
|
task_links: taskLinksWithTitles,
|
|
|
|
|
};
|
|
|
|
|
} catch {
|
|
|
|
|
// If fetching task links fails, return session without them
|
|
|
|
|
return session;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
enabled: !!sessionId,
|
|
|
|
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
2026-06-03 06:35:03 +02:00
|
|
|
// A reaped (404) session must not be retried — that is the storm source.
|
|
|
|
|
retry: retryUnlessNotFound,
|
2026-04-20 15:10:54 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 06:35:03 +02:00
|
|
|
// Fetch messages for a session - WebSocket handles new messages.
|
|
|
|
|
//
|
|
|
|
|
// The transcript is read once and held (staleTime Infinity, no focus/reconnect
|
|
|
|
|
// refetch in the global defaults), so an open session is fetched a single time
|
|
|
|
|
// and a closed session's immutable transcript stays loaded for review. A 404
|
|
|
|
|
// means the session was reaped server-side; that is terminal, so we never retry
|
|
|
|
|
// it — retrying reaped sessions is what produced the growing 404 storm as dead
|
|
|
|
|
// session-ids accumulated. When the consumer unmounts, React Query deactivates
|
|
|
|
|
// the query (no background refetch loop survives) and GCs it after gcTime.
|
2026-04-20 15:10:54 +02:00
|
|
|
export function useSessionMessages(sessionId: string | null) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: messageKeys.list(sessionId || ""),
|
|
|
|
|
queryFn: () => messagesApi.listBySession(sessionId!),
|
|
|
|
|
enabled: !!sessionId,
|
|
|
|
|
staleTime: Infinity,
|
2026-06-03 06:35:03 +02:00
|
|
|
retry: retryUnlessNotFound,
|
2026-04-20 15:10:54 +02:00
|
|
|
});
|
|
|
|
|
}
|