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)
+ }
+}