commit e156a42b70758b2dca5fe7dfe4e3f74a446ef47f
parent df3dcd2f7b47d94ed2c2fd2b9e81024f26e8c5ec
Author: Florian Dold <florian@dold.me>
Date: Tue, 14 Jul 2026 21:40:55 +0200
wallet-core: emit progress phase notifications
Diffstat:
2 files changed, 55 insertions(+), 2 deletions(-)
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
@@ -46,6 +46,7 @@ export enum NotificationType {
TaskObservabilityEvent = "task-observability-event",
RequestObservabilityEvent = "request-observability-event",
RequestProgress = "request-progress",
+ RequestProgressPhase = "request-progress-phase",
}
export interface ErrorInfoSummary {
@@ -225,6 +226,13 @@ export interface RequestProgressNotification {
retryCounter: number;
}
+export interface RequestProgressPhaseNotification {
+ type: NotificationType.RequestProgressPhase;
+ progressToken: string;
+ operation: string;
+ phase: "delayed" | "stalled" | "retryable";
+}
+
export enum ObservabilityEventType {
HttpFetchStart = "http-fetch-start",
HttpFetchFinishError = "http-fetch-finish-error",
@@ -379,4 +387,5 @@ export type WalletNotification =
| TaskProgressNotification
| RequestObservabilityEventNotification
| IdleNotification
- | RequestProgressNotification;
+ | RequestProgressNotification
+ | RequestProgressPhaseNotification;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -539,6 +539,7 @@ export function walletMerchantClient(
export interface ProgressContext {
progressToken: string;
operation: string;
+ finished: boolean;
}
export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
@@ -899,6 +900,43 @@ 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,
@@ -911,13 +949,19 @@ async function withMaybeProgressContext<T>(
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}`);
}
- return await f(pc);
+ 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