/* global self */ import { precacheAndRoute } from 'workbox-precaching' import { offlineFallback } from 'workbox-recipes' import { setDefaultHandler } from 'workbox-routing' import { NetworkOnly } from 'workbox-strategies' import { enable } from 'workbox-navigation-preload' import manifest from './precache-manifest.json' import ServiceWorkerStorage from 'serviceworker-storage' // comment out to enable workbox console logs self.__WB_DISABLE_DEV_LOGS = true const storage = new ServiceWorkerStorage('sw:storage', 1) // preloading improves startup performance // https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/ enable() // ignore precache manifest generated by InjectManifest // they statically check for the presence of this variable console.log(self.__WB_MANIFEST) precacheAndRoute(manifest) self.addEventListener('install', () => { self.skipWaiting() }) // Using network-only as the default strategy ensures that we fallback // to the browser as if the service worker wouldn't exist. // The browser may use own caching (HTTP cache). // Also, the offline fallback only works if request matched a route setDefaultHandler(new NetworkOnly({ // tell us why a request failed in dev plugins: [{ fetchDidFail: async (args) => { process.env.NODE_ENV !== 'production' && console.log('fetch did fail', ...args) } }] })) // This won't work in dev because pages are never cached. // See https://github.com/vercel/next.js/blob/337fb6a9aadb61c916f0121c899e463819cd3f33/server/render.js#L181-L185 offlineFallback({ pageFallback: '/offline' }) self.addEventListener('push', async function (event) { const payload = event.data?.json() if (!payload) return const { tag } = payload.options event.waitUntil((async () => { if (!['REPLY', 'MENTION'].includes(tag)) { return self.registration.showNotification(payload.title, payload.options) } const notifications = await self.registration.getNotifications({ tag }) // since we used a tag filter, there should only be zero or one notification if (notifications.length > 1) { console.error(`more than one notification with tag ${tag} found`) return null } if (notifications.length === 0) { return self.registration.showNotification(payload.title, payload.options) } const currentNotification = notifications[0] const amount = currentNotification.data?.amount ? currentNotification.data.amount + 1 : 2 let title = '' if (tag === 'REPLY') { title = `You have ${amount} new replies` } else if (tag === 'MENTION') { title = `You were mentioned ${amount} times` } currentNotification.close() const { icon } = currentNotification return self.registration.showNotification(title, { icon, tag, data: { url: '/notifications', amount } }) })()) }) self.addEventListener('notificationclick', (event) => { const url = event.notification.data?.url if (url) { event.waitUntil(self.clients.openWindow(url)) } event.notification.close() }) // https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f self.addEventListener('message', (event) => { if (event.data.action === 'STORE_SUBSCRIPTION') { return event.waitUntil(storage.setItem('subscription', event.data.subscription)) } if (event.data.action === 'SYNC_SUBSCRIPTION') { return event.waitUntil(handlePushSubscriptionChange()) } }) async function handlePushSubscriptionChange (oldSubscription, newSubscription) { // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/pushsubscriptionchange_event // fallbacks since browser may not set oldSubscription and newSubscription oldSubscription ??= await storage.getItem('subscription') newSubscription ??= await self.registration.pushManager.getSubscription() if (!newSubscription) { // no subscription exists at the moment return } if (oldSubscription?.endpoint === newSubscription.endpoint) { // subscription did not change. no need to sync with server return } // convert keys from ArrayBuffer to string newSubscription = JSON.parse(JSON.stringify(newSubscription)) const variables = { endpoint: newSubscription.endpoint, p256dh: newSubscription.keys.p256dh, auth: newSubscription.keys.auth, oldEndpoint: oldSubscription?.endpoint } const query = ` mutation savePushSubscription($endpoint: String!, $p256dh: String!, $auth: String!, $oldEndpoint: String!) { savePushSubscription(endpoint: $endpoint, p256dh: $p256dh, auth: $auth, oldEndpoint: $oldEndpoint) { id } }` const body = JSON.stringify({ query, variables }) await fetch('/api/graphql', { method: 'POST', headers: { 'Content-type': 'application/json' }, body }) await storage.setItem('subscription', JSON.parse(JSON.stringify(newSubscription))) } self.addEventListener('pushsubscriptionchange', (event) => { event.waitUntil(handlePushSubscriptionChange(event.oldSubscription, event.newSubscription)) }, false)