You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
7.7 KiB
JavaScript
312 lines
7.7 KiB
JavaScript
const CACHE_NAME = 'know-foolery-v1.0.0'
|
|
const STATIC_CACHE_NAME = 'know-foolery-static-v1.0.0'
|
|
const DYNAMIC_CACHE_NAME = 'know-foolery-dynamic-v1.0.0'
|
|
|
|
// Static assets to cache on install
|
|
const STATIC_ASSETS = [
|
|
'/',
|
|
'/static/js/bundle.js',
|
|
'/static/css/main.css',
|
|
'/manifest.json',
|
|
'/icons/icon-192x192.png',
|
|
'/icons/icon-512x512.png'
|
|
]
|
|
|
|
// API endpoints that should be cached
|
|
const API_CACHE_PATTERNS = [
|
|
/\/api\/themes/,
|
|
/\/api\/questions/,
|
|
/\/api\/leaderboard/
|
|
]
|
|
|
|
// Network-first patterns (always try network first)
|
|
const NETWORK_FIRST_PATTERNS = [
|
|
/\/api\/game/,
|
|
/\/api\/player/,
|
|
/\/api\/admin/
|
|
]
|
|
|
|
// Install event - cache static assets
|
|
self.addEventListener('install', (event) => {
|
|
console.log('[SW] Install event')
|
|
|
|
event.waitUntil(
|
|
caches.open(STATIC_CACHE_NAME)
|
|
.then((cache) => {
|
|
console.log('[SW] Caching static assets')
|
|
return cache.addAll(STATIC_ASSETS)
|
|
})
|
|
.then(() => {
|
|
console.log('[SW] Static assets cached successfully')
|
|
return self.skipWaiting()
|
|
})
|
|
.catch((error) => {
|
|
console.error('[SW] Failed to cache static assets:', error)
|
|
})
|
|
)
|
|
})
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', (event) => {
|
|
console.log('[SW] Activate event')
|
|
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames.map((cacheName) => {
|
|
if (cacheName !== STATIC_CACHE_NAME &&
|
|
cacheName !== DYNAMIC_CACHE_NAME &&
|
|
cacheName.startsWith('know-foolery-')) {
|
|
console.log('[SW] Deleting old cache:', cacheName)
|
|
return caches.delete(cacheName)
|
|
}
|
|
})
|
|
)
|
|
})
|
|
.then(() => {
|
|
console.log('[SW] Cache cleanup complete')
|
|
return self.clients.claim()
|
|
})
|
|
)
|
|
})
|
|
|
|
// Fetch event - implement caching strategies
|
|
self.addEventListener('fetch', (event) => {
|
|
const { request } = event
|
|
const url = new URL(request.url)
|
|
|
|
// Skip non-GET requests
|
|
if (request.method !== 'GET') {
|
|
return
|
|
}
|
|
|
|
// Skip chrome-extension requests
|
|
if (url.protocol === 'chrome-extension:') {
|
|
return
|
|
}
|
|
|
|
// Network-first strategy for dynamic content
|
|
if (NETWORK_FIRST_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
|
event.respondWith(networkFirst(request))
|
|
return
|
|
}
|
|
|
|
// Cache-first strategy for API data that can be stale
|
|
if (API_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
|
|
event.respondWith(cacheFirst(request))
|
|
return
|
|
}
|
|
|
|
// Stale-while-revalidate for static assets
|
|
if (url.origin === location.origin) {
|
|
event.respondWith(staleWhileRevalidate(request))
|
|
return
|
|
}
|
|
|
|
// Network-only for external resources
|
|
event.respondWith(fetch(request))
|
|
})
|
|
|
|
// Network-first strategy
|
|
async function networkFirst(request) {
|
|
try {
|
|
const networkResponse = await fetch(request)
|
|
|
|
if (networkResponse.ok) {
|
|
const cache = await caches.open(DYNAMIC_CACHE_NAME)
|
|
cache.put(request, networkResponse.clone())
|
|
}
|
|
|
|
return networkResponse
|
|
} catch (error) {
|
|
console.log('[SW] Network failed, trying cache:', request.url)
|
|
const cachedResponse = await caches.match(request)
|
|
|
|
if (cachedResponse) {
|
|
return cachedResponse
|
|
}
|
|
|
|
// Return offline fallback for navigation requests
|
|
if (request.mode === 'navigate') {
|
|
return caches.match('/offline.html') || new Response('Offline', { status: 503 })
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Cache-first strategy
|
|
async function cacheFirst(request) {
|
|
const cachedResponse = await caches.match(request)
|
|
|
|
if (cachedResponse) {
|
|
// Update cache in background
|
|
fetch(request).then(response => {
|
|
if (response.ok) {
|
|
caches.open(DYNAMIC_CACHE_NAME).then(cache => {
|
|
cache.put(request, response)
|
|
})
|
|
}
|
|
}).catch(() => {
|
|
// Ignore network errors when updating cache
|
|
})
|
|
|
|
return cachedResponse
|
|
}
|
|
|
|
try {
|
|
const networkResponse = await fetch(request)
|
|
|
|
if (networkResponse.ok) {
|
|
const cache = await caches.open(DYNAMIC_CACHE_NAME)
|
|
cache.put(request, networkResponse.clone())
|
|
}
|
|
|
|
return networkResponse
|
|
} catch (error) {
|
|
console.error('[SW] Cache-first failed:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Stale-while-revalidate strategy
|
|
async function staleWhileRevalidate(request) {
|
|
const cache = await caches.open(STATIC_CACHE_NAME)
|
|
const cachedResponse = await cache.match(request)
|
|
|
|
const networkResponsePromise = fetch(request).then(response => {
|
|
if (response.ok) {
|
|
cache.put(request, response.clone())
|
|
}
|
|
return response
|
|
}).catch(() => {
|
|
// Ignore network errors
|
|
return null
|
|
})
|
|
|
|
return cachedResponse || await networkResponsePromise
|
|
}
|
|
|
|
// Background sync for offline actions
|
|
self.addEventListener('sync', (event) => {
|
|
console.log('[SW] Background sync event:', event.tag)
|
|
|
|
if (event.tag === 'game-actions') {
|
|
event.waitUntil(syncGameActions())
|
|
}
|
|
})
|
|
|
|
async function syncGameActions() {
|
|
try {
|
|
// Get pending actions from IndexedDB
|
|
const pendingActions = await getPendingActions()
|
|
|
|
for (const action of pendingActions) {
|
|
try {
|
|
const response = await fetch(action.url, {
|
|
method: action.method,
|
|
headers: action.headers,
|
|
body: action.body
|
|
})
|
|
|
|
if (response.ok) {
|
|
await removePendingAction(action.id)
|
|
console.log('[SW] Synced action:', action.id)
|
|
}
|
|
} catch (error) {
|
|
console.error('[SW] Failed to sync action:', action.id, error)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[SW] Background sync failed:', error)
|
|
}
|
|
}
|
|
|
|
// Push notifications
|
|
self.addEventListener('push', (event) => {
|
|
console.log('[SW] Push event received:', event)
|
|
|
|
if (!event.data) {
|
|
return
|
|
}
|
|
|
|
const data = event.data.json()
|
|
const options = {
|
|
body: data.body || 'New update available!',
|
|
icon: '/icons/icon-192x192.png',
|
|
badge: '/icons/icon-96x96.png',
|
|
data: data.data || {},
|
|
actions: [
|
|
{
|
|
action: 'view',
|
|
title: 'View',
|
|
icon: '/icons/icon-96x96.png'
|
|
},
|
|
{
|
|
action: 'dismiss',
|
|
title: 'Dismiss'
|
|
}
|
|
]
|
|
}
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(data.title || 'Know Foolery', options)
|
|
)
|
|
})
|
|
|
|
// Notification click handler
|
|
self.addEventListener('notificationclick', (event) => {
|
|
console.log('[SW] Notification click:', event.action)
|
|
|
|
event.notification.close()
|
|
|
|
if (event.action === 'view' || !event.action) {
|
|
const urlToOpen = event.notification.data.url || '/'
|
|
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window' }).then(clientsArr => {
|
|
const hadWindowToFocus = clientsArr.some(windowClient => {
|
|
if (windowClient.url === urlToOpen) {
|
|
windowClient.focus()
|
|
return true
|
|
}
|
|
})
|
|
|
|
if (!hadWindowToFocus) {
|
|
clients.openWindow(urlToOpen)
|
|
}
|
|
})
|
|
)
|
|
}
|
|
})
|
|
|
|
// Helper functions for IndexedDB operations
|
|
async function getPendingActions() {
|
|
return new Promise((resolve) => {
|
|
// Mock implementation - in real app would use IndexedDB
|
|
resolve([])
|
|
})
|
|
}
|
|
|
|
async function removePendingAction(actionId) {
|
|
return new Promise((resolve) => {
|
|
// Mock implementation - in real app would use IndexedDB
|
|
resolve()
|
|
})
|
|
}
|
|
|
|
// Message handler for communication with main thread
|
|
self.addEventListener('message', (event) => {
|
|
console.log('[SW] Message received:', event.data)
|
|
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting()
|
|
}
|
|
|
|
if (event.data && event.data.type === 'GET_VERSION') {
|
|
event.ports[0].postMessage({ version: CACHE_NAME })
|
|
}
|
|
})
|
|
|
|
console.log('[SW] Service Worker loaded successfully') |