mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
perf(app): stop the service worker caching static assets (#1837)
This commit is contained in:
@@ -42,9 +42,8 @@ const ensureServiceWorkerReady: Promise<void> = Promise.resolve();
|
||||
const SERVICE_WORKER_VERSION = typeof __FLUXER_SW_VERSION__ === 'string' ? __FLUXER_SW_VERSION__ : 'dev';
|
||||
const PRECACHE_MANIFEST = typeof __FLUXER_PRECACHE_MANIFEST__ === 'undefined' ? [] : __FLUXER_PRECACHE_MANIFEST__;
|
||||
const PRECACHE_CACHE = `${WORKER_CACHE_PREFIX}-precache-${SERVICE_WORKER_VERSION}`;
|
||||
const ASSET_CACHE = `${WORKER_CACHE_PREFIX}-assets-${SERVICE_WORKER_VERSION}`;
|
||||
const NAVIGATION_CACHE = `${WORKER_CACHE_PREFIX}-navigation-${SERVICE_WORKER_VERSION}`;
|
||||
const EXPECTED_CACHES = new Set([PRECACHE_CACHE, ASSET_CACHE, NAVIGATION_CACHE]);
|
||||
const EXPECTED_CACHES = new Set([PRECACHE_CACHE, NAVIGATION_CACHE]);
|
||||
const NAVIGATION_NETWORK_TIMEOUT_MS = 650;
|
||||
const serviceWorkerCaches = self.caches;
|
||||
const isNativeDesktopUserAgent = (userAgent: string): boolean => /\bElectron\/\d+(?:\.\d+)*/.test(userAgent);
|
||||
@@ -80,20 +79,7 @@ const appShellRuntime: AppShellRuntime = {
|
||||
void log('warn', 'app shell cache put failed', {error: describeError(error)});
|
||||
},
|
||||
};
|
||||
const pruneCacheEntries = async (cache: Cache, maxEntries: number): Promise<void> => {
|
||||
const keys = await cache.keys();
|
||||
const overflow = keys.length - maxEntries;
|
||||
if (overflow <= 0) {
|
||||
return;
|
||||
}
|
||||
await Promise.all(keys.slice(0, overflow).map((request) => cache.delete(request)));
|
||||
};
|
||||
const cacheRequest = async (
|
||||
cacheName: string,
|
||||
request: Request | string,
|
||||
response: Response,
|
||||
maxEntries?: number,
|
||||
): Promise<void> => {
|
||||
const cacheRequest = async (cacheName: string, request: Request | string, response: Response): Promise<void> => {
|
||||
if (!isCacheableResponse(response)) {
|
||||
return;
|
||||
}
|
||||
@@ -103,9 +89,6 @@ const cacheRequest = async (
|
||||
}
|
||||
const cache = await serviceWorkerCaches.open(cacheName);
|
||||
await cache.put(request, response.clone());
|
||||
if (maxEntries != null) {
|
||||
await pruneCacheEntries(cache, maxEntries);
|
||||
}
|
||||
} catch (error) {
|
||||
await log('warn', 'cache put failed', {cacheName, error: describeError(error)});
|
||||
}
|
||||
@@ -124,15 +107,6 @@ const cleanupOldCaches = async (): Promise<void> => {
|
||||
}),
|
||||
);
|
||||
};
|
||||
const fetchCacheFirst = async (request: Request): Promise<Response> => {
|
||||
const cached = await serviceWorkerCaches?.match(request);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const response = await fetch(request);
|
||||
await cacheRequest(ASSET_CACHE, request, response);
|
||||
return response;
|
||||
};
|
||||
const fetchNetworkFirst = async (request: Request): Promise<Response> => {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
@@ -177,10 +151,6 @@ self.addEventListener('fetch', (event: FetchEvent) => {
|
||||
event.respondWith(fetchAppShellNavigation(appShellRuntime, request));
|
||||
return;
|
||||
}
|
||||
if (route === 'static-asset') {
|
||||
event.respondWith(fetchCacheFirst(request));
|
||||
return;
|
||||
}
|
||||
if (route === 'metadata') {
|
||||
event.respondWith(fetchNetworkFirst(request));
|
||||
}
|
||||
|
||||
@@ -4,14 +4,21 @@ import {shouldDeleteWorkerCache} from '@app/features/platform/service_worker/Wor
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
describe('WorkerCacheCleanup', () => {
|
||||
it('deletes legacy expression asset caches even though old asset caches are preserved', () => {
|
||||
const expectedCaches = new Set(['fluxer-precache-current', 'fluxer-assets-current', 'fluxer-navigation-current']);
|
||||
const expectedCaches = new Set(['fluxer-precache-current', 'fluxer-navigation-current']);
|
||||
|
||||
it('reclaims every cache the current worker no longer writes to', () => {
|
||||
expect(shouldDeleteWorkerCache('fluxer-assets-current', expectedCaches)).toBe(true);
|
||||
expect(shouldDeleteWorkerCache('fluxer-assets-previous', expectedCaches)).toBe(true);
|
||||
expect(shouldDeleteWorkerCache('fluxer-expression-assets', expectedCaches)).toBe(true);
|
||||
expect(shouldDeleteWorkerCache('fluxer-expression-assets-2026.604', expectedCaches)).toBe(true);
|
||||
expect(shouldDeleteWorkerCache('fluxer-assets-previous', expectedCaches)).toBe(false);
|
||||
expect(shouldDeleteWorkerCache('fluxer-precache-previous', expectedCaches)).toBe(true);
|
||||
expect(shouldDeleteWorkerCache('third-party-cache', expectedCaches)).toBe(false);
|
||||
expect(shouldDeleteWorkerCache('fluxer-navigation-previous', expectedCaches)).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the current caches and anything the worker does not own', () => {
|
||||
expect(shouldDeleteWorkerCache('fluxer-precache-current', expectedCaches)).toBe(false);
|
||||
expect(shouldDeleteWorkerCache('fluxer-navigation-current', expectedCaches)).toBe(false);
|
||||
expect(shouldDeleteWorkerCache('third-party-cache', expectedCaches)).toBe(false);
|
||||
expect(shouldDeleteWorkerCache('fluxer', expectedCaches)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export const WORKER_CACHE_PREFIX = 'fluxer';
|
||||
const LEGACY_EXPRESSION_ASSET_CACHE_PREFIX = `${WORKER_CACHE_PREFIX}-expression-assets`;
|
||||
|
||||
export function isLegacyExpressionAssetCacheName(cacheName: string): boolean {
|
||||
return (
|
||||
cacheName === LEGACY_EXPRESSION_ASSET_CACHE_PREFIX ||
|
||||
cacheName.startsWith(`${LEGACY_EXPRESSION_ASSET_CACHE_PREFIX}-`)
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldDeleteWorkerCache(cacheName: string, expectedCaches: ReadonlySet<string>): boolean {
|
||||
if (isLegacyExpressionAssetCacheName(cacheName)) {
|
||||
return true;
|
||||
}
|
||||
if (!cacheName.startsWith(`${WORKER_CACHE_PREFIX}-`) || expectedCaches.has(cacheName)) {
|
||||
return false;
|
||||
}
|
||||
if (cacheName.startsWith(`${WORKER_CACHE_PREFIX}-assets-`)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return cacheName.startsWith(`${WORKER_CACHE_PREFIX}-`) && !expectedCaches.has(cacheName);
|
||||
}
|
||||
|
||||
@@ -26,11 +26,34 @@ describe('WorkerFetchRouting', () => {
|
||||
expect(
|
||||
getWorkerFetchRoute(request(`${WORKER_ORIGIN}/admin`, {headers: {accept: 'text/html'}}), WORKER_ORIGIN),
|
||||
).toBe('ignore');
|
||||
expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/assets/app.js`), WORKER_ORIGIN)).toBe('static-asset');
|
||||
expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/manifest.json`), WORKER_ORIGIN)).toBe('metadata');
|
||||
expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/version.json`), WORKER_ORIGIN)).toBe('metadata');
|
||||
});
|
||||
|
||||
it('leaves every build output under /assets/ to the immutable HTTP cache', () => {
|
||||
const buildOutputs = [
|
||||
'/assets/16bf14551996ba83.js',
|
||||
'/assets/2d715e4730758083.worker.js',
|
||||
'/assets/38cf6fb33e42e52d.css',
|
||||
'/assets/a79f1c3119cd700d.woff2',
|
||||
'/assets/0f50c815cd5e74ce.wasm',
|
||||
'/assets/ff9b1f835d5aa8cb.png',
|
||||
'/assets/22e569554c3be0e5.webm',
|
||||
'/assets/63a33cf1048e51c7.mp3',
|
||||
'/assets/fonts-NOTICE.txt',
|
||||
'/assets/chunk',
|
||||
'/assets/fonts/inter',
|
||||
];
|
||||
for (const pathname of buildOutputs) {
|
||||
expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}${pathname}`), WORKER_ORIGIN)).toBe('ignore');
|
||||
}
|
||||
});
|
||||
|
||||
it('only recognises metadata served from the worker origin', () => {
|
||||
expect(getWorkerFetchRoute(request('https://cdn.fluxer.test/manifest.json'), WORKER_ORIGIN)).toBe('ignore');
|
||||
expect(getWorkerFetchRoute(request('https://cdn.fluxer.test/version.json'), WORKER_ORIGIN)).toBe('ignore');
|
||||
});
|
||||
|
||||
it('ignores non-GET requests', () => {
|
||||
expect(getWorkerFetchRoute(request(`${WORKER_ORIGIN}/version.json`, {method: 'POST'}), WORKER_ORIGIN)).toBe(
|
||||
'ignore',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import {isAppNavigationPath} from '@app/features/platform/service_worker/WorkerNavigation';
|
||||
|
||||
export type WorkerFetchRoute = 'ignore' | 'metadata' | 'navigation' | 'static-asset';
|
||||
export type WorkerFetchRoute = 'ignore' | 'metadata' | 'navigation';
|
||||
|
||||
export function isNavigationRequest(request: Request): boolean {
|
||||
if (request.mode === 'navigate') return true;
|
||||
@@ -21,9 +21,6 @@ export function getWorkerFetchRoute(request: Request, workerOrigin: string): Wor
|
||||
if (isNavigationRequest(request)) {
|
||||
return isAppNavigationPath(url.pathname) ? 'navigation' : 'ignore';
|
||||
}
|
||||
if (url.pathname.startsWith('/assets/')) {
|
||||
return 'static-asset';
|
||||
}
|
||||
if (url.pathname === '/manifest.json' || url.pathname === '/version.json') {
|
||||
return 'metadata';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user