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