///
import { clientsClaim } from 'workbox-core';
import {
precacheAndRoute,
cleanupOutdatedCaches,
createHandlerBoundToURL,
} from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import {
DEFAULT_SW_CONFIG,
readSwConfigFromIDB,
validateSwConfig,
type SwCacheConfig,
} from './sync/swConfig';
declare const self: ServiceWorkerGlobalScope;
self.skipWaiting();
clientsClaim();
// Inject precache manifest (replaced by vite-plugin-pwa at build time)
// @ts-expect-error __WB_MANIFEST is injected at build time
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
// ── Static routes (not user-configurable) ─────────────────────────────────────
registerRoute(
new NavigationRoute(createHandlerBoundToURL('index.html'), {
denylist: [/^\/api/, /^\/uploads/, /^\/mcp/],
}),
);
registerRoute(
/^https:\/\/unpkg\.com\/.*/i,
new CacheFirst({
cacheName: 'cdn-libs',
plugins: [
new ExpirationPlugin({ maxEntries: 30, maxAgeSeconds: 365 * 24 * 60 * 60 }),
new CacheableResponsePlugin({ statuses: [0, 200] }),
],
}),
'GET',
);
registerRoute(
/\/uploads\/(?:covers|avatars)\/.*/i,
new CacheFirst({
cacheName: 'user-uploads',
plugins: [
new ExpirationPlugin({ maxEntries: 300, maxAgeSeconds: 7 * 24 * 60 * 60 }),
new CacheableResponsePlugin({ statuses: [200] }),
],
}),
'GET',
);
// ── Configurable routes ────────────────────────────────────────────────────────
// Routes are registered once. Strategy instances are replaced on config change
// so the stable handler wrapper always delegates to the current instance.
const DAY = 24 * 60 * 60;
function buildApiStrategy(cfg: SwCacheConfig): NetworkFirst {
return new NetworkFirst({
cacheName: 'api-data',
networkTimeoutSeconds: 5,
plugins: [
new ExpirationPlugin({
maxEntries: cfg.apiMaxEntries,
maxAgeSeconds: cfg.apiTtlDays * DAY,
}),
new CacheableResponsePlugin({ statuses: [200] }),
],
});
}
function buildTilesStrategy(cfg: SwCacheConfig): CacheFirst {
return new CacheFirst({
cacheName: 'map-tiles',
plugins: [
new ExpirationPlugin({
maxEntries: cfg.tilesMaxEntries,
maxAgeSeconds: cfg.tilesTtlDays * DAY,
}),
new CacheableResponsePlugin({ statuses: [0, 200] }),
],
});
}
let apiStrategy = buildApiStrategy(DEFAULT_SW_CONFIG);
let cartoStrategy = buildTilesStrategy(DEFAULT_SW_CONFIG);
let osmStrategy = buildTilesStrategy(DEFAULT_SW_CONFIG);
function applyConfig(cfg: SwCacheConfig): void {
apiStrategy = buildApiStrategy(cfg);
cartoStrategy = buildTilesStrategy(cfg);
osmStrategy = buildTilesStrategy(cfg);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
registerRoute(/\/api\/(?!auth|admin|backup|settings).*/i, { handle: (o: any) => apiStrategy.handle(o) }, 'GET');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
registerRoute(/^https:\/\/[a-d]\.basemaps\.cartocdn\.com\/.*/i, { handle: (o: any) => cartoStrategy.handle(o) }, 'GET');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
registerRoute(/^https:\/\/[a-c]\.tile\.openstreetmap\.org\/.*/i, { handle: (o: any) => osmStrategy.handle(o) }, 'GET');
// Load persisted config asynchronously; replaces defaults if user has saved settings
readSwConfigFromIDB()
.then(cfg => { if (cfg) applyConfig(cfg); })
.catch(() => {});
// ── Message handler ────────────────────────────────────────────────────────────
self.addEventListener('message', (event: ExtendableMessageEvent) => {
const data = event.data as { type?: string; config?: unknown };
if (data?.type !== 'UPDATE_CACHE_CONFIG' || !data.config) return;
const validated = validateSwConfig(data.config as Partial);
applyConfig(validated);
// Acknowledge back to the sending client
(event.source as WindowClient | null)?.postMessage({ type: 'CACHE_CONFIG_APPLIED' });
});