perf(cache): implement event-driven long TTL cache optimization & fix contact renaming edge-case

This commit is contained in:
VPS 4 Deploy Agent
2026-05-10 21:50:31 +02:00
parent b046f6da7b
commit 10c906b61d
8 changed files with 299 additions and 180 deletions
-1
View File
@@ -2788,7 +2788,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -140,6 +140,44 @@ function sortChats(chats: Chat[]): Chat[] {
// ─── Store ────────────────────────────────────────────────────────────────────
// ─── Cache local (localStorage) — UX instantâneo no reload ─────────────────────
// Persiste os primeiros N chats por instância para hidratar a UI antes do fetch.
// Chave: `inboxCache:v1:${instanceId}:${archived ? '1' : '0'}`
// TTL implícito: validamos pela data salva e descartamos se > 7 dias.
const INBOX_CACHE_PREFIX = 'inboxCache:v1'
const INBOX_CACHE_LIMIT = 10
const INBOX_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
interface InboxCacheEntry {
savedAt: number
chats: Chat[]
}
function inboxCacheKey(instanceId: string, archived: boolean): string {
return `${INBOX_CACHE_PREFIX}:${instanceId}:${archived ? '1' : '0'}`
}
function readInboxCache(instanceId: string, archived: boolean): Chat[] | null {
if (typeof window === 'undefined') return null
try {
const raw = localStorage.getItem(inboxCacheKey(instanceId, archived))
if (!raw) return null
const parsed = JSON.parse(raw) as InboxCacheEntry
if (!parsed || !Array.isArray(parsed.chats)) return null
if (Date.now() - parsed.savedAt > INBOX_CACHE_MAX_AGE_MS) return null
return parsed.chats
} catch { return null }
}
function writeInboxCache(instanceId: string, archived: boolean, chats: Chat[]): void {
if (typeof window === 'undefined') return
try {
const slim = chats.slice(0, INBOX_CACHE_LIMIT)
const entry: InboxCacheEntry = { savedAt: Date.now(), chats: slim }
localStorage.setItem(inboxCacheKey(instanceId, archived), JSON.stringify(entry))
} catch { /* quota exceeded — ignora */ }
}
export const useChatStore = create<ChatState>()(
immer((set, get) => ({
chats: [],
@@ -217,11 +255,34 @@ export const useChatStore = create<ChatState>()(
// ── Async ─────────────────────────────────────────────────────────────────
loadChats: async (instanceId, opts = {}) => {
set((s) => { s.chatsLoading = true })
// Hidratação instantânea: se temos cache local válido para esta instância,
// mostra os 10 chats salvos imediatamente enquanto a request real roda.
// Evita tela em branco ou skeleton de 15s+ em redes lentas.
const archivedFlag = opts.archived ?? false
const hasSearch = !!opts.search
const current = get().chats
if (!hasSearch) {
const sameInstance = current.length > 0 && current[0]?.instanceId === instanceId
if (!sameInstance) {
const cached = readInboxCache(instanceId, archivedFlag)
if (cached && cached.length > 0) {
set((s) => { s.chats = sortChats(cached); s.chatsLoading = true })
} else {
set((s) => { s.chatsLoading = true })
}
} else {
set((s) => { s.chatsLoading = true })
}
} else {
set((s) => { s.chatsLoading = true })
}
try {
const raw = await chatApi.list(instanceId, opts)
const data: Chat[] = (Array.isArray(raw) ? raw : []).map(mapBackendChat)
set((s) => { s.chats = sortChats(data); s.chatsLoading = false })
const sorted = sortChats(data)
set((s) => { s.chats = sorted; s.chatsLoading = false })
if (!hasSearch) writeInboxCache(instanceId, archivedFlag, sorted)
} catch {
set((s) => { s.chatsLoading = false })
}