taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 89c2b5f4044b140695dcb75dd0567457308d4afb
parent 7ec5453abfcc3eebec27313583f9560a7e4cbb1b
Author: Florian Dold <florian@dold.me>
Date:   Fri, 13 Feb 2026 18:10:56 +0100

wallet-core: refactor refresh transitions

Diffstat:
Mpackages/taler-wallet-core/src/refresh.ts | 133+++++++++++++++++++++++--------------------------------------------------------
1 file changed, 39 insertions(+), 94 deletions(-)

diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -84,14 +84,14 @@ import { cancelableFetch, constructTaskIdentifier, genericWaitForState, + getGenericRecordHandle, makeCoinsVisible, PendingTaskType, + RecordHandle, TaskIdStr, TaskRunResult, TaskRunResultType, TransactionContext, - TransitionResult, - TransitionResultType, } from "./common.js"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { @@ -115,7 +115,6 @@ import { WalletDbAllStoresReadOnlyTransaction, WalletDbReadOnlyTransaction, WalletDbReadWriteTransaction, - WalletDbStoresArr, } from "./db.js"; import { selectWithdrawalDenominations } from "./denomSelection.js"; import { fetchFreshExchange, getScopeForAllExchanges } from "./exchanges.js"; @@ -226,84 +225,22 @@ export class RefreshTransactionContext implements TransactionContext { }; } - /** - * Transition a withdrawal transaction. - * Extra object stores may be accessed during the transition. - */ - async transition<StoreNameArray extends WalletDbStoresArr = []>( - opts: { extraStores?: StoreNameArray; transactionLabel?: string }, - f: ( - rec: RefreshGroupRecord | undefined, - tx: WalletDbReadWriteTransaction< - [ - "refreshGroups", - "transactionsMeta", - "operationRetries", - "exchanges", - "exchangeDetails", - ...StoreNameArray, - ] - >, - ) => Promise<TransitionResult<RefreshGroupRecord>>, - ): Promise<boolean> { - const baseStores = [ - "refreshGroups" as const, - "transactionsMeta" as const, - "operationRetries" as const, - "exchanges" as const, - "exchangeDetails" as const, - ]; - let stores = opts.extraStores - ? [...baseStores, ...opts.extraStores] - : baseStores; - return await this.wex.db.runReadWriteTx( - { storeNames: stores }, - async (tx) => { - const wgRec = await tx.refreshGroups.get(this.refreshGroupId); - let oldStId: number; - let oldTxState: TransactionState; - if (wgRec) { - oldTxState = computeRefreshTransactionState(wgRec); - oldStId = wgRec.operationStatus; - } else { - oldTxState = { - major: TransactionMajorState.None, - }; - oldStId = 0; - } - const res = await f(wgRec, tx); - switch (res.type) { - case TransitionResultType.Transition: { - await tx.refreshGroups.put(res.rec); - await this.updateTransactionMeta(tx); - const newTxState = computeRefreshTransactionState(res.rec); - applyNotifyTransition(tx.notify, this.transactionId, { - oldTxState, - newTxState, - balanceEffect: BalanceEffect.PreserveUserVisible, - oldStId, - newStId: res.rec.operationStatus, - }); - return true; - } - case TransitionResultType.Delete: - await tx.refreshGroups.delete(this.refreshGroupId); - await this.updateTransactionMeta(tx); - applyNotifyTransition(tx.notify, this.transactionId, { - oldTxState, - newTxState: { - major: TransactionMajorState.None, - }, - // Deletion will affect balance - balanceEffect: BalanceEffect.Any, - newStId: -1, - oldStId, - }); - return true; - default: - return false; - } + async getRecordHandle( + tx: WalletDbReadWriteTransaction<["refreshGroups", "transactionsMeta"]>, + ): Promise< + [RefreshGroupRecord | undefined, RecordHandle<RefreshGroupRecord>] + > { + return getGenericRecordHandle<RefreshGroupRecord>( + this, + tx as any, + async () => tx.refreshGroups.get(this.refreshGroupId), + async (r) => { + await tx.refreshGroups.put(r); }, + async () => tx.refreshGroups.delete(this.refreshGroupId), + (r) => computeRefreshTransactionState(r), + (r) => r.operationStatus, + () => this.updateTransactionMeta(tx), ); } @@ -355,23 +292,26 @@ export class RefreshTransactionContext implements TransactionContext { } async suspendTransaction(): Promise<void> { - await this.transition({}, async (rec, tx) => { + await this.wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const [rec, h] = await this.getRecordHandle(tx); if (!rec) { - return TransitionResult.stay(); + return; } switch (rec.operationStatus) { case RefreshOperationStatus.Finished: case RefreshOperationStatus.Suspended: case RefreshOperationStatus.SuspendedRedenominate: case RefreshOperationStatus.Failed: - return TransitionResult.stay(); + break; case RefreshOperationStatus.Pending: { rec.operationStatus = RefreshOperationStatus.Suspended; - return TransitionResult.transition(rec); + await h.update(rec); + break; } case RefreshOperationStatus.PendingRedenominate: { rec.operationStatus = RefreshOperationStatus.SuspendedRedenominate; - return TransitionResult.transition(rec); + await h.update(rec); + break; } default: assertUnreachable(rec.operationStatus); @@ -385,23 +325,26 @@ export class RefreshTransactionContext implements TransactionContext { } async resumeTransaction(): Promise<void> { - await this.transition({}, async (rec, tx) => { + await this.wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const [rec, h] = await this.getRecordHandle(tx); if (!rec) { - return TransitionResult.stay(); + return; } switch (rec.operationStatus) { case RefreshOperationStatus.Finished: case RefreshOperationStatus.Failed: case RefreshOperationStatus.Pending: case RefreshOperationStatus.PendingRedenominate: - return TransitionResult.stay(); + break; case RefreshOperationStatus.Suspended: { rec.operationStatus = RefreshOperationStatus.Pending; - return TransitionResult.transition(rec); + await h.update(rec); + break; } case RefreshOperationStatus.SuspendedRedenominate: { rec.operationStatus = RefreshOperationStatus.PendingRedenominate; - return TransitionResult.transition(rec); + await h.update(rec); + break; } default: assertUnreachable(rec.operationStatus); @@ -410,21 +353,23 @@ export class RefreshTransactionContext implements TransactionContext { } async failTransaction(reason?: TalerErrorDetail): Promise<void> { - await this.transition({}, async (rec, tx) => { + await this.wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const [rec, h] = await this.getRecordHandle(tx); if (!rec) { - return TransitionResult.stay(); + return; } switch (rec.operationStatus) { case RefreshOperationStatus.Finished: case RefreshOperationStatus.Failed: - return TransitionResult.stay(); + break; case RefreshOperationStatus.Pending: case RefreshOperationStatus.PendingRedenominate: case RefreshOperationStatus.SuspendedRedenominate: case RefreshOperationStatus.Suspended: { rec.operationStatus = RefreshOperationStatus.Failed; rec.failReason = reason; - return TransitionResult.transition(rec); + await h.update(rec); + break; } default: assertUnreachable(rec.operationStatus);