diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index a2d786df7..8cd271252 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -1,8 +1,3 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - import { get } from 'idb-keyval'; import * as Misskey from 'misskey-js'; import type { PushNotificationDataMap } from '@/types.js'; @@ -12,99 +7,37 @@ import { createEmptyNotification, createNotification } from '@/scripts/create-no import { swLang } from '@/scripts/lang.js'; import * as swos from '@/scripts/operations.js'; -const CACHE_NAME = 'pari-cache-${_VERSION_}'; -const urlsToCache = [ - '/manifest.json', +const STATIC_ASSETS = [ '/assets', '/emoji', '/twemoji', '/fluent-emoji', '/vite', - '/identicon', - '/proxy' ]; -globalThis.addEventListener('install', (event) => { - event.waitUntil( - caches.open(CACHE_NAME) - .then((cache) => cache.addAll(urlsToCache)) +const STATIC_CACHE_NAME = `static-cache-${_VERSION_}`; +const LANG_CACHE_NAME = swLang.cacheName; + +globalThis.addEventListener('install', (ev) => { + ev.waitUntil( + (async () => { + const cache = await caches.open(STATIC_CACHE_NAME); + await cache.addAll(STATIC_ASSETS); + })(), ); }); -globalThis.addEventListener('activate', (event) => { - const cacheWhitelist = [CACHE_NAME, swLang.cacheName]; - event.waitUntil( - caches.keys().then((cacheNames) => { - return Promise.all( - cacheNames.map((cacheName) => { - if (cacheWhitelist.indexOf(cacheName) === -1) { - return caches.delete(cacheName); - } - }) - ); - }).then(() => globalThis.clients.claim()) - ); -}); - -globalThis.addEventListener('fetch', (event) => { - let isHTMLRequest = false; - - if (event.request.headers.get('sec-fetch-dest') === 'document' || - event.request.headers.get('accept')?.includes('/html') || - event.request.url.endsWith('/')) { - isHTMLRequest = true; - } - - if (isHTMLRequest) { - event.respondWith( - fetch(event.request) - .catch(async () => { - const html = await offlineContentHTML(); - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); - }) - ); - return; - } - - event.respondWith( - caches.match(event.request) - .then((response) => { - if (response) { - return response; - } - return fetch(event.request).then( - (response) => { - if (!response || response.status !== 200 || response.type !== 'basic') { - return response; - } - - const responseToCache = response.clone(); - - caches.open(CACHE_NAME) - .then((cache) => { - cache.put(event.request, responseToCache); - }); - - return response; - } - ).catch(async () => { - if (isHTMLRequest) { - const html = await offlineContentHTML(); - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); - } - throw new Error('Failed to fetch'); - }); - }) +globalThis.addEventListener('activate', ev => { + ev.waitUntil( + (async () => { + const cacheNames = await caches.keys(); + const deletePromises = cacheNames + .filter(name => name !== STATIC_CACHE_NAME && name !== LANG_CACHE_NAME) + .map(name => caches.delete(name)); + + await Promise.all(deletePromises); + await globalThis.clients.claim(); + })(), ); }); @@ -119,6 +52,70 @@ async function offlineContentHTML() { return `${messages.title}
${messages.header}
v${_VERSION_}
`; } +globalThis.addEventListener('fetch', ev => { + const url = new URL(ev.request.url); + + const isStaticAsset = STATIC_ASSETS.some(path => url.pathname.startsWith(path)); + + let isHTMLRequest = false; + if (ev.request.headers.get('sec-fetch-dest') === 'document') { + isHTMLRequest = true; + } else if (ev.request.headers.get('accept')?.includes('/html')) { + isHTMLRequest = true; + } else if (ev.request.url.endsWith('/')) { + isHTMLRequest = true; + } + + if (isStaticAsset) { + ev.respondWith( + caches.match(ev.request) + .then(response => { + if (response) { + return response; + } + return fetch(ev.request).then(response => { + if (response.status === 200) { + const responseClone = response.clone(); + caches.open(STATIC_CACHE_NAME).then(cache => { + cache.put(ev.request, responseClone); + }); + } + return response; + }); + }) + .catch(() => { + if (isHTMLRequest) { + return offlineContentHTML().then(html => { + return new Response(html, { + status: 200, + headers: { + 'content-type': 'text/html', + }, + }); + }); + } + }), + ); + return; + } + + if (isHTMLRequest) { + ev.respondWith( + fetch(ev.request) + .catch(async () => { + const html = await offlineContentHTML(); + return new Response(html, { + status: 200, + headers: { + 'content-type': 'text/html', + }, + }); + }), + ); + return; + } +}); + globalThis.addEventListener('push', ev => { ev.waitUntil(globalThis.clients.matchAll({ includeUncontrolled: true, @@ -144,6 +141,10 @@ globalThis.addEventListener('push', ev => { globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { ev.waitUntil((async (): Promise => { + if (_DEV_) { + console.log('notificationclick', ev.action, ev.notification.data); + } + const { action, notification } = ev; const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; const { userId: loginId } = data; @@ -165,45 +166,57 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); break; case 'accept': - if (data.body.type === 'receiveFollowRequest') { - await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); + break; } break; case 'reject': - if (data.body.type === 'receiveFollowRequest') { - await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); + switch (data.body.type) { + case 'receiveFollowRequest': + await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); + break; } break; case 'showFollowRequests': client = await swos.openClient('push', '/my/follow-requests', loginId); break; default: - if (data.body.type === 'receiveFollowRequest') { - client = await swos.openClient('push', '/my/follow-requests', loginId); - } else if (data.body.type === 'reaction') { - client = await swos.openNote(data.body.note.id, loginId); - } else if ('note' in data.body) { - client = await swos.openNote(data.body.note.id, loginId); - } else if ('user' in data.body) { - client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + switch (data.body.type) { + case 'receiveFollowRequest': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + case 'reaction': + client = await swos.openNote(data.body.note.id, loginId); + break; + default: + if ('note' in data.body) { + client = await swos.openNote(data.body.note.id, loginId); + } else if ('user' in data.body) { + client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + } + break; } - break; } break; case 'unreadAntennaNote': client = await swos.openAntenna(data.body.antenna.id, loginId); break; default: - if (action === 'markAllAsRead') { - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - await get[]>('accounts').then(accounts => { - return Promise.all((accounts ?? []).map(async account => { - await swos.sendMarkAllAsRead(account.id); - })); - }); - } else if (action === 'settings') { - client = await swos.openClient('push', '/settings/notifications', loginId); + switch (action) { + case 'markAllAsRead': + await globalThis.registration.getNotifications() + .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); + await get[]>('accounts').then(accounts => { + return Promise.all((accounts ?? []).map(async account => { + await swos.sendMarkAllAsRead(account.id); + })); + }); + break; + case 'settings': + client = await swos.openClient('push', '/settings/notifications', loginId); + break; } } @@ -231,12 +244,13 @@ globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEv globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { ev.waitUntil((async (): Promise => { - if (ev.data === 'clear') { - await caches.keys() - .then(cacheNames => Promise.all( - cacheNames.map(name => caches.delete(name)), - )); - return; + switch (ev.data) { + case 'clear': + await caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)), + )); + return; } if (typeof ev.data === 'object') {