// pack:ignore const CACHE = "v-1" const RELOADS = new Map>() const ETAGS = new Map() const CACHEREFRESH = new Map>() const EXCLUDECACHE = new Set([ "/api/", "/paypal/", "/import/", "chrome-extension" ]) self.addEventListener("install", (evt: InstallEvent) => { console.log("service worker install") evt.waitUntil( (self as any).skipWaiting().then(() => { return installCache() }) ) }) self.addEventListener("activate", (evt: any) => { console.log("service worker activate") // Delete old cache let cacheWhitelist = [CACHE] let deleteOldCache = caches.keys().then(keyList => { return Promise.all(keyList.map(key => { if(cacheWhitelist.indexOf(key) === -1) { return caches.delete(key) } })) }) let immediateClaim = (self as any).clients.claim() // Immediate claim evt.waitUntil( Promise.all([ deleteOldCache, immediateClaim ]) ) }) // controlling service worker self.addEventListener("message", (evt: any) => { let message = JSON.parse(evt.data) let url = message.url let refresh = RELOADS.get(url) let servedETag = ETAGS.get(url) // If the user requests a sub-page we should prefetch the full page, too. if(url.includes("/_/")) { let fullPage = new Request(url.replace("/_/", "/")) fetch(fullPage, { credentials: "same-origin" }) .then(response => { // Save the new version of the resource in the cache let cacheRefresh = caches.open(CACHE).then(cache => { return cache.put(fullPage, response) }) CACHEREFRESH.set(fullPage.url, cacheRefresh) return cacheRefresh }) } if(!refresh || !servedETag) { return } evt.waitUntil( refresh.then((response: Response) => { // If the fresh copy was used to serve the request instead of the cache, // we don"t need to tell the client to do a refresh. if(response.bodyUsed) { return } let eTag = response.headers.get("ETag") if(eTag === servedETag) { return } ETAGS.set(url, eTag) let message = { type: "new content", url } let cacheRefresh = CACHEREFRESH.get(url) if(!cacheRefresh) { console.log("forcing reload, cache refresh null") return evt.source.postMessage(JSON.stringify(message)) } return cacheRefresh.then(() => { console.log("forcing reload after cache refresh") evt.source.postMessage(JSON.stringify(message)) }) }) ) }) self.addEventListener("fetch", async (evt: FetchEvent) => { let request = evt.request as Request let isAuth = request.url.includes("/auth/") || request.url.includes("/logout") let ignoreCache = false console.log("fetch:", request.url) // Exclude certain URLs from being cached for(let pattern of EXCLUDECACHE.keys()) { if(request.url.includes(pattern)) { ignoreCache = true break } } // Delete existing cache on authentication if(isAuth) { caches.delete(CACHE) } // Do not use cache in some cases if(request.method !== "GET" || isAuth || ignoreCache) { return evt.waitUntil(evt.respondWith(fetch(request))) } // Forced cache response? if(request.headers.get("X-CacheOnly") === "true") { console.log("forced cache response") return evt.waitUntil(fromCache(request)) } let servedETag = undefined // Start fetching the request let refresh = fetch(request).then(response => { console.log(response) let clone = response.clone() // Save the new version of the resource in the cache let cacheRefresh = caches.open(CACHE).then(cache => { return cache.put(request, clone) }) CACHEREFRESH.set(request.url, cacheRefresh) return response }) // Save in map RELOADS.set(request.url, refresh) // Forced reload if(request.headers.get("X-Reload") === "true") { return evt.waitUntil(evt.respondWith(refresh.then(response => { servedETag = response.headers.get("ETag") ETAGS.set(request.url, servedETag) return response }))) } // Try to serve cache first and fall back to network response let networkOrCache = fromCache(request).then(response => { console.log("served from cache:", request.url) servedETag = response.headers.get("ETag") ETAGS.set(request.url, servedETag) return response }).catch(error => { // console.log("Cache MISS:", request.url) return refresh }) return evt.waitUntil(evt.respondWith(networkOrCache)) }) self.addEventListener("push", (evt: PushEvent) => { var payload = evt.data ? evt.data.json() : {} evt.waitUntil( (self as any).registration.showNotification(payload.title, { body: payload.message, icon: payload.icon, image: payload.image, data: payload.link }) ) }) self.addEventListener("pushsubscriptionchange", (evt: any) => { evt.waitUntil((self as any).registration.pushManager.subscribe(evt.oldSubscription.options) .then(async subscription => { console.log("send subscription to server...") let rawKey = subscription.getKey("p256dh") let key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : "" let rawSecret = subscription.getKey("auth") let secret = rawSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawSecret))) : "" let endpoint = subscription.endpoint let pushSubscription = { endpoint, p256dh: key, auth: secret, platform: navigator.platform, userAgent: navigator.userAgent, screen: { width: window.screen.width, height: window.screen.height } } let user = await fetch("/api/me").then(response => response.json()) return fetch("/api/pushsubscriptions/" + user.id + "/add", { method: "POST", credentials: "same-origin", body: JSON.stringify(pushSubscription) }) })) }) self.addEventListener("notificationclick", (evt: NotificationEvent) => { let notification = evt.notification notification.close() evt.waitUntil( (self as any).clients.matchAll().then(function(clientList) { // If we have a link, use that link to open a new window. let url = notification.data if(url) { return (self as any).clients.openWindow(url) } // If there is at least one client, focus it. if(clientList.length > 0) { return clientList[0].focus() } // Otherwise open a new window return (self as any).clients.openWindow("https://notify.moe") }) ) }) function installCache() { return caches.open(CACHE).then(cache => { return cache.addAll([ "./", "./scripts", "https://fonts.gstatic.com/s/ubuntu/v10/2Q-AW1e_taO6pHwMXcXW5w.ttf" ]) }) } function fromCache(request) { return caches.open(CACHE).then(cache => { return cache.match(request).then(matching => { if(matching) { // console.log("Cache HIT:", request.url) return Promise.resolve(matching) } return Promise.reject("no-match") }) }) }