@helia/verified-fetch 7.0.4 → 7.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helia/verified-fetch",
3
- "version": "7.0.4",
3
+ "version": "7.2.0",
4
4
  "description": "A fetch-like API for obtaining verified & trustless IPFS content on the web",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "homepage": "https://github.com/ipfs/helia-verified-fetch/tree/main/packages/verified-fetch#readme",
@@ -164,14 +164,14 @@
164
164
  "release": "aegir release"
165
165
  },
166
166
  "dependencies": {
167
- "@helia/block-brokers": "^5.1.0",
168
- "@helia/car": "^5.3.7",
167
+ "@helia/block-brokers": "^5.2.1",
168
+ "@helia/car": "^5.4.0",
169
169
  "@helia/delegated-routing-v1-http-api-client": "^6.0.1",
170
- "@helia/dnslink": "^1.1.4",
171
- "@helia/interface": "^6.1.0",
172
- "@helia/ipns": "^9.1.8",
173
- "@helia/routers": "^5.0.2",
174
- "@helia/unixfs": "^7.0.3",
170
+ "@helia/dnslink": "^1.2.0",
171
+ "@helia/interface": "^6.2.0",
172
+ "@helia/ipns": "^9.2.0",
173
+ "@helia/routers": "^5.1.0",
174
+ "@helia/unixfs": "^7.2.0",
175
175
  "@ipld/dag-cbor": "^9.2.3",
176
176
  "@ipld/dag-json": "^10.2.4",
177
177
  "@ipld/dag-pb": "^4.1.5",
@@ -182,8 +182,10 @@
182
182
  "@libp2p/webrtc": "^6.0.8",
183
183
  "@libp2p/websockets": "^10.1.0",
184
184
  "@multiformats/dns": "^1.0.6",
185
+ "@multiformats/multiaddr": "^13.0.1",
186
+ "@multiformats/multiaddr-matcher": "^3.0.2",
185
187
  "file-type": "^21.1.1",
186
- "helia": "^6.0.18",
188
+ "helia": "^6.1.1",
187
189
  "interface-blockstore": "^6.0.1",
188
190
  "ipfs-unixfs-exporter": "^15.0.2",
189
191
  "ipns": "^10.1.3",
@@ -202,10 +204,10 @@
202
204
  "uint8arrays": "^5.1.0"
203
205
  },
