@@ -14,26 +14,28 @@ import { isErrorUnavoidable } from './utils/errors.js'
14
14
const MAX_NODE_WEIGHT = 100
15
15
/**
16
16
* @typedef {import('./types.js').Node } Node
17
+ * @typedef {import('./types.js').FetchOptions } FetchOptions
17
18
*/
18
19
19
20
export class Saturn {
20
21
static nodesListKey = 'saturn-nodes'
21
22
static defaultRaceCount = 3
22
23
/**
23
24
*
24
- * @param {object } [opts={}]
25
- * @param {string } [opts.clientKey]
26
- * @param {string } [opts.clientId=randomUUID()]
27
- * @param {string } [opts.cdnURL=saturn.ms]
28
- * @param {number } [opts.connectTimeout=5000]
29
- * @param {number } [opts.downloadTimeout=0]
30
- * @param {string } [opts.orchURL]
31
- * @param {number } [opts.fallbackLimit]
32
- * @param {boolean } [opts.experimental]
33
- * @param {import('./storage/index.js').Storage } [opts.storage]
25
+ * @param {object } [config={}]
26
+ * @param {string } [config.clientKey]
27
+ * @param {string } [config.clientId=randomUUID()]
28
+ * @param {string } [config.cdnURL=saturn.ms]
29
+ * @param {number } [config.connectTimeout=5000]
30
+ * @param {number } [config.downloadTimeout=0]
31
+ * @param {string } [config.orchURL]
32
+ * @param {string } [config.customerFallbackURL]
33
+ * @param {number } [config.fallbackLimit]
34
+ * @param {boolean } [config.experimental]
35
+ * @param {import('./storage/index.js').Storage } [config.storage]
34
36
*/
35
- constructor ( opts = { } ) {
36
- this . opts = Object . assign ( { } , {
37
+ constructor ( config = { } ) {
38
+ this . config = Object . assign ( { } , {
37
39
clientId : randomUUID ( ) ,
38
40
cdnURL : 'l1s.saturn.ms' ,
39
41
logURL : 'https://twb3qukm2i654i3tnvx36char40aymqq.lambda-url.us-west-2.on.aws/' ,
@@ -42,9 +44,9 @@ export class Saturn {
42
44
fallbackLimit : 5 ,
43
45
connectTimeout : 5_000 ,
44
46
downloadTimeout : 0
45
- } , opts )
47
+ } , config )
46
48
47
- if ( ! this . opts . clientKey ) {
49
+ if ( ! this . config . clientKey ) {
48
50
throw new Error ( 'clientKey is required' )
49
51
}
50
52
@@ -55,28 +57,24 @@ export class Saturn {
55
57
if ( this . reportingLogs && this . hasPerformanceAPI ) {
56
58
this . _monitorPerformanceBuffer ( )
57
59
}
58
- this . storage = this . opts . storage || memoryStorage ( )
59
- this . loadNodesPromise = this . opts . experimental ? this . _loadNodes ( this . opts ) : null
60
+ this . storage = this . config . storage || memoryStorage ( )
61
+ this . loadNodesPromise = this . config . experimental ? this . _loadNodes ( this . config ) : null
60
62
}
61
63
62
64
/**
63
65
*
64
66
* @param {string } cidPath
65
- * @param {object } [opts={}]
66
- * @param {Node[] } [opts.nodes]
67
- * @param {Node } [opts.node]
68
- * @param {('car'|'raw') } [opts.format]
69
- * @param {number } [opts.connectTimeout=5000]
70
- * @param {number } [opts.downloadTimeout=0]
67
+ * @param {FetchOptions } [opts={}]
71
68
* @returns {Promise<object> }
72
69
*/
73
70
async fetchCIDWithRace ( cidPath , opts = { } ) {
74
- const [ cid ] = ( cidPath ?? '' ) . split ( '/' )
75
- CID . parse ( cid )
76
-
77
- const jwt = await getJWT ( this . opts , this . storage )
78
-
79
- const options = Object . assign ( { } , this . opts , { format : 'car' , jwt } , opts )
71
+ const options = Object . assign ( { } , this . config , { format : 'car' } , opts )
72
+ if ( ! opts . originFallback ) {
73
+ const [ cid ] = ( cidPath ?? '' ) . split ( '/' )
74
+ CID . parse ( cid )
75
+ const jwt = await getJWT ( options , this . storage )
76
+ options . jwt = jwt
77
+ }
80
78
81
79
if ( ! isBrowserContext ) {
82
80
options . headers = {
@@ -87,7 +85,7 @@ export class Saturn {
87
85
88
86
let nodes = options . nodes
89
87
if ( ! nodes || nodes . length === 0 ) {
90
- const replacementNode = options . node ?? { url : this . opts . cdnURL }
88
+ const replacementNode = { url : options . cdnURL }
91
89
nodes = [ replacementNode ]
92
90
}
93
91
const controllers = [ ]
@@ -157,22 +155,20 @@ export class Saturn {
157
155
/**
158
156
*
159
157
* @param {string } cidPath
160
- * @param {object } [opts={}]
161
- * @param {('car'|'raw') } [opts.format]
162
- * @param {Node } [opts.node]
163
- * @param {number } [opts.connectTimeout=5000]
164
- * @param {number } [opts.downloadTimeout=0]
158
+ * @param {FetchOptions } [opts={}]
165
159
* @returns {Promise<object> }
166
160
*/
167
161
async fetchCID ( cidPath , opts = { } ) {
168
- const [ cid ] = ( cidPath ?? '' ) . split ( '/' )
169
- CID . parse ( cid )
170
-
171
- const jwt = await getJWT ( this . opts , this . storage )
162
+ const options = Object . assign ( { } , this . config , { format : 'car' } , opts )
163
+ if ( ! opts . originFallback ) {
164
+ const [ cid ] = ( cidPath ?? '' ) . split ( '/' )
165
+ CID . parse ( cid )
166
+ const jwt = await getJWT ( this . config , this . storage )
167
+ options . jwt = jwt
168
+ }
172
169
173
- const options = Object . assign ( { } , this . opts , { format : 'car' , jwt } , opts )
174
- const node = options . node
175
- const origin = node ?. url ?? this . opts . cdnURL
170
+ const node = options . nodes && options . nodes [ 0 ]
171
+ const origin = node ?. url ?? this . config . cdnURL
176
172
const url = this . createRequestURL ( cidPath , { ...options , url : origin } )
177
173
178
174
let log = {
@@ -242,20 +238,15 @@ export class Saturn {
242
238
/**
243
239
*
244
240
* @param {string } cidPath
245
- * @param {object } [opts={}]
246
- * @param {('car'|'raw') } [opts.format]
247
- * @param {boolean } [opts.raceNodes]
248
- * @param {string } [opts.url]
249
- * @param {number } [opts.connectTimeout=5000]
250
- * @param {number } [opts.downloadTimeout=0]
251
- * @param {AbortController } [opts.controller]
241
+ * @param {FetchOptions } [opts={}]
252
242
* @returns {Promise<AsyncIterable<Uint8Array>> }
253
243
*/
254
244
async * fetchContentWithFallback ( cidPath , opts = { } ) {
255
- const upstreamController = opts . controller ;
256
- delete opts . controller ;
245
+ const upstreamController = opts . controller
246
+ delete opts . controller
257
247
258
248
let lastError = null
249
+ let skipNodes = false
259
250
// we use this to checkpoint at which chunk a request failed.
260
251
// this is temporary until range requests are supported.
261
252
let byteCountCheckpoint = 0
@@ -264,16 +255,17 @@ export class Saturn {
264
255
throw new Error ( `All attempts to fetch content have failed. Last error: ${ lastError . message } ` )
265
256
}
266
257
267
- const fetchContent = async function * ( ) {
268
- const controller = new AbortController ( ) ;
269
- opts . controller = controller ;
258
+ const fetchContent = async function * ( options ) {
259
+ const controller = new AbortController ( )
260
+ opts . controller = controller
270
261
if ( upstreamController ) {
271
262
upstreamController . signal . addEventListener ( 'abort' , ( ) => {
272
- controller . abort ( ) ;
273
- } ) ;
263
+ controller . abort ( )
264
+ } )
274
265
}
275
266
let byteCount = 0
276
- const byteChunks = await this . fetchContent ( cidPath , opts )
267
+ const fetchOptions = Object . assign ( opts , { format : 'car' } , options )
268
+ const byteChunks = await this . fetchContent ( cidPath , fetchOptions )
277
269
for await ( const chunk of byteChunks ) {
278
270
// avoid sending duplicate chunks
279
271
if ( byteCount < byteCountCheckpoint ) {
@@ -291,33 +283,34 @@ export class Saturn {
291
283
}
292
284
} . bind ( this )
293
285
286
+ // Use CDN origin if node list is not loaded
294
287
if ( this . nodes . length === 0 ) {
295
288
// fetch from origin in the case that no nodes are loaded
296
- opts . url = this . opts . cdnURL
289
+ opts . nodes = Array ( { url : this . config . cdnURL } )
297
290
try {
298
291
yield * fetchContent ( )
299
292
return
300
293
} catch ( err ) {
301
294
lastError = err
302
295
if ( err . res ?. status === 410 || isErrorUnavoidable ( err ) ) {
303
- throwError ( )
296
+ skipNodes = true
297
+ } else {
298
+ await this . loadNodesPromise
304
299
}
305
- await this . loadNodesPromise
306
300
}
307
301
}
308
302
309
303
let fallbackCount = 0
310
304
const nodes = this . nodes
311
305
for ( let i = 0 ; i < nodes . length ; i ++ ) {
312
- if ( fallbackCount > this . opts . fallbackLimit || upstreamController ?. signal . aborted ) {
313
- return
306
+ if ( fallbackCount > this . config . fallbackLimit || skipNodes || upstreamController ?. signal . aborted ) {
307
+ break
314
308
}
315
309
if ( opts . raceNodes ) {
316
310
opts . nodes = nodes . slice ( i , i + Saturn . defaultRaceCount )
317
311
} else {
318
- opts . node = nodes [ i ]
312
+ opts . nodes = Array ( nodes [ i ] )
319
313
}
320
-
321
314
try {
322
315
yield * fetchContent ( )
323
316
return
@@ -331,18 +324,25 @@ export class Saturn {
331
324
}
332
325
333
326
if ( lastError ) {
327
+ const originUrl = opts . customerFallbackURL ?? this . config . customerFallbackURL
328
+ // Use customer origin if cid is not retrievable by lassie.
329
+ if ( originUrl ) {
330
+ opts . nodes = Array ( { url : originUrl } )
331
+ try {
332
+ yield * fetchContent ( { format : null , originFallback : true } )
333
+ return
334
+ } catch ( err ) {
335
+ lastError = err
336
+ }
337
+ }
334
338
throwError ( )
335
339
}
336
340
}
337
341
338
342
/**
339
343
*
340
344
* @param {string } cidPath
341
- * @param {object } [opts={}]
342
- * @param {('car'|'raw') } [opts.format]
343
- * @param {boolean } [opts.raceNodes]
344
- * @param {number } [opts.connectTimeout=5000]
345
- * @param {number } [opts.downloadTimeout=0]
345
+ * @param {FetchOptions } [opts={}]
346
346
* @returns {Promise<AsyncIterable<Uint8Array>> }
347
347
*/
348
348
async * fetchContent ( cidPath , opts = { } ) {
@@ -365,7 +365,11 @@ export class Saturn {
365
365
366
366
try {
367
367
const itr = metricsIterable ( asAsyncIterable ( res . body ) )
368
- yield * extractVerifiedContent ( cidPath , itr )
368
+ if ( opts . format === 'car' ) {
369
+ yield * extractVerifiedContent ( cidPath , itr )
370
+ } else {
371
+ yield * itr
372
+ }
369
373
} catch ( err ) {
370
374
log . error = err . message
371
375
controller . abort ( )
@@ -379,11 +383,7 @@ export class Saturn {
379
383
/**
380
384
*
381
385
* @param {string } cidPath
382
- * @param {object } [opts={}]
383
- * @param {('car'|'raw') } [opts.format]
384
- * @param {boolean } [opts.raceNodes]
385
- * @param {number } [opts.connectTimeout=5000]
386
- * @param {number } [opts.downloadTimeout=0]
386
+ * @param {FetchOptions } [opts={}]
387
387
* @returns {Promise<Uint8Array> }
388
388
*/
389
389
async fetchContentBuffer ( cidPath , opts = { } ) {
@@ -395,14 +395,21 @@ export class Saturn {
395
395
* @param {string } cidPath
396
396
* @param {object } [opts={}]
397
397
* @param {string } [opts.url]
398
+ * @param {string } [opts.format]
399
+ * @param {string } [opts.originFallback]
400
+ * @param {object } [opts.jwt]
398
401
* @returns {URL }
399
402
*/
400
- createRequestURL ( cidPath , opts ) {
401
- let origin = opts . url ?? this . opts . cdnURL
403
+ createRequestURL ( cidPath , opts = { } ) {
404
+ let origin = opts . url ?? this . config . cdnURL
402
405
origin = addHttpPrefix ( origin )
406
+ if ( opts . originFallback ) {
407
+ return new URL ( origin )
408
+ }
403
409
const url = new URL ( `${ origin } /ipfs/${ cidPath } ` )
404
410
405
- url . searchParams . set ( 'format' , opts . format )
411
+ if ( opts . format ) url . searchParams . set ( 'format' , opts . format )
412
+
406
413
if ( opts . format === 'car' ) {
407
414
url . searchParams . set ( 'dag-scope' , 'entity' )
408
415
}
@@ -444,10 +451,10 @@ export class Saturn {
444
451
: this . logs
445
452
446
453
await fetch (
447
- this . opts . logURL ,
454
+ this . config . logURL ,
448
455
{
449
456
method : 'POST' ,
450
- body : JSON . stringify ( { bandwidthLogs, logSender : this . opts . logSender } )
457
+ body : JSON . stringify ( { bandwidthLogs, logSender : this . config . logSender } )
451
458
}
452
459
)
453
460
@@ -569,7 +576,7 @@ export class Saturn {
569
576
570
577
const url = new URL ( origin )
571
578
const controller = new AbortController ( )
572
- const options = Object . assign ( { } , { method : 'GET' } , this . opts )
579
+ const options = Object . assign ( { } , { method : 'GET' } , this . config )
573
580
574
581
const connectTimeout = setTimeout ( ( ) => {
575
582
controller . abort ( )
0 commit comments