Skip to content

Instantly share code, notes, and snippets.

@oldrev
Last active December 18, 2020 12:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oldrev/a2039ada2cf5a153317980a649aa574e to your computer and use it in GitHub Desktop.
Save oldrev/a2039ada2cf5a153317980a649aa574e to your computer and use it in GitHub Desktop.
A cancellable Promise with timeout for JS/TS
// A cancellable Promise with timeout for JS/TS
class OperationCancelledError extends Error {
constructor(reason: string = '') {
super(reason);
Object.setPrototypeOf(this, OperationCancelledError.prototype);
}
}
const CANCEL = Symbol();
type OnCancelledCallback = () => void;
class CancellationToken {
private cancelled: boolean = false;
private readonly cancelledCallbacks = new Set<OnCancelledCallback>();
throwIfCancelled(): void {
if (this.isCancelled) {
throw new OperationCancelledError('Promise cancelled');
}
}
get isCancelled(): boolean {
return this.cancelled === true;
}
[CANCEL](): void {
this.cancelled = true;
this.cancelledCallbacks.forEach((cb) => cb());
this.cancelledCallbacks.clear();
}
register(callback: OnCancelledCallback): void {
if (!this.cancelledCallbacks.has(callback)) {
this.cancelledCallbacks.add(callback);
}
}
}
class CancellationTokenSource {
token = new CancellationToken();
constructor() {}
cancel(): void {
this.token[CANCEL]();
}
}
function delay(periodInMS: number, cancellationToken: CancellationToken): Promise<void> {
return new Promise<void>((resolve) => {
let timerCleared = false;
const timer = setTimeout(() => {
if (!timerCleared) {
clearTimeout(timer);
timerCleared = true;
}
resolve();
}, periodInMS);
cancellationToken.register(() => {
if (!timerCleared) {
clearTimeout(timer);
timerCleared = true;
}
});
});
}
function whenAny(promises: Promise<void>[]) {
const reverse = (p: Promise<void>) =>
new Promise<void>((resolve, reject) => Promise.resolve(p).then(reject, resolve))
const reverseMany = (p: Promise<void[]>) =>
new Promise<void[]>((resolve, reject) => Promise.resolve(p).then(reject, resolve))
return reverseMany(Promise.all([...promises].map(reverse)))
}
async function a(cancelToken: CancellationToken) {
for(let i = 0; i < 100; i++) {
await delay(200, cancelToken)
console.log("Alice ", i)
}
}
async function b(cancelToken: CancellationToken) {
for(let i = 0; i < 100; i++) {
await delay(500, cancelToken)
console.log("Bob ", i)
}
}
async function doWork() {
let cts = new CancellationTokenSource()
let tasks = [a(cts.token), b(cts.token), delay(3000, cts.token)] // 3000ms is the timeout
await whenAny(tasks) // delay(3000) will be the winner of the race
cts.cancel()
console.log("finished")
}
const main = async () => {
await doWork()
console.log("all done")
}
main().catch(console.error)
// How to run:
// npm install -g ts-node
// ts-node callable-promise-demo.ts
// Or you can test this script in your browser.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment