From b29048f32046062b072e054a70f6328373c86954 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:19:17 -0800 Subject: [PATCH 01/25] feat: create config page for sw settings --- src/app.tsx | 77 +++----------------------- src/components/Header.tsx | 10 +++- src/components/config.tsx | 55 ++++++++++++++++++ src/components/local-storage-input.tsx | 54 ++++++++++++++++++ src/context/config-context.tsx | 16 ++++++ src/gear-icon.svg | 1 + src/helper-ui.tsx | 36 ++++++++++++ src/index.tsx | 6 +- src/lib/channel.ts | 27 +++++---- src/lib/common.ts | 10 +--- src/lib/config-db.ts | 2 +- src/lib/local-storage.ts | 11 ++++ src/sw.ts | 20 +++++-- 13 files changed, 229 insertions(+), 96 deletions(-) create mode 100644 src/components/config.tsx create mode 100644 src/components/local-storage-input.tsx create mode 100644 src/context/config-context.tsx create mode 100644 src/gear-icon.svg create mode 100644 src/helper-ui.tsx diff --git a/src/app.tsx b/src/app.tsx index 5bbb4112..ac4fcc2a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,77 +1,16 @@ -import React, { useState, useEffect } from 'react' -import CidRenderer from './components/CidRenderer' -import Form from './components/Form.tsx' -import Header from './components/Header.tsx' -import { HeliaServiceWorkerCommsChannel } from './lib/channel.ts' -import { ChannelActions, COLORS } from './lib/common.ts' -import { getLocalStorageKey } from './lib/local-storage.ts' -import type { OutputLine } from './components/types.ts' - -const channel = new HeliaServiceWorkerCommsChannel('WINDOW') +import React, { useContext } from 'react' +import Config from './components/config.tsx' +import { ConfigContext } from './context/config-context.tsx' +import HelperUi from './helper-ui.tsx' function App (): JSX.Element { - const [, setOutput] = useState([]) - const [requestPath, setRequestPath] = useState(localStorage.getItem(getLocalStorageKey('forms', 'requestPath')) ?? '') - - useEffect(() => { - localStorage.setItem(getLocalStorageKey('forms', 'requestPath'), requestPath) - }, [requestPath]) + const { isConfigExpanded } = useContext(ConfigContext) - const showStatus = (text: OutputLine['content'], color: OutputLine['color'] = COLORS.default, id: OutputLine['id'] = ''): void => { - setOutput((prev: OutputLine[]) => { - return [...prev, - { - content: text, - color, - id - } - ] - }) + if (isConfigExpanded) { + return () } - - const handleSubmit = async (e): Promise => { - e.preventDefault() - } - - useEffect(() => { - const onMsg = (event): void => { - const { data } = event - // eslint-disable-next-line no-console - console.log('received message:', data) - switch (data.action) { - case ChannelActions.SHOW_STATUS: - if (data.data.text.trim() !== '') { - showStatus(`${data.source}: ${data.data.text}`, data.data.color, data.data.id) - } else { - showStatus('', data.data.color, data.data.id) - } - break - default: - // eslint-disable-next-line no-console - console.log(`SW action ${data.action} NOT_IMPLEMENTED yet...`) - } - } - channel.onmessage(onMsg) - }, [channel]) - return ( - <> -
- -
-

Fetch content from IPFS using Helia in a SW

-
- -
- -
- -
- + ) } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index e5da5954..f49070ab 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,12 +1,20 @@ -import React from 'react' +import React, { useContext } from 'react' +import { ConfigContext } from '../context/config-context.tsx' +import gearIcon from '../gear-icon.svg' import ipfsLogo from '../ipfs-logo.svg' export default function Header (): JSX.Element { + const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext) + return (
IPFS logo + +
) } diff --git a/src/components/config.tsx b/src/components/config.tsx new file mode 100644 index 00000000..96f289aa --- /dev/null +++ b/src/components/config.tsx @@ -0,0 +1,55 @@ +import React, { useCallback, useContext, useState } from 'react' +import { ConfigContext } from '../context/config-context.tsx' +import { HeliaServiceWorkerCommsChannel } from '../lib/channel.ts' +import { loadConfigFromLocalStorage } from '../lib/config-db.ts' +import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.ts' +import LocalStorageInput from './local-storage-input.tsx' + +const channel = new HeliaServiceWorkerCommsChannel('WINDOW') + +export default (): JSX.Element | null => { + const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext) + + if (!isConfigExpanded) { + return null + } + + const [error, setError] = useState(null) + const urlValidationFn = (value: string): Error | null => { + try { + const urls = JSON.parse(value) satisfies string[] + let i = 0 + try { + urls.map((url, index) => { + i = index + return new URL(url) + }) + } catch (e) { + throw new Error(`URL "${urls[i]}" at index ${i} is not valid`) + } + return null + } catch (err) { + return err as Error + } + } + + const saveConfig = useCallback(async () => { + try { + await loadConfigFromLocalStorage() + channel.postMessage({ target: 'SW', action: 'RELOAD_CONFIG' }) + setConfigExpanded(false) + } catch (err) { + setError(err as Error) + } + }, []) + + return ( +
+ + + + + {error != null && {error.message}} +
+ ) +} diff --git a/src/components/local-storage-input.tsx b/src/components/local-storage-input.tsx new file mode 100644 index 00000000..6973881d --- /dev/null +++ b/src/components/local-storage-input.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react' + +export interface LocalStorageInputProps { + localStorageKey: string + label: string + placeholder?: string + validationFn?(value: string): Error | null +} + +const defaultValidationFunction = (value: string): Error | null => { + try { + JSON.parse(value) + return null + } catch (err) { + return err as Error + } +} +export default ({ localStorageKey, label, placeholder, validationFn }: LocalStorageInputProps): JSX.Element => { + const [value, setValue] = useState(localStorage.getItem(localStorageKey) ?? '[]') + const [error, setError] = useState(null) + + if (validationFn == null) { + validationFn = defaultValidationFunction + } + + useEffect(() => { + try { + const err = validationFn?.(value) + if (err != null) { + throw err + } + localStorage.setItem(localStorageKey, value) + setError(null) + } catch (err) { + setError(err as Error) + } + }, [value]) + + return ( + <> + + { setValue(e.target.value) }} + /> + {error != null && {error.message}} + + ) +} diff --git a/src/context/config-context.tsx b/src/context/config-context.tsx new file mode 100644 index 00000000..4724179f --- /dev/null +++ b/src/context/config-context.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +export const ConfigContext = React.createContext({ + isConfigExpanded: false, + setConfigExpanded: (value: boolean) => {} +}) + +export const ConfigProvider = ({ children }): JSX.Element => { + const [isConfigExpanded, setConfigExpanded] = React.useState(false) + + return ( + + {children} + + ) +} diff --git a/src/gear-icon.svg b/src/gear-icon.svg new file mode 100644 index 00000000..70b19fe1 --- /dev/null +++ b/src/gear-icon.svg @@ -0,0 +1 @@ + diff --git a/src/helper-ui.tsx b/src/helper-ui.tsx new file mode 100644 index 00000000..2b004269 --- /dev/null +++ b/src/helper-ui.tsx @@ -0,0 +1,36 @@ +import React, { useState, useEffect } from 'react' +import CidRenderer from './components/CidRenderer.tsx' +import Form from './components/Form.tsx' +import Header from './components/Header.tsx' +import { LOCAL_STORAGE_KEYS } from './lib/local-storage.ts' + +export default function (): JSX.Element { + const [requestPath, setRequestPath] = useState(localStorage.getItem(LOCAL_STORAGE_KEYS.forms.requestPath) ?? '') + + useEffect(() => { + localStorage.setItem(LOCAL_STORAGE_KEYS.forms.requestPath, requestPath) + }, [requestPath]) + + const handleSubmit = async (e): Promise => { + e.preventDefault() + } + + return ( + <> +
+
+

