diff options
Diffstat (limited to 'packages/taler-wallet-core/src/util')
-rw-r--r-- | packages/taler-wallet-core/src/util/RequestThrottler.ts | 156 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/assertUnreachable.ts | 19 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/asyncMemo.ts | 87 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/coinSelection.test.ts | 254 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/coinSelection.ts | 332 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/contractTerms.test.ts | 122 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/contractTerms.ts | 231 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/debugFlags.ts | 32 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/http.ts | 342 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/invariants.ts | 39 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/promiseUtils.ts | 60 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/query.ts | 615 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/retries.ts | 85 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/timer.ts | 199 |
14 files changed, 0 insertions, 2573 deletions
diff --git a/packages/taler-wallet-core/src/util/RequestThrottler.ts b/packages/taler-wallet-core/src/util/RequestThrottler.ts deleted file mode 100644 index d79afe47a..000000000 --- a/packages/taler-wallet-core/src/util/RequestThrottler.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Implementation of token bucket throttling. - */ - -/** - * Imports. - */ -import { - getTimestampNow, - timestampDifference, - timestampCmp, - Logger, - URL, -} from "@gnu-taler/taler-util"; - -const logger = new Logger("RequestThrottler.ts"); - -/** - * Maximum request per second, per origin. - */ -const MAX_PER_SECOND = 100; - -/** - * Maximum request per minute, per origin. - */ -const MAX_PER_MINUTE = 500; - -/** - * Maximum request per hour, per origin. - */ -const MAX_PER_HOUR = 2000; - -/** - * Throttling state for one origin. - */ -class OriginState { - tokensSecond: number = MAX_PER_SECOND; - tokensMinute: number = MAX_PER_MINUTE; - tokensHour: number = MAX_PER_HOUR; - private lastUpdate = getTimestampNow(); - - private refill(): void { - const now = getTimestampNow(); - if (timestampCmp(now, this.lastUpdate) < 0) { - // Did the system time change? - this.lastUpdate = now; - return; - } - const d = timestampDifference(now, this.lastUpdate); - if (d.d_ms === "forever") { - throw Error("assertion failed"); - } - this.tokensSecond = Math.min( - MAX_PER_SECOND, - this.tokensSecond + d.d_ms / 1000, - ); - this.tokensMinute = Math.min( - MAX_PER_MINUTE, - this.tokensMinute + d.d_ms / 1000 / 60, - ); - this.tokensHour = Math.min( - MAX_PER_HOUR, - this.tokensHour + d.d_ms / 1000 / 60 / 60, - ); - this.lastUpdate = now; - } - - /** - * Return true if the request for this origin should be throttled. - * Otherwise, take a token out of the respective buckets. - */ - applyThrottle(): boolean { - this.refill(); - if (this.tokensSecond < 1) { - logger.warn("request throttled (per second limit exceeded)"); - return true; - } - if (this.tokensMinute < 1) { - logger.warn("request throttled (per minute limit exceeded)"); - return true; - } - if (this.tokensHour < 1) { - logger.warn("request throttled (per hour limit exceeded)"); - return true; - } - this.tokensSecond--; - this.tokensMinute--; - this.tokensHour--; - return false; - } -} - -/** - * Request throttler, used as a "last layer of defense" when some - * other part of the re-try logic is broken and we're sending too - * many requests to the same exchange/bank/merchant. - */ -export class RequestThrottler { - private perOriginInfo: { [origin: string]: OriginState } = {}; - - /** - * Get the throttling state for an origin, or - * initialize if no state is associated with the - * origin yet. - */ - private getState(origin: string): OriginState { - const s = this.perOriginInfo[origin]; - if (s) { - return s; - } - const ns = (this.perOriginInfo[origin] = new OriginState()); - return ns; - } - - /** - * Apply throttling to a request. - * - * @returns whether the request should be throttled. - */ - applyThrottle(requestUrl: string): boolean { - const origin = new URL(requestUrl).origin; - return this.getState(origin).applyThrottle(); - } - - /** - * Get the throttle statistics for a particular URL. - */ - getThrottleStats(requestUrl: string): Record<string, unknown> { - const origin = new URL(requestUrl).origin; - const state = this.getState(origin); - return { - tokensHour: state.tokensHour, - tokensMinute: state.tokensMinute, - tokensSecond: state.tokensSecond, - maxTokensHour: MAX_PER_HOUR, - maxTokensMinute: MAX_PER_MINUTE, - maxTokensSecond: MAX_PER_SECOND, - }; - } -} diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.ts b/packages/taler-wallet-core/src/util/assertUnreachable.ts deleted file mode 100644 index ffdf88f04..000000000 --- a/packages/taler-wallet-core/src/util/assertUnreachable.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -export function assertUnreachable(x: never): never { - throw new Error("Didn't expect to get here"); -} diff --git a/packages/taler-wallet-core/src/util/asyncMemo.ts b/packages/taler-wallet-core/src/util/asyncMemo.ts deleted file mode 100644 index 6e88081b6..000000000 --- a/packages/taler-wallet-core/src/util/asyncMemo.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -interface MemoEntry<T> { - p: Promise<T>; - t: number; - n: number; -} - -export class AsyncOpMemoMap<T> { - private n = 0; - private memoMap: { [k: string]: MemoEntry<T> } = {}; - - private cleanUp(key: string, n: number): void { - const r = this.memoMap[key]; - if (r && r.n === n) { - delete this.memoMap[key]; - } - } - - memo(key: string, pg: () => Promise<T>): Promise<T> { - const res = this.memoMap[key]; - if (res) { - return res.p; - } - const n = this.n++; - // Wrap the operation in case it immediately throws - const p = Promise.resolve().then(() => pg()); - this.memoMap[key] = { - p, - n, - t: new Date().getTime(), - }; - return p.finally(() => { - this.cleanUp(key, n); - }); - } - clear(): void { - this.memoMap = {}; - } -} - -export class AsyncOpMemoSingle<T> { - private n = 0; - private memoEntry: MemoEntry<T> | undefined; - - private cleanUp(n: number): void { - if (this.memoEntry && this.memoEntry.n === n) { - this.memoEntry = undefined; - } - } - - memo(pg: () => Promise<T>): Promise<T> { - const res = this.memoEntry; - if (res) { - return res.p; - } - const n = this.n++; - // Wrap the operation in case it immediately throws - const p = Promise.resolve().then(() => pg()); - p.finally(() => { - this.cleanUp(n); - }); - this.memoEntry = { - p, - n, - t: new Date().getTime(), - }; - return p; - } - clear(): void { - this.memoEntry = undefined; - } -} diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts deleted file mode 100644 index ed48b8dd1..000000000 --- a/packages/taler-wallet-core/src/util/coinSelection.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import test from "ava"; -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { AvailableCoinInfo, selectPayCoins } from "./coinSelection.js"; - -function a(x: string): AmountJson { - const amt = Amounts.parse(x); - if (!amt) { - throw Error("invalid amount"); - } - return amt; -} - -function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo { - return { - availableAmount: a(current), - coinPub: "foobar", - denomPub: "foobar", - feeDeposit: a(feeDeposit), - exchangeBaseUrl: "https://example.com/", - }; -} - -test("coin selection 1", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.1"), - fakeAci("EUR:1.0", "EUR:0.0"), - ]; - - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:2.0"), - depositFeeLimit: a("EUR:0.1"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - - if (!res) { - t.fail(); - return; - } - t.true(res.coinPubs.length === 2); - t.pass(); -}); - -test("coin selection 2", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.5"), - fakeAci("EUR:1.0", "EUR:0.0"), - // Merchant covers the fee, this one shouldn't be used - fakeAci("EUR:1.0", "EUR:0.0"), - ]; - - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:2.0"), - depositFeeLimit: a("EUR:0.5"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - - if (!res) { - t.fail(); - return; - } - t.true(res.coinPubs.length === 2); - t.pass(); -}); - -test("coin selection 3", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.5"), - fakeAci("EUR:1.0", "EUR:0.5"), - // this coin should be selected instead of previous one with fee - fakeAci("EUR:1.0", "EUR:0.0"), - ]; - - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:2.0"), - depositFeeLimit: a("EUR:0.5"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - - if (!res) { - t.fail(); - return; - } - t.true(res.coinPubs.length === 2); - t.pass(); -}); - -test("coin selection 4", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.5"), - fakeAci("EUR:1.0", "EUR:0.5"), - fakeAci("EUR:1.0", "EUR:0.5"), - ]; - - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:2.0"), - depositFeeLimit: a("EUR:0.5"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - - if (!res) { - t.fail(); - return; - } - t.true(res.coinPubs.length === 3); - t.pass(); -}); - -test("coin selection 5", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.5"), - fakeAci("EUR:1.0", "EUR:0.5"), - fakeAci("EUR:1.0", "EUR:0.5"), - ]; - - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:4.0"), - depositFeeLimit: a("EUR:0.2"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - - t.true(!res); - t.pass(); -}); - -test("coin selection 6", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.5"), - fakeAci("EUR:1.0", "EUR:0.5"), - ]; - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:2.0"), - depositFeeLimit: a("EUR:0.2"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - t.true(!res); - t.pass(); -}); - -test("coin selection 7", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.1"), - fakeAci("EUR:1.0", "EUR:0.1"), - ]; - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:2.0"), - depositFeeLimit: a("EUR:0.2"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - t.truthy(res); - t.true(Amounts.cmp(res!.customerDepositFees, "EUR:0.0") === 0); - t.true( - Amounts.cmp(Amounts.sum(res!.coinContributions).amount, "EUR:2.0") === 0, - ); - t.pass(); -}); - -test("coin selection 8", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.2"), - fakeAci("EUR:0.1", "EUR:0.2"), - fakeAci("EUR:0.05", "EUR:0.05"), - fakeAci("EUR:0.05", "EUR:0.05"), - ]; - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:1.1"), - depositFeeLimit: a("EUR:0.4"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - t.truthy(res); - t.true(res!.coinContributions.length === 3); - t.pass(); -}); - -test("coin selection 9", (t) => { - const acis: AvailableCoinInfo[] = [ - fakeAci("EUR:1.0", "EUR:0.2"), - fakeAci("EUR:0.2", "EUR:0.2"), - ]; - const res = selectPayCoins({ - candidates: { - candidateCoins: acis, - wireFeesPerExchange: {}, - }, - contractTermsAmount: a("EUR:1.2"), - depositFeeLimit: a("EUR:0.4"), - wireFeeLimit: a("EUR:0"), - wireFeeAmortization: 1, - }); - t.truthy(res); - t.true(res!.coinContributions.length === 2); - t.true( - Amounts.cmp(Amounts.sum(res!.coinContributions).amount, "EUR:1.2") === 0, - ); - t.pass(); -}); diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts deleted file mode 100644 index 500cee5d8..000000000 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Selection of coins for payments. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { AmountJson, Amounts } from "@gnu-taler/taler-util"; -import { strcmp, Logger } from "@gnu-taler/taler-util"; - -const logger = new Logger("coinSelection.ts"); - -/** - * Result of selecting coins, contains the exchange, and selected - * coins with their denomination. - */ -export interface PayCoinSelection { - /** - * Amount requested by the merchant. - */ - paymentAmount: AmountJson; - - /** - * Public keys of the coins that were selected. - */ - coinPubs: string[]; - - /** - * Amount that each coin contributes. - */ - coinContributions: AmountJson[]; - - /** - * How much of the wire fees is the customer paying? - */ - customerWireFees: AmountJson; - - /** - * How much of the deposit fees is the customer paying? - */ - customerDepositFees: AmountJson; -} - -/** - * Structure to describe a coin that is available to be - * used in a payment. - */ -export interface AvailableCoinInfo { - /** - * Public key of the coin. - */ - coinPub: string; - - /** - * Coin's denomination public key. - */ - denomPub: string; - - /** - * Amount still remaining (typically the full amount, - * as coins are always refreshed after use.) - */ - availableAmount: AmountJson; - - /** - * Deposit fee for the coin. - */ - feeDeposit: AmountJson; - - exchangeBaseUrl: string; -} - -export type PreviousPayCoins = { - coinPub: string; - contribution: AmountJson; - feeDeposit: AmountJson; - exchangeBaseUrl: string; -}[]; - -export interface CoinCandidateSelection { - candidateCoins: AvailableCoinInfo[]; - wireFeesPerExchange: Record<string, AmountJson>; -} - -export interface SelectPayCoinRequest { - candidates: CoinCandidateSelection; - contractTermsAmount: AmountJson; - depositFeeLimit: AmountJson; - wireFeeLimit: AmountJson; - wireFeeAmortization: number; - prevPayCoins?: PreviousPayCoins; -} - -interface CoinSelectionTally { - /** - * Amount that still needs to be paid. - * May increase during the computation when fees need to be covered. - */ - amountPayRemaining: AmountJson; - - /** - * Allowance given by the merchant towards wire fees - */ - amountWireFeeLimitRemaining: AmountJson; - - /** - * Allowance given by the merchant towards deposit fees - * (and wire fees after wire fee limit is exhausted) - */ - amountDepositFeeLimitRemaining: AmountJson; - - customerDepositFees: AmountJson; - - customerWireFees: AmountJson; - - wireFeeCoveredForExchange: Set<string>; -} - -/** - * Account for the fees of spending a coin. - */ -function tallyFees( - tally: CoinSelectionTally, - wireFeesPerExchange: Record<string, AmountJson>, - wireFeeAmortization: number, - exchangeBaseUrl: string, - feeDeposit: AmountJson, -): CoinSelectionTally { - const currency = tally.amountPayRemaining.currency; - let amountWireFeeLimitRemaining = tally.amountWireFeeLimitRemaining; - let amountDepositFeeLimitRemaining = tally.amountDepositFeeLimitRemaining; - let customerDepositFees = tally.customerDepositFees; - let customerWireFees = tally.customerWireFees; - let amountPayRemaining = tally.amountPayRemaining; - const wireFeeCoveredForExchange = new Set(tally.wireFeeCoveredForExchange); - - if (!tally.wireFeeCoveredForExchange.has(exchangeBaseUrl)) { - const wf = - wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.getZero(currency); - const wfForgiven = Amounts.min(amountWireFeeLimitRemaining, wf); - amountWireFeeLimitRemaining = Amounts.sub( - amountWireFeeLimitRemaining, - wfForgiven, - ).amount; - // The remaining, amortized amount needs to be paid by the - // wallet or covered by the deposit fee allowance. - let wfRemaining = Amounts.divide( - Amounts.sub(wf, wfForgiven).amount, - wireFeeAmortization, - ); - - // This is the amount forgiven via the deposit fee allowance. - const wfDepositForgiven = Amounts.min( - amountDepositFeeLimitRemaining, - wfRemaining, - ); - amountDepositFeeLimitRemaining = Amounts.sub( - amountDepositFeeLimitRemaining, - wfDepositForgiven, - ).amount; - - wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount; - customerWireFees = Amounts.add(customerWireFees, wfRemaining).amount; - amountPayRemaining = Amounts.add(amountPayRemaining, wfRemaining).amount; - - wireFeeCoveredForExchange.add(exchangeBaseUrl); - } - - const dfForgiven = Amounts.min(feeDeposit, amountDepositFeeLimitRemaining); - - amountDepositFeeLimitRemaining = Amounts.sub( - amountDepositFeeLimitRemaining, - dfForgiven, - ).amount; - - // How much does the user spend on deposit fees for this coin? - const dfRemaining = Amounts.sub(feeDeposit, dfForgiven).amount; - customerDepositFees = Amounts.add(customerDepositFees, dfRemaining).amount; - amountPayRemaining = Amounts.add(amountPayRemaining, dfRemaining).amount; - - return { - amountDepositFeeLimitRemaining, - amountPayRemaining, - amountWireFeeLimitRemaining, - customerDepositFees, - customerWireFees, - wireFeeCoveredForExchange, - }; -} - -/** - * Given a list of candidate coins, select coins to spend under the merchant's - * constraints. - * - * The prevPayCoins can be specified to "repair" a coin selection - * by adding additional coins, after a broken (e.g. double-spent) coin - * has been removed from the selection. - * - * This function is only exported for the sake of unit tests. - */ -export function selectPayCoins( - req: SelectPayCoinRequest, -): PayCoinSelection | undefined { - const { - candidates, - contractTermsAmount, - depositFeeLimit, - wireFeeLimit, - wireFeeAmortization, - } = req; - - if (candidates.candidateCoins.length === 0) { - return undefined; - } - const coinPubs: string[] = []; - const coinContributions: AmountJson[] = []; - const currency = contractTermsAmount.currency; - - let tally: CoinSelectionTally = { - amountPayRemaining: contractTermsAmount, - amountWireFeeLimitRemaining: wireFeeLimit, - amountDepositFeeLimitRemaining: depositFeeLimit, - customerDepositFees: Amounts.getZero(currency), - customerWireFees: Amounts.getZero(currency), - wireFeeCoveredForExchange: new Set(), - }; - - const prevPayCoins = req.prevPayCoins ?? []; - - // Look at existing pay coin selection and tally up - for (const prev of prevPayCoins) { - tally = tallyFees( - tally, - candidates.wireFeesPerExchange, - wireFeeAmortization, - prev.exchangeBaseUrl, - prev.feeDeposit, - ); - tally.amountPayRemaining = Amounts.sub( - tally.amountPayRemaining, - prev.contribution, - ).amount; - - coinPubs.push(prev.coinPub); - coinContributions.push(prev.contribution); - } - - const prevCoinPubs = new Set(prevPayCoins.map((x) => x.coinPub)); - - // Sort by available amount (descending), deposit fee (ascending) and - // denomPub (ascending) if deposit fee is the same - // (to guarantee deterministic results) - const candidateCoins = [...candidates.candidateCoins].sort( - (o1, o2) => - -Amounts.cmp(o1.availableAmount, o2.availableAmount) || - Amounts.cmp(o1.feeDeposit, o2.feeDeposit) || - strcmp(o1.denomPub, o2.denomPub), - ); - - // FIXME: Here, we should select coins in a smarter way. - // Instead of always spending the next-largest coin, - // we should try to find the smallest coin that covers the - // amount. - - for (const aci of candidateCoins) { - // Don't use this coin if depositing it is more expensive than - // the amount it would give the merchant. - if (Amounts.cmp(aci.feeDeposit, aci.availableAmount) > 0) { - continue; - } - - if (Amounts.isZero(tally.amountPayRemaining)) { - // We have spent enough! - break; - } - - // The same coin can't contribute twice to the same payment, - // by a fundamental, intentional limitation of the protocol. - if (prevCoinPubs.has(aci.coinPub)) { - continue; - } - - tally = tallyFees( - tally, - candidates.wireFeesPerExchange, - wireFeeAmortization, - aci.exchangeBaseUrl, - aci.feeDeposit, - ); - - let coinSpend = Amounts.max( - Amounts.min(tally.amountPayRemaining, aci.availableAmount), - aci.feeDeposit, - ); - - tally.amountPayRemaining = Amounts.sub( - tally.amountPayRemaining, - coinSpend, - ).amount; - coinPubs.push(aci.coinPub); - coinContributions.push(coinSpend); - } - - if (Amounts.isZero(tally.amountPayRemaining)) { - return { - paymentAmount: contractTermsAmount, - coinContributions, - coinPubs, - customerDepositFees: tally.customerDepositFees, - customerWireFees: tally.customerWireFees, - }; - } - return undefined; -} diff --git a/packages/taler-wallet-core/src/util/contractTerms.test.ts b/packages/taler-wallet-core/src/util/contractTerms.test.ts deleted file mode 100644 index 74cae4ca7..000000000 --- a/packages/taler-wallet-core/src/util/contractTerms.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import test from "ava"; -import { ContractTermsUtil } from "./contractTerms.js"; - -test("contract terms canon hashing", (t) => { - const cReq = { - foo: 42, - bar: "hello", - $forgettable: { - foo: true, - }, - }; - - const c1 = ContractTermsUtil.saltForgettable(cReq); - const c2 = ContractTermsUtil.saltForgettable(cReq); - t.assert(typeof cReq.$forgettable.foo === "boolean"); - t.assert(typeof c1.$forgettable.foo === "string"); - t.assert(c1.$forgettable.foo !== c2.$forgettable.foo); - - const h1 = ContractTermsUtil.hashContractTerms(c1); - - const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1))); - - t.assert(c3.foo === undefined); - t.assert(c3.bar === cReq.bar); - - const h2 = ContractTermsUtil.hashContractTerms(c3); - - t.deepEqual(h1, h2); -}); - -test("contract terms canon hashing (nested)", (t) => { - const cReq = { - foo: 42, - bar: { - prop1: "hello, world", - $forgettable: { - prop1: true, - }, - }, - $forgettable: { - bar: true, - }, - }; - - const c1 = ContractTermsUtil.saltForgettable(cReq); - - t.is(typeof c1.$forgettable.bar, "string"); - t.is(typeof c1.bar.$forgettable.prop1, "string"); - - const forgetPath = (x: any, s: string) => - ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s); - - // Forget bar first - const c2 = forgetPath(c1, "bar"); - - // Forget bar.prop1 first - const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar"); - - // Forget everything - const c4 = ContractTermsUtil.scrub(c1); - - const h1 = ContractTermsUtil.hashContractTerms(c1); - const h2 = ContractTermsUtil.hashContractTerms(c2); - const h3 = ContractTermsUtil.hashContractTerms(c3); - const h4 = ContractTermsUtil.hashContractTerms(c4); - - t.is(h1, h2); - t.is(h1, h3); - t.is(h1, h4); - - // Doesn't contain salt - t.false(ContractTermsUtil.validateForgettable(cReq)); - - t.true(ContractTermsUtil.validateForgettable(c1)); - t.true(ContractTermsUtil.validateForgettable(c2)); - t.true(ContractTermsUtil.validateForgettable(c3)); - t.true(ContractTermsUtil.validateForgettable(c4)); -}); - -test("contract terms reference vector", (t) => { - const j = { - k1: 1, - $forgettable: { - k1: "SALT", - }, - k2: { - n1: true, - $forgettable: { - n1: "salt", - }, - }, - k3: { - n1: "string", - }, - }; - - const h = ContractTermsUtil.hashContractTerms(j); - - t.deepEqual( - h, - "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR", - ); -}); diff --git a/packages/taler-wallet-core/src/util/contractTerms.ts b/packages/taler-wallet-core/src/util/contractTerms.ts deleted file mode 100644 index b064079e9..000000000 --- a/packages/taler-wallet-core/src/util/contractTerms.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { canonicalJson, Logger } from "@gnu-taler/taler-util"; -import { kdf } from "@gnu-taler/taler-util"; -import { - decodeCrock, - encodeCrock, - getRandomBytes, - hash, - stringToBytes, -} from "@gnu-taler/taler-util"; - -const logger = new Logger("contractTerms.ts"); - -export namespace ContractTermsUtil { - export type PathPredicate = (path: string[]) => boolean; - - /** - * Scrub all forgettable members from an object. - */ - export function scrub(anyJson: any): any { - return forgetAllImpl(anyJson, [], () => true); - } - - /** - * Recursively forget all forgettable members of an object, - * where the path matches a predicate. - */ - export function forgetAll(anyJson: any, pred: PathPredicate): any { - return forgetAllImpl(anyJson, [], pred); - } - - function forgetAllImpl( - anyJson: any, - path: string[], - pred: PathPredicate, - ): any { - const dup = JSON.parse(JSON.stringify(anyJson)); - if (Array.isArray(dup)) { - for (let i = 0; i < dup.length; i++) { - dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred); - } - } else if (typeof dup === "object" && dup != null) { - if (typeof dup.$forgettable === "object") { - for (const x of Object.keys(dup.$forgettable)) { - if (!pred([...path, x])) { - continue; - } - if (!dup.$forgotten) { - dup.$forgotten = {}; - } - if (!dup.$forgotten[x]) { - const membValCanon = stringToBytes( - canonicalJson(scrub(dup[x])) + "\0", - ); - const membSalt = stringToBytes(dup.$forgettable[x] + "\0"); - const h = kdf(64, membValCanon, membSalt, new Uint8Array([])); - dup.$forgotten[x] = encodeCrock(h); - } - delete dup[x]; - delete dup.$forgettable[x]; - } - if (Object.keys(dup.$forgettable).length === 0) { - delete dup.$forgettable; - } - } - for (const x of Object.keys(dup)) { - if (x.startsWith("$")) { - continue; - } - dup[x] = forgetAllImpl(dup[x], [...path, x], pred); - } - } - return dup; - } - - /** - * Generate a salt for all members marked as forgettable, - * but which don't have an actual salt yet. - */ - export function saltForgettable(anyJson: any): any { - const dup = JSON.parse(JSON.stringify(anyJson)); - if (Array.isArray(dup)) { - for (let i = 0; i < dup.length; i++) { - dup[i] = saltForgettable(dup[i]); - } - } else if (typeof dup === "object" && dup !== null) { - if (typeof dup.$forgettable === "object") { - for (const k of Object.keys(dup.$forgettable)) { - if (dup.$forgettable[k] === true) { - dup.$forgettable[k] = encodeCrock(getRandomBytes(32)); - } - } - } - for (const x of Object.keys(dup)) { - if (x.startsWith("$")) { - continue; - } - dup[x] = saltForgettable(dup[x]); - } - } - return dup; - } - - const nameRegex = /^[0-9A-Za-z_]+$/; - - /** - * Check that the given JSON object is well-formed with regards - * to forgettable fields and other restrictions for forgettable JSON. - */ - export function validateForgettable(anyJson: any): boolean { - if (typeof anyJson === "string") { - return true; - } - if (typeof anyJson === "number") { - return ( - Number.isInteger(anyJson) && - anyJson >= Number.MIN_SAFE_INTEGER && - anyJson <= Number.MAX_SAFE_INTEGER - ); - } - if (typeof anyJson === "boolean") { - return true; - } - if (anyJson === null) { - return true; - } - if (Array.isArray(anyJson)) { - return anyJson.every((x) => validateForgettable(x)); - } - if (typeof anyJson === "object") { - for (const k of Object.keys(anyJson)) { - if (k.match(nameRegex)) { - if (validateForgettable(anyJson[k])) { - continue; - } else { - return false; - } - } - if (k === "$forgettable") { - const fga = anyJson.$forgettable; - if (!fga || typeof fga !== "object") { - return false; - } - for (const fk of Object.keys(fga)) { - if (!fk.match(nameRegex)) { - return false; - } - if (!(fk in anyJson)) { - return false; - } - const fv = anyJson.$forgettable[fk]; - if (typeof fv !== "string") { - return false; - } - } - } else if (k === "$forgotten") { - const fgo = anyJson.$forgotten; - if (!fgo || typeof fgo !== "object") { - return false; - } - for (const fk of Object.keys(fgo)) { - if (!fk.match(nameRegex)) { - return false; - } - // Check that the value has actually been forgotten. - if (fk in anyJson) { - return false; - } - const fv = anyJson.$forgotten[fk]; - if (typeof fv !== "string") { - return false; - } - try { - const decFv = decodeCrock(fv); - if (decFv.length != 64) { - return false; - } - } catch (e) { - return false; - } - // Check that salt has been deleted after forgetting. - if (anyJson.$forgettable?.[k] !== undefined) { - return false; - } - } - } else { - return false; - } - } - return true; - } - return false; - } - - /** - * Check that no forgettable information has been forgotten. - * - * Must only be called on an object already validated with validateForgettable. - */ - export function validateNothingForgotten(contractTerms: any): boolean { - throw Error("not implemented yet"); - } - - /** - * Hash a contract terms object. Forgettable fields - * are scrubbed and JSON canonicalization is applied - * before hashing. - */ - export function hashContractTerms(contractTerms: unknown): string { - const cleaned = scrub(contractTerms); - const canon = canonicalJson(cleaned) + "\0"; - const bytes = stringToBytes(canon); - logger.info(`contract terms before hashing: ${encodeCrock(bytes)}`); - return encodeCrock(hash(bytes)); - } -} diff --git a/packages/taler-wallet-core/src/util/debugFlags.ts b/packages/taler-wallet-core/src/util/debugFlags.ts deleted file mode 100644 index cea249d27..000000000 --- a/packages/taler-wallet-core/src/util/debugFlags.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Debug flags for wallet-core. - * - * @author Florian Dold - */ - -export interface WalletCoreDebugFlags { - /** - * Allow withdrawal of denominations even though they are about to expire. - */ - denomselAllowLate: boolean; -} - -export const walletCoreDebugFlags: WalletCoreDebugFlags = { - denomselAllowLate: false, -}; diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts deleted file mode 100644 index d01f2ee42..000000000 --- a/packages/taler-wallet-core/src/util/http.ts +++ /dev/null @@ -1,342 +0,0 @@ -/* - This file is part of TALER - (C) 2016 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 <http://www.gnu.org/licenses/> - */ - -/** - * Helpers for doing XMLHttpRequest-s that are based on ES6 promises. - * Allows for easy mocking for test cases. - * - * The API is inspired by the HTML5 fetch API. - */ - -/** - * Imports - */ -import { OperationFailedError, makeErrorDetails } from "../errors.js"; -import { - Logger, - Duration, - Timestamp, - getTimestampNow, - timestampAddDuration, - timestampMax, - TalerErrorDetails, - Codec, -} from "@gnu-taler/taler-util"; -import { TalerErrorCode } from "@gnu-taler/taler-util"; - -const logger = new Logger("http.ts"); - -/** - * An HTTP response that is returned by all request methods of this library. - */ -export interface HttpResponse { - requestUrl: string; - requestMethod: string; - status: number; - headers: Headers; - json(): Promise<any>; - text(): Promise<string>; - bytes(): Promise<ArrayBuffer>; -} - -export interface HttpRequestOptions { - method?: "POST" | "PUT" | "GET"; - headers?: { [name: string]: string }; - timeout?: Duration; - body?: string | ArrayBuffer | ArrayBufferView; -} - -export enum HttpResponseStatus { - Ok = 200, - NoContent = 204, - Gone = 210, - NotModified = 304, - BadRequest = 400, - PaymentRequired = 402, - NotFound = 404, - Conflict = 409, -} - -/** - * Headers, roughly modeled after the fetch API's headers object. - */ -export class Headers { - private headerMap = new Map<string, string>(); - - get(name: string): string | null { - const r = this.headerMap.get(name.toLowerCase()); - if (r) { - return r; - } - return null; - } - - set(name: string, value: string): void { - const normalizedName = name.toLowerCase(); - const existing = this.headerMap.get(normalizedName); - if (existing !== undefined) { - this.headerMap.set(normalizedName, existing + "," + value); - } else { - this.headerMap.set(normalizedName, value); - } - } - - toJSON(): any { - const m: Record<string, string> = {}; - this.headerMap.forEach((v, k) => (m[k] = v)); - return m; - } -} - -/** - * Interface for the HTTP request library used by the wallet. - * - * The request library is bundled into an interface to make mocking and - * request tunneling easy. - */ -export interface HttpRequestLibrary { - /** - * Make an HTTP GET request. - */ - get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>; - - /** - * Make an HTTP POST request with a JSON body. - */ - postJson( - url: string, - body: any, - opt?: HttpRequestOptions, - ): Promise<HttpResponse>; - - /** - * Make an HTTP POST request with a JSON body. - */ - fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>; -} - -type TalerErrorResponse = { - code: number; -} & unknown; - -type ResponseOrError<T> = - | { isError: false; response: T } - | { isError: true; talerErrorResponse: TalerErrorResponse }; - -export async function readTalerErrorResponse( - httpResponse: HttpResponse, -): Promise<TalerErrorDetails> { - const errJson = await httpResponse.json(); - const talerErrorCode = errJson.code; - if (typeof talerErrorCode !== "number") { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", - { - requestUrl: httpResponse.requestUrl, - requestMethod: httpResponse.requestMethod, - httpStatusCode: httpResponse.status, - }, - ), - ); - } - return errJson; -} - -export async function readUnexpectedResponseDetails( - httpResponse: HttpResponse, -): Promise<TalerErrorDetails> { - const errJson = await httpResponse.json(); - const talerErrorCode = errJson.code; - if (typeof talerErrorCode !== "number") { - return makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", - { - requestUrl: httpResponse.requestUrl, - requestMethod: httpResponse.requestMethod, - httpStatusCode: httpResponse.status, - }, - ); - } - return makeErrorDetails( - TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - "Unexpected error code in response", - { - requestUrl: httpResponse.requestUrl, - httpStatusCode: httpResponse.status, - errorResponse: errJson, - }, - ); -} - -export async function readSuccessResponseJsonOrErrorCode<T>( - httpResponse: HttpResponse, - codec: Codec<T>, -): Promise<ResponseOrError<T>> { - if (!(httpResponse.status >= 200 && httpResponse.status < 300)) { - return { - isError: true, - talerErrorResponse: await readTalerErrorResponse(httpResponse), - }; - } - const respJson = await httpResponse.json(); - let parsedResponse: T; - try { - parsedResponse = codec.decode(respJson); - } catch (e: any) { - throw OperationFailedError.fromCode( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Response invalid", - { - requestUrl: httpResponse.requestUrl, - httpStatusCode: httpResponse.status, - validationError: e.toString(), - }, - ); - } - return { - isError: false, - response: parsedResponse, - }; -} - -export function getHttpResponseErrorDetails( - httpResponse: HttpResponse, -): Record<string, unknown> { - return { - requestUrl: httpResponse.requestUrl, - httpStatusCode: httpResponse.status, - }; -} - -export function throwUnexpectedRequestError( - httpResponse: HttpResponse, - talerErrorResponse: TalerErrorResponse, -): never { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - "Unexpected error code in response", - { - requestUrl: httpResponse.requestUrl, - httpStatusCode: httpResponse.status, - errorResponse: talerErrorResponse, - }, - ), - ); -} - -export async function readSuccessResponseJsonOrThrow<T>( - httpResponse: HttpResponse, - codec: Codec<T>, -): Promise<T> { - const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec); - if (!r.isError) { - return r.response; - } - throwUnexpectedRequestError(httpResponse, r.talerErrorResponse); -} - -export async function readSuccessResponseTextOrErrorCode<T>( - httpResponse: HttpResponse, -): Promise<ResponseOrError<string>> { - if (!(httpResponse.status >= 200 && httpResponse.status < 300)) { - const errJson = await httpResponse.json(); - const talerErrorCode = errJson.code; - if (typeof talerErrorCode !== "number") { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", - { - httpStatusCode: httpResponse.status, - requestUrl: httpResponse.requestUrl, - requestMethod: httpResponse.requestMethod, - }, - ), - ); - } - return { - isError: true, - talerErrorResponse: errJson, - }; - } - const respJson = await httpResponse.text(); - return { - isError: false, - response: respJson, - }; -} - -export async function checkSuccessResponseOrThrow( - httpResponse: HttpResponse, -): Promise<void> { - if (!(httpResponse.status >= 200 && httpResponse.status < 300)) { - const errJson = await httpResponse.json(); - const talerErrorCode = errJson.code; - if (typeof talerErrorCode !== "number") { - throw new OperationFailedError( - makeErrorDetails( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Error response did not contain error code", - { - httpStatusCode: httpResponse.status, - requestUrl: httpResponse.requestUrl, - requestMethod: httpResponse.requestMethod, - }, - ), - ); - } - throwUnexpectedRequestError(httpResponse, errJson); - } -} - -export async function readSuccessResponseTextOrThrow<T>( - httpResponse: HttpResponse, -): Promise<string> { - const r = await readSuccessResponseTextOrErrorCode(httpResponse); - if (!r.isError) { - return r.response; - } - throwUnexpectedRequestError(httpResponse, r.talerErrorResponse); -} - -/** - * Get the timestamp at which the response's content is considered expired. - */ -export function getExpiryTimestamp( - httpResponse: HttpResponse, - opt: { minDuration?: Duration }, -): Timestamp { - const expiryDateMs = new Date( - httpResponse.headers.get("expiry") ?? "", - ).getTime(); - let t: Timestamp; - if (Number.isNaN(expiryDateMs)) { - t = getTimestampNow(); - } else { - t = { - t_ms: expiryDateMs, - }; - } - if (opt.minDuration) { - const t2 = timestampAddDuration(getTimestampNow(), opt.minDuration); - return timestampMax(t, t2); - } - return t; -} diff --git a/packages/taler-wallet-core/src/util/invariants.ts b/packages/taler-wallet-core/src/util/invariants.ts deleted file mode 100644 index b788d044e..000000000 --- a/packages/taler-wallet-core/src/util/invariants.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Helpers for invariants. - */ - -export function checkDbInvariant(b: boolean, m?: string): asserts b { - if (!b) { - if (m) { - throw Error(`BUG: database invariant failed (${m})`); - } else { - throw Error("BUG: database invariant failed"); - } - } -} - -export function checkLogicInvariant(b: boolean, m?: string): asserts b { - if (!b) { - if (m) { - throw Error(`BUG: logic invariant failed (${m})`); - } else { - throw Error("BUG: logic invariant failed"); - } - } -} 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 d409686d9..000000000 --- a/packages/taler-wallet-core/src/util/promiseUtils.ts +++ /dev/null @@ -1,60 +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 <http://www.gnu.org/licenses/> - */ - -export interface OpenedPromise<T> { - promise: Promise<T>; - resolve: (val: T) => void; - reject: (err: any) => void; -} - -/** - * Get an unresolved promise together with its extracted resolve / reject - * function. - */ -export function openPromise<T>(): OpenedPromise<T> { - let resolve: ((x?: any) => void) | null = null; - let reject: ((reason?: any) => void) | null = null; - const promise = new Promise<T>((res, rej) => { - resolve = res; - reject = rej; - }); - if (!(resolve && reject)) { - // Never happens, unless JS implementation is broken - throw Error(); - } - return { resolve, reject, promise }; -} - -export class AsyncCondition { - private _waitPromise: Promise<void>; - private _resolveWaitPromise: (val: void) => void; - constructor() { - const op = openPromise<void>(); - this._waitPromise = op.promise; - this._resolveWaitPromise = op.resolve; - } - - wait(): Promise<void> { - return this._waitPromise; - } - - trigger(): void { - this._resolveWaitPromise(); - const op = openPromise<void>(); - this._waitPromise = op.promise; - this._resolveWaitPromise = op.resolve; - } -} diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts deleted file mode 100644 index a95cbf1ff..000000000 --- a/packages/taler-wallet-core/src/util/query.ts +++ /dev/null @@ -1,615 +0,0 @@ -/* - This file is part of TALER - (C) 2016 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 <http://www.gnu.org/licenses/> - */ - -/** - * Database query abstractions. - * @module Query - * @author Florian Dold - */ - -/** - * Imports. - */ -import { openPromise } from "./promiseUtils.js"; -import { - IDBRequest, - IDBTransaction, - IDBValidKey, - IDBDatabase, - IDBFactory, - IDBVersionChangeEvent, - IDBCursor, - IDBKeyPath, -} from "@gnu-taler/idb-bridge"; -import { Logger } from "@gnu-taler/taler-util"; -import { performanceNow } from "./timer.js"; - -const logger = new Logger("query.ts"); - -/** - * Exception that should be thrown by client code to abort a transaction. - */ -export const TransactionAbort = Symbol("transaction_abort"); - -/** - * Options for an index. - */ -export interface IndexOptions { - /** - * If true and the path resolves to an array, create an index entry for - * each member of the array (instead of one index entry containing the full array). - * - * Defaults to false. - */ - multiEntry?: boolean; - - /** - * Database version that this store was added in, or - * undefined if added in the first version. - */ - versionAdded?: number; -} - -function requestToPromise(req: IDBRequest): Promise<any> { - const stack = Error("Failed request was started here."); - return new Promise((resolve, reject) => { - req.onsuccess = () => { - resolve(req.result); - }; - req.onerror = () => { - console.error("error in DB request", req.error); - reject(req.error); - console.error("Request failed:", stack); - }; - }); -} - -type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>; - -interface CursorEmptyResult<T> { - hasValue: false; -} - -interface CursorValueResult<T> { - hasValue: true; - value: T; -} - -class TransactionAbortedError extends Error { - constructor(m: string) { - super(m); - - // Set the prototype explicitly. - Object.setPrototypeOf(this, TransactionAbortedError.prototype); - } -} - -class ResultStream<T> { - private currentPromise: Promise<void>; - private gotCursorEnd = false; - private awaitingResult = false; - - constructor(private req: IDBRequest) { - this.awaitingResult = true; - let p = openPromise<void>(); - this.currentPromise = p.promise; - req.onsuccess = () => { - if (!this.awaitingResult) { - throw Error("BUG: invariant violated"); - } - const cursor = req.result; - if (cursor) { - this.awaitingResult = false; - p.resolve(); - p = openPromise<void>(); - this.currentPromise = p.promise; - } else { - this.gotCursorEnd = true; - p.resolve(); - } - }; - req.onerror = () => { - p.reject(req.error); - }; - } - - async toArray(): Promise<T[]> { - const arr: T[] = []; - while (true) { - const x = await this.next(); - if (x.hasValue) { - arr.push(x.value); - } else { - break; - } - } - return arr; - } - - async map<R>(f: (x: T) => R): Promise<R[]> { - const arr: R[] = []; - while (true) { - const x = await this.next(); - if (x.hasValue) { - arr.push(f(x.value)); - } else { - break; - } - } - return arr; - } - - async forEachAsync(f: (x: T) => Promise<void>): Promise<void> { - while (true) { - const x = await this.next(); - if (x.hasValue) { - await f(x.value); - } else { - break; - } - } - } - - async forEach(f: (x: T) => void): Promise<void> { - while (true) { - const x = await this.next(); - if (x.hasValue) { - f(x.value); - } else { - break; - } - } - } - - async filter(f: (x: T) => boolean): Promise<T[]> { - const arr: T[] = []; - while (true) { - const x = await this.next(); - if (x.hasValue) { - if (f(x.value)) { - arr.push(x.value); - } - } else { - break; - } - } - return arr; - } - - async next(): Promise<CursorResult<T>> { - if (this.gotCursorEnd) { - return { hasValue: false }; - } - if (!this.awaitingResult) { - const cursor: IDBCursor | undefined = this.req.result; - if (!cursor) { - throw Error("assertion failed"); - } - this.awaitingResult = true; - cursor.continue(); - } - await this.currentPromise; - if (this.gotCursorEnd) { - return { hasValue: false }; - } - const cursor = this.req.result; - if (!cursor) { - throw Error("assertion failed"); - } - return { hasValue: true, value: cursor.value }; - } -} - -/** - * Return a promise that resolves to the opened IndexedDB database. - */ -export function openDatabase( - idbFactory: IDBFactory, - databaseName: string, - databaseVersion: number, - onVersionChange: () => void, - onUpgradeNeeded: ( - db: IDBDatabase, - oldVersion: number, - newVersion: number, - upgradeTransaction: IDBTransaction, - ) => void, -): Promise<IDBDatabase> { - return new Promise<IDBDatabase>((resolve, reject) => { - const req = idbFactory.open(databaseName, databaseVersion); - req.onerror = (e) => { - logger.error("database error", e); - reject(new Error("database error")); - }; - req.onsuccess = (e) => { - req.result.onversionchange = (evt: IDBVersionChangeEvent) => { - logger.info( - `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`, - ); - req.result.close(); - onVersionChange(); - }; - resolve(req.result); - }; - req.onupgradeneeded = (e) => { - const db = req.result; - const newVersion = e.newVersion; - if (!newVersion) { - throw Error("upgrade needed, but new version unknown"); - } - const transaction = req.transaction; - if (!transaction) { - throw Error("no transaction handle available in upgrade handler"); - } - onUpgradeNeeded(db, e.oldVersion, newVersion, transaction); - }; - }); -} - -export interface IndexDescriptor { - name: string; - keyPath: IDBKeyPath | IDBKeyPath[]; - multiEntry?: boolean; -} - -export interface StoreDescriptor<RecordType> { - _dummy: undefined & RecordType; - name: string; - keyPath?: IDBKeyPath | IDBKeyPath[]; - autoIncrement?: boolean; -} - -export interface StoreOptions { - keyPath?: IDBKeyPath | IDBKeyPath[]; - autoIncrement?: boolean; -} - -export function describeContents<RecordType = never>( - name: string, - options: StoreOptions, -): StoreDescriptor<RecordType> { - return { name, keyPath: options.keyPath, _dummy: undefined as any }; -} - -export function describeIndex( - name: string, - keyPath: IDBKeyPath | IDBKeyPath[], - options: IndexOptions = {}, -): IndexDescriptor { - return { - keyPath, - name, - multiEntry: options.multiEntry, - }; -} - -interface IndexReadOnlyAccessor<RecordType> { - iter(query?: IDBValidKey): ResultStream<RecordType>; - get(query: IDBValidKey): Promise<RecordType | undefined>; - getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>; -} - -type GetIndexReadOnlyAccess<RecordType, IndexMap> = { - [P in keyof IndexMap]: IndexReadOnlyAccessor<RecordType>; -}; - -interface IndexReadWriteAccessor<RecordType> { - iter(query: IDBValidKey): ResultStream<RecordType>; - get(query: IDBValidKey): Promise<RecordType | undefined>; - getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>; -} - -type GetIndexReadWriteAccess<RecordType, IndexMap> = { - [P in keyof IndexMap]: IndexReadWriteAccessor<RecordType>; -}; - -export interface StoreReadOnlyAccessor<RecordType, IndexMap> { - get(key: IDBValidKey): Promise<RecordType | undefined>; - iter(query?: IDBValidKey): ResultStream<RecordType>; - indexes: GetIndexReadOnlyAccess<RecordType, IndexMap>; -} - -export interface StoreReadWriteAccessor<RecordType, IndexMap> { - get(key: IDBValidKey): Promise<RecordType | undefined>; - iter(query?: IDBValidKey): ResultStream<RecordType>; - put(r: RecordType): Promise<void>; - add(r: RecordType): Promise<void>; - delete(key: IDBValidKey): Promise<void>; - indexes: GetIndexReadWriteAccess<RecordType, IndexMap>; -} - -export interface StoreWithIndexes< - SD extends StoreDescriptor<unknown>, - IndexMap -> { - store: SD; - indexMap: IndexMap; - - /** - * Type marker symbol, to check that the descriptor - * has been created through the right function. - */ - mark: Symbol; -} - -export type GetRecordType<T> = T extends StoreDescriptor<infer X> ? X : unknown; - -const storeWithIndexesSymbol = Symbol("StoreWithIndexesMark"); - -export function describeStore<SD extends StoreDescriptor<unknown>, IndexMap>( - s: SD, - m: IndexMap, -): StoreWithIndexes<SD, IndexMap> { - return { - store: s, - indexMap: m, - mark: storeWithIndexesSymbol, - }; -} - -export type GetReadOnlyAccess<BoundStores> = { - [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes< - infer SD, - infer IM - > - ? StoreReadOnlyAccessor<GetRecordType<SD>, IM> - : unknown; -}; - -export type GetReadWriteAccess<BoundStores> = { - [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes< - infer SD, - infer IM - > - ? StoreReadWriteAccessor<GetRecordType<SD>, IM> - : unknown; -}; - -type ReadOnlyTransactionFunction<BoundStores, T> = ( - t: GetReadOnlyAccess<BoundStores>, -) => Promise<T>; - -type ReadWriteTransactionFunction<BoundStores, T> = ( - t: GetReadWriteAccess<BoundStores>, -) => Promise<T>; - -export interface TransactionContext<BoundStores> { - runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>; - runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>; -} - -type CheckDescriptor<T> = T extends StoreWithIndexes<infer SD, infer IM> - ? StoreWithIndexes<SD, IM> - : unknown; - -type GetPickerType<F, SM> = F extends (x: SM) => infer Out - ? { [P in keyof Out]: CheckDescriptor<Out[P]> } - : unknown; - -function runTx<Arg, Res>( - tx: IDBTransaction, - arg: Arg, - f: (t: Arg) => Promise<Res>, -): Promise<Res> { - const stack = Error("Failed transaction was started here."); - return new Promise((resolve, reject) => { - let funResult: any = undefined; - let gotFunResult = false; - let transactionException: any = undefined; - tx.oncomplete = () => { - // This is a fatal error: The transaction completed *before* - // the transaction function returned. Likely, the transaction - // function waited on a promise that is *not* resolved in the - // microtask queue, thus triggering the auto-commit behavior. - // Unfortunately, the auto-commit behavior of IDB can't be switched - // of. There are some proposals to add this functionality in the future. - if (!gotFunResult) { - const msg = - "BUG: transaction closed before transaction function returned"; - console.error(msg); - reject(Error(msg)); - } - resolve(funResult); - }; - tx.onerror = () => { - logger.error("error in transaction"); - logger.error(`${stack}`); - }; - tx.onabort = () => { - let msg: string; - if (tx.error) { - msg = `Transaction aborted (transaction error): ${tx.error}`; - } else if (transactionException !== undefined) { - msg = `Transaction aborted (exception thrown): ${transactionException}`; - } else { - msg = "Transaction aborted (no DB error)"; - } - logger.error(msg); - reject(new TransactionAbortedError(msg)); - }; - const resP = Promise.resolve().then(() => f(arg)); - resP - .then((result) => { - gotFunResult = true; - funResult = result; - }) - .catch((e) => { - if (e == TransactionAbort) { - logger.trace("aborting transaction"); - } else { - transactionException = e; - console.error("Transaction failed:", e); - console.error(stack); - tx.abort(); - } - }) - .catch((e) => { - console.error("fatal: aborting transaction failed", e); - }); - }); -} - -function makeReadContext( - tx: IDBTransaction, - storePick: { [n: string]: StoreWithIndexes<any, any> }, -): any { - const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {}; - for (const storeAlias in storePick) { - const indexes: { [s: string]: IndexReadOnlyAccessor<any> } = {}; - const swi = storePick[storeAlias]; - const storeName = swi.store.name; - for (const indexAlias in storePick[storeAlias].indexMap) { - const indexDescriptor: IndexDescriptor = - storePick[storeAlias].indexMap[indexAlias]; - const indexName = indexDescriptor.name; - indexes[indexAlias] = { - get(key) { - const req = tx.objectStore(storeName).index(indexName).get(key); - return requestToPromise(req); - }, - iter(query) { - const req = tx - .objectStore(storeName) - .index(indexName) - .openCursor(query); - return new ResultStream<any>(req); - }, - getAll(query, count) { - const req = tx.objectStore(storeName).index(indexName).getAll(query, count); - return requestToPromise(req); - } - }; - } - ctx[storeAlias] = { - indexes, - get(key) { - const req = tx.objectStore(storeName).get(key); - return requestToPromise(req); - }, - iter(query) { - const req = tx.objectStore(storeName).openCursor(query); - return new ResultStream<any>(req); - }, - }; - } - return ctx; -} - -function makeWriteContext( - tx: IDBTransaction, - storePick: { [n: string]: StoreWithIndexes<any, any> }, -): any { - const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {}; - for (const storeAlias in storePick) { - const indexes: { [s: string]: IndexReadWriteAccessor<any> } = {}; - const swi = storePick[storeAlias]; - const storeName = swi.store.name; - for (const indexAlias in storePick[storeAlias].indexMap) { - const indexDescriptor: IndexDescriptor = - storePick[storeAlias].indexMap[indexAlias]; - const indexName = indexDescriptor.name; - indexes[indexAlias] = { - get(key) { - const req = tx.objectStore(storeName).index(indexName).get(key); - return requestToPromise(req); - }, - iter(query) { - const req = tx - .objectStore(storeName) - .index(indexName) - .openCursor(query); - return new ResultStream<any>(req); - }, - getAll(query, count) { - const req = tx.objectStore(storeName).index(indexName).getAll(query, count); - return requestToPromise(req); - } - }; - } - ctx[storeAlias] = { - indexes, - get(key) { - const req = tx.objectStore(storeName).get(key); - return requestToPromise(req); - }, - iter(query) { - const req = tx.objectStore(storeName).openCursor(query); - return new ResultStream<any>(req); - }, - add(r) { - const req = tx.objectStore(storeName).add(r); - return requestToPromise(req); - }, - put(r) { - const req = tx.objectStore(storeName).put(r); - return requestToPromise(req); - }, - delete(k) { - const req = tx.objectStore(storeName).delete(k); - return requestToPromise(req); - }, - }; - } - return ctx; -} - -/** - * Type-safe access to a database with a particular store map. - * - * A store map is the metadata that describes the store. - */ -export class DbAccess<StoreMap> { - constructor(private db: IDBDatabase, private stores: StoreMap) {} - - mktx< - PickerType extends (x: StoreMap) => unknown, - BoundStores extends GetPickerType<PickerType, StoreMap> - >(f: PickerType): TransactionContext<BoundStores> { - const storePick = f(this.stores) as any; - if (typeof storePick !== "object" || storePick === null) { - throw Error(); - } - const storeNames: string[] = []; - for (const storeAlias of Object.keys(storePick)) { - const swi = (storePick as any)[storeAlias] as StoreWithIndexes<any, any>; - if (swi.mark !== storeWithIndexesSymbol) { - throw Error("invalid store descriptor returned from selector function"); - } - storeNames.push(swi.store.name); - } - - const runReadOnly = <T>( - txf: ReadOnlyTransactionFunction<BoundStores, T>, - ): Promise<T> => { - const tx = this.db.transaction(storeNames, "readonly"); - const readContext = makeReadContext(tx, storePick); - return runTx(tx, readContext, txf); - }; - - const runReadWrite = <T>( - txf: ReadWriteTransactionFunction<BoundStores, T>, - ): Promise<T> => { - const tx = this.db.transaction(storeNames, "readwrite"); - const writeContext = makeWriteContext(tx, storePick); - return runTx(tx, writeContext, txf); - }; - - return { - runReadOnly, - runReadWrite, - }; - } -} diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts deleted file mode 100644 index cac7b1b52..000000000 --- a/packages/taler-wallet-core/src/util/retries.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Helpers for dealing with retry timeouts. - */ - -/** - * Imports. - */ -import { Timestamp, Duration, getTimestampNow } from "@gnu-taler/taler-util"; - -export interface RetryInfo { - firstTry: Timestamp; - nextRetry: Timestamp; - retryCounter: number; -} - -export interface RetryPolicy { - readonly backoffDelta: Duration; - readonly backoffBase: number; -} - -const defaultRetryPolicy: RetryPolicy = { - backoffBase: 1.5, - backoffDelta: { d_ms: 200 }, -}; - -export function updateRetryInfoTimeout( - r: RetryInfo, - p: RetryPolicy = defaultRetryPolicy, -): void { - const now = getTimestampNow(); - if (now.t_ms === "never") { - throw Error("assertion failed"); - } - if (p.backoffDelta.d_ms === "forever") { - r.nextRetry = { t_ms: "never" }; - return; - } - const t = - now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter); - r.nextRetry = { t_ms: t }; -} - -export function getRetryDuration( - r: RetryInfo | undefined, - p: RetryPolicy = defaultRetryPolicy, -): Duration { - if (!r) { - // If we don't have any retry info, run immediately. - return { d_ms: 0 }; - } - if (p.backoffDelta.d_ms === "forever") { - return { d_ms: "forever" }; - } - const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter); - return { d_ms: t }; -} - -export function initRetryInfo( - p: RetryPolicy = defaultRetryPolicy, -): RetryInfo { - const now = getTimestampNow(); - const info = { - firstTry: now, - nextRetry: now, - retryCounter: 0, - }; - updateRetryInfoTimeout(info, p); - return info; -} diff --git a/packages/taler-wallet-core/src/util/timer.ts b/packages/taler-wallet-core/src/util/timer.ts deleted file mode 100644 index d9fe3439b..000000000 --- a/packages/taler-wallet-core/src/util/timer.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2017-2019 Taler Systems S.A. - - GNU 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. - - GNU 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Cross-platform timers. - * - * NodeJS and the browser use slightly different timer API, - * this abstracts over these differences. - */ - -/** - * Imports. - */ -import { Logger, Duration } from "@gnu-taler/taler-util"; - -const logger = new Logger("timer.ts"); - -/** - * Cancelable timer. - */ -export interface TimerHandle { - clear(): void; - - /** - * Make sure the event loop exits when the timer is the - * only event left. Has no effect in the browser. - */ - unref(): void; -} - -class IntervalHandle { - constructor(public h: any) {} - - clear(): void { - clearInterval(this.h); - } - - /** - * Make sure the event loop exits when the timer is the - * only event left. Has no effect in the browser. - */ - unref(): void { - if (typeof this.h === "object") { - this.h.unref(); - } - } -} - -class TimeoutHandle { - constructor(public h: any) {} - - clear(): void { - clearTimeout(this.h); - } - - /** - * Make sure the event loop exits when the timer is the - * only event left. Has no effect in the browser. - */ - unref(): void { - if (typeof this.h === "object") { - this.h.unref(); - } - } -} - -/** - * Get a performance counter in nanoseconds. - */ -export const performanceNow: () => bigint = (() => { - // @ts-ignore - if (typeof process !== "undefined" && process.hrtime) { - return () => { - return process.hrtime.bigint(); - }; - } - - // @ts-ignore - if (typeof performance !== "undefined") { - // @ts-ignore - return () => BigInt(Math.floor(performance.now() * 1000)) * BigInt(1000); - } - - return () => BigInt(0); -})(); - -/** - * Call a function every time the delay given in milliseconds passes. - */ -export function every(delayMs: number, callback: () => void): TimerHandle { - return new IntervalHandle(setInterval(callback, delayMs)); -} - -/** - * Call a function after the delay given in milliseconds passes. - */ -export function after(delayMs: number, callback: () => void): TimerHandle { - return new TimeoutHandle(setTimeout(callback, delayMs)); -} - -const nullTimerHandle = { - clear() { - // do nothing - return; - }, - unref() { - // do nothing - return; - }, -}; - -/** - * Group of timers that can be destroyed at once. - */ -export class TimerGroup { - private stopped = false; - - private timerMap: { [index: number]: TimerHandle } = {}; - - private idGen = 1; - - stopCurrentAndFutureTimers(): void { - this.stopped = true; - for (const x in this.timerMap) { - if (!this.timerMap.hasOwnProperty(x)) { - continue; - } - this.timerMap[x].clear(); - delete this.timerMap[x]; - } - } - - resolveAfter(delayMs: Duration): Promise<void> { - return new Promise<void>((resolve, reject) => { - if (delayMs.d_ms !== "forever") { - this.after(delayMs.d_ms, () => { - resolve(); - }); - } - }); - } - - after(delayMs: number, callback: () => void): TimerHandle { - if (this.stopped) { - logger.warn("dropping timer since timer group is stopped"); - return nullTimerHandle; - } - const h = after(delayMs, callback); - const myId = this.idGen++; - this.timerMap[myId] = h; - - const tm = this.timerMap; - - return { - clear() { - h.clear(); - delete tm[myId]; - }, - unref() { - h.unref(); - }, - }; - } - - every(delayMs: number, callback: () => void): TimerHandle { - if (this.stopped) { - logger.warn("dropping timer since timer group is stopped"); - return nullTimerHandle; - } - const h = every(delayMs, callback); - const myId = this.idGen++; - this.timerMap[myId] = h; - - const tm = this.timerMap; - - return { - clear() { - h.clear(); - delete tm[myId]; - }, - unref() { - h.unref(); - }, - }; - } -} |