@helia/verified-fetch 7.1.0 → 7.2.1

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.1.0",
3
+ "version": "7.2.1",
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,82 @@
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
+ * The `dur` field is in milliseconds, so `dur=100` took 100ms. It is possible
783
+ * to measure in greater precision, but given these are network operations it's
784
+ * better to limit the precision and have a smaller header size, since
785
+ * downloading the headers impacts the time to first byte.
786
+ *
787
+ * To prevent the header value growing too large, PeerIDs/CIDs are truncated to
788
+ * their first 10 characters and common strings are abbreviated.
789
+ *
790
+ * The values you may expect to see are described in the following table. Note
791
+ * that not all of them may be present in a given response.
792
+ *
793
+ * Router, block broker and transport abbreviations used in the `desc` fields
794
+ * follow.
795
+ *
796
+ * | Timing metric | Elaboration | Detail | Example |
797
+ * | ------------------ | --------------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
798
+ * | d | DNSLink.resolve | Resolving a DNSLink to an IPFS path or IPNS name | `d;dur=0.200` |
799
+ * | i | IPFS.resolve | Resolving a CID + path to a CID | `i;dur=0.200` |
800
+ * | n | IPNS.resolve | Resolving an IPNS name to an IPFS path | `n;dur=0.200` |
801
+ * | 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"` |
802
+ * | 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"` |
803
+ * | 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"` |
804
+ * | 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"` |
805
+ *
806
+ * A full header might look like:
807
+ *
808
+ * ```
809
+ * i;dur=0,p;dur=0;desc="h,bagqbeaawn",p;dur=0;desc="h,bagqbeaawn",p;dur=1;desc="h,bagqbeaa7n",p;dur=1;desc="h,bagqbeaa7n",f;dur=1;desc="h,4",f;dur=1;desc="h,4",f;dur=144;desc="l,0",f;dur=144;desc="l,0",c;dur=206;desc="t,bagqbeaa7n,h",b;dur=1;desc="t,bagqbeaa7n,bafybeigoc"
810
+ * ```
811
+ *
812
+ * Here resolving a CID to a CID+path took less than a millisecond (e.g. a bare
813
+ * CID was requested).
814
+ *
815
+ * Two HTTP Gateway providers were found in the routing (`bagqbeaawn` and
816
+ * `bagqbeaa7n`). They are found twice because two block brokers are configured
817
+ * (bitswap and trustless-gateway) which both make a routing request (results
818
+ * are cached internally so the duration to find them the second time differs).
819
+ *
820
+ * It took 206ms to connect to `bagqbeaa7n` over HTTP, and 1s to retrieve the
821
+ * block for the CID `bafybeigo` from the Trustless Gateway `bagqbeaa7n`.
822
+ *
823
+ * All PeerIDs and CIDs above are truncated to 10 characters.
824
+ *
825
+ * #### Router abbreviations
826
+ *
827
+ * | Router | Elaboration |
828
+ * | ------ | --------------------- |
829
+ * | h | HTTP Gateway |
830
+ * | l | Libp2p (e.g. Kad-DHT) |
831
+ *
832
+ * #### Block broker abbreviations
833
+ *
834
+ * | Block Broker | Elaboration |
835
+ * | ------------ | ----------------- |
836
+ * | t | Trustless Gateway |
837
+ * | b | Bitswap |
838
+ *
839
+ * #### Transport abbreviations
840
+ *
841
+ * | Transport | Elaboration |
842
+ * | --------- | ------------- |
843
+ * | t | TCP |
844
+ * | h | HTTP |
845
+ * | w | WebSockets |
846
+ * | r | WebRTC |
847
+ * | d | WebRTC-Direct |
848
+ * | q | QUIC |
849
+ * | b | WebTransport |
850
+ * | u | Unknown |
775
851
  */
776
852
 
777
853
  import { bitswap, trustlessGateway } from '@helia/block-brokers'
@@ -903,7 +979,7 @@ export interface PluginContext extends ResolveURLResult, Omit<VerifiedFetchInit,
903
979
  /**
904
980
  * A callback that receives progress events
905
981
  */
906
- onProgress?(evt: ProgressEvent): void
982
+ onProgress?(evt: VerifiedFetchProgressEvents): void
907
983
 
908
984
  /**
909
985
  * Any async operations should be invoked using server timings to allow
@@ -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
+ }
@@ -1,10 +1,8 @@
1
1
  export class ServerTiming {
2
2
  private headers: string[]
3
- private precision: number
4
3
 
5
4
  constructor () {
6
5
  this.headers = []
7
- this.precision = 3
8
6
  }
9
7
 
10
8
  getHeader (): string {
@@ -18,13 +16,13 @@ export class ServerTiming {
18
16
  return await promise // Execute the function
19
17
  } finally {
20
18
  const endTime = performance.now()
21
- const duration = (endTime - startTime).toFixed(this.precision) // Duration in milliseconds
19
+ const duration = endTime - startTime
22
20
 
23
21
  this.add(name, description, duration)
24
22
  }
25
23
  }
26
24
 
27
- add (name: string, description: string, duration: number | string): void {
28
- this.headers.push(`${name};dur=${Number(duration).toFixed(this.precision)};desc="${description}"`)
25
+ add (name: string, description: string, ms: number): void {
26
+ this.headers.push(`${name};dur=${Math.round(ms)}${description === '' ? '' : `;desc="${description}"`}`)
29
27
  }
30
28
  }
@@ -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: performance.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)}`, performance.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}`, performance.now() - routing.start)
256
+ } else if (evt.type === 'helia:block-broker:connect') {
257
+ connectTimers[`connect-${evt.detail.broker}-${evt.detail.provider}`] = {
258
+ start: performance.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)}`, performance.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}`] = performance.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)}`, performance.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(', '))