@@ -11,9 +11,13 @@ import { parseUrl, addHttpPrefix } from './utils/url.js'
11
11
import { isBrowserContext } from './utils/runtime.js'
12
12
13
13
const MAX_NODE_WEIGHT = 100
14
+ /**
15
+ * @typedef {import('./types.js').Node } Node
16
+ */
14
17
15
18
export class Saturn {
16
19
static nodesListKey = 'saturn-nodes'
20
+ static defaultRaceCount = 3
17
21
/**
18
22
*
19
23
* @param {object } [opts={}]
@@ -53,6 +57,93 @@ export class Saturn {
53
57
this . loadNodesPromise = this . _loadNodes ( this . opts )
54
58
}
55
59
60
+ /**
61
+ *
62
+ * @param {string } cidPath
63
+ * @param {object } [opts={}]
64
+ * @param {('car'|'raw') } [opts.format]
65
+ * @param {number } [opts.connectTimeout=5000]
66
+ * @param {number } [opts.downloadTimeout=0]
67
+ * @returns {Promise<object> }
68
+ */
69
+ async fetchCIDWithRace ( cidPath , opts = { } ) {
70
+ const [ cid ] = ( cidPath ?? '' ) . split ( '/' )
71
+ CID . parse ( cid )
72
+
73
+ const jwt = await getJWT ( this . opts , this . storage )
74
+
75
+ const options = Object . assign ( { } , this . opts , { format : 'car' , jwt } , opts )
76
+
77
+ if ( ! isBrowserContext ) {
78
+ options . headers = {
79
+ ...( options . headers || { } ) ,
80
+ Authorization : 'Bearer ' + options . jwt
81
+ }
82
+ }
83
+
84
+ const origins = options . origins
85
+ const controllers = [ ]
86
+
87
+ const createFetchPromise = async ( origin ) => {
88
+ const fetchOptions = { ...options , url : origin }
89
+ const url = this . createRequestURL ( cidPath , fetchOptions )
90
+
91
+ const controller = new AbortController ( )
92
+ controllers . push ( controller )
93
+ const connectTimeout = setTimeout ( ( ) => {
94
+ controller . abort ( )
95
+ } , options . connectTimeout )
96
+
97
+ try {
98
+ res = await fetch ( parseUrl ( url ) , { signal : controller . signal , ...options } )
99
+ clearTimeout ( connectTimeout )
100
+ return { res, url, controller }
101
+ } catch ( err ) {
102
+ throw new Error (
103
+ `Non OK response received: ${ res . status } ${ res . statusText } `
104
+ )
105
+ }
106
+ }
107
+
108
+ const abortRemainingFetches = async ( successController , controllers ) => {
109
+ return controllers . forEach ( ( controller ) => {
110
+ if ( successController !== controller ) {
111
+ controller . abort ( 'Request race unsuccessful' )
112
+ }
113
+ } )
114
+ }
115
+
116
+ const fetchPromises = Promise . any ( origins . map ( ( origin ) => createFetchPromise ( origin ) ) )
117
+
118
+ let log = {
119
+ startTime : new Date ( )
120
+ }
121
+
122
+ let res , url , controller
123
+ try {
124
+ ( { res, url, controller } = await fetchPromises )
125
+
126
+ abortRemainingFetches ( controller , controllers )
127
+ log = Object . assign ( log , this . _generateLog ( res , log ) , { url } )
128
+
129
+ if ( ! res . ok ) {
130
+ throw new Error (
131
+ `Non OK response received: ${ res . status } ${ res . statusText } `
132
+ )
133
+ }
134
+ } catch ( err ) {
135
+ if ( ! res ) {
136
+ log . error = err . message
137
+ }
138
+ // Report now if error, otherwise report after download is done.
139
+ this . _finalizeLog ( log )
140
+
141
+ throw err
142
+ }
143
+
144
+ return { res, controller, log }
145
+ }
146
+
56
147
/**
57
148
*
58
149
* @param {string } cidPath
@@ -70,8 +161,7 @@ export class Saturn {
70
161
71
162
const options = Object . assign ( { } , this . opts , { format : 'car' , jwt } , opts )
72
163
const url = this . createRequestURL ( cidPath , options )
73
-
74
- const log = {
164
+ let log = {
75
165
url,
76
166
startTime : new Date ( )
77
167
}
@@ -93,13 +183,7 @@ export class Saturn {
93
183
94
184
clearTimeout ( connectTimeout )
95
185
96
- const { headers } = res
97
- log . ttfbMs = new Date ( ) - log . startTime
98
- log . httpStatusCode = res . status
99
- log . cacheHit = headers . get ( 'saturn-cache-status' ) === 'HIT'
100
- log . nodeId = headers . get ( 'saturn-node-id' )
101
- log . requestId = headers . get ( 'saturn-transfer-id' )
102
- log . httpProtocol = headers . get ( 'quic-status' )
186
+ log = Object . assign ( log , this . _generateLog ( res , log ) )
103
187
104
188
if ( ! res . ok ) {
105
189
throw new Error (
@@ -119,11 +203,32 @@ export class Saturn {
119
203
return { res, controller, log }
120
204
}
121
205
206
+ /**
207
+ * @param {Response } res
208
+ * @param {object } log
209
+ * @returns {object }
210
+ */
211
+ _generateLog ( res , log ) {
212
+ const { headers } = res
213
+ log . httpStatusCode = res . status
214
+ log . cacheHit = headers . get ( 'saturn-cache-status' ) === 'HIT'
215
+ log . nodeId = headers . get ( 'saturn-node-id' )
216
+ log . requestId = headers . get ( 'saturn-transfer-id' )
217
+ log . httpProtocol = headers . get ( 'quic-status' )
218
+
219
+ if ( res . ok ) {
220
+ log . ttfbMs = new Date ( ) - log . startTime
221
+ }
222
+
223
+ return log
224
+ }
225
+
122
226
/**
123
227
*
124
228
* @param {string } cidPath
125
229
* @param {object } [opts={}]
126
230
* @param {('car'|'raw') } [opts.format]
231
+ * @param {boolean } [opts.raceNodes]
127
232
* @param {string } [opts.url]
128
233
* @param {number } [opts.connectTimeout=5000]
129
234
* @param {number } [opts.downloadTimeout=0]
@@ -168,11 +273,18 @@ export class Saturn {
168
273
}
169
274
170
275
let fallbackCount = 0
171
- for ( const origin of this . nodes ) {
276
+ const nodes = this . nodes
277
+ for ( let i = 0 ; i < nodes . length ; i ++ ) {
172
278
if ( fallbackCount > this . opts . fallbackLimit ) {
173
279
return
174
280
}
175
- opts . url = origin . url
281
+ if ( opts . raceNodes ) {
282
+ const origins = nodes . slice ( i , i + Saturn . defaultRaceCount ) . map ( ( node ) => node . url )
283
+ opts . origins = origins
284
+ } else {
285
+ opts . url = nodes [ i ] . url
286
+ }
287
+
176
288
try {
177
289
yield * fetchContent ( )
178
290
return
@@ -191,13 +303,20 @@ export class Saturn {
191
303
*
192
304
* @param {string } cidPath
193
305
* @param {object } [opts={}]
194
- * @param {('car'|'raw') } [opts.format]
306
+ * @param {('car'|'raw') } [opts.format]- -
307
+ * @param {boolean } [opts.raceNodes]- -
195
308
* @param {number } [opts.connectTimeout=5000]
196
309
* @param {number } [opts.downloadTimeout=0]
197
310
* @returns {Promise<AsyncIterable<Uint8Array>> }
198
311
*/
199
312
async * fetchContent ( cidPath , opts = { } ) {
200
- const { res, controller, log } = await this . fetchCID ( cidPath , opts )
313
+ let res , controller , log
314
+
315
+ if ( opts . raceNodes ) {
316
+ ( { res, controller, log } = await this . fetchCIDWithRace ( cidPath , opts ) )
317
+ } else {
318
+ ( { res, controller, log } = await this . fetchCID ( cidPath , opts ) )
319
+ }
201
320
202
321
async function * metricsIterable ( itr ) {
203
322
log . numBytesSent = 0
@@ -226,6 +345,7 @@ export class Saturn {
226
345
* @param {string } cidPath
227
346
* @param {object } [opts={}]
228
347
* @param {('car'|'raw') } [opts.format]
348
+ * @param {boolean } [opts.raceNodes]
229
349
* @param {number } [opts.connectTimeout=5000]
230
350
* @param {number } [opts.downloadTimeout=0]
231
351
* @returns {Promise<Uint8Array> }
@@ -241,7 +361,7 @@ export class Saturn {
241
361
* @returns {URL }
242
362
*/
243
363
createRequestURL ( cidPath , opts ) {
244
- let origin = opts . url || opts . cdnURL
364
+ let origin = opts . url || ( opts . origins && opts . origins [ 0 ] ) || opts . cdnURL
245
365
origin = addHttpPrefix ( origin )
246
366
const url = new URL ( `${ origin } /ipfs/${ cidPath } ` )
247
367
@@ -371,6 +491,12 @@ export class Saturn {
371
491
}
372
492
}
373
493
494
+ /**
495
+ * Sorts nodes based on normalized distance and weights. Distance is prioritized for sorting.
496
+ *
497
+ * @param {Node[] } nodes
498
+ * @returns {Node[] }
499
+ */
374
500
_sortNodes ( nodes ) {
375
501
// Determine the maximum distance for normalization
376
502
const maxDistance = Math . max ( ...nodes . map ( node => node . distance ) )
0 commit comments