@helia/block-brokers 2.0.3 → 2.1.0-59de059

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.
@@ -1,30 +1,56 @@
1
+ import { createTrustlessGatewaySession } from './session.js'
1
2
  import { TrustlessGateway } from './trustless-gateway.js'
2
3
  import { DEFAULT_TRUSTLESS_GATEWAYS } from './index.js'
3
4
  import type { TrustlessGatewayBlockBrokerInit, TrustlessGatewayComponents, TrustlessGatewayGetBlockProgressEvents } from './index.js'
4
- import type { BlockRetrievalOptions, BlockRetriever } from '@helia/interface/blocks'
5
- import type { Logger } from '@libp2p/interface'
5
+ import type { Routing, BlockRetrievalOptions, BlockBroker, CreateSessionOptions } from '@helia/interface'
6
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
6
7
  import type { CID } from 'multiformats/cid'
7
- import type { ProgressOptions } from 'progress-events'
8
+
9
+ export interface CreateTrustlessGatewaySessionOptions extends CreateSessionOptions<TrustlessGatewayGetBlockProgressEvents> {
10
+ /**
11
+ * By default we will only connect to peers with HTTPS addresses, pass true
12
+ * to also connect to HTTP addresses.
13
+ *
14
+ * @default false
15
+ */
16
+ allowInsecure?: boolean
17
+
18
+ /**
19
+ * By default we will only connect to peers with public or DNS addresses, pass
20
+ * true to also connect to private addresses.
21
+ *
22
+ * @default false
23
+ */
24
+ allowLocal?: boolean
25
+ }
8
26
 
9
27
  /**
10
28
  * A class that accepts a list of trustless gateways that are queried
11
29
  * for blocks.
12
30
  */
13
- export class TrustlessGatewayBlockBroker implements BlockRetriever<
14
- ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
15
- > {
31
+ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGatewayGetBlockProgressEvents> {
32
+ private readonly components: TrustlessGatewayComponents
16
33
  private readonly gateways: TrustlessGateway[]
34
+ private readonly routing: Routing
17
35
  private readonly log: Logger
36
+ private readonly logger: ComponentLogger
18
37
 
19
38
  constructor (components: TrustlessGatewayComponents, init: TrustlessGatewayBlockBrokerInit = {}) {
39
+ this.components = components
20
40
  this.log = components.logger.forComponent('helia:trustless-gateway-block-broker')
41
+ this.logger = components.logger
42
+ this.routing = components.routing
21
43
  this.gateways = (init.gateways ?? DEFAULT_TRUSTLESS_GATEWAYS)
22
44
  .map((gatewayOrUrl) => {
23
- return new TrustlessGateway(gatewayOrUrl)
45
+ return new TrustlessGateway(gatewayOrUrl, components.logger)
24
46
  })
25
47
  }
26
48
 
27
- async retrieve (cid: CID, options: BlockRetrievalOptions<ProgressOptions<TrustlessGatewayGetBlockProgressEvents>> = {}): Promise<Uint8Array> {
49
+ addGateway (gatewayOrUrl: string): void {
50
+ this.gateways.push(new TrustlessGateway(gatewayOrUrl, this.components.logger))
51
+ }
52
+
53
+ async retrieve (cid: CID, options: BlockRetrievalOptions<TrustlessGatewayGetBlockProgressEvents> = {}): Promise<Uint8Array> {
28
54
  // Loop through the gateways until we get a block or run out of gateways
29
55
  // TODO: switch to toSorted when support is better
30
56
  const sortedGateways = this.gateways.sort((a, b) => b.reliability() - a.reliability())
@@ -41,7 +67,7 @@ ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
41
67
  this.log.error('failed to validate block for %c from %s', cid, gateway.url, err)
42
68
  gateway.incrementInvalidBlocks()
43
69
 
44
- throw new Error(`unable to validate block for CID ${cid} from gateway ${gateway.url}`)
70
+ throw new Error(`Block for CID ${cid} from gateway ${gateway.url} failed validation`)
45
71
  }
46
72
 
47
73
  return block
@@ -50,7 +76,7 @@ ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
50
76
  if (err instanceof Error) {
51
77
  aggregateErrors.push(err)
52
78
  } else {
53
- aggregateErrors.push(new Error(`unable to fetch raw block for CID ${cid} from gateway ${gateway.url}`))
79
+ aggregateErrors.push(new Error(`Unable to fetch raw block for CID ${cid} from gateway ${gateway.url}`))
54
80
  }
55
81
  // if signal was aborted, exit the loop
56
82
  if (options.signal?.aborted === true) {
@@ -60,6 +86,17 @@ ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
60
86
  }
61
87
  }
62
88
 
63
- throw new AggregateError(aggregateErrors, `unable to fetch raw block for CID ${cid} from any gateway`)
89
+ if (aggregateErrors.length > 0) {
90
+ throw new AggregateError(aggregateErrors, `Unable to fetch raw block for CID ${cid} from any gateway`)
91
+ } else {
92
+ throw new Error(`Unable to fetch raw block for CID ${cid} from any gateway`)
93
+ }
94
+ }
95
+
96
+ createSession (options: CreateTrustlessGatewaySessionOptions = {}): BlockBroker<TrustlessGatewayGetBlockProgressEvents> {
97
+ return createTrustlessGatewaySession({
98
+ logger: this.logger,
99
+ routing: this.routing
100
+ }, options)
64
101
  }
65
102
  }
