From 6bf889b855f5c95496c63f938f0d7ad5030d6727 Mon Sep 17 00:00:00 2001 From: samkul-swe Date: Wed, 15 Oct 2025 17:53:01 -0700 Subject: [PATCH] Invalidating cache to force refresh for CRUD --- surfsense_web/hooks/use-chat.ts | 6 +- surfsense_web/hooks/use-connectors.ts | 36 ++++----- surfsense_web/hooks/use-documents.ts | 14 +++- surfsense_web/hooks/use-llm-configs.ts | 20 ++++- surfsense_web/hooks/use-search-spaces.ts | 94 ++++++++++++------------ surfsense_web/lib/api.ts | 42 ++++++++++- surfsense_web/lib/apiCache.ts | 49 +++++++++--- 7 files changed, 181 insertions(+), 80 deletions(-) diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts index b976411e4..93d09f468 100644 --- a/surfsense_web/hooks/use-chat.ts +++ b/surfsense_web/hooks/use-chat.ts @@ -2,7 +2,7 @@ import type { Message } from "@ai-sdk/react"; import { useCallback, useEffect, useState } from "react"; import type { ResearchMode } from "@/components/chat"; import type { Document } from "@/hooks/use-documents"; -import { fetchWithCache } from "@/lib/apiCache"; +import { fetchWithCache,invalidateCache } from "@/lib/apiCache"; interface UseChatStateProps { search_space_id: string; @@ -61,6 +61,8 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, revalidate: 30 } @@ -116,6 +118,7 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { throw new Error(`Failed to create chat: ${response.statusText}`); } + invalidateCache('chats'); const data = await response.json(); return data.id; } catch (err) { @@ -162,6 +165,7 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { if (!response.ok) { throw new Error(`Failed to update chat: ${response.statusText}`); } + invalidateCache('chats'); } catch (err) { console.error("Error updating chat:", err); } diff --git a/surfsense_web/hooks/use-connectors.ts b/surfsense_web/hooks/use-connectors.ts index 63ee7b372..ca76efb23 100644 --- a/surfsense_web/hooks/use-connectors.ts +++ b/surfsense_web/hooks/use-connectors.ts @@ -1,3 +1,5 @@ +import { invalidateCache, fetchWithCache } from "@/lib/apiCache"; + // Types for connector API export interface ConnectorConfig { [key: string]: string; @@ -49,45 +51,41 @@ export const ConnectorService = { throw new Error(errorData.detail || "Failed to create connector"); } + invalidateCache('connectors'); + return response.json(); }, // Get all connectors async getConnectors(skip = 0, limit = 100): Promise { - const response = await fetch( + return fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/?skip=${skip}&limit=${limit}`, { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, + revalidate: 60, + tag: 'connectors', } ); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to fetch connectors"); - } - - return response.json(); }, // Get a specific connector async getConnector(connectorId: number): Promise { - const response = await fetch( + return fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-source-connectors/${connectorId}`, { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, + revalidate: 60, + tag: 'connectors' } ); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || "Failed to fetch connector"); - } - - return response.json(); }, // Update a connector @@ -109,6 +107,8 @@ export const ConnectorService = { throw new Error(errorData.detail || "Failed to update connector"); } + invalidateCache('connectors'); + return response.json(); }, @@ -128,5 +128,7 @@ export const ConnectorService = { const errorData = await response.json(); throw new Error(errorData.detail || "Failed to delete connector"); } + + invalidateCache('connectors'); }, -}; +}; \ No newline at end of file diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index c109f056f..e277ead2f 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -1,6 +1,6 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { fetchWithCache } from "@/lib/apiCache"; +import { fetchWithCache, invalidateCache } from "@/lib/apiCache"; import { toast } from "sonner"; import { normalizeListResponse } from "@/lib/pagination"; @@ -77,9 +77,12 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, method: "GET", - revalidate: 30 + revalidate: 30, + tag: 'documents' } ).catch(err => { toast.error("Failed to fetch documents"); @@ -145,9 +148,12 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, method: "GET", - revalidate: 15 + revalidate: 15, + tag: 'documents' } ).catch(err => { toast.error("Failed to search documents"); @@ -186,6 +192,8 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption throw new Error("Failed to delete document"); } + invalidateCache('documents'); + toast.success("Document deleted successfully"); // Update the local state after successful deletion setDocuments(documents.filter((doc) => doc.id !== documentId)); diff --git a/surfsense_web/hooks/use-llm-configs.ts b/surfsense_web/hooks/use-llm-configs.ts index f253ac8c5..4a6db8143 100644 --- a/surfsense_web/hooks/use-llm-configs.ts +++ b/surfsense_web/hooks/use-llm-configs.ts @@ -1,6 +1,6 @@ "use client"; import { useEffect, useState } from "react"; -import { fetchWithCache } from "@/lib/apiCache"; +import { fetchWithCache, invalidateCache } from "@/lib/apiCache"; import { toast } from "sonner"; export interface LLMConfig { @@ -67,9 +67,12 @@ export function useLLMConfigs(searchSpaceId: number | null) { { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, method: "GET", - revalidate: 60 + revalidate: 60, + tag: 'llmconfigs' } ).catch(err => { throw new Error("Failed to fetch LLM configurations"); @@ -107,6 +110,8 @@ export function useLLMConfigs(searchSpaceId: number | null) { throw new Error(errorData.detail || "Failed to create LLM configuration"); } + invalidateCache('llmconfigs'); + const newConfig = await response.json(); setLlmConfigs((prev) => [...prev, newConfig]); toast.success("LLM configuration created successfully"); @@ -134,6 +139,8 @@ export function useLLMConfigs(searchSpaceId: number | null) { throw new Error("Failed to delete LLM configuration"); } + invalidateCache('llmconfigs'); + setLlmConfigs((prev) => prev.filter((config) => config.id !== id)); toast.success("LLM configuration deleted successfully"); return true; @@ -166,6 +173,8 @@ export function useLLMConfigs(searchSpaceId: number | null) { throw new Error(errorData.detail || "Failed to update LLM configuration"); } + invalidateCache('llmconfigs'); + const updatedConfig = await response.json(); setLlmConfigs((prev) => prev.map((c) => (c.id === id ? updatedConfig : c))); toast.success("LLM configuration updated successfully"); @@ -206,9 +215,12 @@ export function useLLMPreferences(searchSpaceId: number | null) { { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, method: "GET", - revalidate: 120 + revalidate: 120, + tag: 'searchspaces' } ).catch(err => { throw new Error("Failed to fetch LLM preferences"); @@ -252,6 +264,8 @@ export function useLLMPreferences(searchSpaceId: number | null) { throw new Error(errorData.detail || "Failed to update LLM preferences"); } + invalidateCache('searchspaces'); + const updatedPreferences = await response.json(); setPreferences(updatedPreferences); toast.success("LLM preferences updated successfully"); diff --git a/surfsense_web/hooks/use-search-spaces.ts b/surfsense_web/hooks/use-search-spaces.ts index 6609464a4..186587dd7 100644 --- a/surfsense_web/hooks/use-search-spaces.ts +++ b/surfsense_web/hooks/use-search-spaces.ts @@ -1,7 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; -import { fetchWithCache } from "@/lib/apiCache"; +import { useCallback, useEffect, useState } from "react"; +import { fetchWithCache, invalidateCache } from "@/lib/apiCache"; import { toast } from "sonner"; interface SearchSpace { @@ -17,65 +17,69 @@ export function useSearchSpaces() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - useEffect(() => { - const fetchSearchSpaces = async () => { - try { - setLoading(true); - - const data = await fetchWithCache( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - revalidate: 60 - } - ).catch(err => { - toast.error("Not authenticated"); - throw new Error("Not authenticated"); - }); - setSearchSpaces(data); - setError(null); - } catch (err: any) { - setError(err.message || "Failed to fetch search spaces"); - console.error("Error fetching search spaces:", err); - } finally { - setLoading(false); - } - }; - - fetchSearchSpaces(); - }, []); - - // Function to refresh the search spaces list - const refreshSearchSpaces = async () => { - setLoading(true); + const fetchSearchSpaces = useCallback(async () => { try { - const response = await fetch( + setLoading(true); + + const data = await fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/`, { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' }, method: "GET", + revalidate: 60, + tag: 'searchspaces' } - ); - - if (!response.ok) { + ).catch(err => { toast.error("Not authenticated"); throw new Error("Not authenticated"); - } - - const data = await response.json(); + }); setSearchSpaces(data); setError(null); } catch (err: any) { setError(err.message || "Failed to fetch search spaces"); + console.error("Error fetching search spaces:", err); } finally { setLoading(false); } - }; + }, []); + + useEffect(() => { + fetchSearchSpaces(); + }, [fetchSearchSpaces]); + + const refreshSearchSpaces = useCallback(async () => { + try { + setLoading(true); + + invalidateCache('searchspaces'); + + const data = await fetchWithCache( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + 'Cache-Control': 'no-store, max-age=0, must-revalidate', + 'Pragma': 'no-cache' + }, + method: "GET", + revalidate: 60, + tag: 'searchspaces' + } + ); + + setSearchSpaces(data); + setError(null); + } catch (err: any) { + setError(err.message || "Failed to fetch search spaces"); + console.error("Error refreshing search spaces:", err); + } finally { + setLoading(false); + } + }, []); return { searchSpaces, loading, error, refreshSearchSpaces }; -} +} \ No newline at end of file diff --git a/surfsense_web/lib/api.ts b/surfsense_web/lib/api.ts index 3095bf13e..222a184eb 100644 --- a/surfsense_web/lib/api.ts +++ b/surfsense_web/lib/api.ts @@ -1,5 +1,27 @@ import { toast } from "sonner"; -import { fetchWithCache } from "./apiCache"; +import { fetchWithCache, invalidateCache, cacheKeys } from "./apiCache"; + +type CacheTag = keyof typeof cacheKeys; + +// Define a mapping of endpoints to cache tags +const ENDPOINT_CACHE_TAGS: Record = { + 'api/v1/documents': 'documents', + 'api/v1/chats': 'chats', + 'api/v1/searchspaces': 'searchspaces', + 'api/v1/search-source-connectors': 'connectors', + 'api/v1/llm-configs': 'llmconfigs', + 'users/me': 'user' +}; + +// Helper to determine which cache tag to invalidate +function getCacheTagForEndpoint(path: string): CacheTag | undefined { + for (const [endpoint, tag] of Object.entries(ENDPOINT_CACHE_TAGS)) { + if (path.includes(endpoint)) { + return tag as CacheTag; // Explicit cast to ensure type safety + } + } + return undefined; +} /** * Custom fetch wrapper that handles authentication and redirects to home page on 401 Unauthorized @@ -108,11 +130,15 @@ export const apiClient = { ...(token && { Authorization: `Bearer ${token}` }), }; + // Determine the appropriate cache tag + const tag = getCacheTagForEndpoint(path); + return await fetchWithCache(url, { method: "GET", ...options, headers, revalidate, + tag }); } catch (error) { if (error instanceof Error && error.message.includes('401')) { @@ -149,6 +175,10 @@ export const apiClient = { throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`); } + // Invalidate cache after successful mutation + const tag = getCacheTagForEndpoint(path); + if (tag) invalidateCache(tag); + return response.json(); }, @@ -176,6 +206,10 @@ export const apiClient = { throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`); } + // Invalidate cache after successful mutation + const tag = getCacheTagForEndpoint(path); + if (tag) invalidateCache(tag); + return response.json(); }, @@ -197,6 +231,10 @@ export const apiClient = { throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`); } + // Invalidate cache after successful mutation + const tag = getCacheTagForEndpoint(path); + if (tag) invalidateCache(tag); + return response.json(); }, -}; +}; \ No newline at end of file diff --git a/surfsense_web/lib/apiCache.ts b/surfsense_web/lib/apiCache.ts index 24e491b3b..320cfcb4b 100644 --- a/surfsense_web/lib/apiCache.ts +++ b/surfsense_web/lib/apiCache.ts @@ -1,11 +1,21 @@ -/** - * List of API endpoints that require specific trailing slash handling - */ +export const cacheKeys = { + documents: 0, + chats: 0, + searchspaces: 0, + user: 0, + connectors: 0, + llmconfigs: 0, + config: 0 +}; + const ENDPOINTS_CONFIG = { 'users/me': false, - 'api/v1/chats': false, + 'api/v1/chats': true, 'api/v1/documents': true, - 'api/v1/searchspaces': true + 'api/v1/searchspaces': true, + 'api/v1/search-source-connectors': false, + 'api/v1/llm-configs': true, + 'api/v1/search-spaces': false }; function isDetailEndpoint(url: string): boolean { @@ -47,18 +57,35 @@ function formatUrlPath(url: string): string { } /** - * API client with Next.js caching and careful URL handling + * API client with Next.js caching, URL handling, and cache invalidation */ export async function fetchWithCache(url: string, options: RequestInit & { revalidate?: number | false, + tag?: keyof typeof cacheKeys } = {}) { - const { revalidate, ...fetchOptions } = options; + const { revalidate, tag, ...fetchOptions } = options; - const formattedUrl = formatUrlPath(url); + let formattedUrl = formatUrlPath(url); + + if (tag) { + const separator = formattedUrl.includes('?') ? '&' : '?'; + formattedUrl += `${separator}v=${cacheKeys[tag]}`; + } + + if (formattedUrl.includes('documents') && formattedUrl.includes('page_size=')) { + const separator = formattedUrl.includes('?') ? '&' : '?'; + formattedUrl += `${separator}t=${Date.now()}`; + } + + const headers = new Headers(fetchOptions.headers || {}); + headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + headers.set('Pragma', 'no-cache'); + headers.set('Expires', '0'); const response = await fetch(formattedUrl, { ...fetchOptions, - cache: 'force-cache', + headers, + cache: 'force-cache', // Next.js internal cache still works next: revalidate !== undefined ? { revalidate } : undefined, }); @@ -67,4 +94,8 @@ export async function fetchWithCache(url: string, options: RequestInit & { } return response.json(); +} + +export function invalidateCache(tag: keyof typeof cacheKeys) { + cacheKeys[tag]++; } \ No newline at end of file