Skip to content

Instantly share code, notes, and snippets.

@deshario
Created February 14, 2024 14:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save deshario/578c9f7a07ef671cd684c323786ce854 to your computer and use it in GitHub Desktop.
Save deshario/578c9f7a07ef671cd684c323786ce854 to your computer and use it in GitHub Desktop.
/* eslint-env serviceworker */
/* eslint-disable no-console */
import localForage from 'localforage'
import { matchPrecache, precacheAndRoute } from 'workbox-precaching'
import { registerRoute, setCatchHandler } from 'workbox-routing'
import { CacheFirst, NetworkFirst } from 'workbox-strategies'
self.addEventListener('install', (event) => {
self.skipWaiting()
console.log('Service worker installed')
})
self.addEventListener('activate', (event) => {
self.clients.claim()
console.log('Service worker activated')
})
// Keep last used tab
let lastTabId
function postMessage(tab, message) {
if (!tab) {
return
}
lastTabId = tab.id
tab.postMessage(message)
}
function getUserNameFromBody(body) {
return (body || '').split(':')[0]
}
self.addEventListener('push', (event) => {
// This should not happen at all, our server always send notification with data
// So `event.data` should always exists
// but just in case, we provide nice notification that something is updated
// Chrome and some browsers not allowed us to ignore notification
// They will display 'Site have been updated' automatically if don't show notification
// which will confused user
// So in the worst-case, we display this
if (!event.data) {
event.waitUntil(
self.registration.showNotification('Taskworld', {
body: 'There are some updates in your workspace.',
tag: 'unknown-group',
})
)
return
}
const data = event.data.json()
const receivedDate = new Date().toISOString()
event.waitUntil(
tryGetActiveTab().then((tab) => {
localForage.getItem('doNotDisturbEnabled').then((doNotDisturbEnabled) => {
if (doNotDisturbEnabled) {
return
}
// This will play the notification sound in WebNotificationContainer.
// See https://github.com/taskworld/tw-frontend/blob/8fb350a3e9cf5ac97121238052a6022ab7a9d673/client/src/react/components/v2/notifications/WebNotificationContainer.react.tsx#L186-L188
//
// We need notification sound both when the tab is active and inactive.
postMessage(tab, {
topic: 'noti',
send_date: data.sendDate,
received_date: receivedDate,
// See https://github.com/taskworld/tw-frontend/pull/7477
web_notification_sound: data.web_notification_sound,
})
const userName = getUserNameFromBody(data.body)
self.registration.showNotification(`${userName} Taskworld`, {
body: data.body,
icon: data.icon,
tag: data.key || data.webpath,
data,
receivedDate,
})
})
})
)
})
function tryGetActiveTab() {
return clients
.matchAll({
type: 'window',
includeUncontrolled: true,
})
.then((tabs) => {
if (!tabs || tabs.length === 0) {
return
}
const focused = tabs.find((tab) => tab.focused === true)
if (focused) {
return focused
}
const lastTab = tabs.find((tab) => tab.id === lastTabId)
return lastTab || tabs[0]
})
}
self.addEventListener('notificationclick', (event) => {
event.notification.close()
if (!event || !event.notification || !event.notification.data) {
console.error('invalid event notification data set', event)
return
}
event.waitUntil(
tryGetActiveTab().then((tab) => {
if (!tab && self.clients && self.clients.openWindow) {
return self.clients.openWindow(event.notification.data.webpath)
}
postMessage(tab, {
topic: 'open',
payload: {
webpath: event.notification.data.webpath,
},
})
return tab.focus()
})
)
})
// These workbox entries in console log are too much for me. And I do not think
// we are going to debug workbox everyday, so I will disable this by default.
// See https://developers.google.com/web/tools/workbox/guides/configure-workbox#disable_logging
// Feel free to comment out the line below when you want to debug workbox.
self.__WB_DISABLE_DEV_LOGS = true
// All these caching stuffs done by workbox are for frontend assets only. To
// allow PWA to goes offline without crashing the entire app. Backend API calls
// and external calls to 3rd-party services will still failed when offline. And
// it should not affect Google Lighthouse score (hopefully).
registerRoute(({ url }) => {
const urlString = url.toString()
if (urlString.match(/__dev\.\w+$/)) {
return false
}
return (
urlString.startsWith('https://d30795irbdecem.cloudfront.net/assets/') ||
// PWA icons
urlString.startsWith('https://taskworld.github.io/icons/') ||
urlString.startsWith(`${location.origin}/assets/`) ||
// Static files e.g. manifest.json
urlString.startsWith(`${location.origin}/static__`) ||
urlString.endsWith(`${location.origin}/offline.html`) ||
urlString.endsWith('/unsupported-browsers-page.html') ||
urlString.endsWith('/favicon.ico')
)
}, new CacheFirst())
registerRoute(({ url }) => {
const urlString = url.toString()
return (
urlString.startsWith(`${location.origin}/`) &&
// API resources doesn't need caching.
// Also, workbox requests will fail with websockets.
!urlString.startsWith(`${location.origin}/sockjs-node/`) &&
!urlString.startsWith(`${location.origin}/api/`)
)
}, new NetworkFirst())
precacheAndRoute(self.__WB_MANIFEST)
setCatchHandler(async ({ event }) => {
if (event.request.destination === 'document') {
return matchPrecache('/offline.html')
}
return Response.error()
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment