Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create config page for sw settings #24

Merged
merged 29 commits into from
Feb 27, 2024
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b29048f
feat: create config page for sw settings
SgtPooki Feb 21, 2024
53001c6
Update src/lib/channel.ts
SgtPooki Feb 21, 2024
7c6410f
Update src/sw.ts
SgtPooki Feb 21, 2024
eca1114
Update src/lib/channel.ts
SgtPooki Feb 21, 2024
0d5f91f
Update src/index.tsx
SgtPooki Feb 21, 2024
764ebbc
chore: fix build
SgtPooki Feb 21, 2024
527cc4b
Merge branch 'main' into feat/config-page
SgtPooki Feb 22, 2024
f80b2ed
chore: change gear color
SgtPooki Feb 22, 2024
7c988ad
feat: service worker config is shared to subdomains
SgtPooki Feb 22, 2024
3e73d05
fix: test running
SgtPooki Feb 22, 2024
2c0cd88
fix: service worker registration
SgtPooki Feb 22, 2024
658c41d
chore: remove calls to removed to commsChannel methods
SgtPooki Feb 22, 2024
535fc7e
chore: use LOCAL_STORAGE_KEYS
SgtPooki Feb 22, 2024
c588d2a
feat: config page auto reload works
SgtPooki Feb 22, 2024
d493df7
chore: import react functions directly
SgtPooki Feb 22, 2024
f50182d
chore: use latest verified-fetch
SgtPooki Feb 22, 2024
732f9a4
chore: remove console.logs and cleanup
SgtPooki Feb 23, 2024
ff7bafe
chore: consolidate app logic
SgtPooki Feb 23, 2024
b85f323
feat: users can control debugging output
SgtPooki Feb 23, 2024
8068f14
chore: todo determinism
SgtPooki Feb 23, 2024
ffe08c1
fix: gateway & routers default value
SgtPooki Feb 23, 2024
d078e2d
fix: bug parsing ipfs namespaced subdomains
SgtPooki Feb 23, 2024
9304b9f
chore: comment
SgtPooki Feb 23, 2024
78ebfc6
fix: use configured gateways & routers prior to defaults
SgtPooki Feb 23, 2024
56485a5
Merge branch 'main' into feat/config-page
SgtPooki Feb 23, 2024
d4aa1d0
Merge branch 'main' into feat/config-page
SgtPooki Feb 24, 2024
78ec8a3
Merge branch 'main' into feat/config-page
SgtPooki Feb 27, 2024
21c2e7b
feat: config collapsed, reload button, sw-ready-btn
SgtPooki Feb 27, 2024
4fcc269
chore: remove unused in config.tsx
SgtPooki Feb 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
604 changes: 257 additions & 347 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
"serve": "webpack serve --mode=development",
"serve:prod": "webpack serve --mode=production",
"start": "npm run serve",
"test": "npm run test:node",
"test:node": "webpack --env test && npx mocha test-build/tests.js",
"postinstall": "patch-package"
},
@@ -35,10 +36,12 @@
"@helia/http": "^1.0.0",
"@helia/interface": "^4.0.0",
"@helia/routers": "^1.0.0",
"@helia/verified-fetch": "^0.0.0-3283a5c",
"@helia/verified-fetch": "^0.0.0-28d62f7",
"@libp2p/logger": "^4.0.6",
"@sgtpooki/file-type": "^1.0.1",
"blockstore-idb": "^1.1.8",
"datastore-idb": "^2.1.8",
"debug": "^4.3.4",
"mime-types": "^2.1.35",
"multiformats": "^11.0.2",
"react": "^18.2.0",
4 changes: 4 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
@@ -41,3 +41,7 @@ form {
flex: 1;
word-break: break-word;
}

.cursor-disabled {
cursor: not-allowed;
}
85 changes: 18 additions & 67 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,28 @@
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'
import { isPathOrSubdomainRequest } from './lib/path-or-subdomain.ts'
import RedirectPage from './redirectPage.tsx'

function App (): JSX.Element {
const [, setOutput] = useState<OutputLine[]>([])
const [requestPath, setRequestPath] = useState(localStorage.getItem(getLocalStorageKey('forms', 'requestPath')) ?? '')

useEffect(() => {
localStorage.setItem(getLocalStorageKey('forms', 'requestPath'), requestPath)
}, [requestPath])

const showStatus = (text: OutputLine['content'], color: OutputLine['color'] = COLORS.default, id: OutputLine['id'] = ''): void => {
setOutput((prev: OutputLine[]) => {
return [...prev,
{
content: text,
color,
id
}
]
})
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext)
if (window.location.pathname === '/config') {
setConfigExpanded(true)
}

const handleSubmit = async (e): Promise<void> => {
e.preventDefault()
if (window.location.pathname === '/config') {
return <Config />
}

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])
if (isPathOrSubdomainRequest(window.location)) {
return (<RedirectPage />)
}

if (isConfigExpanded) {
return (<Config />)
}
Comment on lines +21 to +22
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eventually, this landing page should only be the config page....?

