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: allow loading config page from subdomain after sw registration #96

Merged
merged 12 commits into from
Mar 12, 2024
1 change: 0 additions & 1 deletion public/_redirects
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
/config /#/config 302
/* /?helia-sw=/:splat 302
10 changes: 8 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ import RedirectPage from './redirectPage.tsx'

function App (): JSX.Element {
const { isConfigExpanded, setConfigExpanded } = useContext(ConfigContext)
const isRequestToViewConfigPage = isConfigPage(window.location.hash)
const isSubdomainRender = isPathOrSubdomainRequest(window.location)

if (isRequestToViewConfigPage) {
if (isSubdomainRender) {
return <RedirectPage />
}

if (isConfigPage()) {
setConfigExpanded(true)
return <Config />
}

if (isPathOrSubdomainRequest(window.location)) {
if (isSubdomainRender) {
return (<RedirectPage />)
}

Expand Down
1 change: 0 additions & 1 deletion src/components/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export default (): JSX.Element | null => {
return
}
// we get the iframe origin from a query parameter called 'origin', if this is loaded in an iframe
// TODO: why we need this origin here? where is targetOrigin used?
const targetOrigin = decodeURIComponent(window.location.hash.split('@origin=')[1])
const config = await getConfig()
trace('config-page: postMessage config to origin ', config, origin)
Expand Down
2 changes: 1 addition & 1 deletion src/context/config-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const ConfigContext = createContext({

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

const setConfigExpandedWrapped = (value: boolean): void => {
if (isLoadedInIframe || isExplicitlyLoadedConfigPage) {
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const root = ReactDOMClient.createRoot(container)
root.render(
<React.StrictMode>
<ServiceWorkerProvider>
<ConfigProvider expanded={isConfigPage()}>
<ConfigProvider expanded={isConfigPage(window.location.hash)}>
<App />
</ConfigProvider>
</ServiceWorkerProvider>
Expand Down
7 changes: 3 additions & 4 deletions src/lib/is-config-page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export function isConfigPage (): boolean {
const isConfigPathname = window.location.pathname === '/config'
const isConfigHashPath = window.location.hash.startsWith('#/config') // needed for _redirects and IPFS hosted sw gateways
return isConfigPathname || isConfigHashPath
export function isConfigPage (hash: string): boolean {
const isConfigHashPath = hash.startsWith('#/ipfs-sw-config') // needed for _redirects and IPFS hosted sw gateways
return isConfigHashPath
}
14 changes: 10 additions & 4 deletions src/redirectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ 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 { isConfigPage } from './lib/is-config-page'
import { error, trace } from './lib/logger.ts'

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

const portString = window.location.port === '' ? '' : `:${window.location.port}`
const iframeSrc = `${window.location.protocol}//${parentDomain}${portString}#/config@origin=${encodeURIComponent(window.location.origin)}`
const iframeSrc = `${window.location.protocol}//${parentDomain}${portString}#/ipfs-sw-config@origin=${encodeURIComponent(window.location.origin)}`

return (
<iframe id="redirect-config-iframe" src={iframeSrc} style={{ width: '100vw', height: '100vh', border: 'none' }} />
Expand Down Expand Up @@ -55,19 +56,24 @@ export default function RedirectPage (): JSX.Element {
}
}, [])

let reloadUrl = window.location.href
if (isConfigPage(window.location.hash)) {
reloadUrl = window.location.href.replace('#/ipfs-sw-config', '')
}

const displayString = useMemo(() => {
if (!isServiceWorkerRegistered) {
return 'Registering Helia service worker...'
}
if (isAutoReloadEnabled) {
if (isAutoReloadEnabled && !isConfigPage(window.location.hash)) {
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) {
if (isAutoReloadEnabled && isServiceWorkerRegistered && !isConfigPage(window.location.hash)) {
window.location.reload()
}
}, [isAutoReloadEnabled, isServiceWorkerRegistered])
Expand All @@ -76,7 +82,7 @@ export default function RedirectPage (): JSX.Element {
<div className="redirect-page">
<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() }} />
<ServiceWorkerReadyButton id="load-content" label='Load content' waitingLabel='Waiting for service worker registration...' onClick={() => { window.location.href = reloadUrl }} />
</div>
<ConfigIframe />
</div>
Expand Down
20 changes: 17 additions & 3 deletions src/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/chann
import { getConfig } from './lib/config-db.ts'
import { contentTypeParser } from './lib/content-type-parser.ts'
import { getSubdomainParts } from './lib/get-subdomain-parts.ts'
import { isConfigPage } from './lib/is-config-page.ts'
import { error, log, trace } from './lib/logger.ts'
import { findOriginIsolationRedirect } from './lib/path-or-subdomain.ts'

Expand Down Expand Up @@ -52,11 +53,11 @@ const updateVerifiedFetch = async (): Promise<void> => {
self.addEventListener('install', (event) => {
// 👇 When a new version of the SW is installed, activate immediately
void self.skipWaiting()
// ensure verifiedFetch is ready for use
event.waitUntil(updateVerifiedFetch())
})

self.addEventListener('activate', (event) => {
// ensure verifiedFetch is ready for use
event.waitUntil(updateVerifiedFetch())
/**
* 👇 Claim all clients immediately. This handles the case when subdomain is
* loaded for the first time, and config is updated and then a pre-fetch is
Expand Down Expand Up @@ -85,7 +86,11 @@ self.addEventListener('fetch', (event) => {
const urlString = request.url
const url = new URL(urlString)

if (!isValidRequestForSW(event)) {
if (isConfigPageRequest(url) || isSwAssetRequest(event)) {
// get the assets from the server
trace('helia-sw: config page or js asset request, ignoring ', urlString)
return
} else if (!isValidRequestForSW(event)) {
trace('helia-sw: not a valid request for helia-sw, ignoring ', urlString)
return
} else {
Expand Down Expand Up @@ -134,6 +139,10 @@ function isSubdomainRequest (event: FetchEvent): boolean {
return id != null && protocol != null
}

function isConfigPageRequest (url: URL): boolean {
return isConfigPage(url.hash)
}

function isValidRequestForSW (event: FetchEvent): boolean {
return isSubdomainRequest(event) || isRootRequestForContent(event)
}
Expand Down Expand Up @@ -163,6 +172,11 @@ function getVerifiedFetchUrl ({ protocol, id, path }: GetVerifiedFetchUrlOptions
return `${namespaceString}://${pathRootString}/${contentPath}`
}

function isSwAssetRequest (event: FetchEvent): boolean {
const isActualSwAsset = /^.+\/(?:ipfs-sw-).+\.js$/.test(event.request.url)
return isActualSwAsset
}

async function fetchHandler ({ path, request }: FetchHandlerArg): Promise<Response> {
/**
* > Any global variables you set will be lost if the service worker shuts down.
Expand Down
Loading