@@ -20,7 +20,8 @@ const log = logger('delegated-routing-v1-http-api-client')
20
20
21
21
const defaultValues = {
22
22
concurrentRequests : 4 ,
23
- timeout : 30e3
23
+ timeout : 30e3 ,
24
+ cacheTTL : 5 * 60 * 1000 // 5 minutes default as per https://specs.ipfs.tech/routing/http-routing-v1/#response-headers
24
25
}
25
26
26
27
export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV1HttpApiClient {
@@ -34,7 +35,8 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
34
35
private readonly filterAddrs ?: string [ ]
35
36
private readonly filterProtocols ?: string [ ]
36
37
private readonly inFlightRequests : Map < string , Promise < Response > >
37
-
38
+ private cache ?: Cache
39
+ private readonly cacheTTL : number
38
40
/**
39
41
* Create a new DelegatedContentRouting instance
40
42
*/
@@ -52,6 +54,17 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
52
54
this . filterProtocols = init . filterProtocols
53
55
this . contentRouting = new DelegatedRoutingV1HttpApiClientContentRouting ( this )
54
56
this . peerRouting = new DelegatedRoutingV1HttpApiClientPeerRouting ( this )
57
+
58
+ const cacheEnabled = typeof globalThis . caches !== 'undefined'
59
+ if ( cacheEnabled ) {
60
+ log ( 'cache enabled' )
61
+ globalThis . caches . open ( 'delegated-routing-v1-cache' ) . then ( cache => {
62
+ this . cache = cache
63
+ } ) . catch ( ( ) => {
64
+ this . cache = undefined
65
+ } )
66
+ }
67
+ this . cacheTTL = init . cacheTTL ?? defaultValues . cacheTTL
55
68
}
56
69
57
70
get [ contentRoutingSymbol ] ( ) : ContentRouting {
@@ -74,6 +87,11 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
74
87
this . httpQueue . clear ( )
75
88
this . shutDownController . abort ( )
76
89
this . started = false
90
+
91
+ // Clear the cache when stopping
92
+ if ( this . cache != null ) {
93
+ void this . cache . delete ( 'delegated-routing-v1-cache' )
94
+ }
77
95
}
78
96
79
97
async * getProviders ( cid : CID , options : GetProvidersOptions = { } ) : AsyncGenerator < PeerRecord > {
@@ -352,27 +370,60 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
352
370
}
353
371
}
354
372
355
- // Ensures that only one concurrent request is made for the same url-method tuple
373
+ //
374
+ // and caches GET requests
375
+ /**
376
+ * makeRequest has two features:
377
+ * - Ensures only one concurrent request is made for the same URL
378
+ * - Caches GET requests if the Cache API is available
379
+ */
356
380
async #makeRequest ( url : string , options : RequestInit ) : Promise < Response > {
357
- const key = `${ options . method ?? 'GET' } -${ url } `
381
+ const requestMethod = options . method ?? 'GET'
382
+ const key = `${ requestMethod } -${ url } `
383
+
384
+ // Only try to use cache for GET requests
385
+ if ( requestMethod === 'GET' ) {
386
+ const cachedResponse = await this . cache ?. match ( url )
387
+ if ( cachedResponse != null ) {
388
+ // Check if the cached response has expired
389
+ const expires = parseInt ( cachedResponse . headers . get ( 'x-cache-expires' ) ?? '0' , 10 )
390
+ if ( expires > Date . now ( ) ) {
391
+ log ( 'returning cached response for %s' , key )
392
+ return cachedResponse
393
+ } else {
394
+ // Remove expired response from cache
395
+ await this . cache ?. delete ( url )
396
+ }
397
+ }
398
+ }
358
399
359
- // Check if there's already an in-flight request for this ur-method tuple
400
+ // Check if there's already an in-flight request for this URL
360
401
const existingRequest = this . inFlightRequests . get ( key )
361
402
if ( existingRequest != null ) {
362
403
const response = await existingRequest
363
- // Clone the response since it can only be consumed once
404
+ log ( 'deduplicating outgoing request for %s' , key )
364
405
return response . clone ( )
365
406
}
366
407
367
408
// Create new request and track it
368
- const requestPromise = fetch ( url , options ) . finally ( ( ) => {
409
+ const requestPromise = fetch ( url , options ) . then ( async response => {
410
+ // Only cache successful GET requests
411
+ if ( this . cache != null && response . ok && requestMethod === 'GET' ) {
412
+ // Create a new response with expiration header
413
+ const cachedResponse = response . clone ( )
414
+ const expires = Date . now ( ) + this . cacheTTL
415
+ cachedResponse . headers . set ( 'x-cache-expires' , expires . toString ( ) )
416
+
417
+ await this . cache . put ( url , cachedResponse )
418
+ }
419
+ return response
420
+ } ) . finally ( ( ) => {
369
421
// Clean up the tracked request when it completes
370
422
this . inFlightRequests . delete ( key )
371
423
} )
372
424
373
425
this . inFlightRequests . set ( key , requestPromise )
374
426
const response = await requestPromise
375
- // Return a clone for the first caller too, so all callers get a fresh response
376
427
return response . clone ( )
377
428
}
378
429
}
0 commit comments