From 2e94226b1c76231220c054b9a69abf1b5ba2fd18 Mon Sep 17 00:00:00 2001 From: ayoayco Date: Mon, 19 Aug 2024 13:52:51 +0200 Subject: [PATCH] feat: new experimental `strategies` option --- packages/index.js | 1 - .../CacheRevalidatePreloadFallback.js | 150 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 packages/strategies/CacheRevalidatePreloadFallback.js diff --git a/packages/index.js b/packages/index.js index 9e8c7d2..8bd043b 100644 --- a/packages/index.js +++ b/packages/index.js @@ -1,4 +1,3 @@ -// @ts-check /** * @license MIT * @author Ayo Ayco diff --git a/packages/strategies/CacheRevalidatePreloadFallback.js b/packages/strategies/CacheRevalidatePreloadFallback.js new file mode 100644 index 0000000..43e630f --- /dev/null +++ b/packages/strategies/CacheRevalidatePreloadFallback.js @@ -0,0 +1,150 @@ +/** + * Note: @ayco/astro-sw integration injects variables `__prefix`, `__version`, & `__assets` + * -- find usage in `astro.config.mjs` integrations + * @see https://ayco.io/n/@ayco/astro-sw + */ +const cacheName = `${__prefix ?? 'app'}-v${__version ?? '000'}` + +/** + * TODO: respect a log config property for logInfo + */ + +const cleanOldCaches = async () => { + const allowCacheNames = ['cozy-reader', cacheName]; + const allCaches = await caches.keys(); + allCaches.forEach(key => { + if (!allowCacheNames.includes(key)) { + // logInfo('Deleting old cache', {force: !!forceLogging, data: key, context: 'cozy-sw'}); + caches.delete(key); + } + }); +} + +const addResourcesToCache = async (resources) => { + const cache = await caches.open(cacheName); + // logInfo('adding resources to cache...', { force: !!forceLogging, context: 'cozy-sw', data: resources }) + await cache.addAll(resources); +}; + +const putInCache = async (request, response) => { + const cache = await caches.open(cacheName); + // logInfo('adding one response to cache...', { force: forceLogging, context: 'cozy-sw', data: request.url }) + // if exists, replace + + const keys = await cache.keys(); + if (keys.includes(request)) { + cache.delete(request); + } + + await cache.put(request, response); +}; + + +const cacheAndRevalidate = async ({ request, preloadResponsePromise, fallbackUrl }) => { + + const cache = await caches.open(cacheName); + + // Try get the resource from the cache + const responseFromCache = await cache.match(request); + if (responseFromCache) { + // logInfo('using cached response...', { force: forceLogging, context: 'cozy-sw', data: responseFromCache.url }) + // get network response for revalidation of cached assets + fetch(request.clone()).then((responseFromNetwork) => { + if (responseFromNetwork) { + // logInfo('fetched updated resource...', { force: forceLogging, context: 'cozy-sw', data: responseFromNetwork.url }) + putInCache(request, responseFromNetwork.clone()); + } + }).catch((error) => { + logError('failed to fetch updated resource', { force: forceLogging, context: 'cozy-sw', data: error }) + }); + + return responseFromCache; + } + + + + // Try to use the preloaded response, if it's there + // NOTE: Chrome throws errors regarding preloadResponse, see: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1420515 + // https://github.com/mdn/dom-examples/issues/145 + // To avoid those errors, remove or comment out this block of preloadResponse + // code along with enableNavigationPreload() and the "activate" listener. + const preloadResponse = await preloadResponsePromise; + if (preloadResponse) { + putInCache(request, preloadResponse.clone()); + // logInfo('using preload response', { force: forceLogging, context: 'cozy-sw', data: preloadResponse.url }) + return preloadResponse; + } + + try { + // Try to get the resource from the network for 5 seconds + const responseFromNetwork = await fetch(request.clone()); + // response may be used only once + // we need to save clone to put one copy in cache + // and serve second one + putInCache(request, responseFromNetwork.clone()); + // logInfo('using network response', { force: forceLogging, context: 'cozy-sw', data: responseFromNetwork.url }) + return responseFromNetwork; + + } catch (error) { + + // Try the fallback + const fallbackResponse = await cache.match(fallbackUrl); + if (fallbackResponse) { + // logInfo('using fallback cached response...', { force: forceLogging, context: 'cozy-sw', data: fallbackResponse.url }) + return fallbackResponse; + } + + // when even the fallback response is not available, + // there is nothing we can do, but we must always + // return a Response object + return new Response('Network error happened', { + status: 408, + headers: { 'Content-Type': 'text/plain' }, + }); + } +}; + +const enableNavigationPreload = async () => { + if (self.registration.navigationPreload) { + // Enable navigation preloads! + await self.registration.navigationPreload.enable(); + } +}; + +const activateFn = (event) => { + // logInfo('activating service worker...', { force: forceLogging, context: 'cozy-sw' }) + cleanOldCaches(); + event.waitUntil(enableNavigationPreload()); +}; + +const installFn = (event) => { + + // logInfo('installing service worker...', { force: forceLogging, context: 'cozy-sw' }) + self.skipWaiting(); // go straight to activate + + event.waitUntil( + addResourcesToCache(__assets ?? []) + ); +}; + +const fetchFn = (event) => { + // logInfo('fetch happened', {data: event}); + + event.respondWith( + cacheAndRevalidate({ + request: event.request, + preloadResponsePromise: event.preloadResponse, + fallbackUrl: './', + }) + ); +}; + +const waitFn = (event) => {} + +export default { + fetchFn, + installFn, + activateFn, + waitFn +} \ No newline at end of file