Skip to content

Commit 0701c71

Browse files
SgtPooki2color
andauthored
feat: add server timing to responses (#164)
* feat: add server timing to responses * chore: remove unique id on stream-and-chunk * fix: more timings and add timing header before err * fix: serverTiming is easy to add to functions * test: add server timing tests * fix: serverTiming duration testing is robust across platforms * fix: server timing headers are not included by default * docs: explain server timing header capabilities * docs: fix server timing docs and generate new readme * chore: pr suggestion Co-authored-by: Daniel Norman <1992255+2color@users.noreply.github.com> * feat: ipns+dnslink server timing, renames & refactoring * chore: remove unnecessary null coalescing operator --------- Co-authored-by: Daniel Norman <1992255+2color@users.noreply.github.com>
1 parent 430d40d commit 0701c71

8 files changed

Lines changed: 261 additions & 29 deletions

File tree

packages/verified-fetch/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,14 @@ Some known header specifications:
635635
- <https://specs.ipfs.tech/http-gateways/trustless-gateway/#response-headers>
636636
- <https://specs.ipfs.tech/http-gateways/subdomain-gateway/#response-headers>
637637

638+
#### Server Timing headers
639+
640+
By default, we do not include [Server Timing](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Server_timing) headers in responses. If you want to include them, you can pass an
641+
`withServerTiming` option to the `createVerifiedFetch` function to include them in all future responses. You can
642+
also pass the `withServerTiming` option to each fetch call to include them only for that specific response.
643+
644+
See PR where this was added, <https://github.com/ipfs/helia-verified-fetch/pull/164>, for more information.
645+
638646
### Possible Scenarios that could cause confusion
639647

640648
#### Attempting to fetch the CID for content that does not make sense

packages/verified-fetch/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,14 @@
604604
* * https://specs.ipfs.tech/http-gateways/trustless-gateway/#response-headers
605605
* * https://specs.ipfs.tech/http-gateways/subdomain-gateway/#response-headers
606606
*
607+
* #### Server Timing headers
608+
*
609+
* By default, we do not include [Server Timing](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Server_timing) headers in responses. If you want to include them, you can pass an
610+
* `withServerTiming` option to the `createVerifiedFetch` function to include them in all future responses. You can
611+
* also pass the `withServerTiming` option to each fetch call to include them only for that specific response.
612+
*
613+
* See PR where this was added, https://github.com/ipfs/helia-verified-fetch/pull/164, for more information.
614+
*
607615
* ### Possible Scenarios that could cause confusion
608616
*
609617
* #### Attempting to fetch the CID for content that does not make sense
@@ -750,6 +758,14 @@ export interface CreateVerifiedFetchOptions {
750758
* @default 60000
751759
*/
752760
sessionTTLms?: number
761+
762+
/**
763+
* Whether to include server-timing headers in responses. This option can be overridden on a per-request basis.
764+
*
765+
* @default false
766+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
767+
*/
768+
withServerTiming?: boolean
753769
}
754770

755771
export type { ContentTypeParser } from './types.js'
@@ -817,6 +833,14 @@ export interface VerifiedFetchInit extends RequestInit, ProgressOptions<BubbledP
817833
* @default false
818834
*/
819835
allowInsecure?: boolean
836+
837+
/**
838+
* Whether to include server-timing headers in the response for an individual request.
839+
*
840+
* @default false
841+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
842+
*/
843+
withServerTiming?: boolean
820844
}
821845

822846
/**

packages/verified-fetch/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export interface FetchHandlerFunctionArg {
2929
* The originally requested resource
3030
*/
3131
resource: string
32+
33+
/**
34+
* Whether to include server-timing headers in the response.
35+
*/
36+
withServerTiming: boolean
3237
}
3338

