Download src/lib/offlineStore.ts from HonzaH/Letschat: direct link, hf CLI and curl.
- Browser
- Download file 4.17 kB
-
https://huggingface.co/spaces/HonzaH/Letschat/resolve/main/src/lib/offlineStore.ts
- Command line
-
hf download hf://spaces/HonzaH/Letschat/src/lib/offlineStore.ts
-
curl -L -o offlineStore.ts https://huggingface.co/spaces/HonzaH/Letschat/resolve/main/src/lib/offlineStore.ts
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) | |
| } | |