aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-03-28 20:23:47 +0200
committerFlorian Dold <florian@dold.me>2022-03-28 20:24:09 +0200
commit24b71107765172b568803fad5fb79474674b147a (patch)
treeefb49aebb32fdaeb7e9d4ab3e724b62d0ce2bb93 /packages/taler-util
parente5f21ec5bbcb74028c7e49521b19af9437489190 (diff)
vendor CancellationToken
Diffstat (limited to 'packages/taler-util')
-rw-r--r--packages/taler-util/src/CancellationToken.ts285
-rw-r--r--packages/taler-util/src/index.ts1
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";