return (
<>
<Header />

<main className='pa4-l bg-snow mw7 mv5 center pa4'>
<h1 className='pa0 f2 ma0 mb4 aqua tc'>Fetch content from IPFS using Helia in a SW</h1>
<Form
handleSubmit={handleSubmit}
requestPath={requestPath}
setRequestPath={setRequestPath}
/>

<div className="bg-snow mw7 center w-100">
<CidRenderer requestPath={requestPath} />
</div>

</main>
</>
<HelperUi />
)
}

17 changes: 8 additions & 9 deletions src/components/CidRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { CID } from 'multiformats/cid'
import React from 'react'
import React, { useState } from 'react'

/**
* Test files:
@@ -82,12 +82,12 @@ function ValidationMessage ({ cid, requestPath, pathNamespacePrefix, children })
}

export default function CidRenderer ({ requestPath }: { requestPath: string }): JSX.Element {
const [contentType, setContentType] = React.useState<string | null>(null)
const [isLoading, setIsLoading] = React.useState(false)
const [abortController, setAbortController] = React.useState<AbortController | null>(null)
const [blob, setBlob] = React.useState<Blob | null>(null)
const [text, setText] = React.useState('')
const [lastFetchPath, setLastFetchPath] = React.useState<string | null>(null)
const [contentType, setContentType] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const [blob, setBlob] = useState<Blob | null>(null)
const [text, setText] = useState('')
const [lastFetchPath, setLastFetchPath] = useState<string | null>(null)
/**
* requestPath may be any of the following formats:
*
@@ -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',
11 changes: 10 additions & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
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 (
<header className='flex items-center pa3 bg-navy bb bw3 b--aqua'>
<a href='https://ipfs.io' title='home'>
<img alt='IPFS logo' src={ipfsLogo} style={{ height: 50 }} className='v-top' />
</a>

<button onClick={() => { setConfigExpanded(!isConfigExpanded) }} style={{ border: 'none', position: 'absolute', top: '0.5rem', right: '0.5rem', background: 'none', cursor: 'pointer' }}>
{/* https://isotropic.co/tool/hex-color-to-css-filter/ to #ffffff */}
<img alt='Config gear icon' src={gearIcon} style={{ height: 50, filter: 'invert(100%) sepia(100%) saturate(0%) hue-rotate(275deg) brightness(103%) contrast(103%)' }} className='v-top' />
</button>
</header>
)
}
23 changes: 23 additions & 0 deletions src/components/collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { useState } from 'react'

export interface CollapsibleProps {
children: React.ReactNode
collapsedLabel: string
expandedLabel: string
collapsed: boolean
}

export function Collapsible ({ children, collapsedLabel, expandedLabel, collapsed }: CollapsibleProps): JSX.Element {
const [cId] = useState(Math.random().toString(36).substring(7))
const [isCollapsed, setCollapsed] = useState(collapsed)

return (
<React.Fragment>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vscode was being weird in this file yelling about all types of syntax errors that didn't exist after closing it and re-opening. ¯\_(ツ)_/¯

<input type="checkbox" className="dn" name="collapsible" id={`collapsible-${cId}`} onClick={() => { setCollapsed(!isCollapsed) }} />
<label htmlFor={`collapsible-${cId}`} className="collapsible__item-label db pv3 link black hover-blue pointer blue">{isCollapsed ? collapsedLabel : expandedLabel}</label>
<div className={`bb b--black-20 ${isCollapsed ? 'dn' : ''}`}>
{children}
</div>
</React.Fragment>
)
}
96 changes: 96 additions & 0 deletions src/components/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { ConfigContext } from '../context/config-context.tsx'
import { HeliaServiceWorkerCommsChannel } from '../lib/channel.ts'
import { getConfig, loadConfigFromLocalStorage } from '../lib/config-db.ts'
import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.ts'
import { Collapsible } from './collapsible'
import LocalStorageInput from './local-storage-input.tsx'
import { LocalStorageToggle } from './local-storage-toggle'
import { ServiceWorkerReadyButton } from './sw-ready-button.tsx'

const channel = new HeliaServiceWorkerCommsChannel('WINDOW')

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 stringValidationFn = (value: string): Error | null => {
// we accept any string
return null
}

export default (): JSX.Element | null => {
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext)
const [error, setError] = useState<Error | null>(null)

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
})
}, [])

useEffect(() => {
/**
* On initial load, we want to send the config to the parent window, so that the reload page can auto-reload if enabled, and the subdomain registered service worker gets the latest config without user interaction.
*/
void postFromIframeToParentSw()
}, [])

const saveConfig = useCallback(async () => {
try {
await loadConfigFromLocalStorage()
// update the BASE_URL service worker
// TODO: use channel.messageAndWaitForResponse to ensure that the config is loaded before proceeding.
channel.postMessage({ target: 'SW', action: 'RELOAD_CONFIG' })
// update the <subdomain>.<namespace>.BASE_URL service worker
await postFromIframeToParentSw()
setConfigExpanded(false)
} catch (err) {
setError(err as Error)
}
}, [])

if (!isConfigExpanded) {
return null
}

