@fluidframework/core-utils 2.0.0-dev-rc.1.0.0.224419
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +36 -0
- package/.mocharc.js +13 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/api-extractor-lint.json +4 -0
- package/api-extractor.json +4 -0
- package/api-report/core-utils.api.md +147 -0
- package/dist/assert.d.ts +17 -0
- package/dist/assert.d.ts.map +1 -0
- package/dist/assert.js +25 -0
- package/dist/assert.js.map +1 -0
- package/dist/compare.d.ts +16 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +28 -0
- package/dist/compare.js.map +1 -0
- package/dist/core-utils-alpha.d.ts +191 -0
- package/dist/core-utils-beta.d.ts +41 -0
- package/dist/core-utils-public.d.ts +41 -0
- package/dist/core-utils-untrimmed.d.ts +414 -0
- package/dist/delay.d.ts +11 -0
- package/dist/delay.d.ts.map +1 -0
- package/dist/delay.js +15 -0
- package/dist/delay.js.map +1 -0
- package/dist/heap.d.ts +86 -0
- package/dist/heap.d.ts.map +1 -0
- package/dist/heap.js +144 -0
- package/dist/heap.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lazy.d.ts +44 -0
- package/dist/lazy.d.ts.map +1 -0
- package/dist/lazy.js +84 -0
- package/dist/lazy.js.map +1 -0
- package/dist/promiseCache.d.ts +89 -0
- package/dist/promiseCache.d.ts.map +1 -0
- package/dist/promiseCache.js +148 -0
- package/dist/promiseCache.js.map +1 -0
- package/dist/promises.d.ts +38 -0
- package/dist/promises.d.ts.map +1 -0
- package/dist/promises.js +60 -0
- package/dist/promises.js.map +1 -0
- package/dist/timer.d.ts +115 -0
- package/dist/timer.d.ts.map +1 -0
- package/dist/timer.js +189 -0
- package/dist/timer.js.map +1 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/dist/unreachable.d.ts +22 -0
- package/dist/unreachable.d.ts.map +1 -0
- package/dist/unreachable.js +28 -0
- package/dist/unreachable.js.map +1 -0
- package/lib/assert.d.ts +17 -0
- package/lib/assert.d.ts.map +1 -0
- package/lib/assert.js +25 -0
- package/lib/assert.js.map +1 -0
- package/lib/compare.d.ts +16 -0
- package/lib/compare.d.ts.map +1 -0
- package/lib/compare.js +28 -0
- package/lib/compare.js.map +1 -0
- package/lib/core-utils-alpha.d.ts +191 -0
- package/lib/core-utils-beta.d.ts +41 -0
- package/lib/core-utils-public.d.ts +41 -0
- package/lib/core-utils-untrimmed.d.ts +414 -0
- package/lib/delay.d.ts +11 -0
- package/lib/delay.d.ts.map +1 -0
- package/lib/delay.js +15 -0
- package/lib/delay.js.map +1 -0
- package/lib/heap.d.ts +86 -0
- package/lib/heap.d.ts.map +1 -0
- package/lib/heap.js +144 -0
- package/lib/heap.js.map +1 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +30 -0
- package/lib/index.js.map +1 -0
- package/lib/lazy.d.ts +44 -0
- package/lib/lazy.d.ts.map +1 -0
- package/lib/lazy.js +84 -0
- package/lib/lazy.js.map +1 -0
- package/lib/promiseCache.d.ts +89 -0
- package/lib/promiseCache.d.ts.map +1 -0
- package/lib/promiseCache.js +148 -0
- package/lib/promiseCache.js.map +1 -0
- package/lib/promises.d.ts +38 -0
- package/lib/promises.d.ts.map +1 -0
- package/lib/promises.js +60 -0
- package/lib/promises.js.map +1 -0
- package/lib/timer.d.ts +115 -0
- package/lib/timer.d.ts.map +1 -0
- package/lib/timer.js +189 -0
- package/lib/timer.js.map +1 -0
- package/lib/unreachable.d.ts +22 -0
- package/lib/unreachable.d.ts.map +1 -0
- package/lib/unreachable.js +28 -0
- package/lib/unreachable.js.map +1 -0
- package/package.json +111 -0
- package/prettier.config.cjs +8 -0
- package/src/assert.ts +23 -0
- package/src/compare.ts +33 -0
- package/src/delay.ts +12 -0
- package/src/heap.ts +182 -0
- package/src/index.ts +21 -0
- package/src/lazy.ts +88 -0
- package/src/promiseCache.ts +205 -0
- package/src/promises.ts +63 -0
- package/src/timer.ts +289 -0
- package/src/unreachable.ts +24 -0
- package/tsconfig.esnext.json +6 -0
- package/tsconfig.json +12 -0
package/src/heap.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for a comparer.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export interface IComparer<T> {
|
|
11
|
+
/**
|
|
12
|
+
* The minimum value of type T.
|
|
13
|
+
*/
|
|
14
|
+
min: T;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compare the two value
|
|
18
|
+
*
|
|
19
|
+
* @returns 0 if the value is equal, negative number if a is smaller then b, positive number otherwise
|
|
20
|
+
*/
|
|
21
|
+
compare(a: T, b: T): number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A comparer for numbers.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export const NumberComparer: IComparer<number> = {
|
|
29
|
+
/**
|
|
30
|
+
* The compare function for numbers.
|
|
31
|
+
* @returns The difference of the two numbers.
|
|
32
|
+
*/
|
|
33
|
+
compare: (a, b): number => a - b,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The minimum value of a JavaScript number, which is `Number.MIN_VALUE`.
|
|
37
|
+
*/
|
|
38
|
+
min: Number.MIN_VALUE,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interface to a node in {@link Heap}.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export interface IHeapNode<T> {
|
|
46
|
+
value: T;
|
|
47
|
+
position: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ordered {@link https://en.wikipedia.org/wiki/Heap_(data_structure) | Heap} data structure implementation.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export class Heap<T> {
|
|
55
|
+
private L: IHeapNode<T>[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates an instance of `Heap` with comparer.
|
|
59
|
+
* @param comp - A comparer that specify how elements are ordered.
|
|
60
|
+
*/
|
|
61
|
+
constructor(public comp: IComparer<T>) {
|
|
62
|
+
this.L = [{ value: comp.min, position: 0 }];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Return the smallest element in the heap as determined by the order of the comparer
|
|
67
|
+
*
|
|
68
|
+
* @returns Heap node containing the smallest element
|
|
69
|
+
*/
|
|
70
|
+
public peek(): IHeapNode<T> | undefined {
|
|
71
|
+
return this.L[1];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get and remove the smallest element in the heap as determined by the order of the comparer
|
|
76
|
+
*
|
|
77
|
+
* @returns The smallest value in the heap
|
|
78
|
+
*/
|
|
79
|
+
public get(): T | undefined {
|
|
80
|
+
if (this.L.length === 0) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.swap(1, this.count());
|
|
85
|
+
const x = this.L.pop();
|
|
86
|
+
this.fixdown(1);
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
88
|
+
return x!.value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add a value to the heap
|
|
93
|
+
*
|
|
94
|
+
* @param x - value to add
|
|
95
|
+
* @returns The heap node that contains the value
|
|
96
|
+
*/
|
|
97
|
+
public add(x: T): IHeapNode<T> {
|
|
98
|
+
const node = { value: x, position: this.L.length };
|
|
99
|
+
this.L.push(node);
|
|
100
|
+
this.fixup(this.count());
|
|
101
|
+
|
|
102
|
+
return node;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Allows for the Heap to be updated after a node's value changes.
|
|
107
|
+
*/
|
|
108
|
+
public update(node: IHeapNode<T>): void {
|
|
109
|
+
const k = node.position;
|
|
110
|
+
if (this.isGreaterThanParent(k)) {
|
|
111
|
+
this.fixup(k);
|
|
112
|
+
} else {
|
|
113
|
+
this.fixdown(k);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Removes the given node from the heap.
|
|
119
|
+
*
|
|
120
|
+
* @param node - The node to remove from the heap.
|
|
121
|
+
*/
|
|
122
|
+
public remove(node: IHeapNode<T>): void {
|
|
123
|
+
// Move the node we want to remove to the end of the array
|
|
124
|
+
const position = node.position;
|
|
125
|
+
this.swap(node.position, this.L.length - 1);
|
|
126
|
+
this.L.splice(-1);
|
|
127
|
+
|
|
128
|
+
// Update the swapped node assuming we didn't remove the end of the list
|
|
129
|
+
if (position !== this.L.length) {
|
|
130
|
+
this.update(this.L[position]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the number of elements in the Heap.
|
|
136
|
+
*
|
|
137
|
+
* @returns The number of elements in the Heap.
|
|
138
|
+
*/
|
|
139
|
+
public count(): number {
|
|
140
|
+
return this.L.length - 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fixup(pos: number): void {
|
|
144
|
+
let k = pos;
|
|
145
|
+
while (this.isGreaterThanParent(k)) {
|
|
146
|
+
// eslint-disable-next-line no-bitwise
|
|
147
|
+
const parent = k >> 1;
|
|
148
|
+
this.swap(k, parent);
|
|
149
|
+
k = parent;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private isGreaterThanParent(k: number): boolean {
|
|
154
|
+
// eslint-disable-next-line no-bitwise
|
|
155
|
+
return k > 1 && this.comp.compare(this.L[k >> 1].value, this.L[k].value) > 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fixdown(pos: number): void {
|
|
159
|
+
let k = pos;
|
|
160
|
+
// eslint-disable-next-line no-bitwise
|
|
161
|
+
while (k << 1 <= this.count()) {
|
|
162
|
+
// eslint-disable-next-line no-bitwise
|
|
163
|
+
let j = k << 1;
|
|
164
|
+
if (j < this.count() && this.comp.compare(this.L[j].value, this.L[j + 1].value) > 0) {
|
|
165
|
+
j++;
|
|
166
|
+
}
|
|
167
|
+
if (this.comp.compare(this.L[k].value, this.L[j].value) <= 0) {
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
this.swap(k, j);
|
|
171
|
+
k = j;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private swap(k: number, j: number): void {
|
|
176
|
+
const tmp = this.L[k];
|
|
177
|
+
this.L[k] = this.L[j];
|
|
178
|
+
this.L[k].position = k;
|
|
179
|
+
this.L[j] = tmp;
|
|
180
|
+
this.L[j].position = j;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { assert } from "./assert";
|
|
7
|
+
export { compareArrays } from "./compare";
|
|
8
|
+
export { delay } from "./delay";
|
|
9
|
+
export { Heap, IComparer, IHeapNode, NumberComparer } from "./heap";
|
|
10
|
+
export { Lazy, LazyPromise } from "./lazy";
|
|
11
|
+
export { PromiseCache, PromiseCacheExpiry, PromiseCacheOptions } from "./promiseCache";
|
|
12
|
+
export { Deferred } from "./promises";
|
|
13
|
+
export {
|
|
14
|
+
IPromiseTimer,
|
|
15
|
+
IPromiseTimerResult,
|
|
16
|
+
ITimer,
|
|
17
|
+
PromiseTimer,
|
|
18
|
+
setLongTimeout,
|
|
19
|
+
Timer,
|
|
20
|
+
} from "./timer";
|
|
21
|
+
export { unreachableCase } from "./unreachable";
|
package/src/lazy.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper class for lazy initialized values. Ensures the value is only generated once, and remain immutable.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export class Lazy<T> {
|
|
11
|
+
private _value: T | undefined;
|
|
12
|
+
private _evaluated: boolean = false;
|
|
13
|
+
/**
|
|
14
|
+
* Instantiates an instance of Lazy<T>.
|
|
15
|
+
* @param valueGenerator - The function that will generate the value when value is accessed the first time.
|
|
16
|
+
*/
|
|
17
|
+
public constructor(private readonly valueGenerator: () => T) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Return true if the value as been generated, otherwise false.
|
|
21
|
+
*/
|
|
22
|
+
public get evaluated(): boolean {
|
|
23
|
+
return this._evaluated;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the value. If this is the first call the value will be generated.
|
|
28
|
+
*/
|
|
29
|
+
public get value(): T {
|
|
30
|
+
if (!this._evaluated) {
|
|
31
|
+
this._evaluated = true;
|
|
32
|
+
this._value = this.valueGenerator();
|
|
33
|
+
}
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
35
|
+
return this._value!;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A lazy evaluated promise. The execute function is delayed until
|
|
41
|
+
* the promise is used, e.g. await, then, catch ...
|
|
42
|
+
* The execute function is only called once.
|
|
43
|
+
* All calls are then proxied to the promise returned by the execute method.
|
|
44
|
+
* @alpha
|
|
45
|
+
*/
|
|
46
|
+
export class LazyPromise<T> implements Promise<T> {
|
|
47
|
+
public get [Symbol.toStringTag](): string {
|
|
48
|
+
return this.getPromise()[Symbol.toStringTag];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private result: Promise<T> | undefined;
|
|
52
|
+
|
|
53
|
+
public constructor(private readonly execute: () => Promise<T>) {}
|
|
54
|
+
|
|
55
|
+
// eslint-disable-next-line unicorn/no-thenable
|
|
56
|
+
public async then<TResult1 = T, TResult2 = never>(
|
|
57
|
+
// eslint-disable-next-line @rushstack/no-new-null
|
|
58
|
+
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
|
59
|
+
// TODO: Use `unknown` instead (API breaking)
|
|
60
|
+
// eslint-disable-next-line @rushstack/no-new-null, @typescript-eslint/no-explicit-any
|
|
61
|
+
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined,
|
|
62
|
+
): Promise<TResult1 | TResult2> {
|
|
63
|
+
// eslint-disable-next-line prefer-rest-params
|
|
64
|
+
return this.getPromise().then<TResult1, TResult2>(...arguments);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async catch<TResult = never>(
|
|
68
|
+
// TODO: Use `unknown` instead (API breaking)
|
|
69
|
+
// eslint-disable-next-line @rushstack/no-new-null, @typescript-eslint/no-explicit-any
|
|
70
|
+
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined,
|
|
71
|
+
): Promise<T | TResult> {
|
|
72
|
+
// eslint-disable-next-line prefer-rest-params
|
|
73
|
+
return this.getPromise().catch<TResult>(...arguments);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// eslint-disable-next-line @rushstack/no-new-null
|
|
77
|
+
public async finally(onfinally?: (() => void) | null | undefined): Promise<T> {
|
|
78
|
+
// eslint-disable-next-line prefer-rest-params
|
|
79
|
+
return this.getPromise().finally(...arguments);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async getPromise(): Promise<T> {
|
|
83
|
+
if (this.result === undefined) {
|
|
84
|
+
this.result = this.execute();
|
|
85
|
+
}
|
|
86
|
+
return this.result;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Three supported expiry policies:
|
|
8
|
+
* - indefinite: entries don't expire and must be explicitly removed
|
|
9
|
+
* - absolute: entries expire after the given duration in MS, even if accessed multiple times in the mean time
|
|
10
|
+
* - sliding: entries expire after the given duration in MS of inactivity (i.e. get resets the clock)
|
|
11
|
+
* @alpha
|
|
12
|
+
*/
|
|
13
|
+
export type PromiseCacheExpiry =
|
|
14
|
+
| {
|
|
15
|
+
policy: "indefinite";
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
policy: "absolute" | "sliding";
|
|
19
|
+
durationMs: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for configuring the {@link PromiseCache}
|
|
24
|
+
* @alpha
|
|
25
|
+
*/
|
|
26
|
+
export interface PromiseCacheOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Common expiration policy for all items added to this cache
|
|
29
|
+
*/
|
|
30
|
+
expiry?: PromiseCacheExpiry;
|
|
31
|
+
/**
|
|
32
|
+
* If the stored Promise is rejected with a particular error, should the given key be removed?
|
|
33
|
+
*/
|
|
34
|
+
// TODO: Use `unknown` instead (API breaking)
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
removeOnError?: (error: any) => boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handles garbage collection of expiring cache entries.
|
|
41
|
+
* Not exported.
|
|
42
|
+
*/
|
|
43
|
+
class GarbageCollector<TKey> {
|
|
44
|
+
private readonly gcTimeouts = new Map<TKey, ReturnType<typeof setTimeout>>();
|
|
45
|
+
|
|
46
|
+
public constructor(
|
|
47
|
+
private readonly expiry: PromiseCacheExpiry,
|
|
48
|
+
private readonly cleanup: (key: TKey) => void,
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Schedule GC for the given key, as applicable
|
|
53
|
+
*/
|
|
54
|
+
public schedule(key: TKey): void {
|
|
55
|
+
if (this.expiry.policy !== "indefinite") {
|
|
56
|
+
this.gcTimeouts.set(
|
|
57
|
+
key,
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
this.cleanup(key);
|
|
60
|
+
this.cancel(key);
|
|
61
|
+
}, this.expiry.durationMs),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cancel any pending GC for the given key
|
|
68
|
+
*/
|
|
69
|
+
public cancel(key: TKey): void {
|
|
70
|
+
const timeout = this.gcTimeouts.get(key);
|
|
71
|
+
if (timeout !== undefined) {
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
this.gcTimeouts.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Update any pending GC for the given key, as applicable
|
|
79
|
+
*/
|
|
80
|
+
public update(key: TKey): void {
|
|
81
|
+
// Cancel/reschedule new GC if the policy is sliding
|
|
82
|
+
if (this.expiry.policy === "sliding") {
|
|
83
|
+
this.cancel(key);
|
|
84
|
+
this.schedule(key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A specialized cache for async work, allowing you to safely cache the promised result of some async work
|
|
91
|
+
* without fear of running it multiple times or losing track of errors.
|
|
92
|
+
* @alpha
|
|
93
|
+
*/
|
|
94
|
+
export class PromiseCache<TKey, TResult> {
|
|
95
|
+
private readonly cache = new Map<TKey, Promise<TResult>>();
|
|
96
|
+
private readonly gc: GarbageCollector<TKey>;
|
|
97
|
+
|
|
98
|
+
private readonly removeOnError: (error: unknown) => boolean;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create the PromiseCache with the given options, with the following defaults:
|
|
102
|
+
*
|
|
103
|
+
* expiry: indefinite, removeOnError: true for all errors
|
|
104
|
+
*/
|
|
105
|
+
public constructor({
|
|
106
|
+
expiry = { policy: "indefinite" },
|
|
107
|
+
removeOnError = (): boolean => true,
|
|
108
|
+
}: PromiseCacheOptions = {}) {
|
|
109
|
+
this.removeOnError = removeOnError;
|
|
110
|
+
this.gc = new GarbageCollector<TKey>(expiry, (key) => this.remove(key));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if there's anything cached at the given key
|
|
115
|
+
*/
|
|
116
|
+
public has(key: TKey): boolean {
|
|
117
|
+
return this.cache.has(key);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the Promise for the given key, or undefined if it's not found.
|
|
122
|
+
* Extend expiry if applicable.
|
|
123
|
+
*/
|
|
124
|
+
public get(key: TKey): Promise<TResult> | undefined {
|
|
125
|
+
if (this.has(key)) {
|
|
126
|
+
this.gc.update(key);
|
|
127
|
+
}
|
|
128
|
+
return this.cache.get(key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Remove the Promise for the given key, returning true if it was found and removed
|
|
133
|
+
*/
|
|
134
|
+
public remove(key: TKey): boolean {
|
|
135
|
+
this.gc.cancel(key);
|
|
136
|
+
return this.cache.delete(key);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Try to add the result of the given asyncFn, without overwriting an existing cache entry at that key.
|
|
141
|
+
* Returns a Promise for the added or existing async work being done at that key.
|
|
142
|
+
* @param key - key name where to store the async work
|
|
143
|
+
* @param asyncFn - the async work to do and store, if not already in progress under the given key
|
|
144
|
+
*/
|
|
145
|
+
public async addOrGet(key: TKey, asyncFn: () => Promise<TResult>): Promise<TResult> {
|
|
146
|
+
// NOTE: Do not await the Promise returned by asyncFn!
|
|
147
|
+
// Let the caller do so once we return or after a subsequent call to get
|
|
148
|
+
let promise = this.get(key);
|
|
149
|
+
if (promise === undefined) {
|
|
150
|
+
// Wrap in an async lambda in case asyncFn disabled @typescript-eslint/promise-function-async
|
|
151
|
+
const safeAsyncFn = async (): Promise<TResult> => asyncFn();
|
|
152
|
+
|
|
153
|
+
// Start the async work and put the Promise in the cache
|
|
154
|
+
promise = safeAsyncFn();
|
|
155
|
+
this.cache.set(key, promise);
|
|
156
|
+
|
|
157
|
+
// If asyncFn throws, we may remove the Promise from the cache
|
|
158
|
+
promise.catch((error) => {
|
|
159
|
+
if (this.removeOnError(error)) {
|
|
160
|
+
this.remove(key);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
this.gc.schedule(key);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return promise;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Try to add the result of the given asyncFn, without overwriting an existing cache entry at that key.
|
|
172
|
+
* Returns false if the cache already contained an entry at that key, and true otherwise.
|
|
173
|
+
* @param key - key name where to store the async work
|
|
174
|
+
* @param asyncFn - the async work to do and store, if not already in progress under the given key
|
|
175
|
+
*/
|
|
176
|
+
public add(key: TKey, asyncFn: () => Promise<TResult>): boolean {
|
|
177
|
+
const alreadyPresent = this.has(key);
|
|
178
|
+
|
|
179
|
+
// We are blindly adding the Promise to the cache here, which introduces a Promise in this scope.
|
|
180
|
+
// Swallow Promise rejections here, since whoever gets this out of the cache to use it will await/catch.
|
|
181
|
+
this.addOrGet(key, asyncFn).catch(() => {});
|
|
182
|
+
|
|
183
|
+
return !alreadyPresent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Try to add the given value, without overwriting an existing cache entry at that key.
|
|
188
|
+
* Returns a Promise for the added or existing async work being done at that key.
|
|
189
|
+
* @param key - key name where to store the async work
|
|
190
|
+
* @param value - value to store
|
|
191
|
+
*/
|
|
192
|
+
public async addValueOrGet(key: TKey, value: TResult): Promise<TResult> {
|
|
193
|
+
return this.addOrGet(key, async () => value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Try to add the given value, without overwriting an existing cache entry at that key.
|
|
198
|
+
* Returns false if the cache already contained an entry at that key, and true otherwise.
|
|
199
|
+
* @param key - key name where to store the value
|
|
200
|
+
* @param value - value to store
|
|
201
|
+
*/
|
|
202
|
+
public addValue(key: TKey, value: TResult): boolean {
|
|
203
|
+
return this.add(key, async () => value);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/promises.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A deferred creates a promise and the ability to resolve or reject it
|
|
8
|
+
* @alpha
|
|
9
|
+
*/
|
|
10
|
+
export class Deferred<T> {
|
|
11
|
+
private readonly p: Promise<T>;
|
|
12
|
+
private res: ((value: T | PromiseLike<T>) => void) | undefined;
|
|
13
|
+
private rej: ((reason?: unknown) => void) | undefined;
|
|
14
|
+
private completed: boolean = false;
|
|
15
|
+
|
|
16
|
+
public constructor() {
|
|
17
|
+
this.p = new Promise<T>((resolve, reject) => {
|
|
18
|
+
this.res = resolve;
|
|
19
|
+
this.rej = reject;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns whether the underlying promise has been completed
|
|
24
|
+
*/
|
|
25
|
+
public get isCompleted(): boolean {
|
|
26
|
+
return this.completed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Retrieves the underlying promise for the deferred
|
|
31
|
+
*
|
|
32
|
+
* @returns the underlying promise
|
|
33
|
+
*/
|
|
34
|
+
public get promise(): Promise<T> {
|
|
35
|
+
return this.p;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the promise
|
|
40
|
+
*
|
|
41
|
+
* @param value - the value to resolve the promise with
|
|
42
|
+
*/
|
|
43
|
+
public resolve(value: T | PromiseLike<T>): void {
|
|
44
|
+
if (this.res !== undefined) {
|
|
45
|
+
this.completed = true;
|
|
46
|
+
this.res(value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rejects the promise
|
|
52
|
+
*
|
|
53
|
+
* @param value - the value to reject the promise with
|
|
54
|
+
*/
|
|
55
|
+
// TODO: Use `unknown` instead (API breaking)
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
57
|
+
public reject(error: any): void {
|
|
58
|
+
if (this.rej !== undefined) {
|
|
59
|
+
this.completed = true;
|
|
60
|
+
this.rej(error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|