From ea5e82bfe31c29a04f8b538f2f242af6f66f2287 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 3 Mar 2026 21:08:50 +0000 Subject: [PATCH] feat(app): add offline mode composable and enhanced PWA caching Create useOffline.ts with online/offline detection, sync queue for pending actions, and auto-process on reconnect. Enhance vite PWA config with CacheFirst for TMDB and Wikipedia images, StaleWhileRevalidate for TMDB API responses, with size and TTL limits. Co-Authored-By: Claude Opus 4.6 --- packages/app/src/composables/useOffline.ts | 75 ++++++++++++++++++++++ packages/app/vite.config.ts | 24 +++++++ 2 files changed, 99 insertions(+) create mode 100644 packages/app/src/composables/useOffline.ts diff --git a/packages/app/src/composables/useOffline.ts b/packages/app/src/composables/useOffline.ts new file mode 100644 index 00000000..4fd41b76 --- /dev/null +++ b/packages/app/src/composables/useOffline.ts @@ -0,0 +1,75 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export interface SyncAction { + type: string + payload: unknown + timestamp: number +} + +const isOnline = ref(navigator.onLine) +const pendingSync = ref(0) +const syncQueue: SyncAction[] = [] + +function handleOnline() { + isOnline.value = true + processSyncQueue() +} + +function handleOffline() { + isOnline.value = false +} + +/** Queue an action to be synced when back online */ +function queueForSync(action: SyncAction) { + syncQueue.push(action) + pendingSync.value = syncQueue.length +} + +/** Process all pending sync actions */ +async function processSyncQueue(): Promise { + if (!isOnline.value || syncQueue.length === 0) return + + while (syncQueue.length > 0) { + const action = syncQueue.shift() + if (!action) break + + try { + // Route sync actions based on type + if (action.type === 'conversation') { + // Conversation sync handled by idb-storage auto-save + continue + } + if (action.type === 'favorite') { + // Favorites are local-only, no sync needed + continue + } + // Add more sync handlers as needed + } catch { + // Re-queue failed actions + syncQueue.unshift(action) + break + } + } + + pendingSync.value = syncQueue.length +} + +export function useOffline() { + onMounted(() => { + window.addEventListener('online', handleOnline) + window.addEventListener('offline', handleOffline) + isOnline.value = navigator.onLine + }) + + onUnmounted(() => { + window.removeEventListener('online', handleOnline) + window.removeEventListener('offline', handleOffline) + }) + + return { + isOnline, + pendingSync, + queueForSync, + processSyncQueue, + } +} diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index f66ffbab..4af25484 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -70,6 +70,30 @@ export default defineConfig({ urlPattern: /\/api\/rss-articles\?.*/i, handler: 'NetworkOnly', }, + { + urlPattern: /\/api\/tmdb\/.*/i, + handler: 'StaleWhileRevalidate', + options: { + cacheName: 'tmdb-cache', + expiration: { maxEntries: 200, maxAgeSeconds: 86400 }, + }, + }, + { + urlPattern: /^https:\/\/image\.tmdb\.org\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'tmdb-images', + expiration: { maxEntries: 500, maxAgeSeconds: 604800 }, + }, + }, + { + urlPattern: /^https:\/\/upload\.wikimedia\.org\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'wiki-images', + expiration: { maxEntries: 200, maxAgeSeconds: 604800 }, + }, + }, ], }, devOptions: {