mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
fix(web): scope daily brief cache by account
This commit is contained in:
parent
2cebe81512
commit
fc5df55b8c
4 changed files with 148 additions and 8 deletions
|
|
@ -67,6 +67,11 @@ import {
|
|||
} from "@/lib/search-params"
|
||||
import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label"
|
||||
import { getToolDocumentSpace } from "@/lib/plugin-space"
|
||||
import {
|
||||
getSpaceHighlightsCacheKey,
|
||||
LEGACY_SPACE_HIGHLIGHTS_CACHE_NAME,
|
||||
SPACE_HIGHLIGHTS_CACHE_NAME,
|
||||
} from "@/lib/space-highlights-cache"
|
||||
import { getBackendUrl } from "@/lib/url-helpers"
|
||||
|
||||
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
|
||||
|
|
@ -112,7 +117,7 @@ function ViewErrorFallback() {
|
|||
|
||||
export function AppExperience() {
|
||||
const isMobile = useIsMobile()
|
||||
const { user, session, isSessionPending, org } = useAuth()
|
||||
const { user, session, isSessionPending, isRestoring, org } = useAuth()
|
||||
|
||||
const { selectedProject, selectedProjects, setSelectedProject } = useProject()
|
||||
const selectedProjectTag = selectedProjects[0]
|
||||
|
|
@ -316,27 +321,75 @@ export function AppExperience() {
|
|||
generatedAt: string
|
||||
}
|
||||
|
||||
const HIGHLIGHTS_CACHE_NAME = "space-highlights-v1"
|
||||
const HIGHLIGHTS_MAX_AGE = 4 * 60 * 60 * 1000 // 4 hours
|
||||
const highlightsAccountScope =
|
||||
user?.id && org?.id ? JSON.stringify([user.id, org.id]) : null
|
||||
const previousHighlightsAccountScope = useRef<string | null | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isSessionPending || isRestoring) return
|
||||
|
||||
const previousScope = previousHighlightsAccountScope.current
|
||||
previousHighlightsAccountScope.current = highlightsAccountScope
|
||||
if (previousScope === undefined) {
|
||||
try {
|
||||
void caches.delete(LEGACY_SPACE_HIGHLIGHTS_CACHE_NAME)
|
||||
} catch {}
|
||||
return
|
||||
}
|
||||
if (previousScope === highlightsAccountScope) return
|
||||
|
||||
queryClient.removeQueries({
|
||||
queryKey: ["space-highlights"],
|
||||
predicate: (query) =>
|
||||
query.queryKey[1] !== user?.id || query.queryKey[2] !== org?.id,
|
||||
})
|
||||
try {
|
||||
void caches.delete(SPACE_HIGHLIGHTS_CACHE_NAME)
|
||||
} catch {}
|
||||
}, [
|
||||
highlightsAccountScope,
|
||||
isRestoring,
|
||||
isSessionPending,
|
||||
org?.id,
|
||||
queryClient,
|
||||
user?.id,
|
||||
])
|
||||
|
||||
const handleResetHighlights = useCallback(async () => {
|
||||
toast.success("Refreshing daily brief…")
|
||||
try {
|
||||
await caches.delete(HIGHLIGHTS_CACHE_NAME)
|
||||
await caches.delete(SPACE_HIGHLIGHTS_CACHE_NAME)
|
||||
} catch {}
|
||||
setHighlightsForceAt(Date.now())
|
||||
}, [])
|
||||
|
||||
const { data: highlightsData, isLoading: isLoadingHighlights } =
|
||||
useQuery<SpaceHighlightsResponse>({
|
||||
queryKey: ["space-highlights", selectedProject, highlightsForceAt],
|
||||
queryKey: [
|
||||
"space-highlights",
|
||||
user?.id,
|
||||
org?.id,
|
||||
selectedProject,
|
||||
highlightsForceAt,
|
||||
],
|
||||
queryFn: async (): Promise<SpaceHighlightsResponse> => {
|
||||
if (!user?.id || !org?.id) {
|
||||
throw new Error("User and organization are required")
|
||||
}
|
||||
const spaceId = selectedProject || "sm_project_default"
|
||||
const forceRefresh = highlightsForceAt > 0
|
||||
const cacheKey = `${backendUrl}/v3/space-highlights?spaceId=${spaceId}`
|
||||
const cacheKey = getSpaceHighlightsCacheKey({
|
||||
backendUrl,
|
||||
spaceId,
|
||||
userId: user.id,
|
||||
organizationId: org.id,
|
||||
})
|
||||
|
||||
if (!forceRefresh) {
|
||||
const cache = await caches.open(HIGHLIGHTS_CACHE_NAME)
|
||||
const cache = await caches.open(SPACE_HIGHLIGHTS_CACHE_NAME)
|
||||
const cached = await cache.match(cacheKey)
|
||||
if (cached) {
|
||||
const age =
|
||||
|
|
@ -369,7 +422,7 @@ export function AppExperience() {
|
|||
|
||||
// Update browser cache with fresh data (works for both normal and forced refresh)
|
||||
try {
|
||||
const freshCache = await caches.open(HIGHLIGHTS_CACHE_NAME)
|
||||
const freshCache = await caches.open(SPACE_HIGHLIGHTS_CACHE_NAME)
|
||||
const cacheResponse = new Response(JSON.stringify(data), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -387,6 +440,7 @@ export function AppExperience() {
|
|||
},
|
||||
staleTime: HIGHLIGHTS_MAX_AGE,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !!user?.id && !!org?.id,
|
||||
})
|
||||
|
||||
const { data: memoryOfDay = null } = useQuery<MemoryOfDay | null>({
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
import { $fetch } from "@lib/api"
|
||||
import {
|
||||
LEGACY_SPACE_HIGHLIGHTS_CACHE_NAME,
|
||||
SPACE_HIGHLIGHTS_CACHE_NAME,
|
||||
} from "@/lib/space-highlights-cache"
|
||||
|
||||
export function useResetOrganization() {
|
||||
const queryClient = useQueryClient()
|
||||
|
|
@ -30,7 +34,10 @@ export function useResetOrganization() {
|
|||
queryClient.invalidateQueries()
|
||||
// Clear the daily brief Cache API entry so stale highlights don't survive the reset
|
||||
try {
|
||||
await caches.delete("space-highlights-v1")
|
||||
await Promise.all([
|
||||
caches.delete(SPACE_HIGHLIGHTS_CACHE_NAME),
|
||||
caches.delete(LEGACY_SPACE_HIGHLIGHTS_CACHE_NAME),
|
||||
])
|
||||
} catch {
|
||||
// Cache API not available in all environments
|
||||
}
|
||||
|
|
|
|||
59
apps/web/lib/space-highlights-cache.test.ts
Normal file
59
apps/web/lib/space-highlights-cache.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { describe, expect, it } from "bun:test"
|
||||
import {
|
||||
getSpaceHighlightsCacheKey,
|
||||
LEGACY_SPACE_HIGHLIGHTS_CACHE_NAME,
|
||||
SPACE_HIGHLIGHTS_CACHE_NAME,
|
||||
} from "./space-highlights-cache"
|
||||
|
||||
describe("space highlights cache", () => {
|
||||
it("does not reuse the legacy account-agnostic cache", () => {
|
||||
expect(SPACE_HIGHLIGHTS_CACHE_NAME).toBe("space-highlights-v2")
|
||||
expect(SPACE_HIGHLIGHTS_CACHE_NAME).not.toBe(
|
||||
LEGACY_SPACE_HIGHLIGHTS_CACHE_NAME,
|
||||
)
|
||||
})
|
||||
|
||||
it("scopes cache keys by user, organization, and space", () => {
|
||||
const cacheKey = getSpaceHighlightsCacheKey({
|
||||
backendUrl: "https://api.supermemory.ai",
|
||||
spaceId: "sm_project_default",
|
||||
userId: "user-1",
|
||||
organizationId: "org-1",
|
||||
})
|
||||
|
||||
expect(cacheKey).toBe(
|
||||
"https://api.supermemory.ai/v3/space-highlights?spaceId=sm_project_default&userId=user-1&organizationId=org-1",
|
||||
)
|
||||
})
|
||||
|
||||
it("produces different keys for different accounts sharing a project tag", () => {
|
||||
const firstAccount = getSpaceHighlightsCacheKey({
|
||||
backendUrl: "https://api.supermemory.ai",
|
||||
spaceId: "sm_project_default",
|
||||
userId: "user-1",
|
||||
organizationId: "org-1",
|
||||
})
|
||||
const secondAccount = getSpaceHighlightsCacheKey({
|
||||
backendUrl: "https://api.supermemory.ai",
|
||||
spaceId: "sm_project_default",
|
||||
userId: "user-2",
|
||||
organizationId: "org-2",
|
||||
})
|
||||
|
||||
expect(firstAccount).not.toBe(secondAccount)
|
||||
})
|
||||
|
||||
it("safely encodes identifiers used in cache keys", () => {
|
||||
const cacheKey = getSpaceHighlightsCacheKey({
|
||||
backendUrl: "http://localhost:8787/",
|
||||
spaceId: "project&shared=true",
|
||||
userId: "user?admin=true",
|
||||
organizationId: "org/name",
|
||||
})
|
||||
const url = new URL(cacheKey)
|
||||
|
||||
expect(url.searchParams.get("spaceId")).toBe("project&shared=true")
|
||||
expect(url.searchParams.get("userId")).toBe("user?admin=true")
|
||||
expect(url.searchParams.get("organizationId")).toBe("org/name")
|
||||
})
|
||||
})
|
||||
20
apps/web/lib/space-highlights-cache.ts
Normal file
20
apps/web/lib/space-highlights-cache.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export const SPACE_HIGHLIGHTS_CACHE_NAME = "space-highlights-v2"
|
||||
export const LEGACY_SPACE_HIGHLIGHTS_CACHE_NAME = "space-highlights-v1"
|
||||
|
||||
export function getSpaceHighlightsCacheKey({
|
||||
backendUrl,
|
||||
spaceId,
|
||||
userId,
|
||||
organizationId,
|
||||
}: {
|
||||
backendUrl: string
|
||||
spaceId: string
|
||||
userId: string
|
||||
organizationId: string
|
||||
}): string {
|
||||
const url = new URL(`${backendUrl.replace(/\/$/, "")}/v3/space-highlights`)
|
||||
url.searchParams.set("spaceId", spaceId)
|
||||
url.searchParams.set("userId", userId)
|
||||
url.searchParams.set("organizationId", organizationId)
|
||||
return url.toString()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue