Last active
December 18, 2020 12:10
-
-
Save oldrev/a2039ada2cf5a153317980a649aa574e to your computer and use it in GitHub Desktop.
A cancellable Promise with timeout for JS/TS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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