return (
<main className='pa4-l bg-snow mw7 center pa4'>
<Collapsible collapsedLabel="View config" expandedLabel='Hide config' collapsed={true}>
<LocalStorageInput localStorageKey={LOCAL_STORAGE_KEYS.config.gateways} label='Gateways' validationFn={urlValidationFn} defaultValue='[]' />
<LocalStorageInput localStorageKey={LOCAL_STORAGE_KEYS.config.routers} label='Routers' validationFn={urlValidationFn} defaultValue='[]'/>
<LocalStorageToggle localStorageKey={LOCAL_STORAGE_KEYS.config.autoReload} onLabel='Auto Reload' offLabel='Show Config' />
<LocalStorageInput localStorageKey={LOCAL_STORAGE_KEYS.config.debug} label='Debug logging' validationFn={stringValidationFn} defaultValue=''/>
<ServiceWorkerReadyButton id="save-config" label='Save Config' waitingLabel='Waiting for service worker registration...' onClick={() => { void saveConfig() }} />

{error != null && <span style={{ color: 'red' }}>{error.message}</span>}
</Collapsible>
</main>
)
}
55 changes: 55 additions & 0 deletions src/components/local-storage-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { useEffect, useState } from 'react'

export interface LocalStorageInputProps {
localStorageKey: string
label: string
placeholder?: string
defaultValue: 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, defaultValue }: LocalStorageInputProps): JSX.Element => {
const [value, setValue] = useState(localStorage.getItem(localStorageKey) ?? defaultValue)
const [error, setError] = useState<null | Error>(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 (
<>
<label htmlFor={localStorageKey} className='f5 ma0 pb2 aqua fw4 db'>{label}:</label>
<input
className='input-reset bn black-80 bg-white pa3 w-100 mb3'
id={localStorageKey}
name={localStorageKey}
type='text'
placeholder={placeholder}
value={value}
onChange={(e) => { setValue(e.target.value) }}
/>
{error != null && <span style={{ color: 'red' }}>{error.message}</span>}
</>
)
}
64 changes: 64 additions & 0 deletions src/components/local-storage-toggle.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
Inspiration from https://dev.to/codebubb/create-a-simple-on-off-slide-toggle-with-css-db8
*/

.local-storage-toggle input.status {
display: none;
}
/* .local-storage-toggle input.status + label {
height: 100%;
width: 100%;
} */
.local-storage-toggle input.status + label > .status-switch {
cursor: pointer;
/* width: 100%; */
/* height: 100%; */
position: relative;
background-color: grey;
color: white;
transition: all 0.5s ease;
padding: 3px;
border-radius: 3px;
}
.local-storage-toggle input.status + label > .status-switch:before, .local-storage-toggle input.status + label > .status-switch:after {
border-radius: 2px;
height: calc(100% - 6px);
width: calc(50% - 3px);
display: flex;
align-items: center;
position: absolute;
justify-content: center;
transition: all 0.3s ease;
}
.local-storage-toggle input.status + label > .status-switch:before {
background-color: white;
color: black;
box-shadow: 0 0 4px 4px rgba(0, 0, 0, 0.2);
left: 3px;
z-index: 10;
content: attr(data-unchecked);
}
.local-storage-toggle input.status + label > .status-switch:after {
right: 0;
content: attr(data-checked);
/* add strikethrough to show to the user that it's disabled */
text-decoration: line-through;
}
.local-storage-toggle input.status:checked + label > .status-switch {
background-color: #40c253;
}
.local-storage-toggle input.status:checked + label > .status-switch:after {
left: 0;
content: attr(data-unchecked);
/* add strikethrough to show to the user that it's disabled */
text-decoration: line-through;
}
.local-storage-toggle input.status:checked + label > .status-switch:before {
color: #40c253;
left: 50%;
content: attr(data-checked);
}

.h3-custom {
height: 3rem;
}
35 changes: 35 additions & 0 deletions src/components/local-storage-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
Inspiration from https://dev.to/codebubb/create-a-simple-on-off-slide-toggle-with-css-db8
*/
import React, { useState } from 'react'
import './local-storage-toggle.css'

interface LocalStorageToggleProps {
localStorageKey: string
offLabel: string
onLabel: string
}

export const LocalStorageToggle: React.FC<LocalStorageToggleProps> = ({ localStorageKey, onLabel = 'Off', offLabel = 'On' }) => {
const [isChecked, setIsChecked] = useState(() => {
const savedValue = localStorage.getItem(localStorageKey)
return savedValue === 'true'
})

const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const newValue = event.target.checked
setIsChecked(newValue)
localStorage.setItem(localStorageKey, String(newValue))
}

return (
<>
<div className="local-storage-toggle input-reset bn black-80 w-100 mb3">
<input className="status" style={{ display: 'none' }} id={localStorageKey} type="checkbox" name="status" checked={isChecked} onChange={handleChange} />
<label htmlFor={localStorageKey} className="w-100 h-100">
<div className="status-switch relative h3-custom pointer white bg-gray w-100" data-unchecked={offLabel} data-checked={onLabel} />
</label>
</div>
</>
)
}
39 changes: 39 additions & 0 deletions src/components/sw-ready-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useContext, useMemo } from 'react'
import { ServiceWorkerContext } from '../context/service-worker-context.tsx'
type ButtonProps = JSX.IntrinsicElements['button']

