diff --git a/app/service-worker/cache-utils.ts b/app/service-worker/cache-utils.ts index b31c2be..372665c 100644 --- a/app/service-worker/cache-utils.ts +++ b/app/service-worker/cache-utils.ts @@ -1,32 +1,106 @@ -import type { FetchEventWithPreloadResponse } from "./fetch"; +import { json } from "@remix-run/server-runtime"; export const ASSET_CACHE = "asset-cache"; +export const DATA_CACHE = "data-cache"; +export const DOCUMENT_CACHE = "document-cache"; -export async function cacheAsset(event: FetchEventWithPreloadResponse) { - const url = new URL(event.request.url); - const cachedResponse = await caches.match(event.request, { - cacheName: ASSET_CACHE, - ignoreVary: true, - ignoreSearch: true, - }); - - console.debug(`Serving asset from ${cachedResponse ? "cache" : " network"}`, url.pathname); - - const fetchPromise = (async () => { - const cache = await caches.open(ASSET_CACHE); - const preloadedResponse = await event.preloadResponse; - const response = preloadedResponse || (await fetch(event.request)); - switch (response.status) { - case 200: - cache.put(event.request, response.clone()); - break; - case 404: - cache.delete(event.request); - break; - } - - return response; - })(); - - return cachedResponse || fetchPromise; +export function isAssetRequest(request: Request) { + return ["font", "image", "script", "style"].includes(request.destination); +} + +export function isLoaderRequest(request: Request) { + const url = new URL(request.url); + return request.method.toLowerCase() === "get" && url.searchParams.get("_data"); +} + +export function isDocumentGetRequest(request: Request) { + return request.method.toLowerCase() === "get" && request.mode === "navigate"; +} + +export function cacheAsset(event: FetchEvent) { + // stale-while-revalidate + const url = new URL(event.request.url); + return caches + .match(event.request, { + cacheName: ASSET_CACHE, + ignoreVary: true, + ignoreSearch: true, + }) + .then((cachedResponse) => { + console.debug(`Serving asset from ${cachedResponse ? "cache" : " network"}`, url.pathname); + + const fetchPromise = event.preloadResponse + .then((preloadedResponse?: Response) => preloadedResponse || fetch(event.request.clone())) + .then((response) => + caches.open(ASSET_CACHE).then((cache) => { + switch (response.status) { + case 200: + cache.put(event.request, response.clone()); + break; + case 404: + cache.delete(event.request); + break; + } + + return response; + }), + ); + + return cachedResponse || fetchPromise; + }); +} + +export function cacheLoaderData(event: FetchEvent) { + // network-first + const url = new URL(event.request.url); + console.debug("Serving data from network", url.pathname + url.search); + + return event.preloadResponse + .then((preloadedResponse?: Response) => preloadedResponse || fetch(event.request.clone())) + .then((response) => + caches + .open(DATA_CACHE) + .then((cache) => cache.put(event.request, response.clone())) + .then(() => response), + ) + .catch(() => { + console.debug("Serving data from network failed, falling back to cache", url.pathname + url.search); + return caches.match(event.request).then((response) => { + if (!response) { + return json( + { message: "Network Error" }, + { + status: 500, + headers: { "X-Remix-Catch": "yes", "X-Remix-Worker": "yes" }, + }, + ); + } + + response.headers.set("X-Remix-Worker", "yes"); + return response; + }); + }); +} + +export function cacheDocument(event: FetchEvent): Promise { + // network-first + const url = new URL(event.request.url); + console.debug("Serving document from network", url.pathname); + return caches.open(DOCUMENT_CACHE).then((cache) => + fetch(event.request.clone()) + .then((response) => { + cache.put(event.request, response.clone()); + return response; + }) + .catch((error) => { + console.debug("Serving document from network failed, falling back to cache", url.pathname); + return caches.match(event.request).then((response) => { + if (!response) { + throw error; + } + + return response; + }); + }), + ); } diff --git a/app/service-worker/fetch.ts b/app/service-worker/fetch.ts index b49fbfc..bedd28a 100644 --- a/app/service-worker/fetch.ts +++ b/app/service-worker/fetch.ts @@ -1,13 +1,26 @@ -import { ASSET_CACHE, cacheAsset } from "~/service-worker/cache-utils"; +import { + cacheAsset, + cacheDocument, + cacheLoaderData, + isAssetRequest, + isDocumentGetRequest, + isLoaderRequest, +} from "~/service-worker/cache-utils"; declare let self: ServiceWorkerGlobalScope; -export type FetchEventWithPreloadResponse = FetchEvent & { preloadResponse?: Promise }; - -export default async function handleFetch(event: FetchEventWithPreloadResponse) { - if (["font", "image", "script", "style"].includes(event.request.destination)) { +export default async function handleFetch(event: FetchEvent) { + if (isAssetRequest(event.request)) { return cacheAsset(event); } + if (isLoaderRequest(event.request)) { + return cacheLoaderData(event); + } + + if (isDocumentGetRequest(event.request)) { + return cacheDocument(event); + } + return fetch(event.request); }