Answer every segment of an inbound concatenated message as it arrives (#83)
* Regression tests for answering every inbound segment as it arrives * Answer every segment of an inbound concatenated message as it arrives * Record the segment-by-segment answer in AGENTS.md and README * Regression tests for an empty message_id on deliver_sm_resp * Answer a deliver_sm with the empty message_id SMPP 3.4 makes it * Record Jasmin's refusal of a deliver_sm_resp message_id * Mark the Jasmin multipart deadlock fixed * Regression tests for the architecture review's findings * Give the segment id notation an owner, and every segment a status * Correct what the ids reach and what a lost group tells the application * Regression tests for a group lost to its own octet overrun * Report a group lost to its own overrun, and refuse by the command it arrived on * Keep the docs true about what a segment is answered with * Count only the answered segments of a group lost to an overrun * Report only what a lost group cost, and say which cap bit
This commit is contained in:
+42
-10
@@ -7,6 +7,7 @@ import { UnansweredError } from './unanswered-error.ts';
|
||||
import { consts } from './defs/constants.ts';
|
||||
import { receiptCodes, transientStates } from './dlr.ts';
|
||||
import { smppDate } from './message.ts';
|
||||
import { respIdParams, segmentId } from './sms-id.ts';
|
||||
import { uuidv7 } from './uuid.ts';
|
||||
|
||||
/** `pduObjs` holds what the peer took, so a partial failure names what is already receipted. */
|
||||
@@ -28,6 +29,11 @@ export type SendRespOptions = {
|
||||
* every segment's PDU.
|
||||
*/
|
||||
export type Sms = {
|
||||
/**
|
||||
* Whether the peer was answered as the message's segments arrived, which is what a concatenated
|
||||
* message needs and a segment count cannot tell you. `sendResp()` then writes nothing.
|
||||
*/
|
||||
answeredOnArrival: boolean;
|
||||
dlr: boolean;
|
||||
flash: boolean;
|
||||
from: string;
|
||||
@@ -35,16 +41,22 @@ export type Sms = {
|
||||
pduObjs: PduObject[];
|
||||
/** Sends a delivery report back to the sender. Defaults to DELIVERED. */
|
||||
sendDlr: (status?: MessageState) => Promise<SendDlrResult>;
|
||||
/** Answers every segment. Part of the protocol, not optional. Defaults to ESME_ROK. */
|
||||
/**
|
||||
* Answers the message, and says the application is done with it. A concatenated message was
|
||||
* answered segment by segment as it arrived, so there it only releases a shutdown's wait and
|
||||
* refuses an `smsId` or a refusing `status`. Part of the protocol, not optional.
|
||||
*/
|
||||
sendResp: (options?: SendRespOptions) => Promise<VoidResult>;
|
||||
session: Session;
|
||||
/** The id `sendResp()` was given, or a generated UUID v7. */
|
||||
/** The id the segments were answered with, the id `sendResp()` was given, or a generated UUID v7. */
|
||||
readonly smsId: string;
|
||||
submitTime: Date;
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type SmsInput = {
|
||||
/** The id base the segments were already answered with; absent leaves the answer to `sendResp()`. */
|
||||
answeredAs?: string | undefined;
|
||||
from: string;
|
||||
message: string;
|
||||
pduObjs: PduObject[];
|
||||
@@ -59,25 +71,23 @@ export type SmsHandlers = {
|
||||
send: (input: PduObjectInput) => Promise<Result<{ pduObj: PduObject }>>;
|
||||
};
|
||||
|
||||
/** Each segment of a multipart message gets its own message_id, as a separate submit_sm must. */
|
||||
function segmentId(smsId: string, index: number, total: number): string {
|
||||
return total === 1 ? smsId : `${smsId}-${String(index + 1)}`;
|
||||
}
|
||||
|
||||
export function createSms(input: SmsInput, handlers: SmsHandlers): Sms {
|
||||
const first = input.pduObjs[0];
|
||||
const registered = first?.params.registered_delivery;
|
||||
const dataCoding = first?.params.data_coding;
|
||||
const answered = { smsId: uuidv7() };
|
||||
const answered = { smsId: input.answeredAs ?? uuidv7() };
|
||||
|
||||
const sms: Sms = {
|
||||
answeredOnArrival: input.answeredAs !== undefined,
|
||||
dlr: typeof registered === 'number' && registered !== 0,
|
||||
flash: typeof dataCoding === 'number' && (dataCoding & 0xF0) === 0x10,
|
||||
from: input.from,
|
||||
message: input.message,
|
||||
pduObjs: input.pduObjs,
|
||||
sendDlr: status => sendDlr(sms, handlers.send, status),
|
||||
sendResp: options => sendResp(sms, answered, options ?? {}, handlers),
|
||||
sendResp: options => (input.answeredAs === undefined
|
||||
? sendResp(sms, answered, options ?? {}, handlers)
|
||||
: alreadyAnswered(options ?? {}, handlers)),
|
||||
session: input.session,
|
||||
get smsId(): string {
|
||||
return answered.smsId;
|
||||
@@ -89,6 +99,28 @@ export function createSms(input: SmsInput, handlers: SmsHandlers): Sms {
|
||||
return sms;
|
||||
}
|
||||
|
||||
/** Every segment went out answered, so the call is what the shutdown waits for and nothing else. */
|
||||
function alreadyAnswered(
|
||||
options: SendRespOptions,
|
||||
handlers: Pick<SmsHandlers, 'onAnswered'>,
|
||||
): Promise<VoidResult> {
|
||||
if (options.smsId !== undefined) {
|
||||
return Promise.resolve({
|
||||
err: new Error('This message\'s id was fixed when its first segment arrived; read sms.smsId'),
|
||||
});
|
||||
}
|
||||
|
||||
if (options.status !== undefined && options.status !== 'ESME_ROK') {
|
||||
return Promise.resolve({
|
||||
err: new Error('Its segments were answered as they arrived, so there is nothing left to refuse; refuse a segment from onRequest instead'),
|
||||
});
|
||||
}
|
||||
|
||||
handlers.onAnswered();
|
||||
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
async function sendResp(
|
||||
sms: Sms,
|
||||
answered: { smsId: string },
|
||||
@@ -115,7 +147,7 @@ async function sendResp(
|
||||
const results = await Promise.all(sms.pduObjs.map((pduObj, index) => sms.session.sendReturn(
|
||||
pduObj,
|
||||
options.status ?? 'ESME_ROK',
|
||||
{ message_id: segmentId(answered.smsId, index, total) },
|
||||
respIdParams(pduObj.cmdName, segmentId(answered.smsId, index, total)),
|
||||
)));
|
||||
|
||||
const failure = results.find(result => result.err);
|
||||
|
||||
Reference in New Issue
Block a user