Skip to content

Commit 2d76150

Browse files
Copiloteleanorjboyd
andcommitted
Add cancellable async timeout utilities from VS Code core
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent e2db6aa commit 2d76150

File tree

2 files changed

+409
-2
lines changed

2 files changed

+409
-2
lines changed

src/common/utils/asyncUtils.ts

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,137 @@
1-
export async function timeout(milliseconds: number): Promise<void> {
2-
return new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
1+
import { CancellationToken, CancellationTokenSource } from 'vscode';
2+
3+
/**
4+
* A promise that can be cancelled using the `.cancel()` method.
5+
*/
6+
export interface CancelablePromise<T> extends Promise<T> {
7+
cancel(): void;
8+
}
9+
10+
/**
11+
* Error thrown when a promise is cancelled.
12+
*/
13+
export class CancellationError extends Error {
14+
constructor() {
15+
super('Cancelled');
16+
this.name = 'CancellationError';
17+
}
18+
}
19+
20+
/**
21+
* Returns a promise that can be cancelled using the provided cancellation token.
22+
*
23+
* @remarks When cancellation is requested, the promise will be rejected with a {@link CancellationError}.
24+
*
25+
* @param callback A function that accepts a cancellation token and returns a promise
26+
* @returns A promise that can be cancelled
27+
*/
28+
export function createCancelablePromise<T>(callback: (token: CancellationToken) => Promise<T>): CancelablePromise<T> {
29+
const source = new CancellationTokenSource();
30+
31+
const thenable = callback(source.token);
32+
const promise = new Promise<T>((resolve, reject) => {
33+
const subscription = source.token.onCancellationRequested(() => {
34+
subscription.dispose();
35+
reject(new CancellationError());
36+
});
37+
Promise.resolve(thenable).then(
38+
(value) => {
39+
subscription.dispose();
40+
source.dispose();
41+
resolve(value);
42+
},
43+
(err) => {
44+
subscription.dispose();
45+
source.dispose();
46+
reject(err);
47+
},
48+
);
49+
});
50+
51+
return new (class {
52+
cancel() {
53+
source.cancel();
54+
source.dispose();
55+
}
56+
then<TResult1 = T, TResult2 = never>(
57+
resolve?: ((value: T) => TResult1 | Promise<TResult1>) | undefined | null,
58+
reject?: ((reason: unknown) => TResult2 | Promise<TResult2>) | undefined | null,
59+
): Promise<TResult1 | TResult2> {
60+
return promise.then(resolve, reject);
61+
}
62+
catch<TResult = never>(
63+
reject?: ((reason: unknown) => TResult | Promise<TResult>) | undefined | null,
64+
): Promise<T | TResult> {
65+
return this.then(undefined, reject);
66+
}
67+
finally(onfinally?: (() => void) | undefined | null): Promise<T> {
68+
return promise.finally(onfinally);
69+
}
70+
})() as CancelablePromise<T>;
71+
}
72+
73+
/**
74+
* Returns a promise that resolves with `undefined` as soon as the passed token is cancelled.
75+
* @see {@link raceCancellationError}
76+
*/
77+
export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken): Promise<T | undefined>;
78+
79+
/**
80+
* Returns a promise that resolves with `defaultValue` as soon as the passed token is cancelled.
81+
* @see {@link raceCancellationError}
82+
*/
83+
export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken, defaultValue: T): Promise<T>;
84+
85+
export function raceCancellation<T>(promise: Promise<T>, token: CancellationToken, defaultValue?: T): Promise<T | undefined> {
86+
return new Promise((resolve, reject) => {
87+
const ref = token.onCancellationRequested(() => {
88+
ref.dispose();
89+
resolve(defaultValue);
90+
});
91+
promise.then(resolve, reject).finally(() => ref.dispose());
92+
});
93+
}
94+
95+
/**
96+
* Returns a promise that rejects with a {@link CancellationError} as soon as the passed token is cancelled.
97+
* @see {@link raceCancellation}
98+
*/
99+
export function raceCancellationError<T>(promise: Promise<T>, token: CancellationToken): Promise<T> {
100+
return new Promise((resolve, reject) => {
101+
const ref = token.onCancellationRequested(() => {
102+
ref.dispose();
103+
reject(new CancellationError());
104+
});
105+
promise.then(resolve, reject).finally(() => ref.dispose());
106+
});
107+
}
108+
109+
/**
110+
* Creates a timeout promise that resolves after the specified number of milliseconds.
111+
* Can be cancelled using the returned CancelablePromise's cancel() method.
112+
*/
113+
export function timeout(millis: number): CancelablePromise<void>;
114+
115+
/**
116+
* Creates a timeout promise that resolves after the specified number of milliseconds,
117+
* or rejects with CancellationError if the token is cancelled.
118+
*/
119+
export function timeout(millis: number, token: CancellationToken): Promise<void>;
120+
121+
export function timeout(millis: number, token?: CancellationToken): CancelablePromise<void> | Promise<void> {
122+
if (!token) {
123+
return createCancelablePromise((token) => timeout(millis, token));
124+
}
125+
126+
return new Promise((resolve, reject) => {
127+
const handle = setTimeout(() => {
128+
disposable.dispose();
129+
resolve();
130+
}, millis);
131+
const disposable = token.onCancellationRequested(() => {
132+
clearTimeout(handle);
133+
disposable.dispose();
134+
reject(new CancellationError());
135+
});
136+
});
3137
}

0 commit comments

Comments
 (0)