From 20a8f329402dac4cba53fec55bd059eeb7f7137d Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 14 May 2024 07:53:03 -0700 Subject: [PATCH] feat: cache sw assets with service worker (#234) * feat: cache sw assets with service worker * feat: cache index.html requests * feat: also cache cdn.jsdelivr assets * fix: ensure old swAssets cache is cleared on install * feat: include tachyons and ipfs-css in bundle * fix: less duplication of css imports * fix: dep-check on css imports --- .aegir.js | 4 +++ package-lock.json | 14 ++++++++- package.json | 4 ++- public/index.html | 2 -- src/app.css | 9 ------ src/pages/config.tsx | 1 + src/pages/default-page-styles.css | 2 ++ src/pages/helper-ui.tsx | 1 + src/pages/redirect-page.tsx | 1 + src/pages/redirects-interstitial.tsx | 2 +- src/sw.ts | 44 +++++++++++++++++++++++++--- 11 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 src/pages/default-page-styles.css diff --git a/.aegir.js b/.aegir.js index 9d08e54c..198e84d3 100644 --- a/.aegir.js +++ b/.aegir.js @@ -16,6 +16,10 @@ export default { // .jsx files aren't checked properly. 'react-dom', + // .css deps aren't checked properly. + 'ipfs-css', + 'tachyons', + // required by webpack 'webpack-cli', 'webpack-dev-server', diff --git a/package-lock.json b/package-lock.json index 716304ca..8b945f84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "@multiformats/dns": "^1.0.6", "@sgtpooki/file-type": "^1.0.1", "debug": "^4.3.4", + "ipfs-css": "^1.4.0", "multiformats": "^13.1.0", "react": "^18.3.0", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "tachyons": "^4.12.0" }, "devDependencies": { "@babel/core": "^7.24.3", @@ -14499,6 +14501,11 @@ "npm": ">=7.0.0" } }, + "node_modules/ipfs-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ipfs-css/-/ipfs-css-1.4.0.tgz", + "integrity": "sha512-LgwMSbxUQMR7DHwRd+esDT+k5M9vJkXCUyjyY0R745ol0n30QDgbHkGazJ3gT/wSFL5E157l0PKEVgFBJP6c4g==" + }, "node_modules/ipfs-unixfs": { "version": "11.1.4", "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-11.1.4.tgz", @@ -28775,6 +28782,11 @@ "node": ">=0.10.0" } }, + "node_modules/tachyons": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/tachyons/-/tachyons-4.12.0.tgz", + "integrity": "sha512-2nA2IrYFy3raCM9fxJ2KODRGHVSZNTW3BR0YnlGsLUf1DA3pk3YfWZ/DdfbnZK6zLZS+jUenlUGJsKcA5fUiZg==" + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/package.json b/package.json index a0d258f2..b8ebb754 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,11 @@ "@multiformats/dns": "^1.0.6", "@sgtpooki/file-type": "^1.0.1", "debug": "^4.3.4", + "ipfs-css": "^1.4.0", "multiformats": "^13.1.0", "react": "^18.3.0", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "tachyons": "^4.12.0" }, "devDependencies": { "@babel/core": "^7.24.3", diff --git a/public/index.html b/public/index.html index 12fed413..bc75ddd3 100644 --- a/public/index.html +++ b/public/index.html @@ -21,8 +21,6 @@ <%= htmlWebpackPlugin.options.title %> | <%= htmlWebpackPlugin.options.version %> - - diff --git a/src/app.css b/src/app.css index 621f9ff1..8c547e1a 100644 --- a/src/app.css +++ b/src/app.css @@ -33,15 +33,6 @@ form { letter-spacing: -12px; } -.terminal { - margin: 20px; - font-family: monospace; - font-size: 16px; - overflow: auto; - flex: 1; - word-break: break-word; -} - .cursor-disabled { cursor: not-allowed; } diff --git a/src/pages/config.tsx b/src/pages/config.tsx index d408ef40..d1a71f9d 100644 --- a/src/pages/config.tsx +++ b/src/pages/config.tsx @@ -10,6 +10,7 @@ import { HeliaServiceWorkerCommsChannel } from '../lib/channel.js' import { getConfig, loadConfigFromLocalStorage } from '../lib/config-db.js' import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.js' import { getUiComponentLogger, uiLogger } from '../lib/logger.js' +import './default-page-styles.css' const uiComponentLogger = getUiComponentLogger('config-page') const log = uiLogger.forComponent('config-page') diff --git a/src/pages/default-page-styles.css b/src/pages/default-page-styles.css new file mode 100644 index 00000000..d3a3ccc4 --- /dev/null +++ b/src/pages/default-page-styles.css @@ -0,0 +1,2 @@ +@import 'tachyons'; +@import 'ipfs-css'; diff --git a/src/pages/helper-ui.tsx b/src/pages/helper-ui.tsx index 5c0c8309..376e2a60 100644 --- a/src/pages/helper-ui.tsx +++ b/src/pages/helper-ui.tsx @@ -5,6 +5,7 @@ import CidRenderer from '../components/input-validator.jsx' import { ConfigProvider } from '../context/config-context.jsx' import { ServiceWorkerProvider } from '../context/service-worker-context.jsx' import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.js' +import './default-page-styles.css' function HelperUi (): React.JSX.Element { const [requestPath, setRequestPath] = useState(localStorage.getItem(LOCAL_STORAGE_KEYS.forms.requestPath) ?? '') diff --git a/src/pages/redirect-page.tsx b/src/pages/redirect-page.tsx index 9edddc34..0e599beb 100644 --- a/src/pages/redirect-page.tsx +++ b/src/pages/redirect-page.tsx @@ -8,6 +8,7 @@ import { getSubdomainParts } from '../lib/get-subdomain-parts.js' import { isConfigPage } from '../lib/is-config-page.js' import { getUiComponentLogger, uiLogger } from '../lib/logger.js' import { translateIpfsRedirectUrl } from '../lib/translate-ipfs-redirect-url.js' +import './default-page-styles.css' const uiComponentLogger = getUiComponentLogger('redirect-page') const log = uiLogger.forComponent('redirect-page') diff --git a/src/pages/redirects-interstitial.tsx b/src/pages/redirects-interstitial.tsx index 5bdbccee..cd983cae 100644 --- a/src/pages/redirects-interstitial.tsx +++ b/src/pages/redirects-interstitial.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react' import { findOriginIsolationRedirect } from '../lib/path-or-subdomain.js' import { translateIpfsRedirectUrl } from '../lib/translate-ipfs-redirect-url.js' -import RedirectPage from './redirect-page' +import RedirectPage from './redirect-page.jsx' /** * This page is only used to capture the ?helia-sw=/ip[fn]s/blah query parameter that diff --git a/src/sw.ts b/src/sw.ts index d514a819..4970d5f6 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -98,7 +98,8 @@ const log = swLogger.forComponent('main') const CACHE_VERSION = 1 const CURRENT_CACHES = Object.freeze({ mutable: `mutable-cache-v${CACHE_VERSION}`, - immutable: `immutable-cache-v${CACHE_VERSION}` + immutable: `immutable-cache-v${CACHE_VERSION}`, + swAssets: `sw-assets-v${CACHE_VERSION}` }) let verifiedFetch: VerifiedFetch const channel = new HeliaServiceWorkerCommsChannel('SW', swLogger) @@ -123,6 +124,7 @@ self.addEventListener('install', (event) => { // 👇 When a new version of the SW is installed, activate immediately void self.skipWaiting() event.waitUntil(addInstallTimestampToConfig()) + event.waitUntil(clearSwAssetCache()) }) self.addEventListener('activate', (event) => { @@ -199,8 +201,23 @@ async function requestRouting (event: FetchEvent, url: URL): Promise { } else if (isDeregisterRequest(event.request.url)) { event.waitUntil(deregister(event)) return false - } else if (isConfigPageRequest(url) || isSwAssetRequest(event)) { - log.trace('config page or sw-asset request, ignoring ', event.request.url) + } else if (isConfigPageRequest(url)) { + log.trace('config page request, ignoring ', event.request.url) + return false + } else if (isSwAssetRequest(event)) { + log.trace('sw-asset request, returning cached response ', event.request.url) + /** + * Return the asset from the cache if it exists, otherwise fetch it. + */ + event.respondWith(caches.open(CURRENT_CACHES.swAssets).then(async (cache) => { + const cachedResponse = await cache.match(event.request) + if (cachedResponse != null) { + return cachedResponse + } + const response = await fetch(event.request) + await cache.put(event.request, response.clone()) + return response + })) return false } else if (!isValidRequestForSW(event)) { log.trace('not a valid request for helia-sw, ignoring ', event.request.url) @@ -285,7 +302,12 @@ function isAggregateError (err: unknown): err is AggregateError { function isSwAssetRequest (event: FetchEvent): boolean { const isActualSwAsset = /^.+\/(?:ipfs-sw-).+$/.test(event.request.url) - return isActualSwAsset + // if path is not set, then it's a request for index.html which we should consider a sw asset + const url = new URL(event.request.url) + // but only if it's not a subdomain request (root index.html should not be returned for subdomains) + const isIndexHtmlRequest = url.pathname === '/' && !isSubdomainRequest(event) + + return isActualSwAsset || isIndexHtmlRequest } /** @@ -629,3 +651,17 @@ async function addInstallTimestampToConfig (): Promise { log.error('addInstallTimestampToConfig error: ', e) } } + +/** + * To be called on 'install' sw event. This will clear out the old swAssets cache, + * which is used for storing the service worker's css,js, and html assets. + */ +async function clearSwAssetCache (): Promise { + // clear out old swAssets cache + const cacheName = CURRENT_CACHES.swAssets + const cache = await caches.open(cacheName) + const keys = await cache.keys() + for (const request of keys) { + await cache.delete(request) + } +}