1
+ import { base64 } from 'multiformats/bases/base64'
1
2
import type { ComponentLogger , Logger } from '@libp2p/interface'
2
3
import type { CID } from 'multiformats/cid'
3
4
5
+ export interface TrustlessGatewayStats {
6
+ attempts : number
7
+ errors : number
8
+ invalidBlocks : number
9
+ successes : number
10
+ pendingResponses ?: number
11
+ }
12
+
4
13
/**
5
14
* A `TrustlessGateway` keeps track of the number of attempts, errors, and
6
15
* successes for a given gateway url so that we can prioritize gateways that
@@ -37,13 +46,34 @@ export class TrustlessGateway {
37
46
*/
38
47
#successes = 0
39
48
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
+
40
56
private readonly log : Logger
41
57
42
58
constructor ( url : URL | string , logger : ComponentLogger ) {
43
59
this . url = url instanceof URL ? url : new URL ( url )
44
60
this . log = logger . forComponent ( `helia:trustless-gateway-block-broker:${ this . url . hostname } ` )
45
61
}
46
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 )
75
+ }
76
+
47
77
/**
48
78
* Fetch a raw block from `this.url` following the specification defined at
49
79
* https://specs.ipfs.tech/http-gateways/trustless-gateway/
@@ -60,26 +90,29 @@ export class TrustlessGateway {
60
90
throw new Error ( `Signal to fetch raw block for CID ${ cid } from gateway ${ this . url } was aborted prior to fetch` )
61
91
}
62
92
93
+ const blockId = this . #uniqueBlockId( cid )
63
94
try {
64
- this . #attempts++
65
- const res = await fetch ( gwUrl . toString ( ) , {
66
- signal,
67
- headers : {
68
- // also set header, just in case ?format= is filtered out by some
69
- // reverse proxy
70
- Accept : 'application/vnd.ipld.raw'
71
- } ,
72
- cache : 'force-cache'
73
- } )
74
-
75
- this . log ( 'GET %s %d' , gwUrl , res . status )
76
-
77
- if ( ! res . ok ) {
78
- this . #errors++
79
- 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 )
80
114
}
81
- this . #successes++
82
- return new Uint8Array ( await res . arrayBuffer ( ) )
115
+ return await pendingResponse
83
116
} catch ( cause ) {
84
117
// @ts -expect-error - TS thinks signal?.aborted can only be false now
85
118
// because it was checked for true above.
@@ -88,6 +121,8 @@ export class TrustlessGateway {
88
121
}
89
122
this . #errors++
90
123
throw new Error ( `unable to fetch raw block for CID ${ cid } ` )
124
+ } finally {
125
+ this . #pendingResponses. delete ( blockId )
91
126
}
92
127
}
93
128
@@ -130,4 +165,14 @@ export class TrustlessGateway {
130
165
incrementInvalidBlocks ( ) : void {
131
166
this . #invalidBlocks++
132
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
+ }
133
178
}
0 commit comments