@@ -20,7 +20,8 @@ const log = logger('delegated-routing-v1-http-api-client')
2020
2121const defaultValues = {
2222 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
2425}
2526
2627export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV1HttpApiClient {
@@ -34,7 +35,8 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
3435 private readonly filterAddrs ?: string [ ]
3536 private readonly filterProtocols ?: string [ ]
3637 private readonly inFlightRequests : Map < string , Promise < Response > >
37-
38+ private cache ?: Cache
39+ private readonly cacheTTL : number
3840 /**
3941 * Create a new DelegatedContentRouting instance
4042 */
@@ -52,6 +54,17 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
5254 this . filterProtocols = init . filterProtocols
5355 this . contentRouting = new DelegatedRoutingV1HttpApiClientContentRouting ( this )
5456 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
5568 }
5669
5770 get [ contentRoutingSymbol ] ( ) : ContentRouting {
@@ -74,6 +87,11 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
7487 this . httpQueue . clear ( )
7588 this . shutDownController . abort ( )
7689 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+ }
7795 }
7896
7997 async * getProviders ( cid : CID , options : GetProvidersOptions = { } ) : AsyncGenerator < PeerRecord > {
@@ -352,27 +370,60 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
352370 }
353371 }
354372
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+ */
356380 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+ }
358399
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
360401 const existingRequest = this . inFlightRequests . get ( key )
361402 if ( existingRequest != null ) {
362403 const response = await existingRequest
363- // Clone the response since it can only be consumed once
404+ log ( 'deduplicating outgoing request for %s' , key )
364405 return response . clone ( )
365406 }
366407
367408 // 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 ( ( ) => {
369421 // Clean up the tracked request when it completes
370422 this . inFlightRequests . delete ( key )
371423 } )
372424
373425 this . inFlightRequests . set ( key , requestPromise )
374426 const response = await requestPromise
375- // Return a clone for the first caller too, so all callers get a fresh response
376427 return response . clone ( )
377428 }
378429}
0 commit comments