feat: offline resilience and connection status
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
627a1a0d9e
commit
6e092ee7be
@@ -0,0 +1,71 @@
|
||||
const DB_NAME = 'botfights_cache'
|
||||
const STORE_NAME = 'fights'
|
||||
const MAX_CACHED = 5
|
||||
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, 1)
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' })
|
||||
}
|
||||
}
|
||||
req.onsuccess = () => resolve(req.result)
|
||||
req.onerror = () => reject(req.error)
|
||||
})
|
||||
}
|
||||
|
||||
/** Cache a fight replay for offline viewing */
|
||||
export async function cacheFight(fightData: { id: string; [key: string]: any }): Promise<void> {
|
||||
try {
|
||||
const db = await openDB()
|
||||
const tx = db.transaction(STORE_NAME, 'readwrite')
|
||||
const store = tx.objectStore(STORE_NAME)
|
||||
|
||||
// Add with timestamp
|
||||
store.put({ ...fightData, _cachedAt: Date.now() })
|
||||
|
||||
// Evict oldest if over limit
|
||||
const countReq = store.count()
|
||||
countReq.onsuccess = () => {
|
||||
if (countReq.result > MAX_CACHED) {
|
||||
const cursor = store.openCursor()
|
||||
let oldest: { key: IDBValidKey; time: number } | null = null
|
||||
cursor.onsuccess = () => {
|
||||
const c = cursor.result
|
||||
if (c) {
|
||||
const t = (c.value as any)._cachedAt || 0
|
||||
if (!oldest || t < oldest.time) oldest = { key: c.key, time: t }
|
||||
c.continue()
|
||||
} else if (oldest) {
|
||||
store.delete(oldest.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.close()
|
||||
} catch { /* IndexedDB not available */ }
|
||||
}
|
||||
|
||||
/** Get a cached fight by ID */
|
||||
export async function getCachedFight(id: string): Promise<any | null> {
|
||||
try {
|
||||
const db = await openDB()
|
||||
return new Promise((resolve) => {
|
||||
const tx = db.transaction(STORE_NAME, 'readonly')
|
||||
const req = tx.objectStore(STORE_NAME).get(id)
|
||||
req.onsuccess = () => {
|
||||
db.close()
|
||||
resolve(req.result || null)
|
||||
}
|
||||
req.onerror = () => {
|
||||
db.close()
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const isOnline = ref(typeof navigator !== 'undefined' ? navigator.onLine : true)
|
||||
|
||||
let listenerCount = 0
|
||||
|
||||
function handleOnline() { isOnline.value = true }
|
||||
function handleOffline() { isOnline.value = false }
|
||||
|
||||
export function useOnlineStatus() {
|
||||
onMounted(() => {
|
||||
if (listenerCount === 0) {
|
||||
window.addEventListener('online', handleOnline)
|
||||
window.addEventListener('offline', handleOffline)
|
||||
}
|
||||
listenerCount++
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
listenerCount--
|
||||
if (listenerCount === 0) {
|
||||
window.removeEventListener('online', handleOnline)
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
}
|
||||
})
|
||||
|
||||
return { isOnline }
|
||||
}
|
||||
Reference in New Issue
Block a user