(options: GetModelsOptions)
| 292 | * @returns Fresh models from API, or existing cache if refresh yields worse data |
| 293 | */ |
| 294 | export const refreshModels = async (options: GetModelsOptions): Promise<ModelRecord> => { |
| 295 | const { provider } = options |
| 296 | const cacheKey = getCacheKey(options) |
| 297 | |
| 298 | const shouldSkipCache = isAuthScopedProvider(provider) |
| 299 | |
| 300 | // Check if there's already an in-flight refresh for this provider+url combination. |
| 301 | // This prevents race conditions where multiple concurrent refreshes might |
| 302 | // overwrite each other's results. Skip de-duplication for auth-scoped |
| 303 | // providers because two concurrent calls may carry different tokens |
| 304 | // (e.g., after a sign-out/sign-in within the same session) and we must |
| 305 | // not return the first caller's results to the second caller. |
| 306 | if (!shouldSkipCache) { |
| 307 | const existingRequest = inFlightRefresh.get(cacheKey) |
| 308 | if (existingRequest) { |
| 309 | return existingRequest |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | // Create the refresh promise and track it. |
| 314 | // |
| 315 | // The `finally` cleanup below runs only after the first `await` inside this async |
| 316 | // function yields, which cannot happen until the current synchronous run -- including |
| 317 | // the `inFlightRefresh.set(cacheKey, ...)` registration below -- has completed. So the |
| 318 | // entry is always present in the map before `finally` can delete it; the registration |
| 319 | // can never be lost to a microtask race even if the fetch resolves immediately. |
| 320 | const refreshPromise = (async (): Promise<ModelRecord> => { |
| 321 | try { |
| 322 | // Force fresh API fetch - skip getModelsFromCache() check |
| 323 | const models = await fetchModelsFromProvider(options) |
| 324 | const modelCount = Object.keys(models).length |
| 325 | |
| 326 | // Get existing cached data for comparison |
| 327 | const existingCache = shouldSkipCache ? undefined : getModelsFromCache(options) |
| 328 | const existingCount = existingCache ? Object.keys(existingCache).length : 0 |
| 329 | |
| 330 | if (modelCount === 0) { |
| 331 | TelemetryService.instance.captureEvent(TelemetryEventName.MODEL_CACHE_EMPTY_RESPONSE, { |
| 332 | provider, |
| 333 | context: "refreshModels", |
| 334 | hasExistingCache: existingCount > 0, |
| 335 | existingCacheSize: existingCount, |
| 336 | }) |
| 337 | if (existingCount > 0) { |
| 338 | return existingCache! |
| 339 | } else { |
| 340 | return {} |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | if (!shouldSkipCache) { |
| 345 | memoryCache.set(cacheKey, models) |
| 346 | |
| 347 | await writeModels(cacheKey, models).catch((err) => |
| 348 | console.error(`[refreshModels] Error writing ${cacheKey} models to disk:`, err), |
| 349 | ) |
| 350 | } |
| 351 |
no test coverage detected