import { type InfiniteData, keepPreviousData, type QueryClient, useInfiniteQuery, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query' import { isApiClientError } from '@/lib/api/client/errors' import { requestJson } from '@/lib/api/client/request' import { cancelWorkflowExecutionContract, type DashboardStatsResponse, type ExecutionSnapshotData, getDashboardStatsContract, getExecutionSnapshotContract, getLogByExecutionIdContract, getLogDetailContract, listLogsContract, type WorkflowLogDetail, type WorkflowLogSummary, type WorkflowStats, } from '@/lib/api/contracts/logs' import { getEndDateFromTimeRange, getStartDateFromTimeRange } from '@/lib/logs/filters' import { parseQuery, queryToApiParams } from '@/lib/logs/query-parser' import type { TimeRange } from '@/stores/logs/filters/types' export type { DashboardStatsResponse, WorkflowStats } export type LogSortBy = 'date' | 'duration' | 'cost' | 'status' export type LogSortOrder = 'asc' | 'desc' export const LOG_LIST_STALE_TIME = 30 * 1000 export const LOG_DETAIL_STALE_TIME = 30 * 1000 export const LOG_BY_EXECUTION_STALE_TIME = 30 * 1000 export const LOG_DASHBOARD_STATS_STALE_TIME = 30 * 1000 export const EXECUTION_SNAPSHOT_STALE_TIME = 5 * 60 * 1000 export const logKeys = { all: ['logs'] as const, lists: () => [...logKeys.all, 'list'] as const, list: (workspaceId: string | undefined, filters: LogFilters) => [...logKeys.lists(), workspaceId ?? '', filters] as const, details: () => [...logKeys.all, 'detail'] as const, detail: (workspaceId: string | undefined, logId: string | undefined) => [...logKeys.details(), workspaceId ?? '', logId ?? ''] as const, byExecutionAll: () => [...logKeys.all, 'byExecution'] as const, byExecution: (workspaceId: string | undefined, executionId: string | undefined) => [...logKeys.byExecutionAll(), workspaceId ?? '', executionId ?? ''] as const, stats: () => [...logKeys.all, 'stats'] as const, stat: (workspaceId: string | undefined, filters: object) => [...logKeys.stats(), workspaceId ?? '', filters] as const, executionSnapshots: () => [...logKeys.all, 'executionSnapshot'] as const, executionSnapshot: (executionId: string | undefined) => [...logKeys.executionSnapshots(), executionId ?? ''] as const, } export interface LogFilters { timeRange: TimeRange startDate?: string endDate?: string level: string workflowIds: string[] folderIds: string[] triggers: string[] searchQuery: string limit: number sortBy: LogSortBy sortOrder: LogSortOrder } function applyFilterParams( params: URLSearchParams, filters: Omit ): void { if (filters.level !== 'all') { params.set('level', filters.level) } if (filters.triggers.length > 0) { params.set('triggers', filters.triggers.join(',')) } if (filters.workflowIds.length > 0) { params.set('workflowIds', filters.workflowIds.join(',')) } if (filters.folderIds.length > 0) { params.set('folderIds', filters.folderIds.join(',')) } const startDate = getStartDateFromTimeRange(filters.timeRange, filters.startDate) if (startDate) { params.set('startDate', startDate.toISOString()) } const endDate = getEndDateFromTimeRange(filters.timeRange, filters.endDate) if (endDate) { params.set('endDate', endDate.toISOString()) } if (filters.searchQuery.trim()) { const parsedQuery = parseQuery(filters.searchQuery.trim()) const searchParams = queryToApiParams(parsedQuery) for (const [key, value] of Object.entries(searchParams)) { params.set(key, value) } } } function buildListQuery(workspaceId: string, filters: LogFilters, cursor: string | null) { const params = new URLSearchParams() applyFilterParams(params, filters) return { workspaceId, limit: filters.limit, sortBy: filters.sortBy, sortOrder: filters.sortOrder, ...(cursor ? { cursor } : {}), ...Object.fromEntries(params.entries()), } } interface LogsPage { logs: WorkflowLogSummary[] nextCursor: string | null } async function fetchLogsPage( workspaceId: string, filters: LogFilters, cursor: string | null, signal?: AbortSignal ): Promise { const apiData = await requestJson(listLogsContract, { query: buildListQuery(workspaceId, filters, cursor), signal, }) return { logs: apiData.data, nextCursor: apiData.nextCursor, } } export async function fetchLogDetail( logId: string, workspaceId: string, signal?: AbortSignal ): Promise { const { data } = await requestJson(getLogDetailContract, { params: { id: logId }, query: { workspaceId }, signal, }) return data } interface UseLogsListOptions { enabled?: boolean refetchInterval?: number | false } export function useLogsList( workspaceId: string | undefined, filters: LogFilters, options?: UseLogsListOptions ) { return useInfiniteQuery({ queryKey: logKeys.list(workspaceId, filters), queryFn: ({ pageParam, signal }) => fetchLogsPage(workspaceId as string, filters, pageParam, signal), enabled: Boolean(workspaceId) && (options?.enabled ?? true), refetchInterval: options?.refetchInterval ?? false, staleTime: LOG_LIST_STALE_TIME, placeholderData: keepPreviousData, initialPageParam: null as string | null, getNextPageParam: (lastPage) => lastPage.nextCursor, }) } interface UseLogDetailOptions { enabled?: boolean refetchInterval?: | number | false | ((query: { state: { data?: WorkflowLogDetail } }) => number | false | undefined) } export function useLogDetail( logId: string | undefined, workspaceId: string | undefined, options?: UseLogDetailOptions ) { return useQuery({ queryKey: logKeys.detail(workspaceId, logId), queryFn: ({ signal }) => fetchLogDetail(logId as string, workspaceId as string, signal), enabled: Boolean(logId) && Boolean(workspaceId) && (options?.enabled ?? true), refetchInterval: options?.refetchInterval ?? false, staleTime: LOG_DETAIL_STALE_TIME, retry: (failureCount, err) => !(isApiClientError(err) && err.status === 404) && failureCount < 3, }) } export function useLogByExecutionId( workspaceId: string | undefined, executionId: string | null | undefined ) { const queryClient = useQueryClient() return useQuery({ queryKey: logKeys.byExecution(workspaceId, executionId ?? undefined), queryFn: async ({ signal }) => { const { data } = await requestJson(getLogByExecutionIdContract, { params: { executionId: executionId as string }, query: { workspaceId: workspaceId as string }, signal, }) queryClient.setQueryData(logKeys.detail(workspaceId, data.id), data) return data }, enabled: Boolean(workspaceId) && Boolean(executionId), staleTime: LOG_BY_EXECUTION_STALE_TIME, }) } export function prefetchLogDetail(queryClient: QueryClient, logId: string, workspaceId: string) { queryClient.prefetchQuery({ queryKey: logKeys.detail(workspaceId, logId), queryFn: ({ signal }) => fetchLogDetail(logId, workspaceId, signal), staleTime: LOG_DETAIL_STALE_TIME, }) } async function fetchDashboardStats( workspaceId: string, filters: Omit, signal?: AbortSignal ): Promise { const params = new URLSearchParams() applyFilterParams(params, filters) return requestJson(getDashboardStatsContract, { query: { workspaceId, ...Object.fromEntries(params.entries()), }, signal, }) } interface UseDashboardStatsOptions { enabled?: boolean refetchInterval?: number | false } export function useDashboardStats( workspaceId: string | undefined, filters: Omit, options?: UseDashboardStatsOptions ) { return useQuery({ queryKey: logKeys.stat(workspaceId, filters), queryFn: ({ signal }) => fetchDashboardStats(workspaceId as string, filters, signal), enabled: Boolean(workspaceId) && (options?.enabled ?? true), refetchInterval: options?.refetchInterval ?? false, staleTime: LOG_DASHBOARD_STATS_STALE_TIME, placeholderData: keepPreviousData, }) } export type { ExecutionSnapshotData } async function fetchExecutionSnapshot( executionId: string, signal?: AbortSignal ): Promise { const data = await requestJson(getExecutionSnapshotContract, { params: { executionId }, signal, }) if (!data) { throw new Error('No execution snapshot data returned') } return data } export function useExecutionSnapshot(executionId: string | undefined) { return useQuery({ queryKey: logKeys.executionSnapshot(executionId), queryFn: ({ signal }) => fetchExecutionSnapshot(executionId as string, signal), enabled: Boolean(executionId), staleTime: EXECUTION_SNAPSHOT_STALE_TIME, }) } export function useCancelExecution(workspaceId: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ workflowId, executionId, }: { workflowId: string executionId: string }) => { const data = await requestJson(cancelWorkflowExecutionContract, { params: { id: workflowId, executionId }, }) if (!data.success) throw new Error('Failed to cancel run') return data }, onMutate: async ({ executionId }) => { await queryClient.cancelQueries({ queryKey: logKeys.lists() }) const previousQueries = queryClient.getQueriesData>({ queryKey: logKeys.lists(), }) let affectedLogId: string | null = null queryClient.setQueriesData>({ queryKey: logKeys.lists() }, (old) => { if (!old) return old return { ...old, pages: old.pages.map((page) => ({ ...page, logs: page.logs.map((log) => { if (log.executionId !== executionId) return log affectedLogId = log.id return { ...log, status: 'cancelling' } }), })), } }) let previousDetail: WorkflowLogDetail | undefined if (affectedLogId) { previousDetail = queryClient.getQueryData( logKeys.detail(workspaceId, affectedLogId) ) if (previousDetail) { queryClient.setQueryData(logKeys.detail(workspaceId, affectedLogId), { ...previousDetail, status: 'cancelling', }) } } return { previousQueries, affectedLogId, previousDetail } }, onError: (_err, _variables, context) => { for (const [queryKey, data] of context?.previousQueries ?? []) { queryClient.setQueryData(queryKey, data) } if (context?.affectedLogId && context.previousDetail !== undefined) { queryClient.setQueryData( logKeys.detail(workspaceId, context.affectedLogId), context.previousDetail ) } }, onSettled: () => { queryClient.invalidateQueries({ queryKey: logKeys.lists() }) queryClient.invalidateQueries({ queryKey: logKeys.details() }) queryClient.invalidateQueries({ queryKey: logKeys.byExecutionAll() }) queryClient.invalidateQueries({ queryKey: logKeys.stats() }) }, }) } export function useRetryExecution() { const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ workflowId, input }: { workflowId: string; input?: unknown }) => { // boundary-raw-fetch: stream response, body is a ReadableStream consumed one chunk at a time const res = await fetch(`/api/workflows/${workflowId}/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input, triggerType: 'manual', stream: true }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || 'Failed to retry execution') } const reader = res.body?.getReader() if (reader) { await reader.read() reader.cancel() } return { started: true } }, onSettled: () => { queryClient.invalidateQueries({ queryKey: logKeys.lists() }) queryClient.invalidateQueries({ queryKey: logKeys.details() }) queryClient.invalidateQueries({ queryKey: logKeys.byExecutionAll() }) queryClient.invalidateQueries({ queryKey: logKeys.stats() }) }, }) }