refactor: Use stable cache name with incremental asset updates

Instead of invalidating the entire cache on every deployment (via git hash
in cache name), use a stable cache name and leverage Vite's content hashes
in filenames.

Changes:
- Use fixed cache name 'chatwoot-assets-v1' instead of 'chatwoot-{git-hash}'
- Add cleanupStaleAssets() to remove old assets not in current manifest
- Unchanged files (same content hash) stay cached across deployments
- Only new/changed files are fetched
- Remove unused getCacheVersion() and CACHE_VERSION references

This significantly reduces bandwidth on deployments since most assets
don't change between releases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Pranav
2026-01-10 16:28:42 -08:00
co-authored by Claude Opus 4.5
parent 0d21a711e7
commit 38232a73dc
2 changed files with 38 additions and 47 deletions
+36 -14
View File
@@ -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();
+2 -33
View File
@@ -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