taler-typescript-core

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

commit 63854330be05f77652955fe4654713081855c7bf
parent 516ae509b978eb0b6fb1647c094d37d012e498a6
Author: Florian Dold <florian@dold.me>
Date:   Wed,  1 Jul 2026 15:29:09 +0200

wallet-core: implement traffic control dev experiment

Diffstat:
Mpackages/taler-wallet-core/src/dev-experiments.ts | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mpackages/taler-wallet-core/src/wallet.ts | 13++++++++++++-
2 files changed, 95 insertions(+), 5 deletions(-)

diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts @@ -31,7 +31,6 @@ import { Amounts, ContractTermsUtil, DenomLossEventType, - TalerDevExperimentUri, Duration, Logger, MerchantContractTermsV0, @@ -39,6 +38,7 @@ import { PeerContractTerms, RefreshReason, Result, + TalerDevExperimentUri, TalerError, TalerErrorCode, TalerPreciseTimestamp, @@ -173,6 +173,29 @@ export async function applyDevExperiment( wex.ws.devExperimentState.merchantDepositInsufficient = true; return; } + case "start-tc": { + const maybeFloatParam = (name: string) => { + const val = parsedUri.query?.get(name); + if (val == null) { + return undefined; + } + if (typeof val === "string") { + return Number.parseFloat(val); + } + throw Error("unsupported parameter"); + }; + wex.ws.devExperimentState.tc = { + delayResponseMs: maybeFloatParam("delay_resp"), + dropRequestProbability: maybeFloatParam("drop_req"), + dropResponseProbability: maybeFloatParam("drop_resp"), + fake500Probability: maybeFloatParam("fake_500"), + }; + return; + } + case "stop-tc": { + wex.ws.devExperimentState.tc = undefined; + return; + } case "start-fakeprotover": { const baseUrl = parsedUri.query?.get("base_url"); if (!baseUrl) { @@ -734,7 +757,10 @@ async function addFakeTx( } } -function mockResponseJson(resp: HttpResponse, respJson: any): HttpResponse { +function mockResponseReplacementJson( + resp: HttpResponse, + respJson: any, +): HttpResponse { const textEncoder = new TextEncoder(); return { requestMethod: resp.requestMethod, @@ -777,6 +803,19 @@ function mockInternalServerError(resp: HttpResponse): HttpResponse { }; } +function waitMs(tMs: number): Promise<void> { + return new Promise<void>((resolve) => { + setTimeout(() => resolve(), tMs); + }); +} + +function evalFailProbability(p: number | undefined): boolean { + if (p == null) { + return true; + } + return Math.random() >= p; +} + export class DevExperimentHttpLib implements HttpRequestLibrary { _isDevExperimentLib = true; underlyingLib: HttpRequestLibrary; @@ -815,7 +854,7 @@ export class DevExperimentHttpLib implements HttpRequestLibrary { logger.info(`replacing proto version with ${fakeSt.fakeVer}`); const respJson = await resp.json(); respJson.version = fakeSt.fakeVer; - return mockResponseJson(resp, respJson); + return mockResponseReplacementJson(resp, respJson); } } } else if (this.devExperimentState.blockPayResponse) { @@ -827,6 +866,46 @@ export class DevExperimentHttpLib implements HttpRequestLibrary { return mockInternalServerError(realResp); } } - return this.underlyingLib.fetch(url, opt); + if ( + evalFailProbability(this.devExperimentState.tc?.dropRequestProbability) + ) { + await waitMs(5000); + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Request timed out after ${5000} ms`, + ); + } + const resp = await this.underlyingLib.fetch(url, opt); + if (evalFailProbability(this.devExperimentState.tc?.fake500Probability)) { + return mockInternalServerError(resp); + } + if ( + evalFailProbability(this.devExperimentState.tc?.dropResponseProbability) + ) { + await waitMs(5000); + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: url, + requestMethod: method, + httpStatusCode: 0, + }, + `Request timed out after ${5000} ms`, + ); + } + // Delay response if requested + { + const delayMs = this.devExperimentState.tc?.delayResponseMs; + if (delayMs != null) { + logger.info(`delaying response by ${delayMs}ms`); + await waitMs(delayMs); + } + } + return resp; } } diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -2124,7 +2124,7 @@ export async function handleTestingWaitExchangeReady( await waitReadyExchange(wex, req.exchangeBaseUrl, { noBail: req.noBail, forceUpdate: req.forceUpdate, - waitAutoRefresh: req.waitAutoRefresh + waitAutoRefresh: req.waitAutoRefresh, }); return {}; } @@ -3197,6 +3197,17 @@ export interface DevExperimentState { /** Migration test for confirmPay */ flagConfirmPayNoWait?: boolean; + + tc?: { + dropRequestProbability?: number; + dropResponseProbability?: number; + /** + * Fake a 500 response *after* the request has been sent + * and the real response has been received. + */ + fake500Probability?: number; + delayResponseMs?: number; + }; } export class Cache<T> {