refactor: Simplify service worker implementation
Remove over-engineering from asset caching service worker: - Remove retry logic, stats tracking, unused message handlers - Inline helper functions used only once - Reduce sw-runtime.js from 314 to 170 lines (46% reduction) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
d34df7c314
commit
f713678328
@@ -1,8 +1,14 @@
|
||||
/* eslint-disable no-console */
|
||||
import NotificationSubscriptions from '../api/notificationSubscription';
|
||||
import auth from '../api/auth';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
/**
|
||||
* Request the service worker to prefetch all assets in the manifest.
|
||||
*/
|
||||
const prefetchAssets = () => {
|
||||
navigator.serviceWorker?.controller?.postMessage({ type: 'PREFETCH_ASSETS' });
|
||||
};
|
||||
|
||||
export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
// Service Worker isn't supported on this browser, disable or hide UI.
|
||||
@@ -29,13 +35,20 @@ export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
// New service worker available, will activate on next page load
|
||||
console.log(
|
||||
'New service worker available, will activate on next page load'
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('New service worker available');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger asset prefetch during idle time
|
||||
if ('requestIdleCallback' in window) {
|
||||
window.requestIdleCallback(() => prefetchAssets(), { timeout: 5000 });
|
||||
} else {
|
||||
// Fallback: prefetch after 3 seconds
|
||||
setTimeout(prefetchAssets, 3000);
|
||||
}
|
||||
|
||||
callback(registration);
|
||||
})
|
||||
.catch(registrationError => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-restricted-globals, no-console */
|
||||
/* eslint-disable no-restricted-globals */
|
||||
/* globals clients */
|
||||
|
||||
// Push notification handler
|
||||
self.addEventListener('push', event => {
|
||||
let notification = event.data && event.data.json();
|
||||
const notification = event.data && event.data.json();
|
||||
|
||||
if (!notification) return;
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(notification.title, {
|
||||
@@ -17,16 +19,17 @@ self.addEventListener('push', event => {
|
||||
|
||||
// Notification click handler
|
||||
self.addEventListener('notificationclick', event => {
|
||||
let notification = event.notification;
|
||||
const notification = event.notification;
|
||||
notification.close();
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window' }).then(windowClients => {
|
||||
let matchingWindowClients = windowClients.filter(
|
||||
const matchingWindowClients = windowClients.filter(
|
||||
client => client.url === notification.data.url
|
||||
);
|
||||
|
||||
if (matchingWindowClients.length) {
|
||||
let firstWindow = matchingWindowClients[0];
|
||||
const firstWindow = matchingWindowClients[0];
|
||||
if (firstWindow && 'focus' in firstWindow) {
|
||||
firstWindow.focus();
|
||||
return;
|
||||
@@ -38,27 +41,3 @@ self.addEventListener('notificationclick', event => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Cache cleanup on activation
|
||||
self.addEventListener('activate', event => {
|
||||
const currentCacheVersion = '__CACHE_VERSION__';
|
||||
const cacheWhitelist = [
|
||||
`js-cache-${currentCacheVersion}`,
|
||||
`css-cache-${currentCacheVersion}`,
|
||||
`font-cache-${currentCacheVersion}`,
|
||||
];
|
||||
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then(cacheNames => {
|
||||
cacheNames.forEach(cacheName => {
|
||||
if (!cacheWhitelist.includes(cacheName)) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
caches.delete(cacheName);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,84 +1,178 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { registerRoute } from 'workbox-routing';
|
||||
import { CacheFirst } from 'workbox-strategies';
|
||||
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
|
||||
import { ExpirationPlugin } from 'workbox-expiration';
|
||||
/* eslint-disable no-restricted-globals, no-use-before-define */
|
||||
|
||||
// Cache version will be injected at build time
|
||||
// Build-time injected values
|
||||
const CACHE_VERSION = '__CACHE_VERSION__';
|
||||
|
||||
// Asset origin will be injected at build time (CDN host or empty string)
|
||||
const ASSET_ORIGIN = '__ASSET_ORIGIN__';
|
||||
const ASSET_MANIFEST = '__ASSET_MANIFEST__';
|
||||
|
||||
// Define cache names with versioning
|
||||
const JS_CACHE = `js-cache-${CACHE_VERSION}`;
|
||||
const CSS_CACHE = `css-cache-${CACHE_VERSION}`;
|
||||
const FONT_CACHE = `font-cache-${CACHE_VERSION}`;
|
||||
const CACHE_NAME = `chatwoot-${CACHE_VERSION}`;
|
||||
|
||||
// Cache JavaScript bundles from /packs/ or CDN
|
||||
registerRoute(
|
||||
({ request, url }) => {
|
||||
const isScript = request.destination === 'script';
|
||||
const isFromPacks = url.pathname.startsWith('/packs/');
|
||||
const isFromCDN = ASSET_ORIGIN && url.origin === ASSET_ORIGIN;
|
||||
// Paths that should never be cached (API, auth, real-time, etc.)
|
||||
const EXCLUDED_PATHS = [
|
||||
'/api/',
|
||||
'/auth/',
|
||||
'/rails/',
|
||||
'/cable',
|
||||
'/sidekiq',
|
||||
'/super_admin',
|
||||
'/swagger',
|
||||
'/webhooks/',
|
||||
'/widget',
|
||||
'/survey/',
|
||||
'/__vite',
|
||||
'/sw.js',
|
||||
];
|
||||
|
||||
return isScript && (isFromPacks || isFromCDN);
|
||||
},
|
||||
new CacheFirst({
|
||||
cacheName: JS_CACHE,
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({
|
||||
statuses: [0, 200], // 0 for opaque responses from CDN, 200 for same-origin
|
||||
}),
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
// =============================================================================
|
||||
// Fetch Handler
|
||||
// =============================================================================
|
||||
|
||||
// Cache CSS files from /packs/ or CDN
|
||||
registerRoute(
|
||||
({ request, url }) => {
|
||||
const isStyle = request.destination === 'style';
|
||||
const isFromPacks = url.pathname.startsWith('/packs/');
|
||||
const isFromCDN = ASSET_ORIGIN && url.origin === ASSET_ORIGIN;
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
return isStyle && (isFromPacks || isFromCDN);
|
||||
},
|
||||
new CacheFirst({
|
||||
cacheName: CSS_CACHE,
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({
|
||||
statuses: [0, 200],
|
||||
}),
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 30,
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
// Only handle GET requests from same origin or CDN
|
||||
if (request.method !== 'GET') return;
|
||||
const isOurRequest =
|
||||
url.origin === self.location.origin ||
|
||||
(ASSET_ORIGIN && url.origin === ASSET_ORIGIN);
|
||||
if (!isOurRequest) return;
|
||||
|
||||
// Cache fonts (from anywhere)
|
||||
registerRoute(
|
||||
({ request, url }) => {
|
||||
const isFont = request.destination === 'font';
|
||||
const hasFontExtension = /\.(woff|woff2|ttf|otf|eot)$/i.test(url.pathname);
|
||||
// Skip excluded paths
|
||||
if (EXCLUDED_PATHS.some(path => url.pathname.startsWith(path))) return;
|
||||
|
||||
return isFont || hasFontExtension;
|
||||
},
|
||||
new CacheFirst({
|
||||
cacheName: FONT_CACHE,
|
||||
plugins: [
|
||||
new CacheableResponsePlugin({
|
||||
statuses: [0, 200],
|
||||
}),
|
||||
new ExpirationPlugin({
|
||||
maxEntries: 30,
|
||||
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
// Assets (JS/CSS/fonts) → cache-first
|
||||
const isAsset =
|
||||
url.pathname.startsWith('/vite-dev/') ||
|
||||
url.pathname.startsWith('/packs/') ||
|
||||
url.origin === ASSET_ORIGIN ||
|
||||
/\.(js|css|woff2?|ttf|otf|eot)$/i.test(url.pathname);
|
||||
|
||||
if (isAsset) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigation to /app/* → network-first (cache HTML shell as fallback)
|
||||
if (request.mode === 'navigate' && url.pathname.startsWith('/app')) {
|
||||
event.respondWith(networkFirstWithShellCache(request));
|
||||
}
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Caching Strategies
|
||||
// =============================================================================
|
||||
|
||||
async function cacheFirst(request) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
// Return cached version if available
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
|
||||
// Otherwise fetch and cache
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
// Network failed - return stale cache if available
|
||||
const stale = await cache.match(request);
|
||||
if (stale) return stale;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function networkFirstWithShellCache(request) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
|
||||
// Cache the HTML shell for offline fallback
|
||||
if (response.ok) {
|
||||
const html = await response.clone().text();
|
||||
if (html.includes('data-sw-cache')) {
|
||||
cache.put(
|
||||
'/app',
|
||||
new Response(html, {
|
||||
headers: response.headers,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
// Network failed - return cached shell
|
||||
const cached = await cache.match('/app');
|
||||
if (cached) return cached;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Background Prefetch
|
||||
// =============================================================================
|
||||
|
||||
async function prefetchAssets() {
|
||||
if (!ASSET_MANIFEST?.length) return;
|
||||
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const baseUrl = ASSET_ORIGIN || `${self.location.origin}/vite-dev/`;
|
||||
|
||||
// Prefetch in batches of 5 (await in loop is intentional for throttling)
|
||||
for (let i = 0; i < ASSET_MANIFEST.length; i += 5) {
|
||||
const batch = ASSET_MANIFEST.slice(i, i + 5);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
batch.map(async asset => {
|
||||
const url = `${baseUrl}${asset.url}`;
|
||||
if (await cache.match(url)) return; // Already cached
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) cache.put(url, response);
|
||||
} catch {
|
||||
// Ignore prefetch failures
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Lifecycle Events
|
||||
// =============================================================================
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting());
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
// Delete old caches
|
||||
caches
|
||||
.keys()
|
||||
.then(names =>
|
||||
Promise.all(
|
||||
names
|
||||
.filter(
|
||||
name => name.startsWith('chatwoot-') && name !== CACHE_NAME
|
||||
)
|
||||
.map(name => caches.delete(name))
|
||||
)
|
||||
),
|
||||
// Take control of all clients
|
||||
self.clients.claim(),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', async event => {
|
||||
if (event.data?.type === 'PREFETCH_ASSETS') {
|
||||
await prefetchAssets();
|
||||
event.ports[0]?.postMessage({ type: 'PREFETCH_COMPLETE' });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user