Fetch content from IPFS using Helia in a SW

+ + +
+ +
+ +
+ + ) +} diff --git a/src/index.tsx b/src/index.tsx index 1c08d80a..6109758c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,6 +2,7 @@ import React from 'react' import ReactDOMClient from 'react-dom/client' import './app.css' import App from './app.tsx' +import { ConfigProvider } from './context/config-context.tsx' import { loadConfigFromLocalStorage } from './lib/config-db.ts' import { isPathOrSubdomainRequest } from './lib/path-or-subdomain.ts' import RedirectPage from './redirectPage.tsx' @@ -11,6 +12,7 @@ await loadConfigFromLocalStorage() * You can change the BASE_URL when deploying this app to a different domain. */ const BASE_URL = process.env.BASE_URL ?? 'helia-sw-gateway.localhost' +// const BASE_URL = 'sw.sgtpooki.com' const container = document.getElementById('root') @@ -32,7 +34,9 @@ if (isPathOrSubdomainRequest(BASE_URL, window.location)) { } else { root.render( - + + + ) } diff --git a/src/lib/channel.ts b/src/lib/channel.ts index c130c006..ee8d229d 100644 --- a/src/lib/channel.ts +++ b/src/lib/channel.ts @@ -26,8 +26,8 @@ type NotSourceUser = T extends ChannelUsers export interface ChannelMessage> { source: Source target?: ChannelUserValues - action: ChannelActions | keyof typeof ChannelActions | 'TEST' - data: Data + action: keyof typeof ChannelActions + data?: Data } export class HeliaServiceWorkerCommsChannel { @@ -70,15 +70,12 @@ export class HeliaServiceWorkerCommsChannel>): void => { this.log('onMsgHandler: ', e) if (e.data.source !== source) { return } - if (e.data.action === 'PING') { - this.postMessage({ action: 'PONG', data: e.data.data }) - return - } void cb(e) } this.channel.addEventListener('message', onMsgHandler) @@ -99,10 +96,7 @@ export class HeliaServiceWorkerCommsChannel(cb: (e: MessageEvent>) => void | Promise): void { + if (!this.canListen()) { + throw new Error('Cannot use onmessageOnce on EMITTER_ONLY channel') + } + const onMsgHandler = (e: MessageEvent>): void => { + this.log('onMsgHandler: ', e) + this.channel.removeEventListener('message', onMsgHandler) + void cb(e) + } + this.channel.addEventListener('message', onMsgHandler) + } + async messageAndWaitForResponse, MSendType = unknown, MReceiveType = unknown>(responseSource: ResponseSource, data: Omit, 'source'>): Promise { if (!this.canListen()) { throw new Error('Cannot use messageAndWaitForResponse on EMITTER_ONLY channel') @@ -123,7 +129,6 @@ export class HeliaServiceWorkerCommsChannel { export async function loadConfigFromLocalStorage (): Promise { if (typeof globalThis.localStorage !== 'undefined') { const db = await openDatabase() - const localStorage = global.localStorage + const localStorage = globalThis.localStorage const localStorageGatewaysString = localStorage.getItem(getLocalStorageKey('config', 'gateways')) ?? '[]' const localStorageRoutersString = localStorage.getItem(getLocalStorageKey('config', 'routers')) ?? '[]' const gateways = JSON.parse(localStorageGatewaysString) diff --git a/src/lib/local-storage.ts b/src/lib/local-storage.ts index fe7f6151..b30ae6c9 100644 --- a/src/lib/local-storage.ts +++ b/src/lib/local-storage.ts @@ -1,4 +1,15 @@ export type LocalStorageRoots = 'config' | 'forms' + export function getLocalStorageKey (root: LocalStorageRoots, key: string): string { return `helia-service-worker-gateway.${root}.${key}` } + +export const LOCAL_STORAGE_KEYS = { + config: { + gateways: getLocalStorageKey('config', 'gateways'), + routers: getLocalStorageKey('config', 'routers') + }, + forms: { + requestPath: getLocalStorageKey('forms', 'requestPath') + } +} diff --git a/src/sw.ts b/src/sw.ts index 46789685..d4398b5e 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -2,6 +2,7 @@ // import { clientsClaim } from 'workbox-core' import mime from 'mime-types' import { getHelia } from './get-helia.ts' +import { HeliaServiceWorkerCommsChannel, type ChannelMessage, type ChannelUsers } from './lib/channel.ts' import { heliaFetch } from './lib/heliaFetch.ts' import type { Helia } from '@helia/interface' @@ -9,12 +10,24 @@ declare let self: ServiceWorkerGlobalScope let helia: Helia self.addEventListener('install', () => { - console.log('sw installing') void self.skipWaiting() }) +const channel = new HeliaServiceWorkerCommsChannel('SW') + self.addEventListener('activate', () => { - console.log('sw activating') + channel.onmessagefrom('WINDOW', async (message: MessageEvent>) => { + const { action } = message.data + switch (action) { + case 'RELOAD_CONFIG': + void getHelia().then((newHelia) => { + helia = newHelia + }) + break + default: + console.log('unknown action: ', action) + } + }) }) /** @@ -39,8 +52,6 @@ const fetchHandler = async ({ path, request }: FetchHandlerArg): Promise { function getSubdomainParts (request: Request): { origin: string | null, protocol: string | null } { const BASE_URL = 'helia-sw-gateway.localhost' + // const BASE_URL = 'sw.sgtpooki.com' const urlString = request.url const url = new URL(urlString) const subdomain = url.hostname.replace(`.${BASE_URL}`, '') From 53001c69f022a5dea3ee0a6140f5183cf1a849be Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:22:00 -0800 Subject: [PATCH 02/25] Update src/lib/channel.ts --- src/lib/channel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/channel.ts b/src/lib/channel.ts index ee8d229d..4f3efbcd 100644 --- a/src/lib/channel.ts +++ b/src/lib/channel.ts @@ -70,7 +70,6 @@ export class HeliaServiceWorkerCommsChannel>): void => { this.log('onMsgHandler: ', e) if (e.data.source !== source) { From 7c6410f777ea0f06f550cb3bf0cec6f7e810356b Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:22:06 -0800 Subject: [PATCH 03/25] Update src/sw.ts --- src/sw.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sw.ts b/src/sw.ts index d4398b5e..479a98a6 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -103,7 +103,6 @@ const isRootRequestForContent = (event: FetchEvent): boolean => { function getSubdomainParts (request: Request): { origin: string | null, protocol: string | null } { const BASE_URL = 'helia-sw-gateway.localhost' - // const BASE_URL = 'sw.sgtpooki.com' const urlString = request.url const url = new URL(urlString) const subdomain = url.hostname.replace(`.${BASE_URL}`, '') From eca11146e2c51c5768d15f85dc3687bb928a329e Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:22:12 -0800 Subject: [PATCH 04/25] Update src/lib/channel.ts --- src/lib/channel.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/lib/channel.ts b/src/lib/channel.ts index 4f3efbcd..8b89bcb2 100644 --- a/src/lib/channel.ts +++ b/src/lib/channel.ts @@ -104,17 +104,6 @@ export class HeliaServiceWorkerCommsChannel(cb: (e: MessageEvent>) => void | Promise): void { - if (!this.canListen()) { - throw new Error('Cannot use onmessageOnce on EMITTER_ONLY channel') - } - const onMsgHandler = (e: MessageEvent>): void => { - this.log('onMsgHandler: ', e) - this.channel.removeEventListener('message', onMsgHandler) - void cb(e) - } - this.channel.addEventListener('message', onMsgHandler) - } async messageAndWaitForResponse, MSendType = unknown, MReceiveType = unknown>(responseSource: ResponseSource, data: Omit, 'source'>): Promise { if (!this.canListen()) { From 0d5f91f7616610f8021250224313b129c458f95d Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:22:16 -0800 Subject: [PATCH 05/25] Update src/index.tsx --- src/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 6109758c..e44df412 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,7 +12,6 @@ await loadConfigFromLocalStorage() * You can change the BASE_URL when deploying this app to a different domain. */ const BASE_URL = process.env.BASE_URL ?? 'helia-sw-gateway.localhost' -// const BASE_URL = 'sw.sgtpooki.com' const container = document.getElementById('root') From 764ebbce277dc55c40cf8fc8e2f68288f188c728 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 20 Feb 2024 19:24:29 -0800 Subject: [PATCH 06/25] chore: fix build --- src/lib/channel.ts | 1 - src/sw.ts | 22 ++++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/lib/channel.ts b/src/lib/channel.ts index 8b89bcb2..caacf171 100644 --- a/src/lib/channel.ts +++ b/src/lib/channel.ts @@ -104,7 +104,6 @@ export class HeliaServiceWorkerCommsChannel, MSendType = unknown, MReceiveType = unknown>(responseSource: ResponseSource, data: Omit, 'source'>): Promise { if (!this.canListen()) { throw new Error('Cannot use messageAndWaitForResponse on EMITTER_ONLY channel') diff --git a/src/sw.ts b/src/sw.ts index 479a98a6..76efa829 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -1,8 +1,6 @@ -/* eslint-disable @typescript-eslint/strict-boolean-expressions, no-console */ -// import { clientsClaim } from 'workbox-core' import mime from 'mime-types' import { getHelia } from './get-helia.ts' -import { HeliaServiceWorkerCommsChannel, type ChannelMessage, type ChannelUsers } from './lib/channel.ts' +import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/channel.ts' import { heliaFetch } from './lib/heliaFetch.ts' import type { Helia } from '@helia/interface' @@ -25,6 +23,7 @@ self.addEventListener('activate', () => { }) break default: + // eslint-disable-next-line no-console console.log('unknown action: ', action) } }) @@ -66,13 +65,16 @@ const fetchHandler = async ({ path, request }: FetchHandlerArg): Promise { const request = event.request const urlString = request.url const url = new URL(urlString) + // eslint-disable-next-line no-console console.log('helia-sw: urlString: ', urlString) if (urlString.includes('?helia-sw-subdomain')) { + // eslint-disable-next-line no-console console.log('helia-sw: subdomain request: ', urlString) // subdomain request where URL has .ip[fn]s and any paths should be appended to the url // const subdomain = url.searchParams.get('helia-sw-subdomain') @@ -140,13 +142,15 @@ self.addEventListener('fetch', event => { return } if (!isValidRequestForSW(event)) { + // eslint-disable-next-line no-console console.warn('helia-sw: not a valid request for helia-sw, ignoring ', urlString) return } - // console.log('request: ', request) - // console.log('self.location.origin: ', self.location.origin) + + // eslint-disable-next-line no-console console.log('helia-sw: intercepting request to ', urlString) if (isReferrerPreviouslyIntercepted(event)) { + // eslint-disable-next-line no-console console.log('helia-sw: referred from ', request.referrer) const destinationParts = urlString.split('/') const referrerParts = request.referrer.split('/') @@ -168,10 +172,11 @@ self.addEventListener('fetch', event => { * respond with redirect to newUrl */ if (newUrl.toString() !== urlString) { + // eslint-disable-next-line no-console console.log('helia-sw: rerouting request to: ', newUrl.toString()) const redirectHeaders = new Headers() redirectHeaders.set('Location', newUrl.toString()) - if (mime.lookup(newUrl.toString())) { + if (mime.lookup(newUrl.toString()) != null) { redirectHeaders.set('Content-Type', mime.lookup(newUrl.toString())) } redirectHeaders.set('X-helia-sw', 'redirected') @@ -181,6 +186,7 @@ self.addEventListener('fetch', event => { }) event.respondWith(redirectResponse) } else { + // eslint-disable-next-line no-console console.log('helia-sw: not rerouting request to same url: ', newUrl.toString()) event.respondWith(fetchHandler({ path: url.pathname, request })) From f80b2ed8d597d9ec16a25d5db40d2d986ff9df7f Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:18:06 -0800 Subject: [PATCH 07/25] chore: change gear color --- src/components/Header.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index f49070ab..768e6203 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -13,7 +13,8 @@ export default function Header (): JSX.Element {
) From 7c988ad4e38ecc1921cd54041c1781dd0e882454 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:40:15 -0800 Subject: [PATCH 08/25] feat: service worker config is shared to subdomains --- src/app.css | 4 ++ src/app.tsx | 12 +++- src/components/CidRenderer.tsx | 3 +- src/components/config.tsx | 79 +++++++++++++++++++------- src/context/config-context.tsx | 17 ++++-- src/context/service-worker-context.tsx | 27 +++++++++ src/get-helia.ts | 2 + src/index.tsx | 47 ++++++++------- src/lib/channel.ts | 17 +----- src/lib/config-db.ts | 7 +++ src/lib/heliaFetch.ts | 3 - src/redirectPage.tsx | 38 ++++++++++++- src/service-worker-utils.ts | 14 +++++ src/sw.ts | 14 ----- 14 files changed, 201 insertions(+), 83 deletions(-) create mode 100644 src/context/service-worker-context.tsx create mode 100644 src/service-worker-utils.ts diff --git a/src/app.css b/src/app.css index e37a6ec8..621f9ff1 100644 --- a/src/app.css +++ b/src/app.css @@ -41,3 +41,7 @@ form { flex: 1; word-break: break-word; } + +.cursor-disabled { + cursor: not-allowed; +} diff --git a/src/app.tsx b/src/app.tsx index ac4fcc2a..1a24db39 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,10 +1,18 @@ -import React, { useContext } from 'react' +import React, { useContext, useEffect } from 'react' import Config from './components/config.tsx' import { ConfigContext } from './context/config-context.tsx' import HelperUi from './helper-ui.tsx' +import { registerServiceWorker } from './service-worker-utils' function App (): JSX.Element { - const { isConfigExpanded } = useContext(ConfigContext) + const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext) + if (window.location.pathname === '/config') { + setConfigExpanded(true) + } + + useEffect(() => { + void registerServiceWorker() + }, []) if (isConfigExpanded) { return () diff --git a/src/components/CidRenderer.tsx b/src/components/CidRenderer.tsx index 118a2fc4..a01b4e15 100644 --- a/src/components/CidRenderer.tsx +++ b/src/components/CidRenderer.tsx @@ -106,8 +106,7 @@ export default function CidRenderer ({ requestPath }: { requestPath: string }): setAbortController(newAbortController) setLastFetchPath(swPath) setIsLoading(true) - // eslint-disable-next-line no-console - console.log(`fetching '${swPath}' from service worker`) + const res = await fetch(swPath, { signal: newAbortController.signal, method: 'GET', diff --git a/src/components/config.tsx b/src/components/config.tsx index 96f289aa..62fbdd4e 100644 --- a/src/components/config.tsx +++ b/src/components/config.tsx @@ -1,53 +1,88 @@ import React, { useCallback, useContext, useState } from 'react' import { ConfigContext } from '../context/config-context.tsx' +import { ServiceWorkerContext } from '../context/service-worker-context.tsx' import { HeliaServiceWorkerCommsChannel } from '../lib/channel.ts' -import { loadConfigFromLocalStorage } from '../lib/config-db.ts' +import { getConfig, loadConfigFromLocalStorage } from '../lib/config-db.ts' import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.ts' import LocalStorageInput from './local-storage-input.tsx' const channel = new HeliaServiceWorkerCommsChannel('WINDOW') -export default (): JSX.Element | null => { - const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext) - - if (!isConfigExpanded) { +const urlValidationFn = (value: string): Error | null => { + try { + const urls = JSON.parse(value) satisfies string[] + let i = 0 + try { + urls.map((url, index) => { + i = index + return new URL(url) + }) + } catch (e) { + throw new Error(`URL "${urls[i]}" at index ${i} is not valid`) + } return null + } catch (err) { + return err as Error } +} + +export default (): JSX.Element | null => { + const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext) + const { isServiceWorkerRegistered } = React.useContext(ServiceWorkerContext) const [error, setError] = useState(null) - const urlValidationFn = (value: string): Error | null => { - try { - const urls = JSON.parse(value) satisfies string[] - let i = 0 - try { - urls.map((url, index) => { - i = index - return new URL(url) - }) - } catch (e) { - throw new Error(`URL "${urls[i]}" at index ${i} is not valid`) - } - return null - } catch (err) { - return err as Error + + const isLoadedInIframe = window.self !== window.top + + const postFromIframeToParentSw = useCallback(async () => { + if (!isLoadedInIframe) { + return } - } + // we get the iframe origin from a query parameter called 'origin', if this is loaded in an iframe + const targetOrigin = decodeURIComponent(window.location.search.split('origin=')[1]) + const config = await getConfig() + + /** + * The reload page in the parent window is listening for this message, and then it passes a RELOAD_CONFIG message to the service worker + */ + window.parent?.postMessage({ source: 'helia-sw-config-iframe', target: 'PARENT', action: 'RELOAD_CONFIG', config }, { + targetOrigin + }) + }, []) const saveConfig = useCallback(async () => { try { await loadConfigFromLocalStorage() + // update the BASE_URL service worker channel.postMessage({ target: 'SW', action: 'RELOAD_CONFIG' }) + // update the ..BASE_URL service worker + await postFromIframeToParentSw() setConfigExpanded(false) } catch (err) { setError(err as Error) } }, []) + if (!isConfigExpanded) { + return null + } + let saveDisabled = true + const buttonClasses = new Set(['button-reset', 'pv3', 'tc', 'bn', 'white', 'w-100', 'cursor-disabled', 'bg-gray']) + if (isServiceWorkerRegistered) { + saveDisabled = false + buttonClasses.delete('bg-gray') + buttonClasses.delete('cursor-disabled') + buttonClasses.add('bg-animate') + buttonClasses.add('bg-black-80') + buttonClasses.add('hover-bg-aqua') + buttonClasses.add('pointer') + } + return (
- + {error != null && {error.message}}
diff --git a/src/context/config-context.tsx b/src/context/config-context.tsx index 4724179f..4c7b8f41 100644 --- a/src/context/config-context.tsx +++ b/src/context/config-context.tsx @@ -1,15 +1,24 @@ import React from 'react' +const isLoadedInIframe = window.self !== window.top export const ConfigContext = React.createContext({ - isConfigExpanded: false, + isConfigExpanded: isLoadedInIframe, setConfigExpanded: (value: boolean) => {} }) -export const ConfigProvider = ({ children }): JSX.Element => { - const [isConfigExpanded, setConfigExpanded] = React.useState(false) +export const ConfigProvider = ({ children, expanded = isLoadedInIframe }: { children: JSX.Element[] | JSX.Element, expanded?: boolean }): JSX.Element => { + const [isConfigExpanded, setConfigExpanded] = React.useState(expanded) + + const setConfigExpandedWrapped = (value: boolean): void => { + if (isLoadedInIframe) { + // ignore it + } else { + setConfigExpanded(value) + } + } return ( - + {children} ) diff --git a/src/context/service-worker-context.tsx b/src/context/service-worker-context.tsx new file mode 100644 index 00000000..add8e5aa --- /dev/null +++ b/src/context/service-worker-context.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { registerServiceWorker } from '../service-worker-utils' + +export const ServiceWorkerContext = React.createContext({ + isServiceWorkerRegistered: false +}) + +export const ServiceWorkerProvider = ({ children }): JSX.Element => { + const [isServiceWorkerRegistered, setIsServiceWorkerRegistered] = React.useState(false) + + React.useEffect(() => { + if (isServiceWorkerRegistered) { + return + } + void registerServiceWorker().then(() => { + // eslint-disable-next-line no-console + console.log('config-debug: service worker registered', self.location.origin) + setIsServiceWorkerRegistered(true) + }) + }, []) + + return ( + + {children} + + ) +} diff --git a/src/get-helia.ts b/src/get-helia.ts index 27fe523d..c191fcbf 100644 --- a/src/get-helia.ts +++ b/src/get-helia.ts @@ -8,6 +8,8 @@ import type { Helia } from '@helia/interface' export async function getHelia (): Promise { const config = await getConfig() + // eslint-disable-next-line no-console + console.log(`config-debug: got config for sw location ${self.location.origin}`, config) const blockstore = new IDBBlockstore('./helia-sw/blockstore') const datastore = new IDBDatastore('./helia-sw/datastore') await blockstore.open() diff --git a/src/index.tsx b/src/index.tsx index c5e2d2f1..b4907838 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,9 @@ import React from 'react' import ReactDOMClient from 'react-dom/client' import './app.css' import App from './app.tsx' +import Config from './components/config.tsx' import { ConfigProvider } from './context/config-context.tsx' +import { ServiceWorkerProvider } from './context/service-worker-context.tsx' import { loadConfigFromLocalStorage } from './lib/config-db.ts' import { isPathOrSubdomainRequest } from './lib/path-or-subdomain.ts' import { BASE_URL } from './lib/webpack-constants.ts' @@ -10,33 +12,40 @@ import RedirectPage from './redirectPage.tsx' await loadConfigFromLocalStorage() +// SW did not trigger for this request const container = document.getElementById('root') -const sw = await navigator.serviceWorker.register(new URL('sw.ts', import.meta.url)) const root = ReactDOMClient.createRoot(container) -// SW did not trigger for this request -if (isPathOrSubdomainRequest(BASE_URL, window.location)) { +if (window.location.pathname === '/config') { + root.render( + + + + + + + + ) +} else if (isPathOrSubdomainRequest(BASE_URL, window.location)) { // but the requested path is something it should, so show redirect and redirect to the same URL root.render( - + + + + + + + ) - window.location.replace(window.location.href) } else { - // the requested path is not recognized as a path or subdomain request, so render the app UI - if (window.location.pathname !== '/') { - // pathname is not blank, but is invalid. redirect to the root - window.location.replace('/') - } else { - root.render( - + root.render( + + - + - - ) - } + + + ) } - -// always update the service worker -void sw.update() diff --git a/src/lib/channel.ts b/src/lib/channel.ts index caacf171..57a08a05 100644 --- a/src/lib/channel.ts +++ b/src/lib/channel.ts @@ -34,27 +34,14 @@ export class HeliaServiceWorkerCommsChannel { - this.error('onmessageerror', e) + // eslint-disable-next-line no-console + console.error('onmessageerror', e) } } - log (...args: unknown[]): void { - if (!this.debug) return - // eslint-disable-next-line no-console - console.log(`HeliaServiceWorkerCommsChannel(${this.source}): `, ...args) - } - - error (...args: unknown[]): void { - if (!this.debug) return - // eslint-disable-next-line no-console - console.error(`HeliaServiceWorkerCommsChannel(${this.source}): `, ...args) - } - canListen (): boolean { return this.source !== 'EMITTER_ONLY' } diff --git a/src/lib/config-db.ts b/src/lib/config-db.ts index 7ed57086..30ea7adc 100644 --- a/src/lib/config-db.ts +++ b/src/lib/config-db.ts @@ -61,6 +61,13 @@ export async function loadConfigFromLocalStorage (): Promise { } } +export async function setConfig (config: ConfigDb): Promise { + const db = await openDatabase() + await setInDatabase(db, 'gateways', config.gateways) + await setInDatabase(db, 'routers', config.routers) + await closeDatabase(db) +} + export async function getConfig (): Promise { const db = await openDatabase() diff --git a/src/lib/heliaFetch.ts b/src/lib/heliaFetch.ts index e1067ce2..4b20f83b 100644 --- a/src/lib/heliaFetch.ts +++ b/src/lib/heliaFetch.ts @@ -15,9 +15,6 @@ export interface HeliaFetchOptions { // default from verified-fetch is application/octect-stream, which forces a download. This is not what we want for MANY file types. const defaultMimeType = 'text/html' const contentTypeParser: ContentTypeParser = async (bytes, fileName) => { - // eslint-disable-next-line no-console - console.log('bytes received in contentTypeParser for ', fileName, ' : ', bytes.slice(0, 10), '...') - const detectedType = (await fileTypeFromBuffer(bytes))?.mime if (detectedType != null) { return detectedType diff --git a/src/redirectPage.tsx b/src/redirectPage.tsx index d696a457..722af10b 100644 --- a/src/redirectPage.tsx +++ b/src/redirectPage.tsx @@ -1,9 +1,43 @@ -import React from 'react' +import React, { useEffect } from 'react' +import { HeliaServiceWorkerCommsChannel } from './lib/channel.ts' +import { setConfig } from './lib/config-db.ts' +import { BASE_URL } from './lib/webpack-constants.ts' + +const ConfigIframe = (): JSX.Element => { + const iframeSrc = `${window.location.protocol}//${BASE_URL}/config?origin=${encodeURIComponent(window.location.origin)}` + + return ( +