@helia/block-brokers 2.0.2 → 2.0.3-329652a

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,67 @@
1
+ import { DEFAULT_SESSION_MIN_PROVIDERS, DEFAULT_SESSION_MAX_PROVIDERS, DEFAULT_SESSION_PROVIDER_QUERY_CONCURRENCY, DEFAULT_SESSION_PROVIDER_QUERY_TIMEOUT } from '@helia/interface'
2
+ import { PeerQueue } from '@libp2p/utils/peer-queue'
3
+ import { isPrivateIp } from '@libp2p/utils/private-ip'
4
+ import { DNS, HTTP, HTTPS } from '@multiformats/multiaddr-matcher'
5
+ import { multiaddrToUri } from '@multiformats/multiaddr-to-uri'
6
+ import pDefer from 'p-defer'
1
7
  import { TrustlessGateway } from './trustless-gateway.js'
2
8
  import { DEFAULT_TRUSTLESS_GATEWAYS } from './index.js'
3
9
  import type { TrustlessGatewayBlockBrokerInit, TrustlessGatewayComponents, TrustlessGatewayGetBlockProgressEvents } from './index.js'
4
- import type { BlockRetrievalOptions, BlockRetriever } from '@helia/interface/blocks'
10
+ import type { Routing, BlockRetrievalOptions, BlockBroker, CreateSessionOptions } from '@helia/interface'
5
11
  import type { Logger } from '@libp2p/interface'
6
12
  import type { CID } from 'multiformats/cid'
7
- import type { ProgressOptions } from 'progress-events'
13
+
14
+ export interface CreateTrustlessGatewaySessionOptions extends CreateSessionOptions<TrustlessGatewayGetBlockProgressEvents> {
15
+ /**
16
+ * Specify the cache control header to send to the remote. 'only-if-cached'
17
+ * will prevent the gateway from fetching the content if they don't have it.
18
+ *
19
+ * @default only-if-cached
20
+ */
21
+ cacheControl?: string
22
+
23
+ /**
24
+ * By default we will only connect to peers with HTTPS addresses, pass true
25
+ * to also connect to HTTP addresses.
26
+ *
27
+ * @default false
28
+ */
29
+ allowInsecure?: boolean
30
+
31
+ /**
32
+ * By default we will only connect to peers with public or DNS addresses, pass
33
+ * true to also connect to private addresses.
34
+ *
35
+ * @default false
36
+ */
37
+ allowLocal?: boolean
38
+ }
8
39
 
9
40
  /**
10
41
  * A class that accepts a list of trustless gateways that are queried
11
42
  * for blocks.
12
43
  */
