aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/util
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-09-05 18:12:30 +0200
committerFlorian Dold <florian@dold.me>2022-09-13 16:10:41 +0200
commit13e7a674778754c0ed641dfd428e3d6b2b71ab2d (patch)
treef2a0e5029305a9b818416fd94908ef77cdd7446f /packages/taler-wallet-core/src/util
parentf9f2911c761af1c8ed1c323dcd414cbaa9eeae7c (diff)
downloadwallet-core-13e7a674778754c0ed641dfd428e3d6b2b71ab2d.tar.xz
wallet-core: uniform retry handling
Diffstat (limited to 'packages/taler-wallet-core/src/util')
-rw-r--r--packages/taler-wallet-core/src/util/query.ts41
-rw-r--r--packages/taler-wallet-core/src/util/retries.ts116
2 files changed, 156 insertions, 1 deletions
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<T> {
return arr;
}
+ async mapAsync<R>(f: (x: T) => Promise<R>): Promise<R[]> {
+ 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<void>): Promise<void> {
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> = 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<StoreMap> {
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<PickerType, StoreMap>,
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<void> {
+ 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<void> {
+ 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<T>(
+ res: OperationAttemptResult<T>,
+): Promise<T> {
+ 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})`);
+ }
+}