From 13e7a674778754c0ed641dfd428e3d6b2b71ab2d Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 5 Sep 2022 18:12:30 +0200 Subject: wallet-core: uniform retry handling --- packages/taler-wallet-core/src/util/query.ts | 41 +++++++++ packages/taler-wallet-core/src/util/retries.ts | 116 ++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 1 deletion(-) (limited to 'packages/taler-wallet-core/src/util') diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index e954e5c78..65b67eff2 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -152,6 +152,19 @@ class ResultStream { return arr; } + async mapAsync(f: (x: T) => Promise): Promise { + const arr: R[] = []; + while (true) { + const x = await this.next(); + if (x.hasValue) { + arr.push(await f(x.value)); + } else { + break; + } + } + return arr; + } + async forEachAsync(f: (x: T) => Promise): Promise { while (true) { const x = await this.next(); @@ -572,6 +585,26 @@ function makeWriteContext( return ctx; } +const storeList = [ + { name: "foo" as const, value: 1 as const }, + { name: "bar" as const, value: 2 as const }, +]; +// => { foo: { value: 1}, bar: {value: 2} } + +type StoreList = typeof storeList; + +type StoreNames = StoreList[number] extends { name: infer I } ? I : never; + +type H = StoreList[number] & { name: "foo"}; + +type Cleanup = V extends { name: infer N, value: infer X} ? {name: N, value: X} : never; + +type G = { + [X in StoreNames]: { + X: StoreList[number] & { name: X }; + }; +}; + /** * Type-safe access to a database with a particular store map. * @@ -584,6 +617,14 @@ export class DbAccess { return this.db; } + mktx2< + StoreNames extends keyof StoreMap, + Stores extends StoreMap[StoreNames], + StoreList extends Stores[], + >(namePicker: (x: StoreMap) => StoreList): StoreList { + return namePicker(this.stores); + } + mktx< PickerType extends (x: StoreMap) => unknown, BoundStores extends GetPickerType, diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index 13a05b385..3a41e8348 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -21,7 +21,29 @@ /** * Imports. */ -import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + Duration, + TalerErrorDetail, +} from "@gnu-taler/taler-util"; +import { + BackupProviderRecord, + DepositGroupRecord, + ExchangeRecord, + OperationAttemptResult, + OperationAttemptResultType, + ProposalRecord, + PurchaseRecord, + RecoupGroupRecord, + RefreshGroupRecord, + TipRecord, + WalletStoresV1, + WithdrawalGroupRecord, +} from "../db.js"; +import { TalerError } from "../errors.js"; +import { InternalWalletState } from "../internal-wallet-state.js"; +import { PendingTaskType } from "../pending-types.js"; +import { GetReadWriteAccess } from "./query.js"; export interface RetryInfo { firstTry: AbsoluteTime; @@ -108,3 +130,95 @@ export namespace RetryInfo { return r2; } } + +export namespace RetryTags { + export function forWithdrawal(wg: WithdrawalGroupRecord): string { + return `${PendingTaskType.Withdraw}:${wg.withdrawalGroupId}`; + } + export function forExchangeUpdate(exch: ExchangeRecord): string { + return `${PendingTaskType.ExchangeUpdate}:${exch.baseUrl}`; + } + export function forExchangeCheckRefresh(exch: ExchangeRecord): string { + return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}`; + } + export function forProposalClaim(pr: ProposalRecord): string { + return `${PendingTaskType.ProposalDownload}:${pr.proposalId}`; + } + export function forTipPickup(tipRecord: TipRecord): string { + return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}`; + } + export function forRefresh(refreshGroupRecord: RefreshGroupRecord): string { + return `${PendingTaskType.TipPickup}:${refreshGroupRecord.refreshGroupId}`; + } + export function forPay(purchaseRecord: PurchaseRecord): string { + return `${PendingTaskType.Pay}:${purchaseRecord.proposalId}`; + } + export function forRefundQuery(purchaseRecord: PurchaseRecord): string { + return `${PendingTaskType.RefundQuery}:${purchaseRecord.proposalId}`; + } + export function forRecoup(recoupRecord: RecoupGroupRecord): string { + return `${PendingTaskType.Recoup}:${recoupRecord.recoupGroupId}`; + } + export function forDeposit(depositRecord: DepositGroupRecord): string { + return `${PendingTaskType.Deposit}:${depositRecord.depositGroupId}`; + } + export function forBackup(backupRecord: BackupProviderRecord): string { + return `${PendingTaskType.Backup}:${backupRecord.baseUrl}`; + } +} + +export async function scheduleRetryInTx( + ws: InternalWalletState, + tx: GetReadWriteAccess<{ + operationRetries: typeof WalletStoresV1.operationRetries; + }>, + opId: string, + errorDetail?: TalerErrorDetail, +): Promise { + let retryRecord = await tx.operationRetries.get(opId); + if (!retryRecord) { + retryRecord = { + id: opId, + retryInfo: RetryInfo.reset(), + }; + if (errorDetail) { + retryRecord.lastError = errorDetail; + } + } else { + retryRecord.retryInfo = RetryInfo.increment(retryRecord.retryInfo); + if (errorDetail) { + retryRecord.lastError = errorDetail; + } else { + delete retryRecord.lastError; + } + } + await tx.operationRetries.put(retryRecord); +} + +export async function scheduleRetry( + ws: InternalWalletState, + opId: string, + errorDetail?: TalerErrorDetail, +): Promise { + return await ws.db + .mktx((x) => ({ operationRetries: x.operationRetries })) + .runReadWrite(async (tx) => { + scheduleRetryInTx(ws, tx, opId, errorDetail); + }); +} + +/** + * Run an operation handler, expect a success result and extract the success value. + */ +export async function runOperationHandlerForResult( + res: OperationAttemptResult, +): Promise { + switch (res.type) { + case OperationAttemptResultType.Finished: + return res.result; + case OperationAttemptResultType.Error: + throw TalerError.fromUncheckedDetail(res.errorDetail); + default: + throw Error(`unexpected operation result (${res.type})`); + } +} -- cgit v1.2.3