diff --git a/AGENTS.md b/AGENTS.md index cc83610..b05736e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -304,8 +304,8 @@ the file. - The drain's wait on the application ignores `shutdownTimeout: 0`. - What the application holds unanswered is capped on constants, and a message past the cap is refused. -- A store at its bound answers `ESME_RTHROTTLED` to a `submit_sm` and `ESME_RX_T_APPN` to a - `deliver_sm`. +- A store at its bound answers `ESME_RTHROTTLED` to a submission and `ESME_RX_T_APPN` to a delivery, + a `data_sm` by whichever it stands in for. - A reconnect keeps the delivery-receipt merges; everything else the link held is dropped. - A message id base is merged at most once. - A send that never reached the socket waits for the next link; one that did is counted, not resent. diff --git a/CHANGELOG.md b/CHANGELOG.md index fc2bcb3..3a0dbda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ the cap is lost, since its segments were already answered. - A message arriving while the application holds 1000 unanswered, or 64 MiB of them counted the way `maxOctets` counts segments, is refused with `ESME_RTHROTTLED` (`ESME_RX_T_APPN` on a - `deliver_sm`), so the peer keeps it and retries. **Call `sendResp()` on every `sms`, multipart + delivery), so the peer keeps it and retries. **Call `sendResp()` on every `sms`, multipart included**: 1000 left unanswered now stop inbound traffic for up to five minutes, where the oldest used to be dropped with a warning. - A `submit_sm` segment the reassembly buffer has no room for is refused with `ESME_RTHROTTLED`, diff --git a/README.md b/README.md index 4f5f349..aee9db5 100644 --- a/README.md +++ b/README.md @@ -432,13 +432,12 @@ const { err, pduObj } = await session.send({ each is two messages. - **Answered on arrival.** Each segment was answered as it landed, before you see the message: [Server in depth](#server-in-depth). -- **Unanswered messages.** While 1000 messages you have not called `sendResp()` on, or 64 MiB of - them counted the way `maxOctets` counts segments, are held, every new message is refused with - `ESME_RTHROTTLED` (`ESME_RX_T_APPN` on a `deliver_sm`) so the peer retries it, and no `sms` fires. - Reaching the bound logs one `warn`, and the first message accepted once both are down to half one - `info`. A message left five - minutes is dropped from the count with a `warn`; a later `sendResp()` still answers it. Neither - bound is an option. +- **Unanswered messages.** While a session holds 1000 messages you have not called `sendResp()` on, + or 64 MiB of them counted the way `maxOctets` counts segments, every new message and segment is + refused so the peer retries it: `ESME_RTHROTTLED` on a submission, `ESME_RX_T_APPN` on a delivery. + No `sms` fires. Reaching the bound logs one `warn`, and the first message accepted once both are + down to half one `info`. A message left five minutes is dropped from the count with a `warn`; a + later `sendResp()` still answers it. None of the three is an option. - **Where the body is.** A body in the `message_payload` TLV, SMPP's way of carrying up to 64 KB and the only place a `data_sm` has, reads exactly like one in `short_message`, concatenated messages and receipts included. A PDU filling both is read from `short_message`. diff --git a/docs/decisions.md b/docs/decisions.md index ea9cad1..b77ffaa 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -679,25 +679,25 @@ rule and an index of the titles below. - **What the application holds unanswered is capped on constants, and a message past the cap is refused.** A bound the application cannot raise is the point: an application that answers nothing - would otherwise grow it for the life of the link, which goal 4 forbids. Reassembly's `maxOctets` - is an option because it bounds what the peer sends; this bounds what the application leaves - unanswered. Maintainer's call, 2026-09-26: a message arriving past the cap is refused with the - retry status, so the peer keeps it (goal 2). Rejected: dropping the oldest to make room, which - frees nothing while the application still holds its `Sms`, and stops the drain waiting for a - message the peer is owed. Rejected: pausing the socket, which also stalls every answer and - `enquire_link` on the link. Reaching the bound is a log line and nothing more (goal 8): an event - or a public count would be surface for what the application already knows, since it is the one - not answering. A message held past its timeout is still dropped, so `close()` can - report fewer unanswered than there were — accepted, because the alternative is holding what - nothing will answer. + would otherwise hold ever more messages, which goal 4 forbids. Reassembly's `maxOctets` is an + option because it bounds what the peer sends; this bounds what the application leaves unanswered. + Maintainer's call, 2026-09-26. Refusing leaves the message with the peer, which will send it again + (goal 2). Rejected: dropping the oldest to make room, which frees nothing while the application + still holds its `Sms`, and stops the drain waiting for a message the peer is owed. Rejected: + pausing the socket, which also stalls every answer and `enquire_link` on the link. Reaching the + bound shows only in the log (goal 8): an event or a public count would be surface for what the + application already knows, since it is the one not answering. A message held past its timeout is + still dropped, so `close()` can report fewer unanswered than there were — accepted, because the + alternative is holding what nothing will answer. -- **A store at its bound answers `ESME_RTHROTTLED` to a `submit_sm` and `ESME_RX_T_APPN` to a - `deliver_sm`.** Maintainer's call, 2026-09-26, for reassembly and held messages alike, so "keep it - and retry" has one spelling per direction. `ESME_RTHROTTLED` asks the sender to slow down, which - is what the peer outrunning us needs, and operators send it (Vonage, LINK Mobility, Route Mobile, - Jasmin), so clients built against them meet it (goal 1). Rejected: `ESME_RMSGQFUL`, which names an - exhausted queue and no rate. `ESME_RTHROTTLED` is the SMSC's to send, so an ESME answers with - SMPP 3.4's temporary receiver error, the one an SMSC retries on (goal 3). +- **A store at its bound answers `ESME_RTHROTTLED` to a submission and `ESME_RX_T_APPN` to a + delivery, a `data_sm` by whichever it stands in for.** Maintainer's call, 2026-09-26, for + reassembly and held messages alike, so "keep it and retry" has one spelling per direction. + `ESME_RTHROTTLED` asks the sender to slow down, which is what the peer outrunning us needs, and + operators send it (Vonage, LINK Mobility, Route Mobile, Jasmin), so clients built against them + meet it (goal 1). Rejected: `ESME_RMSGQFUL`, which names an exhausted queue and no rate. + `ESME_RTHROTTLED` is the SMSC's to send, so an ESME answers with SMPP 3.4's temporary receiver + error, the one an SMSC retries on (goal 3). - **A reconnect keeps the delivery-receipt merges; everything else the link held is dropped.** `onDelivery()` answers each receipt before the group it belongs to is complete, and `teardown()` diff --git a/src/incoming-requests.ts b/src/incoming-requests.ts index 64867c2..13e84fe 100644 --- a/src/incoming-requests.ts +++ b/src/incoming-requests.ts @@ -19,7 +19,7 @@ import { paramText } from './defs/types.ts'; import { respIdParams, segmentId } from './sms-id.ts'; import { respNameFor } from './defs/commands.ts'; -/** Asks the peer to keep the message and retry: RTHROTTLED is the SMSC's to send, so an ESME's is another. */ +/** Asks the peer to keep the message and retry. */ function throttledStatus(carriedAs: string): ErrorName { return carriedAs === 'submit_sm' ? 'ESME_RTHROTTLED' : 'ESME_RX_T_APPN'; }