mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 14:51:45 +00:00
a83b369cb7
navigator.onLine is unreliable on Android — returns true whenever any network interface is up, regardless of actual reachability. This caused all repo reads to take the API branch and either wait 5 s for the SW NetworkFirst timeout (cache hit) or hang indefinitely (cache miss). - All read repos (list/get) now return cached IndexedDB data instantly and carry a background refresh promise that resolves to fresh data or null on failure. Callers that opted in (loadTrip, loadTrips) apply fresh data silently when it arrives. - tripStore.loadTrip: Promise.all now reads all 7 resources from IndexedDB (instant), fires network refreshes in background, sets isLoading: false immediately, then applies fresh data via a second Promise.all when ready. Tags/categories use upsertTags/upsertCategories. - DashboardPage.loadTrips: same pattern — renders from cache instantly, silently updates trip list on refresh. - axios timeout set to 8 s so requests can never hang indefinitely. - SW networkTimeoutSeconds lowered from 5 to 2 as defence in depth.
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
/// <reference lib="webworker" />
|
|
|
|
import { clientsClaim } from 'workbox-core';
|
|
import {
|
|
precacheAndRoute,
|
|
cleanupOutdatedCaches,
|
|
matchPrecache,
|
|
} 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)
|
|
precacheAndRoute(self.__WB_MANIFEST);
|
|
cleanupOutdatedCaches();
|
|
|
|
// ── Static routes (not user-configurable) ─────────────────────────────────────
|
|
|
|
// Network-first navigations so reverse-proxy auth redirects (Cloudflare Zero
|
|
// Trust, Pangolin, etc.) reach the browser instead of being swallowed by the
|
|
// precached app shell. `redirect: 'manual'` produces an opaqueredirect Response
|
|
// which, per Fetch spec, the browser follows for navigation requests returned
|
|
// from FetchEvent.respondWith. Falls back to precached app shell offline.
|
|
registerRoute(
|
|
new NavigationRoute(
|
|
async ({ request }) => {
|
|
try {
|
|
return await fetch(request, { redirect: 'manual' });
|
|
} catch {
|
|
const cached = await matchPrecache('index.html');
|
|
return cached ?? Response.error();
|
|
}
|
|
},
|
|
{ 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;
|
|
|
|
// Detects when an upstream reverse-proxy auth gate (Cloudflare Zero Trust,
|
|
// Pangolin, etc.) redirects a mid-session API call to an external SSO login
|
|
// page. Uses redirect:'manual' so the response stays as opaqueredirect instead
|
|
// of being silently followed; converts it to a 401 that the Axios interceptor
|
|
// in api/client.ts already handles (→ window.location.href = '/login').
|
|
const authRedirectPlugin = {
|
|
async requestWillFetch({ request }: { request: Request }): Promise<Request> {
|
|
return new Request(request, { redirect: 'manual' });
|
|
},
|
|
async fetchDidSucceed({ response }: { response: Response }): Promise<Response> {
|
|
if (response.type === 'opaqueredirect') {
|
|
return new Response(JSON.stringify({ code: 'AUTH_REQUIRED' }), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
return response;
|
|
},
|
|
};
|
|
|
|
function buildApiStrategy(cfg: SwCacheConfig): NetworkFirst {
|
|
return new NetworkFirst({
|
|
cacheName: 'api-data',
|
|
networkTimeoutSeconds: 2,
|
|
plugins: [
|
|
authRedirectPlugin,
|
|
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<SwCacheConfig>);
|
|
applyConfig(validated);
|
|
|
|
// Acknowledge back to the sending client
|
|
(event.source as WindowClient | null)?.postMessage({ type: 'CACHE_CONFIG_APPLIED' });
|
|
});
|