feat: add caching for backend objects

This commit is contained in:
2026-04-21 19:38:57 -04:00
parent 5b4955f07e
commit 7f1e82acc2
14 changed files with 637 additions and 62 deletions

View File

@@ -0,0 +1,74 @@
import { weekMonday } from '~/utils/toronto'
const TTL_MS = 24 * 60 * 60 * 1000
interface CacheEntry<T> {
data: T
ts: number
}
function storageKey(key: string) {
return `cache:${key}`
}
export function useAppCache() {
function set<T>(key: string, data: T): void {
try {
localStorage.setItem(storageKey(key), JSON.stringify({ data, ts: Date.now() } satisfies CacheEntry<T>))
} catch { /* quota exceeded or unavailable */ }
}
/** Returns data only if fresh (< 24 h). Returns null if stale or absent. */
function get<T>(key: string): T | null {
try {
const raw = localStorage.getItem(storageKey(key))
if (!raw) return null
const entry = JSON.parse(raw) as CacheEntry<T>
if (Date.now() - entry.ts > TTL_MS) return null
return entry.data
} catch { return null }
}
/** Returns data regardless of age — for offline fallback when cache is stale. */
function peek<T>(key: string): T | null {
try {
const raw = localStorage.getItem(storageKey(key))
if (!raw) return null
return (JSON.parse(raw) as CacheEntry<T>).data
} catch { return null }
}
/** Returns age in ms, or null if absent. */
function age(key: string): number | null {
try {
const raw = localStorage.getItem(storageKey(key))
if (!raw) return null
return Date.now() - (JSON.parse(raw) as CacheEntry<unknown>).ts
} catch { return null }
}
function invalidate(key: string): void {
localStorage.removeItem(storageKey(key))
}
/** Removes all cache entries whose key starts with prefix. */
function invalidatePrefix(prefix: string): void {
const full = storageKey(prefix)
const toRemove: string[] = []
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i)
if (k?.startsWith(full)) toRemove.push(k)
}
toRemove.forEach(k => localStorage.removeItem(k))
}
/**
* Cache key for schedule data (intervals or slots) for the ISO week containing
* the given UTC ISO timestamp. Desktop and mobile both key to week-Monday.
*/
function weekKey(utcIso: string): string {
return weekMonday(utcIso.slice(0, 10))
}
return { set, get, peek, age, invalidate, invalidatePrefix, weekKey }
}

View File

@@ -0,0 +1,26 @@
import type { Database } from '~/types/supabase'
type Boat = Database['public']['Tables']['boats']['Row']
export interface BookingDraft {
boat: Boat
startTime: string
endTime: string
}
const _draft = ref<BookingDraft | null>(null)
export function useBookingDraft() {
function set(boat: Boat, startTime: string, endTime: string): void {
_draft.value = { boat, startTime, endTime }
}
/** Reads and clears the draft — call once in the destination page. */
function take(): BookingDraft | null {
const val = _draft.value
_draft.value = null
return val
}
return { set, take }
}

View File

@@ -0,0 +1,10 @@
const _isOnline = ref(typeof navigator !== 'undefined' ? navigator.onLine : true)
if (import.meta.client) {
window.addEventListener('online', () => { _isOnline.value = true })
window.addEventListener('offline', () => { _isOnline.value = false })
}
export function useOfflineStatus() {
return { isOnline: readonly(_isOnline) }
}