aboutsummaryrefslogtreecommitdiff
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
parente5f21ec5bbcb74028c7e49521b19af9437489190 (diff)
vendor CancellationToken
-rw-r--r--packages/taler-util/src/CancellationToken.ts285
-rw-r--r--packages/taler-util/src/index.ts1
-rw-r--r--packages/taler-wallet-cli/package.json1
-rw-r--r--packages/taler-wallet-cli/src/integrationtests/testrunner.ts85
-rw-r--r--packages/taler-wallet-core/src/util/http.ts12
5 files changed, 340 insertions, 44 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";
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
index 96f699391..e43090560 100644
--- a/packages/taler-wallet-cli/package.json
+++ b/packages/taler-wallet-cli/package.json
@@ -47,7 +47,6 @@
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/taler-wallet-core": "workspace:*",
"axios": "^0.25.0",
- "cancellationtoken": "^2.2.0",
"source-map-support": "^0.5.21",
"tslib": "^2.3.1"
}
diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
index 3839266c0..d8dc569d2 100644
--- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
+++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts
@@ -14,80 +14,79 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { minimatch } from "@gnu-taler/taler-util";
+import { CancellationToken, minimatch } from "@gnu-taler/taler-util";
+import * as child_process from "child_process";
+import * as fs from "fs";
+import * as os from "os";
+import * as path from "path";
import {
GlobalTestState,
runTestWithState,
shouldLingerInTest,
TestRunResult,
} from "../harness/harness.js";
-import { runPaymentTest } from "./test-payment";
-import { runPaymentDemoTest } from "./test-payment-on-demo";
-import * as fs from "fs";
-import * as path from "path";
-import * as os from "os";
-import * as child_process from "child_process";
import { runBankApiTest } from "./test-bank-api";
import { runClaimLoopTest } from "./test-claim-loop";
+import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
+import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
+import { runDepositTest } from "./test-deposit";
import { runExchangeManagementTest } from "./test-exchange-management";
+import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
import { runFeeRegressionTest } from "./test-fee-regression";
+import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount";
+import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection";
+import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade";
+import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request";
+import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions";
+import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt";
+import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions";
+import { runLibeufinApiSchedulingTest } from "./test-libeufin-api-scheduling";
+import { runLibeufinApiUsersTest } from "./test-libeufin-api-users";
+import { runLibeufinBadGatewayTest } from "./test-libeufin-bad-gateway";
+import { runLibeufinBasicTest } from "./test-libeufin-basic";
+import { runLibeufinC5xTest } from "./test-libeufin-c5x";
+import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis";
+import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation";
+import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance";
+import { runLibeufinRefundTest } from "./test-libeufin-refund";
+import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users";
+import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli";
+import { runLibeufinTutorialTest } from "./test-libeufin-tutorial";
+import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion";
+import { runMerchantInstancesTest } from "./test-merchant-instances";
+import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete";
+import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls";
import { runMerchantLongpollingTest } from "./test-merchant-longpolling";
import { runMerchantRefundApiTest } from "./test-merchant-refund-api";
+import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js";
import { runPayAbortTest } from "./test-pay-abort";
import { runPayPaidTest } from "./test-pay-paid";
+import { runPaymentTest } from "./test-payment";
import { runPaymentClaimTest } from "./test-payment-claim";
import { runPaymentFaultTest } from "./test-payment-fault";
+import { runPaymentForgettableTest } from "./test-payment-forgettable.js";
import { runPaymentIdempotencyTest } from "./test-payment-idempotency";
import { runPaymentMultipleTest } from "./test-payment-multiple";
+import { runPaymentDemoTest } from "./test-payment-on-demo";
import { runPaymentTransientTest } from "./test-payment-transient";
+import { runPaymentZeroTest } from "./test-payment-zero.js";
import { runPaywallFlowTest } from "./test-paywall-flow";
+import { runRefundTest } from "./test-refund";
import { runRefundAutoTest } from "./test-refund-auto";
import { runRefundGoneTest } from "./test-refund-gone";
import { runRefundIncrementalTest } from "./test-refund-incremental";
-import { runRefundTest } from "./test-refund";
import { runRevocationTest } from "./test-revocation";
import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh";
import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw";
import { runTippingTest } from "./test-tipping";
+import { runWalletBackupBasicTest } from "./test-wallet-backup-basic";
+import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend";
+import { runWalletDblessTest } from "./test-wallet-dbless.js";
import { runWallettestingTest } from "./test-wallettesting";
-import { runTestWithdrawalManualTest } from "./test-withdrawal-manual";
import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated";
-import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion";
-import { runLibeufinBasicTest } from "./test-libeufin-basic";
-import { runLibeufinC5xTest } from "./test-libeufin-c5x";
-import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance";
-import { runLibeufinBadGatewayTest } from "./test-libeufin-bad-gateway";
-import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation";
-import { runLibeufinRefundTest } from "./test-libeufin-refund";
-import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users";
-import { runLibeufinTutorialTest } from "./test-libeufin-tutorial";
-import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions";
-import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade";
-import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request";
-import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis";
-import { runLibeufinApiSchedulingTest } from "./test-libeufin-api-scheduling";
-import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection";
-import { runLibeufinApiUsersTest } from "./test-libeufin-api-users";
-import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount";
-import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions";
-import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt";
-import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli";
-import { runDepositTest } from "./test-deposit";
-import CancellationToken from "cancellationtoken";
-import { runMerchantInstancesTest } from "./test-merchant-instances";
-import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls";
-import { runWalletBackupBasicTest } from "./test-wallet-backup-basic";
-import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete";
-import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend";
-import { runPaymentForgettableTest } from "./test-payment-forgettable.js";
-import { runPaymentZeroTest } from "./test-payment-zero.js";
-import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js";
-import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
-import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
-import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
-import { runWalletDblessTest } from "./test-wallet-dbless.js";
+import { runTestWithdrawalManualTest } from "./test-withdrawal-manual";
/**
* Test runner.
diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts
index 31e38b609..9ccd560d9 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -31,6 +31,7 @@ import {
TalerErrorDetail,
Codec,
j2s,
+ CancellationToken,
} from "@gnu-taler/taler-util";
import { TalerErrorCode } from "@gnu-taler/taler-util";
import { makeErrorDetail, TalerError } from "../errors.js";
@@ -53,7 +54,18 @@ export interface HttpResponse {
export interface HttpRequestOptions {
method?: "POST" | "PUT" | "GET";
headers?: { [name: string]: string };
+
+ /**
+ * Timeout after which the request should be aborted.
+ */
timeout?: Duration;
+
+ /**
+ * Cancellation token that should abort the request when
+ * cancelled.
+ */
+ cancellationToken?: CancellationToken;
+
body?: string | ArrayBuffer | ArrayBufferView;
}