taler-typescript-core

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

commit ea4d0e3eb96997c9e036c4cc3d7e7edec3fbec39
parent f6fa69844bc790b6dbec56f88848254870b1326e
Author: Florian Dold <dold@taler.net>
Date:   Mon, 20 Jul 2026 20:43:16 +0200

util: fix duration parsing and release fired timers

fromPrettyString dropped a trailing number that carried no unit, so "1h 30"
parsed as one hour.

TimerGroup kept the handle of a one-shot timer after it had fired, so the map
grew for the lifetime of the group, and resolveAfter scheduled a timer even
for an already-cancelled token.

Diffstat:
Mpackages/taler-util/src/time.ts | 9+++++++++
Mpackages/taler-util/src/timer.ts | 14+++++++++++---
2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts @@ -229,6 +229,7 @@ export namespace Duration { let dMs = 0; let currentNum = ""; let parsingNum = true; + let sawUnit = false; for (let i = 0; i < s.length; i++) { const cc = s.charCodeAt(i); if (cc >= "0".charCodeAt(0) && cc <= "9".charCodeAt(0)) { @@ -272,9 +273,17 @@ export namespace Duration { } else { throw Error("invalid duration, unsupported unit"); } + sawUnit = true; currentNum = ""; parsingNum = true; } + // A number is only added when its unit is seen; anything left has none. + if (currentNum !== "") { + throw Error("invalid duration, number without unit"); + } + if (dMs === 0 && !sawUnit) { + throw Error("invalid duration, no unit given"); + } return { d_ms: dMs, }; diff --git a/packages/taler-util/src/timer.ts b/packages/taler-util/src/timer.ts @@ -173,6 +173,11 @@ export class TimerGroup { resolve(false); }); } + // onCancelled fires synchronously for an already-cancelled token, so + // the promise may have settled; a timer scheduled now is an orphan. + if (cancellationToken?.isCancelled) { + return; + } if (delayMs.d_ms !== "forever") { timerHandle = this.after(delayMs.d_ms, () => { unregisterCt?.(); @@ -187,11 +192,14 @@ export class TimerGroup { logger.warn("dropping timer since timer group is stopped"); return nullTimerHandle; } - const h = this.timerApi.after(delayMs, callback); const myId = this.idGen++; - this.timerMap[myId] = h; - const tm = this.timerMap; + // A one-shot timer is dead once it has fired; drop it from the map. + const h = this.timerApi.after(delayMs, () => { + delete tm[myId]; + callback(); + }); + this.timerMap[myId] = h; return { clear() {