From 7a5c30998e7afa1e58556c038fd776dfaee06544 Mon Sep 17 00:00:00 2001 From: hudy9x <95471659+hudy9x@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:09:22 +0700 Subject: [PATCH] feat(cache): cache resources (project pages, css, font, js, image) using service worker (#239) --- .../app/[orgName]/meeting/MeetingRoomList.tsx | 2 +- .../app/_features/ServiceWorker/index.tsx | 13 ++ packages/ui-app/app/layout.tsx | 2 + packages/ui-app/public/sw-cache-resources.js | 169 ++++++++++++++++++ 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 packages/ui-app/app/_features/ServiceWorker/index.tsx create mode 100644 packages/ui-app/public/sw-cache-resources.js diff --git a/packages/ui-app/app/[orgName]/meeting/MeetingRoomList.tsx b/packages/ui-app/app/[orgName]/meeting/MeetingRoomList.tsx index 78865e79..8911a304 100644 --- a/packages/ui-app/app/[orgName]/meeting/MeetingRoomList.tsx +++ b/packages/ui-app/app/[orgName]/meeting/MeetingRoomList.tsx @@ -48,7 +48,7 @@ export default function MeetingRoomList() { // only clear fixed loading as the page unmount useEffect(() => { return () => { - setFixLoading(false) + // setFixLoading(false) } }) diff --git a/packages/ui-app/app/_features/ServiceWorker/index.tsx b/packages/ui-app/app/_features/ServiceWorker/index.tsx new file mode 100644 index 00000000..e7b8eb9e --- /dev/null +++ b/packages/ui-app/app/_features/ServiceWorker/index.tsx @@ -0,0 +1,13 @@ +'use client' +import { useEffect } from "react" + +export default function RegisterServiceWorker() { + useEffect(() => { + if ('serviceWorker' in window.navigator) { + window.navigator.serviceWorker + .register('/sw-cache-resources.js') + .then(registration => console.log('Scope is:', registration, registration.scope)) + } + }) + return <> +} diff --git a/packages/ui-app/app/layout.tsx b/packages/ui-app/app/layout.tsx index 1dd835b6..99fab0f4 100644 --- a/packages/ui-app/app/layout.tsx +++ b/packages/ui-app/app/layout.tsx @@ -7,6 +7,7 @@ import RootLayoutComp from '../layouts/RootLayout' import { GoalieProvider } from '@goalie/nextjs' import dynamic from 'next/dynamic' +import RegisterServiceWorker from './_features/ServiceWorker' const inter = Inter({ subsets: ['latin'] }) const PushNotification = dynamic( @@ -33,6 +34,7 @@ export default function RootLayout({ {children} + diff --git a/packages/ui-app/public/sw-cache-resources.js b/packages/ui-app/public/sw-cache-resources.js new file mode 100644 index 00000000..5dfb2b68 --- /dev/null +++ b/packages/ui-app/public/sw-cache-resources.js @@ -0,0 +1,169 @@ +// everytime you deploy new frontend version, please update the cache version +const cacheVersion = 'v0.1' + +const cacheClone = async (e) => { + const res = await fetch(e.request); + const resClone = res.clone(); + + const cache = await caches.open(cacheVersion); + await cache.put(e.request, resClone); + return res; +}; + +const deleteOldCaches = async () => { + const keys = await caches.keys() + keys.map(async k => { + if (k === cacheVersion) return + console.log('delete key', k) + await caches.delete(k) + }) +} + +const cacheResource = async (cacheName, event) => { + const cache = await caches.open(cacheName); + const cachedResponse = await cache.match(event.request) + + if (cachedResponse) { + return cachedResponse + } + + const res = await fetch(event.request); + const resClone = res.clone(); + + await cache.put(event.request, resClone); + return res; +} + +const cacheFirstThenFetch = async (cacheName, event) => { + const cache = await caches.open(cacheName); + const cachedResponse = await cache.match(event.request) + + if (cachedResponse) { + event.waitUntil( // Schedule a background update + fetch(event.request).then((networkResponse) => { + cache.put(event.request, networkResponse.clone()); + }) + ); + + return cachedResponse + } + + const res = await fetch(event.request); + const resClone = res.clone(); + + await cache.put(event.request, resClone); + return res; +} + + +const isEmojiResources = (url) => { + return url.includes('cdn.jsdelivr.net/npm/emoji-datasource-twitter/img') +} + +const cacheEmojiResources = async (event) => { + cacheResource(cacheVersion, event) +} + +const isNextjsStaticResource = (url) => { + return url.includes('_next/static'); + +} + +const cachedNextStaticResources = async (event) => { + cacheResource(cacheVersion, event) +} + +const isOtherNextResource = (url) => { + return url.includes('__nextjs_original-stack-frame'); +} + +const cacheOtherNextResource = async (event) => { + cacheResource(cacheVersion, event) +} + +const cacheProjectPage = async (event) => { + cacheFirstThenFetch(cacheVersion, event) +} + +const isApiRequest = (url) => { + return url.includes('/api/'); +} + +const isProjectPage = (url) => { + const urlObj = new URL(url) + + if (!urlObj.pathname) return + + const path = urlObj.pathname.split('/').filter(Boolean) + + return path.length === 3 && path[1] === 'project' + +} + +const isGmailAvatar = (url) => { + return url.includes('lh3.googleusercontent.com'); +} + +const cacheGmailAvatar = async (event) => { + cacheFirstThenFetch(cacheVersion, event) +} + +const fetchEvent = () => { + + // delete old caches + deleteOldCaches() + + self.addEventListener('fetch', (e) => { + const url = e.request.url + + // cache fixed images like emoji picker + if (isEmojiResources(url)) { + cacheEmojiResources(e) + return + } + + if (isNextjsStaticResource(url)) { + cachedNextStaticResources(e) + return + } + + if (isOtherNextResource(url)) { + cacheOtherNextResource(e) + // e.respondWith(fetch(e.request)) + return + } + + if (isProjectPage(url)) { + cacheProjectPage(e) + return + } + + if (isGmailAvatar(url)) { + cacheGmailAvatar(e) + } + + if (isApiRequest(url)) { + // console.log('request url', url) + // e.respondWith(fetch(e.request)) + return + } + + }); +}; + +fetchEvent(); + +const installEvent = () => { + self.addEventListener('install', () => { + console.log('service worker installed'); + }); +}; +installEvent(); + +const activateEvent = () => { + self.addEventListener('activate', () => { + console.log('service worker activated'); + }); +}; +activateEvent(); +