Decompose the codec, document the session surface and record the review decisions
This commit is contained in:
@@ -154,6 +154,20 @@ exactly 140.
|
|||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
|
- **The published surface is frozen at what `src/index.ts` exports today.** `Session` is exported and
|
||||||
|
publicly constructible, which is why `SessionOptions` and `ReconnectOptions` are public too — that
|
||||||
|
is correct, not a leak, and it has been raised twice. The collaborators `session.ts` delegates to
|
||||||
|
(`Reassembler`, `PendingRequests`, `SendWindow`, `ReconnectLoop`, `LinkTimers`, `DlrMerger`,
|
||||||
|
`submitSms`) stay unpublished so they can be reshaped.
|
||||||
|
- **The sub-3.4 optional-parameter rule is a predicate, not a chokepoint.** `acceptsOptionalParams()`
|
||||||
|
is consulted by the library's own senders; `session.send({ tlvs })` is passed through as written,
|
||||||
|
because silently stripping a caller's explicit TLVs off a deliberately public low-level surface
|
||||||
|
would be worse than sending them. The guarantee is "what this library sends honours the rule",
|
||||||
|
never "the session cannot send optional parameters to an old peer".
|
||||||
|
- **Only the server feeds `peerInterfaceVersion`.** `acceptBind()` records what the peer declared;
|
||||||
|
the client never reads `sc_interface_version` out of its bind response, so a client session is
|
||||||
|
permissive. That is not a defect today — this library's ESME direction sends no TLVs at all — but
|
||||||
|
anyone adding a client-side TLV owes the other half of the feed.
|
||||||
- **The library speaks SMPP 3.4 on the wire, and `defs/` keeps the 5.0 tables as a superset.**
|
- **The library speaks SMPP 3.4 on the wire, and `defs/` keeps the 5.0 tables as a superset.**
|
||||||
Maintainer's call, 2026-08-26: 3.4 is what SMSCs actually run, while the wider tables let the codec
|
Maintainer's call, 2026-08-26: 3.4 is what SMSCs actually run, while the wider tables let the codec
|
||||||
parse and build whatever a peer sends. The declared version is an option on both `client()` and
|
parse and build whatever a peer sends. The declared version is an option on both `client()` and
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ await smpp.close(); // stop listening and close every live session
|
|||||||
| `tls` | `false` | A `tls.TlsOptions` object with your certificate and key. |
|
| `tls` | `false` | A `tls.TlsOptions` object with your certificate and key. |
|
||||||
| `idleTimeout` | `40000` | Drop a peer that has been silent this long. |
|
| `idleTimeout` | `40000` | Drop a peer that has been silent this long. |
|
||||||
| `maxReassembly` | `1000` | Incomplete multipart messages held per session. |
|
| `maxReassembly` | `1000` | Incomplete multipart messages held per session. |
|
||||||
| `reassemblyTimeout` | `300000` | How long an incomplete multipart message is held. |
|
| `reassemblyTimeout` | `300000` | How long a late segment can still join an incomplete message. |
|
||||||
| `responseTimeout`, `maxOutstanding`, `log`, `signal` | as for the client | |
|
| `responseTimeout`, `maxOutstanding`, `log`, `signal` | as for the client | |
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
@@ -218,6 +218,11 @@ const { err, pduObj } = await session.send({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`acceptsOptionalParams()` answers whether the peer declared SMPP 3.4 or later, which is the version
|
||||||
|
at and above which the spec allows optional parameters to be sent to it; `peerInterfaceVersion` is
|
||||||
|
the raw value it declared. The library's own senders consult the first before attaching a TLV — a
|
||||||
|
`send()` you build yourself is passed through as written, so consult it too when you attach TLVs.
|
||||||
|
|
||||||
## Working with PDUs directly
|
## Working with PDUs directly
|
||||||
|
|
||||||
The codec is exported, synchronous, and never throws — handy for inspecting captured traffic:
|
The codec is exported, synchronous, and never throws — handy for inspecting captured traffic:
|
||||||
|
|||||||
+3
-7
@@ -39,13 +39,9 @@ export default tseslint.config(
|
|||||||
rules: { 'max-lines': 'off' },
|
rules: { 'max-lines': 'off' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// The codec branches per wire type and per parameter; splitting it scatters the wire format
|
// ESLint counts every ?. and ?? in dlrFromPdu as a branch; the 19 is 26 lines of flat field resolution.
|
||||||
// across files instead. These two keep the ceiling they have today.
|
files: ['src/dlr.ts'],
|
||||||
files: ['src/dlr.ts', 'src/pdu.ts'],
|
rules: { complexity: ['error', 19] },
|
||||||
rules: {
|
|
||||||
complexity: ['error', 22],
|
|
||||||
'max-lines-per-function': ['error', { max: 75, skipBlankLines: true, skipComments: true }],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// ESC (0x1B) is the GSM 03.38 escape character, so it belongs in these patterns.
|
// ESC (0x1B) is the GSM 03.38 escape character, so it belongs in these patterns.
|
||||||
|
|||||||
+3
-2
@@ -120,6 +120,7 @@ export function parseReceipt(message: string): Receipt {
|
|||||||
export function dlrFromPdu(pduObj: PduObject): Dlr | undefined {
|
export function dlrFromPdu(pduObj: PduObject): Dlr | undefined {
|
||||||
const message = pduObj.params.short_message;
|
const message = pduObj.params.short_message;
|
||||||
const receipt = typeof message === 'string' ? parseReceipt(message) : undefined;
|
const receipt = typeof message === 'string' ? parseReceipt(message) : undefined;
|
||||||
|
const receiptState = receiptStates[receipt?.stat?.toUpperCase() ?? ''];
|
||||||
|
|
||||||
const tlvState = pduObj.tlvs.message_state?.tagValue;
|
const tlvState = pduObj.tlvs.message_state?.tagValue;
|
||||||
const tlvId = pduObj.tlvs.receipted_message_id?.tagValue;
|
const tlvId = pduObj.tlvs.receipted_message_id?.tagValue;
|
||||||
@@ -132,13 +133,13 @@ export function dlrFromPdu(pduObj: PduObject): Dlr | undefined {
|
|||||||
|
|
||||||
const statusMsg = typeof tlvState === 'number'
|
const statusMsg = typeof tlvState === 'number'
|
||||||
? constsById.MESSAGE_STATE?.[tlvState]
|
? constsById.MESSAGE_STATE?.[tlvState]
|
||||||
: receiptStates[receipt?.stat?.toUpperCase() ?? ''];
|
: receiptState;
|
||||||
|
|
||||||
if (statusMsg === undefined) return undefined;
|
if (statusMsg === undefined) return undefined;
|
||||||
|
|
||||||
const statusId = typeof tlvState === 'number'
|
const statusId = typeof tlvState === 'number'
|
||||||
? tlvState
|
? tlvState
|
||||||
: consts.MESSAGE_STATE[receiptStates[receipt?.stat?.toUpperCase() ?? ''] ?? 'UNKNOWN'];
|
: consts.MESSAGE_STATE[receiptState ?? 'UNKNOWN'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
doneDate: receiptDate(receipt?.doneDate),
|
doneDate: receiptDate(receipt?.doneDate),
|
||||||
|
|||||||
+120
-68
@@ -1,7 +1,7 @@
|
|||||||
import type { CommandName, PduParams, PduParamsInput } from './defs/commands.ts';
|
import type { CommandDefinition, CommandName, PduParams, PduParamsInput } from './defs/commands.ts';
|
||||||
import type { ErrorName } from './defs/errors.ts';
|
import type { ErrorName } from './defs/errors.ts';
|
||||||
import type { ParamValue } from './defs/types.ts';
|
import type { ParamValue } from './defs/types.ts';
|
||||||
import type { Result } from './result.ts';
|
import type { Result, VoidResult } from './result.ts';
|
||||||
import type { Tlv } from './defs/tlvs.ts';
|
import type { Tlv } from './defs/tlvs.ts';
|
||||||
import { cmds, commandNameById, isCommandName } from './defs/commands.ts';
|
import { cmds, commandNameById, isCommandName } from './defs/commands.ts';
|
||||||
import { consts } from './defs/constants.ts';
|
import { consts } from './defs/constants.ts';
|
||||||
@@ -79,46 +79,31 @@ function tagIdOf(name: string, input: TlvInput): Result<{ tagId: number }> {
|
|||||||
return { tagId };
|
return { tagId };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPdu(
|
/** Encoding a string short_message also settles data_coding and sm_length. */
|
||||||
cmdName: CommandName,
|
function resolveShortMessage(
|
||||||
cmdStatus: ErrorName,
|
|
||||||
seqNr: number,
|
|
||||||
params: Record<string, ParamValue | undefined>,
|
params: Record<string, ParamValue | undefined>,
|
||||||
tlvs: Record<string, TlvInput> | undefined,
|
): Record<string, ParamValue | undefined> {
|
||||||
): Result<{ buffer: Buffer }> {
|
const message = params.short_message;
|
||||||
const definition = cmds[cmdName];
|
|
||||||
|
|
||||||
if (!definition) {
|
if (typeof message !== 'string') return { ...params };
|
||||||
return { err: new Error(`Invalid cmdName: ${JSON.stringify(cmdName)}`) };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isErrorName(cmdStatus)) {
|
const dataCoding = params.data_coding;
|
||||||
return { err: new Error(`Invalid cmdStatus: ${JSON.stringify(cmdStatus)}`) };
|
const encoding = typeof dataCoding === 'number' ? encodingByDataCoding(dataCoding) : detect(message);
|
||||||
}
|
const encoded = encodeMessage(message, encoding);
|
||||||
|
|
||||||
if (!Number.isInteger(seqNr) || seqNr < 0 || seqNr > maxSeqNr) {
|
return {
|
||||||
return { err: new Error(`Invalid seqNr: ${JSON.stringify(seqNr)}`) };
|
...params,
|
||||||
}
|
data_coding: typeof dataCoding === 'number' ? dataCoding : consts.ENCODING[encoded.encoding],
|
||||||
|
short_message: encoded.buffer,
|
||||||
const resolved = { ...params };
|
sm_length: encoded.buffer.length,
|
||||||
const message = resolved.short_message;
|
};
|
||||||
|
}
|
||||||
// A string short_message is encoded here, which also settles data_coding and sm_length.
|
|
||||||
if (typeof message === 'string') {
|
|
||||||
const dataCoding = resolved.data_coding;
|
|
||||||
const encoding = typeof dataCoding === 'number'
|
|
||||||
? encodingByDataCoding(dataCoding)
|
|
||||||
: detect(message);
|
|
||||||
const encoded = encodeMessage(message, encoding);
|
|
||||||
|
|
||||||
resolved.short_message = encoded.buffer;
|
|
||||||
resolved.sm_length = encoded.buffer.length;
|
|
||||||
|
|
||||||
if (typeof dataCoding !== 'number') {
|
|
||||||
resolved.data_coding = consts.ENCODING[encoded.encoding];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function writeParams(
|
||||||
|
definition: CommandDefinition,
|
||||||
|
resolved: Record<string, ParamValue | undefined>,
|
||||||
|
cmdName: CommandName,
|
||||||
|
): Result<{ chunks: Buffer[] }> {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
for (const [name, type] of Object.entries(definition.params ?? {})) {
|
for (const [name, type] of Object.entries(definition.params ?? {})) {
|
||||||
@@ -139,6 +124,12 @@ function buildPdu(
|
|||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { chunks };
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeTlvs(tlvs: Record<string, TlvInput> | undefined): Result<{ chunks: Buffer[] }> {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
for (const [name, tlv] of Object.entries(tlvs ?? {})) {
|
for (const [name, tlv] of Object.entries(tlvs ?? {})) {
|
||||||
const tag = tagIdOf(name, tlv);
|
const tag = tagIdOf(name, tlv);
|
||||||
|
|
||||||
@@ -169,7 +160,39 @@ function buildPdu(
|
|||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = Buffer.concat(chunks);
|
return { chunks };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPdu(
|
||||||
|
cmdName: CommandName,
|
||||||
|
cmdStatus: ErrorName,
|
||||||
|
seqNr: number,
|
||||||
|
params: Record<string, ParamValue | undefined>,
|
||||||
|
tlvs: Record<string, TlvInput> | undefined,
|
||||||
|
): Result<{ buffer: Buffer }> {
|
||||||
|
const definition = cmds[cmdName];
|
||||||
|
|
||||||
|
if (!definition) {
|
||||||
|
return { err: new Error(`Invalid cmdName: ${JSON.stringify(cmdName)}`) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isErrorName(cmdStatus)) {
|
||||||
|
return { err: new Error(`Invalid cmdStatus: ${JSON.stringify(cmdStatus)}`) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(seqNr) || seqNr < 0 || seqNr > maxSeqNr) {
|
||||||
|
return { err: new Error(`Invalid seqNr: ${JSON.stringify(seqNr)}`) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const written = writeParams(definition, resolveShortMessage(params), cmdName);
|
||||||
|
|
||||||
|
if (written.err) return { err: written.err };
|
||||||
|
|
||||||
|
const writtenTlvs = writeTlvs(tlvs);
|
||||||
|
|
||||||
|
if (writtenTlvs.err) return { err: writtenTlvs.err };
|
||||||
|
|
||||||
|
const body = Buffer.concat([...written.chunks, ...writtenTlvs.chunks]);
|
||||||
const header = Buffer.alloc(16);
|
const header = Buffer.alloc(16);
|
||||||
|
|
||||||
header.writeUInt32BE(body.length + 16, 0);
|
header.writeUInt32BE(body.length + 16, 0);
|
||||||
@@ -223,22 +246,11 @@ function parseTlvs(
|
|||||||
return { offset, tlvs };
|
return { offset, tlvs };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolean; pduObj: PduObject }> {
|
function readParams(
|
||||||
const cmdLength = pdu.readUInt32BE(0);
|
cmdName: CommandName,
|
||||||
const cmdId = pdu.readUInt32BE(4);
|
pdu: Buffer,
|
||||||
const cmdName = commandNameById(cmdId);
|
trailingNull: boolean,
|
||||||
|
): Result<{ offset: number; params: Record<string, ParamValue> }> {
|
||||||
if (!cmdName) {
|
|
||||||
return { err: new Error(`Unknown PDU command id: ${String(cmdId)}`) };
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmdStatusId = pdu.readUInt32BE(8);
|
|
||||||
const seqNr = pdu.readUInt32BE(12);
|
|
||||||
|
|
||||||
if (seqNr > maxSeqNr) {
|
|
||||||
return { err: new Error(`Invalid seqNr, exceeds ${String(maxSeqNr)}: ${String(seqNr)}`) };
|
|
||||||
}
|
|
||||||
|
|
||||||
const params: Record<string, ParamValue> = {};
|
const params: Record<string, ParamValue> = {};
|
||||||
let offset = 16;
|
let offset = 16;
|
||||||
|
|
||||||
@@ -255,10 +267,34 @@ function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolea
|
|||||||
if (name === 'short_message' && trailingNull) offset++;
|
if (name === 'short_message' && trailingNull) offset++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parseTlvs(pdu, offset, cmdLength);
|
return { offset, params };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolean; pduObj: PduObject }> {
|
||||||
|
const cmdLength = pdu.readUInt32BE(0);
|
||||||
|
const cmdId = pdu.readUInt32BE(4);
|
||||||
|
const cmdName = commandNameById(cmdId);
|
||||||
|
|
||||||
|
if (!cmdName) {
|
||||||
|
return { err: new Error(`Unknown PDU command id: ${String(cmdId)}`) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmdStatusId = pdu.readUInt32BE(8);
|
||||||
|
const seqNr = pdu.readUInt32BE(12);
|
||||||
|
|
||||||
|
if (seqNr > maxSeqNr) {
|
||||||
|
return { err: new Error(`Invalid seqNr, exceeds ${String(maxSeqNr)}: ${String(seqNr)}`) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const read = readParams(cmdName, pdu, trailingNull);
|
||||||
|
|
||||||
|
if (read.err) return { err: read.err };
|
||||||
|
|
||||||
|
const parsed = parseTlvs(pdu, read.offset, cmdLength);
|
||||||
|
|
||||||
if (parsed.err) return { err: parsed.err };
|
if (parsed.err) return { err: parsed.err };
|
||||||
|
|
||||||
|
const params = read.params;
|
||||||
const message = params.short_message;
|
const message = params.short_message;
|
||||||
const esmClass = numberOr(params.esm_class, 0);
|
const esmClass = numberOr(params.esm_class, 0);
|
||||||
|
|
||||||
@@ -282,7 +318,7 @@ function parseOnce(pdu: Buffer, trailingNull: boolean): Result<{ aligned: boolea
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
function checkFraming(pdu: Buffer): VoidResult {
|
||||||
if (pdu.length < 16) {
|
if (pdu.length < 16) {
|
||||||
return { err: new Error(`PDU is too short, minimum is 16 octets, got ${String(pdu.length)}`) };
|
return { err: new Error(`PDU is too short, minimum is 16 octets, got ${String(pdu.length)}`) };
|
||||||
}
|
}
|
||||||
@@ -297,6 +333,14 @@ export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
|||||||
return { err: new Error(`cmd_length ${String(cmdLength)} exceeds the ${String(pdu.length)} octets given`) };
|
return { err: new Error(`cmd_length ${String(cmdLength)} exceeds the ${String(pdu.length)} octets given`) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
||||||
|
const framing = checkFraming(pdu);
|
||||||
|
|
||||||
|
if (framing.err) return { err: framing.err };
|
||||||
|
|
||||||
const plain = parseOnce(pdu, false);
|
const plain = parseOnce(pdu, false);
|
||||||
|
|
||||||
if (!plain.err && plain.aligned) return { pduObj: plain.pduObj };
|
if (!plain.err && plain.aligned) return { pduObj: plain.pduObj };
|
||||||
@@ -311,6 +355,25 @@ export function pduToObj(pdu: Buffer): Result<{ pduObj: PduObject }> {
|
|||||||
return { err: plain.err };
|
return { err: plain.err };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fields the response shares with the request are echoed back unless the caller overrode them. */
|
||||||
|
function echoParams(
|
||||||
|
respName: CommandName,
|
||||||
|
pdu: PduObject,
|
||||||
|
params: Record<string, ParamValue>,
|
||||||
|
): Record<string, ParamValue> {
|
||||||
|
const respParams: Record<string, ParamValue> = { ...params };
|
||||||
|
|
||||||
|
for (const name of Object.keys(cmds[respName]?.params ?? {})) {
|
||||||
|
const value = pdu.params[name];
|
||||||
|
|
||||||
|
if (respParams[name] === undefined && value !== undefined) {
|
||||||
|
respParams[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return respParams;
|
||||||
|
}
|
||||||
|
|
||||||
export function pduReturn(
|
export function pduReturn(
|
||||||
pdu: Buffer | PduObject,
|
pdu: Buffer | PduObject,
|
||||||
status: ErrorName = 'ESME_ROK',
|
status: ErrorName = 'ESME_ROK',
|
||||||
@@ -329,16 +392,5 @@ export function pduReturn(
|
|||||||
return { err: new Error(`"${pdu.cmdName}" has no response command`) };
|
return { err: new Error(`"${pdu.cmdName}" has no response command`) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const respParams: Record<string, ParamValue> = { ...params };
|
return buildPdu(respName, status, pdu.seqNr, echoParams(respName, pdu, params), tlvs);
|
||||||
|
|
||||||
// Fields the response shares with the request are echoed back unless the caller overrode them.
|
|
||||||
for (const name of Object.keys(cmds[respName]?.params ?? {})) {
|
|
||||||
const value = pdu.params[name];
|
|
||||||
|
|
||||||
if (respParams[name] === undefined && value !== undefined) {
|
|
||||||
respParams[name] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildPdu(respName, status, pdu.seqNr, respParams, tlvs);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import type { Result } from './result.ts';
|
|||||||
import type { Server as NetServer, Socket } from 'node:net';
|
import type { Server as NetServer, Socket } from 'node:net';
|
||||||
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
import type { Server as TlsServer, TlsOptions } from 'node:tls';
|
||||||
import { EventEmitter } from 'node:events';
|
import { EventEmitter } from 'node:events';
|
||||||
import { Session, bindCommands } from './session.ts';
|
import { Session, bindCommands, defaultSystemId } from './session.ts';
|
||||||
import { createServer as createNetServer } from 'node:net';
|
import { createServer as createNetServer } from 'node:net';
|
||||||
import { createServer as createTlsServer } from 'node:tls';
|
import { createServer as createTlsServer } from 'node:tls';
|
||||||
import { defaultInterfaceVersion } from './defs/constants.ts';
|
import { defaultInterfaceVersion } from './defs/constants.ts';
|
||||||
@@ -45,7 +45,7 @@ const defaults = {
|
|||||||
idleTimeout: 40_000,
|
idleTimeout: 40_000,
|
||||||
interfaceVersion: defaultInterfaceVersion,
|
interfaceVersion: defaultInterfaceVersion,
|
||||||
port: 2775,
|
port: 2775,
|
||||||
systemId: '',
|
systemId: defaultSystemId,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A listening SMPP server. Sessions arrive as `session` events; `close()` stops listening. */
|
/** A listening SMPP server. Sessions arrive as `session` events; `close()` stops listening. */
|
||||||
|
|||||||
+3
-1
@@ -72,6 +72,8 @@ export type SessionOptions = {
|
|||||||
systemId?: string | undefined;
|
systemId?: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultSystemId = '';
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
maxDelay: 30_000,
|
maxDelay: 30_000,
|
||||||
maxOutstanding: 10,
|
maxOutstanding: 10,
|
||||||
@@ -79,7 +81,7 @@ const defaults = {
|
|||||||
minDelay: 1000,
|
minDelay: 1000,
|
||||||
reassemblyTimeout: 300_000,
|
reassemblyTimeout: 300_000,
|
||||||
responseTimeout: 30_000,
|
responseTimeout: 30_000,
|
||||||
systemId: '',
|
systemId: defaultSystemId,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bindCommands: readonly string[] = [
|
export const bindCommands: readonly string[] = [
|
||||||
|
|||||||
+16
-13
@@ -84,20 +84,23 @@ describe('dlrFromPdu()', () => {
|
|||||||
assert.equal(dlr.doneDate?.toISOString(), '2025-08-25T14:31:00.000Z');
|
assert.equal(dlr.doneDate?.toISOString(), '2025-08-25T14:31:00.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('maps every spec status code back to its message state', () => {
|
test('maps every spec status code back to its message state and id', () => {
|
||||||
for (const [code, expected] of [
|
for (const [code, expected, statusId] of [
|
||||||
['DELIVRD', 'DELIVERED'],
|
['DELIVRD', 'DELIVERED', 2],
|
||||||
['UNDELIV', 'UNDELIVERABLE'],
|
['UNDELIV', 'UNDELIVERABLE', 5],
|
||||||
['EXPIRED', 'EXPIRED'],
|
['EXPIRED', 'EXPIRED', 3],
|
||||||
['DELETED', 'DELETED'],
|
['DELETED', 'DELETED', 4],
|
||||||
['ACCEPTD', 'ACCEPTED'],
|
['ACCEPTD', 'ACCEPTED', 6],
|
||||||
['REJECTD', 'REJECTED'],
|
['REJECTD', 'REJECTED', 8],
|
||||||
['ENROUTE', 'ENROUTE'],
|
['ENROUTE', 'ENROUTE', 1],
|
||||||
['UNKNOWN', 'UNKNOWN'],
|
['UNKNOWN', 'UNKNOWN', 7],
|
||||||
]) {
|
['delivrd', 'DELIVERED', 2],
|
||||||
const dlr = dlrFromPdu(deliverSm(`id:x stat:${String(code)} err:0`));
|
] as const) {
|
||||||
|
const dlr = dlrFromPdu(deliverSm(`id:x stat:${code} err:0`));
|
||||||
|
|
||||||
assert.equal(dlr?.statusMsg, expected);
|
assert.ok(dlr);
|
||||||
|
assert.equal(dlr.statusMsg, expected);
|
||||||
|
assert.equal(dlr.statusId, statusId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,21 @@ describe('encoding submit_sm', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('encodes a string short_message with the given data_coding instead of a detected one', () => {
|
||||||
|
const pdu = encode({
|
||||||
|
cmdName: 'submit_sm',
|
||||||
|
params: {
|
||||||
|
data_coding: 0x08,
|
||||||
|
destination_addr: '46709771337',
|
||||||
|
short_message: 'hi',
|
||||||
|
source_addr: '46701113311',
|
||||||
|
},
|
||||||
|
seqNr: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(pdu.subarray(-7).toString('hex'), '08000400680069');
|
||||||
|
});
|
||||||
|
|
||||||
test('keeps a UDH-carrying short_message as a buffer', () => {
|
test('keeps a UDH-carrying short_message as a buffer', () => {
|
||||||
const message = Buffer.concat([
|
const message = Buffer.concat([
|
||||||
Buffer.from('050003010101', 'hex'),
|
Buffer.from('050003010101', 'hex'),
|
||||||
|
|||||||
@@ -77,8 +77,10 @@ Every defect listed in the AGENTS.md table has a regression test naming the beha
|
|||||||
- [ ] **`session.ts` is 459 lines.** The one seam left in it is a socket-to-PDU transport, which
|
- [ ] **`session.ts` is 459 lines.** The one seam left in it is a socket-to-PDU transport, which
|
||||||
would move the deliberately public `sock` field out of `Session` or turn it into a getter —
|
would move the deliberately public `sock` field out of `Session` or turn it into a getter —
|
||||||
a public-surface change, so it waits for a decision.
|
a public-surface change, so it waits for a decision.
|
||||||
- [ ] **`buildPdu` and `dlrFromPdu` carry a complexity of 22.** `eslint.config.js` holds them at
|
- [ ] **Group the session's collaborators under `src/session/`.** Only `session.ts` imports
|
||||||
that ceiling rather than below the repo-wide 10, so neither can grow but neither shrinks.
|
`reassembly`, `dlr-merger`, `send-window`, `link-timers`, `reconnect-loop`, `pending-requests`
|
||||||
|
and `send-sms`, so the directory would make that boundary visible. Do it on the next
|
||||||
|
extraction out of `session.ts`, not as a move of its own.
|
||||||
- [ ] **`submit_multi` and the broadcast commands** encode and decode, but nothing exercises them
|
- [ ] **`submit_multi` and the broadcast commands** encode and decode, but nothing exercises them
|
||||||
end to end. The interop suite is the natural place.
|
end to end. The interop suite is the natural place.
|
||||||
- [ ] **Move to TypeScript 7** once `typescript-eslint` supports it; `renovate.json` pins TypeScript
|
- [ ] **Move to TypeScript 7** once `typescript-eslint` supports it; `renovate.json` pins TypeScript
|
||||||
|
|||||||
Reference in New Issue
Block a user