305 lines
7.6 KiB
TypeScript
305 lines
7.6 KiB
TypeScript
/// <reference types="@sveltejs/kit" />
|
|
/// <reference no-default-lib="true"/>
|
|
/// <reference lib="esnext" />
|
|
/// <reference lib="webworker" />
|
|
|
|
import { build, files, version } from '$service-worker';
|
|
|
|
declare let self: ServiceWorkerGlobalScope;
|
|
|
|
// Create a unique cache name for this deployment
|
|
const CACHE = `cache-${version}`;
|
|
const ASSETS = [
|
|
...build, // the app itself
|
|
...files // everything in `static`
|
|
];
|
|
|
|
// Global error handlers (preserve existing)
|
|
self.addEventListener('error', (event) => {
|
|
console.error('[SW] Global error:', event.error);
|
|
console.error('[SW] Error details:', {
|
|
message: event.message,
|
|
filename: event.filename,
|
|
lineno: event.lineno,
|
|
colno: event.colno,
|
|
error: event.error
|
|
});
|
|
});
|
|
|
|
self.addEventListener('unhandledrejection', (event) => {
|
|
console.error('[SW] Unhandled promise rejection:', event.reason);
|
|
event.preventDefault(); // Prevent default browser behavior
|
|
});
|
|
|
|
console.log('[SW] Service worker script loading...');
|
|
|
|
// Install event - cache all assets
|
|
self.addEventListener('install', (event) => {
|
|
console.log('[SW] Installing service worker...');
|
|
|
|
async function addFilesToCache() {
|
|
const cache = await caches.open(CACHE);
|
|
await cache.addAll(ASSETS);
|
|
console.log(`[SW] Cached ${ASSETS.length} assets`);
|
|
}
|
|
|
|
event.waitUntil(addFilesToCache());
|
|
});
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', (event) => {
|
|
console.log('[SW] Activating service worker...');
|
|
|
|
async function deleteOldCaches() {
|
|
for (const key of await caches.keys()) {
|
|
if (key !== CACHE) {
|
|
console.log('[SW] Deleting old cache:', key);
|
|
await caches.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
event.waitUntil(deleteOldCaches());
|
|
});
|
|
|
|
// Fetch event - serve from cache with network fallback
|
|
self.addEventListener('fetch', (event) => {
|
|
// ignore POST requests etc
|
|
if (event.request.method !== 'GET') return;
|
|
|
|
async function respond() {
|
|
const url = new URL(event.request.url);
|
|
const cache = await caches.open(CACHE);
|
|
|
|
// `build`/`files` can always be served from the cache
|
|
if (ASSETS.includes(url.pathname)) {
|
|
const response = await cache.match(url.pathname);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
}
|
|
|
|
// for everything else, try the network first, but
|
|
// fall back to the cache if we're offline
|
|
try {
|
|
const response = await fetch(event.request);
|
|
|
|
// if we're offline, fetch can return a value that is not a Response
|
|
// instead of throwing - and we can't pass this non-Response to respondWith
|
|
if (!(response instanceof Response)) {
|
|
throw new Error('invalid response from fetch');
|
|
}
|
|
|
|
if (response.status === 200) {
|
|
cache.put(event.request, response.clone());
|
|
}
|
|
return response;
|
|
} catch (err) {
|
|
const response = await cache.match(event.request);
|
|
if (response) {
|
|
return response;
|
|
}
|
|
|
|
// if there's no cache, then just error out
|
|
// as there is nothing we can do to respond to this request
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
event.respondWith(respond());
|
|
});
|
|
|
|
// Push notification handling
|
|
self.addEventListener('push', (event) => {
|
|
console.log('[SW] Push event received:', event);
|
|
|
|
if (!event.data) {
|
|
console.log('[SW] Push event but no data');
|
|
return;
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = event.data.json();
|
|
} catch (e) {
|
|
console.error('[SW] Failed to parse push data:', e);
|
|
return;
|
|
}
|
|
|
|
console.log('[SW] Push data:', data);
|
|
|
|
const options: NotificationOptions = {
|
|
body: data.body || 'Recipe processing update',
|
|
icon: '/favicon.png',
|
|
badge: '/favicon.png',
|
|
data: data,
|
|
requireInteraction: data.requireInteraction || false,
|
|
silent: false,
|
|
tag: data.tag || 'recipe-update',
|
|
timestamp: Date.now(),
|
|
actions: []
|
|
};
|
|
|
|
// Add actions based on notification type
|
|
if (data.type === 'success' && data.itemId) {
|
|
options.actions = [
|
|
{
|
|
action: 'view',
|
|
title: 'View Recipe',
|
|
icon: '/favicon.png'
|
|
},
|
|
{
|
|
action: 'dismiss',
|
|
title: 'Dismiss'
|
|
}
|
|
];
|
|
} else if (data.type === 'error' && data.itemId) {
|
|
options.actions = [
|
|
{
|
|
action: 'retry',
|
|
title: 'Retry',
|
|
icon: '/favicon.png'
|
|
},
|
|
{
|
|
action: 'view',
|
|
title: 'View Details'
|
|
}
|
|
];
|
|
}
|
|
|
|
const title = data.title || getNotificationTitle(data.type, data);
|
|
|
|
event.waitUntil(self.registration.showNotification(title, options));
|
|
});
|
|
|
|
// Handle notification clicks
|
|
self.addEventListener('notificationclick', (event) => {
|
|
console.log('[SW] Notification click received:', event);
|
|
|
|
event.notification.close();
|
|
|
|
const data = event.notification.data;
|
|
const action = event.action;
|
|
|
|
let url = '/';
|
|
|
|
// Handle 'view' action - redirect to Tandoor if available
|
|
if (action === 'view') {
|
|
if (data?.tandoorUrl) {
|
|
// Success notification with Tandoor URL - redirect to recipe
|
|
url = data.tandoorUrl;
|
|
} else if (data?.itemId) {
|
|
// Fallback to dashboard highlight
|
|
url = `/?highlight=${data.itemId}`;
|
|
}
|
|
} else if (action === 'retry' && data?.itemId) {
|
|
// Navigate to dashboard and trigger retry
|
|
url = `/?highlight=${data.itemId}&action=retry`;
|
|
} else if (data?.itemId) {
|
|
// Default: highlight item in dashboard
|
|
url = `/?highlight=${data.itemId}`;
|
|
}
|
|
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientsList) => {
|
|
// For external URLs (Tandoor), always open new window
|
|
if (url.startsWith('http') && !url.includes(self.location.origin)) {
|
|
if (clients.openWindow) {
|
|
return clients.openWindow(url);
|
|
}
|
|
}
|
|
|
|
// For internal URLs, check for existing window
|
|
for (const client of clientsList) {
|
|
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
|
return client.focus().then(() => {
|
|
return client.postMessage({
|
|
type: 'notification-action',
|
|
action: action,
|
|
data: data
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// No window open, open new one
|
|
if (clients.openWindow) {
|
|
return clients.openWindow(url);
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// Handle notification close
|
|
self.addEventListener('notificationclose', (event) => {
|
|
console.log('[SW] Notification closed:', event);
|
|
|
|
// Track notification dismissals if needed
|
|
const data = event.notification.data;
|
|
if (data?.analytics) {
|
|
// Could send analytics event here
|
|
console.log('[SW] Notification dismissed:', data);
|
|
}
|
|
});
|
|
|
|
// Background sync for retry operations
|
|
self.addEventListener('sync', (event) => {
|
|
console.log('[SW] Background sync:', event.tag);
|
|
|
|
if (event.tag === 'retry-queue-item') {
|
|
event.waitUntil(handleRetrySync());
|
|
}
|
|
});
|
|
|
|
// Helper functions
|
|
function getNotificationTitle(type: string, data: any): string {
|
|
switch (type) {
|
|
case 'success':
|
|
return data.recipeName
|
|
? `✅ Recipe Ready: ${data.recipeName}`
|
|
: '✅ Recipe extraction complete';
|
|
case 'error':
|
|
return '❌ Recipe extraction failed';
|
|
case 'progress':
|
|
return `🔄 Processing recipe...`;
|
|
default:
|
|
return '📱 InstaRecipe Update';
|
|
}
|
|
}
|
|
|
|
async function handleRetrySync() {
|
|
try {
|
|
// Get retry items from IndexedDB or localStorage if needed
|
|
console.log('[SW] Handling retry sync');
|
|
|
|
// This could implement background retry logic
|
|
// For now, we'll let the main app handle retries
|
|
return Promise.resolve();
|
|
} catch (error) {
|
|
console.error('[SW] Retry sync failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Message handling for communication with main app
|
|
self.addEventListener('message', (event) => {
|
|
console.log('[SW] Message received:', event.data);
|
|
|
|
const { type, data } = event.data;
|
|
|
|
switch (type) {
|
|
case 'SKIP_WAITING':
|
|
self.skipWaiting();
|
|
break;
|
|
case 'GET_VERSION':
|
|
event.ports[0].postMessage({ version: '1.0.0' });
|
|
break;
|
|
case 'QUEUE_RETRY':
|
|
// Queue a background sync for retry
|
|
self.registration.sync.register('retry-queue-item');
|
|
break;
|
|
default:
|
|
console.log('[SW] Unknown message type:', type);
|
|
}
|
|
});
|