Document the finished API and make the test scripts portable across Node versions

This commit is contained in:
2026-08-25 17:02:57 +02:00
parent b671ebafb2
commit 54b73205ac
4 changed files with 173 additions and 211 deletions
+102 -16
View File
@@ -6,13 +6,13 @@ Successor to [larvitsmpp](https://www.npmjs.com/package/larvitsmpp) 0.4.0. The A
it has always been — connect, send an SMS, listen for delivery reports — with callbacks replaced by
promises and the rough edges taken off.
> **Status: not released.** This branch is a rewrite in progress and nothing here is published yet.
> The API below is the agreed design and is being implemented against it; see
> [todo.md](todo.md) for what is done and what is not. Use `larvitsmpp` 0.4.0 until 1.0.0 ships.
> **Not published yet.** The implementation is complete and tested, but 1.0.0 has not been released
> to npm. Until it is, use `larvitsmpp` 0.4.0. Remaining release steps are in [todo.md](todo.md).
## Requirements
Node 18 or later.
Node 18 or later. The only runtime dependency is
[`@larvit/log`](https://www.npmjs.com/package/@larvit/log).
## Install
@@ -68,6 +68,46 @@ const { err: sendErr, smsIds } = await session.sendSms({
});
```
### Client options
Every one is optional.
| Option | Default | |
| --- | --- | --- |
| `host` / `port` | `localhost` / `2775` | Where to connect. |
| `username` / `password` | `user` / `pass` | Bind credentials (`system_id` and `password`). |
| `bindType` | `transceiver` | `transceiver`, `transmitter` or `receiver`. |
| `systemType`, `addressRange`, `addrTon`, `addrNpi`, `interfaceVersion` | `''`, `''`, `0`, `0`, `0x50` | The remaining bind fields, for operators that require them. |
| `tls` | `false` | `true` for defaults, or a `tls.ConnectionOptions` object for a private CA or a client certificate. |
| `enquireLinkInterval` | `20000` | How often to send `enquire_link` on a quiet link. |
| `responseTimeout` | `30000` | How long to wait for a response before giving up on it. |
| `maxOutstanding` | `10` | Requests allowed on the wire at once; further sends queue. |
| `reconnect` | off | `{ minDelay, maxDelay }` to re-bind automatically after a drop, with exponential backoff. |
| `log` | silent | A `@larvit/log` instance. |
| `signal` | — | An `AbortSignal` that cancels connecting and tears the session down. |
### Sending
```javascript
await session.sendSms({
dlr: true, // ask for a delivery report
encoding: 'UCS2', // override the automatic choice
flash: false,
from: 'MyBrand', // alphanumeric -> TON 5, digits -> TON 1
message: 'Hello world',
scheduleDeliveryTime: new Date(Date.now() + 3600_000),
to: '46709771337',
validityPeriod: 3600, // seconds, or a Date
}, { signal }); // optional per-call AbortSignal
```
Messages too long for one SMS are split automatically and sent as a concatenated message. You get
one id per segment:
```javascript
const { err, pduObjs, smsIds } = await session.sendSms({ from, message, to });
```
## Server
The simplest possible server — no authentication, listening on port 2775:
@@ -112,12 +152,25 @@ smpp.on('session', session => {
});
});
await smpp.close();
console.log(smpp.port); // the port actually bound, useful when 0 was requested
await smpp.close(); // stop listening and close every live session
```
`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`.
### Server options
| Option | Default | |
| --- | --- | --- |
| `host` / `port` | all interfaces / `2775` | Where to listen. Pass `0` for any free port. |
| `authenticate` | accept everything | `({ password, session, systemId, systemType }) => false \| { userData }`, sync or async. |
| `tls` | `false` | `true`, or a `tls.TlsOptions` object with your certificate and key. |
| `idleTimeout` | `40000` | Drop a peer that has been silent this long. |
| `maxReassembly` | `1000` | Incomplete multipart messages held per session. |
| `reassemblyTimeout` | `300000` | How long an incomplete multipart message is held. |
| `responseTimeout`, `maxOutstanding`, `log`, `signal` | as for the client | |
## Errors
Nothing in this library throws. Every fallible call returns a result carrying an optional `err`, so
@@ -140,10 +193,10 @@ is exactly what this library promises not to do.
| Event | Fires when |
| --- | --- |
| `sms` | An SMS arrives. Carries `sendResp()` and `sendDlr()`. |
| `sms` | An SMS arrives, reassembled if it was multipart. Carries `sendResp()` and `sendDlr()`. |
| `dlr` | A delivery report arrives, one per segment. |
| `messageDlr` | Every segment of a multipart message has been reported on. |
| `close` | The socket closed. |
| `close` | The connection closed. |
| `reconnected` | The client re-bound after a drop (only with `reconnect` configured). |
| `sessionError` | Something failed on a live session. |
| `data` | Raw bytes arrived on the socket. |
@@ -153,22 +206,48 @@ is exactly what this library promises not to do.
### Methods
`sendSms()`, `send()`, `sendReturn()`, `unbind()` and `close()`. `send()` reaches any of the 33 SMPP
commands the codec knows, not just the four the session handles natively.
commands the codec knows, not just the four the session handles natively:
```javascript
const { err, pduObj } = await session.send({
cmdName: 'query_sm',
params: { message_id: smsId },
});
```
## Working with PDUs directly
The codec is exported, synchronous, and never throws — handy for inspecting captured traffic:
```javascript
import { isCommand, objToPdu, pduToObj } from '@larvit/smpp';
const { err, pduObj } = pduToObj(buffer);
if (err) return;
if (isCommand(pduObj, 'submit_sm')) {
pduObj.params.destination_addr; // typed as a string
}
```
The spec tables are exported both individually (`cmds`, `consts`, `encodings`, `errors`, `tlvs`,
`types`, and the matching `*ById` maps) and grouped as `defs`.
## Migrating from larvitsmpp 0.4.0
- **The package is now `@larvit/smpp`** and is ESM only. `require()` no longer works.
- **Callbacks are gone.** `client`, `server`, `sendSms`, `sendResp` and `sendDlr` are all promises
resolving to a result object with an optional `err`. Nothing rejects.
- **`server()` resolves once, when it is listening**, and gives you a handle with `close()` and a
`session` event. It no longer calls your callback once per incoming connection.
- **`checkuserpass` is now `authenticate`**, takes `{ password, systemId }` and returns `false` or
`{ userData }`.
- **`server()` resolves once, when it is listening**, and gives you a handle with `close()`, `port`
and a `session` event. It no longer calls your callback once per incoming connection.
- **`checkuserpass` is now `authenticate`**, takes `{ password, session, systemId, systemType }` and
returns `false` or `{ userData }`.
- **Renamed options:** `enqLinkTiming``enquireLinkInterval`, server `timeout``idleTimeout`.
- **`larvitsmpp.utils` is gone.** Its contents are named exports: `bitCount`, `decodeMessage`,
`encodeMessage`, `objToPdu`, `pduReturn`, `pduToObj`, `smppDate`, `smppTime`, `splitMessage`. The PDU codec is
synchronous and returns `{ err, pduObj }` / `{ err, buffer }`.
- **`pduObj.isResp()` is now the standalone `isResp(pduObj)`.**
`encodeMessage`, `objToPdu`, `pduReturn`, `pduToObj`, `smppDate`, `smppTime`, `splitMessage`. The
PDU codec is synchronous and returns `{ err, pduObj }` / `{ err, buffer }`.
- **`pduObj.isResp()` is now the standalone `isResp(pduObj)`**, and `pduObj.cmdStatus` is `undefined`
for a status code the library does not know, with the raw number in `pduObj.cmdStatusId`.
- **`defs.filters` is gone.** It was declared on every command and TLV but never invoked, so it did
nothing. SMPP time formatting, the one part worth keeping, is exported as `smppTime`.
- **The `error` event is `sessionError`** (and `serverError` on the server handle).
@@ -196,6 +275,12 @@ have worked around any of these, remove the workaround:
reported the full length, so it went out corrupt. In UCS2 that is any message ending in a
character like 一 (U+4E00), which made the bug routine for CJK text.
- Short or malformed PDUs threw out of the codec instead of being reported as a parse failure.
- Binds now declare `interface_version` 0x50. 0.4.0 declared 0x00, because the default in its own
command table was never applied. Pass `interfaceVersion: 0x34` to bind as SMPP 3.4.
- `submit_multi` was missing its `sm_length` field, so its `short_message` never round-tripped.
The corrected framing is cross-checked against [node-smpp](https://github.com/farhadi/node-smpp), an
independent implementation, in both directions and over a live session.
## Development
@@ -203,8 +288,9 @@ Everything runs in the container; nothing is installed on the host.
```bash
docker compose run --rm node npm install
docker compose run --rm node npm test # lint, typecheck and tests
docker compose run --rm node npm test # lint, typecheck and tests
docker compose run --rm node npm run build
docker compose run --rm node npm run test:compiled # what CI runs on older Node versions
```
Tests are TypeScript and run directly under Node's type stripping, so there is no build step in the