@@ -88,53 +88,109 @@ export class ProviderKeysManager {
8888 return JSON . parse ( keys ) as ProviderKey [ ] ;
8989 }
9090
91+ /**
92+ * Get provider key with read-through cache pattern.
93+ * Returns cached data immediately, always refreshes in background.
94+ */
9195 async getProviderKeyWithFetch (
9296 provider : ModelProviderName ,
9397 providerModelId : string ,
9498 orgId : string ,
95- keyCuid ?: string
99+ keyCuid ?: string ,
100+ ctx ?: ExecutionContext
96101 ) : Promise < ProviderKey | null > {
102+ const cacheKey = `provider_keys_${ orgId } ` ;
103+ const ttl = 43200 ; // 12 hours
104+
105+ // Get cached keys
97106 const keys = await this . getProviderKeys ( orgId ) ;
107+
108+ // Try to find key from cache
98109 const validKey = this . chooseProviderKey (
99110 keys ?? [ ] ,
100111 provider ,
101112 providerModelId ,
102113 keyCuid
103114 ) ;
104115
105- if ( ! validKey ) {
106- const keys = await this . store . getProviderKeysWithFetch (
116+ if ( validKey ) {
117+ // Cache hit - trigger background refresh and return immediately
118+ if ( ctx ) {
119+ ctx . waitUntil (
120+ this . fetchAndCacheProviderKey (
121+ provider ,
122+ providerModelId ,
123+ orgId ,
124+ keyCuid ,
125+ cacheKey ,
126+ ttl
127+ )
128+ ) ;
129+ }
130+ return validKey ;
131+ }
132+
133+ // Cache miss - must wait for fetch
134+ return this . fetchAndCacheProviderKey (
135+ provider ,
136+ providerModelId ,
137+ orgId ,
138+ keyCuid ,
139+ cacheKey ,
140+ ttl
141+ ) ;
142+ }
143+
144+ /**
145+ * Fetch provider key from Supabase and update cache.
146+ * Used both for cache miss (awaited) and background refresh (fire-and-forget).
147+ */
148+ private async fetchAndCacheProviderKey (
149+ provider : ModelProviderName ,
150+ providerModelId : string ,
151+ orgId : string ,
152+ keyCuid : string | undefined ,
153+ cacheKey : string ,
154+ ttl : number
155+ ) : Promise < ProviderKey | null > {
156+ try {
157+ const fetchedKeys = await this . store . getProviderKeysWithFetch (
107158 provider ,
108159 orgId ,
109160 keyCuid
110161 ) ;
111162
112- if ( ! keys ) return null ;
163+ if ( ! fetchedKeys || fetchedKeys . length === 0 ) return null ;
164+
165+ // Merge with existing cache
166+ const existingCached = await this . getProviderKeys ( orgId ) ;
167+ const existingKeys = existingCached ?? [ ] ;
168+
169+ // Dedupe by cuid (or provider if no cuid)
170+ const keyMap = new Map < string , ProviderKey > ( ) ;
171+ for ( const key of existingKeys ) {
172+ const id = key . cuid ?? `${ key . provider } ` ;
173+ keyMap . set ( id , key ) ;
174+ }
175+ for ( const key of fetchedKeys ) {
176+ const id = key . cuid ?? `${ key . provider } ` ;
177+ keyMap . set ( id , key ) ;
178+ }
113179
114- const existingKeys = await getFromKVCacheOnly (
115- `provider_keys_${ orgId } ` ,
180+ const mergedKeys = Array . from ( keyMap . values ( ) ) ;
181+
182+ await storeInCache (
183+ cacheKey ,
184+ JSON . stringify ( mergedKeys ) ,
116185 this . env ,
117- 43200 // 12 hours
186+ ttl ,
187+ false // Don't use memory cache to avoid test contamination
118188 ) ;
119- if ( existingKeys ) {
120- const existingKeysData = JSON . parse ( existingKeys ) as ProviderKey [ ] ;
121- existingKeysData . push ( ...keys ) ;
122- await storeInCache (
123- `provider_keys_${ orgId } ` ,
124- JSON . stringify ( existingKeysData ) ,
125- this . env ,
126- 43200 // 12 hours
127- ) ;
128- } else {
129- await storeInCache (
130- `provider_keys_${ orgId } ` ,
131- JSON . stringify ( keys ) ,
132- this . env ,
133- 43200 // 12 hours
134- ) ;
135- }
136- return this . chooseProviderKey ( keys , provider , providerModelId , keyCuid ) ;
189+
190+ return this . chooseProviderKey ( fetchedKeys , provider , providerModelId , keyCuid ) ;
191+ } catch ( e ) {
192+ console . error ( `Failed to fetch/cache provider key for ${ orgId } :` , e ) ;
193+ return null ;
137194 }
138- return validKey ;
139195 }
140196}
0 commit comments