mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
Invalidating cache to force refresh for CRUD
This commit is contained in:
parent
bf0f4e3e68
commit
6bf889b855
7 changed files with 181 additions and 80 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Connector[]> {
|
||||
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<Connector> {
|
||||
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');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, CacheTag> = {
|
||||
'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();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -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]++;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue