311 lines
8.5 KiB
JavaScript
311 lines
8.5 KiB
JavaScript
// Miracles in Motion - Service Worker
|
|
// Version 1.0.0
|
|
|
|
const CACHE_NAME = 'miracles-in-motion-v1'
|
|
const STATIC_CACHE = 'static-v1'
|
|
const DYNAMIC_CACHE = 'dynamic-v1'
|
|
|
|
// Assets to cache immediately
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/index.html',
|
|
'/favicon.svg',
|
|
'/robots.txt',
|
|
'/site.webmanifest'
|
|
]
|
|
|
|
// Assets to cache on demand
|
|
const CACHE_STRATEGIES = {
|
|
// Cache first for static assets
|
|
CACHE_FIRST: ['.js', '.css', '.woff', '.woff2', '.ttf', '.eot'],
|
|
// Network first for API calls
|
|
NETWORK_FIRST: ['/api/', '/analytics/'],
|
|
// Stale while revalidate for images
|
|
STALE_WHILE_REVALIDATE: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']
|
|
}
|
|
|
|
// Install event - cache static assets
|
|
self.addEventListener('install', event => {
|
|
console.log('Service Worker: Installing...')
|
|
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE)
|
|
.then(cache => {
|
|
console.log('Service Worker: Caching static assets')
|
|
return cache.addAll(STATIC_ASSETS)
|
|
})
|
|
.then(() => {
|
|
console.log('Service Worker: Installation complete')
|
|
return self.skipWaiting()
|
|
})
|
|
.catch(error => {
|
|
console.error('Service Worker: Installation failed', error)
|
|
})
|
|
)
|
|
})
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', event => {
|
|
console.log('Service Worker: Activating...')
|
|
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then(cacheNames => {
|
|
return Promise.all(
|
|
cacheNames.map(cacheName => {
|
|
if (cacheName !== CACHE_NAME && cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
|
|
console.log('Service Worker: Deleting old cache', cacheName)
|
|
return caches.delete(cacheName)
|
|
}
|
|
})
|
|
)
|
|
})
|
|
.then(() => {
|
|
console.log('Service Worker: Activation complete')
|
|
return self.clients.claim()
|
|
})
|
|
)
|
|
})
|
|
|
|
// Fetch event - handle requests with appropriate caching strategy
|
|
self.addEventListener('fetch', event => {
|
|
const { request } = event
|
|
const url = new URL(request.url)
|
|
|
|
// Skip non-GET requests
|
|
if (request.method !== 'GET') return
|
|
|
|
// Skip cross-origin requests
|
|
if (url.origin !== location.origin) return
|
|
|
|
event.respondWith(handleFetch(request))
|
|
})
|
|
|
|
// Determine caching strategy based on file type
|
|
function getCachingStrategy(url) {
|
|
const pathname = new URL(url).pathname.toLowerCase()
|
|
|
|
// Check for cache-first assets
|
|
if (CACHE_STRATEGIES.CACHE_FIRST.some(ext => pathname.includes(ext))) {
|
|
return 'cache-first'
|
|
}
|
|
|
|
// Check for network-first assets
|
|
if (CACHE_STRATEGIES.NETWORK_FIRST.some(path => pathname.includes(path))) {
|
|
return 'network-first'
|
|
}
|
|
|
|
// Check for stale-while-revalidate assets
|
|
if (CACHE_STRATEGIES.STALE_WHILE_REVALIDATE.some(ext => pathname.includes(ext))) {
|
|
return 'stale-while-revalidate'
|
|
}
|
|
|
|
// Default to network-first
|
|
return 'network-first'
|
|
}
|
|
|
|
// Handle fetch requests with appropriate strategy
|
|
async function handleFetch(request) {
|
|
const strategy = getCachingStrategy(request.url)
|
|
|
|
switch (strategy) {
|
|
case 'cache-first':
|
|
return cacheFirst(request)
|
|
case 'network-first':
|
|
return networkFirst(request)
|
|
case 'stale-while-revalidate':
|
|
return staleWhileRevalidate(request)
|
|
default:
|
|
return networkFirst(request)
|
|
}
|
|
}
|
|
|
|
// Cache-first strategy
|
|
async function cacheFirst(request) {
|
|
try {
|
|
const cache = await caches.open(STATIC_CACHE)
|
|
const cachedResponse = await cache.match(request)
|
|
|
|
if (cachedResponse) {
|
|
return cachedResponse
|
|
}
|
|
|
|
const networkResponse = await fetch(request)
|
|
if (networkResponse.ok) {
|
|
cache.put(request, networkResponse.clone())
|
|
}
|
|
return networkResponse
|
|
} catch (error) {
|
|
console.error('Cache-first strategy failed:', error)
|
|
return new Response('Offline content unavailable', { status: 503 })
|
|
}
|
|
}
|
|
|
|
// Network-first strategy
|
|
async function networkFirst(request) {
|
|
try {
|
|
const networkResponse = await fetch(request)
|
|
|
|
if (networkResponse.ok) {
|
|
const cache = await caches.open(DYNAMIC_CACHE)
|
|
cache.put(request, networkResponse.clone())
|
|
}
|
|
|
|
return networkResponse
|
|
} catch (error) {
|
|
console.log('Network failed, trying cache:', error)
|
|
|
|
const cache = await caches.open(DYNAMIC_CACHE)
|
|
const cachedResponse = await cache.match(request)
|
|
|
|
if (cachedResponse) {
|
|
return cachedResponse
|
|
}
|
|
|
|
return new Response('Content unavailable offline', {
|
|
status: 503,
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
})
|
|
}
|
|
}
|
|
|
|
// Stale-while-revalidate strategy
|
|
async function staleWhileRevalidate(request) {
|
|
const cache = await caches.open(DYNAMIC_CACHE)
|
|
const cachedResponse = await cache.match(request)
|
|
|
|
const networkUpdate = fetch(request).then(response => {
|
|
if (response.ok) {
|
|
cache.put(request, response.clone())
|
|
}
|
|
return response
|
|
})
|
|
|
|
return cachedResponse || networkUpdate
|
|
}
|
|
|
|
// Background sync for offline form submissions
|
|
self.addEventListener('sync', event => {
|
|
if (event.tag === 'donation-submission') {
|
|
event.waitUntil(syncDonations())
|
|
}
|
|
if (event.tag === 'assistance-request') {
|
|
event.waitUntil(syncAssistanceRequests())
|
|
}
|
|
})
|
|
|
|
// Sync offline donations
|
|
async function syncDonations() {
|
|
try {
|
|
const donations = await getOfflineData('pending-donations')
|
|
for (const donation of donations) {
|
|
await fetch('/api/donations', {
|
|
method: 'POST',
|
|
body: JSON.stringify(donation),
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
}
|
|
await clearOfflineData('pending-donations')
|
|
console.log('Offline donations synced successfully')
|
|
} catch (error) {
|
|
console.error('Failed to sync donations:', error)
|
|
}
|
|
}
|
|
|
|
// Sync offline assistance requests
|
|
async function syncAssistanceRequests() {
|
|
try {
|
|
const requests = await getOfflineData('pending-requests')
|
|
for (const request of requests) {
|
|
await fetch('/api/assistance-requests', {
|
|
method: 'POST',
|
|
body: JSON.stringify(request),
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
}
|
|
await clearOfflineData('pending-requests')
|
|
console.log('Offline assistance requests synced successfully')
|
|
} catch (error) {
|
|
console.error('Failed to sync assistance requests:', error)
|
|
}
|
|
}
|
|
|
|
// Helper functions for offline data management
|
|
function getOfflineData(key) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('MiraclesInMotion', 1)
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result
|
|
const transaction = db.transaction(['offlineData'], 'readonly')
|
|
const store = transaction.objectStore('offlineData')
|
|
const getRequest = store.get(key)
|
|
|
|
getRequest.onsuccess = () => {
|
|
resolve(getRequest.result?.data || [])
|
|
}
|
|
|
|
getRequest.onerror = () => reject(getRequest.error)
|
|
}
|
|
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
}
|
|
|
|
function clearOfflineData(key) {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open('MiraclesInMotion', 1)
|
|
|
|
request.onsuccess = () => {
|
|
const db = request.result
|
|
const transaction = db.transaction(['offlineData'], 'readwrite')
|
|
const store = transaction.objectStore('offlineData')
|
|
const deleteRequest = store.delete(key)
|
|
|
|
deleteRequest.onsuccess = () => resolve()
|
|
deleteRequest.onerror = () => reject(deleteRequest.error)
|
|
}
|
|
|
|
request.onerror = () => reject(request.error)
|
|
})
|
|
}
|
|
|
|
// Push notification handling
|
|
self.addEventListener('push', event => {
|
|
const options = {
|
|
body: event.data?.text() || 'New update available',
|
|
icon: '/favicon.svg',
|
|
badge: '/favicon.svg',
|
|
vibrate: [200, 100, 200],
|
|
data: {
|
|
timestamp: Date.now(),
|
|
url: '/'
|
|
},
|
|
actions: [
|
|
{
|
|
action: 'view',
|
|
title: 'View',
|
|
icon: '/favicon.svg'
|
|
},
|
|
{
|
|
action: 'close',
|
|
title: 'Close'
|
|
}
|
|
]
|
|
}
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification('Miracles in Motion', options)
|
|
)
|
|
})
|
|
|
|
// Notification click handling
|
|
self.addEventListener('notificationclick', event => {
|
|
event.notification.close()
|
|
|
|
if (event.action === 'view') {
|
|
event.waitUntil(
|
|
clients.openWindow(event.notification.data.url)
|
|
)
|
|
}
|
|
}) |