cache loader data and document, network-first approach

This commit is contained in:
m5r 2022-06-04 23:34:33 +02:00
parent 8c0a6ccf7f
commit 5943044509
2 changed files with 120 additions and 33 deletions

View File

@ -1,32 +1,106 @@
import type { FetchEventWithPreloadResponse } from "./fetch"; import { json } from "@remix-run/server-runtime";
export const ASSET_CACHE = "asset-cache"; export const ASSET_CACHE = "asset-cache";
export const DATA_CACHE = "data-cache";
export const DOCUMENT_CACHE = "document-cache";
export async function cacheAsset(event: FetchEventWithPreloadResponse) { export function isAssetRequest(request: Request) {
const url = new URL(event.request.url); return ["font", "image", "script", "style"].includes(request.destination);
const cachedResponse = await caches.match(event.request, { }
cacheName: ASSET_CACHE,
ignoreVary: true, export function isLoaderRequest(request: Request) {
ignoreSearch: true, const url = new URL(request.url);
}); return request.method.toLowerCase() === "get" && url.searchParams.get("_data");
}
console.debug(`Serving asset from ${cachedResponse ? "cache" : " network"}`, url.pathname);
export function isDocumentGetRequest(request: Request) {
const fetchPromise = (async () => { return request.method.toLowerCase() === "get" && request.mode === "navigate";
const cache = await caches.open(ASSET_CACHE); }
const preloadedResponse = await event.preloadResponse;
const response = preloadedResponse || (await fetch(event.request)); export function cacheAsset(event: FetchEvent) {
switch (response.status) { // stale-while-revalidate
case 200: const url = new URL(event.request.url);
cache.put(event.request, response.clone()); return caches
break; .match(event.request, {
case 404: cacheName: ASSET_CACHE,
cache.delete(event.request); ignoreVary: true,
break; ignoreSearch: true,
} })
.then((cachedResponse) => {
return response; console.debug(`Serving asset from ${cachedResponse ? "cache" : " network"}`, url.pathname);
})();
const fetchPromise = event.preloadResponse
return cachedResponse || fetchPromise; .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<Response> {
// 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;
});
}),
);
} }

View File

@ -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; declare let self: ServiceWorkerGlobalScope;
export type FetchEventWithPreloadResponse = FetchEvent & { preloadResponse?: Promise<Response | undefined> }; export default async function handleFetch(event: FetchEvent) {
if (isAssetRequest(event.request)) {
export default async function handleFetch(event: FetchEventWithPreloadResponse) {
if (["font", "image", "script", "style"].includes(event.request.destination)) {
return cacheAsset(event); return cacheAsset(event);
} }
if (isLoaderRequest(event.request)) {
return cacheLoaderData(event);
}
if (isDocumentGetRequest(event.request)) {
return cacheDocument(event);
}
return fetch(event.request); return fetch(event.request);
} }