@helia/verified-fetch 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.min.js +28 -31
- package/dist/src/index.d.ts +22 -11
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +17 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/parse-resource.d.ts +2 -1
- package/dist/src/utils/parse-resource.d.ts.map +1 -1
- package/dist/src/utils/parse-resource.js +4 -3
- package/dist/src/utils/parse-resource.js.map +1 -1
- package/dist/src/utils/parse-url-string.d.ts +8 -3
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +30 -4
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/server-timing.d.ts +13 -0
- package/dist/src/utils/server-timing.d.ts.map +1 -0
- package/dist/src/utils/server-timing.js +19 -0
- package/dist/src/utils/server-timing.js.map +1 -0
- package/dist/src/utils/set-content-type.d.ts +12 -0
- package/dist/src/utils/set-content-type.d.ts.map +1 -0
- package/dist/src/utils/set-content-type.js +28 -0
- package/dist/src/utils/set-content-type.js.map +1 -0
- package/dist/src/utils/type-guards.d.ts +2 -0
- package/dist/src/utils/type-guards.d.ts.map +1 -0
- package/dist/src/utils/type-guards.js +4 -0
- package/dist/src/utils/type-guards.js.map +1 -0
- package/dist/src/utils/walk-path.d.ts +0 -1
- package/dist/src/utils/walk-path.d.ts.map +1 -1
- package/dist/src/utils/walk-path.js +1 -1
- package/dist/src/utils/walk-path.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +9 -1
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +50 -46
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +0 -1
- package/package.json +2 -2
- package/src/index.ts +24 -11
- package/src/types.ts +19 -0
- package/src/utils/parse-resource.ts +5 -4
- package/src/utils/parse-url-string.ts +38 -7
- package/src/utils/server-timing.ts +37 -0
- package/src/utils/set-content-type.ts +38 -0
- package/src/utils/type-guards.ts +3 -0
- package/src/utils/walk-path.ts +1 -1
- package/src/verified-fetch.ts +59 -51
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Logger } from '@libp2p/interface'
|
|
2
|
+
import { type ContentTypeParser } from '../types.js'
|
|
3
|
+
import { isPromise } from './type-guards.js'
|
|
4
|
+
|
|
5
|
+
export interface SetContentTypeOptions {
|
|
6
|
+
bytes: Uint8Array
|
|
7
|
+
path: string
|
|
8
|
+
response: Response
|
|
9
|
+
defaultContentType?: string
|
|
10
|
+
contentTypeParser: ContentTypeParser | undefined
|
|
11
|
+
log: Logger
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function setContentType ({ bytes, path, response, contentTypeParser, log, defaultContentType = 'application/octet-stream' }: SetContentTypeOptions): Promise<void> {
|
|
15
|
+
let contentType: string | undefined
|
|
16
|
+
|
|
17
|
+
if (contentTypeParser != null) {
|
|
18
|
+
try {
|
|
19
|
+
let fileName = path.split('/').pop()?.trim()
|
|
20
|
+
fileName = fileName === '' ? undefined : fileName
|
|
21
|
+
const parsed = contentTypeParser(bytes, fileName)
|
|
22
|
+
|
|
23
|
+
if (isPromise(parsed)) {
|
|
24
|
+
const result = await parsed
|
|
25
|
+
|
|
26
|
+
if (result != null) {
|
|
27
|
+
contentType = result
|
|
28
|
+
}
|
|
29
|
+
} else if (parsed != null) {
|
|
30
|
+
contentType = parsed
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
log.error('error parsing content type', err)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
log.trace('setting content type to "%s"', contentType ?? defaultContentType)
|
|
37
|
+
response.headers.set('content-type', contentType ?? defaultContentType)
|
|
38
|
+
}
|
package/src/utils/walk-path.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface PathWalkerFn {
|
|
|
19
19
|
(blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise<PathWalkerResponse>
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
async function walkPath (blockstore: ReadableStorage, path: string, options?: PathWalkerOptions): Promise<PathWalkerResponse> {
|
|
23
23
|
const ipfsRoots: CID[] = []
|
|
24
24
|
let terminalElement: UnixFSEntry | undefined
|
|
25
25
|
|
package/src/verified-fetch.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { code as dagPbCode } from '@ipld/dag-pb'
|
|
|
6
6
|
import { type AbortOptions, type Logger, type PeerId } from '@libp2p/interface'
|
|
7
7
|
import { Record as DHTRecord } from '@libp2p/kad-dht'
|
|
8
8
|
import { Key } from 'interface-datastore'
|
|
9
|
-
import { exporter } from 'ipfs-unixfs-exporter'
|
|
9
|
+
import { exporter, type ObjectNode } from 'ipfs-unixfs-exporter'
|
|
10
10
|
import toBrowserReadableStream from 'it-to-browser-readablestream'
|
|
11
11
|
import { LRUCache } from 'lru-cache'
|
|
12
12
|
import { type CID } from 'multiformats/cid'
|
|
@@ -32,12 +32,13 @@ import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.js'
|
|
|
32
32
|
import { setCacheControlHeader, setIpfsRoots } from './utils/response-headers.js'
|
|
33
33
|
import { badRequestResponse, movedPermanentlyResponse, notAcceptableResponse, notSupportedResponse, okResponse, badRangeResponse, okRangeResponse, badGatewayResponse, notFoundResponse } from './utils/responses.js'
|
|
34
34
|
import { selectOutputType } from './utils/select-output-type.js'
|
|
35
|
+
import { serverTiming } from './utils/server-timing.js'
|
|
36
|
+
import { setContentType } from './utils/set-content-type.js'
|
|
35
37
|
import { handlePathWalking, isObjectNode } from './utils/walk-path.js'
|
|
36
38
|
import type { CIDDetail, ContentTypeParser, CreateVerifiedFetchOptions, Resource, ResourceDetail, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
37
39
|
import type { FetchHandlerFunctionArg, RequestFormatShorthand } from './types.js'
|
|
38
40
|
import type { Helia, SessionBlockstore } from '@helia/interface'
|
|
39
41
|
import type { Blockstore } from 'interface-blockstore'
|
|
40
|
-
import type { ObjectNode } from 'ipfs-unixfs-exporter'
|
|
41
42
|
|
|
42
43
|
const SESSION_CACHE_MAX_SIZE = 100
|
|
43
44
|
const SESSION_CACHE_TTL_MS = 60 * 1000
|
|
@@ -111,6 +112,8 @@ export class VerifiedFetch {
|
|
|
111
112
|
private readonly log: Logger
|
|
112
113
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
113
114
|
private readonly blockstoreSessions: LRUCache<string, SessionBlockstore>
|
|
115
|
+
private serverTimingHeaders: string[] = []
|
|
116
|
+
private readonly withServerTiming: boolean
|
|
114
117
|
|
|
115
118
|
constructor ({ helia, ipns }: VerifiedFetchComponents, init?: CreateVerifiedFetchOptions) {
|
|
116
119
|
this.helia = helia
|
|
@@ -124,6 +127,7 @@ export class VerifiedFetch {
|
|
|
124
127
|
store.close()
|
|
125
128
|
}
|
|
126
129
|
})
|
|
130
|
+
this.withServerTiming = init?.withServerTiming ?? false
|
|
127
131
|
this.log.trace('created VerifiedFetch instance')
|
|
128
132
|
}
|
|
129
133
|
|
|
@@ -248,13 +252,14 @@ export class VerifiedFetch {
|
|
|
248
252
|
return response
|
|
249
253
|
}
|
|
250
254
|
|
|
251
|
-
private async handleDagCbor ({ resource, cid, path, accept, session, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
255
|
+
private async handleDagCbor ({ resource, cid, path, accept, session, options, withServerTiming }: FetchHandlerFunctionArg): Promise<Response> {
|
|
252
256
|
this.log.trace('fetching %c/%s', cid, path)
|
|
253
257
|
let terminalElement: ObjectNode
|
|
254
258
|
const blockstore = this.getBlockstore(cid, resource, session, options)
|
|
255
259
|
|
|
256
260
|
// need to walk path, if it exists, to get the terminal element
|
|
257
|
-
const pathDetails = await handlePathWalking({ cid, path, resource, options, blockstore, log: this.log })
|
|
261
|
+
const pathDetails = await this.handleServerTiming('path-walking', '', async () => handlePathWalking({ cid, path, resource, options, blockstore, log: this.log, withServerTiming }), withServerTiming)
|
|
262
|
+
|
|
258
263
|
if (pathDetails instanceof Response) {
|
|
259
264
|
return pathDetails
|
|
260
265
|
}
|
|
@@ -312,11 +317,12 @@ export class VerifiedFetch {
|
|
|
312
317
|
return response
|
|
313
318
|
}
|
|
314
319
|
|
|
315
|
-
private async handleDagPb ({ cid, path, resource, session, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
320
|
+
private async handleDagPb ({ cid, path, resource, session, options, withServerTiming }: FetchHandlerFunctionArg): Promise<Response> {
|
|
316
321
|
let redirected = false
|
|
317
322
|
const byteRangeContext = new ByteRangeContext(this.helia.logger, options?.headers)
|
|
318
323
|
const blockstore = this.getBlockstore(cid, resource, session, options)
|
|
319
|
-
const pathDetails = await handlePathWalking({ cid, path, resource, options, blockstore, log: this.log })
|
|
324
|
+
const pathDetails = await this.handleServerTiming('path-walking', '', async () => handlePathWalking({ cid, path, resource, options, blockstore, log: this.log, withServerTiming }), withServerTiming)
|
|
325
|
+
|
|
320
326
|
if (pathDetails instanceof Response) {
|
|
321
327
|
return pathDetails
|
|
322
328
|
}
|
|
@@ -347,10 +353,10 @@ export class VerifiedFetch {
|
|
|
347
353
|
try {
|
|
348
354
|
this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
349
355
|
|
|
350
|
-
const entry = await exporter(`/ipfs/${dirCid}/${rootFilePath}`, this.helia.blockstore, {
|
|
356
|
+
const entry = await this.handleServerTiming('exporter-dir', '', async () => exporter(`/ipfs/${dirCid}/${rootFilePath}`, this.helia.blockstore, {
|
|
351
357
|
signal: options?.signal,
|
|
352
358
|
onProgress: options?.onProgress
|
|
353
|
-
})
|
|
359
|
+
}), withServerTiming)
|
|
354
360
|
|
|
355
361
|
this.log.trace('found root file at %c/%s with cid %c', dirCid, rootFilePath, entry.cid)
|
|
356
362
|
path = rootFilePath
|
|
@@ -375,10 +381,10 @@ export class VerifiedFetch {
|
|
|
375
381
|
this.log.trace('calling exporter for %c/%s with offset=%o & length=%o', resolvedCID, path, offset, length)
|
|
376
382
|
|
|
377
383
|
try {
|
|
378
|
-
const entry = await exporter(resolvedCID, this.helia.blockstore, {
|
|
384
|
+
const entry = await this.handleServerTiming('exporter-file', '', async () => exporter(resolvedCID, this.helia.blockstore, {
|
|
379
385
|
signal: options?.signal,
|
|
380
386
|
onProgress: options?.onProgress
|
|
381
|
-
})
|
|
387
|
+
}), withServerTiming)
|
|
382
388
|
|
|
383
389
|
const asyncIter = entry.content({
|
|
384
390
|
signal: options?.signal,
|
|
@@ -388,17 +394,19 @@ export class VerifiedFetch {
|
|
|
388
394
|
})
|
|
389
395
|
this.log('got async iterator for %c/%s', cid, path)
|
|
390
396
|
|
|
391
|
-
const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
397
|
+
const { stream, firstChunk } = await this.handleServerTiming('stream-and-chunk', '', async () => getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
392
398
|
onProgress: options?.onProgress,
|
|
393
399
|
signal: options?.signal
|
|
394
|
-
})
|
|
400
|
+
}), withServerTiming)
|
|
401
|
+
|
|
395
402
|
byteRangeContext.setBody(stream)
|
|
396
403
|
// if not a valid range request, okRangeRequest will call okResponse
|
|
397
404
|
const response = okRangeResponse(resource, byteRangeContext.getBody(), { byteRangeContext, log: this.log }, {
|
|
398
405
|
redirected
|
|
399
406
|
})
|
|
400
407
|
|
|
401
|
-
await this.setContentType(firstChunk, path, response)
|
|
408
|
+
await this.handleServerTiming('set-content-type', '', async () => setContentType({ bytes: firstChunk, path, response, contentTypeParser: this.contentTypeParser, log: this.log }), withServerTiming)
|
|
409
|
+
|
|
402
410
|
setIpfsRoots(response, ipfsRoots)
|
|
403
411
|
|
|
404
412
|
return response
|
|
@@ -434,37 +442,11 @@ export class VerifiedFetch {
|
|
|
434
442
|
// if the user has specified an `Accept` header that corresponds to a raw
|
|
435
443
|
// type, honour that header, so for example they don't request
|
|
436
444
|
// `application/vnd.ipld.raw` but get `application/octet-stream`
|
|
437
|
-
await
|
|
445
|
+
await setContentType({ bytes: result, path, response, defaultContentType: getOverridenRawContentType({ headers: options?.headers, accept }), contentTypeParser: this.contentTypeParser, log: this.log })
|
|
438
446
|
|
|
439
447
|
return response
|
|
440
448
|
}
|
|
441
449
|
|
|
442
|
-
private async setContentType (bytes: Uint8Array, path: string, response: Response, defaultContentType = 'application/octet-stream'): Promise<void> {
|
|
443
|
-
let contentType: string | undefined
|
|
444
|
-
|
|
445
|
-
if (this.contentTypeParser != null) {
|
|
446
|
-
try {
|
|
447
|
-
let fileName = path.split('/').pop()?.trim()
|
|
448
|
-
fileName = fileName === '' ? undefined : fileName
|
|
449
|
-
const parsed = this.contentTypeParser(bytes, fileName)
|
|
450
|
-
|
|
451
|
-
if (isPromise(parsed)) {
|
|
452
|
-
const result = await parsed
|
|
453
|
-
|
|
454
|
-
if (result != null) {
|
|
455
|
-
contentType = result
|
|
456
|
-
}
|
|
457
|
-
} else if (parsed != null) {
|
|
458
|
-
contentType = parsed
|
|
459
|
-
}
|
|
460
|
-
} catch (err) {
|
|
461
|
-
this.log.error('error parsing content type', err)
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
this.log.trace('setting content type to "%s"', contentType ?? defaultContentType)
|
|
465
|
-
response.headers.set('content-type', contentType ?? defaultContentType)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
450
|
/**
|
|
469
451
|
* If the user has not specified an Accept header or format query string arg,
|
|
470
452
|
* use the CID codec to choose an appropriate handler for the block data.
|
|
@@ -478,6 +460,34 @@ export class VerifiedFetch {
|
|
|
478
460
|
[identity.code]: this.handleRaw
|
|
479
461
|
}
|
|
480
462
|
|
|
463
|
+
private async handleServerTiming<T> (name: string, description: string, fn: () => Promise<T>, withServerTiming: boolean): Promise<T> {
|
|
464
|
+
if (!withServerTiming) {
|
|
465
|
+
return fn()
|
|
466
|
+
}
|
|
467
|
+
const { error, result, header } = await serverTiming(name, description, fn)
|
|
468
|
+
this.serverTimingHeaders.push(header)
|
|
469
|
+
if (error != null) {
|
|
470
|
+
throw error
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return result
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The last place a Response touches in verified-fetch before being returned to the user. This is where we add the
|
|
478
|
+
* Server-Timing header to the response if it has been collected. It should be used for any final processing of the
|
|
479
|
+
* response before it is returned to the user.
|
|
480
|
+
*/
|
|
481
|
+
private handleFinalResponse (response: Response): Response {
|
|
482
|
+
if (this.serverTimingHeaders.length > 0) {
|
|
483
|
+
const headerString = this.serverTimingHeaders.join(', ')
|
|
484
|
+
response.headers.set('Server-Timing', headerString)
|
|
485
|
+
this.serverTimingHeaders = []
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return response
|
|
489
|
+
}
|
|
490
|
+
|
|
481
491
|
/**
|
|
482
492
|
* We're starting to get to the point where we need a queue or pipeline of
|
|
483
493
|
* operations to perform and a single place to handle errors.
|
|
@@ -489,6 +499,7 @@ export class VerifiedFetch {
|
|
|
489
499
|
this.log('fetch %s', resource)
|
|
490
500
|
|
|
491
501
|
const options = convertOptions(opts)
|
|
502
|
+
const withServerTiming = options?.withServerTiming ?? this.withServerTiming
|
|
492
503
|
|
|
493
504
|
options?.onProgress?.(new CustomProgressEvent<ResourceDetail>('verified-fetch:request:start', { resource }))
|
|
494
505
|
// resolve the CID/path from the requested resource
|
|
@@ -499,18 +510,19 @@ export class VerifiedFetch {
|
|
|
499
510
|
let protocol: ParsedUrlStringResults['protocol']
|
|
500
511
|
let ipfsPath: string
|
|
501
512
|
try {
|
|
502
|
-
const result = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
513
|
+
const result = await this.handleServerTiming('parse-resource', '', async () => parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, { withServerTiming, ...options }), withServerTiming)
|
|
503
514
|
cid = result.cid
|
|
504
515
|
path = result.path
|
|
505
516
|
query = result.query
|
|
506
517
|
ttl = result.ttl
|
|
507
518
|
protocol = result.protocol
|
|
508
519
|
ipfsPath = result.ipfsPath
|
|
520
|
+
this.serverTimingHeaders.push(...result.serverTimings.map(({ header }) => header))
|
|
509
521
|
} catch (err: any) {
|
|
510
522
|
options?.signal?.throwIfAborted()
|
|
511
523
|
this.log.error('error parsing resource %s', resource, err)
|
|
512
524
|
|
|
513
|
-
return badRequestResponse(resource.toString(), err)
|
|
525
|
+
return this.handleFinalResponse(badRequestResponse(resource.toString(), err))
|
|
514
526
|
}
|
|
515
527
|
|
|
516
528
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
@@ -521,7 +533,7 @@ export class VerifiedFetch {
|
|
|
521
533
|
this.log('output type %s', accept)
|
|
522
534
|
|
|
523
535
|
if (acceptHeader != null && accept == null) {
|
|
524
|
-
return notAcceptableResponse(resource.toString())
|
|
536
|
+
return this.handleFinalResponse(notAcceptableResponse(resource.toString()))
|
|
525
537
|
}
|
|
526
538
|
|
|
527
539
|
let response: Response
|
|
@@ -529,10 +541,10 @@ export class VerifiedFetch {
|
|
|
529
541
|
|
|
530
542
|
const redirectResponse = await getRedirectResponse({ resource, options, logger: this.helia.logger, cid })
|
|
531
543
|
if (redirectResponse != null) {
|
|
532
|
-
return redirectResponse
|
|
544
|
+
return this.handleFinalResponse(redirectResponse)
|
|
533
545
|
}
|
|
534
546
|
|
|
535
|
-
const handlerArgs: FetchHandlerFunctionArg = { resource: resource.toString(), cid, path, accept, session: options?.session ?? true, options }
|
|
547
|
+
const handlerArgs: FetchHandlerFunctionArg = { resource: resource.toString(), cid, path, accept, session: options?.session ?? true, options, withServerTiming }
|
|
536
548
|
|
|
537
549
|
if (accept === 'application/vnd.ipfs.ipns-record') {
|
|
538
550
|
// the user requested a raw IPNS record
|
|
@@ -562,7 +574,7 @@ export class VerifiedFetch {
|
|
|
562
574
|
const codecHandler = this.codecHandlers[cid.code]
|
|
563
575
|
|
|
564
576
|
if (codecHandler == null) {
|
|
565
|
-
return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia-verified-fetch/issues/new`)
|
|
577
|
+
return this.handleFinalResponse(notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia-verified-fetch/issues/new`))
|
|
566
578
|
}
|
|
567
579
|
this.log.trace('calling handler "%s"', codecHandler.name)
|
|
568
580
|
|
|
@@ -597,7 +609,7 @@ export class VerifiedFetch {
|
|
|
597
609
|
|
|
598
610
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
599
611
|
|
|
600
|
-
return response
|
|
612
|
+
return this.handleFinalResponse(response)
|
|
601
613
|
}
|
|
602
614
|
|
|
603
615
|
/**
|
|
@@ -614,7 +626,3 @@ export class VerifiedFetch {
|
|
|
614
626
|
await this.helia.stop()
|
|
615
627
|
}
|
|
616
628
|
}
|
|
617
|
-
|
|
618
|
-
function isPromise <T> (p?: any): p is Promise<T> {
|
|
619
|
-
return p?.then != null
|
|
620
|
-
}
|