11 KiB
todo.md
Remaining work for the @larvit/smpp 1.0.0 rewrite. Read AGENTS.md first — the hard
rules there constrain every item below.
How to resume
docker compose run --rm node npm install
docker compose run --rm node npm test
Work top to bottom. Each task states what "done" means. Tests come before implementation: write the
failing test for the behaviour, then implement until it passes. The 0.4.0 implementation is on the
master branch of this repository and is the reference for protocol behaviour — read it, do not
copy its structure.
The agreed API
Settled with the maintainer before implementation started. Do not change any of it without asking; it is the contract the tasks below implement.
import { client, server, consts, defs, errors, objToPdu, pduToObj } from '@larvit/smpp';
// --- client ---------------------------------------------------------------
const { err, session } = await client({
addrNpi: 0, // optional, bind field
addrTon: 0, // optional, bind field
addressRange: '', // optional, bind field
bindType: 'transceiver', // 'transceiver' | 'transmitter' | 'receiver'
enquireLinkInterval: 20000,
host: 'localhost',
interfaceVersion: 0x50, // optional, bind field
log, // @larvit/log LogInt, silent by default
maxOutstanding: 10, // in-flight requests before sends queue
password: 'pass',
port: 2775,
reconnect: { maxDelay: 30000, minDelay: 1000 }, // optional, opt-in
responseTimeout: 30000,
signal, // optional AbortSignal
systemType: '', // optional, bind field
tls: false, // boolean or tls.ConnectionOptions
username: 'user',
});
const { err, pduObjs, smsIds } = await session.sendSms({
dlr: false,
encoding: 'UCS2', // optional, overrides auto-detection
flash: false,
from: 'MyBrand', // alphanumeric -> TON 5, digits -> TON 1
message: 'Hello world',
scheduleDeliveryTime: undefined, // optional
to: '46709771337',
validityPeriod: undefined, // optional
}, { signal }); // optional per-call AbortSignal
await session.unbind();
await session.close();
// --- server ---------------------------------------------------------------
const { err, server: smpp } = await server({
authenticate: async ({ password, systemId }) => {
if (systemId !== 'foo' || password !== 'bar') return false;
return { userData: { userId: 123 } }; // attached to session.userData
},
idleTimeout: 40000,
log,
maxReassembly: 1000,
port: 2775,
reassemblyTimeout: 300000,
signal,
tls: false,
});
smpp.port; // resolved port, useful when 0 was requested
await smpp.close();
smpp.on('session', session => {
session.on('sms', async sms => {
const { err } = await sms.sendResp(); // defaults to ESME_ROK
if (sms.dlr) await sms.sendDlr('DELIVERED'); // defaults to DELIVERED
});
});
Events
| Emitter | Event | Payload |
|---|---|---|
| server | session |
Session |
| server | serverError |
Error |
| session | sms |
Sms (live handle: sendResp(), sendDlr()) |
| session | dlr |
one per segment — { doneDate?, errorCode?, smsId, statusId, statusMsg } |
| session | messageDlr |
merged once all segments of a message are accounted for — adds segments and the worst status across them |
| session | sessionError |
Error |
| session | close |
— |
| session | reconnected |
— (only when reconnect is configured) |
| session | data |
Buffer |
| session | incomingPdu |
Buffer |
| session | incomingPduObj |
PduObject |
Rules the API follows
- Never throws. Everything fallible resolves to
{ err?, … }. See AGENTS.md rule 1. - Named exports only. No default export.
defsstays as a grouped export alongside the individual tables (cmds,consts,encodings,errors,tlvs,types, and the*ByIdmaps). utilsis gone. Its contents are named exports:bitCount,decodeMessage,encodeMessage,objToPdu,pduReturn,pduToObj,smppDate,smppTime,splitMessage.defs.filtersis gone — it never did anything.smppTimereplaces the one useful part.- The PDU codec is synchronous and returns
{ err?, pduObj? }/{ err?, buffer? }. - Low-level surface stays public, including
session.sock,session.send()andsession.sendReturn()— that is how the other 29 commands are reached. pduObj.isResp()is now the standalone exportisResp(pduObj); PDU objects are pure data.
Tasks
1. Definition tables — src/defs/
All done, with tests in test/encodings.test.ts, test/types.test.ts and test/commands.test.ts.
constants.ts—consts+constsById.errors.ts— allESME_*codes pluserrorsById,isErrorName,errorNameById.types.ts— wire types.readis bounds-checked and reportsbytesRead, so no caller has to re-derive a length that could disagree with what was written.sizeandwritevalidate their input and return results.encodings.ts— GSM 03.38, LATIN1, UCS2, FLASH,detectandencodingByDataCoding.iconv-liteis gone; Buffer does it natively.tlvs.ts— 67 TLV definitions,tlvsById, and the two aliases.commands.ts— all 33 commands, wire-ordered, pluscmdsByIdand thePduParams<C>/PduParamsInput<C>per-command types.filters.ts— not ported.defs.filterswas declared on commands and TLVs but never invoked anywhere in 0.4.0. The one piece that is genuinely needed, SMPP time formatting, is task 2'ssmppTime.
2. Message helpers — src/message.ts
Done, tested in test/message.test.ts. Segments are 153 GSM / 67 UCS2 characters, smppDate is
UTC and one-based, decodeMessage goes through encodingByDataCoding, and splitMessage takes the
concatenation reference as an argument rather than owning a module-global counter.
bitCount,encodeMessage,decodeMessage,smppDate,splitMessage,smppTime.
3. PDU codec — src/pdu.ts
Done, tested in test/pdu.test.ts — which includes the two byte-for-byte comparisons against 0.4.0's
output and the real captured SMSC PDUs from its suite.
pduToObj,objToPdu,pduReturn,isResp,isCommand.- Trailing-NULL retry kept, now an explicit second parse rather than a side effect of a length function.
cmdLengthguarded bymaxPduLength(1 MiB) before anything is allocated.objToPdunarrowsparamsto the named command.pduToObjreturns loosely-typed params — the command is only known at runtime — andisCommand(pduObj, 'submit_sm')narrows them.
The encoder measures each field, allocates exactly that, and writes into it, so a size/write
disagreement surfaces as an error instead of a corrupt PDU. That class of bug is what the trailing
NULL defect was.
4. Session — src/session.ts
- Framing off the socket. Replace 0.4.0's
Buffer.concatper chunk with an accumulating reader — concatenating the whole queue on everydataevent is quadratic. - Sequence numbers, wrapping at 2147483646.
send()withresponseTimeout,maxOutstandingwindowing and a queue, andAbortSignal. Listeners must be removed on every exit path — timeout, abort and close included.sendReturn(),unbind(),close().submit_smhandling, including long-SMS reassembly withmaxReassemblyand a realreassemblyTimeouttimer.deliver_smhandling: prefer the TLVs, fall back to parsing the receipt text (id:… sub:001 dlvrd:1 submit date:… done date:… stat:DELIVRD err:0 text:…).- Per-segment
dlrplus mergedmessageDlr. enquire_linkandunbindhandling.
5. Sms handle — src/sms.ts
sendResp(status?)andsendDlr(status?)returning result DTOs.- Generated
message_idvalues are UUID v7. - Fix:
sendDlremits the 7-character spec status (UNDELIV, notUNDELIVERABLE).
6. Client and server — src/client.ts, src/server.ts
client()— bind bybindType, all bind fields optional with 0.4.0's defaults.- Fix: real TLS via
tls.connect()/tls.createServer(), accepting an options object. - Opt-in reconnect with exponential backoff between
minDelayandmaxDelay, re-binding and emittingreconnected. Decide and document what happens to in-flight sends across a reconnect. server()— resolves once when listening, exposesport,close()and thesessionevent.authenticatehook;userDataattached to the session.idleTimeoutdrops silent peers.
7. Public surface — src/index.ts
- Named exports only, matching "The agreed API" above exactly.
8. Tests
Port the 0.4.0 suite (test/01_encodings.js … test/04_session.js on master) and extend it. Every
defect in the AGENTS.md table needs a test that fails against 0.4.0 behaviour.
- Encodings, wire types, PDU round-trips, return PDUs, message sizing and splitting.
- Sessions: bind, auth success and failure, simple SMS, long SMS, DLRs, and the Kannel capture
with the large UDH from
test/04_session.js. - Interop: add the reference
smpppackage (farhadi/node-smpp) as a dev dependency and assert both directions — our encoder against its parser, its encoder against our parser. This is how the wire-format fixes are validated; the maintainer asked specifically for proof that the corrected framing is right rather than differently wrong. - Reassembly bounds, response timeouts, abort, and the send window.
9. CI and release
.github/workflows/test.yaml— lint, typecheck and test on Node 18, 20, 22 and 24. The 18/20 legs need the tests compiled first, since type stripping needs Node 22.18+..github/workflows/release.yaml—npm publish --provenanceon av*tag.renovate.json.- Delete nothing from
master; this branch simply does not carry.travis.yml.
10. Release chores
- Fill in README usage docs as each part lands — right now the README documents the target API, and it must not claim anything that is not implemented and tested.
npm deprecate larvitsmpppointing at@larvit/smpponce 1.0.0 is published. Maintainer's call to run it; not something CI should do.
Open questions
None outstanding. Everything above was settled with the maintainer; anything genuinely new that comes up during implementation should be asked rather than assumed.