MapComplete/src/service-worker/index.ts

147 lines
4.1 KiB
TypeScript

// This file must have worker types, but not DOM types.
// The global should be that of a service worker.
// This fixes `self`'s type.
import { SWGenerated } from "./SWGenerated"
declare var self: ServiceWorkerGlobalScope
export {}
const selfDomain = self.location.hostname
function jsonResponse(object: object | []): Response {
return new Response(JSON.stringify(object), {
headers: {
"Content-Type": "application/json",
},
})
}
const cacheKey = SWGenerated.vNumber + "-" + SWGenerated.vNumber
function cleanOldCaches() {
caches.keys().then((keys) => {
return Promise.all(
keys.map((k) => {
if (k !== cacheKey) {
return caches.delete(k)
}
})
)
})
}
function getCache() {
return caches.open(cacheKey)
}
function respondFromCache(event: FetchEvent) {
event.respondWith(
getCache().then(async (cache) => {
const cached = await cache.match(event.request)
if (!cached) {
const response = await fetch(event.request)
cache.put(event.request, response.clone())
return response
}
return cached
})
)
}
async function listCachedRequests(): Promise<string[]> {
const cache = await getCache()
const requests = await cache.keys()
return requests.map((req) => req.url)
}
class Router {
private readonly _endpoints: Record<string, (event: FetchEvent) => void>
private readonly _subpaths: Record<string, (event: FetchEvent, rest: string) => void>
private readonly _fallback: undefined | ((event: FetchEvent, rest: string) => void)
constructor(
endpoints: Record<string, (event: FetchEvent) => void>,
subpaths: Record<string, (event: FetchEvent, rest: string) => void>,
fallback?: undefined | ((event: FetchEvent, rest: string) => void)
) {
this._endpoints = endpoints
this._subpaths = subpaths
this._fallback = fallback
}
public route(event: FetchEvent, rest?: string) {
const url = new URL(event.request.url)
rest ??= url.pathname.split("/service-worker/")[1]
console.log(">>> routing", rest)
if (rest.indexOf("/") > 0) {
const nextSegment = rest.split("/")[0]
if (this._subpaths[nextSegment]) {
return this._subpaths[nextSegment](event, rest.substring(nextSegment.length + 1))
}
} else if (this._endpoints[rest] !== undefined) {
return this._endpoints[rest](event)
}
if (this._fallback) {
return this._fallback(event, rest)
}
}
}
const allOffline = new Router(
{
"status.json": (event) => {
event.respondWith(
listCachedRequests().then((cached) =>
jsonResponse({
status: "ok",
cached,
})
)
)
},
},
{}
)
self.addEventListener("fetch", (event) => {
const url = event.request.url
if (url.endsWith("/service-worker.js")) {
return // Installation of a new version, we don't interfere
}
console.log("Intercepting event", event.request.url)
if (url.indexOf("/service-worker/") >= 0) {
allOffline.route(event)
return
}
const urlObj = new URL(url)
if (
urlObj.hostname === selfDomain &&
selfDomain !== "localhost" &&
selfDomain !== "127.0.0.1"
) {
respondFromCache(event)
return
}
})
self.addEventListener("install", async () => {
await self.skipWaiting()
cleanOldCaches()
})
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim())
// Delete the old caches (of an older version number
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.map(async (key) => {
if (key !== SWGenerated.vNumber) {
await caches.delete(key)
}
})
)
)
)
})