From ddb718ff4c06aaab1f1da5d9d9ef1c173dd9119a Mon Sep 17 00:00:00 2001 From: samkul-swe Date: Sun, 12 Oct 2025 14:49:21 -0700 Subject: [PATCH] Added api caching capabilities - testing if changes break anything --- surfsense_web/app/dashboard/page.tsx | 2 +- .../components/sidebar/AppSidebarProvider.tsx | 8 ++- surfsense_web/hooks/use-chat.ts | 14 +++--- surfsense_web/hooks/use-documents.ts | 49 ++++++++---------- surfsense_web/hooks/use-llm-configs.ts | 23 ++++----- surfsense_web/hooks/use-search-spaces.ts | 15 +++--- surfsense_web/lib/api.ts | 40 +++++++++++++-- surfsense_web/lib/apiCache.ts | 50 +++++++++++++++++++ 8 files changed, 138 insertions(+), 63 deletions(-) create mode 100644 surfsense_web/lib/apiCache.ts diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index e99454ae0..8a09a1919 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -169,7 +169,7 @@ const DashboardPage = () => { if (typeof window === "undefined") return; try { - const userData = await apiClient.get("users/me"); + const userData = await apiClient.get('users/me', {}, 120); setUser(userData); setUserError(null); } catch (error) { diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index 684e04653..b7fe123e8 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -87,7 +87,9 @@ export function AppSidebarProvider({ if (typeof window === "undefined") return; const chats: Chat[] = await apiClient.get( - `api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}` + `api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}`, + {}, + 15 ); // Sort chats by created_at in descending order (newest first) @@ -132,7 +134,9 @@ export function AppSidebarProvider({ if (typeof window === "undefined") return; const data: SearchSpace = await apiClient.get( - `api/v1/searchspaces/${searchSpaceId}` + `api/v1/searchspaces/${searchSpaceId}`, + {}, + 30 ); setSearchSpace(data); setSearchSpaceError(null); diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts index a081a96fb..b976411e4 100644 --- a/surfsense_web/hooks/use-chat.ts +++ b/surfsense_web/hooks/use-chat.ts @@ -2,6 +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"; interface UseChatStateProps { search_space_id: string; @@ -53,7 +54,7 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { if (!token) return null; try { - const response = await fetch( + const data = await fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${Number(chatId)}`, { method: "GET", @@ -61,14 +62,13 @@ export function useChatAPI({ token, search_space_id }: UseChatAPIProps) { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, + revalidate: 30 } - ); + ).catch(err => { + throw new Error(`Failed to fetch chat details: ${err}`); + }); - if (!response.ok) { - throw new Error(`Failed to fetch chat details: ${response.statusText}`); - } - - return await response.json(); + return await data; } catch (err) { console.error("Error fetching chat details:", err); return null; diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index 4c22a8707..c109f056f 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -1,5 +1,6 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { fetchWithCache } from "@/lib/apiCache"; import { toast } from "sonner"; import { normalizeListResponse } from "@/lib/pagination"; @@ -71,22 +72,19 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption params.append("page_size", effectivePageSize.toString()); } - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } - ); - - if (!response.ok) { + const data = await fetchWithCache( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/?${params.toString()}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + method: "GET", + revalidate: 30 + } + ).catch(err => { toast.error("Failed to fetch documents"); throw new Error("Failed to fetch documents"); - } - - const data = await response.json(); + }); const normalized = normalizeListResponse(data); setDocuments(normalized.items); setTotal(normalized.total); @@ -142,22 +140,19 @@ export function useDocuments(searchSpaceId: number, options?: UseDocumentsOption params.append("page_size", effectivePageSize.toString()); } - const response = await fetch( + const data = await fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/search/?${params.toString()}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } - ); - - if (!response.ok) { + { + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + method: "GET", + revalidate: 15 + } + ).catch(err => { toast.error("Failed to search documents"); throw new Error("Failed to search documents"); - } - - const data = await response.json(); + }); const normalized = normalizeListResponse(data); setDocuments(normalized.items); setTotal(normalized.total); diff --git a/surfsense_web/hooks/use-llm-configs.ts b/surfsense_web/hooks/use-llm-configs.ts index 73950736d..f253ac8c5 100644 --- a/surfsense_web/hooks/use-llm-configs.ts +++ b/surfsense_web/hooks/use-llm-configs.ts @@ -1,5 +1,6 @@ "use client"; import { useEffect, useState } from "react"; +import { fetchWithCache } from "@/lib/apiCache"; import { toast } from "sonner"; export interface LLMConfig { @@ -60,21 +61,19 @@ export function useLLMConfigs(searchSpaceId: number | null) { try { setLoading(true); - const response = await fetch( + + const data = await fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/llm-configs/?search_space_id=${searchSpaceId}`, { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, }, method: "GET", + revalidate: 60 } - ); - - if (!response.ok) { + ).catch(err => { throw new Error("Failed to fetch LLM configurations"); - } - - const data = await response.json(); + }); setLlmConfigs(data); setError(null); } catch (err: any) { @@ -202,21 +201,19 @@ export function useLLMPreferences(searchSpaceId: number | null) { try { setLoading(true); - const response = await fetch( + const data = await fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/search-spaces/${searchSpaceId}/llm-preferences`, { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, }, method: "GET", + revalidate: 120 } - ); - - if (!response.ok) { + ).catch(err => { throw new Error("Failed to fetch LLM preferences"); - } + }); - const data = await response.json(); setPreferences(data); setError(null); } catch (err: any) { diff --git a/surfsense_web/hooks/use-search-spaces.ts b/surfsense_web/hooks/use-search-spaces.ts index 4dae07431..6609464a4 100644 --- a/surfsense_web/hooks/use-search-spaces.ts +++ b/surfsense_web/hooks/use-search-spaces.ts @@ -1,6 +1,7 @@ "use client"; import { useEffect, useState } from "react"; +import { fetchWithCache } from "@/lib/apiCache"; import { toast } from "sonner"; interface SearchSpace { @@ -20,24 +21,20 @@ export function useSearchSpaces() { const fetchSearchSpaces = async () => { try { setLoading(true); - const response = await fetch( + + const data = await fetchWithCache( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/`, { headers: { Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, }, method: "GET", - cache: 'force-cache', - next: { revalidate: 60 } + revalidate: 60 } - ); - - 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) { diff --git a/surfsense_web/lib/api.ts b/surfsense_web/lib/api.ts index 6c52cac77..3095bf13e 100644 --- a/surfsense_web/lib/api.ts +++ b/surfsense_web/lib/api.ts @@ -1,4 +1,5 @@ import { toast } from "sonner"; +import { fetchWithCache } from "./apiCache"; /** * Custom fetch wrapper that handles authentication and redirects to home page on 401 Unauthorized @@ -79,18 +80,49 @@ export const apiClient = { * @param options - Additional fetch options * @returns The response data */ - async get(path: string, options: RequestInit = {}): Promise { - const response = await fetchWithAuth(getApiUrl(path), { + async get( + path: string, + options: RequestInit = {}, + revalidate: number | false = 30 // Default 30 second cache + ): Promise { + const url = getApiUrl(path); + + if (typeof window === "undefined") { + const response = await fetch(url, { method: "GET", ...options, }); - + if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error(`API error: ${response.status} ${errorData?.detail || response.statusText}`); } - + return response.json(); + } + + try { + const token = localStorage.getItem("surfsense_bearer_token"); + const headers = { + ...options.headers, + ...(token && { Authorization: `Bearer ${token}` }), + }; + + return await fetchWithCache(url, { + method: "GET", + ...options, + headers, + revalidate, + }); + } catch (error) { + if (error instanceof Error && error.message.includes('401')) { + toast.error("Session expired. Please log in again."); + localStorage.removeItem("surfsense_bearer_token"); + window.location.href = "/"; + throw new Error("Unauthorized: Redirecting to login page"); + } + throw error; + } }, /** diff --git a/surfsense_web/lib/apiCache.ts b/surfsense_web/lib/apiCache.ts new file mode 100644 index 000000000..cb6887edb --- /dev/null +++ b/surfsense_web/lib/apiCache.ts @@ -0,0 +1,50 @@ +const NO_TRAILING_SLASH_ENDPOINTS = [ + 'users/me', + 'api/v1/chats', + 'api/v1/search-spaces' +]; + +function shouldSkipTrailingSlash(url: string): boolean { + const basePath = url.includes('?') ? url.split('?')[0] : url; + + return NO_TRAILING_SLASH_ENDPOINTS.some(endpoint => + basePath.includes(endpoint) + ); +} + +/** + * API client with Next.js caching and trailing slash handling + */ +export async function fetchWithCache(url: string, options: RequestInit & { + revalidate?: number | false, + } = {}) { + const { revalidate, ...fetchOptions } = options; + + let basePath = url; + let queryParams = ''; + + if (url.includes('?')) { + [basePath, queryParams] = url.split('?'); + queryParams = `?${queryParams}`; + } + + if (!shouldSkipTrailingSlash(basePath)) { + basePath = basePath.endsWith('/') ? basePath : `${basePath}/`; + } else { + basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + } + + const formattedUrl = basePath + queryParams; + + const response = await fetch(formattedUrl, { + ...fetchOptions, + cache: 'force-cache', + next: revalidate !== undefined ? { revalidate } : undefined, + }); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + return response.json(); +} \ No newline at end of file