interface ServiceWorkerReadyButtonProps extends ButtonProps {
label: string
waitingLabel?: string
}

export const ServiceWorkerReadyButton = ({ className, label, waitingLabel, ...props }: ServiceWorkerReadyButtonProps): JSX.Element => {
const { isServiceWorkerRegistered } = useContext(ServiceWorkerContext)

const buttonClasses = new Set(['button-reset', 'pv3', 'tc', 'bn', 'white', 'w-100', 'cursor-disabled', 'bg-gray'])
if (isServiceWorkerRegistered) {
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')
}
Comment on lines +13 to +21
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be a useMemo and two distinct string values


const lbl = useMemo(() => {
if (!isServiceWorkerRegistered) {
return waitingLabel ?? label
}
return label
}, [isServiceWorkerRegistered, waitingLabel, label])

return (
<button
disabled={!isServiceWorkerRegistered}
className={`${Array.from(buttonClasses).join(' ')} ${className}`}
{...props}
>
{lbl}
</button>
)
}
27 changes: 27 additions & 0 deletions src/context/config-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { createContext, useState } from 'react'

const isLoadedInIframe = window.self !== window.top

export const ConfigContext = createContext({
isConfigExpanded: isLoadedInIframe,
setConfigExpanded: (value: boolean) => {}
})

export const ConfigProvider = ({ children, expanded = isLoadedInIframe }: { children: JSX.Element[] | JSX.Element, expanded?: boolean }): JSX.Element => {
const [isConfigExpanded, setConfigExpanded] = useState(expanded)
const isExplicitlyLoadedConfigPage = window.location.pathname === '/config'

const setConfigExpandedWrapped = (value: boolean): void => {
if (isLoadedInIframe || isExplicitlyLoadedConfigPage) {
// ignore it
} else {
setConfigExpanded(value)
}
}

return (
<ConfigContext.Provider value={{ isConfigExpanded, setConfigExpanded: setConfigExpandedWrapped }}>
{children}
</ConfigContext.Provider>
)
}
42 changes: 42 additions & 0 deletions src/context/service-worker-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { createContext, useEffect, useState } from 'react'
import { error } from '../lib/logger.ts'
import { registerServiceWorker } from '../service-worker-utils.ts'

export const ServiceWorkerContext = createContext({
isServiceWorkerRegistered: false
})

export const ServiceWorkerProvider = ({ children }): JSX.Element => {
const [isServiceWorkerRegistered, setIsServiceWorkerRegistered] = useState(false)

useEffect(() => {
if (isServiceWorkerRegistered) {
return
}
async function doWork (): Promise<void> {
const registration = await navigator.serviceWorker.getRegistration()

if (registration != null) {
// service worker already registered
// attempt to update it
await registration.update()
setIsServiceWorkerRegistered(true)
} else {
try {
const registration = await registerServiceWorker()
await registration.update()
setIsServiceWorkerRegistered(true)
} catch (err) {
error('error registering service worker', err)
}
}
}
void doWork()
}, [])

return (
<ServiceWorkerContext.Provider value={{ isServiceWorkerRegistered }}>
{children}
</ServiceWorkerContext.Provider>
)
}
1 change: 1 addition & 0 deletions src/gear-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions src/get-helia.ts
Original file line number Diff line number Diff line change
@@ -4,10 +4,12 @@ import { delegatedHTTPRouting } from '@helia/routers'
import { IDBBlockstore } from 'blockstore-idb'
import { IDBDatastore } from 'datastore-idb'
import { getConfig } from './lib/config-db.ts'
import { trace } from './lib/logger.ts'
import type { Helia } from '@helia/interface'

