diff options
author | Florian Dold <florian@dold.me> | 2022-03-28 20:23:47 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-03-28 20:24:09 +0200 |
commit | 24b71107765172b568803fad5fb79474674b147a (patch) | |
tree | efb49aebb32fdaeb7e9d4ab3e724b62d0ce2bb93 /packages/taler-util | |
parent | e5f21ec5bbcb74028c7e49521b19af9437489190 (diff) |
vendor CancellationToken
Diffstat (limited to 'packages/taler-util')
-rw-r--r-- | packages/taler-util/src/CancellationToken.ts | 285 | ||||
-rw-r--r-- | packages/taler-util/src/index.ts | 1 |
2 files changed, 286 insertions, 0 deletions
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<T>(asyncOperation: Promise<T>): Promise<T> { + if (!this.canBeCancelled) { + return asyncOperation; + } + return new Promise<T>((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<T>.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<T>.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 }; diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index 573b4a5c7..199218d69 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -31,3 +31,4 @@ export { crypto_sign_keyPair_fromSeed, } from "./nacl-fast.js"; export { RequestThrottler } from "./RequestThrottler.js"; +export * from "./CancellationToken.js"; |