Skip to content

Commit

Permalink
fix: config UI is up to date with IDB
Browse files Browse the repository at this point in the history
  • Loading branch information
SgtPooki committed Dec 12, 2024
1 parent b29cd3c commit 0f8e84f
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 140 deletions.
133 changes: 77 additions & 56 deletions src/lib/config-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,65 +81,86 @@ export async function setConfig (config: ConfigDbWithoutPrivateFields, logger: C
}
}

export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {
const log = logger.forComponent('get-config')
let gateways = defaultGateways
let routers = defaultRouters
let dnsJsonResolvers = defaultDnsJsonResolvers
let enableRecursiveGateways
let enableWss
let enableWebTransport
let enableGatewayProviders
let debug = ''
let _supportsSubdomains = defaultSupportsSubdomains

try {
await configDb.open()

gateways = await configDb.get('gateways')
let getConfigPromise: Promise<ConfigDb> | null = null

routers = await configDb.get('routers')

dnsJsonResolvers = await configDb.get('dnsJsonResolvers')

enableRecursiveGateways = await configDb.get('enableRecursiveGateways') ?? defaultEnableRecursiveGateways
enableWss = await configDb.get('enableWss') ?? defaultEnableWss
enableWebTransport = await configDb.get('enableWebTransport') ?? defaultEnableWebTransport
enableGatewayProviders = await configDb.get('enableGatewayProviders') ?? defaultEnableGatewayProviders

debug = await configDb.get('debug') ?? defaultDebug()
enable(debug)

_supportsSubdomains ??= await configDb.get('_supportsSubdomains')
} catch (err) {
log('error loading config from db', err)
} finally {
configDb.close()
}

if (gateways == null || gateways.length === 0) {
gateways = [...defaultGateways]
}

if (routers == null || routers.length === 0) {
routers = [...defaultRouters]
}
if (dnsJsonResolvers == null || Object.keys(dnsJsonResolvers).length === 0) {
dnsJsonResolvers = { ...defaultDnsJsonResolvers }
export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {
if (getConfigPromise != null) {
/**
* If there is already a promise to get the config, return it.
* This is to prevent multiple calls to the db to get the same config, because
* each request will close the DB when done, and then the next request will fail at some point
*/
return getConfigPromise
}

// always return the config, even if we failed to load it.
return {
gateways,
routers,
dnsJsonResolvers,
enableRecursiveGateways,
enableWss,
enableWebTransport,
enableGatewayProviders,
debug,
_supportsSubdomains
}
getConfigPromise = (async () => {
const log = logger.forComponent('get-config')
let gateways = defaultGateways
let routers = defaultRouters
let dnsJsonResolvers = defaultDnsJsonResolvers
let enableRecursiveGateways
let enableWss
let enableWebTransport
let enableGatewayProviders
let debug = ''
let _supportsSubdomains = defaultSupportsSubdomains

let config: ConfigDb

log('config-debug: getting config for domain %s', globalThis.location.origin)
try {
await configDb.open()

config = await configDb.getAll()
debug = config.debug ?? defaultDebug()
enable(debug)

gateways = config.gateways

routers = config.routers

dnsJsonResolvers = config.dnsJsonResolvers
enableRecursiveGateways = config.enableRecursiveGateways ?? defaultEnableRecursiveGateways
enableWss = config.enableWss ?? defaultEnableWss
enableWebTransport = config.enableWebTransport ?? defaultEnableWebTransport
enableGatewayProviders = config.enableGatewayProviders ?? defaultEnableGatewayProviders

_supportsSubdomains ??= config.thing
} catch (err) {
log('error loading config from db', err)
} finally {
configDb.close()
}

if (gateways == null || gateways.length === 0) {
gateways = [...defaultGateways]
}

if (routers == null || routers.length === 0) {
routers = [...defaultRouters]
}
if (dnsJsonResolvers == null || Object.keys(dnsJsonResolvers).length === 0) {
dnsJsonResolvers = { ...defaultDnsJsonResolvers }
}

// always return the config, even if we failed to load it.
return {
gateways,
routers,
dnsJsonResolvers,
enableRecursiveGateways,
enableWss,
enableWebTransport,
enableGatewayProviders,
debug,
_supportsSubdomains
}
})().finally(() => {
getConfigPromise = null
})

const result = await getConfigPromise
return result
}

export async function validateConfig (config: ConfigDbWithoutPrivateFields, logger: ComponentLogger): Promise<void> {
Expand Down
28 changes: 28 additions & 0 deletions src/lib/generic-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ export class GenericIDB<T extends BaseDbConfig> {
})
}

async getAll (): Promise<T> {
if (this.db == null) {
throw new Error('Database not opened')
}
const transaction = this.db.transaction(this.storeName, 'readonly')
const store = transaction.objectStore(this.storeName)

return new Promise((resolve, reject) => {
// @ts-expect-error - its empty right now...
const result: { [K in keyof T]: T[K] } = {}
const request = store.openCursor()

request.onerror = () => {
reject(request.error ?? new Error(`Could not get all keys and values from store "${this.storeName}"`))
}

request.onsuccess = () => {
const cursor = request.result
if (cursor != null) {
result[cursor.key as keyof T] = cursor.value as T[keyof T]
cursor.continue()
} else {
resolve(result)
}
}
})
}

close (): void {
if (this.db != null) {
this.db.close()
Expand Down
36 changes: 28 additions & 8 deletions src/pages/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Input from '../components/textarea-input.jsx'
import { ConfigContext, ConfigProvider } from '../context/config-context.jsx'
import { RouteContext } from '../context/router-context.jsx'
import { ServiceWorkerProvider } from '../context/service-worker-context.jsx'
import { setConfig as storeConfig } from '../lib/config-db.js'
import { getConfig, setConfig as storeConfig } from '../lib/config-db.js'
import { convertDnsResolverInputToObject, convertDnsResolverObjectToInput, convertUrlArrayToInput, convertUrlInputToArray } from '../lib/input-helpers.js'
import { isConfigPage } from '../lib/is-config-page.js'
import { getUiComponentLogger, uiLogger } from '../lib/logger.js'
Expand Down Expand Up @@ -82,7 +82,7 @@ export interface ConfigPageProps extends React.HTMLProps<HTMLElement> {
// Config Page can be loaded either as a page or as a component in the landing helper-ui page
const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
const { gotoPage } = useContext(RouteContext)
const { setConfig, resetConfig, gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, _supportsSubdomains } = useContext(ConfigContext)
const { isLoading, setConfig, resetConfig, gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, _supportsSubdomains } = useContext(ConfigContext)
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [resetKey, setResetKey] = useState(0)
Expand All @@ -107,11 +107,14 @@ const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
}, [gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport])

useEffect(() => {
if (isLoading) {
return
}
/**
* 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()
}, [])
}, [isLoading])

const saveConfig = useCallback(async () => {
try {
Expand All @@ -133,6 +136,13 @@ const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
setError(err as Error)
} finally {
setIsSaving(false)
await getConfig(uiComponentLogger).then((config) => {
// eslint-disable-next-line no-console
console.log('config directly from idb: ', config)
}).catch((err) => {
// eslint-disable-next-line no-console
console.error('error getting config directly from idb: ', err)
})
}
}, [gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport])

Expand All @@ -143,30 +153,40 @@ const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
setResetKey((prev) => prev + 1)
}, [])

console.log('config-page: isLoading', isLoading)

// if (isLoading) {
// return null
// }

// eslint-disable-next-line no-console
console.log('config is done loading: ', { gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport })
// console.log('config directly from idb: ')

return (
<>
{isConfigPage(window.location.hash) ? <Header /> : null}
<section className='e2e-config-page pa4-l bg-snow mw7 center pa4'>
<h1 className='pa0 f3 ma0 mb4 teal tc'>Configure your IPFS Gateway</h1>
<InputSection label='Direct Retrieval'>
<InputToggle
className="e2e-config-page-input"
className="e2e-config-page-input e2e-config-page-input-enableGatewayProviders"
label="Enable Delegated HTTP Gateway Providers"
description="Use gateway providers returned from delegated routers for direct retrieval."
value={enableGatewayProviders}
onChange={(value) => { setConfig('enableGatewayProviders', value) }}
resetKey={resetKey}
/>
<InputToggle
className="e2e-config-page-input"
className="e2e-config-page-input e2e-config-page-input-enableWss"
label="Enable Secure WebSocket Providers"
description="Use Secure WebSocket providers returned from delegated routers for direct retrieval."
value={enableWss}
onChange={(value) => { setConfig('enableWss', value) }}
resetKey={resetKey}
/>
<InputToggle
className="e2e-config-page-input"
className="e2e-config-page-input e2e-config-page-input-enableWebTransport"
label="Enable WebTransport Providers"
description="Use WebTransport providers returned from delegated routers for direct retrieval."
value={enableWebTransport}
Expand All @@ -186,7 +206,7 @@ const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
</InputSection>
<InputSection label='Fallback Retrieval'>
<InputToggle
className="e2e-config-page-input"
className="e2e-config-page-input e2e-config-page-input-enableRecursiveGateways"
label="Enable Recursive Gateways"
description="Use recursive gateways configured below for retrieval of content."
value={enableRecursiveGateways}
Expand Down Expand Up @@ -216,7 +236,7 @@ const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
resetKey={resetKey}
/>
<Input
className="e2e-config-page-input"
className="e2e-config-page-input e2e-config-page-input-debug"
description="A string that enables debug logging. Use '*,*:trace' to enable all debug logging."
label='Debug'
value={debug}
Expand Down
34 changes: 34 additions & 0 deletions test-e2e/config-ui.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Ensure the config saves to IDB and on refresh, the config is loaded from IDB
*/

import { testPathRouting as test, expect } from './fixtures/config-test-fixtures.js'
import { getConfig, getConfigUi, setConfigViaUi } from './fixtures/set-sw-config.js'

test.describe('config-ui', () => {
test('setting the config via UI actually works', async ({ page, protocol, rootDomain }) => {
await page.goto(`${protocol}//${rootDomain}`)
// await waitForServiceWorker(page)

// read the config from the page
const config = await getConfigUi({ page })

// change the config
const testConfig: typeof config = {
...config,
gateways: ['https://example.com'],
routers: ['https://example2.com']
}

// change the UI & save it
await setConfigViaUi({ page, config: testConfig })

// verify that the IndexedDB has the new config
expect(await getConfig({ page })).toMatchObject(testConfig)

// reload the page, and ensure the config is the same as the one we set
await page.reload()
expect(await getConfigUi({ page })).toMatchObject(testConfig)
expect(await getConfig({ page })).toMatchObject(testConfig)
})
})
22 changes: 1 addition & 21 deletions test-e2e/fixtures/config-test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test as base, type Page } from '@playwright/test'
import { setConfig, setSubdomainConfig } from './set-sw-config.js'
import { setConfig } from './set-sw-config.js'
import { waitForServiceWorker } from './wait-for-service-worker.js'

function isNoServiceWorkerProject <T extends typeof base = typeof base> (test: T): boolean {
Expand Down Expand Up @@ -90,26 +90,6 @@ export const testSubdomainRouting = test.extend<{ rootDomain: string, baseURL: s
throw new Error('KUBO_GATEWAY not set')
}
const kuboGateway = process.env.KUBO_GATEWAY
const oldPageGoto = page.goto.bind(page)
page.goto = async (url: Parameters<Page['goto']>[0], options: Parameters<Page['goto']>[1]): ReturnType<Page['goto']> => {
const response = await oldPageGoto(url, options)
if (['.ipfs.', '.ipns.'].some((part) => url.includes(part))) {
await setSubdomainConfig({
page,
config: {
autoReload: true,
gateways: [kuboGateway],
routers: [kuboGateway],
dnsJsonResolvers: {
'.': 'https://delegated-ipfs.dev/dns-query'
}
}
})
} else {
// already set on root.
}
return response
}

// set config for the initial page
await setConfig({
Expand Down
10 changes: 7 additions & 3 deletions test-e2e/fixtures/locators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ export const getConfigPage: GetLocator = (page) => page.locator('.e2e-config-pag
export const getConfigPageInput: GetLocator = (page) => page.locator('.e2e-config-page-input')
export const getConfigPageSaveButton: GetLocator = (page) => page.locator('.e2e-config-page-button#save-config')
export const getIframeLocator: GetFrameLocator = (page) => page.frameLocator('iframe')
export const getConfigGatewaysInput: GetLocator = (page) => page.locator('.e2e-config-page-input-gateways')
export const getConfigEnableGatewayProviders: GetLocator = (page) => page.locator('.e2e-config-page-input-enableGatewayProviders')
export const getConfigEnableWss: GetLocator = (page) => page.locator('.e2e-config-page-input-enableWss')
export const getConfigEnableWebTransport: GetLocator = (page) => page.locator('.e2e-config-page-input-enableWebTransport')
export const getConfigRoutersInput: GetLocator = (page) => page.locator('.e2e-config-page-input-routers')
export const getConfigAutoReloadInput: GetLocator = (page) => page.locator('.e2e-config-page-input-autoreload')
export const getConfigEnableRecursiveGateways: GetLocator = (page) => page.locator('.e2e-config-page-input-enableRecursiveGateways')
export const getConfigGatewaysInput: GetLocator = (page) => page.locator('.e2e-config-page-input-gateways')
export const getConfigDnsJsonResolvers: GetLocator = (page) => page.locator('.e2e-config-page-input-dnsJsonResolvers')
export const getConfigDebug: GetLocator = (page) => page.locator('.e2e-config-page-input-debug')

export const getNoServiceWorkerError: GetLocator = (page) => page.locator('.e2e-no-service-worker-error')

Expand All @@ -31,5 +36,4 @@ export const getAboutSection: GetLocator = (page) => page.locator('.e2e-about-se
export const getConfigButtonIframe: GetLocator = (page) => getIframeLocator(page).locator('.e2e-collapsible-button')
export const getConfigGatewaysInputIframe: GetLocator = (page) => getConfigGatewaysInput(getIframeLocator(page))
export const getConfigRoutersInputIframe: GetLocator = (page) => getConfigRoutersInput(getIframeLocator(page))
export const getConfigAutoReloadInputIframe: GetLocator = (page) => getConfigAutoReloadInput(getIframeLocator(page))
export const getConfigPageSaveButtonIframe: GetLocator = (page) => getConfigPageSaveButton(getIframeLocator(page))
Loading

0 comments on commit 0f8e84f

Please sign in to comment.