3439
/**

packages/verified-fetch/src/utils/parse-resource.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ export interface ParseResourceComponents {
1111
}
1212

1313
export interface ParseResourceOptions extends ParseUrlStringOptions {
14-
14+
withServerTiming?: boolean
1515
}
1616
/**
1717
* Handles the different use cases for the `resource` argument.
1818
* The resource can represent an IPFS path, IPNS path, or CID.
1919
* If the resource represents an IPNS path, we need to resolve it to a CID.
2020
*/
21-
export async function parseResource (resource: Resource, { ipns, logger }: ParseResourceComponents, options?: ParseResourceOptions): Promise<ParsedUrlStringResults> {
21+
export async function parseResource (resource: Resource, { ipns, logger }: ParseResourceComponents, { withServerTiming = false, ...options }: ParseResourceOptions = { withServerTiming: false }): Promise<ParsedUrlStringResults> {
2222
if (typeof resource === 'string') {
23-
return parseUrlString({ urlString: resource, ipns, logger }, options)
23+
return parseUrlString({ urlString: resource, ipns, logger, withServerTiming }, options)
2424
}
2525

2626
const cid = CID.asCID(resource)
@@ -33,7 +33,8 @@ export async function parseResource (resource: Resource, { ipns, logger }: Parse
3333
path: '',
3434
query: {},
3535
ipfsPath: `/ipfs/${cid.toString()}`,
36-
ttl: 29030400 // 1 year for ipfs content
36+
ttl: 29030400, // 1 year for ipfs content
37+
serverTimings: []
3738
} satisfies ParsedUrlStringResults
3839
}
3940

packages/verified-fetch/src/utils/parse-url-string.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CID } from 'multiformats/cid'
22
import { getPeerIdFromString } from './get-peer-id-from-string.js'
3+
import { serverTiming, type ServerTimingResult } from './server-timing.js'
34
import { TLRU } from './tlru.js'
45
import type { RequestFormatShorthand } from '../types.js'
56
import type { DNSLinkResolveResult, IPNS, IPNSResolveResult, IPNSRoutingEvents, ResolveDNSLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
@@ -12,6 +13,7 @@ export interface ParseUrlStringInput {
1213
urlString: string
1314
ipns: IPNS
1415
logger: ComponentLogger
16+
withServerTiming?: boolean
1517
}
1618
export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDNSLinkProgressEvents>, AbortOptions {
1719

@@ -23,7 +25,7 @@ export interface ParsedUrlQuery extends Record<string, string | unknown> {
2325
filename?: string
2426
}
2527

26-
interface ParsedUrlStringResultsBase extends ResolveResult {
28+
export interface ParsedUrlStringResults extends ResolveResult {
2729
protocol: 'ipfs' | 'ipns'
2830
query: ParsedUrlQuery
2931

@@ -41,9 +43,12 @@ interface ParsedUrlStringResultsBase extends ResolveResult {
4143
* seconds as a number
4244
*/
4345
ttl?: number
44-
}
4546

46-
export type ParsedUrlStringResults = ParsedUrlStringResultsBase
47+
/**
48+
* serverTiming items
49+
*/
50+
serverTimings: Array<ServerTimingResult<any>>
51+
}
4752

4853
const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
4954
const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
@@ -145,14 +150,15 @@ function dnsLinkLabelDecoder (linkLabel: string): string {
145150
* @todo we need to break out each step of this function (cid parsing, ipns resolving, dnslink resolving) into separate functions and then remove the eslint-disable comment
146151
*/
147152
// eslint-disable-next-line complexity
148-
export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
153+
export async function parseUrlString ({ urlString, ipns, logger, withServerTiming = false }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
149154
const log = logger.forComponent('helia:verified-fetch:parse-url-string')
150155
const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = matchURLString(urlString)
151156

152157
let cid: CID | undefined
153158
let resolvedPath: string | undefined
154159
const errors: Error[] = []
155160
let resolveResult: IPNSResolveResult | DNSLinkResolveResult | undefined
161+
const serverTimings: Array<ServerTimingResult<any>> = []
156162

157163
if (protocol === 'ipfs') {
158164
try {
@@ -182,7 +188,19 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
182188
if (peerId.publicKey == null) {
183189
throw new TypeError('cidOrPeerIdOrDnsLink contains no public key')
184190
}
185-
resolveResult = await ipns.resolve(peerId.publicKey, options)
191+
192+
if (withServerTiming) {
193+
const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, ipns.resolve.bind(null, peerId.publicKey, options))
194+
serverTimings.push(resolveResultWithServerTiming)
195+
196+
// eslint-disable-next-line max-depth
197+
if (resolveResultWithServerTiming.error != null) {
198+
throw resolveResultWithServerTiming.error
199+
}
200+
resolveResult = resolveResultWithServerTiming.result
201+
} else {
202+
resolveResult = await ipns.resolve(peerId.publicKey, options)
203+
}
186204
cid = resolveResult?.cid
187205
resolvedPath = resolveResult?.path
188206
log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
@@ -207,7 +225,19 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
207225
log.trace('Attempting to resolve DNSLink for %s', decodedDnsLinkLabel)
208226

209227
try {
210-
resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
228+
// eslint-disable-next-line max-depth
229+
if (withServerTiming) {
230+
const resolveResultWithServerTiming = await serverTiming('ipns.resolveDNSLink', `Resolve DNSLink ${decodedDnsLinkLabel}`, ipns.resolveDNSLink.bind(ipns, decodedDnsLinkLabel, options))
231+
serverTimings.push(resolveResultWithServerTiming)
232+
// eslint-disable-next-line max-depth
233+
if (resolveResultWithServerTiming.error != null) {
234+
throw resolveResultWithServerTiming.error
235+
}
236+
resolveResult = resolveResultWithServerTiming.result
237+
} else {
238+
resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
239+
}
240+
211241
cid = resolveResult?.cid
212242
resolvedPath = resolveResult?.path
213243
log.trace('resolved %s to %c', decodedDnsLinkLabel, cid)
@@ -263,7 +293,8 @@ export async function parseUrlString ({ urlString, ipns, logger }: ParseUrlStrin
263293
path: joinPaths(resolvedPath, urlPath ?? ''),
264294
query,
265295
ttl,
266-
ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`
296+
ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`,
297+
serverTimings
267298
} satisfies ParsedUrlStringResults
268299
}
269300

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export interface ServerTimingSuccess<T> {
2+
error: null
3+
result: T
4+
header: string
5+
}
6+
export interface ServerTimingError {
7+
result: null
8+
error: Error
9+
header: string
10+
}
11+
export type ServerTimingResult<T> = ServerTimingSuccess<T> | ServerTimingError
12+
13+
export async function serverTiming<T> (
14+
name: string,
15+
description: string,
16+
fn: () => Promise<T>
17+
): Promise<ServerTimingResult<T>> {
18+
const startTime = performance.now()
19+
20+
try {
21+
const result = await fn() // Execute the function
22+
const endTime = performance.now()
23+
24+
const duration = (endTime - startTime).toFixed(1) // Duration in milliseconds
25+
26+
// Create the Server-Timing header string
27+
const header = `${name};dur=${duration};desc="${description}"`
28+
return { result, header, error: null }
29+
} catch (error: any) {
30+
const endTime = performance.now()
31+
const duration = (endTime - startTime).toFixed(1)
32+
33+
// Still return a timing header even on error
34+
const header = `${name};dur=${duration};desc="${description}"`
35+
return { result: null, error, header } // Pass error with timing info
36+
}
37+
}

0 commit comments

Comments
 (0)