From 3504be34133dd0584c48bbc96eaa6492ec30dda5 Mon Sep 17 00:00:00 2001 From: guangyang1206 Date: Sat, 16 May 2026 12:10:04 +0800 Subject: [PATCH] fix(web): make cacheKeys.*.withQueryParams order-stable (sort entries) Fixes #1370 Object.values() produces order-dependent cache keys because the order of values depends on the order of keys in the object. This causes the same logical query to produce different cache keys when the parameter object has keys in different orders. Added stableEntries() helper that: 1. Filters out undefined values 2. Sorts entries by key name 3. Returns flat array of [key, value] pairs This ensures cache key identity is stable regardless of parameter object key order. Co-authored-by: guangyang1206 --- surfsense_web/lib/query-client/cache-keys.ts | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index a05055075..ce45ee143 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -3,6 +3,19 @@ import type { GetDocumentsRequest } from "@/contracts/types/document.types"; import type { GetLogsRequest } from "@/contracts/types/log.types"; import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types"; +/** + * Convert an object to a stable array of [key, value] pairs sorted by key. + * This ensures cache keys are order-independent (avoiding Object.values order-dependency). + * Filters out undefined values. + */ +function stableEntries(obj: Record | null | undefined): unknown[] { + if (!obj) return []; + return Object.entries(obj) + .filter(([, v]) => v !== undefined) + .sort(([a], [b]) => a.localeCompare(b)) + .flat(); +} + export const cacheKeys = { // New chat threads (assistant-ui) threads: { @@ -13,9 +26,9 @@ export const cacheKeys = { }, documents: { globalQueryParams: (queries: GetDocumentsRequest["queryParams"]) => - ["documents", ...(queries ? Object.values(queries) : [])] as const, + ["documents", ...stableEntries(queries)] as const, withQueryParams: (queries: GetDocumentsRequest["queryParams"]) => - ["documents-with-queries", ...(queries ? Object.values(queries) : [])] as const, + ["documents-with-queries", ...stableEntries(queries)] as const, document: (documentId: string) => ["document", documentId] as const, byChunk: (chunkId: string) => ["documents", "by-chunk", chunkId] as const, }, @@ -24,7 +37,7 @@ export const cacheKeys = { detail: (logId: number | string) => ["logs", "detail", logId] as const, summary: (searchSpaceId?: number | string) => ["logs", "summary", searchSpaceId] as const, withQueryParams: (queries: GetLogsRequest["queryParams"]) => - ["logs", "with-query-params", ...(queries ? Object.values(queries) : [])] as const, + ["logs", "with-query-params", ...stableEntries(queries)] as const, }, newLLMConfigs: { all: (searchSpaceId: number) => ["new-llm-configs", searchSpaceId] as const, @@ -51,7 +64,7 @@ export const cacheKeys = { searchSpaces: { all: ["search-spaces"] as const, withQueryParams: (queries: GetSearchSpacesRequest["queryParams"]) => - ["search-spaces", ...(queries ? Object.values(queries) : [])] as const, + ["search-spaces", ...stableEntries(queries)] as const, detail: (searchSpaceId: string) => ["search-spaces", searchSpaceId] as const, }, user: { @@ -78,7 +91,7 @@ export const cacheKeys = { connectors: { all: (searchSpaceId: string) => ["connectors", searchSpaceId] as const, withQueryParams: (queries: GetConnectorsRequest["queryParams"]) => - ["connectors", ...(queries ? Object.values(queries) : [])] as const, + ["connectors", ...stableEntries(queries)] as const, byId: (connectorId: string) => ["connector", connectorId] as const, index: () => ["connector", "index"] as const, googleDrive: {