@@ -1,5 +1,5 @@
1
1
  import { TrustlessGatewayBlockBroker } from './broker.js'
2
- import type { BlockRetriever } from '@helia/interface/src/blocks.js'
2
+ import type { Routing, BlockBroker } from '@helia/interface'
3
3
  import type { ComponentLogger } from '@libp2p/interface'
4
4
  import type { ProgressEvent } from 'progress-events'
5
5
 
@@ -22,9 +22,10 @@ export interface TrustlessGatewayBlockBrokerInit {
22
22
  }
23
23
 
24
24
  export interface TrustlessGatewayComponents {
25
+ routing: Routing
25
26
  logger: ComponentLogger
26
27
  }
27
28
 
28
- export function trustlessGateway (init: TrustlessGatewayBlockBrokerInit = {}): (components: TrustlessGatewayComponents) => BlockRetriever {
29
+ export function trustlessGateway (init: TrustlessGatewayBlockBrokerInit = {}): (components: TrustlessGatewayComponents) => BlockBroker<TrustlessGatewayGetBlockProgressEvents> {
29
30
  return (components) => new TrustlessGatewayBlockBroker(components, init)
30
31
  }
@@ -0,0 +1,98 @@
1
+ import { AbstractSession } from '@helia/utils'
2
+ import { isPrivateIp } from '@libp2p/utils/private-ip'
3
+ import { DNS, HTTP, HTTPS } from '@multiformats/multiaddr-matcher'
4
+ import { multiaddrToUri } from '@multiformats/multiaddr-to-uri'
5
+ import { TrustlessGateway } from './trustless-gateway.js'
6
+ import type { CreateTrustlessGatewaySessionOptions } from './broker.js'
7
+ import type { TrustlessGatewayGetBlockProgressEvents } from './index.js'
8
+ import type { BlockRetrievalOptions, Routing } from '@helia/interface'
9
+ import type { ComponentLogger } from '@libp2p/interface'
10
+ import type { Multiaddr } from '@multiformats/multiaddr'
11
+ import type { AbortOptions } from 'interface-store'
12
+ import type { CID } from 'multiformats/cid'
13
+
14
+ const DEFAULT_ALLOW_INSECURE = false
15
+ const DEFAULT_ALLOW_LOCAL = false
16
+
17
+ export interface TrustlessGatewaySessionComponents {
18
+ logger: ComponentLogger
19
+ routing: Routing
20
+ }
21
+
22
+ class TrustlessGatewaySession extends AbstractSession<TrustlessGateway, TrustlessGatewayGetBlockProgressEvents> {
23
+ private readonly routing: Routing
24
+ private readonly allowInsecure: boolean
25
+ private readonly allowLocal: boolean
26
+
27
+ constructor (components: TrustlessGatewaySessionComponents, init: CreateTrustlessGatewaySessionOptions) {
28
+ super(components, {
29
+ ...init,
30
+ name: 'helia:trustless-gateway:session'
31
+ })
32
+
33
+ this.routing = components.routing
34
+ this.allowInsecure = init.allowInsecure ?? DEFAULT_ALLOW_INSECURE
35
+ this.allowLocal = init.allowLocal ?? DEFAULT_ALLOW_LOCAL
36
+ }
37
+
38
+ async queryProvider (cid: CID, provider: TrustlessGateway, options: BlockRetrievalOptions): Promise<Uint8Array> {
39
+ this.log('fetching BLOCK for %c from %s', cid, provider.url)
40
+
41
+ const block = await provider.getRawBlock(cid, options.signal)
42
+ this.log.trace('got block for %c from %s', cid, provider.url)
43
+
44
+ await options.validateFn?.(block)
45
+
46
+ return block
47
+ }
48
+
49
+ async * findNewProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator<TrustlessGateway> {
50
+ for await (const provider of this.routing.findProviders(cid, options)) {
51
+ // require http(s) addresses
52
+ const httpAddresses = filterMultiaddrs(provider.multiaddrs, this.allowInsecure, this.allowLocal)
53
+
54
+ if (httpAddresses.length === 0) {
55
+ continue
56
+ }
57
+
58
+ // take first address?
59
+ // /ip4/x.x.x.x/tcp/31337/http
60
+ // /ip4/x.x.x.x/tcp/31337/https
61
+ // etc
62
+ const uri = multiaddrToUri(httpAddresses[0])
63
+
64
+ this.log('found http-gateway provider %p %s for cid %c', provider.id, uri, cid)
65
+ yield new TrustlessGateway(uri, this.logger)
66
+ }
67
+ }
68
+
69
+ toEvictionKey (provider: TrustlessGateway): Uint8Array | string {
70
+ return provider.url.toString()
71
+ }
72
+
73
+ equals (providerA: TrustlessGateway, providerB: TrustlessGateway): boolean {
74
+ return providerA.url.toString() === providerB.url.toString()
75
+ }
76
+ }
77
+
78
+ function filterMultiaddrs (multiaddrs: Multiaddr[], allowInsecure: boolean, allowLocal: boolean): Multiaddr[] {
79
+ return multiaddrs.filter(ma => {
80
+ if (HTTPS.matches(ma) || (allowInsecure && HTTP.matches(ma))) {
81
+ if (allowLocal) {
82
+ return true
83
+ }
84
+
85
+ if (DNS.matches(ma)) {
86
+ return true
87
+ }
88
+
89
+ return isPrivateIp(ma.toOptions().host) === false
90
+ }
91
+
92
+ return false
93
+ })
94
+ }
95
+
96
+ export function createTrustlessGatewaySession (components: TrustlessGatewaySessionComponents, init: CreateTrustlessGatewaySessionOptions): TrustlessGatewaySession {
97
+ return new TrustlessGatewaySession(components, init)
98
+ }
@@ -1,5 +1,15 @@
1
+ import { base64 } from 'multiformats/bases/base64'
2
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
1
3
  import type { CID } from 'multiformats/cid'
2
4
 
5
+ export interface TrustlessGatewayStats {
6
+ attempts: number
7
+ errors: number
8
+ invalidBlocks: number
9
+ successes: number
10
+ pendingResponses?: number
11
+ }
12
+
3
13
  /**
4
14
  * A `TrustlessGateway` keeps track of the number of attempts, errors, and
5
15
  * successes for a given gateway url so that we can prioritize gateways that
@@ -36,8 +46,32 @@ export class TrustlessGateway {
36
46
  */
37
47
  #successes = 0
38
48
 
39
- constructor (url: URL | string) {
49
+ /**
50
+ * A map of pending responses for this gateway. This is used to ensure that
51
+ * only one request per CID is made to a given gateway at a time, and that we
52
+ * don't make multiple in-flight requests for the same CID to the same gateway.
53
+ */
54
+ #pendingResponses = new Map<string, Promise<Uint8Array>>()
55
+
56
+ private readonly log: Logger
57
+
58
+ constructor (url: URL | string, logger: ComponentLogger) {
40
59
  this.url = url instanceof URL ? url : new URL(url)
60
+ this.log = logger.forComponent(`helia:trustless-gateway-block-broker:${this.url.hostname}`)
61
+ }
62
+
63
+ /**
64
+ * This function returns a unique string for the multihash.bytes of the CID.
65
+ *
66
+ * Some useful resources for why this is needed can be found using the links below:
67
+ *
68
+ * - https://github.com/ipfs/helia/pull/503#discussion_r1572451331
69
+ * - https://github.com/ipfs/kubo/issues/6815
70
+ * - https://www.notion.so/pl-strflt/Handling-ambiguity-around-CIDs-9d5e14f6516f438980b01ef188efe15d#d9d45cd1ed8b4d349b96285de4aed5ab
71
+ */
72
+ #uniqueBlockId (cid: CID): string {
73
+ const multihashBytes = cid.multihash.bytes
74
+ return base64.encode(multihashBytes)
41
75
  }
42
76
 
43
77
  /**
@@ -45,7 +79,7 @@ export class TrustlessGateway {
45
79
  * https://specs.ipfs.tech/http-gateways/trustless-gateway/
46
80
  */
47
81
  async getRawBlock (cid: CID, signal?: AbortSignal): Promise<Uint8Array> {
48
- const gwUrl = this.url
82
+ const gwUrl = new URL(this.url.toString())
49
83
  gwUrl.pathname = `/ipfs/${cid.toString()}`
50
84
 
51
85
  // necessary as not every gateway supports dag-cbor, but every should support
@@ -56,23 +90,29 @@ export class TrustlessGateway {
56
90
  throw new Error(`Signal to fetch raw block for CID ${cid} from gateway ${this.url} was aborted prior to fetch`)
57
91
  }
58
92
 
93
+ const blockId = this.#uniqueBlockId(cid)
59
94
  try {
60
- this.#attempts++
61
- const res = await fetch(gwUrl.toString(), {
62
- signal,
63
- headers: {
64
- // also set header, just in case ?format= is filtered out by some
65
- // reverse proxy
66
- Accept: 'application/vnd.ipld.raw'
67
- },
68
- cache: 'force-cache'
69
- })
70
- if (!res.ok) {
71
- this.#errors++
72
- throw new Error(`unable to fetch raw block for CID ${cid} from gateway ${this.url}`)
95
+ let pendingResponse: Promise<Uint8Array> | undefined = this.#pendingResponses.get(blockId)
96
+ if (pendingResponse == null) {
97
+ this.#attempts++
98
+ pendingResponse = fetch(gwUrl.toString(), {
99
+ signal,
100
+ headers: {
101
+ Accept: 'application/vnd.ipld.raw'
102
+ },
103
+ cache: 'force-cache'
104
+ }).then(async (res) => {
105
+ this.log('GET %s %d', gwUrl, res.status)
106
+ if (!res.ok) {
107
+ this.#errors++
108
+ throw new Error(`unable to fetch raw block for CID ${cid} from gateway ${this.url}`)
109
+ }
110
+ this.#successes++
111
+ return new Uint8Array(await res.arrayBuffer())
112
+ })
113
+ this.#pendingResponses.set(blockId, pendingResponse)
73
114
  }
74
- this.#successes++
75
- return new Uint8Array(await res.arrayBuffer())
115
+ return await pendingResponse
76
116
  } catch (cause) {
77
117
  // @ts-expect-error - TS thinks signal?.aborted can only be false now
78
118
  // because it was checked for true above.
@@ -81,6 +121,8 @@ export class TrustlessGateway {
81
121
  }
82
122
  this.#errors++
83
123
  throw new Error(`unable to fetch raw block for CID ${cid}`)
124
+ } finally {
125
+ this.#pendingResponses.delete(blockId)
84
126
  }
85
127
  }
86
128
 
@@ -123,4 +165,14 @@ export class TrustlessGateway {
123
165
  incrementInvalidBlocks (): void {
124
166
  this.#invalidBlocks++
125
167
  }
168
+
169
+ getStats (): TrustlessGatewayStats {
170
+ return {
171
+ attempts: this.#attempts,
172
+ errors: this.#errors,
173
+ invalidBlocks: this.#invalidBlocks,
174
+ successes: this.#successes,
175
+ pendingResponses: this.#pendingResponses.size
176
+ }
177
+ }
126
178
  }
@@ -1,4 +0,0 @@
1
- {
2
- "bitswap": "https://ipfs.github.io/helia/functions/_helia_block_brokers.bitswap.html",
3
- "trustlessGateway": "https://ipfs.github.io/helia/functions/_helia_block_brokers.trustlessGateway.html"
4
- }