File size: 4,165 Bytes
9853b20 24b7e75 9853b20 24b7e75 9853b20 24b7e75 9853b20 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | // 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<IDBDatabase> {
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<void>((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<any[]> {
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<void>((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<OfflineMessage, 'id' | 'created_at'> & Partial<Pick<OfflineMessage, 'created_at'>>) {
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<OfflineMessage, 'id' | 'created_at'> & Partial<Pick<OfflineMessage, 'created_at'>>) {
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<OfflineMessage[]> {
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<OfflineMessage[]> {
if (!isBrowser()) return []
return getAll(STORE_PENDING)
}
export async function clearPendingMessages(ids: string[]) {
if (!isBrowser() || ids.length === 0) return
await deleteKeys(STORE_PENDING, ids)
}
|