taler-typescript-core

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

commit 1c7787e59c127624bd320de333bf79b24de4ede2
parent e156a42b70758b2dca5fe7dfe4e3f74a446ef47f
Author: Florian Dold <florian@dold.me>
Date:   Tue, 14 Jul 2026 22:35:17 +0200

wallet-core: move progressToken logic to separate file, cancellation/retry implementation WIP

Diffstat:
Mpackages/taler-wallet-core/src/exchanges.ts | 2+-
Apackages/taler-wallet-core/src/progress.ts | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/wallet.ts | 103+++++++------------------------------------------------------------------------
3 files changed, 186 insertions(+), 96 deletions(-)

diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -172,6 +172,7 @@ import { PeerPullCreditTransactionContext } from "./pay-peer-pull-credit.js"; import { PeerPullDebitTransactionContext } from "./pay-peer-pull-debit.js"; import { PeerPushCreditTransactionContext } from "./pay-peer-push-credit.js"; import { PeerPushDebitTransactionContext } from "./pay-peer-push-debit.js"; +import { ProgressContext } from "./progress.js"; import { RecoupTransactionContext, createRecoupGroup } from "./recoup.js"; import { RefreshTransactionContext, createRefreshGroup } from "./refresh.js"; import { @@ -184,7 +185,6 @@ import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js"; import { InternalWalletState, LegacyWalletTxHandle, - ProgressContext, WalletExecutionContext, walletExchangeClient, } from "./wallet.js"; diff --git a/packages/taler-wallet-core/src/progress.ts b/packages/taler-wallet-core/src/progress.ts @@ -0,0 +1,177 @@ +/* + This file is part of GNU Taler + (C) 2026 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 { + CancellationToken, + CancelProgressTokenRequest, + Duration, + EmptyObject, + Logger, + NotificationType, + safeStringifyException, +} from "@gnu-taler/taler-util"; +import { WalletExecutionContext } from "./wallet.js"; + +const logger = new Logger("progress.ts"); + +export interface ProgressContext { + progressToken: string; + operation: string; + finished: boolean; + cts: CancellationToken.Source | undefined; + onRetryNow?: () => Promise<void>; +} + +function getProgressKey(args: { + operation: string; + progressToken: string; +}): string { + return `${args.operation}:${encodeURIComponent(args.progressToken)}`; +} + +export async function withMaybeProgressContext<T>( + wex: WalletExecutionContext, + op: string, + tok: string | undefined, + onRetryNow: undefined | (() => Promise<void>), + f: (pc?: ProgressContext) => Promise<T>, +): Promise<T> { + if (tok != null) { + const pc: ProgressContext = { + operation: op, + progressToken: tok, + finished: false, + onRetryNow, + cts: wex.cts, + }; + const key = getProgressKey(pc); + try { + if (!wex.cts) { + logger.warn(`no cancellation source for ${op}:${tok}`); + } + wex.ws.progressMap.set(key, pc); + emitProgressPhaseNotifications(wex, pc).catch((e) => { + logger.warn(safeStringifyException(e)); + }); + const res = await f(pc); + pc.finished = true; + return res; + } finally { + wex.ws.progressMap.delete(key); + // Clean up progress token registration + } + } else { + return await f(undefined); + } +} + +/** + * Emit progress notifications until the request is finished/cancelled. + */ +async function emitProgressPhaseNotifications( + wex: WalletExecutionContext, + pc: ProgressContext, +): Promise<void> { + await wex.ws.timerGroup.resolveAfter( + Duration.fromSpec({ + seconds: 5, + }), + ); + if (wex.cancellationToken.isCancelled || pc.finished) { + return; + } + wex.ws.notify({ + type: NotificationType.RequestProgressPhase, + operation: pc.operation, + progressToken: pc.progressToken, + phase: "delayed", + }); + await wex.ws.timerGroup.resolveAfter( + Duration.fromSpec({ + seconds: 5, + }), + ); + if (wex.cancellationToken.isCancelled || pc.finished) { + return; + } + wex.ws.notify({ + type: NotificationType.RequestProgressPhase, + operation: pc.operation, + progressToken: pc.progressToken, + phase: "stalled", + }); +} + +async function emitProgressPhaseRetryNotification( + wex: WalletExecutionContext, + pc: ProgressContext, +): Promise<void> { + await wex.ws.timerGroup.resolveAfter( + Duration.fromSpec({ + seconds: 3, + }), + ); + if (wex.cancellationToken.isCancelled || pc.finished) { + return; + } + wex.ws.notify({ + type: NotificationType.RequestProgressPhase, + operation: pc.operation, + progressToken: pc.progressToken, + phase: "retryable", + }); +} + +export async function handleCancelProgressToken( + wex: WalletExecutionContext, + req: CancelProgressTokenRequest, +): Promise<EmptyObject> { + const key = getProgressKey({ + operation: req.operation, + progressToken: req.progressToken, + }); + const pc = wex.ws.progressMap.get(key); + if (!pc) { + logger.warn("unable to cancel progress token, not found"); + return {}; + } + pc.cts?.cancel(); + return {}; +} + +export async function handleRetryProgressTokenNow( + wex: WalletExecutionContext, + req: CancelProgressTokenRequest, +): Promise<EmptyObject> { + const key = getProgressKey({ + operation: req.operation, + progressToken: req.progressToken, + }); + const pc = wex.ws.progressMap.get(key); + if (!pc) { + logger.warn("unable to retry progress token, not found"); + return {}; + } + if (!pc.onRetryNow) { + logger.warn("unable to retry progress token, no retry logic defined"); + return {}; + } + await pc.onRetryNow(); + emitProgressPhaseRetryNotification(wex, pc).catch((e) => { + logger.warn(safeStringifyException(e)); + }); + return {}; +} diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -238,6 +238,7 @@ import { codecForListDiscountsRequest, codecForListExchangesRequest, codecForListSubscriptionsRequest, + codecForMailboxBaseUrl, codecForMailboxConfiguration, codecForPrepareBankIntegratedWithdrawalRequest, codecForPreparePayRequest, @@ -259,7 +260,6 @@ import { codecForSharePaymentRequest, codecForStartExchangeWalletKycRequest, codecForStartRefundQueryRequest, - codecForMailboxBaseUrl, codecForSuspendTransaction, codecForTestPayArgs, codecForTestingCorruptWithdrawalCoinSelRequest, @@ -404,6 +404,7 @@ import { } from "./pay-peer-push-debit.js"; import { checkPayForTemplate } from "./pay-template.js"; import { fillDefaults } from "./preset-exchanges.js"; +import { handleCancelProgressToken, handleRetryProgressTokenNow, ProgressContext, withMaybeProgressContext } from "./progress.js"; import { AfterCommitInfo, DbAccess, @@ -536,12 +537,6 @@ export function walletMerchantClient( ); } -export interface ProgressContext { - progressToken: string; - operation: string; - finished: boolean; -} - export const EXCHANGE_COINS_LOCK = "exchange-coins-lock"; export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock"; @@ -900,77 +895,6 @@ async function recoverStoredBackup( logger.info(`import done`); } -/** - * Emit progress notifications until the request is finished/cancelled. - */ -async function emitProgressPhaseNotifications( - wex: WalletExecutionContext, - pc: ProgressContext, -): Promise<void> { - await wex.ws.timerGroup.resolveAfter( - Duration.fromSpec({ - seconds: 5, - }), - ); - if (wex.cancellationToken.isCancelled || pc.finished) { - return; - } - wex.ws.notify({ - type: NotificationType.RequestProgressPhase, - operation: pc.operation, - progressToken: pc.progressToken, - phase: "delayed", - }); - await wex.ws.timerGroup.resolveAfter( - Duration.fromSpec({ - seconds: 5, - }), - ); - if (wex.cancellationToken.isCancelled || pc.finished) { - return; - } - wex.ws.notify({ - type: NotificationType.RequestProgressPhase, - operation: pc.operation, - progressToken: pc.progressToken, - phase: "stalled", - }); -} - -async function withMaybeProgressContext<T>( - wex: WalletExecutionContext, - op: string, - tok: string | undefined, - f: (pc?: ProgressContext) => Promise<T>, -): Promise<T> { - if (tok != null) { - const key = `${op}:${encodeURIComponent(tok)}`; - try { - const pc: ProgressContext = { - operation: op, - progressToken: tok, - finished: false, - }; - if (wex.cts) { - wex.ws.progressCancellationMap.set(key, wex.cts); - } else { - logger.warn(`no cancellation source for ${op}:${tok}`); - } - emitProgressPhaseNotifications(wex, pc).catch((e) => { - logger.warn(safeStringifyException(e)); - }); - const res = await f(pc); - pc.finished = true; - return res; - } finally { - wex.ws.progressCancellationMap.delete(key); - // Clean up progress token registration - } - } else { - return await f(undefined); - } -} - async function handlePrepareWithdrawExchange( wex: WalletExecutionContext, req: PrepareWithdrawExchangeRequest, @@ -985,6 +909,9 @@ async function handlePrepareWithdrawExchange( wex, "prepareWithdrawExchange", req.progressToken, + async () => { + logger.warn("retryNow for prepareWithdrawExchange not implemented"); + }, async (pc) => { const exchange = await fetchFreshExchange(wex, exchangeBaseUrl, { noBail: pc != null, @@ -2238,20 +2165,6 @@ export async function handleGetPerformanceStats( }; } -export async function handleCancelRequest( - wex: WalletExecutionContext, - req: CancelProgressTokenRequest, -): Promise<EmptyObject> { - return {}; -} - -export async function handleRetryRequestNow( - wex: WalletExecutionContext, - req: CancelProgressTokenRequest, -): Promise<EmptyObject> { - return {}; -} - interface HandlerWithValidator<Tag extends WalletApiOperation> { codec: Codec<WalletCoreRequestType<Tag>>; handler: ( @@ -2263,11 +2176,11 @@ interface HandlerWithValidator<Tag extends WalletApiOperation> { const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { [WalletApiOperation.CancelProgressToken]: { codec: codecForCancelProgressToken(), - handler: handleCancelRequest, + handler: handleCancelProgressToken, }, [WalletApiOperation.RetryProgressTokenNow]: { codec: codecForRetryProgressTokenNowRequest(), - handler: handleRetryRequestNow, + handler: handleRetryProgressTokenNow, }, [WalletApiOperation.TestingWaitExchangeReady]: { codec: codecForTestingWaitExchangeReadyRequest(), @@ -3465,7 +3378,7 @@ export class InternalWalletState { clientCancellationMap: Map<string, CancellationToken.Source> = new Map(); - progressCancellationMap: Map<string, CancellationToken.Source> = new Map(); + progressMap: Map<string, ProgressContext> = new Map(); longpollQueue = new LongpollQueue();