mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
Added api caching capabilities - testing if changes break anything
This commit is contained in:
parent
fff082b4ee
commit
ddb718ff4c
8 changed files with 138 additions and 63 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
50
surfsense_web/lib/apiCache.ts
Normal file
50
surfsense_web/lib/apiCache.ts
Normal 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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue