taler-typescript-core

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

commit b79efd6f9786ac59089571d18ce88fd262b0b0a1
parent fddafb9fc9b6934b8840e1487eef1c28dd6b1502
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 11:02:41 +0200

util: settle pending waits when a timer group is stopped

Stopping a group clears the armed handles and after() then drops any new
timer, so every wait in flight at shutdown stayed suspended forever and
every later one hung.

Diffstat:
Mpackages/taler-util/src/timer.ts | 32++++++++++++++++++++++++--------
1 file changed, 24 insertions(+), 8 deletions(-)

diff --git a/packages/taler-util/src/timer.ts b/packages/taler-util/src/timer.ts @@ -145,6 +145,13 @@ export class TimerGroup { private readonly timerMap: { [index: number]: TimerHandle } = {}; + /** + * Resolvers of pending resolveAfter() calls. Stopping the group clears the + * armed timers, so without these the waits they belong to would never + * settle and their callers would stay suspended forever. + */ + private readonly pendingWaits = new Set<(fired: boolean) => void>(); + private idGen = 1; constructor(public readonly timerApi: TimerAPI) {} @@ -158,20 +165,32 @@ export class TimerGroup { this.timerMap[x].clear(); delete this.timerMap[x]; } + const waits = [...this.pendingWaits]; + this.pendingWaits.clear(); + for (const w of waits) { + w(false); + } } resolveAfter( delayMs: Duration, cancellationToken?: CancellationToken, ): Promise<boolean> { + if (this.stopped) { + return Promise.resolve(false); + } return new Promise<boolean>((resolve, reject) => { let unregisterCt: (() => void) | undefined; let timerHandle: TimerHandle | undefined; + const settle = (fired: boolean): void => { + this.pendingWaits.delete(settle); + unregisterCt?.(); + timerHandle?.clear(); + resolve(fired); + }; + this.pendingWaits.add(settle); if (cancellationToken != null) { - unregisterCt = cancellationToken.onCancelled(() => { - timerHandle?.clear(); - resolve(false); - }); + unregisterCt = cancellationToken.onCancelled(() => settle(false)); } // onCancelled fires synchronously for an already-cancelled token, so // the promise may have settled; a timer scheduled now is an orphan. @@ -179,10 +198,7 @@ export class TimerGroup { return; } if (delayMs.d_ms !== "forever") { - timerHandle = this.after(delayMs.d_ms, () => { - unregisterCt?.(); - resolve(true); - }); + timerHandle = this.after(delayMs.d_ms, () => settle(true)); } }); }