@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.
- package/dist/index.min.js +6 -1
- package/dist/src/bitswap.d.ts +5 -3
- package/dist/src/bitswap.d.ts.map +1 -1
- package/dist/src/bitswap.js +17 -6
- package/dist/src/bitswap.js.map +1 -1
- package/dist/src/trustless-gateway/broker.d.ts +24 -4
- package/dist/src/trustless-gateway/broker.d.ts.map +1 -1
- package/dist/src/trustless-gateway/broker.js +25 -4
- package/dist/src/trustless-gateway/broker.js.map +1 -1
- package/dist/src/trustless-gateway/index.d.ts +3 -2
- package/dist/src/trustless-gateway/index.d.ts.map +1 -1
- package/dist/src/trustless-gateway/index.js.map +1 -1
- package/dist/src/trustless-gateway/session.d.ts +25 -0
- package/dist/src/trustless-gateway/session.d.ts.map +1 -0
- package/dist/src/trustless-gateway/session.js +68 -0
- package/dist/src/trustless-gateway/session.js.map +1 -0
- package/dist/src/trustless-gateway/trustless-gateway.d.ts +11 -1
- package/dist/src/trustless-gateway/trustless-gateway.d.ts.map +1 -1
- package/dist/src/trustless-gateway/trustless-gateway.js +57 -17
- package/dist/src/trustless-gateway/trustless-gateway.js.map +1 -1
- package/package.json +14 -3
- package/src/bitswap.ts +26 -13
- package/src/trustless-gateway/broker.ts +48 -11
- package/src/trustless-gateway/index.ts +3 -2
- package/src/trustless-gateway/session.ts +98 -0
- package/src/trustless-gateway/trustless-gateway.ts +69 -17
- package/dist/typedoc-urls.json +0 -4
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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 {
|
|
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) =>
|
|
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
|
-
|
|
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.#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/typedoc-urls.json
DELETED