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
24 changes: 2 additions & 22 deletions archon-ui-main/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from './features/shared/queryClient';
import { KnowledgeBasePage } from './pages/KnowledgeBasePage';
import { SettingsPage } from './pages/SettingsPage';
import { MCPPage } from './pages/MCPPage';
Expand All @@ -18,27 +19,6 @@ import { MigrationBanner } from './components/ui/MigrationBanner';
import { serverHealthService } from './services/serverHealthService';
import { useMigrationStatus } from './hooks/useMigrationStatus';

// Create a client with optimized settings for our polling use case
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Keep data fresh for 2 seconds by default
staleTime: 2000,
// Cache data for 5 minutes
gcTime: 5 * 60 * 1000,
// Retry failed requests 3 times
retry: 3,
// Refetch on window focus
refetchOnWindowFocus: true,
// Don't refetch on reconnect by default (we handle this manually)
refetchOnReconnect: false,
},
mutations: {
// Retry mutations once on failure
retry: 1,
},
},
});

const AppRoutes = () => {
const { projectsEnabled } = useSettings();
Expand Down
17 changes: 6 additions & 11 deletions archon-ui-main/src/components/layout/hooks/useBackendHealth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { callAPIWithETag } from "../../../features/shared/apiWithEtag";
import { createRetryLogic, STALE_TIMES } from "../../../features/shared/queryPatterns";
import type { HealthResponse } from "../types";

/**
Expand All @@ -25,23 +26,17 @@ export function useBackendHealth() {
clearTimeout(timeoutId);
});
},
// Retry configuration for startup scenarios
retry: (failureCount) => {
// Keep retrying during startup, up to 5 times
if (failureCount < 5) {
return true;
}
return false;
},
// Retry configuration for startup scenarios - respect 4xx but allow more attempts
retry: createRetryLogic(5),
retryDelay: (attemptIndex) => {
// Exponential backoff: 1.5s, 2.25s, 3.375s, etc.
return Math.min(1500 * 1.5 ** attemptIndex, 10000);
},
// Refetch every 30 seconds when healthy
refetchInterval: 30000,
refetchInterval: STALE_TIMES.normal,
// Keep trying to connect on window focus
refetchOnWindowFocus: true,
// Consider data fresh for 20 seconds
staleTime: 20000,
// Consider data fresh for 30 seconds
staleTime: STALE_TIMES.normal,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { STALE_TIMES } from "@/features/shared/queryPatterns";
import { knowledgeKeys } from "../../hooks/useKnowledgeQueries";
import { knowledgeService } from "../../services";
import type { ChunksResponse, CodeExample, CodeExamplesResponse, DocumentChunk } from "../../types";
Expand All @@ -19,7 +20,7 @@ export interface UseInspectorPaginationResult {
items: (DocumentChunk | CodeExample)[];
isLoading: boolean;
hasNextPage: boolean;
fetchNextPage: () => void;
fetchNextPage: (options?: any) => Promise<any>;
isFetchingNextPage: boolean;
totalCount: number;
loadedCount: number;
Expand Down Expand Up @@ -56,7 +57,7 @@ export function useInspectorPagination({
return hasMore ? allPages.length : undefined;
},
enabled: !!sourceId,
staleTime: 60000,
staleTime: STALE_TIMES.normal,
initialPageParam: 0,
});

Expand Down
18 changes: 12 additions & 6 deletions archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ export function useProjectTasks(projectId: string | undefined, enabled = true) {

// Fetch task counts for all projects
export function useTaskCounts() {
const { refetchInterval: countsRefetchInterval } = useSmartPolling(10_000); // 10s bg polling with smart pause
return useQuery<Awaited<ReturnType<typeof taskService.getTaskCountsForAllProjects>>>({
queryKey: taskKeys.counts(),
queryFn: () => taskService.getTaskCountsForAllProjects(),
refetchInterval: false, // Don't poll, only refetch manually
staleTime: STALE_TIMES.rare,
refetchInterval: countsRefetchInterval,
staleTime: STALE_TIMES.frequent,
});
Comment on lines +37 to 43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix polling inversion: background ends up polling faster than foreground.

useSmartPolling(10_000) returns 5s when not focused, which is faster than the 10s active interval and contradicts the polling guideline. Use visibility to pause when hidden and keep 10s while visible.

-export function useTaskCounts() {
-  const { refetchInterval: countsRefetchInterval } = useSmartPolling(10_000); // 10s bg polling with smart pause
+export function useTaskCounts() {
+  // Pause when tab is hidden; poll every 10s when visible (foreground or background).
+  const { isVisible } = useSmartPolling();
   return useQuery<Awaited<ReturnType<typeof taskService.getTaskCountsForAllProjects>>>({
     queryKey: taskKeys.counts(),
     queryFn: () => taskService.getTaskCountsForAllProjects(),
-    refetchInterval: countsRefetchInterval,
+    refetchInterval: isVisible ? 10_000 : false,
     staleTime: STALE_TIMES.frequent,
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { refetchInterval: countsRefetchInterval } = useSmartPolling(10_000); // 10s bg polling with smart pause
return useQuery<Awaited<ReturnType<typeof taskService.getTaskCountsForAllProjects>>>({
queryKey: taskKeys.counts(),
queryFn: () => taskService.getTaskCountsForAllProjects(),
refetchInterval: false, // Don't poll, only refetch manually
staleTime: STALE_TIMES.rare,
refetchInterval: countsRefetchInterval,
staleTime: STALE_TIMES.frequent,
});
export function useTaskCounts() {
// Pause when tab is hidden; poll every 10s when visible (foreground or background).
const { isVisible } = useSmartPolling();
return useQuery<Awaited<ReturnType<typeof taskService.getTaskCountsForAllProjects>>>({
queryKey: taskKeys.counts(),
queryFn: () => taskService.getTaskCountsForAllProjects(),
refetchInterval: isVisible ? 10_000 : false,
staleTime: STALE_TIMES.frequent,
});
}
🤖 Prompt for AI Agents
archon-ui-main/src/features/projects/tasks/hooks/useTaskQueries.ts around lines
37 to 43: the current call useSmartPolling(10_000) produces a shorter interval
when the page is hidden (background polling faster than foreground). Change the
useSmartPolling call so the foreground interval remains 10_000ms and polling is
paused (or slowed to effectively disabled) when the document is not visible;
specifically, pass the options/arguments that enforce a 10s interval when
visible and pause on hidden (e.g., useSmartPolling({ interval: 10_000,
pauseWhenHidden: true }) or the equivalent for our hook API) so background does
not poll faster than foreground.

}

Expand All @@ -47,7 +48,7 @@ export function useCreateTask() {
const queryClient = useQueryClient();
const { showToast } = useToast();

return useMutation({
return useMutation<Task, Error, CreateTaskRequest, { previousTasks?: Task[]; optimisticId: string }>({
mutationFn: (taskData: CreateTaskRequest) => taskService.createTask(taskData),
onMutate: async (newTaskData) => {
// Cancel any outgoing refetches
Expand Down Expand Up @@ -82,7 +83,9 @@ export function useCreateTask() {
},
onError: (error, variables, context) => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Failed to create task:", error, { variables });
console.error("Failed to create task:", error?.message, {
project_id: variables?.project_id,
});
// Rollback on error
if (context?.previousTasks) {
queryClient.setQueryData(taskKeys.byProject(variables.project_id), context.previousTasks);
Expand Down Expand Up @@ -138,7 +141,10 @@ export function useUpdateTask(projectId: string) {
},
onError: (error, variables, context) => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Failed to update task:", error, { variables });
console.error("Failed to update task:", error?.message, {
taskId: variables?.taskId,
changedFields: Object.keys(variables?.updates ?? {}),
});
// Rollback on error
if (context?.previousTasks) {
queryClient.setQueryData(taskKeys.byProject(projectId), context.previousTasks);
Expand Down Expand Up @@ -190,7 +196,7 @@ export function useDeleteTask(projectId: string) {
},
onError: (error, taskId, context) => {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Failed to delete task:", error, { taskId });
console.error("Failed to delete task:", error?.message, { taskId });
// Rollback on error
if (context?.previousTasks) {
queryClient.setQueryData(taskKeys.byProject(projectId), context.previousTasks);
Expand Down
68 changes: 68 additions & 0 deletions archon-ui-main/src/features/shared/queryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { QueryClient } from "@tanstack/react-query";
import { createRetryLogic, STALE_TIMES } from "./queryPatterns";

/**
* Centralized QueryClient configuration for the entire application
*
* Benefits:
* - Single source of truth for cache configuration
* - Automatic request deduplication for same query keys
* - Smart retry logic that avoids retrying on client errors
* - Optimized garbage collection and structural sharing
*/
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Default stale time - most data is considered fresh for 30 seconds
staleTime: STALE_TIMES.normal,

// Keep unused data in cache for 10 minutes (was 5 minutes)
gcTime: 10 * 60 * 1000,

// Smart retry logic - don't retry on 4xx errors or aborts
retry: createRetryLogic(2),

// Exponential backoff for retries
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),

