From c909d6fc0657002a2e5d117e98b9685f7a04a9d4 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 16 Feb 2024 00:30:25 +0100 Subject: taler-util: cancellation and timeouts on qjs --- packages/taler-harness/src/harness/harness.ts | 2 +- packages/taler-util/src/http-impl.qtart.ts | 80 ++++++++++++++- packages/taler-util/src/index.ts | 1 + .../src/crypto/workers/crypto-dispatcher.ts | 12 ++- packages/taler-wallet-core/src/index.ts | 1 - .../taler-wallet-core/src/internal-wallet-state.ts | 1 - .../taler-wallet-core/src/operations/exchanges.ts | 2 +- .../src/operations/pay-merchant.ts | 2 +- .../taler-wallet-core/src/operations/testing.ts | 8 +- packages/taler-wallet-core/src/remote.ts | 3 +- packages/taler-wallet-core/src/shepherd.ts | 2 +- .../taler-wallet-core/src/util/promiseUtils.ts | 112 --------------------- packages/taler-wallet-core/src/util/query.ts | 3 +- packages/taler-wallet-core/src/wallet.ts | 8 +- packages/taler-wallet-embedded/src/wallet-qjs.ts | 3 +- 15 files changed, 100 insertions(+), 140 deletions(-) delete mode 100644 packages/taler-wallet-core/src/util/promiseUtils.ts (limited to 'packages') diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 975d73cf8..410462af2 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -44,6 +44,7 @@ import { encodeCrock, hash, j2s, + openPromise, parsePaytoUri, stringToBytes, } from "@gnu-taler/taler-util"; @@ -57,7 +58,6 @@ import { WalletCoreRequestType, WalletCoreResponseType, WalletOperations, - openPromise, } from "@gnu-taler/taler-wallet-core"; import { RemoteWallet, diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts index a37029d6e..0be9f2c23 100644 --- a/packages/taler-util/src/http-impl.qtart.ts +++ b/packages/taler-util/src/http-impl.qtart.ts @@ -19,9 +19,9 @@ /** * Imports. */ -import { Logger } from "@gnu-taler/taler-util"; +import { Logger, openPromise } from "@gnu-taler/taler-util"; import { TalerError } from "./errors.js"; -import { encodeBody, getDefaultHeaders, HttpLibArgs } from "./http-common.js"; +import { HttpLibArgs, encodeBody, getDefaultHeaders } from "./http-common.js"; import { Headers, HttpRequestLibrary, @@ -29,12 +29,26 @@ import { HttpResponse, } from "./http.js"; import { RequestThrottler, TalerErrorCode, URL } from "./index.js"; -import { qjsOs } from "./qtart.js"; +import { QjsHttpResp, qjsOs } from "./qtart.js"; const logger = new Logger("http-impl.qtart.ts"); const textDecoder = new TextDecoder(); +export class RequestTimeoutError extends Error { + public constructor() { + super("Request timed out"); + Object.setPrototypeOf(this, RequestTimeoutError.prototype); + } +} + +export class RequestCancelledError extends Error { + public constructor() { + super("Request cancelled"); + Object.setPrototypeOf(this, RequestCancelledError.prototype); + } +} + /** * Implementation of the HTTP request library interface for node. */ @@ -92,12 +106,70 @@ export class HttpLibImpl implements HttpRequestLibrary { if (method === "POST") { data = encodeBody(opt?.body); } - const res = await qjsOs.fetchHttp(url, { + + const cancelPromCap = openPromise(); + + // Just like WHATWG fetch(), the qjs http client doesn't + // really support cancellation, so cancellation here just + // means that the result is ignored! + const fetchProm = qjsOs.fetchHttp(url, { method, data, headers: headersList, }); + let timeoutHandle: any = undefined; + let cancelCancelledHandler: (() => void) | undefined = undefined; + + if (opt?.timeout && opt.timeout.d_ms !== "forever") { + timeoutHandle = setTimeout(() => { + cancelPromCap.reject(new RequestTimeoutError()); + }, opt.timeout.d_ms); + } + + if (opt?.cancellationToken) { + cancelCancelledHandler = opt.cancellationToken.onCancelled(() => { + cancelPromCap.reject(new RequestCancelledError()); + }); + } + + let res: QjsHttpResp; + try { + res = await Promise.race([fetchProm, cancelPromCap.promise]); + } catch (e) { + if (e instanceof RequestCancelledError) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Request cancelled`, + ); + } + if (e instanceof RequestTimeoutError) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Request timed out`, + ); + } + throw e; + } + + if (timeoutHandle != null) { + clearTimeout(timeoutHandle); + } + + if (cancelCancelledHandler != null) { + cancelCancelledHandler(); + } + const headers: Headers = new Headers(); if (res.headers) { diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts index 2045a4717..edc9c4ff2 100644 --- a/packages/taler-util/src/index.ts +++ b/packages/taler-util/src/index.ts @@ -44,6 +44,7 @@ export { export * from "./notifications.js"; export * from "./operation.js"; export * from "./payto.js"; +export * from "./promises.js"; export * from "./rfc3548.js"; export * from "./taler-crypto.js"; export * from "./taler-types.js"; diff --git a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts index 192e9cda1..83897f331 100644 --- a/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts +++ b/packages/taler-wallet-core/src/crypto/workers/crypto-dispatcher.ts @@ -23,10 +23,14 @@ /** * Imports. */ -import { j2s, Logger, TalerErrorCode } from "@gnu-taler/taler-util"; -import { TalerError } from "@gnu-taler/taler-util"; -import { openPromise } from "../../util/promiseUtils.js"; -import { timer, performanceNow, TimerHandle } from "../../util/timer.js"; +import { + j2s, + Logger, + openPromise, + TalerError, + TalerErrorCode, +} from "@gnu-taler/taler-util"; +import { performanceNow, timer, TimerHandle } from "../../util/timer.js"; import { nullCrypto, TalerCryptoInterface } from "../cryptoImplementation.js"; import { CryptoWorker } from "./cryptoWorkerInterface.js"; diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts index 643d65620..0eca64d1f 100644 --- a/packages/taler-wallet-core/src/index.ts +++ b/packages/taler-wallet-core/src/index.ts @@ -19,7 +19,6 @@ */ // Util functionality -export * from "./util/promiseUtils.js"; export * from "./util/query.js"; export * from "./versions.js"; diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 4379f20b5..13578adda 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -39,7 +39,6 @@ import { HttpRequestLibrary } from "@gnu-taler/taler-util/http"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { WalletStoresV1 } from "./db.js"; import { TaskScheduler } from "./shepherd.js"; -import { AsyncCondition } from "./util/promiseUtils.js"; import { DbAccess, GetReadOnlyAccess, diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index 3f8126dba..460b47e73 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -27,6 +27,7 @@ import { AbsoluteTime, AgeRestriction, Amounts, + AsyncFlag, CancellationToken, CoinRefreshRequest, CoinStatus, @@ -95,7 +96,6 @@ import { WalletStoresV1, } from "../db.js"; import { - AsyncFlag, ExchangeEntryDbRecordStatus, ExchangeEntryDbUpdateStatus, PendingTaskType, diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index e00432bd0..fc34feb30 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -31,6 +31,7 @@ import { AmountJson, Amounts, AmountString, + AsyncFlag, codecForAbortResponse, codecForMerchantContractTerms, codecForMerchantOrderRefundPickupResponse, @@ -103,7 +104,6 @@ import { WalletStoresV1, } from "../db.js"; import { - AsyncFlag, getCandidateWithdrawalDenomsTx, PendingTaskType, RefundGroupRecord, diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts index 5902e8362..17863450c 100644 --- a/packages/taler-wallet-core/src/operations/testing.ts +++ b/packages/taler-wallet-core/src/operations/testing.ts @@ -38,6 +38,8 @@ import { j2s, Logger, NotificationType, + OpenedPromise, + openPromise, parsePaytoUri, PreparePayResultType, TalerCorebankApiClient, @@ -54,11 +56,7 @@ import { HttpRequestLibrary, readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; -import { - getRefreshesForTransaction, - OpenedPromise, - openPromise, -} from "../index.js"; +import { getRefreshesForTransaction } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { checkLogicInvariant } from "../util/invariants.js"; import { getBalances } from "./balance.js"; diff --git a/packages/taler-wallet-core/src/remote.ts b/packages/taler-wallet-core/src/remote.ts index 164f7cfe9..1ee0e1993 100644 --- a/packages/taler-wallet-core/src/remote.ts +++ b/packages/taler-wallet-core/src/remote.ts @@ -19,11 +19,12 @@ import { CoreApiResponse, j2s, Logger, + OpenedPromise, + openPromise, TalerError, WalletNotification, } from "@gnu-taler/taler-util"; import { connectRpc, JsonMessage } from "@gnu-taler/taler-util/twrpc"; -import { OpenedPromise, openPromise } from "./index.js"; import { WalletCoreApiClient } from "./wallet-api-types.js"; const logger = new Logger("remote.ts"); diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts index d1648acc7..4aea2d15d 100644 --- a/packages/taler-wallet-core/src/shepherd.ts +++ b/packages/taler-wallet-core/src/shepherd.ts @@ -20,6 +20,7 @@ import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { AbsoluteTime, + AsyncCondition, CancellationToken, Duration, Logger, @@ -66,7 +67,6 @@ import { processRefreshGroup } from "./operations/refresh.js"; import { constructTransactionIdentifier } from "./operations/transactions.js"; import { processWithdrawalGroup } from "./operations/withdraw.js"; import { PendingTaskType, TaskId } from "./pending-types.js"; -import { AsyncCondition } from "./util/promiseUtils.js"; const logger = new Logger("shepherd.ts"); diff --git a/packages/taler-wallet-core/src/util/promiseUtils.ts b/packages/taler-wallet-core/src/util/promiseUtils.ts deleted file mode 100644 index bc1e40260..000000000 --- a/packages/taler-wallet-core/src/util/promiseUtils.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - -/** - * An opened promise. - * - * @see {@link openPromise} - */ -export interface OpenedPromise { - promise: Promise; - resolve: (val: T) => void; - reject: (err: any) => void; - lastError?: any; -} - -/** - * Get an unresolved promise together with its extracted resolve / reject - * function. - * - * Recent ECMAScript proposals also call this a promise capability. - */ -export function openPromise(): OpenedPromise { - let resolve: ((x?: any) => void) | null = null; - let promiseReject: ((reason?: any) => void) | null = null; - const promise = new Promise((res, rej) => { - resolve = res; - promiseReject = rej; - }); - if (!(resolve && promiseReject)) { - // Never happens, unless JS implementation is broken - throw Error("JS implementation is broken"); - } - const result: OpenedPromise = { resolve, reject: promiseReject, promise }; - function saveLastError(reason?: any) { - result.lastError = reason; - promiseReject!(reason); - } - result.reject = saveLastError; - return result; -} - -export class AsyncCondition { - private promCap?: OpenedPromise = undefined; - constructor() {} - - wait(): Promise { - if (!this.promCap) { - this.promCap = openPromise(); - } - return this.promCap.promise; - } - - trigger(): void { - if (this.promCap) { - this.promCap.resolve(); - } - this.promCap = undefined; - } -} - -/** - * Flag that can be raised to notify asynchronous waiters. - * - * You can think of it as a promise that can - * be un-resolved. - */ -export class AsyncFlag { - private promCap?: OpenedPromise = undefined; - private internalFlagRaised: boolean = false; - - constructor() {} - - /** - * Wait until the flag is raised. - * - * Reset if before returning. - */ - wait(): Promise { - if (this.internalFlagRaised) { - return Promise.resolve(); - } - if (!this.promCap) { - this.promCap = openPromise(); - } - return this.promCap.promise; - } - - raise(): void { - this.internalFlagRaised = true; - if (this.promCap) { - this.promCap.resolve(); - } - } - - reset(): void { - this.internalFlagRaised = false; - this.promCap = undefined; - } -} diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 5fba61f11..19fa0dbfd 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -34,8 +34,7 @@ import { IDBValidKey, IDBVersionChangeEvent, } from "@gnu-taler/idb-bridge"; -import { Codec, Logger } from "@gnu-taler/taler-util"; -import { openPromise } from "./promiseUtils.js"; +import { Codec, Logger, openPromise } from "@gnu-taler/taler-util"; const logger = new Logger("query.ts"); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 0246597be..cfe171bd0 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -26,6 +26,7 @@ import { IDBFactory } from "@gnu-taler/idb-bridge"; import { AmountString, Amounts, + AsyncCondition, CoinDumpJson, CoinStatus, CoreApiResponse, @@ -40,6 +41,7 @@ import { ListGlobalCurrencyAuditorsResponse, ListGlobalCurrencyExchangesResponse, Logger, + OpenedPromise, PrepareWithdrawExchangeRequest, PrepareWithdrawExchangeResponse, RecoverStoredBackupRequest, @@ -122,6 +124,7 @@ import { codecForWithdrawTestBalance, getErrorDetailFromException, j2s, + openPromise, parsePaytoUri, parseTalerUri, sampleWalletCoreTransactions, @@ -265,11 +268,6 @@ import { getMaxPeerPushAmount, } from "./util/instructedAmountConversion.js"; import { checkDbInvariant } from "./util/invariants.js"; -import { - AsyncCondition, - OpenedPromise, - openPromise, -} from "./util/promiseUtils.js"; import { DbAccess, GetReadOnlyAccess, diff --git a/packages/taler-wallet-embedded/src/wallet-qjs.ts b/packages/taler-wallet-embedded/src/wallet-qjs.ts index 0296dfeb6..0fbcd7583 100644 --- a/packages/taler-wallet-embedded/src/wallet-qjs.ts +++ b/packages/taler-wallet-embedded/src/wallet-qjs.ts @@ -39,6 +39,7 @@ import { enableNativeLogging, getErrorDetailFromException, j2s, + openPromise, setGlobalLogLevelFromString, } from "@gnu-taler/taler-util"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; @@ -48,7 +49,6 @@ import { Wallet, WalletApiOperation, createNativeWalletHost2, - openPromise, performanceNow, } from "@gnu-taler/taler-wallet-core"; @@ -277,6 +277,7 @@ export async function testWithGv() { corebankApiBaseUrl: "https://bank.demo.taler.net/", exchangeBaseUrl: "https://exchange.demo.taler.net/", merchantBaseUrl: "https://backend.demo.taler.net/", + merchantAuthToken: "secret-token:sandbox", }); await w.wallet.runTaskLoop({ stopWhenDone: true, -- cgit v1.2.3