Letschat / src /lib /offlineStore.ts
HonzaH's picture
Upload 192 files
24b7e75 verified
Raw
History Blame Contribute Delete
4.17 kB
// 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)
}