// Disable aggressive refetching to reduce API calls
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: true,

// Network behavior
networkMode: "online",

// Enable structural sharing for efficient re-renders
structuralSharing: true,
},

mutations: {
// No retries for mutations - let user explicitly retry
retry: false,

// Network behavior
networkMode: "online",
},
},
});

/**
* Create a test QueryClient with optimized settings for tests
* Used by test-utils.tsx for consistent test behavior
*/
export function createTestQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: 0, // Always fresh in tests
gcTime: 0, // No caching in tests
refetchOnWindowFocus: false,
},
mutations: {
retry: false,
},
},
});
}
51 changes: 51 additions & 0 deletions archon-ui-main/src/features/shared/queryPatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* USAGE GUIDELINES:
* - Always use DISABLED_QUERY_KEY for disabled queries
* - Always use STALE_TIMES constants for staleTime configuration
* - Use createRetryLogic() for consistent retry behavior across the app
* - Never hardcode stale times directly in hooks
*/

Expand All @@ -22,3 +23,53 @@ export const STALE_TIMES = {
rare: 300_000, // 5 minutes - for rarely changing configuration
static: Infinity, // Never stale - for static data like settings
} as const;

// Re-export commonly used TanStack Query types for convenience
export type { QueryKey, QueryOptions } from "@tanstack/react-query";

