From 24b71107765172b568803fad5fb79474674b147a Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 28 Mar 2022 20:23:47 +0200 Subject: vendor CancellationToken --- packages/taler-util/src/CancellationToken.ts | 285 +++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 packages/taler-util/src/CancellationToken.ts (limited to 'packages/taler-util/src/CancellationToken.ts') diff --git a/packages/taler-util/src/CancellationToken.ts b/packages/taler-util/src/CancellationToken.ts new file mode 100644 index 000000000..134805274 --- /dev/null +++ b/packages/taler-util/src/CancellationToken.ts @@ -0,0 +1,285 @@ +/* +MIT License + +Copyright (c) 2017 Conrad Reuter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +const NOOP = () => {}; + +/** + * A token that can be passed around to inform consumers of the token that a + * certain operation has been cancelled. + */ +class CancellationToken { + private _reason: any; + private _callbacks?: Set<(reason?: any) => void> = new Set(); + + /** + * A cancellation token that is already cancelled. + */ + public static readonly CANCELLED: CancellationToken = new CancellationToken( + true, + true, + ); + + /** + * A cancellation token that is never cancelled. + */ + public static readonly CONTINUE: CancellationToken = new CancellationToken( + false, + false, + ); + + /** + * Whether the token has been cancelled. + */ + public get isCancelled(): boolean { + return this._isCancelled; + } + + /** + * Whether the token can be cancelled. + */ + public get canBeCancelled(): boolean { + return this._canBeCancelled; + } + + /** + * Why this token has been cancelled. + */ + public get reason(): any { + if (this.isCancelled) { + return this._reason; + } else { + throw new Error("This token is not cancelled."); + } + } + + /** + * Make a promise that resolves when the async operation resolves, + * or rejects when the operation is rejected or this token is cancelled. + */ + public racePromise(asyncOperation: Promise): Promise { + if (!this.canBeCancelled) { + return asyncOperation; + } + return new Promise((resolve, reject) => { + // we could use Promise.finally here as soon as it's implemented in the major browsers + const unregister = this.onCancelled((reason) => + reject(new CancellationToken.CancellationError(reason)), + ); + asyncOperation.then( + (value) => { + resolve(value); + unregister(); + }, + (err) => { + reject(err); + unregister(); + }, + ); + }); + } + + /** + * Throw a {CancellationToken.CancellationError} if this token is cancelled. + */ + public throwIfCancelled(): void { + if (this._isCancelled) { + throw new CancellationToken.CancellationError(this._reason); + } + } + + /** + * Invoke the callback when this token is cancelled. + * If this token is already cancelled, the callback is invoked immediately. + * Returns a function that unregisters the cancellation callback. + */ + public onCancelled(cb: (reason?: any) => void): () => void { + if (!this.canBeCancelled) { + return NOOP; + } + if (this.isCancelled) { + cb(this.reason); + return NOOP; + } + + /* istanbul ignore next */ + this._callbacks?.add(cb); + return () => this._callbacks?.delete(cb); + } + + private constructor( + /** + * Whether the token is already cancelled. + */ + private _isCancelled: boolean, + /** + * Whether the token can be cancelled. + */ + private _canBeCancelled: boolean, + ) {} + + /** + * Create a {CancellationTokenSource}. + */ + public static create(): CancellationToken.Source { + const token = new CancellationToken(false, true); + + const cancel = (reason?: any) => { + if (token._isCancelled) return; + token._isCancelled = true; + token._reason = reason; + token._callbacks?.forEach((cb) => cb(reason)); + dispose(); + }; + + const dispose = () => { + token._canBeCancelled = token.isCancelled; + delete token._callbacks; // release memory + }; + + return { token, cancel, dispose }; + } + + /** + * Create a {CancellationTokenSource}. + * The token will be cancelled automatically after the specified timeout in milliseconds. + */ + public static timeout(ms: number): CancellationToken.Source { + const { + token, + cancel: originalCancel, + dispose: originalDispose, + } = CancellationToken.create(); + + let timer: NodeJS.Timeout | null; + timer = setTimeout(() => originalCancel(CancellationToken.timeout), ms); + const disposeTimer = () => { + if (timer == null) return; + clearTimeout(timer); + timer = null; + }; + + const cancel = (reason?: any) => { + disposeTimer(); + originalCancel(reason); + }; + + /* istanbul ignore next */ + const dispose = () => { + disposeTimer(); + originalDispose(); + }; + + return { token, cancel, dispose }; + } + + /** + * Create a {CancellationToken} that is cancelled when all of the given tokens are cancelled. + * + * This is like {Promise.all} for {CancellationToken}s. + */ + public static all(...tokens: CancellationToken[]): CancellationToken { + // If *any* of the tokens cannot be cancelled, then the token we return can never be. + if (tokens.some((token) => !token.canBeCancelled)) { + return CancellationToken.CONTINUE; + } + + const combined = CancellationToken.create(); + let countdown = tokens.length; + const handleNextTokenCancelled = () => { + if (--countdown === 0) { + const reasons = tokens.map((token) => token._reason); + combined.cancel(reasons); + } + }; + tokens.forEach((token) => token.onCancelled(handleNextTokenCancelled)); + return combined.token; + } + + /** + * Create a {CancellationToken} that is cancelled when at least one of the given tokens is cancelled. + * + * This is like {Promise.race} for {CancellationToken}s. + */ + public static race(...tokens: CancellationToken[]): CancellationToken { + // If *any* of the tokens is already cancelled, immediately return that token. + for (const token of tokens) { + if (token._isCancelled) { + return token; + } + } + + const combined = CancellationToken.create(); + let unregistrations: (() => void)[]; + const handleAnyTokenCancelled = (reason?: any) => { + unregistrations.forEach((unregister) => unregister()); // release memory + combined.cancel(reason); + }; + unregistrations = tokens.map((token) => + token.onCancelled(handleAnyTokenCancelled), + ); + return combined.token; + } +} + +/* istanbul ignore next */ +namespace CancellationToken { + /** + * Provides a {CancellationToken}, along with some methods to operate on it. + */ + export interface Source { + /** + * The token provided by this source. + */ + token: CancellationToken; + + /** + * Cancel the provided token with the given reason. + * Do nothing if the provided token cannot be cancelled or is already cancelled. + */ + cancel(reason?: any): void; + + /** + * Dipose of the token and this source and release memory. + */ + dispose(): void; + } + + /** + * The error that is thrown when a {CancellationToken} has been cancelled and a + * consumer of the token calls {CancellationToken.throwIfCancelled} on it. + */ + export class CancellationError extends Error { + public constructor( + /** + * The reason why the token was cancelled. + */ + public readonly reason: any, + ) { + super("Operation cancelled"); + Object.setPrototypeOf(this, CancellationError.prototype); + } + } +} + +export { CancellationToken }; -- cgit v1.2.3