export async function getHelia (): Promise<Helia> {
const config = await getConfig()
trace(`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()
@@ -18,10 +20,10 @@ export async function getHelia (): Promise<Helia> {
datastore,
blockBrokers: [
trustlessGateway({
gateways: ['https://trustless-gateway.link', ...config.gateways]
gateways: [...config.gateways, 'https://trustless-gateway.link']
})
],
routers: ['https://delegated-ipfs.dev', ...config.routers].map(rUrl => delegatedHTTPRouting(rUrl))
routers: [...config.routers, 'https://delegated-ipfs.dev'].map(rUrl => delegatedHTTPRouting(rUrl))
})

return helia
36 changes: 36 additions & 0 deletions src/helper-ui.tsx
Original file line number Diff line number Diff line change
@@ -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<void> => {
e.preventDefault()
}

return (
<>
<Header />
<main className='pa4-l bg-snow mw7 mv5 center pa4'>
<h1 className='pa0 f2 ma0 mb4 aqua tc'>Fetch content from IPFS using Helia in a SW</h1>
<Form
handleSubmit={handleSubmit}
requestPath={requestPath}
setRequestPath={setRequestPath}
/>

<div className="bg-snow mw7 center w-100">
<CidRenderer requestPath={requestPath} />
</div>

</main>
</>
)
}
39 changes: 12 additions & 27 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -2,38 +2,23 @@ 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 { ServiceWorkerProvider } from './context/service-worker-context.tsx'
import { loadConfigFromLocalStorage } from './lib/config-db.ts'
import { isPathOrSubdomainRequest } from './lib/path-or-subdomain.ts'
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(window.location)) {
// but the requested path is something it should, so show redirect and redirect to the same URL
root.render(
<RedirectPage />
)
window.location.replace(window.location.href)
} else {
// TODO: add detection of DNSLink gateways (alowing use with Host: en.wikipedia-on-ipfs.org)
// 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(
<React.StrictMode>
<App />
</React.StrictMode>
)
}
}

// always update the service worker
void sw.update()
root.render(
<React.StrictMode>
<ServiceWorkerProvider>
<ConfigProvider expanded={window.location.pathname === '/config'}>
<App />
</ConfigProvider>
</ServiceWorkerProvider>
</React.StrictMode>
)
Comment on lines +16 to +24
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified root logic. page rendering determined in app.tsx

34 changes: 5 additions & 29 deletions src/lib/channel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { error } from './logger.ts'
import type { ChannelActions } from './common.ts'

export enum ChannelUsers {
@@ -26,35 +27,21 @@ type NotSourceUser<T extends ChannelUserValues> = T extends ChannelUsers
export interface ChannelMessage<Source extends ChannelUserValues, Data = Record<string, unknown>> {
source: Source
target?: ChannelUserValues
action: ChannelActions | keyof typeof ChannelActions | 'TEST'
data: Data
action: keyof typeof ChannelActions
data?: Data
}

export class HeliaServiceWorkerCommsChannel<S extends ChannelUserValues = 'EMITTER_ONLY'> {
channel: BroadcastChannel
debug = false
constructor (public source: S, private readonly channelName = 'helia:sw') {
this.log('HeliaServiceWorkerCommsChannel construction: ', source)

// NOTE: We're supposed to close the channel when we're done with it, but we're not doing that anywhere yet.
this.channel = new BroadcastChannel(this.channelName)
this.channel.onmessageerror = (e) => {
this.error('onmessageerror', e)
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'
}
@@ -71,14 +58,9 @@ export class HeliaServiceWorkerCommsChannel<S extends ChannelUserValues = 'EMITT
throw new Error('Cannot use onmessagefrom on EMITTER_ONLY channel')
}
const onMsgHandler = (e: MessageEvent<ChannelMessage<Source, MType>>): 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)
@@ -95,14 +77,10 @@ export class HeliaServiceWorkerCommsChannel<S extends ChannelUserValues = 'EMITT
throw new Error('Cannot use onmessagefromother on EMITTER_ONLY channel')
}
const onMsgHandler = (e: MessageEvent<ChannelMessage<Source, MType>>): 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.removeEventListener('message', onMsgHandler)
@@ -123,14 +101,12 @@ export class HeliaServiceWorkerCommsChannel<S extends ChannelUserValues = 'EMITT
this.channel.removeEventListener('message', onMessage)
resolve(e.data)
}
// this.channel.onmessage = onMessage;
this.channel.addEventListener('message', onMessage)
this.postMessage(data)
})
}

postMessage<MType>(msg: Omit<ChannelMessage<S, MType>, 'source'>): void {
this.log('postMessage: ', msg)
const msgObj: ChannelMessage<typeof this.source, MType> = {
...msg,
source: this.source
10 changes: 1 addition & 9 deletions src/lib/common.ts
Original file line number Diff line number Diff line change
@@ -6,13 +6,5 @@ export enum COLORS {
}

export enum ChannelActions {
GET_FILE = 'GET_FILE',
DIAL = 'DIAL',
UPDATE_OUTPUT = 'UPDATE_OUTPUT',
PING = 'PING',
PONG = 'PONG',
/**
* Intended only for pushing from SW to the UI output terminal
*/
SHOW_STATUS = 'SHOW_STATUS',
RELOAD_CONFIG = 'RELOAD_CONFIG',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably need a RELOAD_CONFIG_DONE here that SW emits so we can block on config saving until the SW is refreshed with any updated config. This should prevent any confusion when adding a new gateway and not seeing requests go to that gateway.

}
37 changes: 31 additions & 6 deletions src/lib/config-db.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { getLocalStorageKey } from './local-storage.ts'
import debugLib from 'debug'
import { LOCAL_STORAGE_KEYS } from './local-storage.ts'
import { log } from './logger'

export interface ConfigDb {
gateways: string[]
routers: string[]
autoReload: boolean
debug: string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use https://npmjs.com/package/debug strings to set debug levels on the service worker. similar to setting DEBUG=... env var in the terminal

}

export type configDbKeys = keyof ConfigDb
@@ -49,26 +53,47 @@ export async function closeDatabase (db: IDBDatabase): Promise<void> {
export async function loadConfigFromLocalStorage (): Promise<void> {
if (typeof globalThis.localStorage !== 'undefined') {
const db = await openDatabase()
const localStorage = global.localStorage
const localStorageGatewaysString = localStorage.getItem(getLocalStorageKey('config', 'gateways')) ?? '[]'
const localStorageRoutersString = localStorage.getItem(getLocalStorageKey('config', 'routers')) ?? '[]'
const localStorage = globalThis.localStorage
const localStorageGatewaysString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) ?? '[]'
const localStorageRoutersString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.routers) ?? '[]'
const autoReload = localStorage.getItem(LOCAL_STORAGE_KEYS.config.autoReload) === 'true'
const debug = localStorage.getItem(LOCAL_STORAGE_KEYS.config.debug) ?? ''
const gateways = JSON.parse(localStorageGatewaysString)
const routers = JSON.parse(localStorageRoutersString)
debugLib.enable(debug)

await setInDatabase(db, 'gateways', gateways)
await setInDatabase(db, 'routers', routers)
await setInDatabase(db, 'autoReload', autoReload)
await setInDatabase(db, 'debug', debug)
await closeDatabase(db)
}
}

export async function setConfig (config: ConfigDb): Promise<void> {
log('config-debug: setting config', config)
debugLib.enable(config.debug ?? '')
const db = await openDatabase()
await setInDatabase(db, 'gateways', config.gateways)
await setInDatabase(db, 'routers', config.routers)
await setInDatabase(db, 'autoReload', config.autoReload)
await setInDatabase(db, 'debug', config.debug ?? '')
await closeDatabase(db)
}

export async function getConfig (): Promise<ConfigDb> {
const db = await openDatabase()

const gateways = await getFromDatabase(db, 'gateways') ?? []
const routers = await getFromDatabase(db, 'routers') ?? []
const autoReload = await getFromDatabase(db, 'autoReload') ?? false
const debug = await getFromDatabase(db, 'debug') ?? ''
debugLib.enable(debug)

return {
gateways: gateways instanceof Array ? gateways : [],
routers: routers instanceof Array ? routers : []
gateways,
routers,
autoReload,
debug
}
}
35 changes: 35 additions & 0 deletions src/lib/get-subdomain-parts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { dnsLinkLabelDecoder, isInlinedDnsLink } from './dns-link-labels.ts'

export interface UrlParts {
id: string | null
protocol: string | null
parentDomain: string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved this over from sw mostly so i could use this for iframe rendering of the config page now that we dont have BASE_URL. Only change is the addition of this parentDomain

}

export function getSubdomainParts (urlString: string): UrlParts {
const labels = new URL(urlString).hostname.split('.')
let id: string | null = null
let protocol: string | null = null
let parentDomain: string = urlString

// DNS label inspection happens from from right to left
// to work fine with edge cases like docs.ipfs.tech.ipns.foo.localhost
for (let i = labels.length - 1; i >= 0; i--) {
if (labels[i].startsWith('ipfs') || labels[i].startsWith('ipns')) {
protocol = labels[i]
id = labels.slice(0, i).join('.')
parentDomain = labels.slice(i + 1).join('.')
if (protocol === 'ipns' && isInlinedDnsLink(id)) {
// un-inline DNSLink names according to https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
id = dnsLinkLabelDecoder(id)
}
break
}
}

return {
id,
parentDomain,
protocol
}
}
17 changes: 6 additions & 11 deletions src/lib/heliaFetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createVerifiedFetch, type ContentTypeParser } from '@helia/verified-fetch'
import { fileTypeFromBuffer } from '@sgtpooki/file-type'
import { trace } from './logger'
import type { Helia } from '@helia/interface'

export interface HeliaFetchOptions {
@@ -14,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
@@ -83,18 +81,16 @@ const cssPathRegex = /(?<cssPath>.*\.css)(?<fontPath>.*\.(ttf|otf|woff|woff2){1}
function changeCssFontPath (path: string): string {
const match = path.match(cssPathRegex)
if (match == null) {
// eslint-disable-next-line no-console
console.log(`changeCssFontPath: No match for ${path}`)
trace(`changeCssFontPath: No match for ${path}`)
return path
}
const { cssPath, fontPath } = match.groups as { cssPath?: string, fontPath?: string }
if (cssPath == null || fontPath == null) {
// eslint-disable-next-line no-console
console.log(`changeCssFontPath: No groups for ${path}`, match.groups)
trace(`changeCssFontPath: No groups for ${path}`, match.groups)
return path
}
// eslint-disable-next-line no-console
console.log(`changeCssFontPath: Changing font path from ${path} to ${fontPath}`)

trace(`changeCssFontPath: Changing font path from ${path} to ${fontPath}`)
return fontPath
}

@@ -158,8 +154,7 @@ export async function heliaFetch ({ path, helia, signal, headers, id, protocol }
signal,
headers,
onProgress: (e) => {
// eslint-disable-next-line no-console
console.log(`${e.type}: `, e.detail)
trace(`${e.type}: `, e.detail)
}
})
}
13 changes: 13 additions & 0 deletions src/lib/local-storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
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'),
autoReload: getLocalStorageKey('config', 'autoReload'),
debug: getLocalStorageKey('config', 'debug')
},
forms: {
requestPath: getLocalStorageKey('forms', 'requestPath')
}
}
7 changes: 7 additions & 0 deletions src/lib/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { logger } from '@libp2p/logger'

const logObj = logger('helia:service-worker-gateway')

export const log = logObj
export const error = logObj.error
export const trace = logObj.trace
84 changes: 82 additions & 2 deletions src/redirectPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,89 @@
import React from 'react'
import React, { useContext, useEffect, useMemo, useState } from 'react'
import { ServiceWorkerReadyButton } from './components/sw-ready-button.tsx'
import { ServiceWorkerContext } from './context/service-worker-context.tsx'
import { HeliaServiceWorkerCommsChannel } from './lib/channel.ts'
import { setConfig, type ConfigDb } from './lib/config-db.ts'
import { getSubdomainParts } from './lib/get-subdomain-parts'
import { error } from './lib/logger.ts'

const ConfigIframe = (): JSX.Element => {
const { parentDomain } = getSubdomainParts(window.location.href)

const iframeSrc = `${window.location.protocol}//${parentDomain}/config?origin=${encodeURIComponent(window.location.origin)}`

return (
<iframe id="redirect-config-iframe" src={iframeSrc} style={{ width: '100vw', height: '100vh', border: 'none' }} />
)
}

const channel = new HeliaServiceWorkerCommsChannel('WINDOW')

export default function RedirectPage (): JSX.Element {
const [isAutoReloadEnabled, setIsAutoReloadEnabled] = useState(false)
const { isServiceWorkerRegistered } = useContext(ServiceWorkerContext)

useEffect(() => {
async function doWork (config: ConfigDb): Promise<void> {
try {
await setConfig(config)
// TODO: use channel.messageAndWaitForResponse to ensure that the config is loaded before proceeding.
channel.postMessage({ target: 'SW', action: 'RELOAD_CONFIG' })
// try to preload the content
setTimeout(() => {
fetch(window.location.href, { method: 'GET' }).then((response) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when user is done on config page, "load content" or refresh should result in immediate rendering (or at least less wait time)

// eslint-disable-next-line no-console
console.log('response', response)
}).catch((err) => {
// eslint-disable-next-line no-console
console.error('error fetching', err)
})
}, 500)
} catch (err) {
error('config-debug: error setting config on subdomain', err)
}

if (config.autoReload) {
setIsAutoReloadEnabled(config.autoReload)
}
}
const listener = (event: MessageEvent): void => {
if (event.data?.source === 'helia-sw-config-iframe') {
const config = event.data?.config
if (config != null) {
void doWork(config as ConfigDb)
}
}
}
window.addEventListener('message', listener)
return () => {
window.removeEventListener('message', listener)
}
}, [])

const displayString = useMemo(() => {
if (!isServiceWorkerRegistered) {
return 'Registering Helia service worker...'
}
if (isAutoReloadEnabled) {
return 'Redirecting you because Auto Reload is enabled.'
}

return 'Please save your changes to the config to apply them. You can then refresh the page to load your content.'
}, [isAutoReloadEnabled, isServiceWorkerRegistered])

useEffect(() => {
if (isAutoReloadEnabled && isServiceWorkerRegistered) {
window.location.reload()
}
}, [isAutoReloadEnabled, isServiceWorkerRegistered])

return (
<div className="redirect-page">
<h1>Registering Helia service worker and Redirecting...</h1>
<div className="pa4-l mw7 mv5 center pa4">
<h3 className="">{displayString}</h3>
<ServiceWorkerReadyButton id="load-content" label='Load content' waitingLabel='Waiting for service worker registration...' onClick={() => { window.location.reload() }} />
</div>
<ConfigIframe />
</div>
)
}
16 changes: 16 additions & 0 deletions src/service-worker-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { log } from './lib/logger.ts'

