diff --git a/app/entry.client.tsx b/app/entry.client.tsx index bc75b68..13fa3dc 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -8,6 +8,20 @@ if ("serviceWorker" in navigator) { try { await navigator.serviceWorker.register("/entry.worker.js"); await navigator.serviceWorker.ready; + + if (navigator.serviceWorker.controller) { + return navigator.serviceWorker.controller.postMessage({ + type: "SYNC_REMIX_MANIFEST", + manifest: window.__remixManifest, + }); + } + + navigator.serviceWorker.addEventListener("controllerchange", () => { + navigator.serviceWorker.controller?.postMessage({ + type: "SYNC_REMIX_MANIFEST", + manifest: window.__remixManifest, + }); + }); } catch (error) { console.error("Service worker registration failed", error, (error as Error).name); } diff --git a/app/entry.worker.ts b/app/entry.worker.ts index 8af55b8..2e87d83 100644 --- a/app/entry.worker.ts +++ b/app/entry.worker.ts @@ -5,6 +5,7 @@ import handleActivate from "./service-worker/activate"; import handlePush from "./service-worker/push"; import handleNotificationClick from "./service-worker/notification-click"; import handleFetch from "./service-worker/fetch"; +import handleMessage from "./service-worker/message"; declare let self: ServiceWorkerGlobalScope; @@ -24,6 +25,10 @@ self.addEventListener("notificationclick", (event) => { event.waitUntil(handleNotificationClick(event)); }); +self.addEventListener("message", (event) => { + event.waitUntil(handleMessage(event)); +}); + self.addEventListener("fetch", (event) => { event.respondWith(handleFetch(event)); }); diff --git a/app/service-worker/message.ts b/app/service-worker/message.ts new file mode 100644 index 0000000..0824cf0 --- /dev/null +++ b/app/service-worker/message.ts @@ -0,0 +1,110 @@ +import type { AssetsManifest } from "@remix-run/react/entry"; +import type { EntryRoute } from "@remix-run/react/routes"; + +import { ASSET_CACHE } from "./cache-utils"; + +declare let self: ServiceWorkerGlobalScope; + +export default async function handleMessage(event: ExtendableMessageEvent) { + if (event.data.type === "SYNC_REMIX_MANIFEST") { + return handleSyncRemixManifest(event); + } +} + +async function handleSyncRemixManifest(event: ExtendableMessageEvent) { + console.debug("Caching routes modules"); + + await cacheStaticAssets(event.data.manifest); + + // await cacheConversations(manifest); +} + +async function cacheStaticAssets(manifest: AssetsManifest) { + const cachePromises: Map> = new Map(); + const assetCache = await caches.open(ASSET_CACHE); + const routes = [...Object.values(manifest.routes), manifest.entry]; + + for (const route of routes) { + if (!cachePromises.has(route.module)) { + cachePromises.set(route.module, cacheAsset(route.module)); + } + + if (route.imports) { + for (const assetUrl of route.imports) { + if (!cachePromises.has(assetUrl)) { + cachePromises.set(assetUrl, cacheAsset(assetUrl)); + } + } + } + } + + await Promise.all(cachePromises.values()); + + async function cacheAsset(assetUrl: string) { + if (await assetCache.match(assetUrl)) { + return; + } + + console.debug("Caching asset", assetUrl); + return assetCache.add(assetUrl).catch((error) => { + console.debug(`Failed to cache asset ${assetUrl}:`, error); + }); + } +} + +/*async function cacheConversations(manifest: AssetsManifest) { + console.log("caching conversation"); + const cachePromises: Map> = new Map(); + const dataCache = await caches.open(DATA_CACHE); + const messagesResponse = await getMessagesResponse(); + if (!messagesResponse) { + console.log("rip never happened"); + return; + } + + const { json } = await messagesResponse.json(); + const recipients = Object.keys(json.conversations); + recipients.forEach((recipient) => cacheConversation(recipient)); + + await Promise.all(cachePromises.values()); + + function getMessagesResponse() { + const route = manifest.routes["routes/__app/messages"]; + const pathname = getPathname(route, manifest); + const params = new URLSearchParams({ _data: route.id }); + const search = `?${params.toString()}`; + const url = pathname + search; + return dataCache.match(url); + } + + function cacheConversation(recipient: string) { + const route = manifest.routes["routes/__app/messages.$recipient"]; + const pathname = getPathname(route, manifest).replace(":recipient", encodeURIComponent(recipient)); + const params = new URLSearchParams({ _data: route.id }); + const search = `?${params.toString()}`; + const url = pathname + search; + if (!cachePromises.has(url)) { + console.debug("Caching conversation with", recipient); + cachePromises.set( + url, + dataCache.add(url).catch((error) => { + console.debug(`Failed to cache data for ${url}:`, error); + }), + ); + } + } +}*/ + +function getPathname(route: EntryRoute, manifest: AssetsManifest) { + let pathname = ""; + if (route.path && route.path.length > 0) { + pathname = "/" + route.path; + } + if (route.parentId) { + const parentPath = getPathname(manifest.routes[route.parentId], manifest); + if (parentPath) { + pathname = parentPath + pathname; + } + } + return pathname; +}