13
- export class TrustlessGatewayBlockBroker implements BlockRetriever<
14
- ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
15
- > {
44
+ export class TrustlessGatewayBlockBroker implements BlockBroker<TrustlessGatewayGetBlockProgressEvents> {
45
+ private readonly components: TrustlessGatewayComponents
16
46
  private readonly gateways: TrustlessGateway[]
47
+ private readonly routing: Routing
17
48
  private readonly log: Logger
18
49
 
19
50
  constructor (components: TrustlessGatewayComponents, init: TrustlessGatewayBlockBrokerInit = {}) {
51
+ this.components = components
20
52
  this.log = components.logger.forComponent('helia:trustless-gateway-block-broker')
53
+ this.routing = components.routing
21
54
  this.gateways = (init.gateways ?? DEFAULT_TRUSTLESS_GATEWAYS)
22
55
  .map((gatewayOrUrl) => {
23
- return new TrustlessGateway(gatewayOrUrl)
56
+ return new TrustlessGateway(gatewayOrUrl, components.logger)
24
57
  })
25
58
  }
26
59
 
27
- async retrieve (cid: CID, options: BlockRetrievalOptions<ProgressOptions<TrustlessGatewayGetBlockProgressEvents>> = {}): Promise<Uint8Array> {
60
+ addGateway (gatewayOrUrl: string): void {
61
+ this.gateways.push(new TrustlessGateway(gatewayOrUrl, this.components.logger))
62
+ }
63
+
64
+ async retrieve (cid: CID, options: BlockRetrievalOptions<TrustlessGatewayGetBlockProgressEvents> = {}): Promise<Uint8Array> {
28
65
  // Loop through the gateways until we get a block or run out of gateways
29
66
  // TODO: switch to toSorted when support is better
30
67
  const sortedGateways = this.gateways.sort((a, b) => b.reliability() - a.reliability())
@@ -41,7 +78,7 @@ ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
41
78
  this.log.error('failed to validate block for %c from %s', cid, gateway.url, err)
42
79
  gateway.incrementInvalidBlocks()
43
80
 
44
- throw new Error(`unable to validate block for CID ${cid} from gateway ${gateway.url}`)
81
+ throw new Error(`Block for CID ${cid} from gateway ${gateway.url} failed validation`)
45
82
  }
46
83
 
47
84
  return block
@@ -50,7 +87,7 @@ ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
50
87
  if (err instanceof Error) {
51
88
  aggregateErrors.push(err)
52
89
  } else {
53
- aggregateErrors.push(new Error(`unable to fetch raw block for CID ${cid} from gateway ${gateway.url}`))
90
+ aggregateErrors.push(new Error(`Unable to fetch raw block for CID ${cid} from gateway ${gateway.url}`))
54
91
  }
55
92
  // if signal was aborted, exit the loop
56
93
  if (options.signal?.aborted === true) {
@@ -60,6 +97,106 @@ ProgressOptions<TrustlessGatewayGetBlockProgressEvents>
60
97
  }
61
98
  }
62
99
 
63
- throw new AggregateError(aggregateErrors, `unable to fetch raw block for CID ${cid} from any gateway`)
100
+ if (aggregateErrors.length > 0) {
101
+ throw new AggregateError(aggregateErrors, `Unable to fetch raw block for CID ${cid} from any gateway`)
102
+ } else {
103
+ throw new Error(`Unable to fetch raw block for CID ${cid} from any gateway`)
104
+ }
105
+ }
106
+
107
+ async createSession (root: CID, options: CreateTrustlessGatewaySessionOptions = {}): Promise<BlockBroker<TrustlessGatewayGetBlockProgressEvents>> {
108
+ const gateways: string[] = []
109
+ const minProviders = options.minProviders ?? DEFAULT_SESSION_MIN_PROVIDERS
110
+ const maxProviders = options.minProviders ?? DEFAULT_SESSION_MAX_PROVIDERS
111
+ const deferred = pDefer<BlockBroker<TrustlessGatewayGetBlockProgressEvents>>()
112
+ const broker = new TrustlessGatewayBlockBroker(this.components, {
113
+ gateways
114
+ })
115
+
116
+ this.log('finding transport-ipfs-gateway-http providers for cid %c', root)
117
+
118
+ const queue = new PeerQueue({
119
+ concurrency: options.providerQueryConcurrency ?? DEFAULT_SESSION_PROVIDER_QUERY_CONCURRENCY
120
+ })
121
+
122
+ Promise.resolve().then(async () => {
123
+ for await (const provider of this.routing.findProviders(root, options)) {
124
+ const httpAddresses = provider.multiaddrs.filter(ma => {
125
+ if (HTTPS.matches(ma) || (options.allowInsecure === true && HTTP.matches(ma))) {
126
+ if (options.allowLocal === true) {
127
+ return true
128
+ }
129
+
130
+ if (DNS.matches(ma)) {
131
+ return true
132
+ }
133
+
134
+ return isPrivateIp(ma.toOptions().host) === false
135
+ }
136
+
137
+ return false
138
+ })
139
+
140
+ if (httpAddresses.length === 0) {
141
+ continue
142
+ }
143
+
144
+ this.log('found transport-ipfs-gateway-http provider %p for cid %c', provider.id, root)
145
+
146
+ void queue.add(async () => {
147
+ for (const ma of httpAddresses) {
148
+ let uri: string | undefined
149
+
150
+ try {
151
+ // /ip4/x.x.x.x/tcp/31337/http
152
+ // /ip4/x.x.x.x/tcp/31337/https
153
+ // etc
154
+ uri = multiaddrToUri(ma)
155
+
156
+ const resource = `${uri}/ipfs/${root.toString()}?format=raw`
157
+
158
+ // make sure the peer is available - HEAD support doesn't seem to
159
+ // be very widely implemented so as long as the remote responds
160
+ // we are happy they are valid
161
+ // https://specs.ipfs.tech/http-gateways/trustless-gateway/#head-ipfs-cid-path-params
162
+
163
+ // in the future we should be able to request `${uri}/.well-known/libp2p-http
164
+ // and discover an IPFS gateway from $.protocols['/ipfs/gateway'].path
165
+ // in the response
166
+ // https://github.com/libp2p/specs/pull/508/files
167
+ const response = await fetch(resource, {
168
+ method: 'HEAD',
169
+ headers: {
170
+ Accept: 'application/vnd.ipld.raw',
171
+ 'Cache-Control': options.cacheControl ?? 'only-if-cached'
172
+ },
173
+ signal: AbortSignal.timeout(options.providerQueryTimeout ?? DEFAULT_SESSION_PROVIDER_QUERY_TIMEOUT)
174
+ })
175
+
176
+ this.log('HEAD %s %d', resource, response.status)
177
+ gateways.push(uri)
178
+ broker.addGateway(uri)
179
+
180
+ this.log('found %d transport-ipfs-gateway-http providers for cid %c', gateways.length, root)
181
+
182
+ if (gateways.length === minProviders) {
183
+ deferred.resolve(broker)
184
+ }
185
+
186
+ if (gateways.length === maxProviders) {
187
+ queue.clear()
188
+ }
189
+ } catch (err: any) {
190
+ this.log.error('could not fetch %c from %a', root, uri ?? ma, err)
191
+ }
192
+ }
193
+ })
194
+ }
195
+ })
196
+ .catch(err => {
197
+ this.log.error('error creating session for %c', root, err)
198
+ })
199
+
200
+ return deferred.promise
64
201
  }
65
202
  }
@@ -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
  }
@@ -1,3 +1,4 @@
1
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
1
2
  import type { CID } from 'multiformats/cid'
2
3
 
3
4
  /**
@@ -36,8 +37,11 @@ export class TrustlessGateway {
36
37
  */
37
38
  #successes = 0
38
39
 
39
- constructor (url: URL | string) {
40
+ private readonly log: Logger
41
+
42
+ constructor (url: URL | string, logger: ComponentLogger) {
40
43
  this.url = url instanceof URL ? url : new URL(url)
44
+ this.log = logger.forComponent(`helia:trustless-gateway-block-broker:${this.url.hostname}`)
41
45
  }
42
46
 
43
47
  /**
@@ -67,6 +71,9 @@ export class TrustlessGateway {
67
71
  },
68
72
  cache: 'force-cache'
69
73
  })
74
+
75
+ this.log('GET %s %d', gwUrl, res.status)
76
+
70
77
  if (!res.ok) {
71
78
  this.#errors++
72
79
  throw new Error(`unable to fetch raw block for CID ${cid} from gateway ${this.url}`)
@@ -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
- }