From 042a62be9790e7038282608ff1e1461f6e6d281b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 1 Sep 2026 18:02:15 +0200 Subject: [PATCH] Sweep held messages before each hold, so expiry needs no live timer --- src/held-messages.ts | 12 +++++++++++- test/session-extras.test.ts | 20 ++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/held-messages.ts b/src/held-messages.ts index 5eeb99e..57ddfde 100644 --- a/src/held-messages.ts +++ b/src/held-messages.ts @@ -6,6 +6,8 @@ import { IdleWaiters } from './idle-waiters.ts'; export type HeldMessagesOptions = { log: SmppLog; max: number; + /** Injected so expiry can be exercised without a wall clock. */ + now?: (() => number) | undefined; timeout: number; }; @@ -26,6 +28,7 @@ export class HeldMessages { constructor(options: HeldMessagesOptions) { this.held = new ExpiringGroups({ max: options.max, + now: options.now, onSweep: () => { this.sweep(); }, timeout: options.timeout, }); @@ -33,12 +36,18 @@ export class HeldMessages { this.max = options.max; } + get size(): number { + return this.held.size; + } + /** An application that answers no message at all may not grow this without end. */ hold(pduObjs: PduObject[]): void { const key = keyOf(pduObjs); if (key === undefined) return; + this.sweep(); + if (this.held.get(key)) { this.log.warn('heldMessages - replacing a message on a re-used sequence number', { seqNr: key }); } else if (this.held.full) { @@ -86,7 +95,8 @@ export class HeldMessages { this.log.warn('heldMessages - buffer full, dropping the oldest message', { max: this.max, seqNr }); } - private sweep(): void { + /** Drops every message past its deadline. Runs before each hold and on its own timer. */ + sweep(): void { const expired = this.held.takeExpired(); if (expired.length === 0) return; diff --git a/test/session-extras.test.ts b/test/session-extras.test.ts index 3bac0f8..f011bbc 100644 --- a/test/session-extras.test.ts +++ b/test/session-extras.test.ts @@ -879,7 +879,7 @@ describe('held message bounds', () => { return [heldPdu(seqNr)]; } - test('drops the message held longest rather than holding every one', async () => { + test('drops the message held longest rather than holding every one', () => { const held = new HeldMessages({ log: silentLog, max: 2, timeout: 10_000 }); const oldest = message(1); @@ -887,17 +887,25 @@ describe('held message bounds', () => { held.hold(message(2)); held.hold(message(3)); + assert.equal(held.size, 2); assert.equal(held.has(oldest), false); - assert.equal(await held.idle(1, undefined), 2); + + held.clear(); }); - test('gives up on a message the application never answers', async () => { - const held = new HeldMessages({ log: silentLog, max: 10, timeout: 20 }); + test('gives up on a message the application never answers', () => { + let now = 0; + const held = new HeldMessages({ log: silentLog, max: 10, now: () => now, timeout: 60 }); held.hold(message(1)); + now = 61; - assert.equal(await held.idle(1, undefined), 1); - assert.equal(await held.idle(1000, undefined), 0); + // The next message sweeps the one that expired, so only the new one is still waited for. + held.hold(message(2)); + + assert.equal(held.size, 1); + + held.clear(); }); // A receipt cannot be resent wholesale without duplicating the segments that landed, so sendDlr()