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
+23 -21
View File
@@ -16,12 +16,14 @@ import { PduTransport } from './pdu-transport.ts';
import { PendingRequests, UnansweredError } from './pending-requests.ts';
import { ReconnectLoop } from './reconnect-loop.ts';
import { SendWindow } from './send-window.ts';
import { leftOf } from './idle-waiters.ts';
import { errorFrom } from './error-from.ts';
import { optionalParamsMinVersion } from './defs/constants.ts';
import { bindCarries, bindCommands, defaultSystemId, defaults } from './session-options.ts';
import { isResp, objToPdu, pduReturn } from './pdu.ts';
import { silentLog } from './log.ts';
import { submitSms } from './send-sms.ts';
import { submitSms, unsent } from './send-sms.ts';
import { ConcatReference } from './udh.ts';
export type {
CloseOptions,
@@ -64,6 +66,7 @@ export class Session extends EventEmitter<SessionEvents> {
peerInterfaceVersion: number | undefined = undefined;
userData: unknown = undefined;
private readonly concatReference = new ConcatReference();
private readonly dlrMerger: DlrMerger;
private readonly gate: LinkGate;
private readonly incoming: IncomingRequests;
@@ -75,7 +78,6 @@ export class Session extends EventEmitter<SessionEvents> {
private readonly window: SendWindow;
private closed = false;
private concatReference = 0;
private draining = false;
private ended = false;
@@ -129,6 +131,7 @@ export class Session extends EventEmitter<SessionEvents> {
maxReassembly: options.maxReassembly,
onRequest: options.onRequest,
reassemblyTimeout: options.reassemblyTimeout,
sendHeld: input => this.sendThrough(input, {}),
session: this,
smsIdFormat: options.smsIdFormat,
systemId: options.systemId,
@@ -166,7 +169,15 @@ export class Session extends EventEmitter<SessionEvents> {
}
/** Sends a request and resolves with the peer's response. */
async send(
send(input: PduObjectInput, options: SendOptions = {}): Promise<Result<{ pduObj: PduObject }>> {
// A drain on a live link. A link that is down is the gate's answer, which says closed instead.
if (this.draining && !this.linkDown()) return Promise.resolve({ err: new Error('Session is shutting down') });
return this.sendThrough(input, options);
}
/** The same path without that refusal, which a receipt for a held message has to take. */
private async sendThrough(
input: PduObjectInput,
options: SendOptions = {},
): Promise<Result<{ pduObj: PduObject }>> {
@@ -199,9 +210,6 @@ export class Session extends EventEmitter<SessionEvents> {
return new Error(`Use sendReturn() for responses, not send(): ${input.cmdName}`);
}
// A drain on a live link. A link that is down is the gate's answer, which says closed instead.
if (this.draining && !this.linkDown()) return new Error('Session is shutting down');
// Before the gate and the window, or an aborted call waits for what it will never use.
if (options.signal?.aborted === true) return abortedBeforeSend();
@@ -239,17 +247,12 @@ export class Session extends EventEmitter<SessionEvents> {
async sendSms(sms: SendSmsOptions, options: SendOptions = {}): Promise<SendSmsResult> {
if (!this.bindAllows('submit_sm')) {
return {
err: new Error('A receiver-bound session does not carry submit_sm'),
pduObjs: [],
smsIds: [],
unanswered: 0,
};
return unsent(new Error('A receiver-bound session does not carry submit_sm'));
}
const sent = await submitSms({
log: this.log,
reference: this.nextConcatReference(),
reference: this.concatReference.next(),
respIdNotation: this.options.smsIdFormat?.submitResp,
send: input => this.send(input, options),
}, sms);
@@ -379,7 +382,7 @@ export class Session extends EventEmitter<SessionEvents> {
return { result: answered.err ? { err: new UnansweredError(answered.err) } : answered, retryOnNextLink: false };
}
/** Stops new sends and waits out the ones already issued. */
/** Stops new sends and waits out the messages we hold and the requests already issued. */
private async drain(signal: AbortSignal | undefined): Promise<VoidResult> {
this.reconnectLoop?.stop();
this.draining = true;
@@ -387,11 +390,16 @@ export class Session extends EventEmitter<SessionEvents> {
if (this.linkDown()) return {};
const timeout = this.options.shutdownTimeout ?? defaults.shutdownTimeout;
const unfinished = await this.window.idle(timeout, signal);
const deadline = timeout > 0 ? Date.now() + timeout : 0;
// Answering a message can put a receipt on the wire; nothing on the wire produces a message.
const messages = await this.incoming.drain(timeout, signal);
const unfinished = await this.window.idle(leftOf(deadline), signal);
// The window empties on a teardown too, which settles everything the link was carrying.
if (this.linkDown()) return { err: new Error('The session closed before the drain finished') };
if (messages.err) return messages;
if (unfinished === 0) return {};
this.log.warn('session - shutting down with requests unfinished', { timeout, unfinished });
@@ -433,12 +441,6 @@ export class Session extends EventEmitter<SessionEvents> {
return this.reconnectLoop !== undefined && !this.reconnectLoop.isStopped();
}
private nextConcatReference(): number {
this.concatReference = this.concatReference >= 255 ? 1 : this.concatReference + 1;
return this.concatReference;
}
private onData(chunk: Buffer): void {
this.emit('data', chunk);
this.resetTimers();