Spaces:
Running
Running
| /** | |
| * Message Cache Utility | |
| * Lưu messages và conversations vào localStorage để hiển thị ngay lập tức (stale-while-revalidate) | |
| */ | |
| const CACHE_PREFIX = "vc_msgs_"; | |
| const CONV_CACHE_KEY = `${CACHE_PREFIX}conversations`; | |
| const SPACES_CACHE_KEY = `${CACHE_PREFIX}spaces`; | |
| const MEMBERS_CACHE_KEY = `${CACHE_PREFIX}members`; | |
| const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours | |
| const MAX_CACHE_ENTRIES = 50; // Giới hạn số conversation/room được cache | |
| function getKey(type, id) { | |
| return `${CACHE_PREFIX}${type}_${id}`; | |
| } | |
| /** | |
| * Get cached messages for a conversation or room | |
| * @param {'dm' | 'room'} type | |
| * @param {string} id - conversationId or roomId | |
| * @returns {{ messages: Array, fetchedAt: number, page: number } | null} | |
| */ | |
| export function getCachedMessages(type, id) { | |
| if (!id) return null; | |
| try { | |
| const raw = localStorage.getItem(getKey(type, id)); | |
| if (!raw) return null; | |
| const parsed = JSON.parse(raw); | |
| // Validate structure | |
| if (!Array.isArray(parsed.messages)) return null; | |
| return { | |
| messages: parsed.messages, | |
| fetchedAt: parsed.fetchedAt || 0, | |
| page: parsed.page || 1, | |
| }; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Save messages to cache | |
| * @param {'dm' | 'room'} type | |
| * @param {string} id | |
| * @param {Array} messages | |
| * @param {number} page | |
| */ | |
| export function setCachedMessages(type, id, messages, page = 1) { | |
| if (!id || !Array.isArray(messages)) return; | |
| try { | |
| // Enforce max entries: remove oldest if exceeded | |
| enforceMaxEntries(); | |
| const payload = { | |
| messages, | |
| fetchedAt: Date.now(), | |
| page, | |
| }; | |
| localStorage.setItem(getKey(type, id), JSON.stringify(payload)); | |
| } catch (err) { | |
| // localStorage might be full — clear old caches | |
| if (err.name === "QuotaExceededError") { | |
| clearOldestCaches(10); | |
| try { | |
| const payload = { | |
| messages, | |
| fetchedAt: Date.now(), | |
| page, | |
| }; | |
| localStorage.setItem(getKey(type, id), JSON.stringify(payload)); | |
| } catch { | |
| // Still full, skip caching | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Check if cache is still valid (within TTL) | |
| * @param {'dm' | 'room'} type | |
| * @param {string} id | |
| * @param {number} ttlMs | |
| * @returns {boolean} | |
| */ | |
| export function isCacheValid(type, id, ttlMs = DEFAULT_TTL_MS) { | |
| const cached = getCachedMessages(type, id); | |
| if (!cached) return false; | |
| return Date.now() - cached.fetchedAt < ttlMs; | |
| } | |
| /** | |
| * Check if cache exists (regardless of TTL) | |
| * @param {'dm' | 'room'} type | |
| * @param {string} id | |
| * @returns {boolean} | |
| */ | |
| export function hasCache(type, id) { | |
| return getCachedMessages(type, id) !== null; | |
| } | |
| /** | |
| * Clear cache for a specific conversation/room | |
| * @param {'dm' | 'room'} type | |
| * @param {string} id | |
| */ | |
| export function clearCache(type, id) { | |
| if (!id) return; | |
| try { | |
| localStorage.removeItem(getKey(type, id)); | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| /** | |
| * Clear all message caches (including conversations) | |
| */ | |
| export function clearAllMessageCaches() { | |
| try { | |
| const keysToRemove = []; | |
| for (let i = 0; i < localStorage.length; i++) { | |
| const key = localStorage.key(i); | |
| if (key && key.startsWith(CACHE_PREFIX)) { | |
| keysToRemove.push(key); | |
| } | |
| } | |
| keysToRemove.forEach((key) => localStorage.removeItem(key)); | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| // Helper: enforce max cache entries | |
| function enforceMaxEntries() { | |
| const entries = []; | |
| for (let i = 0; i < localStorage.length; i++) { | |
| const key = localStorage.key(i); | |
| if (key && key.startsWith(CACHE_PREFIX)) { | |
| try { | |
| const raw = localStorage.getItem(key); | |
| const parsed = JSON.parse(raw); | |
| entries.push({ key, fetchedAt: parsed.fetchedAt || 0 }); | |
| } catch { | |
| // ignore invalid entries | |
| } | |
| } | |
| } | |
| if (entries.length > MAX_CACHE_ENTRIES) { | |
| // Sort by fetchedAt ascending (oldest first) | |
| entries.sort((a, b) => a.fetchedAt - b.fetchedAt); | |
| const toRemove = entries.slice(0, entries.length - MAX_CACHE_ENTRIES); | |
| toRemove.forEach((e) => localStorage.removeItem(e.key)); | |
| } | |
| } | |
| // ==================== Conversations Cache ==================== | |
| /** | |
| * Get cached conversations list | |
| * @returns {{ conversations: Array, fetchedAt: number } | null} | |
| */ | |
| export function getCachedConversations() { | |
| try { | |
| const raw = localStorage.getItem(CONV_CACHE_KEY); | |
| if (!raw) return null; | |
| const parsed = JSON.parse(raw); | |
| if (!Array.isArray(parsed.conversations)) return null; | |
| return { | |
| conversations: parsed.conversations, | |
| fetchedAt: parsed.fetchedAt || 0, | |
| }; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Save conversations list to cache | |
| * @param {Array} conversations | |
| */ | |
| export function setCachedConversations(conversations) { | |
| if (!Array.isArray(conversations)) return; | |
| try { | |
| const payload = { | |
| conversations, | |
| fetchedAt: Date.now(), | |
| }; | |
| localStorage.setItem(CONV_CACHE_KEY, JSON.stringify(payload)); | |
| } catch (err) { | |
| if (err.name === "QuotaExceededError") { | |
| clearAllMessageCaches(); | |
| try { | |
| localStorage.setItem(CONV_CACHE_KEY, JSON.stringify({ | |
| conversations, | |
| fetchedAt: Date.now(), | |
| })); | |
| } catch { | |
| // skip | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Check if conversations cache is valid | |
| * @param {number} ttlMs | |
| * @returns {boolean} | |
| */ | |
| export function isConversationsCacheValid(ttlMs = DEFAULT_TTL_MS) { | |
| const cached = getCachedConversations(); | |
| if (!cached) return false; | |
| return Date.now() - cached.fetchedAt < ttlMs; | |
| } | |
| /** | |
| * Check if conversations cache exists | |
| * @returns {boolean} | |
| */ | |
| export function hasConversationsCache() { | |
| return getCachedConversations() !== null; | |
| } | |
| // ==================== Spaces & Rooms Cache ==================== | |
| /** | |
| * Get cached spaces and roomsMap | |
| * @returns {{ spaces: Array, roomsMap: Object, fetchedAt: number } | null} | |
| */ | |
| export function getCachedSpaces() { | |
| try { | |
| const raw = localStorage.getItem(SPACES_CACHE_KEY); | |
| if (!raw) return null; | |
| const parsed = JSON.parse(raw); | |
| if (!Array.isArray(parsed.spaces)) return null; | |
| return { | |
| spaces: parsed.spaces, | |
| roomsMap: parsed.roomsMap || {}, | |
| fetchedAt: parsed.fetchedAt || 0, | |
| }; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Save spaces and roomsMap to cache | |
| * @param {Array} spaces | |
| * @param {Object} roomsMap | |
| */ | |
| export function setCachedSpaces(spaces, roomsMap) { | |
| if (!Array.isArray(spaces)) return; | |
| try { | |
| const payload = { | |
| spaces, | |
| roomsMap: roomsMap || {}, | |
| fetchedAt: Date.now(), | |
| }; | |
| localStorage.setItem(SPACES_CACHE_KEY, JSON.stringify(payload)); | |
| } catch (err) { | |
| if (err.name === "QuotaExceededError") { | |
| clearAllMessageCaches(); | |
| try { | |
| localStorage.setItem(SPACES_CACHE_KEY, JSON.stringify({ | |
| spaces, | |
| roomsMap: roomsMap || {}, | |
| fetchedAt: Date.now(), | |
| })); | |
| } catch { | |
| // skip | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Check if spaces cache exists | |
| * @returns {boolean} | |
| */ | |
| export function hasSpacesCache() { | |
| return getCachedSpaces() !== null; | |
| } | |
| // ==================== Members Cache ==================== | |
| /** | |
| * Get cached membersMap | |
| * @returns {{ membersMap: Object, fetchedAt: number } | null} | |
| */ | |
| export function getCachedMembers() { | |
| try { | |
| const raw = localStorage.getItem(MEMBERS_CACHE_KEY); | |
| if (!raw) return null; | |
| const parsed = JSON.parse(raw); | |
| return { | |
| membersMap: parsed.membersMap || {}, | |
| fetchedAt: parsed.fetchedAt || 0, | |
| }; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Save membersMap to cache | |
| * @param {Object} membersMap | |
| */ | |
| export function setCachedMembers(membersMap) { | |
| if (!membersMap || typeof membersMap !== "object") return; | |
| try { | |
| const payload = { | |
| membersMap, | |
| fetchedAt: Date.now(), | |
| }; | |
| localStorage.setItem(MEMBERS_CACHE_KEY, JSON.stringify(payload)); | |
| } catch (err) { | |
| if (err.name === "QuotaExceededError") { | |
| clearAllMessageCaches(); | |
| try { | |
| localStorage.setItem(MEMBERS_CACHE_KEY, JSON.stringify({ | |
| membersMap, | |
| fetchedAt: Date.now(), | |
| })); | |
| } catch { | |
| // skip | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Check if members cache exists | |
| * @returns {boolean} | |
| */ | |
| export function hasMembersCache() { | |
| return getCachedMembers() !== null; | |
| } | |
| // Helper: clear N oldest caches | |
| function clearOldestCaches(count) { | |
| const entries = []; | |
| for (let i = 0; i < localStorage.length; i++) { | |
| const key = localStorage.key(i); | |
| if (key && key.startsWith(CACHE_PREFIX)) { | |
| try { | |
| const raw = localStorage.getItem(key); | |
| const parsed = JSON.parse(raw); | |
| entries.push({ key, fetchedAt: parsed.fetchedAt || 0 }); | |
| } catch { | |
| // ignore | |
| } | |
| } | |
| } | |
| entries.sort((a, b) => a.fetchedAt - b.fetchedAt); | |
| entries.slice(0, count).forEach((e) => localStorage.removeItem(e.key)); | |
| } | |