mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 23:24:22 +01:00
feat: add Service Worker for robust offline image caching
- Implement Service Worker to intercept and cache image requests - Service Worker persists across hard reloads unlike Cache API alone - Simplify useImageCache hook to work with Service Worker - Images now work offline even after hard reload - Service Worker handles transparent cache-first serving for images
This commit is contained in:
56
public/sw.js
Normal file
56
public/sw.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Service Worker for Boris - handles offline image caching
|
||||
const CACHE_NAME = 'boris-image-cache-v1'
|
||||
|
||||
// Install event - activate immediately
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Installing service worker...')
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
// Activate event - take control immediately
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating service worker...')
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
// Fetch event - intercept image requests
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url)
|
||||
|
||||
// Only intercept image requests
|
||||
const isImage = event.request.destination === 'image' ||
|
||||
/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(url.pathname)
|
||||
|
||||
if (!isImage) {
|
||||
return // Let other requests pass through
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
return cache.match(event.request).then(cachedResponse => {
|
||||
if (cachedResponse) {
|
||||
console.log('[SW] Serving cached image:', url.pathname)
|
||||
return cachedResponse
|
||||
}
|
||||
|
||||
// Not in cache, try to fetch
|
||||
return fetch(event.request)
|
||||
.then(response => {
|
||||
// Only cache successful responses
|
||||
if (response && response.status === 200) {
|
||||
// Clone the response before caching
|
||||
cache.put(event.request, response.clone())
|
||||
console.log('[SW] Cached new image:', url.pathname)
|
||||
}
|
||||
return response
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[SW] Fetch failed for:', url.pathname, error)
|
||||
// Return a fallback or let it fail
|
||||
throw error
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,94 +1,46 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { cacheImage, getCachedImage, loadCachedImage } from '../services/imageCacheService'
|
||||
import { cacheImage, getCachedImage } from '../services/imageCacheService'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
|
||||
/**
|
||||
* Hook to cache and retrieve images using Cache API
|
||||
* Hook to pre-cache images and return the URL for display
|
||||
* With Service Worker active, images are automatically cached and served offline
|
||||
* This hook ensures proactive caching for better offline experience
|
||||
*
|
||||
* @param imageUrl - The URL of the image to cache
|
||||
* @param settings - User settings to determine if caching is enabled
|
||||
* @returns The cached blob URL or the original URL
|
||||
* @returns The image URL (Service Worker handles caching transparently)
|
||||
*/
|
||||
export function useImageCache(
|
||||
imageUrl: string | undefined,
|
||||
settings: UserSettings | undefined
|
||||
): string | undefined {
|
||||
const [cachedUrl, setCachedUrl] = useState<string | undefined>(imageUrl)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [displayUrl, setDisplayUrl] = useState<string | undefined>(imageUrl)
|
||||
|
||||
useEffect(() => {
|
||||
if (!imageUrl) {
|
||||
setCachedUrl(undefined)
|
||||
setDisplayUrl(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
// If caching is disabled, just use the original URL
|
||||
const enableCache = settings?.enableImageCache ?? true // Default to enabled
|
||||
if (!enableCache) {
|
||||
setCachedUrl(imageUrl)
|
||||
// Always show the original URL - Service Worker will serve from cache if available
|
||||
setDisplayUrl(imageUrl)
|
||||
|
||||
// If caching is disabled, don't pre-cache
|
||||
const enableCache = settings?.enableImageCache ?? true
|
||||
if (!enableCache || !navigator.onLine) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store imageUrl in local variable for closure
|
||||
const urlToCache = imageUrl
|
||||
const isOffline = !navigator.onLine
|
||||
|
||||
// When online: show original URL first for immediate display
|
||||
// When offline: don't show anything until we load from cache
|
||||
if (!isOffline) {
|
||||
setCachedUrl(urlToCache)
|
||||
}
|
||||
|
||||
// Try to load from cache asynchronously
|
||||
loadCachedImage(urlToCache)
|
||||
.then(blobUrl => {
|
||||
if (blobUrl) {
|
||||
console.log('📦 Using cached image:', urlToCache.substring(0, 50))
|
||||
setCachedUrl(blobUrl)
|
||||
} else if (!isOffline) {
|
||||
// Not cached and online - cache it now
|
||||
if (!isLoading) {
|
||||
setIsLoading(true)
|
||||
// Pre-cache the image for offline availability
|
||||
// Service Worker will handle serving it, but we ensure it's cached
|
||||
const maxSize = settings?.imageCacheSizeMB ?? 210
|
||||
|
||||
cacheImage(urlToCache, maxSize)
|
||||
.then(newBlobUrl => {
|
||||
// Only update if we got a blob URL back
|
||||
if (newBlobUrl && newBlobUrl.startsWith('blob:')) {
|
||||
setCachedUrl(newBlobUrl)
|
||||
}
|
||||
cacheImage(imageUrl, maxSize).catch(err => {
|
||||
console.warn('Failed to pre-cache image:', err)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to cache image:', err)
|
||||
// Keep using original URL on error
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Offline and not cached - no image available
|
||||
console.warn('⚠️ Image not available offline:', urlToCache.substring(0, 50))
|
||||
setCachedUrl(undefined)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load cached image:', err)
|
||||
// If online, fall back to original URL
|
||||
if (!isOffline) {
|
||||
setCachedUrl(urlToCache)
|
||||
}
|
||||
})
|
||||
|
||||
// Cleanup: revoke blob URLs when component unmounts or URL changes
|
||||
return () => {
|
||||
if (cachedUrl && cachedUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(cachedUrl)
|
||||
}
|
||||
}
|
||||
}, [imageUrl, settings?.enableImageCache, settings?.imageCacheSizeMB])
|
||||
|
||||
return cachedUrl
|
||||
return displayUrl
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
26
src/main.tsx
26
src/main.tsx
@@ -3,6 +3,32 @@ import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
// Register Service Worker for offline image caching
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('✅ Service Worker registered:', registration.scope)
|
||||
|
||||
// Update service worker when a new version is available
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'activated') {
|
||||
console.log('🔄 Service Worker updated, page may need reload')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Service Worker registration failed:', error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
Reference in New Issue
Block a user