204
206
  "devDependencies": {
205
- "@helia/dag-cbor": "^5.0.0",
206
- "@helia/dag-json": "^5.0.0",
207
- "@helia/http": "^3.0.5",
208
- "@helia/json": "^5.0.0",
207
+ "@helia/dag-cbor": "^5.1.0",
208
+ "@helia/dag-json": "^5.1.0",
209
+ "@helia/http": "^3.1.1",
210
+ "@helia/json": "^5.1.0",
209
211
  "@ipld/car": "^5.4.2",
210
212
  "@libp2p/crypto": "^5.1.13",
211
213
  "@libp2p/logger": "^6.2.0",
@@ -214,7 +216,7 @@
214
216
  "aegir": "^47.0.24",
215
217
  "blockstore-core": "^6.1.1",
216
218
  "browser-readablestream-to-it": "^2.0.9",
217
- "cborg": "^4.2.11",
219
+ "cborg": "^5.1.0",
218
220
  "ipfs-unixfs-importer": "^16.0.1",
219
221
  "it-all": "^3.0.8",
220
222
  "magic-bytes.js": "^1.12.1",
package/src/index.ts CHANGED
@@ -772,6 +772,77 @@
772
772
  * - Any thrown error immediately stops the pipeline and returns the error response.
773
773
  *
774
774
  * For a detailed explanation of the pipeline, please refer to the discussion in [Issue #167](https://github.com/ipfs/helia-verified-fetch/issues/167).
775
+ *
776
+ * ### Server-Timing
777
+ *
778
+ * Detailed timing is found in the [Server-Timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing)
779
+ * HTTP header that is returned with every response when a resource is requested
780
+ * with the `withServerTiming` init option set to `true`.
781
+ *
782
+ * To prevent the header value growing too large, PeerIDs/CIDs are truncated to
783
+ * their first 10 characters and common strings are abbreviated.
784
+ *
785
+ * The values you may expect to see are described in the following table. Note
786
+ * that not all of them may be present in a given response.
787
+ *
788
+ * Router, block broker and transport abbreviations used in the `desc` fields
789
+ * follow.
790
+ *
791
+ * | Timing metric | Elaboration | Detail | Example |
792
+ * | ------------------ | --------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
793
+ * | d | DNSLink.resolve | Resolving a DNSLink to an IPFS path or IPNS name | `d;dur=0.200` |
794
+ * | i | IPFS.resolve | Resolving a CID + path to a CID | `i;dur=0.200` |
795
+ * | n | IPNS.resolve | Resolving an IPNS name to an IPFS path | `n;dur=0.200` |
796
+ * | p | Provider | A provider was found. The `desc` field contains the routing system that found the provider and the first 10 characters of the PeerId | `p;dur=0.000;desc="h,bagqbeaawn"` |
797
+ * | f | Find Providers | The total duration of the routing systems Find Providers operation. The `desc` field contains the routing system and how many providers were found | `f;dur=2.000;desc="h,4"` |
798
+ * | c | Connect | How long it took to connect to a provider. The `desc` field contains the type of provider, the first 10 characters of their PeerId and the transport used | `b;dur=0.000;desc="t,bagqbeaa7n,bafybeigoc,t"` |
799
+ * | b | Block | How long it took to retrieve a block from the provider once connected. The `desc` field contains the type of provider, the first 10 characters of their PeerId and the first 10 characters of the CID | `b;dur=0.000;desc="t,bagqbeaa7n,bafybeigoc"` |
800
+ *
801
+ * A full header might look like:
802
+ *
803
+ * ```
804
+ * i;dur=0.000,p;dur=0.000;desc="h,bagqbeaawn",p;dur=0.000;desc="h,bagqbeaawn",p;dur=1.000;desc="h,bagqbeaa7n",p;dur=1.000;desc="h,bagqbeaa7n",f;dur=1.000;desc="h,4",f;dur=1.000;desc="h,4",f;dur=144.000;desc="l,0",f;dur=144.000;desc="l,0",c;dur=206.000;desc="t,bagqbeaa7n,h",b;dur=1.000;desc="t,bagqbeaa7n,bafybeigoc"
805
+ * ```
806
+ *
807
+ * Here resolving a CID to a CID+path took less than a millisecond (e.g. a bare
808
+ * CID was requested).
809
+ *
810
+ * Two HTTP Gateway providers were found in the routing (`bagqbeaawn` and
811
+ * `bagqbeaa7n`). They are found twice because two block brokers are configured
812
+ * (bitswap and trustless-gateway) which both make a routing request (results
813
+ * are cached internally so the duration to find them the second time differs).
814
+ *
815
+ * It took 206ms to connect to `bagqbeaa7n` over HTTP, and 1s to retrieve the
816
+ * block for the CID `bafybeigo` from the Trustless Gateway `bagqbeaa7n`.
817
+ *
818
+ * All PeerIDs and CIDs above are truncated to 10 characters.
819
+ *
820
+ * #### Router abbreviations
821
+ *
822
+ * | Router | Elaboration |
823
+ * | ------ | --------------------- |
824
+ * | h | HTTP Gateway |
825
+ * | l | Libp2p (e.g. Kad-DHT) |
826
+ *
827
+ * #### Block broker abbreviations
828
+ *
829
+ * | Block Broker | Elaboration |
830
+ * | ------------ | ----------------- |
831
+ * | t | Trustless Gateway |
832
+ * | b | Bitswap |
833
+ *
834
+ * #### Transport abbreviations
835
+ *
836
+ * | Transport | Elaboration |
837
+ * | --------- | ------------- |
838
+ * | t | TCP |
839
+ * | h | HTTP |
840
+ * | w | WebSockets |
841
+ * | r | WebRTC |
842
+ * | d | WebRTC-Direct |
843
+ * | q | QUIC |
844
+ * | b | WebTransport |
845
+ * | u | Unknown |
775
846
  */
776
847
 
777
848
  import { bitswap, trustlessGateway } from '@helia/block-brokers'
@@ -903,7 +974,7 @@ export interface PluginContext extends ResolveURLResult, Omit<VerifiedFetchInit,
903
974
  /**
904
975
  * A callback that receives progress events
905
976
  */
906
- onProgress?(evt: ProgressEvent): void
977
+ onProgress?(evt: VerifiedFetchProgressEvents): void
907
978
 
908
979
  /**
909
980
  * Any async operations should be invoked using server timings to allow
@@ -1218,6 +1289,17 @@ export interface VerifiedFetchInit extends RequestInit, ProgressOptions<Verified
1218
1289
  */
1219
1290
  supportWebRedirects?: boolean
1220
1291
 
1292
+ /**
1293
+ * If a HAMT-sharded directory is encountered, paths will be translated
1294
+ * automatically, e.g. a request for `QmHamt/bar.txt` will be translated to
1295
+ * `QmHamt/F0/A1bar.txt` internally, pass `false` here to not perform this
1296
+ * translation which will then treat HAMT shard structures as
1297
+ * regular directories.
1298
+ *
1299
+ * @default true
1300
+ */
1301
+ translateHAMTPath?: boolean
1302
+
1221
1303
  /**
1222
1304
  * If true, only operate on the local blockstore, do not perform any network
1223
1305
  * operations.
@@ -6,6 +6,7 @@ import { CID } from 'multiformats/cid'
6
6
  import QuickLRU from 'quick-lru'
7
7
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
8
8
  import { CODEC_LIBP2P_KEY, SESSION_CACHE_MAX_SIZE, SESSION_CACHE_TTL_MS } from './constants.ts'
9
+ import { abbreviate } from './utils/abbreviate.ts'
9
10
  import { applyRedirects } from './utils/apply-redirect.ts'
10
11
  import { ServerTiming } from './utils/server-timing.ts'
11
12
  import type { ResolveURLOptions, ResolveURLResult, URLResolver as URLResolverInterface } from './index.ts'
@@ -124,7 +125,7 @@ export class URLResolver implements URLResolverInterface {
124
125
  }
125
126
 
126
127
  private async resolveDNSLink (url: URL, serverTiming: ServerTiming, options?: ResolveURLOptions): Promise<ResolveURLResult | Response> {
127
- const results = await serverTiming.time('dnsLink.resolve', `Resolve DNSLink ${url.hostname}`, this.components.dnsLink.resolve(url.hostname, options))
128
+ const results = await serverTiming.time(abbreviate('dnsLink.resolve'), '', this.components.dnsLink.resolve(url.hostname, options))
128
129
  const result = results?.[0]
129
130
 
130
131
  if (result == null) {
@@ -161,7 +162,7 @@ export class URLResolver implements URLResolverInterface {
161
162
 
162
163
  private async resolveIPNSName (url: URL, serverTiming: ServerTiming, options?: ResolveURLOptions): Promise<ResolveURLResult | Response> {
163
164
  const peerId = peerIdFromString(url.hostname)
164
- const result = await serverTiming.time('ipns.resolve', `Resolve IPNS name ${peerId}`, this.components.ipnsResolver.resolve(peerId, options))
165
+ const result = await serverTiming.time(abbreviate('ipns.resolve'), '', this.components.ipnsResolver.resolve(peerId, options))
165
166
  const path = normalizePath(`${result.path ?? ''}/${url.pathname}`)
166
167
 
167
168
  const ipfsUrl = new URL(`ipfs://${result.cid}${path}`)
@@ -180,7 +181,7 @@ export class URLResolver implements URLResolverInterface {
180
181
  }
181
182
 
182
183
  private async resolveIPFSPath (url: URL, serverTiming: ServerTiming, options?: ResolveURLOptions): Promise<ResolveURLResult | Response> {
183
- const walkPathResult = await serverTiming.time('ipfs.resolve', '', this.walkPath(url, options))
184
+ const walkPathResult = await serverTiming.time(abbreviate('ipfs.resolve'), '', this.walkPath(url, options))
184
185
 
185
186
  if (walkPathResult instanceof Response) {
186
187
  return walkPathResult
@@ -0,0 +1,57 @@
1
+ import { HTTP, HTTPS, QUIC, QUIC_V1, TCP, WebRTC, WebRTCDirect, WebSockets, WebSocketsSecure, WebTransport } from '@multiformats/multiaddr-matcher'
2
+ import type { Multiaddr } from '@multiformats/multiaddr'
3
+
4
+ const ABBREVIATIONS: Record<string, string> = {
5
+ // operations
6
+ 'ipfs.resolve': 'i',
7
+ 'dnsLink.resolve': 'd',
8
+ 'ipns.resolve': 'n',
9
+ 'found-provider': 'p',
10
+ 'find-providers': 'f',
11
+ connect: 'c',
12
+ block: 'b',
13
+
14
+ // routers
15
+ 'http-gateway-router': 'h',
16
+ 'libp2p-router': 'l',
17
+
18
+ // block brokers
19
+ 'trustless-gateway': 't',
20
+ bitswap: 'b'
21
+ }
22
+
23
+ export function abbreviate (str: string): string {
24
+ return ABBREVIATIONS[str] ?? str
25
+ }
26
+
27
+ export function abbreviateAddress (ma: Multiaddr): string {
28
+ if (TCP.exactMatch(ma)) {
29
+ return 't'
30
+ }
31
+
32
+ if (HTTP.exactMatch(ma) || HTTPS.exactMatch(ma)) {
33
+ return 'h'
34
+ }
35
+
36
+ if (WebSockets.exactMatch(ma) || WebSocketsSecure.exactMatch(ma)) {
37
+ return 'w'
38
+ }
39
+
40
+ if (WebRTC.exactMatch(ma)) {
41
+ return 'r'
42
+ }
43
+
44
+ if (WebRTCDirect.exactMatch(ma)) {
45
+ return 'd'
46
+ }
47
+
48
+ if (QUIC.exactMatch(ma) || QUIC_V1.exactMatch(ma)) {
49
+ return 'q'
50
+ }
51
+
52
+ if (WebTransport.exactMatch(ma)) {
53
+ return 'b'
54
+ }
55
+
56
+ return 'u'
57
+ }
@@ -25,6 +25,6 @@ export class ServerTiming {
25
25
  }
26
26
 
27
27
  add (name: string, description: string, duration: number | string): void {
28
- this.headers.push(`${name};dur=${Number(duration).toFixed(this.precision)};desc="${description}"`)
28
+ this.headers.push(`${name};dur=${Number(duration).toFixed(this.precision)}${description === '' ? '' : `;desc="${description}"`}`)
29
29
  }
30
30
  }
@@ -12,6 +12,7 @@ import { RawPlugin } from './plugins/plugin-handle-raw.ts'
12
12
  import { TarPlugin } from './plugins/plugin-handle-tar.js'
13
13
  import { UnixFSPlugin } from './plugins/plugin-handle-unixfs.js'
14
14
  import { URLResolver } from './url-resolver.ts'
15
+ import { abbreviate, abbreviateAddress } from './utils/abbreviate.ts'
15
16
  import { contentTypeParser } from './utils/content-type-parser.js'
16
17
  import { getContentType, getSupportedContentTypes, CONTENT_TYPE_OCTET_STREAM, MEDIA_TYPE_IPNS_RECORD, CONTENT_TYPE_IPNS } from './utils/content-types.ts'
17
18
  import { errorToObject } from './utils/error-to-object.ts'
@@ -58,6 +59,10 @@ function isIPNSRecordRequest (headers: Headers): boolean {
58
59
  return mediaType === MEDIA_TYPE_IPNS_RECORD
59
60
  }
60
61
 
62
+ function truncate (obj?: any): string {
63
+ return `${obj}`.substring(0, 10)
64
+ }
65
+
61
66
  export class VerifiedFetch {
62
67
  private readonly helia: Helia
63
68
  private readonly ipnsResolver: IPNSResolver
@@ -211,6 +216,10 @@ export class VerifiedFetch {
211
216
  return this.handleFinalResponse(accept)
212
217
  }
213
218
 
219
+ const routingTimers: Record<string, { start: number, found: number }> = {}
220
+ const connectTimers: Record<string, { start: number, transport: string }> = {}
221
+ const blockTimers: Record<string, number> = {}
222
+
214
223
  const context: PluginContext = {
215
224
  ...options,
216
225
  ...resolveResult,
@@ -219,7 +228,56 @@ export class VerifiedFetch {
219
228
  range,
220
229
  serverTiming,
221
230
  headers,
222
- requestedMimeTypes
231
+ requestedMimeTypes,
232
+ onProgress: (evt) => {
233
+ options?.onProgress?.(evt)
234
+
235
+ if (evt.type === 'helia:routing:find-providers:start') {
236
+ routingTimers[evt.detail.routing] = {
237
+ start: Date.now(),
238
+ found: 0
239
+ }
240
+ } else if (evt.type === 'helia:routing:find-providers:provider') {
241
+ if (routingTimers[evt.detail.routing] == null) {
242
+ return
243
+ }
244
+
245
+ routingTimers[evt.detail.routing].found++
246
+
247
+ serverTiming.add(abbreviate('found-provider'), `${abbreviate(evt.detail.routing)},${truncate(evt.detail.provider.id)}`, Date.now() - routingTimers[evt.detail.routing].start)
248
+ } else if (evt.type === 'helia:routing:find-providers:end') {
249
+ const routing = routingTimers[evt.detail.routing]
250
+
251
+ if (routing == null) {
252
+ return
253
+ }
254
+
255
+ serverTiming.add(abbreviate('find-providers'), `${abbreviate(evt.detail.routing)},${routing.found}`, Date.now() - routing.start)
256
+ } else if (evt.type === 'helia:block-broker:connect') {
257
+ connectTimers[`connect-${evt.detail.broker}-${evt.detail.provider}`] = {
258
+ start: Date.now(),
259
+ transport: ''
260
+ }
261
+ } else if (evt.type === 'helia:block-broker:connected') {
262
+ const start = connectTimers[`connect-${evt.detail.broker}-${evt.detail.provider}`]
263
+
264
+ if (start == null) {
265
+ return
266
+ }
267
+
268
+ serverTiming.add(abbreviate('connect'), `${abbreviate(evt.detail.broker)},${truncate(evt.detail.provider)},${abbreviateAddress(evt.detail.address)}`, Date.now() - start.start)
269
+ } else if (evt.type === 'helia:block-broker:request-block') {
270
+ blockTimers[`block-${evt.detail.broker}-${evt.detail.cid}-${evt.detail.provider}`] = Date.now()
271
+ } else if (evt.type === 'helia:block-broker:receive-block') {
272
+ const start = blockTimers[`block-${evt.detail.broker}-${evt.detail.cid}-${evt.detail.provider}`]
273
+
274
+ if (start == null) {
275
+ return
276
+ }
277
+
278
+ serverTiming.add(abbreviate('block'), `${abbreviate(evt.detail.broker)},${truncate(evt.detail.provider)},${truncate(evt.detail.cid)}`, Date.now() - start)
279
+ }
280
+ }
223
281
  }
224
282
 
225
283
  this.log.trace('finding handler for cid code "0x%s" and response content types %s', resolveResult.terminalElement.cid.code.toString(16), accept.map(header => header.contentType.mediaType).join(', '))