Download vetcopilot-app/src/utils/offlineCache.ts from WWsCa/VetNet-LCPS: direct link, hf CLI and curl.
- Browser
- Download file 8.2 kB
-
https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/utils/offlineCache.ts
- Command line
-
hf download hf://WWsCa/VetNet-LCPS/vetcopilot-app/src/utils/offlineCache.ts
-
curl -L -o offlineCache.ts https://huggingface.co/WWsCa/VetNet-LCPS/resolve/main/vetcopilot-app/src/utils/offlineCache.ts
8.2 kB
| /** | |
| * Offline SOAP Cache Utility | |
| * | |
| * Uses IndexedDB to cache consultation data (consultation info + SOAP + patient) | |
| * locally for offline access. Supports up to 50 entries with automatic eviction | |
| * of the oldest entries when the limit is exceeded. | |
| */ | |
| import type { SoapData } from '../api' | |
| // ================================================================ | |
| // Types | |
| // ================================================================ | |
| /** Consultation summary stored alongside SOAP and patient data */ | |
| export interface CachedConsultationInfo { | |
| id: string | |
| animal_name: string | |
| species: string | |
| breed: string | |
| age: string | |
| sex: string | |
| weight: string | |
| doctor_name: string | |
| status: string | |
| created_at: string | |
| } | |
| /** Patient data stored in the cache */ | |
| export interface CachedPatientInfo { | |
| animal_name: string | |
| species: string | |
| breed: string | |
| age: string | |
| weight: string | |
| allergy_history: string | |
| } | |
| /** Full cache entry matching the shared record pattern */ | |
| export interface CachedConsultation { | |
| /** Consultation ID (also the cache key) */ | |
| id: string | |
| /** Consultation summary metadata */ | |
| consultation: CachedConsultationInfo | |
| /** SOAP data (may be null if not yet generated) */ | |
| soap: SoapData | null | |
| /** Patient information */ | |
| patient: CachedPatientInfo | null | |
| /** Unix timestamp (ms) when this entry was cached */ | |
| cached_at: number | |
| } | |
| // ================================================================ | |
| // Constants | |
| // ================================================================ | |
| const DB_NAME = 'vetcopilot_offline_cache' | |
| const DB_VERSION = 1 | |
| const STORE_NAME = 'consultations' | |
| const MAX_ENTRIES = 50 | |
| // ================================================================ | |
| // IndexedDB helpers | |
| // ================================================================ | |
| /** Open (or create) the IndexedDB database */ | |
| function openDB(): Promise<IDBDatabase> { | |
| return new Promise((resolve, reject) => { | |
| const request = indexedDB.open(DB_NAME, DB_VERSION) | |
| request.onupgradeneeded = (event) => { | |
| const db = (event.target as IDBOpenDBRequest).result | |
| if (!db.objectStoreNames.contains(STORE_NAME)) { | |
| const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' }) | |
| // Create an index on cached_at for efficient eviction queries | |
| store.createIndex('cached_at', 'cached_at', { unique: false }) | |
| } | |
| } | |
| request.onsuccess = (event) => { | |
| resolve((event.target as IDBOpenDBRequest).result) | |
| } | |
| request.onerror = (event) => { | |
| reject( | |
| new Error( | |
| `Failed to open IndexedDB: ${(event.target as IDBOpenDBRequest).error?.message ?? 'unknown error'}`, | |
| ), | |
| ) | |
| } | |
| request.onblocked = () => { | |
| reject(new Error('IndexedDB blocked: close other tabs using this database')) | |
| } | |
| }) | |
| } | |
| /** Perform a read-write transaction on the store and return its result */ | |
| async function withStore<T>( | |
| mode: IDBTransactionMode, | |
| callback: (store: IDBObjectStore) => IDBRequest<T>, | |
| ): Promise<T> { | |
| const db = await openDB() | |
| return new Promise<T>((resolve, reject) => { | |
| const transaction = db.transaction(STORE_NAME, mode) | |
| const store = transaction.objectStore(STORE_NAME) | |
| const request = callback(store) | |
| request.onsuccess = () => { | |
| resolve(request.result) | |
| } | |
| request.onerror = () => { | |
| reject( | |
| new Error( | |
| `IndexedDB request failed: ${request.error?.message ?? 'unknown error'}`, | |
| ), | |
| ) | |
| } | |
| // Close DB when transaction completes (or fails) | |
| transaction.oncomplete = () => { | |
| db.close() | |
| } | |
| transaction.onerror = () => { | |
| db.close() | |
| reject( | |
| new Error( | |
| `IndexedDB transaction failed: ${transaction.error?.message ?? 'unknown error'}`, | |
| ), | |
| ) | |
| } | |
| transaction.onabort = () => { | |
| db.close() | |
| reject(new Error('IndexedDB transaction aborted')) | |
| } | |
| }) | |
| } | |
| // ================================================================ | |
| // Eviction | |
| // ================================================================ | |
| /** | |
| * Remove the oldest entries if the store exceeds MAX_ENTRIES. | |
| * Uses the cached_at index to find and delete the oldest. | |
| */ | |
| async function evictOldestIfNeeded(): Promise<void> { | |
| const db = await openDB() | |
| return new Promise((resolve, reject) => { | |
| const transaction = db.transaction(STORE_NAME, 'readwrite') | |
| const store = transaction.objectStore(STORE_NAME) | |
| const countRequest = store.count() | |
| countRequest.onsuccess = () => { | |
| const count = countRequest.result | |
| if (count <= MAX_ENTRIES) { | |
| db.close() | |
| resolve() | |
| return | |
| } | |
| const excess = count - MAX_ENTRIES | |
| const index = store.index('cached_at') | |
| const cursorRequest = index.openCursor() | |
| let deleted = 0 | |
| cursorRequest.onsuccess = (event) => { | |
| const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result | |
| if (cursor && deleted < excess) { | |
| cursor.delete() | |
| deleted++ | |
| cursor.continue() | |
| } | |
| // Cursor exhausted or enough deleted | |
| } | |
| } | |
| countRequest.onerror = () => { | |
| db.close() | |
| reject(new Error('Failed to count cached entries')) | |
| } | |
| transaction.oncomplete = () => { | |
| db.close() | |
| resolve() | |
| } | |
| transaction.onerror = () => { | |
| db.close() | |
| reject(new Error('Eviction transaction failed')) | |
| } | |
| transaction.onabort = () => { | |
| db.close() | |
| reject(new Error('Eviction transaction aborted')) | |
| } | |
| }) | |
| } | |
| // ================================================================ | |
| // Public API | |
| // ================================================================ | |
| /** | |
| * Cache a full consultation record (consultation info + SOAP + patient). | |
| * | |
| * If an entry with the same ID already exists, it will be replaced. | |
| * After insertion, if total entries exceed MAX_ENTRIES (50), the oldest | |
| * entries are automatically evicted. | |
| */ | |
| export async function cacheConsultation( | |
| data: CachedConsultation, | |
| ): Promise<void> { | |
| await withStore('readwrite', (store) => store.put(data)) | |
| await evictOldestIfNeeded() | |
| } | |
| /** | |
| * Retrieve a single cached consultation by its ID. | |
| * Returns null if the consultation is not cached. | |
| */ | |
| export async function getCachedConsultation( | |
| id: string, | |
| ): Promise<CachedConsultation | null> { | |
| try { | |
| const result = await withStore<CachedConsultation | undefined>( | |
| 'readonly', | |
| (store) => store.get(id), | |
| ) | |
| return result ?? null | |
| } catch { | |
| return null | |
| } | |
| } | |
| /** | |
| * Retrieve all cached consultations ordered by cached_at descending (newest first). | |
| */ | |
| export async function getAllCachedConsultations(): Promise<CachedConsultation[]> { | |
| try { | |
| const db = await openDB() | |
| return new Promise((resolve, reject) => { | |
| const transaction = db.transaction(STORE_NAME, 'readonly') | |
| const store = transaction.objectStore(STORE_NAME) | |
| const results: CachedConsultation[] = [] | |
| const request = store.openCursor() | |
| request.onsuccess = (event) => { | |
| const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result | |
| if (cursor) { | |
| results.push(cursor.value as CachedConsultation) | |
| cursor.continue() | |
| } else { | |
| // Sort newest first | |
| results.sort((a, b) => b.cached_at - a.cached_at) | |
| resolve(results) | |
| } | |
| } | |
| request.onerror = () => { | |
| reject(new Error('Failed to read all cached items')) | |
| } | |
| transaction.oncomplete = () => { | |
| db.close() | |
| } | |
| transaction.onerror = () => { | |
| db.close() | |
| reject(new Error('Transaction failed')) | |
| } | |
| transaction.onabort = () => { | |
| db.close() | |
| reject(new Error('Transaction aborted')) | |
| } | |
| }) | |
| } catch { | |
| return [] | |
| } | |
| } | |
| /** | |
| * Remove a single cached consultation by its ID. | |
| */ | |
| export async function removeConsultation(id: string): Promise<void> { | |
| await withStore('readwrite', (store) => store.delete(id)) | |
| } | |
| /** | |
| * Remove all cached consultations from the store. | |
| */ | |
| export async function clearAll(): Promise<void> { | |
| await withStore('readwrite', (store) => store.clear()) | |
| } | |