@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
274 lines (248 loc) • 12.3 kB
JavaScript
// Workbox modules are bundled inline by esbuild — no CDN, no importScripts.
import { setCacheNameDetails, clientsClaim } from 'workbox-core';
import { precacheAndRoute, cleanupOutdatedCaches, matchPrecache } from 'workbox-precaching';
import { registerRoute, setCatchHandler } from 'workbox-routing';
import { StaleWhileRevalidate, NetworkFirst, NetworkOnly } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
// ─── Runtime config injected by client-build.js ───────────────────────────────
// OFFLINE_URL and MAINTENANCE_URL are absolute (proxy-prefixed) index.html paths
// resolved at build time from the SSR config's `fallbacks` map.
const payload = self.renderPayload || {};
const CACHE_PREFIX = payload.CACHE_PREFIX || 'engine-core';
const PRE_CACHED_RESOURCES = Array.isArray(payload.PRE_CACHED_RESOURCES) ? payload.PRE_CACHED_RESOURCES : [];
const OFFLINE_URL = payload.OFFLINE_URL || '/offline/index.html';
const MAINTENANCE_URL = payload.MAINTENANCE_URL || '/maintenance/index.html';
// Dedicated cache for fallback pages so they're available even if precacheAndRoute
// fails (e.g. a precache manifest entry returns non-200 during install).
const FALLBACK_CACHE = `${CACHE_PREFIX}-fallbacks`;
/**
* Ultimate inline HTML fallback used ONLY when the network is unavailable and
* the custom SSR pages (NoNetworkConnection / Maintenance views from config)
* have not yet been cached. Once cached on first successful visit, the custom
* pages take over.
* @param {'offline'|'maintenance'} kind
* @returns {Response}
*/
const inlineFallback = (kind) => {
const title = kind === 'offline' ? 'Offline' : 'Maintenance';
const icon = kind === 'offline' ? '🌐' : '🔧';
const msg =
kind === 'offline'
? 'You are offline. Please check your internet connection and try again.'
: 'The server is under maintenance. We\'ll be back shortly.';
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><title>${title}</title><style>body{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f5f5f5;color:#333;padding:1rem;text-align:center}h1{font-size:2rem;margin-bottom:.5rem}p{font-size:1rem;color:#666;max-width:24rem}</style></head><body><h1>${icon} ${title}</h1><p>${msg}</p></body></html>`;
return new Response(html, {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
};
/**
* Returns a Response for the cached fallback page.
* Tries (in order):
* 1. Dedicated FALLBACK_CACHE (primary & secondary URL)
* 2. Workbox precache (primary & secondary URL)
* 3. If online, fetches the custom SSR page from the server (caches it for next time)
* 4. Ultimate inline HTML string (never a browser error page)
*
* @param {'offline'|'maintenance'} kind
* @returns {Promise<Response>}
*/
const getFallback = async (kind) => {
const primary = kind === 'maintenance' ? MAINTENANCE_URL : OFFLINE_URL;
const secondary = kind === 'maintenance' ? OFFLINE_URL : MAINTENANCE_URL;
const cache = await caches.open(FALLBACK_CACHE);
// ── 1. Try cache (own + precache) ──────────────────────────────────────
const cached =
(await cache.match(primary)) ||
(await matchPrecache(primary)) ||
(await cache.match(secondary)) ||
(await matchPrecache(secondary));
if (cached) return cached;
// ── 2. If we appear online, try to fetch the custom SSR page from server ─
// This handles the case where the install-time caching didn't happen yet
// (e.g. first install while server was up, but fallback pages weren't cached).
if (typeof navigator === 'undefined' || navigator.onLine !== false) {
try {
const fetchResp = await fetch(primary, { credentials: 'same-origin' });
if (fetchResp.ok) {
cache.put(primary, fetchResp.clone()).catch(() => { });
return fetchResp;
}
} catch (_) { }
try {
const fetchResp = await fetch(secondary, { credentials: 'same-origin' });
if (fetchResp.ok) {
cache.put(secondary, fetchResp.clone()).catch(() => { });
return fetchResp;
}
} catch (_) { }
}
// ── 3. Ultimate inline fallback ────────────────────────────────────────
return inlineFallback(kind);
};
/**
* Background-caches the offline and maintenance SSR pages for future use.
* Called after each successful navigation so the custom views are available
* the next time the network is unreachable.
*/
const cacheFallbackPages = async () => {
const cache = await caches.open(FALLBACK_CACHE);
for (const url of [OFFLINE_URL, MAINTENANCE_URL]) {
try {
if (!(await cache.match(url))) {
const response = await fetch(url, { credentials: 'same-origin' });
if (response.ok) {
await cache.put(url, response);
}
}
} catch (_) {
// Network unavailable now — will try again on next navigation.
}
}
};
// ─── Core setup ───────────────────────────────────────────────────────────────
setCacheNameDetails({ prefix: CACHE_PREFIX });
clientsClaim();
self.addEventListener('install', (event) => {
self.skipWaiting();
// Note: install-time caching of fallback pages is unreliable in some browsers.
// We rely on the post-navigation eager caching in the navigation handler.
// The inline HTML fallback ensures the user never sees a browser error page
// before the custom SSR pages are cached.
});
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
}
})(),
);
});
self.addEventListener('message', (event) => {
const { status } = event.data || {};
if (status === 'skipWaiting') {
self.skipWaiting();
return;
}
if (status === 'workbox-reset') {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(names.map((name) => caches.delete(name)));
event.ports?.[0]?.postMessage({ status: 'workbox-reset-done', deleted: names.length });
})(),
);
}
});
// ─── Precaching ───────────────────────────────────────────────────────────────
precacheAndRoute(PRE_CACHED_RESOURCES.map((url) => ({ url, revision: null })));
cleanupOutdatedCaches();
// ─── Static assets: StaleWhileRevalidate ─────────────────────────────────────
registerRoute(
({ request, url }) =>
request.method === 'GET' &&
url.origin === self.location.origin &&
['style', 'script', 'font', 'image'].includes(request.destination),
new StaleWhileRevalidate({
cacheName: `${CACHE_PREFIX}-assets`,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 350, maxAgeSeconds: 30 * 24 * 60 * 60 }),
],
}),
);
// ─── API GET: NetworkFirst with short timeout ─────────────────────────────────
registerRoute(
({ request, url }) => request.method === 'GET' && url.pathname.includes('/api/'),
new NetworkFirst({
cacheName: `${CACHE_PREFIX}-api-get`,
networkTimeoutSeconds: 5,
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({ maxEntries: 120, maxAgeSeconds: 5 * 60 }),
],
}),
);
// ─── API mutations: NetworkOnly + background sync replay ─────────────────────
registerRoute(
({ request, url }) => request.method !== 'GET' && url.pathname.includes('/api/'),
new NetworkOnly({
plugins: [new BackgroundSyncPlugin('api-mutation-queue', { maxRetentionTime: 24 * 60 })],
}),
);
// ─── Navigation with offline/maintenance dispatcher ─────────────────────────
//
// CRITICAL: We use direct fetch() instead of NetworkFirst for navigation so
// that when the server is unreachable:
// - NetworkFirst would silently return a STALE cached real page (status 200)
// making it look like the app still works when it doesn't
// - fetch() throws on network failure → catch → fallback page
// - fetch() returns 5xx → maintenance fallback
//
// After each successful fetch, we eagerly cache the custom offline/maintenance
// SSR pages so they're available when the network goes down.
//
// The dispatcher logic:
// 1. If navigator.onLine === false → offline fallback immediately
// 2. Try navigation preload — if it succeeds (< 500) use it, else fall through
// 3. Try direct fetch() — if it succeeds (< 500) cache and return it
// 4. If fetch returned 5xx → maintenance fallback
// 5. If fetch threw (network down) → fallback based on connectivity
// ─────────────────────────────────────────────────────────────────────────────
registerRoute(
({ request }) => request.mode === 'navigate',
async ({ event }) => {
// ── 1. Check browser offline state ────────────────────────────────────
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
return getFallback('offline');
}
// ── 2. Try navigation preload ────────────────────────────────────────
try {
const preload = await event.preloadResponse;
if (preload && preload.status < 500) {
// Fire-and-forget: eagerly cache fallback pages in the background
// after a successful navigation so they're available when offline.
cacheFallbackPages().catch(() => { });
return preload;
}
} catch (_) {
// Preload threw — fall through to direct fetch
}
// ── 3. Try direct fetch to the server ─────────────────────────────────
try {
const response = await fetch(event.request);
if (response.status >= 500) {
return getFallback('maintenance');
}
if (response.ok) {
// Cache the successful response for future navigations
const cache = await caches.open(`${CACHE_PREFIX}-pages`);
cache.put(event.request, response.clone()).catch(() => { });
// Eagerly cache fallback pages in the background
cacheFallbackPages().catch(() => { });
return response;
}
return response;
} catch (_) {
const reallyOffline =
typeof navigator !== 'undefined' && navigator.onLine === false;
return getFallback(reallyOffline ? 'offline' : 'maintenance');
}
},
);
// ─── Global catch handler (non-navigation failures) ──────────────────────────
setCatchHandler(async ({ request }) => {
if (request.mode === 'navigate') {
const reallyOffline =
typeof navigator !== 'undefined' && navigator.onLine === false;
return getFallback(reallyOffline ? 'offline' : 'maintenance');
}
return new Response(JSON.stringify({ status: 'error', message: 'request failed' }), {
status: 503,
headers: { 'Content-Type': 'application/json' },
});
});