Wait out the messages the application holds before shutting a session down

This commit is contained in:
2026-09-01 13:58:28 +02:00
parent a99e1227ac
commit 3bcea108e3
12 changed files with 263 additions and 79 deletions
+32 -4
View File
@@ -1,9 +1,11 @@
import type { DlrMerger } from './dlr-merger.ts';
import type { OnRequest } from './session-options.ts';
import type { PduObject } from './pdu.ts';
import type { PduObject, PduObjectInput } from './pdu.ts';
import type { Result, VoidResult } from './result.ts';
import type { Session } from './session.ts';
import type { SmppLog } from './log.ts';
import type { SmsIdFormat } from './sms-id.ts';
import { HeldMessages } from './held-messages.ts';
import { Reassembler, decodeSegments } from './reassembly.ts';
import { bindCommands, defaults } from './session-options.ts';
import { concatInfo } from './udh.ts';
@@ -19,6 +21,8 @@ export type IncomingRequestsOptions = {
maxReassembly?: number | undefined;
onRequest?: OnRequest | undefined;
reassemblyTimeout?: number | undefined;
/** Past the drain gate: a receipt answering a message the shutdown is still waiting for. */
sendHeld: (input: PduObjectInput) => Promise<Result<{ pduObj: PduObject }>>;
session: Session;
smsIdFormat?: SmsIdFormat | undefined;
systemId?: string | undefined;
@@ -27,9 +31,11 @@ export type IncomingRequestsOptions = {
/** Everything the peer asks of a session: messages, receipts, links and the answers to them. */
export class IncomingRequests {
private readonly dlrMerger: DlrMerger;
private readonly held = new HeldMessages();
private readonly log: SmppLog;
private readonly onRequest: OnRequest | undefined;
private readonly reassembler: Reassembler;
private readonly sendHeld: IncomingRequestsOptions['sendHeld'];
private readonly session: Session;
private readonly smsIdFormat: SmsIdFormat;
private readonly systemId: string;
@@ -44,6 +50,7 @@ export class IncomingRequests {
maxOctets: options.maxOctets,
timeout: options.reassemblyTimeout ?? defaults.reassemblyTimeout,
});
this.sendHeld = options.sendHeld;
this.session = options.session;
this.smsIdFormat = options.smsIdFormat ?? {};
this.systemId = options.systemId ?? defaults.systemId;
@@ -82,11 +89,23 @@ export class IncomingRequests {
}
}
/** Drops the segments of every message that never became whole. */
/** Drops the segments of every message that never became whole, and of every one still held. */
clear(): void {
this.held.clear();
this.reassembler.clear();
}
/** Waits out the messages the application still holds, and says how many it never answered. */
async drain(timeout: number, signal?: AbortSignal): Promise<VoidResult> {
const unanswered = await this.held.idle(timeout, signal);
if (unanswered === 0) return {};
this.log.warn('session - shutting down with messages unanswered', { timeout, unanswered });
return { err: new Error(`Shut down with ${String(unanswered)} message(s) unanswered`) };
}
private async unhandled(pduObj: PduObject): Promise<void> {
if (bindCommands.includes(pduObj.cmdName)) {
this.log.info('session - bind on an already bound session', { cmdName: pduObj.cmdName });
@@ -139,12 +158,21 @@ export class IncomingRequests {
if (!first) return;
this.session.emit('sms', createSms({
const sms = createSms({
from: paramText(first.params.source_addr),
message: decodeSegments(pduObjs),
pduObjs,
session: this.session,
to: paramText(first.params.destination_addr),
}));
}, {
// A turn later, so a listener sending its receipt straight after the response still holds.
onAnswered: () => { setImmediate(() => { this.held.release(pduObjs); }); },
send: this.sendHeld,
});
this.held.hold(pduObjs);
// A message nobody took is not work a shutdown can wait for.
if (!this.session.emit('sms', sms)) this.held.release(pduObjs);
}
}