// Simple IndexedDB-backed offline store for messages and pending queue. // All functions are no-ops on server/SSR. // Use browser crypto.randomUUID when available, fallback to simple UUID v4 function genId() { try { if (typeof crypto !== 'undefined' && (crypto as any).randomUUID) return (crypto as any).randomUUID() } catch (e) { // ignore } // fallback UUID v4 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } type OfflineMessage = { id: string room_id: string code_id: string user_id?: string | null content: string image_url?: string | null created_at: string pending?: boolean } const DB_NAME = 'letschat_offline' const DB_VERSION = 1 const STORE_MESSAGES = 'messages' const STORE_PENDING = 'pending_messages' function isBrowser() { return typeof window !== 'undefined' && typeof indexedDB !== 'undefined' } function openDB(): Promise { return new Promise((resolve, reject) => { if (!isBrowser()) { reject(new Error('IndexedDB not available')) return } const req = indexedDB.open(DB_NAME, DB_VERSION) req.onupgradeneeded = () => { const db = req.result if (!db.objectStoreNames.contains(STORE_MESSAGES)) { db.createObjectStore(STORE_MESSAGES, { keyPath: 'id' }) } if (!db.objectStoreNames.contains(STORE_PENDING)) { db.createObjectStore(STORE_PENDING, { keyPath: 'id' }) } } req.onsuccess = () => resolve(req.result) req.onerror = () => reject(req.error || new Error('IndexedDB open error')) }) } async function put(storeName: string, value: any) { const db = await openDB() await new Promise((resolve, reject) => { const tx = db.transaction(storeName, 'readwrite') tx.objectStore(storeName).put(value) tx.oncomplete = () => resolve() tx.onerror = () => reject(tx.error || new Error('IndexedDB put error')) }) } async function getAll(storeName: string): Promise { const db = await openDB() return new Promise((resolve, reject) => { const tx = db.transaction(storeName, 'readonly') const req = tx.objectStore(storeName).getAll() req.onsuccess = () => resolve(req.result || []) req.onerror = () => reject(req.error || new Error('IndexedDB getAll error')) }) } async function deleteKeys(storeName: string, ids: string[]) { const db = await openDB() await new Promise((resolve, reject) => { const tx = db.transaction(storeName, 'readwrite') const store = tx.objectStore(storeName) ids.forEach((id) => store.delete(id)) tx.oncomplete = () => resolve() tx.onerror = () => reject(tx.error || new Error('IndexedDB delete error')) }) } export async function saveLocalMessage(message: Omit & Partial>) { if (!isBrowser()) return null const msg: OfflineMessage = { id: genId(), created_at: message.created_at || new Date().toISOString(), ...message } await put(STORE_MESSAGES, msg) return msg } export async function savePendingMessage(message: Omit & Partial>) { if (!isBrowser()) return null const msg: OfflineMessage = { id: genId(), created_at: message.created_at || new Date().toISOString(), pending: true, ...message } await put(STORE_PENDING, msg) await put(STORE_MESSAGES, msg) return msg } export async function getLocalMessages(roomId: string, limit = 50): Promise { if (!isBrowser()) return [] const all = await getAll(STORE_MESSAGES) return all .filter((m) => m.room_id === roomId) .sort((a, b) => a.created_at.localeCompare(b.created_at)) .slice(-limit) } export async function getPendingMessages(): Promise { if (!isBrowser()) return [] return getAll(STORE_PENDING) } export async function clearPendingMessages(ids: string[]) { if (!isBrowser() || ids.length === 0) return await deleteKeys(STORE_PENDING, ids) }