How GeoLibre PWA Service Worker Implements Offline Caching for Pyodide and PGlite
The GeoLibre PWA service worker precaches the app shell and runtime-caches heavy engine binaries from jsDelivr using a CacheFirst strategy with 30-day expiration, enabling full offline use of Pyodide and PGlite after the first online load.
The GeoLibre desktop application is a Progressive Web App (PWA) designed to function without network connectivity. Its offline capability relies on service worker caching strategies configured in apps/geolibre-desktop/vite.config.ts that specifically handle cross-origin WebAssembly binaries. This article explains how Pyodide and PGlite engines are cached for offline use.
Service Worker Architecture and Caching Strategy
GeoLibre uses the Vite PWA plugin to generate its service worker. The configuration separates caching into two distinct approaches: precaching for the app shell and runtime caching for heavy, lazily-loaded binaries.
According to the inline documentation in the Vite configuration:
"The service worker precaches the app shell … and runtime-caches the heavy, lazily-fetched same-origin binaries (DuckDB-WASM + spatial extension, MapLibre feature plugins) … PGlite/PostGIS and the Pyodide runtime are fetched cross-origin …"
Source: apps/geolibre-desktop/vite.config.ts#L58-L66
This dual strategy ensures that the core application loads instantly from the cache while engine binaries are fetched on demand and stored for subsequent offline sessions.
Runtime Caching Rules for CDN-Loaded Engines
The critical configuration for Pyodide and PGlite caching resides in the runtimeCaching array. A dedicated rule intercepts requests to cdn.jsdelivr.net that match specific path patterns for the Python and PostgreSQL engines.
CacheFirst Handler Configuration
{
urlPattern: ({ url }) =>
url.hostname === "cdn.jsdelivr.net" &&
(url.pathname.startsWith("/pyodide/") ||
url.pathname.startsWith("/npm/@electric-sql/") ||
url.pathname.startsWith("/npm/@cereusdb/") ||
url.pathname.startsWith("/npm/gdal3.js")),
handler: "CacheFirst",
options: {
cacheName: "geolibre-cdn-engines",
expiration: {
maxEntries: 400,
maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
},
cacheableResponse: { statuses: [0, 200] },
},
}
Source: apps/geolibre-desktop/vite.config.ts#L94-L114
Key implementation details:
CacheFirsthandler: Returns cached responses immediately when available, falling back to network only on cache miss- Versioned URLs: jsDelivr paths include exact versions (e.g.,
/pyodide/v0.26.1/full/pyodide.wasm), ensuring cache coherence across deployments - 400-entry limit with 30-day retention: Prevents storage exhaustion while allowing substantial engine libraries
- Status code 0 handling: Accommodates opaque responses from cross-origin requests
Detecting Active Service Worker State
Before relying on cached engines, the application verifies that a service worker controls the page. The hasActiveServiceWorker() utility in offline-tiles.ts performs this check:
export function hasActiveServiceWorker(): boolean {
return (
typeof navigator !== "undefined" &&
"serviceWorker" in navigator &&
!!navigator.serviceWorker.controller
);
}
Source: apps/geolibre-desktop/src/lib/offline-tiles.ts#L16-L22
This boolean guard prevents the application from attempting offline operations when caching infrastructure is unavailable.
Complete Offline Workflow
1. Service Worker Registration
The application manually registers the service worker in main.tsx:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/service-worker.js");
}
2. First-Use Engine Fetching
When a user opens the Python console or executes a PostGIS query, the browser requests engine files from jsDelivr. The service worker intercepts these requests, applies the CacheFirst rule, and populates the geolibre-cdn-engines cache.
3. Subsequent Offline Operation
With hasActiveServiceWorker() returning true, the application bypasses network dependencies:
if (hasActiveServiceWorker()) {
// Cached Pyodide/PGlite binaries available
await loadPyodideRuntime(); // Retrieves from geolibre-cdn-engines
} else {
// Network-dependent fallback
}
4. Proactive Engine Warming
Applications can force early caching by triggering engine requests before they're needed:
async function warmPGlite() {
// Dummy fetch triggers WASM and .data file caching
await fetch(
"https://cdn.jsdelivr.net/npm/@electric-sql/pglite@0.8.0/dist/pglite.wasm"
);
// Subsequent offline loads served from cache
}
Supporting Files and Their Roles
| File | Purpose |
|---|---|
apps/geolibre-desktop/vite.config.ts |
Service worker configuration including runtimeCaching rules for Pyodide and PGlite |
apps/geolibre-desktop/src/lib/offline-tiles.ts |
hasActiveServiceWorker() utility for cache availability checking |
apps/geolibre-desktop/public/pyodide/pyodide-worker.js |
Precached worker shim enabling Pyodide loading when CDN binaries are cached |
apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts |
Provides VITE_PYODIDE_INDEX_URL; service worker caches binaries, not the index |
Summary
- Precaching + runtime caching: GeoLibre uses Vite PWA to cache the app shell at build time and engine binaries at runtime
- jsDelivr interception: The
geolibre-cdn-enginescache stores Pyodide, PGlite, CereusDB, and GDAL3.js fromcdn.jsdelivr.net - CacheFirst with expiration: 30-day retention and 400-entry limit balance offline capability with storage constraints
- Versioned URLs prevent staleness: Exact version strings in CDN paths ensure correct cache invalidation
- Runtime detection:
hasActiveServiceWorker()guards offline operations against missing service worker support
Frequently Asked Questions
How does GeoLibre handle Pyodide updates without stale caches?
The jsDelivr URLs include exact version identifiers (e.g., /pyodide/v0.26.1/). When GeoLibre updates to a new Pyodide version, the URL changes, creating a new cache entry. The old version remains cached until naturally evicted by the maxEntries or maxAgeSeconds limits, but is never served for new requests.
Can users force re-download of cached engines?
The service worker follows standard PWA update mechanisms. When GeoLibre redeploys with a new service worker version, the browser installs the updated worker, which can include revised runtimeCaching rules. Users can also clear site data through browser settings to manually purge the geolibre-cdn-engines cache.
Why does the configuration cache status code 0 responses?
Cross-origin requests to jsDelivr without CORS headers return opaque responses with status code 0. The cacheableResponse: { statuses: [0, 200] } option ensures these valid but non-standard responses are stored in the cache, enabling offline functionality for engines loaded from third-party CDNs.
What happens if a user exceeds the 400-entry cache limit?
The Cache API's LRU (least-recently-used) eviction policy removes oldest entries when maxEntries is exceeded. For GeoLibre, this means rarely-used engine versions are purged first, while actively-used Pyodide and PGlite binaries persist. The 30-day maxAgeSeconds provides additional protection against permanent cache growth.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →