Added api caching capabilities - testing if changes break anything

This commit is contained in:
samkul-swe 2025-10-12 14:49:21 -07:00
parent fff082b4ee
commit ddb718ff4c
8 changed files with 138 additions and 63 deletions

View file

@ -169,7 +169,7 @@ const DashboardPage = () => {
if (typeof window === "undefined") return;
try {
const userData = await apiClient.get<User>("users/me");
const userData = await apiClient.get<User>('users/me', {}, 120);
setUser(userData);
setUserError(null);
} catch (error) {

View file

@ -87,7 +87,9 @@ export function AppSidebarProvider({
if (typeof window === "undefined") return;
const chats: Chat[] = await apiClient.get<Chat[]>(
`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<SearchSpace>(
`api/v1/searchspaces/${searchSpaceId}`
`api/v1/searchspaces/${searchSpaceId}`,
{},
30
);
setSearchSpace(data);
setSearchSpaceError(null);

View file

@ -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;

View file

@ -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<Document>(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<Document>(data);
setDocuments(normalized.items);
setTotal(normalized.total);

View file

@ -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) {

View file

@ -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) {

View file

@ -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<T>(path: string, options: RequestInit = {}): Promise<T> {
const response = await fetchWithAuth(getApiUrl(path), {
async get<T>(
path: string,
options: RequestInit = {},
revalidate: number | false = 30 // Default 30 second cache
): Promise<T> {
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;
}
},
/**

View file

@ -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();
}