Write a transient state as an intermediate delivery notification
This commit is contained in:
@@ -302,6 +302,16 @@ Grouped by what each one constrains.
|
|||||||
transient rather than as saying nothing, maintainer's call, 2026-09-03, since the codec refuses a
|
transient rather than as saying nothing, maintainer's call, 2026-09-03, since the codec refuses a
|
||||||
zero-length integer TLV and so an absent one cannot land there.
|
zero-length integer TLV and so an absent one cannot land there.
|
||||||
|
|
||||||
|
- **A transient state goes out as an intermediate delivery notification (0x20), every other state as
|
||||||
|
a delivery receipt (0x04).** Appendix B makes a receipt's `stat` the message's final status, so
|
||||||
|
0x04 over `ENROUTE` emits the two disagreeing spellings of finality the reading side above has to
|
||||||
|
reconcile, and goal 3 has our own senders write the marker 3.4 defines. `sendDlr()` takes the list
|
||||||
|
from `transientStates` in `dlr.ts`, the same one the reader uses, so the two cannot drift.
|
||||||
|
Rejected: 0x04 for every state, for the sake of a peer keyed to that marker alone — such a peer
|
||||||
|
then reads a transient report as a final one, which is goal 2's wrong answer handed to the peer
|
||||||
|
instead of to the application. A 0.4.0 client is unaffected either way: its bind declares 0x00, so
|
||||||
|
it is sent no TLVs and refuses the receipt regardless.
|
||||||
|
|
||||||
- **`smsIdFormat` names a notation per place, and normalisation never reaches inside a `<base>-<n>`
|
- **`smsIdFormat` names a notation per place, and normalisation never reaches inside a `<base>-<n>`
|
||||||
id.** An SMSC may answer `submit_sm_resp` in hex and write the receipt's `id:` in decimal, so one
|
id.** An SMSC may answer `submit_sm_resp` in hex and write the receipt's `id:` in decimal, so one
|
||||||
transform over both sides cannot make them equal. `submitResp` covers the `receipted_message_id`
|
transform over both sides cannot make them equal. `submitResp` covers the `receipted_message_id`
|
||||||
|
|||||||
@@ -232,7 +232,8 @@ await smpp.close(); // stop listening, then drain and close every live sess
|
|||||||
```
|
```
|
||||||
|
|
||||||
`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
|
`sendDlr` accepts `SCHEDULED`, `ENROUTE`, `DELIVERED`, `EXPIRED`, `DELETED`, `UNDELIVERABLE`,
|
||||||
`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`.
|
`ACCEPTED`, `UNKNOWN`, `REJECTED` and `SKIPPED`. `SCHEDULED` and `ENROUTE` go out as intermediate
|
||||||
|
delivery notifications (`esm_class` 0x20), the rest as delivery receipts (0x04).
|
||||||
|
|
||||||
A message whose `data_coding` says 8-bit binary arrives as Latin-1, so `Buffer.from(sms.message,
|
A message whose `data_coding` says 8-bit binary arrives as Latin-1, so `Buffer.from(sms.message,
|
||||||
'latin1')` gives you back the original octets.
|
'latin1')` gives you back the original octets.
|
||||||
@@ -429,6 +430,9 @@ have worked around any of these, remove the workaround:
|
|||||||
- LATIN1 (`data_coding` 0x03) was silently decoded as ASCII, corrupting the message.
|
- LATIN1 (`data_coding` 0x03) was silently decoded as ASCII, corrupting the message.
|
||||||
- Delivery receipt dates were a month off, and the status field read `UNDELIVERABLE` where the spec
|
- Delivery receipt dates were a month off, and the status field read `UNDELIVERABLE` where the spec
|
||||||
defines the 7-character `UNDELIV`.
|
defines the 7-character `UNDELIV`.
|
||||||
|
- Every receipt went out as `esm_class` 0x04, which SMPP 3.4 defines as the report of a message's
|
||||||
|
final state. A receipt for a transient state — `sendDlr('ENROUTE')` — is now marked 0x20, the
|
||||||
|
intermediate delivery notification.
|
||||||
- `flash: true` discarded UCS2, mangling flash messages containing non-GSM characters.
|
- `flash: true` discarded UCS2, mangling flash messages containing non-GSM characters.
|
||||||
- The multipart reference counter was shared by every session in the process.
|
- The multipart reference counter was shared by every session in the process.
|
||||||
- `tls: true` never performed a handshake, so the connection was not actually encrypted.
|
- `tls: true` never performed a handshake, so the connection was not actually encrypted.
|
||||||
|
|||||||
+1
-1
@@ -131,7 +131,7 @@ export function parseReceipt(message: string): Receipt {
|
|||||||
type MessageType = 'intermediate' | 'other' | 'receipt' | 'unmarked';
|
type MessageType = 'intermediate' | 'other' | 'receipt' | 'unmarked';
|
||||||
|
|
||||||
/** SMPP 3.4 Appendix B lists every other receipt state as final. */
|
/** SMPP 3.4 Appendix B lists every other receipt state as final. */
|
||||||
const transientStates: MessageState[] = ['ENROUTE', 'SCHEDULED'];
|
export const transientStates: MessageState[] = ['ENROUTE', 'SCHEDULED'];
|
||||||
|
|
||||||
/** Written by the far-end SME, not by the MC reporting on a message we submitted. */
|
/** Written by the far-end SME, not by the MC reporting on a message we submitted. */
|
||||||
const smeMessageTypes: number[] = [
|
const smeMessageTypes: number[] = [
|
||||||
|
|||||||
+4
-2
@@ -5,7 +5,7 @@ import type { Result, VoidResult } from './result.ts';
|
|||||||
import type { Session } from './session.ts';
|
import type { Session } from './session.ts';
|
||||||
import { UnansweredError } from './unanswered-error.ts';
|
import { UnansweredError } from './unanswered-error.ts';
|
||||||
import { consts } from './defs/constants.ts';
|
import { consts } from './defs/constants.ts';
|
||||||
import { receiptCodes } from './dlr.ts';
|
import { receiptCodes, transientStates } from './dlr.ts';
|
||||||
import { smppDate } from './message.ts';
|
import { smppDate } from './message.ts';
|
||||||
import { uuidv7 } from './uuid.ts';
|
import { uuidv7 } from './uuid.ts';
|
||||||
|
|
||||||
@@ -192,7 +192,9 @@ async function sendDlr(
|
|||||||
cmdName: 'deliver_sm',
|
cmdName: 'deliver_sm',
|
||||||
params: {
|
params: {
|
||||||
destination_addr: sms.from,
|
destination_addr: sms.from,
|
||||||
esm_class: consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
|
esm_class: transientStates.includes(status)
|
||||||
|
? consts.ESM_CLASS.INTERMEDIATE_DELIVERY
|
||||||
|
: consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
|
||||||
short_message: receiptText(sms, smsId, status),
|
short_message: receiptText(sms, smsId, status),
|
||||||
source_addr: sms.to,
|
source_addr: sms.to,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { closeAfter, closeListenerAfter } from './teardown.ts';
|
|||||||
import { consts } from '../src/defs/constants.ts';
|
import { consts } from '../src/defs/constants.ts';
|
||||||
import { errors } from '../src/defs/errors.ts';
|
import { errors } from '../src/defs/errors.ts';
|
||||||
import { objToPdu } from '../src/pdu.ts';
|
import { objToPdu } from '../src/pdu.ts';
|
||||||
import { paramText } from '../src/defs/types.ts';
|
import { paramNumber, paramText } from '../src/defs/types.ts';
|
||||||
import { server } from '../src/server.ts';
|
import { server } from '../src/server.ts';
|
||||||
import { silentLog } from '../src/log.ts';
|
import { silentLog } from '../src/log.ts';
|
||||||
import { submitSms } from '../src/send-sms.ts';
|
import { submitSms } from '../src/send-sms.ts';
|
||||||
@@ -179,8 +179,12 @@ describe('merged delivery reports', () => {
|
|||||||
|
|
||||||
const merged = once<MessageDlr>(resolve => { session.on('messageDlr', resolve); });
|
const merged = once<MessageDlr>(resolve => { session.on('messageDlr', resolve); });
|
||||||
const perSegment: boolean[] = [];
|
const perSegment: boolean[] = [];
|
||||||
|
const markers: (number | undefined)[] = [];
|
||||||
|
|
||||||
session.on('dlr', dlr => perSegment.push(dlr.intermediate));
|
session.on('dlr', (dlr, pduObj) => {
|
||||||
|
perSegment.push(dlr.intermediate);
|
||||||
|
markers.push(paramNumber(pduObj.params.esm_class, 0));
|
||||||
|
});
|
||||||
|
|
||||||
const [sms] = await Promise.all([
|
const [sms] = await Promise.all([
|
||||||
incoming.then(async received => {
|
incoming.then(async received => {
|
||||||
@@ -205,6 +209,14 @@ describe('merged delivery reports', () => {
|
|||||||
assert.equal(report.statusMsg, 'DELIVERED');
|
assert.equal(report.statusMsg, 'DELIVERED');
|
||||||
assert.equal(report.segments.length, 3);
|
assert.equal(report.segments.length, 3);
|
||||||
assert.deepEqual(perSegment, [true, true, true, false, false, false]);
|
assert.deepEqual(perSegment, [true, true, true, false, false, false]);
|
||||||
|
const notification = consts.ESM_CLASS.INTERMEDIATE_DELIVERY;
|
||||||
|
const receipt = consts.ESM_CLASS.MC_DELIVERY_RECEIPT;
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
markers,
|
||||||
|
[notification, notification, notification, receipt, receipt, receipt],
|
||||||
|
'a transient state goes out under the marker the spec gives it',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reports the worst status across the segments', async t => {
|
test('reports the worst status across the segments', async t => {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ Rules the API follows:
|
|||||||
| Every runnable README example | `test/readme.test.ts` |
|
| Every runnable README example | `test/readme.test.ts` |
|
||||||
| Receipt-versus-message classification by `esm_class` | `test/dlr.test.ts`, `test/session.test.ts` |
|
| Receipt-versus-message classification by `esm_class` | `test/dlr.test.ts`, `test/session.test.ts` |
|
||||||
| An intermediate delivery notification read as a report marked `intermediate`, as is a receipt reporting `ENROUTE` or `SCHEDULED`, and never counted into a merge | `test/dlr.test.ts`, `test/session.test.ts`, `test/session-extras.test.ts` |
|
| An intermediate delivery notification read as a report marked `intermediate`, as is a receipt reporting `ENROUTE` or `SCHEDULED`, and never counted into a merge | `test/dlr.test.ts`, `test/session.test.ts`, `test/session-extras.test.ts` |
|
||||||
|
| A transient state sent under the marker the spec gives it, off the same list the reader uses | `test/session-extras.test.ts` |
|
||||||
| A listener that throws, or rejects, reaching `sessionError`/`serverError` rather than the process | `test/session.test.ts`, `test/error-from.test.ts` |
|
| A listener that throws, or rejects, reaching `sessionError`/`serverError` rather than the process | `test/session.test.ts`, `test/error-from.test.ts` |
|
||||||
| Cross-checked against node-smpp both ways and over a live session | `test/interop.test.ts` |
|
| Cross-checked against node-smpp both ways and over a live session | `test/interop.test.ts` |
|
||||||
| CI on Node 18/20/22/24, Renovate, tag-triggered publish | `.github/workflows/` |
|
| CI on Node 18/20/22/24, Renovate, tag-triggered publish | `.github/workflows/` |
|
||||||
@@ -132,13 +133,6 @@ session message is a change to every call site.
|
|||||||
and applies it to the other is wrong. A budget type both take would close it. Raised by review,
|
and applies it to the other is wrong. A budget type both take would close it. Raised by review,
|
||||||
2026-09-01.
|
2026-09-01.
|
||||||
|
|
||||||
- [ ] **Write a transient state as an intermediate delivery notification.** `sendDlr()` marks every
|
|
||||||
receipt `esm_class` 0x04, so `sendDlr('ENROUTE')` announces a non-final state with the marker
|
|
||||||
SMPP 3.4 Appendix B reserves for the final one — the two disagreeing spellings of finality the
|
|
||||||
reading side was taught to reconcile on 2026-09-03. Decided the same day: fix it, off
|
|
||||||
`transientStates` in `dlr.ts` so the writer and the reader cannot drift, in a PR of its own.
|
|
||||||
Costs a bullet in README's "Behaviour that changed on the wire".
|
|
||||||
|
|
||||||
- [ ] **`once()` is copied into four test files, and two copies never give up.**
|
- [ ] **`once()` is copied into four test files, and two copies never give up.**
|
||||||
`session-extras.test.ts` and `readme.test.ts` reject after 5000 ms; `session.test.ts` and
|
`session-extras.test.ts` and `readme.test.ts` reject after 5000 ms; `session.test.ts` and
|
||||||
`tls.test.ts` wait forever, so an event that never fires still hangs the run the way an
|
`tls.test.ts` wait forever, so an event that never fires still hangs the run the way an
|
||||||
|
|||||||
Reference in New Issue
Block a user