From 0e2a2457062f80e444e91497525d9048844d40c5 Mon Sep 17 00:00:00 2001 From: muridot0 Date: Sun, 10 Mar 2024 12:38:11 +0000 Subject: [PATCH 1/2] set up service worker for compa pwa --- client/.env | 2 +- client/app/entry.client.tsx | 19 ++-- client/app/root.tsx | 43 +++++++++ client/app/routes/offline.tsx | 61 +++++++++++++ client/public/service-worker.js | 151 ++++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 client/app/routes/offline.tsx create mode 100644 client/public/service-worker.js diff --git a/client/.env b/client/.env index ff153ae..711b86f 100644 --- a/client/.env +++ b/client/.env @@ -8,7 +8,7 @@ DATABASE_URL="file:./dev.db" SCHOOL=knust COOKIE_SECRET=secret1,secret2 SECRET_KEY=secret1 -RESEND_API_KEY= +RESEND_API_KEY=123 AWS_UPLOAD_ENDPOINT="eu-central-1.linodeobjects.com" AWS_REGION="eu-central-1" AWS_ACCESS_KEY_ID= diff --git a/client/app/entry.client.tsx b/client/app/entry.client.tsx index 98533cd..22c0fc9 100644 --- a/client/app/entry.client.tsx +++ b/client/app/entry.client.tsx @@ -8,12 +8,12 @@ import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import posthog from "posthog-js"; -import AudioRecorder from 'audio-recorder-polyfill' -import mpegEncoder from 'audio-recorder-polyfill/mpeg-encoder' +import AudioRecorder from "audio-recorder-polyfill"; +import mpegEncoder from "audio-recorder-polyfill/mpeg-encoder"; -AudioRecorder.encoder = mpegEncoder -AudioRecorder.prototype.mimeType = 'audio/mpeg' -window.MediaRecorder = AudioRecorder +AudioRecorder.encoder = mpegEncoder; +AudioRecorder.prototype.mimeType = "audio/mpeg"; +window.MediaRecorder = AudioRecorder; if (process.env.NODE_ENV === "production") { posthog.init("phc_qmxF7NTz6XUnYUDoMpkTign6mujS8F8VqR75wb0Bsl7", { @@ -21,6 +21,15 @@ if (process.env.NODE_ENV === "production") { }); } +if ("serviceWorker" in navigator) { + try { + await navigator.serviceWorker.register("/service-worker.js", { + scope: "/", + }); + } catch (error) { + console.error(`Registration failed with ${error}`); + } +} startTransition(() => { hydrateRoot( diff --git a/client/app/root.tsx b/client/app/root.tsx index 989939e..12ba194 100644 --- a/client/app/root.tsx +++ b/client/app/root.tsx @@ -12,6 +12,7 @@ import { ScrollRestoration, json, useLoaderData, + useLocation, } from "@remix-run/react"; import { BottomNav, Navbar } from "./components/navbar"; import { Footer } from "./components/footer"; @@ -21,6 +22,8 @@ import { prisma } from "./lib/prisma.server"; import { GlobalCtx } from "./lib/global-ctx"; import { User } from "@prisma/client"; import { CommonHead } from "./components/common-head"; +import { useRef } from "react"; +import React from "react"; export const loader = async ({ request }: LoaderFunctionArgs) => { let user: User | undefined | null; @@ -43,6 +46,46 @@ export { ErrorBoundary } from "./components/error-boundary"; export default function App() { const { user } = useLoaderData(); + const location = useLocation(); + const isMount = useRef(true); + + React.useEffect(() => { + const mounted = isMount; + isMount.current = false; + + if ("serviceWorker" in navigator) { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller?.postMessage({ + action: "clearCache", + }); + navigator.serviceWorker.controller?.postMessage({ + type: "REMIX_NAVIGATION", + isMount: mounted, + location, + }); + } else { + const listener = async () => { + await navigator.serviceWorker.ready; + navigator.serviceWorker.controller?.postMessage({ + action: "clearCache", + }); + navigator.serviceWorker.controller?.postMessage({ + type: "REMIX_NAVIGATION", + isMount: mounted, + location, + }); + }; + navigator.serviceWorker.addEventListener("controllerchange", listener); + return () => { + navigator.serviceWorker.removeEventListener( + "controllerchange", + listener, + ); + }; + } + } + }); + return ( diff --git a/client/app/routes/offline.tsx b/client/app/routes/offline.tsx new file mode 100644 index 0000000..4392d0a --- /dev/null +++ b/client/app/routes/offline.tsx @@ -0,0 +1,61 @@ +import React from 'react' + +export default function Offline() { + const getBrowsingHistory = async () => { + const browsingHistory: Array = [] + const parser = new DOMParser() + + const cache = await caches.open( + await caches.keys().then((res) => { + return res.filter((word) => word.includes('pages'))[0] + }) + ) + + const keys = await cache.keys() + + console.log(keys) + + for (const request of keys) { + const data: Record = {} + data.url = request.url + browsingHistory.push(data) + } + + if (browsingHistory?.length) { + const markup = document.getElementById('browsing-history') + if (!markup) return + markup.innerHTML = + '

In the meantime, you still have things you can access:

' + browsingHistory.forEach((data) => { + if (!data) return + markup.innerHTML += ` + + ` + }) + document + .getElementById('browsing-history') + ?.insertAdjacentElement('beforeend', markup) + } + } + + React.useEffect(() => { + getBrowsingHistory() + }) + + return ( +
+

+ Oops, you are currently offline +

+

+ Please check your internet connection and try again +

+ +
+
+ ) +} diff --git a/client/public/service-worker.js b/client/public/service-worker.js new file mode 100644 index 0000000..2dac8a4 --- /dev/null +++ b/client/public/service-worker.js @@ -0,0 +1,151 @@ +const cacheHeader = 'compa:v1.0.0:' + +const offlineFundamentals = ['./entry.worker.js', '/offline'] + +//Add core website files to cache during serviceworker installation +const updateStaticCache = async () => { + const cache = await caches.open(`${cacheHeader}fundamentals`) + return await Promise.all( + offlineFundamentals.map((value) => { + let request = new Request(value) + const url = new URL(request.url) + if (url.origin !== location.origin) { + request = new Request(value) + } + return fetch(request) + .then((response) => { + const cachedCopy = response.clone() + return cache.put(request, cachedCopy) + }) + .catch(() => { + return caches.match(new Request('/offline')).then((response_1) => { + if (response_1) { + self.clients.matchAll().then((clients) => { + for (const client of clients) { + client.navigate('/offline') + } + }) + } + return response_1 + }) + }) + }) + ) +} + +//Clear caches with a different version number +const clearOldCaches = async () => { + const keys = await caches.keys() + return await Promise.all( + keys + .filter((key) => { + return key.indexOf(cacheHeader) !== 0 + }) + .map((key_1) => { + return caches.delete(key_1) + }) + ) +} + +//When the service worker is first added to a computer +self.addEventListener('install', (event) => { + event.waitUntil( + updateStaticCache().then(() => { + return self.skipWaiting() + }) + ) +}) + +//Service worker handles networking +self.addEventListener('fetch', (event) => { + //This service worker won't touch non-get requests + if (event.request.method !== 'GET') { + return + } + + //Fetch from network and cache + const fetchFromNetwork = async (response) => { + const cacheCopy = response.clone() + if (event.request.headers.get('Accept').indexOf('image') !== -1) { + await caches.open(`${cacheHeader}images`).then(async (cache) => { + await cache.put(event.request, cacheCopy) + }) + } else { + await caches.open(`${cacheHeader}assets`).then(async function add(cache) { + await cache.put(event.request, cacheCopy) + }) + } + + return response + } + + //For non-HTML requests, look for file in cache, then network if no cache exists. + event.respondWith( + caches.match(event.request).then((cached) => { + return ( + cached || + fetch(event.request) + .then((response) => { + return fetchFromNetwork(response) + }) + .catch(async () => { + return await caches + .match(new Request('/offline')) + .then((response) => { + if (response) { + self.clients.matchAll().then((clients) => { + for (const client of clients) { + client.navigate('/offline') + } + }) + } + return response + }) + }) + ) + }) + ) +}) + +//After the install event +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async (event) => { + if (event.data.action === 'clearCache') { + clearOldCaches() + } + + if (event.data.type === 'REMIX_NAVIGATION') { + const { location, isMounted } = event.data + const existingUrl = await caches.match(location.pathname) + + if (!existingUrl || !isMounted) { + await fetch(location.pathname) + .then(async (response) => { + const cacheCopy = response.clone() + await caches.open(`${cacheHeader}pages`).then(async (cache) => { + await cache.put(event.data.location.pathname, cacheCopy) + }) + }) + .catch(async () => { + return ( + (await caches.match(location.pathname)) || + (await caches + .match(new Request('/offline'), {}) + .then((response) => { + if (response) { + self.clients.matchAll().then((clients) => { + for (const client of clients) { + client.navigate('/offline') + } + }) + } + return response + })) + ) + }) + } + } +}) From c22ad632738044164f77da8b350ee68d026651bf Mon Sep 17 00:00:00 2001 From: muridot0 Date: Wed, 13 Mar 2024 09:07:05 +0000 Subject: [PATCH 2/2] service worker improvements --- client/app/entry.client.tsx | 2 +- client/app/root.tsx | 49 ++++++++++++++++++- client/public/service-worker.js | 87 +++++++++++++++++++++++---------- 3 files changed, 110 insertions(+), 28 deletions(-) diff --git a/client/app/entry.client.tsx b/client/app/entry.client.tsx index 22c0fc9..709c72c 100644 --- a/client/app/entry.client.tsx +++ b/client/app/entry.client.tsx @@ -23,7 +23,7 @@ if (process.env.NODE_ENV === "production") { if ("serviceWorker" in navigator) { try { - await navigator.serviceWorker.register("/service-worker.js", { + navigator.serviceWorker.register("/service-worker.js", { scope: "/", }); } catch (error) { diff --git a/client/app/root.tsx b/client/app/root.tsx index 12ba194..d858410 100644 --- a/client/app/root.tsx +++ b/client/app/root.tsx @@ -10,9 +10,11 @@ import { Outlet, Scripts, ScrollRestoration, + UIMatch, json, useLoaderData, useLocation, + useMatches, } from "@remix-run/react"; import { BottomNav, Navbar } from "./components/navbar"; import { Footer } from "./components/footer"; @@ -48,6 +50,21 @@ export default function App() { const location = useLocation(); const isMount = useRef(true); + const matches = useMatches(); + + function isPromise(p: any): boolean { + if (p && typeof p === "object" && typeof p.then === "function") { + return true; + } + return false; + } + + function isFunction(p: any): boolean { + if (typeof p === "function") { + return true; + } + return false; + } React.useEffect(() => { const mounted = isMount; @@ -62,6 +79,8 @@ export default function App() { type: "REMIX_NAVIGATION", isMount: mounted, location, + manifest: window.__remixManifest, + matches: matches.filter(filteredMatches).map(sanitizeHandleObject), }); } else { const listener = async () => { @@ -73,6 +92,8 @@ export default function App() { type: "REMIX_NAVIGATION", isMount: mounted, location, + manifest: window.__remixManifest, + matches: matches.filter(filteredMatches).map(sanitizeHandleObject), }); }; navigator.serviceWorker.addEventListener("controllerchange", listener); @@ -84,7 +105,31 @@ export default function App() { }; } } - }); + + function filteredMatches(route: UIMatch) { + if (route.data) { + return ( + Object.values(route.data).filter((elem) => { + return isPromise(elem); + }).length === 0 + ); + } + return true; + } + + function sanitizeHandleObject(route: UIMatch) { + let handle = route.handle; + + if (handle) { + const filterInvalidTypes = ([, value]: any) => + !isPromise(value) && !isFunction(value); + handle = Object.fromEntries( + Object.entries(route.handle!).filter(filterInvalidTypes), + ); + } + return { ...route, handle }; + } + }, [location, matches]); return ( @@ -103,7 +148,7 @@ export default function App() { - +