Skip to content

Commit b3a9b8d

Browse files
authored
feat: add sw registration ttl config (#809)
* fix: use config based service worker registration TTL * feat: add sw registration TTL config input * test: fix test and ensure timestamp is available * chore: minor comment fix
1 parent a787aa7 commit b3a9b8d

File tree

8 files changed

+78
-19
lines changed

8 files changed

+78
-19
lines changed

src/context/config-context.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { createContext, useCallback, useEffect, useState } from 'react'
2-
import { defaultDebug, defaultDnsJsonResolvers, defaultEnableGatewayProviders, defaultEnableRecursiveGateways, defaultEnableWebTransport, defaultEnableWss, defaultFetchTimeout, defaultGateways, defaultRouters, defaultSupportsSubdomains, getConfig, resetConfig } from '../lib/config-db.js'
2+
import { defaultDebug, defaultDnsJsonResolvers, defaultEnableGatewayProviders, defaultEnableRecursiveGateways, defaultEnableWebTransport, defaultEnableWss, defaultFetchTimeout, defaultGateways, defaultRouters, defaultServiceWorkerRegistrationTTL, defaultSupportsSubdomains, getConfig, resetConfig } from '../lib/config-db.js'
33
import { getUiComponentLogger } from '../lib/logger.js'
44
import type { ConfigDb } from '../lib/config-db.js'
55
import type { ComponentLogger } from '@libp2p/logger'
@@ -26,6 +26,7 @@ export const ConfigContext = createContext<ConfigContextType>({
2626
debug: defaultDebug(),
2727
fetchTimeout: defaultFetchTimeout,
2828
_supportsSubdomains: defaultSupportsSubdomains,
29+
serviceWorkerRegistrationTTL: defaultServiceWorkerRegistrationTTL,
2930
isLoading: true
3031
})
3132

@@ -40,6 +41,7 @@ export const ConfigProvider: React.FC<{ children: ReactElement[] | ReactElement,
4041
const [enableRecursiveGateways, setEnableRecursiveGateways] = useState(defaultEnableRecursiveGateways)
4142
const [debug, setDebug] = useState(defaultDebug())
4243
const [fetchTimeout, setFetchTimeout] = useState(defaultFetchTimeout)
44+
const [serviceWorkerRegistrationTTL, setServiceWorkerRegistrationTTL] = useState(defaultServiceWorkerRegistrationTTL)
4345
const [_supportsSubdomains, setSupportsSubdomains] = useState(defaultSupportsSubdomains)
4446
const logger = getUiComponentLogger('config-context')
4547
const log = logger.forComponent('main')
@@ -55,6 +57,7 @@ export const ConfigProvider: React.FC<{ children: ReactElement[] | ReactElement,
5557
setEnableRecursiveGateways(config.enableRecursiveGateways)
5658
setDebug(config.debug)
5759
setFetchTimeout(config.fetchTimeout)
60+
setServiceWorkerRegistrationTTL(config.serviceWorkerRegistrationTTL)
5861
}
5962
/**
6063
* We need to make sure that the configDb types are loaded with the values from IDB
@@ -99,6 +102,9 @@ export const ConfigProvider: React.FC<{ children: ReactElement[] | ReactElement,
99102
case 'fetchTimeout':
100103
setFetchTimeout(value)
101104
break
105+
case 'serviceWorkerRegistrationTTL':
106+
setServiceWorkerRegistrationTTL(value)
107+
break
102108
case '_supportsSubdomains':
103109
setSupportsSubdomains(value)
104110
break
@@ -124,6 +130,7 @@ export const ConfigProvider: React.FC<{ children: ReactElement[] | ReactElement,
124130
enableGatewayProviders,
125131
enableRecursiveGateways,
126132
fetchTimeout,
133+
serviceWorkerRegistrationTTL,
127134
debug,
128135
_supportsSubdomains,
129136
isLoading

src/lib/config-db.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ export interface ConfigDbWithoutPrivateFields extends BaseDbConfig {
1818
* The timeout for fetching content from the gateway, in milliseconds. User input is in seconds, but we store in milliseconds.
1919
*/
2020
fetchTimeout: number
21+
22+
/**
23+
* The TTL (time to live) for the service worker, in milliseconds.
24+
* This is used to determine if the service worker should be unregistered, in order to trigger a new install.
25+
*
26+
* @see https://github.com/ipfs/service-worker-gateway/issues/724
27+
*
28+
* @default 86_400_000 (24 hours)
29+
*/
30+
serviceWorkerRegistrationTTL: number
2131
}
2232

2333
/**
@@ -38,6 +48,7 @@ export const defaultEnableWss = true
3848
export const defaultEnableWebTransport = false
3949
export const defaultEnableGatewayProviders = true
4050
export const defaultSupportsSubdomains: null | boolean = null
51+
export const defaultServiceWorkerRegistrationTTL = 86_400_000 // 24 hours
4152

4253
/**
4354
* The default fetch timeout for the gateway, in seconds.
@@ -64,6 +75,7 @@ export async function resetConfig (logger: ComponentLogger): Promise<void> {
6475
await configDb.put('enableGatewayProviders', defaultEnableGatewayProviders)
6576
await configDb.put('debug', defaultDebug())
6677
await configDb.put('fetchTimeout', defaultFetchTimeout * 1000)
78+
await configDb.put('serviceWorkerRegistrationTTL', defaultServiceWorkerRegistrationTTL)
6779
// leave private/app-only fields as is
6880
} catch (err) {
6981
log('error resetting config in db', err)
@@ -88,6 +100,7 @@ export async function setConfig (config: ConfigDbWithoutPrivateFields, logger: C
88100
await configDb.put('enableGatewayProviders', config.enableGatewayProviders)
89101
await configDb.put('debug', config.debug ?? defaultDebug())
90102
await configDb.put('fetchTimeout', config.fetchTimeout ?? (defaultFetchTimeout * 1000))
103+
await configDb.put('serviceWorkerRegistrationTTL', config.serviceWorkerRegistrationTTL ?? (defaultServiceWorkerRegistrationTTL * 1000))
91104
// ignore private/app-only fields
92105
} catch (err) {
93106
log('error setting config in db', err)
@@ -119,6 +132,7 @@ export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {
119132
let enableGatewayProviders
120133
let fetchTimeout
121134
let debug = ''
135+
let serviceWorkerRegistrationTTL = defaultServiceWorkerRegistrationTTL
122136
let _supportsSubdomains = defaultSupportsSubdomains
123137

124138
let config: ConfigDb
@@ -142,6 +156,7 @@ export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {
142156
enableGatewayProviders = config.enableGatewayProviders ?? defaultEnableGatewayProviders
143157
fetchTimeout = config.fetchTimeout ?? (defaultFetchTimeout * 1000)
144158
_supportsSubdomains ??= config._supportsSubdomains
159+
serviceWorkerRegistrationTTL = config.serviceWorkerRegistrationTTL ?? defaultServiceWorkerRegistrationTTL
145160
} catch (err) {
146161
log('error loading config from db', err)
147162
} finally {
@@ -170,6 +185,7 @@ export async function getConfig (logger: ComponentLogger): Promise<ConfigDb> {
170185
enableGatewayProviders,
171186
debug,
172187
fetchTimeout,
188+
serviceWorkerRegistrationTTL,
173189
_supportsSubdomains
174190
}
175191
})().finally(() => {

src/pages/config.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,14 @@ export interface ConfigPageProps extends React.HTMLProps<HTMLElement> {
8383

8484
// Config Page can be loaded either as a page or as a component in the landing helper-ui page
8585
const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
86-
const { setConfig, resetConfig, gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, fetchTimeout, isLoading: isConfigDataLoading } = useContext(ConfigContext)
86+
const { setConfig, resetConfig, gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, fetchTimeout, serviceWorkerRegistrationTTL, isLoading: isConfigDataLoading } = useContext(ConfigContext)
8787
const [isSaving, setIsSaving] = useState(false)
8888
const [error, setError] = useState<Error | null>(null)
8989
const [resetKey, setResetKey] = useState(0)
9090

9191
const saveConfig = useCallback(async () => {
9292
try {
93-
const config = { gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, fetchTimeout }
93+
const config = { gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, fetchTimeout, serviceWorkerRegistrationTTL }
9494
setIsSaving(true)
9595
await storeConfig(config, uiComponentLogger)
9696
log.trace('config-page: sending RELOAD_CONFIG to service worker')
@@ -104,7 +104,7 @@ const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
104104
} finally {
105105
setIsSaving(false)
106106
}
107-
}, [gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, fetchTimeout])
107+
}, [gateways, routers, dnsJsonResolvers, debug, enableGatewayProviders, enableRecursiveGateways, enableWss, enableWebTransport, fetchTimeout, serviceWorkerRegistrationTTL])
108108

109109
const doResetConfig = useCallback(async () => {
110110
// we need to clear out the localStorage items and make sure default values are set, and that all of our inputs are updated
@@ -207,6 +207,21 @@ const ConfigPage: FunctionComponent<ConfigPageProps> = () => {
207207
onChange={(value) => { setConfig('fetchTimeout', value) }}
208208
resetKey={resetKey}
209209
/>
210+
<NumberInput
211+
className='e2e-config-page-input e2e-config-page-input-serviceWorkerRegistrationTTL'
212+
description='The time for the service worker registration to last, in hours, prior to triggering an explicit unregister event.'
213+
label='Service Worker Registration TTL'
214+
value={serviceWorkerRegistrationTTL / 1000 / 60 / 60}
215+
validationFn={(value) => {
216+
if (value < 0.01) {
217+
return new Error('Service worker registration TTL must be at least 0.01 hours')
218+
}
219+
return null
220+
}}
221+
preSaveFormat={(value) => value * 1000 * 60 * 60}
222+
onChange={(value) => { setConfig('serviceWorkerRegistrationTTL', value) }}
223+
resetKey={resetKey}
224+
/>
210225
<TextInput
211226
className='e2e-config-page-input e2e-config-page-input-debug'
212227
description="A string that enables debug logging. Use '*,*:trace' to enable all debug logging."

src/sw.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ self.addEventListener('fetch', (event) => {
177177
const request = event.request
178178
const urlString = request.url
179179
const url = new URL(urlString)
180+
if (firstInstallTime == null) {
181+
// if service worker is shut down, the firstInstallTime may be null
182+
log('firstInstallTime is null, getting install timestamp')
183+
event.waitUntil(getInstallTimestamp())
184+
}
185+
180186
log.trace('incoming request url: %s:', event.request.url)
181187

182188
event.waitUntil(requestRouting(event, url).then(async (shouldHandle) => {
@@ -195,8 +201,8 @@ self.addEventListener('fetch', (event) => {
195201
******************************************************
196202
*/
197203
async function requestRouting (event: FetchEvent, url: URL): Promise<boolean> {
198-
if (await isTimebombExpired()) {
199-
log.trace('timebomb expired, unregistering service worker')
204+
if (!isServiceWorkerRegistrationTTLValid()) {
205+
log.trace('Service worker registration TTL expired, unregistering service worker')
200206
event.waitUntil(self.registration.unregister())
201207
return false
202208
} else if (isUnregisterRequest(event.request.url)) {
@@ -748,12 +754,22 @@ function getResponseDetails (response: Response, responseBody: string): Response
748754
}
749755
}
750756

751-
async function isTimebombExpired (): Promise<boolean> {
752-
firstInstallTime = firstInstallTime ?? await getInstallTimestamp()
757+
function isServiceWorkerRegistrationTTLValid (): boolean {
758+
if (!navigator.onLine) {
759+
/**
760+
* When we unregister the service worker, the a new one will be installed on the next page load.
761+
*
762+
* @see https://github.com/ipfs/service-worker-gateway/issues/724
763+
*/
764+
return true
765+
}
766+
if (firstInstallTime == null || config?.serviceWorkerRegistrationTTL == null) {
767+
// no firstInstallTime or serviceWorkerRegistrationTTL, assume new and valid
768+
return true
769+
}
770+
753771
const now = Date.now()
754-
// max life (for now) is 24 hours
755-
const timebomb = 24 * 60 * 60 * 1000
756-
return now - firstInstallTime > timebomb
772+
return now - firstInstallTime <= config.serviceWorkerRegistrationTTL
757773
}
758774

759775
async function getInstallTimestamp (): Promise<number> {
@@ -789,7 +805,7 @@ async function setOriginIsolationWarningAccepted (): Promise<void> {
789805
await swidb.put('originIsolationWarningAccepted', true)
790806
swidb.close()
791807
} catch (e) {
792-
log.error('addInstallTimestampToConfig error: ', e)
808+
log.error('setOriginIsolationWarningAccepted error: ', e)
793809
}
794810
}
795811

test-e2e/config-loading.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ test.describe('ipfs-sw configuration', () => {
1818
enableWebTransport: true,
1919
enableRecursiveGateways: false,
2020
enableGatewayProviders: false,
21-
fetchTimeout: 29 * 1000
21+
fetchTimeout: 29 * 1000,
22+
serviceWorkerRegistrationTTL: 24 * 60 * 60 * 1000
2223
}
2324
test.beforeAll(async () => {
2425
if (process.env.KUBO_GATEWAY == null || process.env.KUBO_GATEWAY === '') {
@@ -101,7 +102,8 @@ test.describe('ipfs-sw configuration', () => {
101102
enableWss: !testConfig.enableWss,
102103
enableWebTransport: !testConfig.enableWebTransport,
103104
enableRecursiveGateways: !testConfig.enableRecursiveGateways,
104-
enableGatewayProviders: !testConfig.enableGatewayProviders
105+
enableGatewayProviders: !testConfig.enableGatewayProviders,
106+
serviceWorkerRegistrationTTL: 86_400_000
105107
}
106108
const compressedConfig = await compressConfig(newConfig)
107109
const responses: PlaywrightResponse[] = []

test-e2e/fixtures/locators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const getConfigGatewaysInput: GetLocator = (page) => page.locator('.e2e-c
2424
export const getConfigDnsJsonResolvers: GetLocator = (page) => page.locator('.e2e-config-page-input-dnsJsonResolvers')
2525
export const getConfigDebug: GetLocator = (page) => page.locator('.e2e-config-page-input-debug')
2626
export const getConfigFetchTimeout: GetLocator = (page) => page.locator('.e2e-config-page-input-fetchTimeout')
27+
export const getConfigServiceWorkerRegistrationTTL: GetLocator = (page) => page.locator('.e2e-config-page-input-serviceWorkerRegistrationTTL')
2728
export const getNoServiceWorkerError: GetLocator = (page) => page.locator('.e2e-no-service-worker-error')
2829

2930
export const getHelperUi: GetLocator = (page) => page.locator('.e2e-helper-ui')

test-e2e/fixtures/set-sw-config.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*
55
* Note that this was only tested and confirmed working for subdomain pages.
66
*/
7-
import { getConfigDebug, getConfigDnsJsonResolvers, getConfigEnableGatewayProviders, getConfigEnableRecursiveGateways, getConfigEnableWebTransport, getConfigEnableWss, getConfigFetchTimeout, getConfigGatewaysInput, getConfigPage, getConfigPageSaveButton, getConfigRoutersInput } from './locators.js'
7+
import { getConfigDebug, getConfigDnsJsonResolvers, getConfigEnableGatewayProviders, getConfigEnableRecursiveGateways, getConfigEnableWebTransport, getConfigEnableWss, getConfigFetchTimeout, getConfigGatewaysInput, getConfigPage, getConfigPageSaveButton, getConfigRoutersInput, getConfigServiceWorkerRegistrationTTL } from './locators.js'
88
import { waitForServiceWorker } from './wait-for-service-worker.js'
99
import type { ConfigDb, ConfigDbWithoutPrivateFields } from '../../src/lib/config-db.js'
1010
import type { Page } from '@playwright/test'
@@ -91,7 +91,7 @@ export async function getConfigUi ({ page, expectedSwScope }: { page: Page, expe
9191
}, {})
9292
})
9393
const debug = await getConfigDebug(page).locator('textarea').inputValue()
94-
94+
const serviceWorkerRegistrationTTL = parseInt(await getConfigServiceWorkerRegistrationTTL(page).locator('input').inputValue(), 10) * 1000 * 60 * 60 // convert from hours to milliseconds
9595
return {
9696
enableGatewayProviders,
9797
enableWss,
@@ -101,7 +101,8 @@ export async function getConfigUi ({ page, expectedSwScope }: { page: Page, expe
101101
gateways,
102102
dnsJsonResolvers,
103103
debug,
104-
fetchTimeout
104+
fetchTimeout,
105+
serviceWorkerRegistrationTTL
105106
}
106107
}
107108

@@ -183,7 +184,8 @@ export async function getConfig ({ page }: { page: Page }): Promise<ConfigDb> {
183184
enableGatewayProviders: await get('enableGatewayProviders'),
184185
debug: await get('debug'),
185186
_supportsSubdomains: await get('_supportsSubdomains'),
186-
fetchTimeout: await get('fetchTimeout')
187+
fetchTimeout: await get('fetchTimeout'),
188+
serviceWorkerRegistrationTTL: await get('serviceWorkerRegistrationTTL')
187189
}
188190

189191
db.close()

test-e2e/layout.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ test.describe('smoketests', () => {
2828
const inputLocator = getConfigPageInput(page)
2929
// see https://playwright.dev/docs/locators#strictness
3030
await inputLocator.first().waitFor()
31-
expect(await inputLocator.count()).toEqual(9)
31+
expect(await inputLocator.count()).toEqual(10)
3232
const submitButton = getConfigPageSaveButton(page)
3333
await expect(submitButton).toBeVisible()
3434
})

0 commit comments

Comments
 (0)