092_user_interface / src /store /messageCache.js
anotherath's picture
update space and room
57f5158
/**
* 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));
}