@@ -75,11 +75,13 @@ const toBase64UrlBuffer = (input) => Buffer.from(input.replace(/-/g, '+').replac
7575/**
7676 * In-memory cache for Google's public JWK keys.
7777 * Keys are refreshed when the cache expires (based on Cache-Control max-age).
78+ * An in-flight promise is stored to prevent redundant concurrent fetches (single-flight).
7879 */
79- const googleJwkCache = { keys : null , expiresAt : 0 } ;
80+ const googleJwkCache = { keys : null , expiresAt : 0 , inflight : null } ;
8081
8182/**
8283 * Fetches Google's public JWK keys, using an in-memory cache keyed by Cache-Control max-age.
84+ * Uses a single-flight pattern so that concurrent requests share one fetch instead of many.
8385 * @returns {Promise<Array> } Array of JWK key objects
8486 */
8587const getGooglePublicKeys = async ( ) => {
@@ -88,25 +90,38 @@ const getGooglePublicKeys = async () => {
8890 return googleJwkCache . keys ;
8991 }
9092
91- const certsResponse = await fetch ( 'https://www.googleapis.com/oauth2/v3/certs' ) ;
92- if ( ! certsResponse . ok ) {
93- throw new Error ( 'Unable to fetch Google JWK keys' ) ;
93+ // Single-flight: reuse in-flight promise if a fetch is already in progress.
94+ if ( googleJwkCache . inflight ) {
95+ return googleJwkCache . inflight ;
9496 }
9597
96- const certsPayload = await certsResponse . json ( ) ;
97- const keys = certsPayload . keys || [ ] ;
98+ googleJwkCache . inflight = ( async ( ) => {
99+ try {
100+ const certsResponse = await fetch ( 'https://www.googleapis.com/oauth2/v3/certs' ) ;
101+ if ( ! certsResponse . ok ) {
102+ throw new Error ( 'Unable to fetch Google JWK keys' ) ;
103+ }
104+
105+ const certsPayload = await certsResponse . json ( ) ;
106+ const keys = certsPayload . keys || [ ] ;
98107
99- // Parse Cache-Control max-age from response headers to determine TTL.
100- const cacheControl = ( typeof certsResponse . headers ?. get === 'function' )
101- ? ( certsResponse . headers . get ( 'cache-control' ) || '' )
102- : '' ;
103- const maxAgeMatch = cacheControl . match ( / m a x - a g e = ( \d + ) / ) ;
104- const ttlMs = maxAgeMatch ? parseInt ( maxAgeMatch [ 1 ] , 10 ) * 1000 : 3600 * 1000 ;
108+ // Parse Cache-Control max-age from response headers to determine TTL.
109+ const cacheControl = ( typeof certsResponse . headers ?. get === 'function' )
110+ ? ( certsResponse . headers . get ( 'cache-control' ) || '' )
111+ : '' ;
112+ const maxAgeMatch = cacheControl . match ( / m a x - a g e = ( \d + ) / ) ;
113+ const ttlMs = maxAgeMatch ? parseInt ( maxAgeMatch [ 1 ] , 10 ) * 1000 : 3600 * 1000 ;
105114
106- googleJwkCache . keys = keys ;
107- googleJwkCache . expiresAt = now + ttlMs ;
115+ googleJwkCache . keys = keys ;
116+ googleJwkCache . expiresAt = Date . now ( ) + ttlMs ;
117+
118+ return keys ;
119+ } finally {
120+ googleJwkCache . inflight = null ;
121+ }
122+ } ) ( ) ;
108123
109- return keys ;
124+ return googleJwkCache . inflight ;
110125} ;
111126
112127/**
0 commit comments