export async function registerServiceWorker (): Promise<ServiceWorkerRegistration> {
const swRegistration = await navigator.serviceWorker.register(new URL('sw.ts', import.meta.url))
return new Promise((resolve, reject) => {
swRegistration.addEventListener('updatefound', () => {
const newWorker = swRegistration.installing
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
log('service worker activated')
resolve(swRegistration)
}
})
})
})
}
89 changes: 33 additions & 56 deletions src/sw.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
/* 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 { dnsLinkLabelDecoder, isInlinedDnsLink } from './lib/dns-link-labels.ts'
import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/channel.ts'
import { getSubdomainParts } from './lib/get-subdomain-parts.ts'
import { heliaFetch } from './lib/heliaFetch.ts'
import { error, log, trace } from './lib/logger.ts'
import type { Helia } from '@helia/interface'

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<ChannelMessage<'WINDOW', null>>) => {
const { action } = message.data
switch (action) {
case 'RELOAD_CONFIG':
void getHelia().then((newHelia) => {
helia = newHelia
})
break
default:
log('unknown action: ', action)
}
})
})

/**
@@ -40,8 +52,6 @@ const fetchHandler = async ({ path, request }: FetchHandlerArg): Promise<Respons
if (helia == null) {
helia = await getHelia()
}
// 2 second timeout - for debugging
// const abortController = new AbortAbort({ timeout: 2 * 1000 })

/**
* Note that there are existing bugs regarding service worker signal handling:
@@ -51,19 +61,19 @@ const fetchHandler = async ({ path, request }: FetchHandlerArg): Promise<Respons
// 5 minute timeout
const abortController = AbortSignal.timeout(5 * 60 * 1000)
try {
const { id, protocol } = getSubdomainParts(request)
const { id, protocol } = getSubdomainParts(request.url)
return await heliaFetch({ path, helia, signal: abortController, headers: request.headers, id, protocol })
} catch (err: unknown) {
const errorMessages: string[] = []
if (isAggregateError(err)) {
console.error('fetchHandler aggregate error: ', err.message)
error('fetchHandler aggregate error: ', err.message)
for (const e of err.errors) {
errorMessages.push(e.message)
console.error('fetchHandler errors: ', e)
error('fetchHandler errors: ', e)
}
} else {
errorMessages.push(err instanceof Error ? err.message : JSON.stringify(err))
console.error('fetchHandler error: ', err)
error('fetchHandler error: ', err)
}
const errorMessage = errorMessages.join('\n')

@@ -91,32 +101,10 @@ const isRootRequestForContent = (event: FetchEvent): boolean => {
return isRootRequest // && getCidFromUrl(event.request.url) != null
}

function getSubdomainParts (request: Request): { id: string | null, protocol: string | null } {
const urlString = request.url
const labels = new URL(urlString).hostname.split('.')
let id: string | null = null; let protocol: string | null = null

// DNS label inspection happens from from right to left
// to work fine with edge cases like docs.ipfs.tech.ipns.foo.localhost
for (let i = labels.length - 1; i >= 0; i--) {
if (labels[i].startsWith('ipfs') || labels[i].startsWith('ipns')) {
protocol = labels[i]
id = labels.slice(0, i).join('.')
if (protocol === 'ipns' && isInlinedDnsLink(id)) {
// un-inline DNSLink names according to https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
id = dnsLinkLabelDecoder(id)
}
break
}
}

return { id, protocol }
}

function isSubdomainRequest (event: FetchEvent): boolean {
const { id, protocol } = getSubdomainParts(event.request)
console.log('isSubdomainRequest.id: ', id)
console.log('isSubdomainRequest.protocol: ', protocol)
const { id, protocol } = getSubdomainParts(event.request.url)
trace('isSubdomainRequest.id: ', id)
trace('isSubdomainRequest.protocol: ', protocol)

return id != null && protocol != null
}
@@ -128,26 +116,16 @@ self.addEventListener('fetch', event => {
const request = event.request
const urlString = request.url
const url = new URL(urlString)
console.log('helia-sw: urlString: ', urlString)

if (urlString.includes('?helia-sw-subdomain')) {
console.log('helia-sw: subdomain request: ', urlString)
// subdomain request where URL has <subdomain>.ip[fn]s and any paths should be appended to the url
// const subdomain = url.searchParams.get('helia-sw-subdomain')
// console.log('url.href: ', url.href)
// const path = `${url.searchParams.get('helia-sw-subdomain')}/${url.pathname}`
event.respondWith(fetchHandler({ path: url.pathname, request }))
return
}

if (!isValidRequestForSW(event)) {
console.warn('helia-sw: not a valid request for helia-sw, ignoring ', urlString)
trace('helia-sw: not a valid request for helia-sw, ignoring ', urlString)
return
} else {
log('helia-sw: valid request for helia-sw: ', urlString)
}
// console.log('request: ', request)
// console.log('self.location.origin: ', self.location.origin)
console.log('helia-sw: intercepting request to ', urlString)

if (isReferrerPreviouslyIntercepted(event)) {
console.log('helia-sw: referred from ', request.referrer)
log('helia-sw: referred from ', request.referrer)
const destinationParts = urlString.split('/')
const referrerParts = request.referrer.split('/')
const newParts: string[] = []
@@ -156,7 +134,6 @@ self.addEventListener('fetch', event => {
newParts.push(destinationParts[index])
index++
}
// console.log(`leftover parts for '${request.referrer}' -> '${urlString}': `, referrerParts.slice(index))
newParts.push(...referrerParts.slice(index))

const newUrlString = newParts.join('/') + '/' + destinationParts.slice(index).join('/')
@@ -166,10 +143,10 @@ self.addEventListener('fetch', event => {
* respond with redirect to newUrl
*/
if (newUrl.toString() !== urlString) {
console.log('helia-sw: rerouting request to: ', newUrl.toString())
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')
@@ -179,7 +156,7 @@ self.addEventListener('fetch', event => {
})
event.respondWith(redirectResponse)
} else {
console.log('helia-sw: not rerouting request to same url: ', newUrl.toString())
log('helia-sw: not rerouting request to same url: ', newUrl.toString())

event.respondWith(fetchHandler({ path: url.pathname, request }))
}
15 changes: 13 additions & 2 deletions webpack.config.js
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ const paths = {
*/
const prod = {
mode: 'production',
devtool: false,
devtool: 'inline-source-map',
output: {
path: paths.build,
publicPath: '/',
@@ -66,7 +66,18 @@ const prod = {
})
}
}
]
],
optimization: {
splitChunks: {
cacheGroups: {
reactVendor: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'vendor-react',
chunks: 'all'
}
}
}
}
}

/**