Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import esbuild from 'esbuild'

const getConfigFilePath = () => {
if (process.env.NODE_ENV === 'test') {
return path.resolve('src/config/index-test.ts')
}

if (process.env.NODE_ENV === 'development') {
return path.resolve('src/config/index-development.ts')
}

return path.resolve('src/config/index.ts')
}

const connectionHintTags = async () => {
const configPath = getConfigFilePath()
const content = await fs.readFile(configPath, 'utf8')
const matches = content.match(/https?:\/\/[^'"\s]+/g) ?? []
const origins = [...new Set(matches.map(url => new URL(url).origin))]

return origins
.map(origin => `<link rel="preconnect" href="${origin}"${origin.startsWith('https://') ? ' crossorigin' : ''} />`)
.join('\n ')
}

const copyPublicFiles = async () => {
const srcDir = path.resolve('public')
const destDir = path.resolve('dist')
Expand Down Expand Up @@ -66,6 +89,7 @@ function gitRevision () {
* @param {string} revision - Pre-computed Git revision string
*/
const injectHtmlPages = async (metafile, revision) => {
const hints = await connectionHintTags()
const htmlFilePaths = await fs.readdir(path.resolve('dist'), { withFileTypes: true })
.then(files => files.filter(file => file.isFile() && file.name.endsWith('.html')))
.then(files => files.map(file => path.resolve('dist', file.name)))
Expand Down Expand Up @@ -124,6 +148,11 @@ const injectHtmlPages = async (metafile, revision) => {
}
}

if (htmlContent.includes('<%= CONNECTION_HINTS %>')) {
htmlContent = htmlContent.replace(/<%= CONNECTION_HINTS %>/g, hints)
console.log(`Injected connection hints into ${path.relative(process.cwd(), htmlFilePath)}.`)
}

if (!htmlContent.includes('<%= GIT_VERSION %>')) {
// if you see this error, make sure you update the HTML file to include an html comment similar to the one in public/index.html
throw new Error(`${path.relative(process.cwd(), htmlFilePath)} does not contain <%= GIT_VERSION %>.`)
Expand Down
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<title>IPFS Service Worker Gateway | <%= GIT_VERSION %></title>
<meta name="description" content="A static HTML page that initializes an instance of IPFS Service Worker Gateway" />
<meta name="robots" content="noindex" />
<%= CONNECTION_HINTS %>
<link rel="icon" href="/ipfs-sw-favicon.ico" type="image/ico"/>
<link rel="shortcut icon" href="/ipfs-sw-favicon.ico" type="image/x-icon"/>
<%= CSS_STYLES %>
Expand Down
3 changes: 3 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { config } from './config/index.ts'
import { QUERY_PARAMS } from './lib/constants.ts'
import { parseRequest } from './lib/parse-request.ts'
import { isSafeOrigin } from './lib/path-or-subdomain.ts'
import { prewarmContentRequest } from './lib/prewarm-content.ts'
import { registerServiceWorker } from './lib/register-service-worker.ts'
import type { ResolvableURI } from './lib/parse-request.ts'

Expand Down Expand Up @@ -159,6 +160,8 @@ async function main (): Promise<void> {
const hasActiveWorker = registration?.active != null

if (!hasActiveWorker) {
prewarmContentRequest(request, config)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We discussed this briefly during colo, I had slight concern that executing prewarm code in the main page may backfire

  • due to the way JS is execurted and sheduled by V8, it may slow things down (execution, request ordering)
  • we end up with redundant requests

Not against it, just noting we should measure if there is visible improvement on top of other things like hints alone (#984 etc)


// install the service worker on the root path of this domain, either
// path or subdomain gateway
await registerServiceWorker()
Expand Down
79 changes: 79 additions & 0 deletions src/lib/prewarm-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { base36 } from 'multiformats/bases/base36'
import type { ResolvableURI } from './parse-request.ts'
import type { Config } from '../config/index.ts'

const FILTER_ADDRS = 'wss,tls,https'
const FILTER_PROTOCOLS = 'unknown,transport-bitswap,transport-ipfs-gateway-http'

function toArray (value: string | string[]): string[] {
return Array.isArray(value) ? value : [value]
}

function joinPath (basePathname: string, suffix: string): string {
const base = basePathname.endsWith('/') ? basePathname.slice(0, -1) : basePathname

if (base.length === 0) {
return suffix
}

return `${base}${suffix}`
}

function warmRequest (url: URL): void {
void fetch(url.toString(), {
method: 'GET',
mode: 'cors',
credentials: 'omit',
cache: 'force-cache',
keepalive: true
}).catch(() => {
// ignore prewarm errors, this is a best-effort optimization
})
}

export function buildPrewarmUrls (request: ResolvableURI, options: Pick<Config, 'routers' | 'dnsResolvers'>): URL[] {
const urls: URL[] = []
const routers = options.routers.map(router => new URL(router))
const resolvers = Object.values(options.dnsResolvers)
.flatMap(value => toArray(value))
.map(resolver => new URL(resolver))

if (request.type === 'internal' || request.type === 'external') {
return urls
}

if (request.protocol === 'ipfs') {
routers.forEach(router => {
const url = new URL(router.toString())
url.pathname = joinPath(url.pathname, `/routing/v1/providers/${request.cid.toString()}`)
url.searchParams.set('filter-addrs', FILTER_ADDRS)
url.searchParams.set('filter-protocols', FILTER_PROTOCOLS)
urls.push(url)
})
}

if (request.protocol === 'ipns') {
routers.forEach(router => {
const url = new URL(router.toString())
url.pathname = joinPath(url.pathname, `/routing/v1/ipns/${request.peerId.toCID().toString(base36)}`)
urls.push(url)
})
}

if (request.protocol === 'dnslink') {
resolvers.forEach(resolver => {
const url = new URL(resolver.toString())
url.searchParams.set('name', `_dnslink.${request.domain}`)
url.searchParams.set('type', 'TXT')
urls.push(url)
})
}

return urls
}

export function prewarmContentRequest (request: ResolvableURI, options: Pick<Config, 'routers' | 'dnsResolvers'>): void {
buildPrewarmUrls(request, options).forEach(url => {
warmRequest(url)
})
}
39 changes: 39 additions & 0 deletions test/lib/prewarm-content.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect } from 'aegir/chai'
import { dnsLinkLabelEncoder } from '../../src/lib/dns-link-labels.ts'
import { parseRequest } from '../../src/lib/parse-request.ts'
import { buildPrewarmUrls } from '../../src/lib/prewarm-content.ts'

const options = {
routers: ['https://delegated-ipfs.dev'],
dnsResolvers: {
'.': 'https://delegated-ipfs.dev/dns-query'
}
}

describe('prewarm-content', () => {
it('builds provider query for ipfs requests', () => {
const request = parseRequest(new URL('https://bafyaaaa.ipfs.localhost:3000/'), new URL('https://localhost:3000'))
const urls = buildPrewarmUrls(request, options)

expect(urls).to.have.lengthOf(1)
expect(urls[0].toString()).to.equal('https://delegated-ipfs.dev/routing/v1/providers/bafyaaaa?filter-addrs=wss%2Ctls%2Chttps&filter-protocols=unknown%2Ctransport-bitswap%2Ctransport-ipfs-gateway-http')
})

it('builds routing query for ipns requests', () => {
const ipnsName = 'k51qzi5uqu5djpzsgwway43y9oc6p6zf2mco05x7yo6njcs9n1u4s19416t69w'
const request = parseRequest(new URL(`https://${ipnsName}.ipns.localhost:3000/`), new URL('https://localhost:3000'))
const urls = buildPrewarmUrls(request, options)

expect(urls).to.have.lengthOf(1)
expect(urls[0].toString()).to.equal(`https://delegated-ipfs.dev/routing/v1/ipns/${ipnsName}`)
})

it('builds dns query for dnslink requests', () => {
const domain = 'docs.ipfs.tech'
const request = parseRequest(new URL(`https://${dnsLinkLabelEncoder(domain)}.ipns.localhost:3000/`), new URL('https://localhost:3000'))
const urls = buildPrewarmUrls(request, options)

expect(urls).to.have.lengthOf(1)
expect(urls[0].toString()).to.equal('https://delegated-ipfs.dev/dns-query?name=_dnslink.docs.ipfs.tech&type=TXT')
})
})
Loading