Path: blob/main/extensions/copilot/src/shared-fetch-utils/common/fetchedValue.ts
13397 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { FetchBlockedError } from './fetchTypes';67export interface FetchedValueOptions<T> {8/**9* The async function that fetches the value from the network.10*/11fetch: () => Promise<T>;1213/**14* Determines whether the current cached value is stale and should be re-fetched.15* Called by {@link FetchedValue.resolve} to decide if a new fetch is needed.16*17* Before the value has been fetched for the first time, the value is always considered stale18* and this function is not called.19*/20isStale: (value: T) => boolean;2122/**23* When `true`, automatically calls {@link FetchedValue.resolve} once per minute so24* the synchronous {@link FetchedValue.value} getter stays up-to-date.25*26* **Caution:** enabling this will lead to more network requests because the value27* is re-fetched periodically regardless of whether it is being read.28*/29keepCacheHot?: boolean;30}3132/**33* A cached value backed by an async fetch operation.34*35* Provides both a synchronous {@link value} accessor (returns the current cached value36* or `undefined`) and an asynchronous {@link resolve} method that only fetches when the37* value is stale, as determined by the caller-supplied {@link FetchedValueOptions.isStale}38* predicate.39*40* Concurrent calls to {@link resolve} are coalesced into a single in-flight fetch so the41* network is hit at most once per staleness window.42*43* @example44* ```ts45* const token = new FetchedValue({46* fetch: () => fetchTokenFromServer(),47* isStale: (t) => t.expires_at - 300 < nowSeconds(),48* });49*50* // Synchronous read — may be undefined before first resolve51* const current = token.value;52*53* // Async — fetches only when stale54* const fresh = await token.resolve();55* ```56*/57export class FetchedValue<T> {58private _value: T | undefined;59private _hasFetched = false;60private _inflightFetch: Promise<T> | undefined;61private _disposed = false;62private _keepCacheHotTimer: ReturnType<typeof setInterval> | undefined;6364private _fetch: (() => Promise<T>) | undefined;65private readonly _isStale: (value: T) => boolean;6667constructor(options: FetchedValueOptions<T>) {68this._fetch = options.fetch;69this._isStale = options.isStale;70if (options.keepCacheHot) {71this._keepCacheHotTimer = setInterval(() => {72this.resolve().catch(() => { /* swallow — next interval will retry */ });73}, 60_000);74}75}7677/**78* The current cached value, or `undefined` if no value has been fetched yet.79*80* This is a synchronous accessor — it never triggers a fetch.81*/82get value(): T | undefined {83return this._value;84}8586/**87* Returns the cached value if it is still fresh, otherwise fetches a new value.88*89* Concurrent calls while a fetch is in-flight share the same promise, so the90* network is never hit more than once per staleness window.91*92* @param force When `true`, bypasses the staleness check and always fetches.93*/94async resolve(force?: boolean): Promise<T> {95this._throwIfDisposed();96if (!force && this._hasFetched && !this._isStale(this._value as T)) {97return this._value as T;98}99if (this._inflightFetch) {100return this._inflightFetch;101}102this._inflightFetch = this._doFetch();103try {104return await this._inflightFetch;105} finally {106this._inflightFetch = undefined;107}108}109110dispose(): void {111this._disposed = true;112this._value = undefined;113this._hasFetched = false;114this._inflightFetch = undefined;115this._fetch = undefined;116if (this._keepCacheHotTimer !== undefined) {117clearInterval(this._keepCacheHotTimer);118this._keepCacheHotTimer = undefined;119}120}121122private async _doFetch(): Promise<T> {123this._throwIfDisposed();124try {125const newValue = await this._fetch!();126this._throwIfDisposed();127this._value = newValue;128this._hasFetched = true;129return newValue;130} catch (err) {131if (err instanceof FetchBlockedError && this._hasFetched) {132return this._value as T;133}134throw err;135}136}137138private _throwIfDisposed(): void {139if (this._disposed) {140throw new Error('FetchedValue has been disposed');141}142}143}144145146147148