diff --git a/app/javascript/service-worker/sw-runtime.js b/app/javascript/service-worker/sw-runtime.js index b0648377f..4829b32a7 100644 --- a/app/javascript/service-worker/sw-runtime.js +++ b/app/javascript/service-worker/sw-runtime.js @@ -1,12 +1,13 @@ /* eslint-disable no-restricted-globals, no-use-before-define, no-undef */ // Build-time injected values (replaced by Vite define at build time) -const CACHE_VERSION = __CACHE_VERSION__; const ASSET_ORIGIN = __ASSET_ORIGIN__; const ASSET_PATH = __ASSET_PATH__; const ASSET_MANIFEST = __ASSET_MANIFEST__; -const CACHE_NAME = `chatwoot-${CACHE_VERSION}`; +// Stable cache name - Vite includes content hashes in filenames, +// so unchanged files keep the same URL and don't need refetching +const CACHE_NAME = 'chatwoot-assets-v1'; // Paths that should never be cached (API, auth, real-time, etc.) const EXCLUDED_PATHS = [ @@ -153,24 +154,45 @@ 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)) - ) - ), + // Clean up stale assets not in current manifest + cleanupStaleAssets(), // Take control of all clients self.clients.claim(), ]) ); }); +/** + * Remove cached assets that aren't in the current manifest. + * This keeps unchanged files (same content hash) while removing old versions. + */ +async function cleanupStaleAssets() { + if (!ASSET_MANIFEST?.length) return; + + const cache = await caches.open(CACHE_NAME); + const cachedRequests = await cache.keys(); + const baseUrl = ASSET_ORIGIN || `${self.location.origin}${ASSET_PATH}`; + + // Build set of valid asset URLs from current manifest + const validUrls = new Set( + ASSET_MANIFEST.map(asset => `${baseUrl}${asset.url}`) + ); + + // Delete cached entries that are assets but not in current manifest + const deletions = cachedRequests + .filter(request => { + const url = new URL(request.url); + const isAssetPath = + url.pathname.startsWith('/packs/assets/') || + url.pathname.startsWith('/vite-dev/assets/'); + // Only clean up asset files, keep other cached items (like /app shell) + return isAssetPath && !validUrls.has(request.url); + }) + .map(request => cache.delete(request)); + + await Promise.all(deletions); +} + self.addEventListener('message', async event => { if (event.data?.type === 'PREFETCH_ASSETS') { await prefetchAssets(); diff --git a/scripts/build-service-worker.js b/scripts/build-service-worker.js index 6f91412a7..8d2f5a51b 100644 --- a/scripts/build-service-worker.js +++ b/scripts/build-service-worker.js @@ -2,29 +2,8 @@ const fs = require('fs'); const path = require('path'); -const { execSync } = require('child_process'); const { build } = require('vite'); -/** - * Get the cache version for service worker - * Priority: CHATWOOT_VERSION env var > git commit hash > timestamp - */ -function getCacheVersion() { - if (process.env.CHATWOOT_VERSION) { - return process.env.CHATWOOT_VERSION; - } - - try { - const gitHash = execSync('git rev-parse --short HEAD', { - encoding: 'utf8', - }).trim(); - return `git-${gitHash}`; - } catch (error) { - console.warn('Could not get git hash, using timestamp'); - return `v${Date.now()}`; - } -} - /** * Generate asset manifest from Vite build output * Returns array of { url, revision } for JS/CSS files @@ -75,13 +54,11 @@ function generateAssetManifest() { async function buildServiceWorker() { console.log('🔨 Building service worker...'); - const cacheVersion = getCacheVersion(); const assetOrigin = process.env.ASSET_CDN_HOST || ''; const isProduction = process.env.NODE_ENV === 'production'; // In production assets are in /packs/, in development they're in /vite-dev/ const assetPath = isProduction ? '/packs/' : '/vite-dev/'; - console.log(`📦 Cache version: ${cacheVersion}`); console.log(`🌐 Asset origin: ${assetOrigin || '(local)'}`); console.log(`📁 Asset path: ${assetPath}`); console.log(`🏭 Environment: ${isProduction ? 'production' : 'development'}`); @@ -144,7 +121,6 @@ async function buildServiceWorker() { }, }, define: { - __CACHE_VERSION__: JSON.stringify(cacheVersion), __ASSET_ORIGIN__: JSON.stringify(assetOrigin), __ASSET_PATH__: JSON.stringify(assetPath), __ASSET_MANIFEST__: JSON.stringify(assetManifest), @@ -161,7 +137,7 @@ async function buildServiceWorker() { 'utf8' ); - // Read the push handlers template + // Read the push handlers const pushHandlers = fs.readFileSync( path.resolve( __dirname, @@ -170,21 +146,14 @@ async function buildServiceWorker() { 'utf8' ); - // Replace version placeholder in push handlers - const processedHandlers = pushHandlers.replace( - /__CACHE_VERSION__/g, - cacheVersion - ); - // Combine them const finalServiceWorker = `/* Service Worker for Chatwoot - Generated at build time */ -/* Cache version: ${cacheVersion} */ /* Generated: ${new Date().toISOString()} */ /* Precached assets: ${assetManifest.length} */ ${runtimeBundle} -${processedHandlers} +${pushHandlers} `; // Write to public/sw.js