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: {