/**
* Extract HTTP status code from various error objects
* Handles different client libraries and error structures
*/
function getErrorStatus(error: unknown): number | undefined {
if (!error || typeof error !== "object") return undefined;

const anyErr = error as any;

// Check common status properties in order of likelihood
if (typeof anyErr.statusCode === "number") return anyErr.statusCode; // APIServiceError
if (typeof anyErr.status === "number") return anyErr.status; // fetch Response
if (typeof anyErr.response?.status === "number") return anyErr.response.status; // axios

return undefined;
}

/**
* Check if error is an abort/cancel operation that shouldn't be retried
*/
function isAbortError(error: unknown): boolean {
if (!error || typeof error !== "object") return false;

const anyErr = error as any;
return anyErr?.name === "AbortError" || anyErr?.code === "ERR_CANCELED";
}

/**
* Unified retry logic for TanStack Query
* - No retries on 4xx client errors (permanent failures)
* - No retries on abort/cancel operations
* - Configurable retry count for other errors
*/
export function createRetryLogic(maxRetries: number = 2) {
return (failureCount: number, error: unknown) => {
// Don't retry aborted operations
if (isAbortError(error)) return false;

// Don't retry 4xx client errors (400-499)
const status = getErrorStatus(error);
if (status && status >= 400 && status < 500) return false;

// Retry up to maxRetries for other errors (5xx, network, etc)
return failureCount < maxRetries;
};
}
10 changes: 3 additions & 7 deletions archon-ui-main/src/features/testing/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
import { render as rtlRender } from "@testing-library/react";
import type React from "react";
import { createTestQueryClient } from "../shared/queryClient";
import { ToastProvider } from "../ui/components/ToastProvider";
import { TooltipProvider } from "../ui/primitives/tooltip";

Expand All @@ -11,12 +12,7 @@ import { TooltipProvider } from "../ui/primitives/tooltip";
export function renderWithProviders(
ui: React.ReactElement,
{
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
}),
queryClient = createTestQueryClient(),
...renderOptions
} = {},
) {
Expand Down
Loading