fix(ssr): resolve EventSource SSR violations and implement best practices
- Fix EventSource is not defined error in queue dashboard - Add browser guards for all EventSource usage - Replace static constants (EventSource.OPEN/CLOSED) with numeric values - Fix setInterval SSR violation in LLM health indicator - Replace $effect anti-pattern with onMount in share page - Add comprehensive SvelteKit SSR best practices documentation - Add SSR audit and testing verification All changes follow SvelteKit best practices and are verified against official documentation. Production build succeeds with no SSR errors. Closes: FixEventSourceSSR See: docs/outcomes/FixEventSourceSSR.md
This commit is contained in:
201
src/service-worker.ts
Normal file
201
src/service-worker.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
|
||||
import { NavigationRoute, registerRoute } from 'workbox-routing';
|
||||
|
||||
declare let self: ServiceWorkerGlobalScope;
|
||||
|
||||
// PWA Workbox caching
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
cleanupOutdatedCaches();
|
||||
|
||||
// Handle navigation requests
|
||||
const handler = createHandlerBoundToURL('/');
|
||||
const navigationRoute = new NavigationRoute(handler, {
|
||||
denylist: [/^\/api/]
|
||||
});
|
||||
registerRoute(navigationRoute);
|
||||
|
||||
// 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 = '/';
|
||||
|
||||
if (action === 'view' && data?.itemId) {
|
||||
url = `/?highlight=${data.itemId}`;
|
||||
} else if (action === 'retry' && data?.itemId) {
|
||||
// Navigate to dashboard and trigger retry via postMessage
|
||||
url = `/?highlight=${data.itemId}&action=retry`;
|
||||
} else if (data?.itemId) {
|
||||
url = `/?highlight=${data.itemId}`;
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then((clientsList) => {
|
||||
// Check if there's already a window/tab open
|
||||
for (const client of clientsList) {
|
||||
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
||||
return client.focus().then(() => {
|
||||
// Send message to the client about the action
|
||||
return client.postMessage({
|
||||
type: 'notification-action',
|
||||
action: action,
|
||||
data: data
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If no window is open, open a 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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user