5 Commits

Author SHA1 Message Date
lilleman e1c2c2fb76 File the scoring run's findings and record its scores
Mirror / push (push) Successful in 5s
Test / lint (pull_request) Successful in 21s
Test / test (20) (pull_request) Successful in 29s
Test / test (22) (pull_request) Successful in 30s
Test / test (18) (pull_request) Successful in 29s
Test / test (24) (pull_request) Successful in 32s
Test / test (26) (pull_request) Successful in 30s
2026-09-30 20:38:09 +02:00
lilleman be0f2c33c9 Amend plan 3: a file keeps its export's name until the export is renamed
Mirror / push (push) Has been cancelled
Test / lint (pull_request) Successful in 25s
Test / test (18) (pull_request) Successful in 30s
Test / test (20) (pull_request) Successful in 29s
Test / test (26) (pull_request) Successful in 31s
Test / test (22) (pull_request) Successful in 33s
Test / test (24) (pull_request) Successful in 30s
2026-09-30 20:35:46 +02:00
lilleman bf1bddba3b Name files for what they export, and move the session files into session/
Mirror / push (push) Has been cancelled
Test / lint (pull_request) Successful in 21s
Test / test (18) (pull_request) Successful in 29s
Test / test (20) (pull_request) Successful in 29s
Test / test (22) (pull_request) Successful in 30s
Test / test (24) (pull_request) Successful in 29s
Test / test (26) (pull_request) Successful in 34s
2026-09-30 20:33:13 +02:00
lilleman 92479d697d Record the layered src decision and the pdu-to-message edge
Mirror / push (push) Has been cancelled
Test / lint (pull_request) Successful in 26s
Test / test (18) (pull_request) Successful in 32s
Test / test (24) (pull_request) Successful in 32s
Test / test (26) (pull_request) Successful in 29s
Test / test (20) (pull_request) Successful in 30s
Test / test (22) (pull_request) Successful in 30s
2026-09-30 20:21:59 +02:00
lilleman 980e9d4569 Move src into codec, protocol, messages, session, client and server
Mirror / push (push) Has been cancelled
Test / lint (pull_request) Successful in 22s
Test / test (18) (pull_request) Successful in 32s
Test / test (20) (pull_request) Successful in 29s
Test / test (22) (pull_request) Successful in 32s
Test / test (24) (pull_request) Successful in 29s
Test / test (26) (pull_request) Successful in 36s
2026-09-30 20:15:04 +02:00
81 changed files with 653 additions and 652 deletions
+49 -45
View File
@@ -37,55 +37,59 @@ These are not preferences. Breaking one is a defect.
``` ```
src/ src/
index.ts Public surface. Named exports only, no default export. index.ts Public surface. Named exports only, no default export; assembles `defs`.
client.ts client() -> { err, session } log.ts SmppLog, silentLog — the default, and guardedLog(): a logger that cannot throw
server.ts server() -> { err, server }, server owns the listener + close() message.ts Message bodies: encodeBody/decodeMessage under a data_coding, splitting, bit counting, smppDate/smppTime
session.ts Session: the socket's life, dispatch, events, and the collaborators below options.ts SessionOptions, ReconnectOptions, their checks, and `defaults`: every default and internal cap
sms.ts The live handle emitted as the 'sms' event (sendResp/sendDlr) result.ts Result<T>, and an untyped value as error material: errorFrom(), namedValue(), quoted()
concat.ts How a PDU says it is a segment: its UDH, or the sar_* TLVs unanswered-error.ts
dlr.ts Delivery receipts: text and TLV parsing, receipt status codes codec/ Bytes <-> PduObject
dlr-merger.ts DlrMerger: per-segment receipts counted into one MessageDlr
error-from.ts An untyped value as error material: errorFrom() an Error, namedValue() a name
expiring-groups.ts ExpiringGroups: the capped, weighed, expiring store DlrMerger, HeldMessages and Reassembler share
held-messages.ts HeldMessages: a message from its `sms` event to its answer, capped and expiring, one MessageHold each
idle-waiters.ts IdleWaiters: waiting for a count to fall to zero, and what is left of a budget
incoming-requests.ts Every request the peer sends: messages, receipts, links, unknown commands
link-life.ts LinkLife: whether the link lives, and where a request waits for the next one
link-timers.ts LinkTimers: the enquire_link heartbeat and the idle timeout
log.ts SmppLog, the logger contract, and silentLog — the default
message.ts Encoding detection, splitting, bit counting, SMPP date formatting
message-body.ts Where an inbound body is: short_message, or the message_payload TLV
outgoing-requests.ts OutgoingRequests: the window, the pending map and the retry
pdu.ts pduToObj / objToPdu / pduReturn — synchronous, result-returning
pdu-framer.ts PduFramer: a byte stream cut into complete PDUs
pdu-refusal.ts A PDU the codec would not read, and the answer SMPP names for it
pdu-transport.ts PduTransport: the socket a session reads complete PDUs off
pending-requests.ts PendingRequests: sequence numbers, correlation, timeout, abort
reassembly.ts Reassembler: capped, expiring multipart groups
reconnect-loop.ts ReconnectLoop: backoff, retry timer, stopped-ness
result.ts Result<T> — the shape every fallible call returns
retained-pdu.ts A PDU copied off the wire so holding it pins nothing else, and what holding it costs
send-sms.ts submitSms composition and the submitSmParams builder
send-window.ts SendWindow: the maxOutstanding semaphore
session-options.ts SessionOptions, ReconnectOptions, bind direction and the session defaults
sms-id.ts Message ids: the peer's notation, the <base>-<n> a segment gets, which response carries one
udh.ts User data header: its length, the concatenation fields of a long SMS and their reference
unanswered-error.ts UnansweredError: it went out and no answer came back
uuid.ts uuidv7() — the ids the library generates for messages
defs/
commands.ts The 33 commands, their ids and ordered parameter lists commands.ts The 33 commands, their ids and ordered parameter lists
constants.ts consts + constsById, and the SMPP version constants constants.ts consts + constsById, optionalParamsMinVersion, and esm_class's readers: hasUdh(), messageTypeOf()
encodings.ts GSM 03.38, LATIN1, UCS2, detection, data_coding resolution encodings.ts GSM 03.38, LATIN1, UCS2, detection, data_coding resolution, message class
errors.ts errors + errorsById (ESME_*) errors.ts errors + errorsById (ESME_*)
index.ts defs: every table as one group pdu-framer.ts PduFramer
pdu.ts pduToObj / objToPdu / pduReturn — synchronous, result-returning
refusal.ts A PDU the codec would not read, and the answer SMPP names for it
retained-pdu.ts A PDU copied off the wire so holding it pins nothing else, and what holding it costs
tlvs.ts TLV definitions, tlvsById, the typed read and input shapes, and reading and writing a TLV stream tlvs.ts TLV definitions, tlvsById, the typed read and input shapes, and reading and writing a TLV stream
types.ts Wire types: int8/int16/int32/string/cstring/buffer/arrays types.ts Wire types: int8/int16/int32/string/cstring/buffer/arrays
protocol/ What the fields mean
bind.ts Bind directions: which commands bind, what a direction carries, data_sm's stand-in, checkedBind()
concat.ts How a PDU says it is a segment: its UDH, or the sar_* TLVs
dlr.ts Delivery receipts: text and TLV parsing, receipt status codes
message-body.ts Where an inbound body is: short_message, or the message_payload TLV
message-ids.ts Message ids: the peer's notation, the <base>-<n> a segment gets, which response carries one
udh.ts User data header: its length, the concatenation fields of a long SMS and their reference
uuid.ts uuidv7() — the ids the library generates for messages
messages/ Whole messages across segments and time
dlr-merger.ts DlrMerger: per-segment receipts counted into one MessageDlr
expiring-groups.ts ExpiringGroups: the capped, weighed, expiring store DlrMerger, HeldMessages and Reassembler share
reassembly.ts Reassembler: capped, expiring multipart groups
submit.ts submitSms composition and the submitSmParams builder
session/ One socket's life, and reconnecting it
held-messages.ts HeldMessages: a message from its `sms` event to its answer, capped and expiring, one MessageHold each
idle-waiters.ts IdleWaiters: waiting for a count to fall to zero, and what is left of a budget
incoming-requests.ts IncomingRequests: every request the peer sends — messages, receipts, links, unknown commands
link-life.ts LinkLife: whether the link lives, and where a request waits for the next one
link-timers.ts LinkTimers: the enquire_link heartbeat and the idle timeout
outgoing-requests.ts OutgoingRequests: the window, the pending map and the retry
pdu-transport.ts PduTransport: the socket a session reads complete PDUs off
pending-requests.ts PendingRequests: sequence numbers, correlation, timeout, abort
reconnect-loop.ts ReconnectLoop: backoff, retry timer, stopped-ness
send-window.ts SendWindow: the maxOutstanding semaphore
session.ts Session: the socket's life, dispatch and events, composing the rest of session/
sms.ts The live handle emitted as the 'sms' event (sendResp/sendDlr)
client/client.ts client() -> { err, session }
server/server.ts server() -> { err, server }, server owns the listener + close()
``` ```
Imports point one way: `defs` knows nothing above it but `result.ts`, `pdu` uses `defs`, `session` Imports point one way: `codec` ← `protocol` ← `messages` ← `session` ← `client`/`server`. At the
uses `pdu`, and `client`/`server` use `session`. The ways back up are the `Session` handed to root, `result.ts`, `log.ts` and `unanswered-error.ts` sit below `codec`, and `message.ts` and
`createSms()`, `HeldMessages` and `IncomingRequests`, which call back into it, and to `OnRequest` `options.ts` with `protocol`. One edge runs up: `codec/pdu.ts` imports `encodeBody()` and
and `onConnected` in `session-options.ts`, all imported as a type only. `decodeMessage()` from `message.ts`. The ways back up are the `Session` handed to `createSms()`,
`HeldMessages` and `IncomingRequests`, which call back into it, and to `OnRequest` and `onConnected`
in `options.ts`, all imported as a type only.
**Parameter order is wire order.** The key order inside `cmds.*.params` is the order the fields are **Parameter order is wire order.** The key order inside `cmds.*.params` is the order the fields are
written to and read from the buffer. Never sort those alphabetically — the alphabetical-ordering written to and read from the buffer. Never sort those alphabetically — the alphabetical-ordering
@@ -314,8 +318,8 @@ this is not a changelog.
`SendWindow` rather than extracted. `SendWindow` rather than extracted.
- `SmppLog` is a five-method contract this library declares, not a dependency. - `SmppLog` is a five-method contract this library declares, not a dependency.
- The TLS tests build their own self-signed certificate in DER - The TLS tests build their own self-signed certificate in DER
- `src/` stays flat until a module has to move for another reason. - `src/` is grouped by layer, and imports point down the layers.
- `test/` stays flat too, and a file there is named for the question it answers rather than for the - `test/` stays flat, and a file there is named for the question it answers rather than for the
module it covers. module it covers.
- CI tests on Linux only; `src/` keeps off what is known to break on macOS or Windows. - CI tests on Linux only; `src/` keeps off what is known to break on macOS or Windows.
- GitHub mirrors Gitea without pruning, and a ref deleted on Gitea is deleted on GitHub by a run of - GitHub mirrors Gitea without pruning, and a ref deleted on Gitea is deleted on GitHub by a run of
+1 -1
View File
@@ -1,4 +1,4 @@
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
/** /**
* Answers every submit_sm ESME_ROK and does nothing else, so a measurement against it reads this * Answers every submit_sm ESME_ROK and does nothing else, so a measurement against it reads this
+1 -1
View File
@@ -1,4 +1,4 @@
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
/** /**
* Pushes `count` single-segment messages and reports what the wire carried per second. Keeps * Pushes `count` single-segment messages and reports what the wire carried per second. Keeps
+3
View File
@@ -260,6 +260,9 @@ overturned recorded decisions without naming them. todo.md carries the reordered
Reassembly refuses at its bound: an evicted group is answered segments lost, goal 2 outranks goal 4, Reassembly refuses at its bound: an evicted group is answered segments lost, goal 2 outranks goal 4,
and the store is per session. The spent set expires by age. and the store is per session. The spent set expires by age.
8. **`retained.ts` goes to `codec/`**; `options.ts` joins AGENTS.md's type-only ways back up. 8. **`retained.ts` goes to `codec/`**; `options.ts` joins AGENTS.md's type-only ways back up.
9. **A file keeps its export's name until the chunk that renames the export.** The scoring run of
2026-09-30 read §2's names on unrenamed classes (`keepalive.ts` holding `LinkTimers`) as lies, so
§2 and §4 name where a file ends, not what it is called before its export changes.
Public API questions, answered as the review recommends. Maintainer's call, 2026-09-30; each Public API questions, answered as the review recommends. Maintainer's call, 2026-09-30; each
lands in docs/decisions.md with the chunk that builds it: lands in docs/decisions.md with the chunk that builds it:
+8 -8
View File
@@ -295,7 +295,7 @@ rule and an index of the titles below.
a delivery receipt (0x04).** Appendix B makes a receipt's `stat` the message's final status, so 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 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 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. from `transientStates` in `protocol/dlr.ts`, the same one the reader uses, so the two cannot drift.
Rejected: 0x04 for every state, for the sake of a peer that classifies on the marker — the cost Rejected: 0x04 for every state, for the sake of a peer that classifies on the marker — the cost
accepted here is that such a peer stops recognising a transient report as a report at all and hands accepted here is that such a peer stops recognising a transient report as a report at all and hands
its application receipt text as an inbound message, where under 0x04 it would have read the state its application receipt text as an inbound message, where under 0x04 it would have read the state
@@ -590,7 +590,7 @@ rule and an index of the titles below.
deadlocked every multi-segment message against a production gateway deadlocked every multi-segment message against a production gateway
([interop-tests/findings/03-jasmin.md](../interop-tests/findings/03-jasmin.md)). Goal 1 has the answer ([interop-tests/findings/03-jasmin.md](../interop-tests/findings/03-jasmin.md)). Goal 1 has the answer
a real SMSC gives — one `message_id` per `submit_sm`, immediately — so the group's id base is a real SMSC gives — one `message_id` per `submit_sm`, immediately — so the group's id base is
generated when it opens and each segment is answered `<base>-<n>`, the notation `sms-id.ts` owns generated when it opens and each segment is answered `<base>-<n>`, the notation `protocol/message-ids.ts` owns
and `DlrMerger` reads back. The id is therefore fixed by the first segment, which is why an `smsId` and `DlrMerger` reads back. The id is therefore fixed by the first segment, which is why an `smsId`
or a refusing `status` passed to `sendResp()` on such a message is an error rather than a silent or a refusing `status` passed to `sendResp()` on such a message is an error rather than a silent
no-op. `answeredOnArrival` is on `Sms` because nothing the application can compute says it, and the no-op. `answeredOnArrival` is on `Sms` because nothing the application can compute says it, and the
@@ -795,13 +795,13 @@ rule and an index of the titles below.
fail on every developer machine, and a committed key leaks in a public repository. Valid while the fail on every developer machine, and a committed key leaks in a public repository. Valid while the
dev image has no openssl. dev image has no openssl.
- **`src/` stays flat until a module has to move for another reason.** Architecture review, - **`src/` is grouped by layer, and imports point down the layers.** Maintainer's call, 2026-09-30,
2026-09-06: the grouping the [file map](../AGENTS.md#architecture) already implies — `wire/` for `pdu*` and `defs`, with the Locality rewrite; the [map](../AGENTS.md#architecture) names the order and places each
`link/` for `link-*`, `reconnect-*`, `pdu-transport` and `send-window`, `messages/` for `sms*`, root file in a layer. Serves goal 8: internals are reshapeable only once a reader can find them,
`dlr*`, `message*`, `reassembly` and `udh` — rewrites every import for no change to and every comprehension panel navigated by the map. Rejected: `src/` flat until a module has to
`dist/index.js`, the one published entry. Valid while that map is what a reader navigates by. move for another reason. Valid while the map is what readers navigate by.
- **`test/` stays flat too, and a file there is named for the question it answers rather than for the - **`test/` stays flat, and a file there is named for the question it answers rather than for the
module it covers.** Architecture review, 2026-09-08, at 18 test files: what keeps that count honest module it covers.** Architecture review, 2026-09-08, at 18 test files: what keeps that count honest
is the naming rule rather than a tree — `operator-receipts.test.ts` holds a corpus defined by where is the naming rule rather than a tree — `operator-receipts.test.ts` holds a corpus defined by where
it came from, cutting across four modules, where filing it by module would enter each new operator it came from, cutting across four modules, where filing it by module would enter each new operator
+3 -3
View File
@@ -35,17 +35,17 @@ export default tseslint.config(
}, },
{ {
// The spec tables are data: their length tracks the specification, not any complexity. // The spec tables are data: their length tracks the specification, not any complexity.
files: ['src/defs/*.ts'], files: ['src/codec/{commands,constants,encodings,errors,tlvs,types}.ts'],
rules: { 'max-lines': 'off' }, rules: { 'max-lines': 'off' },
}, },
{ {
// ESLint counts every ?. and ?? in dlrFromPdu as a branch; the 19 is 26 lines of flat field resolution. // ESLint counts every ?. and ?? in dlrFromPdu as a branch; the 19 is 26 lines of flat field resolution.
files: ['src/dlr.ts'], files: ['src/protocol/dlr.ts'],
rules: { complexity: ['error', 19] }, rules: { complexity: ['error', 19] },
}, },
{ {
// 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.
files: ['src/defs/encodings.ts'], files: ['src/codec/encodings.ts'],
rules: { 'no-control-regex': 'off' }, rules: { 'no-control-regex': 'off' },
}, },
{ {
+4 -4
View File
@@ -1,10 +1,10 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import type { SmppServer } from '../src/server.ts'; import type { SmppServer } from '../src/server/server.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
const CLOUDHOPPER_HOST = process.env.CLOUDHOPPER_HOST ?? 'cloudhopper:8080'; const CLOUDHOPPER_HOST = process.env.CLOUDHOPPER_HOST ?? 'cloudhopper:8080';
const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775'); const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775');
+1 -1
View File
@@ -32,7 +32,7 @@ services:
dumbclient-netns: dumbclient-netns:
condition: service_started condition: service_started
# S9's comparison run: window below maxHeldMessages (1000, session-options.ts), where nothing # S9's comparison run: window below maxHeldMessages (1000, options.ts), where nothing
# should ever be throttled - see findings/07-load.md. # should ever be throttled - see findings/07-load.md.
dumbclient-w500: dumbclient-w500:
build: ./interop-tests/peers/dumbclient build: ./interop-tests/peers/dumbclient
+5 -5
View File
@@ -1,9 +1,9 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import type { LogMethod, SmppLog } from '../src/log.ts'; import type { LogMethod, SmppLog } from '../src/log.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775'); const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775');
/** Slower than every scenario's submission rate (2000/s for the window runs), so a real backlog /** Slower than every scenario's submission rate (2000/s for the window runs), so a real backlog
@@ -206,7 +206,7 @@ after(async () => {
// S9 (target 11) and the backpressure-at-server scenario: window 2000 at a high rate against a // S9 (target 11) and the backpressure-at-server scenario: window 2000 at a high rate against a
// handler slowed enough to build a real backlog. window500 is the same shape with a window below // handler slowed enough to build a real backlog. window500 is the same shape with a window below
// maxHeldMessages (1000, session-options.ts defaults.maxHeldMessages), the bound past which a // maxHeldMessages (1000, options.ts defaults.maxHeldMessages), the bound past which a
// peer's window is answered ESME_RTHROTTLED. smpp-dumb-client counts a throttled message as sent // peer's window is answered ESME_RTHROTTLED. smpp-dumb-client counts a throttled message as sent
// and never resends it, so window 2000 accounts for 20,000 as answered plus throttled. // and never resends it, so window 2000 accounts for 20,000 as answered plus throttled.
const throttleMessage = 'session - unanswered messages at their bound, asking the peer to retry'; const throttleMessage = 'session - unanswered messages at their bound, asking the peer to retry';
@@ -270,7 +270,7 @@ describe('S6 - idle peer, no enquire_link at all', () => {
assert.ok(bound, 'dumb-idle never submitted its one message'); assert.ok(bound, 'dumb-idle never submitted its one message');
// idleTimeout is 40s from the last byte the peer sent (its submit_sm), never from our own // idleTimeout is 40s from the last byte the peer sent (its submit_sm), never from our own
// writes (link-timers.ts resets only on inbound data). This test may start running well // writes (session/link-timers.ts resets only on inbound data). This test may start running well
// past that mark on its own (S9 above can take a minute) - statsFor(...).closed is set from // past that mark on its own (S9 above can take a minute) - statsFor(...).closed is set from
// a 'close' listener attached at session-creation time, so a close from before this test // a 'close' listener attached at session-creation time, so a close from before this test
// even started is still seen; budget is slack for a session that is still open, not a clock. // even started is still seen; budget is slack for a session that is still open, not a clock.
+10 -10
View File
@@ -1,18 +1,18 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import http from 'node:http'; import http from 'node:http';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { Dlr } from '../src/dlr.ts'; import type { Dlr } from '../src/protocol/dlr.ts';
import type { EncodingName } from '../src/defs/encodings.ts'; import type { EncodingName } from '../src/codec/encodings.ts';
import type { PduObject } from '../src/pdu.ts'; import type { PduObject } from '../src/codec/pdu.ts';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { ConcatReference } from '../src/udh.ts'; import { ConcatReference } from '../src/protocol/udh.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from '../test/teardown.ts'; import { closeAfter } from '../test/teardown.ts';
import { paramText } from '../src/defs/types.ts'; import { paramText } from '../src/codec/types.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
import { encodeMessage, splitMessage } from '../src/message.ts'; import { encodeMessage, splitMessage } from '../src/message.ts';
import { submitSmParams } from '../src/send-sms.ts'; import { submitSmParams } from '../src/messages/submit.ts';
const PEER_HOST = process.env.PEER_HOST ?? 'jasmin'; const PEER_HOST = process.env.PEER_HOST ?? 'jasmin';
const PEER_PORT = Number(process.env.PEER_PORT ?? '2775'); const PEER_PORT = Number(process.env.PEER_PORT ?? '2775');
+3 -3
View File
@@ -1,11 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import net from 'node:net'; import net from 'node:net';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { PduRefusedError } from '../src/index.ts'; import { PduRefusedError } from '../src/index.ts';
import { bareTlvHeader, pduBytes } from '../test/raw-pdus.ts'; import { bareTlvHeader, pduBytes } from '../test/raw-pdus.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
const JSMPP_HOST = process.env.JSMPP_HOST ?? 'jsmpp:8080'; const JSMPP_HOST = process.env.JSMPP_HOST ?? 'jsmpp:8080';
const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775'); const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775');
+10 -10
View File
@@ -1,17 +1,17 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import http from 'node:http'; import http from 'node:http';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { MessageState } from '../src/defs/constants.ts'; import type { MessageState } from '../src/codec/constants.ts';
import type { Dlr } from '../src/dlr.ts'; import type { Dlr } from '../src/protocol/dlr.ts';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { ConcatReference } from '../src/udh.ts'; import { ConcatReference } from '../src/protocol/udh.ts';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { detect, encodings } from '../src/defs/encodings.ts'; import { detect, encodings } from '../src/codec/encodings.ts';
import { paramText } from '../src/defs/types.ts'; import { paramText } from '../src/codec/types.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
import { splitMessage } from '../src/message.ts'; import { splitMessage } from '../src/message.ts';
import { submitSmParams } from '../src/send-sms.ts'; import { submitSmParams } from '../src/messages/submit.ts';
// smsbox HTTP hosts, one per variant - all point at the same node:2775 SMPP server. // smsbox HTTP hosts, one per variant - all point at the same node:2775 SMPP server.
const MAIN_SMSBOX = process.env.MAIN_SMSBOX ?? 'kannel-smsbox:13013'; const MAIN_SMSBOX = process.env.MAIN_SMSBOX ?? 'kannel-smsbox:13013';
+3 -3
View File
@@ -1,8 +1,8 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { paramText } from '../src/defs/types.ts'; import { paramText } from '../src/codec/types.ts';
import { isCommand, server } from '../src/index.ts'; import { isCommand, server } from '../src/index.ts';
const DRIVER = process.env.PHP_DRIVER ?? 'php:8080'; const DRIVER = process.env.PHP_DRIVER ?? 'php:8080';
+4 -4
View File
@@ -1,9 +1,9 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { encodings } from '../src/defs/encodings.ts'; import { encodings } from '../src/codec/encodings.ts';
import { paramText } from '../src/defs/types.ts'; import { paramText } from '../src/codec/types.ts';
import { isCommand, server } from '../src/index.ts'; import { isCommand, server } from '../src/index.ts';
const DRIVER = process.env.PYTHON_DRIVER ?? 'python:8080'; const DRIVER = process.env.PYTHON_DRIVER ?? 'python:8080';
+3 -3
View File
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { after, describe } from 'node:test'; import test, { after, describe } from 'node:test';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775'); const SMPP_PORT = Number(process.env.SMPP_PORT ?? '2775');
@@ -54,7 +54,7 @@ describe('smppload (blocked)', () => {
const refusal = await waitFor(() => sessionErr, 10_000); const refusal = await waitFor(() => sessionErr, 10_000);
assert.ok(refusal); assert.ok(refusal);
// maxPduLength (pdu-refusal.ts) is 1MiB; the corrupted command_length (0x2a shifted into the // maxPduLength (codec/refusal.ts) is 1MiB; the corrupted command_length (0x2a shifted into the
// high bytes) reads as roughly 2.75M, so this is the "unreadable stream" teardown, not the // high bytes) reads as roughly 2.75M, so this is the "unreadable stream" teardown, not the
// "one bad PDU, link stays up" path - see AGENTS.md, "A stream this library cannot frame...". // "one bad PDU, link stays up" path - see AGENTS.md, "A stream this library cannot frame...".
assert.match(refusal.message, /Refusing a cmd_length of \d+/); assert.match(refusal.message, /Refusing a cmd_length of \d+/);
+10 -10
View File
@@ -1,16 +1,16 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { Dlr } from '../src/dlr.ts'; import type { Dlr } from '../src/protocol/dlr.ts';
import type { EncodingName } from '../src/defs/encodings.ts'; import type { EncodingName } from '../src/codec/encodings.ts';
import type { MessageDlr } from '../src/dlr-merger.ts'; import type { MessageDlr } from '../src/messages/dlr-merger.ts';
import type { PduObject } from '../src/pdu.ts'; import type { PduObject } from '../src/codec/pdu.ts';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from '../test/teardown.ts'; import { closeAfter } from '../test/teardown.ts';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { paramText } from '../src/defs/types.ts'; import { paramText } from '../src/codec/types.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
const PEER_HOST = process.env.PEER_HOST ?? 'smppsim'; const PEER_HOST = process.env.PEER_HOST ?? 'smppsim';
const PEER_PORT = Number(process.env.PEER_PORT ?? '2775'); const PEER_PORT = Number(process.env.PEER_PORT ?? '2775');
+4 -4
View File
@@ -1,9 +1,9 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { Dlr } from '../src/dlr.ts'; import type { Dlr } from '../src/protocol/dlr.ts';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from '../test/teardown.ts'; import { closeAfter } from '../test/teardown.ts';
const PEER_HOST = process.env.PEER_HOST ?? 'smscsim'; const PEER_HOST = process.env.PEER_HOST ?? 'smscsim';
+9 -22
View File
@@ -1,18 +1,18 @@
import type { ConnectionOptions } from 'node:tls'; import type { ConnectionOptions } from 'node:tls';
import type { Result, VoidResult } from './result.ts'; import type { Result, VoidResult } from '../result.ts';
import type { BindType, ReconnectOptions } from './session-options.ts'; import type { BindType } from '../protocol/bind.ts';
import type { SmppLog } from './log.ts'; import type { ReconnectOptions } from '../options.ts';
import type { SmsIdFormat } from './sms-id.ts'; import type { SmppLog } from '../log.ts';
import type { SmsIdFormat } from '../protocol/message-ids.ts';
import type { Socket } from 'node:net'; import type { Socket } from 'node:net';
export type { BindType }; export type { BindType };
import { ReconnectLoop } from './reconnect-loop.ts'; import { ReconnectLoop } from '../session/reconnect-loop.ts';
import { Session } from './session.ts'; import { Session } from '../session/session.ts';
import { checkSessionOptions } from './session-options.ts'; import { checkSessionOptions, defaults } from '../options.ts';
import { connect as netConnect } from 'node:net'; import { connect as netConnect } from 'node:net';
import { connect as tlsConnect } from 'node:tls'; import { connect as tlsConnect } from 'node:tls';
import { defaultInterfaceVersion } from './defs/constants.ts'; import { guardedLog } from '../log.ts';
import { guardedLog } from './log.ts';
/** `fromStart` puts the very first connect and bind through the same backoff loop as a drop. */ /** `fromStart` puts the very first connect and bind through the same backoff loop as a drop. */
type ReconnectTuning = { fromStart?: boolean; maxDelay?: number; minDelay?: number }; type ReconnectTuning = { fromStart?: boolean; maxDelay?: number; minDelay?: number };
@@ -41,19 +41,6 @@ export type ClientOptions = {
username?: string; username?: string;
}; };
const defaults = {
bindType: 'transceiver',
connectTimeout: 10_000,
enquireLinkInterval: 20_000,
host: 'localhost',
/** The idle timeout is what notices a dead link, so it has to outlast one silent probe. */
idleTimeoutFactor: 2,
interfaceVersion: defaultInterfaceVersion,
password: 'pass',
port: 2775,
username: 'user',
} as const;
function armConnectTimeout( function armConnectTimeout(
sock: Socket, sock: Socket,
connectTimeout: number | false, connectTimeout: number | false,
@@ -1,6 +1,3 @@
/** The version declared on the wire. The tables below cover 5.0, which is a superset of it. */
export const defaultInterfaceVersion = 0x34;
/** Spec rule, not a preference: a peer declaring less than 3.4 is sent no optional parameters. */ /** Spec rule, not a preference: a peer declaring less than 3.4 is sent no optional parameters. */
export const optionalParamsMinVersion = 0x34; export const optionalParamsMinVersion = 0x34;
@@ -1,5 +1,5 @@
import type { Result } from './result.ts'; import type { Result } from '../result.ts';
import { framingRefusal } from './pdu-refusal.ts'; import { framingRefusal } from './refusal.ts';
/** /**
* Cuts a byte stream into whole PDUs. * Cuts a byte stream into whole PDUs.
+13 -13
View File
@@ -1,16 +1,16 @@
import type { CommandDefinition, CommandName, PduParams, PduParamsInput } from './defs/commands.ts'; import type { CommandDefinition, CommandName, PduParams, PduParamsInput } from './commands.ts';
import type { ErrorName } from './defs/errors.ts'; import type { ErrorName } from './errors.ts';
import type { ParamValue } from './defs/types.ts'; import type { ParamValue } from './types.ts';
import type { PduHeader } from './pdu-refusal.ts'; import type { PduHeader } from './refusal.ts';
import type { Result, VoidResult } from './result.ts'; import type { Result, VoidResult } from '../result.ts';
import type { TlvInputs, Tlvs } from './defs/tlvs.ts'; import type { TlvInputs, Tlvs } from './tlvs.ts';
import { PduRefusedError, framingRefusal } from './pdu-refusal.ts'; import { PduRefusedError, framingRefusal } from './refusal.ts';
import { cmds, commandNameById, respNameFor } from './defs/commands.ts'; import { cmds, commandNameById, respNameFor } from './commands.ts';
import { hasUdh } from './defs/constants.ts'; import { hasUdh } from './constants.ts';
import { decodeMessage, encodeBody } from './message.ts'; import { decodeMessage, encodeBody } from '../message.ts';
import { errorNameById, errors, isErrorName } from './defs/errors.ts'; import { errorNameById, errors, isErrorName } from './errors.ts';
import { paramNumber, valueText } from './defs/types.ts'; import { paramNumber, valueText } from './types.ts';
import { parseTlvs, writeTlvs } from './defs/tlvs.ts'; import { parseTlvs, writeTlvs } from './tlvs.ts';
/** The highest sequence number this library hands out; SMPP 3.4 4.7.1 reserves 0x7fffffff. */ /** The highest sequence number this library hands out; SMPP 3.4 4.7.1 reserves 0x7fffffff. */
export const maxSeqNr = 2147483646; export const maxSeqNr = 2147483646;
+3 -3
View File
@@ -1,6 +1,6 @@
import type { CommandName } from './defs/commands.ts'; import type { CommandName } from './commands.ts';
import type { ErrorName } from './defs/errors.ts'; import type { ErrorName } from './errors.ts';
import { respNameFor } from './defs/commands.ts'; import { respNameFor } from './commands.ts';
/** A hostile peer must not be able to make us allocate arbitrarily. */ /** A hostile peer must not be able to make us allocate arbitrarily. */
export const maxPduLength = 1024 * 1024; export const maxPduLength = 1024 * 1024;
@@ -1,6 +1,6 @@
import type { ParamValue } from './defs/types.ts'; import type { ParamValue } from './types.ts';
import type { PduObject } from './pdu.ts'; import type { PduObject } from './pdu.ts';
import { tlvOctets } from './defs/types.ts'; import { tlvOctets } from './types.ts';
/** Wire reads hand back views, so retaining one PDU would pin the whole chunk it arrived in. */ /** Wire reads hand back views, so retaining one PDU would pin the whole chunk it arrived in. */
export function detach(pduObj: PduObject): PduObject { export function detach(pduObj: PduObject): PduObject {
-19
View File
@@ -1,19 +0,0 @@
import { cmds, cmdsById } from './commands.ts';
import { consts, constsById } from './constants.ts';
import { encodings } from './encodings.ts';
import { errors, errorsById } from './errors.ts';
import { tlvs, tlvsById } from './tlvs.ts';
import { types } from './types.ts';
export const defs = {
cmds,
cmdsById,
consts,
constsById,
encodings,
errors,
errorsById,
tlvs,
tlvsById,
types,
};
-17
View File
@@ -1,17 +0,0 @@
/** Whatever was thrown or rejected, as an Error. `String()` throws on some values; this cannot. */
export function errorFrom(reason: unknown): Error {
if (reason instanceof Error) return reason;
try {
return new Error(String(reason));
} catch {
return new Error('A thrown value that cannot be converted to a string');
}
}
const printable: readonly string[] = ['boolean', 'number', 'string'];
/** String() throws on a null-prototype object, so anything but these is named by its type. */
export function namedValue(value: unknown): string {
return printable.includes(typeof value) ? String(value) : typeof value;
}
+52 -34
View File
@@ -1,13 +1,20 @@
export { client } from './client.ts'; import { cmds, cmdsById } from './codec/commands.ts';
export { server, SmppServer } from './server.ts'; import { consts, constsById } from './codec/constants.ts';
export { Session } from './session.ts'; import { encodings } from './codec/encodings.ts';
import { errors, errorsById } from './codec/errors.ts';
import { tlvs, tlvsById } from './codec/tlvs.ts';
import { types } from './codec/types.ts';
export { cmds, cmdsById, commandNameById, isCommandName } from './defs/commands.ts'; export { client } from './client/client.ts';
export { consts, constsById } from './defs/constants.ts'; export { server, SmppServer } from './server/server.ts';
export { dataCodingByEncoding, detect, encodingByDataCoding, encodings, isEncodingName, messageClassOf, unencodable } from './defs/encodings.ts'; export { Session } from './session/session.ts';
export { errorNameById, errors, errorsById, isErrorName } from './defs/errors.ts';
export { isTlvName, tlvs, tlvsById } from './defs/tlvs.ts'; export { cmds, cmdsById, commandNameById, isCommandName } from './codec/commands.ts';
export { types } from './defs/types.ts'; export { consts, constsById } from './codec/constants.ts';
export { dataCodingByEncoding, detect, encodingByDataCoding, encodings, isEncodingName, messageClassOf, unencodable } from './codec/encodings.ts';
export { errorNameById, errors, errorsById, isErrorName } from './codec/errors.ts';
export { isTlvName, tlvs, tlvsById } from './codec/tlvs.ts';
export { types } from './codec/types.ts';
export { export {
isCommand, isCommand,
@@ -16,9 +23,9 @@ export {
objToPdu, objToPdu,
pduReturn, pduReturn,
pduToObj, pduToObj,
} from './pdu.ts'; } from './codec/pdu.ts';
export { maxPduLength, PduRefusedError } from './pdu-refusal.ts'; export { maxPduLength, PduRefusedError } from './codec/refusal.ts';
export { export {
bitCount, bitCount,
@@ -29,27 +36,27 @@ export {
splitMessage, splitMessage,
} from './message.ts'; } from './message.ts';
export { dlrFromPdu, parseReceipt, receiptCodes } from './dlr.ts'; export { dlrFromPdu, parseReceipt, receiptCodes } from './protocol/dlr.ts';
export { messageOctets } from './message-body.ts'; export { messageOctets } from './protocol/message-body.ts';
export { concatOf } from './concat.ts'; export { concatOf } from './protocol/concat.ts';
export { concatInfo } from './udh.ts'; export { concatInfo } from './protocol/udh.ts';
export { PduFramer } from './pdu-framer.ts'; export { PduFramer } from './codec/pdu-framer.ts';
export { uuidv7 } from './uuid.ts'; export { uuidv7 } from './protocol/uuid.ts';
export type { BindType, ClientOptions } from './client.ts'; export type { BindType, ClientOptions } from './client/client.ts';
export type { Dlr, Receipt } from './dlr.ts'; export type { Dlr, Receipt } from './protocol/dlr.ts';
export type { SendDlrResult, SendRespOptions, Sms } from './sms.ts'; export type { SendDlrResult, SendRespOptions, Sms } from './session/sms.ts';
export type { Concat } from './concat.ts'; export type { Concat } from './protocol/concat.ts';
export type { ConcatInfo } from './udh.ts'; export type { ConcatInfo } from './protocol/udh.ts';
export type { Result, VoidResult } from './result.ts'; export type { Result, VoidResult } from './result.ts';
export type { SmppLog } from './log.ts'; export type { SmppLog } from './log.ts';
export type { SmsIdFormat, SmsIdNotation } from './sms-id.ts'; export type { SmsIdFormat, SmsIdNotation } from './protocol/message-ids.ts';
export type { export type {
AuthenticateInput, AuthenticateInput,
AuthenticateResult, AuthenticateResult,
ServerEvents, ServerEvents,
ServerOptions, ServerOptions,
} from './server.ts'; } from './server/server.ts';
export type { export type {
CloseOptions, CloseOptions,
MessageDlr, MessageDlr,
@@ -59,16 +66,27 @@ export type {
SendSmsResult, SendSmsResult,
SessionEvents, SessionEvents,
SessionOptions, SessionOptions,
} from './session.ts'; } from './session/session.ts';
export type { CommandName, PduParams, PduParamsInput } from './defs/commands.ts'; export type { CommandName, PduParams, PduParamsInput } from './codec/commands.ts';
export type { ConstGroup, MessageState, SubmitMessagingMode } from './defs/constants.ts'; export type { ConstGroup, MessageState, SubmitMessagingMode } from './codec/constants.ts';
export type { Encoding, EncodingName, Unencodable } from './defs/encodings.ts'; export type { Encoding, EncodingName, Unencodable } from './codec/encodings.ts';
export type { ErrorName } from './defs/errors.ts'; export type { ErrorName } from './codec/errors.ts';
export type { PduObject, PduObjectInput, TlvInputs } from './pdu.ts'; export type { PduObject, PduObjectInput, TlvInputs } from './codec/pdu.ts';
export type { PduHeader } from './pdu-refusal.ts'; export type { PduHeader } from './codec/refusal.ts';
export type { SplitOptions } from './message.ts'; export type { SplitOptions } from './message.ts';
export type { Tlv, TlvDefinition, TlvName, Tlvs } from './defs/tlvs.ts'; export type { Tlv, TlvDefinition, TlvName, Tlvs } from './codec/tlvs.ts';
export type { DestAddress, ParamValue, TlvValue, UnsuccessSme, WireType } from './defs/types.ts'; export type { DestAddress, ParamValue, TlvValue, UnsuccessSme, WireType } from './codec/types.ts';
/** The spec tables, grouped the way `larvitsmpp.defs` was in 0.4.0. */ /** The spec tables, grouped the way `larvitsmpp.defs` was in 0.4.0. */
export { defs } from './defs/index.ts'; export const defs = {
cmds,
cmdsById,
consts,
constsById,
encodings,
errors,
errorsById,
tlvs,
tlvsById,
types,
};
+4 -4
View File
@@ -1,8 +1,8 @@
import type { Result } from './result.ts'; import type { Result } from './result.ts';
import type { EncodingName } from './defs/encodings.ts'; import type { EncodingName } from './codec/encodings.ts';
import { dataCodingByEncoding, detect, encodingByDataCoding, encodings, unencodable, unencodableText } from './defs/encodings.ts'; import { dataCodingByEncoding, detect, encodingByDataCoding, encodings, unencodable, unencodableText } from './codec/encodings.ts';
import { hasUdh } from './defs/constants.ts'; import { hasUdh } from './codec/constants.ts';
import { udhLength } from './udh.ts'; import { udhLength } from './protocol/udh.ts';
/** A single SMS carries 1120 bits, whatever the alphabet. */ /** A single SMS carries 1120 bits, whatever the alphabet. */
const singleMessageBits = 1120; const singleMessageBits = 1120;
@@ -1,8 +1,8 @@
import type { Dlr } from './dlr.ts'; import type { Dlr } from '../protocol/dlr.ts';
import type { MessageState } from './defs/constants.ts'; import type { MessageState } from '../codec/constants.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import { ExpiringGroups } from './expiring-groups.ts'; import { ExpiringGroups } from './expiring-groups.ts';
import { parseSegmentId } from './sms-id.ts'; import { parseSegmentId } from '../protocol/message-ids.ts';
export type MessageDlr = Dlr & { segments: Dlr[]; smsId: string }; export type MessageDlr = Dlr & { segments: Dlr[]; smsId: string };
@@ -1,12 +1,13 @@
import type { Concat } from './concat.ts'; import type { Concat } from '../protocol/concat.ts';
import type { PduObject } from './pdu.ts'; import type { PduObject } from '../codec/pdu.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import { ExpiringGroups } from './expiring-groups.ts'; import { ExpiringGroups } from './expiring-groups.ts';
import { decodeMessage } from './message.ts'; import { decodeMessage } from '../message.ts';
import { detach, retainedOctets } from './retained-pdu.ts'; import { defaults } from '../options.ts';
import { messageOctets } from './message-body.ts'; import { detach, retainedOctets } from '../codec/retained-pdu.ts';
import { paramNumber, paramText } from './defs/types.ts'; import { messageOctets } from '../protocol/message-body.ts';
import { uuidv7 } from './uuid.ts'; import { paramNumber, paramText } from '../codec/types.ts';
import { uuidv7 } from '../protocol/uuid.ts';
/** A concatenated message given up on, whose segments the peer has already been answered for. */ /** A concatenated message given up on, whose segments the peer has already been answered for. */
export type LostGroup = { export type LostGroup = {
@@ -42,8 +43,6 @@ export type Collected =
whole?: PduObject[] | undefined; whole?: PduObject[] | undefined;
}; };
export const defaultMaxOctets = 64 * 1024 * 1024;
type Group = { type Group = {
parts: Map<number, PduObject>; parts: Map<number, PduObject>;
smsId: string; smsId: string;
@@ -89,7 +88,7 @@ export class Reassembler {
private readonly onLost: (lost: LostGroup) => void; private readonly onLost: (lost: LostGroup) => void;
constructor(options: ReassemblerOptions) { constructor(options: ReassemblerOptions) {
this.maxOctets = options.maxOctets ?? defaultMaxOctets; this.maxOctets = options.maxOctets ?? defaults.maxOctets;
this.groups = new ExpiringGroups<Group>({ this.groups = new ExpiringGroups<Group>({
max: options.max, max: options.max,
maxWeight: this.maxOctets, maxWeight: this.maxOctets,
+14 -14
View File
@@ -1,17 +1,17 @@
import type { EncodingName, Unencodable } from './defs/encodings.ts'; import type { EncodingName, Unencodable } from '../codec/encodings.ts';
import type { ParamValue } from './defs/types.ts'; import type { ParamValue } from '../codec/types.ts';
import type { SubmitMessagingMode } from './defs/constants.ts'; import type { SubmitMessagingMode } from '../codec/constants.ts';
import type { PduObject, PduObjectInput } from './pdu.ts'; import type { PduObject, PduObjectInput } from '../codec/pdu.ts';
import type { Result } from './result.ts'; import type { Result } from '../result.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import type { SmsIdNotation } from './sms-id.ts'; import type { SmsIdNotation } from '../protocol/message-ids.ts';
import { UnansweredError } from './unanswered-error.ts'; import { UnansweredError } from '../unanswered-error.ts';
import { consts, defaultMessagingMode, isMessagingMode, isSubmitMessagingMode, submitMessagingModes } from './defs/constants.ts'; import { consts, defaultMessagingMode, isMessagingMode, isSubmitMessagingMode, submitMessagingModes } from '../codec/constants.ts';
import { cstring, paramText } from './defs/types.ts'; import { cstring, paramText } from '../codec/types.ts';
import { dataCodingByEncoding, detect, encodingNames, isEncodingName, unencodable, unencodableText } from './defs/encodings.ts'; import { dataCodingByEncoding, detect, encodingNames, isEncodingName, unencodable, unencodableText } from '../codec/encodings.ts';
import { namedValue } from './error-from.ts'; import { namedValue } from '../result.ts';
import { normaliseSmsId } from './sms-id.ts'; import { normaliseSmsId } from '../protocol/message-ids.ts';
import { maxSegments, smppTime, splitMessage } from './message.ts'; import { maxSegments, smppTime, splitMessage } from '../message.ts';
export type SendSmsOptions = { export type SendSmsOptions = {
dlr?: boolean; dlr?: boolean;
+25 -106
View File
@@ -1,77 +1,11 @@
import type { Dlr } from './dlr.ts'; import type { PduObject } from './codec/pdu.ts';
import type { MessageDlr } from './dlr-merger.ts';
import type { PduObject } from './pdu.ts';
import type { PduRefusedError } from './pdu-refusal.ts';
import type { Result, VoidResult } from './result.ts'; import type { Result, VoidResult } from './result.ts';
import type { Session } from './session.ts'; import type { Session } from './session/session.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from './log.ts';
import type { SmsIdFormat } from './sms-id.ts'; import type { SmsIdFormat } from './protocol/message-ids.ts';
import type { Sms } from './sms.ts';
import type { Socket } from 'node:net'; import type { Socket } from 'node:net';
import { backoffDefaults } from './reconnect-loop.ts'; import { isSmsIdNotation, smsIdNotations, smsIdPlaces } from './protocol/message-ids.ts';
import { defaultMaxOctets } from './reassembly.ts'; import { namedValue, quoted } from './result.ts';
import { isSmsIdNotation, smsIdNotations, smsIdPlaces } from './sms-id.ts';
import { namedValue } from './error-from.ts';
export type SessionEvents = {
close: [];
data: [Buffer];
disconnected: [];
dlr: [Dlr, PduObject];
incomingPdu: [Buffer];
incomingPduObj: [PduObject];
messageDlr: [MessageDlr];
reconnected: [];
sessionError: [Error | PduRefusedError];
sms: [Sms];
};
export const bindCommands: readonly string[] = [
'bind_receiver',
'bind_transceiver',
'bind_transmitter',
];
export type BindType = 'receiver' | 'transceiver' | 'transmitter';
/** Which end of the link a session is. Only `server()` is the SMSC; everything else is the ESME. */
export type LinkEnd = 'esme' | 'smsc';
export function bindTypeFromCommand(cmdName: string): BindType | undefined {
if (cmdName === 'bind_receiver') return 'receiver';
if (cmdName === 'bind_transceiver') return 'transceiver';
if (cmdName === 'bind_transmitter') return 'transmitter';
return undefined;
}
/**
* Which message-carrying command an inbound one stands in for. Every command but `data_sm` names
* its own direction; that one travels either way, so the end it arrived at is what says.
*/
export function standsInFor(cmdName: string, linkEnd: LinkEnd): string {
if (cmdName !== 'data_sm') return cmdName;
return linkEnd === 'smsc' ? 'submit_sm' : 'deliver_sm';
}
/**
* Whether a bind direction carries a command at all. A receiver-bound ESME submits nothing and a
* transmitter-bound one is delivered nothing, whichever end of the link is looking. A session that
* has not bound carries everything, since nothing has declared a direction yet.
*/
export function bindCarries(
bindType: BindType | undefined,
cmdName: string,
linkEnd: LinkEnd,
): boolean {
const carried = standsInFor(cmdName, linkEnd);
if (bindType === 'receiver') return carried !== 'submit_sm';
if (bindType === 'transmitter') return carried !== 'deliver_sm';
return true;
}
export type SendOptions = { signal?: AbortSignal | undefined }; export type SendOptions = { signal?: AbortSignal | undefined };
@@ -116,51 +50,36 @@ export type SessionOptions = {
systemId?: string | undefined; systemId?: string | undefined;
}; };
export const defaultSystemId = '';
/** SMPP 3.4: a peer that declares no version at all is one from before optional parameters. */
export const undeclaredInterfaceVersion = 0x00;
export type SessionBind = { as: BindType; peerVersion: number };
function quoted(value: unknown): string {
return typeof value === 'string' ? JSON.stringify(value) : namedValue(value);
}
function isBindType(value: unknown): value is BindType {
return typeof value === 'string' && bindTypeFromCommand(`bind_${value}`) !== undefined;
}
/** A bind as `Session.bound()` records it: undefined declares no version, which is pre-3.4. */
export function checkedBind(bindType: unknown, declaredVersion: unknown): Result<{ bind: SessionBind }> {
if (!isBindType(bindType)) {
return { err: new Error(`bindType must be receiver, transceiver or transmitter, the bind command's name without "bind_", got ${quoted(bindType)}`) };
}
if (declaredVersion === undefined) return { bind: { as: bindType, peerVersion: undeclaredInterfaceVersion } };
if (typeof declaredVersion !== 'number' || !Number.isInteger(declaredVersion) || declaredVersion < 0 || declaredVersion > 0xFF) {
return { err: new Error(`declaredVersion must be an integer 0-255, the interface_version param or the sc_interface_version TLV's tagValue, or undefined where the peer declared none, got ${quoted(declaredVersion)}`) };
}
return { bind: { as: bindType, peerVersion: declaredVersion } };
}
export const defaults = { export const defaults = {
bindType: 'transceiver',
connectTimeout: 10_000,
/** Receipts of a multipart message can be a working day apart, so the cap does the bounding. */ /** Receipts of a multipart message can be a working day apart, so the cap does the bounding. */
dlrMergeTimeout: 86_400_000, dlrMergeTimeout: 86_400_000,
enquireLinkInterval: 20_000,
/** The peer gave up on an unanswered message long before this; the bound is against growth. */ /** The peer gave up on an unanswered message long before this; the bound is against growth. */
heldMessageTimeout: 300_000, heldMessageTimeout: 300_000,
host: 'localhost',
/** The idle timeout is what notices a dead link, so it has to outlast one silent probe. */
idleTimeoutFactor: 2,
/** The version declared on the wire. */
interfaceVersion: 0x34,
maxDelay: 30_000,
maxDlrMerges: 1000, maxDlrMerges: 1000,
maxHeldMessages: 1000, maxHeldMessages: 1000,
maxHeldOctets: 64 * 1024 * 1024, maxHeldOctets: 64 * 1024 * 1024,
maxOctets: 64 * 1024 * 1024,
maxOutstanding: 10, maxOutstanding: 10,
maxReassembly: 1000, maxReassembly: 1000,
minDelay: 1000,
password: 'pass',
port: 2775,
reassemblyTimeout: 300_000, reassemblyTimeout: 300_000,
responseTimeout: 30_000, responseTimeout: 30_000,
serverIdleTimeout: 40_000,
shutdownTimeout: 5000, shutdownTimeout: 5000,
systemId: defaultSystemId, systemId: '',
}; username: 'user',
} as const;
/** /**
* A count below 1 does not fail loudly anywhere downstream: `maxOutstanding: 0` leaves every send * A count below 1 does not fail loudly anywhere downstream: `maxOutstanding: 0` leaves every send
@@ -187,7 +106,7 @@ export function checkSessionOptions(options: CheckableOptions): VoidResult {
function limitsOf(options: CheckableOptions): [string, number, number][] { function limitsOf(options: CheckableOptions): [string, number, number][] {
return [ return [
['idleTimeout', options.idleTimeout ?? 0, 0], ['idleTimeout', options.idleTimeout ?? 0, 0],
['maxOctets', options.maxOctets ?? defaultMaxOctets, 1], ['maxOctets', options.maxOctets ?? defaults.maxOctets, 1],
['maxOutstanding', options.maxOutstanding ?? defaults.maxOutstanding, 1], ['maxOutstanding', options.maxOutstanding ?? defaults.maxOutstanding, 1],
['maxReassembly', options.maxReassembly ?? defaults.maxReassembly, 1], ['maxReassembly', options.maxReassembly ?? defaults.maxReassembly, 1],
['reassemblyTimeout', options.reassemblyTimeout ?? defaults.reassemblyTimeout, 0], ['reassemblyTimeout', options.reassemblyTimeout ?? defaults.reassemblyTimeout, 0],
@@ -243,8 +162,8 @@ function checkReconnect(reconnect: unknown): VoidResult {
return { err: new Error(`reconnect.fromStart must be true or false, got ${typeof reconnect.fromStart}`) }; return { err: new Error(`reconnect.fromStart must be true or false, got ${typeof reconnect.fromStart}`) };
} }
const maxDelay = delayOr(reconnect.maxDelay, backoffDefaults.maxDelay); const maxDelay = delayOr(reconnect.maxDelay, defaults.maxDelay);
const minDelay = delayOr(reconnect.minDelay, backoffDefaults.minDelay); const minDelay = delayOr(reconnect.minDelay, defaults.minDelay);
// A delay of 0 never doubles, so the backoff never starts and every retry lands at once. // A delay of 0 never doubles, so the backoff never starts and every retry lands at once.
const checked = checkLimits([['maxDelay', maxDelay, 1], ['minDelay', minDelay, 1]]); const checked = checkLimits([['maxDelay', maxDelay, 1], ['minDelay', minDelay, 1]]);
+73
View File
@@ -0,0 +1,73 @@
import type { Result } from '../result.ts';
import { quoted } from '../result.ts';
export const bindCommands: readonly string[] = [
'bind_receiver',
'bind_transceiver',
'bind_transmitter',
];
export type BindType = 'receiver' | 'transceiver' | 'transmitter';
/** Which end of the link a session is. Only `server()` is the SMSC; everything else is the ESME. */
export type LinkEnd = 'esme' | 'smsc';
export function bindTypeFromCommand(cmdName: string): BindType | undefined {
if (cmdName === 'bind_receiver') return 'receiver';
if (cmdName === 'bind_transceiver') return 'transceiver';
if (cmdName === 'bind_transmitter') return 'transmitter';
return undefined;
}
/**
* Which message-carrying command an inbound one stands in for. Every command but `data_sm` names
* its own direction; that one travels either way, so the end it arrived at is what says.
*/
export function standsInFor(cmdName: string, linkEnd: LinkEnd): string {
if (cmdName !== 'data_sm') return cmdName;
return linkEnd === 'smsc' ? 'submit_sm' : 'deliver_sm';
}
/**
* Whether a bind direction carries a command at all. A receiver-bound ESME submits nothing and a
* transmitter-bound one is delivered nothing, whichever end of the link is looking. A session that
* has not bound carries everything, since nothing has declared a direction yet.
*/
export function bindCarries(
bindType: BindType | undefined,
cmdName: string,
linkEnd: LinkEnd,
): boolean {
const carried = standsInFor(cmdName, linkEnd);
if (bindType === 'receiver') return carried !== 'submit_sm';
if (bindType === 'transmitter') return carried !== 'deliver_sm';
return true;
}
/** SMPP 3.4: a peer that declares no version at all is one from before optional parameters. */
export const undeclaredInterfaceVersion = 0x00;
export type SessionBind = { as: BindType; peerVersion: number };
function isBindType(value: unknown): value is BindType {
return typeof value === 'string' && bindTypeFromCommand(`bind_${value}`) !== undefined;
}
/** A bind as `Session.bound()` records it: undefined declares no version, which is pre-3.4. */
export function checkedBind(bindType: unknown, declaredVersion: unknown): Result<{ bind: SessionBind }> {
if (!isBindType(bindType)) {
return { err: new Error(`bindType must be receiver, transceiver or transmitter, the bind command's name without "bind_", got ${quoted(bindType)}`) };
}
if (declaredVersion === undefined) return { bind: { as: bindType, peerVersion: undeclaredInterfaceVersion } };
if (typeof declaredVersion !== 'number' || !Number.isInteger(declaredVersion) || declaredVersion < 0 || declaredVersion > 0xFF) {
return { err: new Error(`declaredVersion must be an integer 0-255, the interface_version param or the sc_interface_version TLV's tagValue, or undefined where the peer declared none, got ${quoted(declaredVersion)}`) };
}
return { bind: { as: bindType, peerVersion: declaredVersion } };
}
+3 -3
View File
@@ -1,9 +1,9 @@
import type { ConcatInfo } from './udh.ts'; import type { ConcatInfo } from './udh.ts';
import type { PduObject } from './pdu.ts'; import type { PduObject } from '../codec/pdu.ts';
import { concatInfo } from './udh.ts'; import { concatInfo } from './udh.ts';
import { hasUdh } from './defs/constants.ts'; import { hasUdh } from '../codec/constants.ts';
import { messageOctets } from './message-body.ts'; import { messageOctets } from './message-body.ts';
import { paramNumber } from './defs/types.ts'; import { paramNumber } from '../codec/types.ts';
/** Where a segment sits in its message, and what ties it to the rest of that message. */ /** Where a segment sits in its message, and what ties it to the rest of that message. */
export type Concat = ConcatInfo & { export type Concat = ConcatInfo & {
+8 -8
View File
@@ -1,12 +1,12 @@
import type { MessageState } from './defs/constants.ts'; import type { MessageState } from '../codec/constants.ts';
import type { TlvValue } from './defs/types.ts'; import type { TlvValue } from '../codec/types.ts';
import type { PduObject } from './pdu.ts'; import type { PduObject } from '../codec/pdu.ts';
import type { SmsIdFormat } from './sms-id.ts'; import type { SmsIdFormat } from './message-ids.ts';
import { consts, constsById, hasUdh, messageTypeOf } from './defs/constants.ts'; import { consts, constsById, hasUdh, messageTypeOf } from '../codec/constants.ts';
import { encodings } from './defs/encodings.ts'; import { encodings } from '../codec/encodings.ts';
import { messageOctets } from './message-body.ts'; import { messageOctets } from './message-body.ts';
import { normaliseSmsId } from './sms-id.ts'; import { normaliseSmsId } from './message-ids.ts';
import { paramNumber, paramText } from './defs/types.ts'; import { paramNumber, paramText } from '../codec/types.ts';
import { udhLength } from './udh.ts'; import { udhLength } from './udh.ts';
/** /**
@@ -1,4 +1,4 @@
import type { PduObject } from './pdu.ts'; import type { PduObject } from '../codec/pdu.ts';
/** /**
* The user data, wherever the peer put it. SMPP 3.4 5.3.2.32 carries up to 64 KB in * The user data, wherever the peer put it. SMPP 3.4 5.3.2.32 carries up to 64 KB in
@@ -1,5 +1,5 @@
import type { CommandName } from './defs/commands.ts'; import type { CommandName } from '../codec/commands.ts';
import type { ParamValue } from './defs/types.ts'; import type { ParamValue } from '../codec/types.ts';
const notations = { const notations = {
decimal: { digits: /^[0-9]+$/, prefix: '' }, decimal: { digits: /^[0-9]+$/, prefix: '' },
View File
+23
View File
@@ -7,3 +7,26 @@ export type Result<T> =
| ({ err?: undefined } & T); | ({ err?: undefined } & T);
export type VoidResult = { err?: Error }; export type VoidResult = { err?: Error };
/** Whatever was thrown or rejected, as an Error. `String()` throws on some values; this cannot. */
export function errorFrom(reason: unknown): Error {
if (reason instanceof Error) return reason;
try {
return new Error(String(reason));
} catch {
return new Error('A thrown value that cannot be converted to a string');
}
}
const printable: readonly string[] = ['boolean', 'number', 'string'];
/** String() throws on a null-prototype object, so anything but these is named by its type. */
export function namedValue(value: unknown): string {
return printable.includes(typeof value) ? String(value) : typeof value;
}
/** A value named in an error: a string quoted, anything else as `namedValue()` names it. */
export function quoted(value: unknown): string {
return typeof value === 'string' ? JSON.stringify(value) : namedValue(value);
}
+13 -19
View File
@@ -1,19 +1,20 @@
import type { BindType, CloseOptions, OnRequest } from './session-options.ts'; import type { BindType } from '../protocol/bind.ts';
import type { PduObject, TlvInputs } from './pdu.ts'; import type { CloseOptions, OnRequest } from '../options.ts';
import type { Result, VoidResult } from './result.ts'; import type { PduObject, TlvInputs } from '../codec/pdu.ts';
import type { Result, VoidResult } 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 type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { Session, defaultSystemId } from './session.ts'; import { Session } from '../session/session.ts';
import { bindTypeFromCommand, checkSessionOptions } from './session-options.ts'; import { bindTypeFromCommand } from '../protocol/bind.ts';
import { checkSessionOptions, defaults } from '../options.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 { errorFrom } from '../result.ts';
import { errorFrom } from './error-from.ts'; import { paramText } from '../codec/types.ts';
import { paramText } from './defs/types.ts'; import { guardedLog } from '../log.ts';
import { guardedLog } from './log.ts'; import { respNameFor } from '../codec/commands.ts';
import { respNameFor } from './defs/commands.ts';
export type AuthenticateResult = { userData?: unknown } | boolean; export type AuthenticateResult = { userData?: unknown } | boolean;
@@ -49,13 +50,6 @@ export type ServerEvents = {
session: [Session]; session: [Session];
}; };
const defaults = {
idleTimeout: 40_000,
interfaceVersion: defaultInterfaceVersion,
port: 2775,
systemId: defaultSystemId,
};
/** A listener may return a promise: an `async` one that rejects is routed like one that throws. */ /** A listener may return a promise: an `async` one that rejects is routed like one that throws. */
type ServerListener<K extends keyof ServerEvents> = (...args: ServerEvents[K]) => unknown; type ServerListener<K extends keyof ServerEvents> = (...args: ServerEvents[K]) => unknown;
@@ -233,7 +227,7 @@ async function handleRequest(
function onConnection(sock: Socket, options: ServerOptions, server: SmppServer): void { function onConnection(sock: Socket, options: ServerOptions, server: SmppServer): void {
const log = guardedLog(options.log); const log = guardedLog(options.log);
const session = new Session({ const session = new Session({
idleTimeout: options.idleTimeout ?? defaults.idleTimeout, idleTimeout: options.idleTimeout ?? defaults.serverIdleTimeout,
log, log,
maxOutstanding: options.maxOutstanding, maxOutstanding: options.maxOutstanding,
maxOctets: options.maxOctets, maxOctets: options.maxOctets,
@@ -1,13 +1,13 @@
import type { LinkLife } from './link-life.ts'; import type { LinkLife } from './link-life.ts';
import type { PduObject, PduObjectInput } from './pdu.ts'; import type { PduObject, PduObjectInput } from '../codec/pdu.ts';
import type { Result } from './result.ts'; import type { Result } from '../result.ts';
import type { Session } from './session.ts'; import type { Session } from './session.ts';
import type { SmsHandlers } from './sms.ts'; import type { SmsHandlers } from './sms.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import { ExpiringGroups } from './expiring-groups.ts'; import { ExpiringGroups } from '../messages/expiring-groups.ts';
import { IdleWaiters } from './idle-waiters.ts'; import { IdleWaiters } from './idle-waiters.ts';
import { createSms } from './sms.ts'; import { createSms } from './sms.ts';
import { retainedOctets } from './retained-pdu.ts'; import { retainedOctets } from '../codec/retained-pdu.ts';
export type HeldMessagesOptions = { export type HeldMessagesOptions = {
link: LinkLife; link: LinkLife;
@@ -1,23 +1,24 @@
import type { Concat } from './concat.ts'; import type { Concat } from '../protocol/concat.ts';
import type { DlrMerger } from './dlr-merger.ts'; import type { DlrMerger } from '../messages/dlr-merger.ts';
import type { ErrorName } from './defs/errors.ts'; import type { ErrorName } from '../codec/errors.ts';
import type { HeldMessagesOptions } from './held-messages.ts'; import type { HeldMessagesOptions } from './held-messages.ts';
import type { LinkLife } from './link-life.ts'; import type { LinkLife } from './link-life.ts';
import type { LostGroup, Refusal } from './reassembly.ts'; import type { LostGroup, Refusal } from '../messages/reassembly.ts';
import type { OnRequest } from './session-options.ts'; import type { OnRequest } from '../options.ts';
import type { PduObject } from './pdu.ts'; import type { PduObject } from '../codec/pdu.ts';
import type { VoidResult } from './result.ts'; import type { VoidResult } from '../result.ts';
import type { Session } from './session.ts'; import type { Session } from './session.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import type { SmsIdFormat } from './sms-id.ts'; import type { SmsIdFormat } from '../protocol/message-ids.ts';
import { HeldMessages } from './held-messages.ts'; import { HeldMessages } from './held-messages.ts';
import { Reassembler } from './reassembly.ts'; import { Reassembler } from '../messages/reassembly.ts';
import { bindCommands, defaults, standsInFor } from './session-options.ts'; import { bindCommands, standsInFor } from '../protocol/bind.ts';
import { concatOf } from './concat.ts'; import { defaults } from '../options.ts';
import { detach } from './retained-pdu.ts'; import { concatOf } from '../protocol/concat.ts';
import { dlrFromPdu } from './dlr.ts'; import { detach } from '../codec/retained-pdu.ts';
import { respIdParams, segmentId } from './sms-id.ts'; import { dlrFromPdu } from '../protocol/dlr.ts';
import { respNameFor } from './defs/commands.ts'; import { respIdParams, segmentId } from '../protocol/message-ids.ts';
import { respNameFor } from '../codec/commands.ts';
/** Asks the peer to keep the message and retry. */ /** Asks the peer to keep the message and retry. */
function throttledStatus(carriedAs: string): ErrorName { function throttledStatus(carriedAs: string): ErrorName {
@@ -1,5 +1,5 @@
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import type { VoidResult } from './result.ts'; import type { VoidResult } from '../result.ts';
export type LinkLifeOptions = { export type LinkLifeOptions = {
log: SmppLog; log: SmppLog;
@@ -1,4 +1,4 @@
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
export type LinkTimersOptions = { export type LinkTimersOptions = {
/** How long between enquire_link probes. Undefined or 0 never probes. */ /** How long between enquire_link probes. Undefined or 0 never probes. */
@@ -1,14 +1,14 @@
import type { LinkLife } from './link-life.ts'; import type { LinkLife } from './link-life.ts';
import type { PduObject, PduObjectInput } from './pdu.ts'; import type { PduObject, PduObjectInput } from '../codec/pdu.ts';
import type { PduTransport } from './pdu-transport.ts'; import type { PduTransport } from './pdu-transport.ts';
import type { Result, VoidResult } from './result.ts'; import type { Result, VoidResult } from '../result.ts';
import type { SendOptions } from './session-options.ts'; import type { SendOptions } from '../options.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import { PendingRequests } from './pending-requests.ts'; import { PendingRequests } from './pending-requests.ts';
import { SendWindow } from './send-window.ts'; import { SendWindow } from './send-window.ts';
import { UnansweredError } from './unanswered-error.ts'; import { UnansweredError } from '../unanswered-error.ts';
import { bindCommands } from './session-options.ts'; import { bindCommands } from '../protocol/bind.ts';
import { objToPdu } from './pdu.ts'; import { objToPdu } from '../codec/pdu.ts';
export type OutgoingRequestsOptions = { export type OutgoingRequestsOptions = {
link: LinkLife; link: LinkLife;
@@ -1,10 +1,10 @@
import type { PduObject } from './pdu.ts'; import type { PduObject } from '../codec/pdu.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import type { Socket } from 'node:net'; import type { Socket } from 'node:net';
import type { VoidResult } from './result.ts'; import type { VoidResult } from '../result.ts';
import { PduFramer } from './pdu-framer.ts'; import { PduFramer } from '../codec/pdu-framer.ts';
import { PduRefusedError } from './pdu-refusal.ts'; import { PduRefusedError } from '../codec/refusal.ts';
import { pduToObj } from './pdu.ts'; import { pduToObj } from '../codec/pdu.ts';
export type PduTransportOptions = { export type PduTransportOptions = {
log: SmppLog; log: SmppLog;
@@ -1,7 +1,7 @@
import type { PduObject } from './pdu.ts'; import type { PduObject } from '../codec/pdu.ts';
import type { Result } from './result.ts'; import type { Result } from '../result.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import { maxSeqNr } from './pdu.ts'; import { maxSeqNr } from '../codec/pdu.ts';
export type WaitOptions = { export type WaitOptions = {
signal?: AbortSignal | undefined; signal?: AbortSignal | undefined;
@@ -1,11 +1,7 @@
import type { Result, VoidResult } from './result.ts'; import type { Result, VoidResult } from '../result.ts';
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import type { Socket } from 'node:net'; import type { Socket } from 'node:net';
import { defaults } from '../options.ts';
export const backoffDefaults = {
maxDelay: 30_000,
minDelay: 1000,
};
export type ReconnectLoopOptions = { export type ReconnectLoopOptions = {
connect: () => Promise<Result<{ sock: Socket }>>; connect: () => Promise<Result<{ sock: Socket }>>;
@@ -32,8 +28,8 @@ export class ReconnectLoop {
private upAt: number | undefined; private upAt: number | undefined;
constructor(options: ReconnectLoopOptions) { constructor(options: ReconnectLoopOptions) {
this.maxDelay = options.maxDelay ?? backoffDefaults.maxDelay; this.maxDelay = options.maxDelay ?? defaults.maxDelay;
this.minDelay = options.minDelay ?? backoffDefaults.minDelay; this.minDelay = options.minDelay ?? defaults.minDelay;
this.now = options.now ?? Date.now; this.now = options.now ?? Date.now;
this.options = options; this.options = options;
this.delay = this.minDelay; this.delay = this.minDelay;
@@ -1,5 +1,5 @@
import type { SmppLog } from './log.ts'; import type { SmppLog } from '../log.ts';
import type { VoidResult } from './result.ts'; import type { VoidResult } from '../result.ts';
import { IdleWaiters } from './idle-waiters.ts'; import { IdleWaiters } from './idle-waiters.ts';
export type SendWindowOptions = { export type SendWindowOptions = {
+35 -20
View File
@@ -1,14 +1,17 @@
import type { ErrorName } from './defs/errors.ts'; import type { Dlr } from '../protocol/dlr.ts';
import type { MessageDlr } from './dlr-merger.ts'; import type { ErrorName } from '../codec/errors.ts';
import type { ParamValue } from './defs/types.ts'; import type { MessageDlr } from '../messages/dlr-merger.ts';
import type { PduObject, PduObjectInput, TlvInputs } from './pdu.ts'; import type { ParamValue } from '../codec/types.ts';
import type { PduRefusedError } from './pdu-refusal.ts'; import type { PduObject, PduObjectInput, TlvInputs } from '../codec/pdu.ts';
import type { BindType, CloseOptions, LinkEnd, ReconnectOptions, SendOptions, SessionBind, SessionEvents, SessionOptions } from './session-options.ts'; import type { PduRefusedError } from '../codec/refusal.ts';
import type { Result, VoidResult } from './result.ts'; import type { BindType, LinkEnd, SessionBind } from '../protocol/bind.ts';
import type { SendSmsOptions, SendSmsResult } from './send-sms.ts'; import type { CloseOptions, ReconnectOptions, SendOptions, SessionOptions } from '../options.ts';
import type { SmppLog } from './log.ts'; import type { Result, VoidResult } from '../result.ts';
import type { SendSmsOptions, SendSmsResult } from '../messages/submit.ts';
import type { SmppLog } from '../log.ts';
import type { Sms } from './sms.ts';
import type { Socket } from 'node:net'; import type { Socket } from 'node:net';
import { DlrMerger } from './dlr-merger.ts'; import { DlrMerger } from '../messages/dlr-merger.ts';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { IncomingRequests } from './incoming-requests.ts'; import { IncomingRequests } from './incoming-requests.ts';
import { LinkLife } from './link-life.ts'; import { LinkLife } from './link-life.ts';
@@ -17,14 +20,15 @@ import { OutgoingRequests } from './outgoing-requests.ts';
import { PduTransport } from './pdu-transport.ts'; import { PduTransport } from './pdu-transport.ts';
import { ReconnectLoop } from './reconnect-loop.ts'; import { ReconnectLoop } from './reconnect-loop.ts';
import { leftOf } from './idle-waiters.ts'; import { leftOf } from './idle-waiters.ts';
import { errorFrom } from './error-from.ts'; import { errorFrom } from '../result.ts';
import { optionalParamsMinVersion } from './defs/constants.ts'; import { optionalParamsMinVersion } from '../codec/constants.ts';
import { bindCarries, bindCommands, checkedBind, defaultSystemId, defaults } from './session-options.ts'; import { bindCarries, checkedBind } from '../protocol/bind.ts';
import { isResp, objToPdu, pduReturn } from './pdu.ts'; import { defaults } from '../options.ts';
import { refusalAnswer } from './pdu-refusal.ts'; import { isResp, objToPdu, pduReturn } from '../codec/pdu.ts';
import { guardedLog } from './log.ts'; import { refusalAnswer } from '../codec/refusal.ts';
import { submitSms, unsent } from './send-sms.ts'; import { guardedLog } from '../log.ts';
import { ConcatReference } from './udh.ts'; import { submitSms, unsent } from '../messages/submit.ts';
import { ConcatReference } from '../protocol/udh.ts';
export type { export type {
CloseOptions, CloseOptions,
@@ -33,11 +37,22 @@ export type {
SendOptions, SendOptions,
SendSmsOptions, SendSmsOptions,
SendSmsResult, SendSmsResult,
SessionEvents,
SessionOptions, SessionOptions,
}; };
export type { BindType }; export type { BindType };
export { bindCommands, defaultSystemId };
export type SessionEvents = {
close: [];
data: [Buffer];
disconnected: [];
dlr: [Dlr, PduObject];
incomingPdu: [Buffer];
incomingPduObj: [PduObject];
messageDlr: [MessageDlr];
reconnected: [];
sessionError: [Error | PduRefusedError];
sms: [Sms];
};
/** A listener may return a promise: an `async` one that rejects is routed like one that throws. */ /** A listener may return a promise: an `async` one that rejects is routed like one that throws. */
type SessionListener<K extends keyof SessionEvents> = (...args: SessionEvents[K]) => unknown; type SessionListener<K extends keyof SessionEvents> = (...args: SessionEvents[K]) => unknown;
+13 -13
View File
@@ -1,17 +1,17 @@
import type { ErrorName } from './defs/errors.ts'; import type { ErrorName } from '../codec/errors.ts';
import type { MessageState } from './defs/constants.ts'; import type { MessageState } from '../codec/constants.ts';
import type { PduObject, PduObjectInput, TlvInputs } from './pdu.ts'; import type { PduObject, PduObjectInput, TlvInputs } from '../codec/pdu.ts';
import type { Result, VoidResult } from './result.ts'; 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 '../codec/constants.ts';
import { decodeSegments } from './reassembly.ts'; import { decodeSegments } from '../messages/reassembly.ts';
import { messageClassOf } from './defs/encodings.ts'; import { messageClassOf } from '../codec/encodings.ts';
import { paramText } from './defs/types.ts'; import { paramText } from '../codec/types.ts';
import { receiptCodes, transientStates } from './dlr.ts'; import { receiptCodes, transientStates } from '../protocol/dlr.ts';
import { smppDate } from './message.ts'; import { smppDate } from '../message.ts';
import { respIdParams, segmentId } from './sms-id.ts'; import { respIdParams, segmentId } from '../protocol/message-ids.ts';
import { uuidv7 } from './uuid.ts'; import { uuidv7 } from '../protocol/uuid.ts';
/** `pduObjs` holds what the peer took, so a partial failure names what is already receipted. */ /** `pduObjs` holds what the peer took, so a partial failure names what is already receipted. */
export type SendDlrResult = { export type SendDlrResult = {
+2 -2
View File
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { PduParams, PduParamsInput } from '../src/defs/commands.ts'; import type { PduParams, PduParamsInput } from '../src/codec/commands.ts';
import { cmds, cmdsById, commandNameById, isCommandName } from '../src/defs/commands.ts'; import { cmds, cmdsById, commandNameById, isCommandName } from '../src/codec/commands.ts';
describe('command table', () => { describe('command table', () => {
test('every command is reachable by name and by id', () => { test('every command is reachable by name and by id', () => {
+8 -8
View File
@@ -1,16 +1,16 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts'; import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from './teardown.ts'; import { closeAfter } from './teardown.ts';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { decodeMessage } from '../src/message.ts'; import { decodeMessage } from '../src/message.ts';
import { dlrFromPdu } from '../src/dlr.ts'; import { dlrFromPdu } from '../src/protocol/dlr.ts';
import { encodingByDataCoding, encodings } from '../src/defs/encodings.ts'; import { encodingByDataCoding, encodings } from '../src/codec/encodings.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts'; import { objToPdu, pduToObj } from '../src/codec/pdu.ts';
import { paramNumber } from '../src/defs/types.ts'; import { paramNumber } from '../src/codec/types.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
import type { PduObject } from '../src/pdu.ts'; import type { PduObject } from '../src/codec/pdu.ts';
const from = '46701113311'; const from = '46701113311';
const to = '46709771337'; const to = '46709771337';
+4 -4
View File
@@ -1,10 +1,10 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { dlrFromPdu, parseReceipt, receiptCodes } from '../src/dlr.ts'; import { dlrFromPdu, parseReceipt, receiptCodes } from '../src/protocol/dlr.ts';
import { encodeMessage } from '../src/message.ts'; import { encodeMessage } from '../src/message.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts'; import { objToPdu, pduToObj } from '../src/codec/pdu.ts';
import type { PduObject, TlvInputs } from '../src/pdu.ts'; import type { PduObject, TlvInputs } from '../src/codec/pdu.ts';
const receiptText = 'id:0195f0c7 sub:001 dlvrd:001 submit date:2508251430 done date:2508251431 stat:DELIVRD err:000 text:hello there'; const receiptText = 'id:0195f0c7 sub:001 dlvrd:001 submit date:2508251430 done date:2508251431 stat:DELIVRD err:000 text:hello there';
+6 -6
View File
@@ -1,13 +1,13 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import net from 'node:net'; import net from 'node:net';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
import { PduFramer } from '../src/pdu-framer.ts'; import { PduFramer } from '../src/codec/pdu-framer.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter, closeListenerAfter } from './teardown.ts'; import { closeAfter, closeListenerAfter } from './teardown.ts';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { objToPdu, pduReturn, pduToObj } from '../src/pdu.ts'; import { objToPdu, pduReturn, pduToObj } from '../src/codec/pdu.ts';
import { uuidv7 } from '../src/uuid.ts'; import { uuidv7 } from '../src/protocol/uuid.ts';
export type DummySmsc = { export type DummySmsc = {
/** Writes a delivery receipt to the ESME, its body spelled as the test names it. */ /** Writes a delivery receipt to the ESME, its body spelled as the test names it. */
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import { dataCodingByEncoding, detect, encodingByDataCoding, encodings, isEncodingName, unencodable } from '../src/defs/encodings.ts'; import { dataCodingByEncoding, detect, encodingByDataCoding, encodings, isEncodingName, unencodable } from '../src/codec/encodings.ts';
describe('ASCII (GSM 03.38)', () => { describe('ASCII (GSM 03.38)', () => {
const samples: [string, number[]][] = [ const samples: [string, number[]][] = [
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { errorFrom } from '../src/error-from.ts'; import { errorFrom } from '../src/result.ts';
test('carries an Error through and describes anything else, including what String() refuses', () => { test('carries an Error through and describes anything else, including what String() refuses', () => {
const original = new Error('the original'); const original = new Error('the original');
+5 -5
View File
@@ -2,12 +2,12 @@ import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import reference from 'smpp'; import reference from 'smpp';
import type { ReferenceSession } from 'smpp'; import type { ReferenceSession } from 'smpp';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from './teardown.ts'; import { closeAfter } from './teardown.ts';
import { concatInfo } from '../src/udh.ts'; import { concatInfo } from '../src/protocol/udh.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts'; import { objToPdu, pduToObj } from '../src/codec/pdu.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
import { splitMessage } from '../src/message.ts'; import { splitMessage } from '../src/message.ts';
/** /**
+10 -10
View File
@@ -1,19 +1,19 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { PduObjectInput } from '../src/pdu.ts'; import type { PduObjectInput } from '../src/codec/pdu.ts';
import type { SendSmsDeps } from '../src/send-sms.ts'; import type { SendSmsDeps } from '../src/messages/submit.ts';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts'; import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from './teardown.ts'; import { closeAfter } from './teardown.ts';
import { messageClassOf } from '../src/defs/encodings.ts'; import { messageClassOf } from '../src/codec/encodings.ts';
import { paramNumber } from '../src/defs/types.ts'; import { paramNumber } from '../src/codec/types.ts';
import { pduToObj } from '../src/pdu.ts'; import { pduToObj } from '../src/codec/pdu.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/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/messages/submit.ts';
const from = '46701113311'; const from = '46701113311';
const to = '46709771337'; const to = '46709771337';
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { EncodingName } from '../src/defs/encodings.ts'; import type { EncodingName } from '../src/codec/encodings.ts';
import { import {
bitCount, bitCount,
decodeMessage, decodeMessage,
+8 -8
View File
@@ -1,16 +1,16 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { PduObjectInput } from '../src/pdu.ts'; import type { PduObjectInput } from '../src/codec/pdu.ts';
import type { SendSmsDeps } from '../src/send-sms.ts'; import type { SendSmsDeps } from '../src/messages/submit.ts';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { SubmitMessagingMode } from '../src/defs/constants.ts'; import type { SubmitMessagingMode } from '../src/codec/constants.ts';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts'; import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { consts, submitMessagingModes } from '../src/defs/constants.ts'; import { consts, submitMessagingModes } from '../src/codec/constants.ts';
import { paramNumber } from '../src/defs/types.ts'; import { paramNumber } from '../src/codec/types.ts';
import { pduToObj } from '../src/pdu.ts'; import { pduToObj } from '../src/codec/pdu.ts';
import { silentLog } from '../src/log.ts'; import { silentLog } from '../src/log.ts';
import { submitSms } from '../src/send-sms.ts'; import { submitSms } from '../src/messages/submit.ts';
const from = '46701113311'; const from = '46701113311';
const to = '46709771337'; const to = '46709771337';
+6 -6
View File
@@ -1,12 +1,12 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { Dlr, Receipt } from '../src/dlr.ts'; import type { Dlr, Receipt } from '../src/protocol/dlr.ts';
import type { MessageDlr } from '../src/session.ts'; import type { MessageDlr } from '../src/session/session.ts';
import type { PduObject, TlvInputs } from '../src/pdu.ts'; import type { PduObject, TlvInputs } from '../src/codec/pdu.ts';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts'; import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { dlrFromPdu, parseReceipt, receiptCodes, transientStates } from '../src/dlr.ts'; import { dlrFromPdu, parseReceipt, receiptCodes, transientStates } from '../src/protocol/dlr.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts'; import { objToPdu, pduToObj } from '../src/codec/pdu.ts';
/** /**
* Receipt bodies as commercial operators document them, from `interop-tests/research/operator-quirks.md` * Receipt bodies as commercial operators document them, from `interop-tests/research/operator-quirks.md`
+2 -2
View File
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import { PduFramer } from '../src/pdu-framer.ts'; import { PduFramer } from '../src/codec/pdu-framer.ts';
import { objToPdu } from '../src/pdu.ts'; import { objToPdu } from '../src/codec/pdu.ts';
function pdu(seqNr: number): Buffer { function pdu(seqNr: number): Buffer {
const { buffer } = objToPdu({ cmdName: 'enquire_link', seqNr }); const { buffer } = objToPdu({ cmdName: 'enquire_link', seqNr });
+4 -4
View File
@@ -1,9 +1,9 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import { PduRefusedError, refusalAnswer } from '../src/pdu-refusal.ts'; import { PduRefusedError, refusalAnswer } from '../src/codec/refusal.ts';
import { isCommand, isResp, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts'; import { isCommand, isResp, objToPdu, pduReturn, pduToObj } from '../src/codec/pdu.ts';
import { paramText } from '../src/defs/types.ts'; import { paramText } from '../src/codec/types.ts';
import { isTlvName, tlvsById } from '../src/defs/tlvs.ts'; import { isTlvName, tlvsById } from '../src/codec/tlvs.ts';
function encode(...args: Parameters<typeof objToPdu>): Buffer { function encode(...args: Parameters<typeof objToPdu>): Buffer {
const { buffer, err } = objToPdu(...args); const { buffer, err } = objToPdu(...args);
+2 -2
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import type { PduObjectInput } from '../src/pdu.ts'; import type { PduObjectInput } from '../src/codec/pdu.ts';
import { objToPdu } from '../src/pdu.ts'; import { objToPdu } from '../src/codec/pdu.ts';
/** The octets a test writes straight to a socket, which objToPdu builds for every valid PDU. */ /** The octets a test writes straight to a socket, which objToPdu builds for every valid PDU. */
export function pduBytes(input: PduObjectInput): Buffer { export function pduBytes(input: PduObjectInput): Buffer {
+8 -8
View File
@@ -1,16 +1,16 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { Dlr } from '../src/dlr.ts'; import type { Dlr } from '../src/protocol/dlr.ts';
import type { Session } from '../src/session.ts'; import type { Session } from '../src/session/session.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import type { SmppLog } from '../src/log.ts'; import type { SmppLog } from '../src/log.ts';
import type { SmppServer } from '../src/server.ts'; import type { SmppServer } from '../src/server/server.ts';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
import { PduRefusedError } from '../src/pdu-refusal.ts'; import { PduRefusedError } from '../src/codec/refusal.ts';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from './teardown.ts'; import { closeAfter } from './teardown.ts';
import { isCommand, objToPdu } from '../src/pdu.ts'; import { isCommand, objToPdu } from '../src/codec/pdu.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> { function once<T>(register: (resolve: (value: T) => void) => void): Promise<T> {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
+30 -29
View File
@@ -1,42 +1,43 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import net from 'node:net'; import net from 'node:net';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { Collected, LostGroup } from '../src/reassembly.ts'; import type { Collected, LostGroup } from '../src/messages/reassembly.ts';
import type { Dlr } from '../src/dlr.ts'; import type { Dlr } from '../src/protocol/dlr.ts';
import type { ErrorName } from '../src/defs/errors.ts'; import type { ErrorName } from '../src/codec/errors.ts';
import type { IncomingRequestsOptions } from '../src/incoming-requests.ts'; import type { IncomingRequestsOptions } from '../src/session/incoming-requests.ts';
import type { HeldMessagesOptions, MessageHold } from '../src/held-messages.ts'; import type { HeldMessagesOptions, MessageHold } from '../src/session/held-messages.ts';
import type { MessageState } from '../src/defs/constants.ts'; import type { MessageState } from '../src/codec/constants.ts';
import type { MessageDlr } from '../src/session.ts'; import type { MessageDlr } from '../src/session/session.ts';
import type { PduObject, PduObjectInput } from '../src/pdu.ts'; import type { PduObject, PduObjectInput } from '../src/codec/pdu.ts';
import type { Result } from '../src/result.ts'; import type { Result } from '../src/result.ts';
import type { SendSmsResult } from '../src/send-sms.ts'; import type { SendSmsResult } from '../src/messages/submit.ts';
import type { SmppLog } from '../src/log.ts'; import type { SmppLog } from '../src/log.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import type { SmppServer } from '../src/server.ts'; import type { SmppServer } from '../src/server/server.ts';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
import { HeldMessages } from '../src/held-messages.ts'; import { HeldMessages } from '../src/session/held-messages.ts';
import { IncomingRequests, refusedSegmentStatus } from '../src/incoming-requests.ts'; import { IncomingRequests, refusedSegmentStatus } from '../src/session/incoming-requests.ts';
import { UnansweredError } from '../src/unanswered-error.ts'; import { UnansweredError } from '../src/unanswered-error.ts';
import { createSms } from '../src/sms.ts'; import { createSms } from '../src/session/sms.ts';
import { LinkLife } from '../src/link-life.ts'; import { LinkLife } from '../src/session/link-life.ts';
import { SendWindow } from '../src/send-window.ts'; import { SendWindow } from '../src/session/send-window.ts';
import { Reassembler, decodeSegments } from '../src/reassembly.ts'; import { Reassembler, decodeSegments } from '../src/messages/reassembly.ts';
import { Session } from '../src/session.ts'; import { Session } from '../src/session/session.ts';
import { DlrMerger } from '../src/dlr-merger.ts'; import { DlrMerger } from '../src/messages/dlr-merger.ts';
import { PduRefusedError } from '../src/pdu-refusal.ts'; import { PduRefusedError } from '../src/codec/refusal.ts';
import { objToPdu } from '../src/pdu.ts'; import { objToPdu } from '../src/codec/pdu.ts';
import { checkSessionOptions, defaults, standsInFor } from '../src/session-options.ts'; import { checkSessionOptions, defaults } from '../src/options.ts';
import { client } from '../src/client.ts'; import { standsInFor } from '../src/protocol/bind.ts';
import { client } from '../src/client/client.ts';
import { closeAfter, closeListenerAfter } from './teardown.ts'; import { closeAfter, closeListenerAfter } from './teardown.ts';
import { concatOf } from '../src/concat.ts'; import { concatOf } from '../src/protocol/concat.ts';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { errors } from '../src/defs/errors.ts'; import { errors } from '../src/codec/errors.ts';
import { paramNumber, paramText } from '../src/defs/types.ts'; import { paramNumber, paramText } from '../src/codec/types.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
import { silentLog } from '../src/log.ts'; import { silentLog } from '../src/log.ts';
import { splitMessage } from '../src/message.ts'; import { splitMessage } from '../src/message.ts';
import { submitSms, submitSmParams } from '../src/send-sms.ts'; import { submitSms, submitSmParams } from '../src/messages/submit.ts';
async function startServer( async function startServer(
t: TestContext, t: TestContext,
+16 -15
View File
@@ -1,25 +1,26 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import net from 'node:net'; import net from 'node:net';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { Dlr } from '../src/dlr.ts'; import type { Dlr } from '../src/protocol/dlr.ts';
import type { PduObject, PduObjectInput } from '../src/pdu.ts'; import type { PduObject, PduObjectInput } from '../src/codec/pdu.ts';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import type { ServerOptions, SmppServer } from '../src/server.ts'; import type { ServerOptions, SmppServer } from '../src/server/server.ts';
import type { SmppLog } from '../src/log.ts'; import type { SmppLog } from '../src/log.ts';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
import type { VoidResult } from '../src/result.ts'; import type { VoidResult } from '../src/result.ts';
import { DlrMerger } from '../src/dlr-merger.ts'; import { DlrMerger } from '../src/messages/dlr-merger.ts';
import { PduFramer } from '../src/pdu-framer.ts'; import { PduFramer } from '../src/codec/pdu-framer.ts';
import { ReconnectLoop } from '../src/reconnect-loop.ts'; import { ReconnectLoop } from '../src/session/reconnect-loop.ts';
import { Session, bindCommands } from '../src/session.ts'; import { Session } from '../src/session/session.ts';
import { checkSessionOptions } from '../src/session-options.ts'; import { bindCommands } from '../src/protocol/bind.ts';
import { client } from '../src/client.ts'; import { checkSessionOptions } from '../src/options.ts';
import { client } from '../src/client/client.ts';
import { closeAfter, closeListenerAfter } from './teardown.ts'; import { closeAfter, closeListenerAfter } from './teardown.ts';
import { consts } from '../src/defs/constants.ts'; import { consts } from '../src/codec/constants.ts';
import { PduRefusedError } from '../src/pdu-refusal.ts'; import { PduRefusedError } from '../src/codec/refusal.ts';
import { isCommand, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts'; import { isCommand, objToPdu, pduReturn, pduToObj } from '../src/codec/pdu.ts';
import { paramText } from '../src/defs/types.ts'; import { paramText } from '../src/codec/types.ts';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
import { bareTlvHeader, pduBytes, shortened, truncatedTlv, withUnknownCmdId } from './raw-pdus.ts'; import { bareTlvHeader, pduBytes, shortened, truncatedTlv, withUnknownCmdId } from './raw-pdus.ts';
import { silentLog } from '../src/log.ts'; import { silentLog } from '../src/log.ts';
import { splitMessage } from '../src/message.ts'; import { splitMessage } from '../src/message.ts';
+1 -1
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import { normaliseSmsId, parseSegmentId, respIdParams, segmentId } from '../src/sms-id.ts'; import { normaliseSmsId, parseSegmentId, respIdParams, segmentId } from '../src/protocol/message-ids.ts';
describe('normaliseSmsId()', () => { describe('normaliseSmsId()', () => {
test('reads an id the length a message_id may be, and leaves a longer one alone', () => { test('reads an id the length a message_id may be, and leaves a longer one alone', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { CloseOptions } from '../src/session-options.ts'; import type { CloseOptions } from '../src/options.ts';
import type { Server, Socket } from 'node:net'; import type { Server, Socket } from 'node:net';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
+4 -4
View File
@@ -1,15 +1,15 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import net from 'node:net'; import net from 'node:net';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { Sms } from '../src/sms.ts'; import type { Sms } from '../src/session/sms.ts';
import type { SmppServer } from '../src/server.ts'; import type { SmppServer } from '../src/server/server.ts';
import type { TestContext } from 'node:test'; import type { TestContext } from 'node:test';
import { Log } from '@larvit/log'; import { Log } from '@larvit/log';
import { TLSSocket } from 'node:tls'; import { TLSSocket } from 'node:tls';
import { client } from '../src/client.ts'; import { client } from '../src/client/client.ts';
import { closeAfter } from './teardown.ts'; import { closeAfter } from './teardown.ts';
import { generateKeyPairSync, randomBytes, sign } from 'node:crypto'; import { generateKeyPairSync, randomBytes, sign } from 'node:crypto';
import { server } from '../src/server.ts'; import { server } from '../src/server/server.ts';
const host = 'localhost'; const host = 'localhost';
+3 -3
View File
@@ -1,8 +1,8 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { DestAddress, UnsuccessSme } from '../src/defs/types.ts'; import type { DestAddress, UnsuccessSme } from '../src/codec/types.ts';
import { paramText, types } from '../src/defs/types.ts'; import { paramText, types } from '../src/codec/types.ts';
import { tlvs } from '../src/defs/tlvs.ts'; import { tlvs } from '../src/codec/tlvs.ts';
describe('integers', () => { describe('integers', () => {
test('int8 reads, sizes and writes one octet', () => { test('int8 reads, sizes and writes one octet', () => {
+6 -6
View File
@@ -1,14 +1,14 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test, { describe } from 'node:test'; import test, { describe } from 'node:test';
import type { PduObjectInput } from '../src/pdu.ts'; import type { PduObjectInput } from '../src/codec/pdu.ts';
import type { SendSmsDeps, SendSmsInput } from '../src/send-sms.ts'; import type { SendSmsDeps, SendSmsInput } from '../src/messages/submit.ts';
import { bindToSmsc, dummySmsc } from './dummy-smsc.ts'; import { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
import { decodeMessage } from '../src/message.ts'; import { decodeMessage } from '../src/message.ts';
import { messageOctets } from '../src/message-body.ts'; import { messageOctets } from '../src/protocol/message-body.ts';
import { objToPdu, pduToObj } from '../src/pdu.ts'; import { objToPdu, pduToObj } from '../src/codec/pdu.ts';
import { paramNumber, paramText } from '../src/defs/types.ts'; import { paramNumber, paramText } from '../src/codec/types.ts';
import { silentLog } from '../src/log.ts'; import { silentLog } from '../src/log.ts';
import { submitSms } from '../src/send-sms.ts'; import { submitSms } from '../src/messages/submit.ts';
const from = '46701113311'; const from = '46701113311';
const to = '46709771337'; const to = '46709771337';
+40 -34
View File
@@ -207,6 +207,10 @@ not SMPP's difficulty. Maintainer's call, 2026-09-30: build plan 3. The backgrou
[docs/comprehension-rewrite/](docs/comprehension-rewrite/): the plans, the board, every panel report, [docs/comprehension-rewrite/](docs/comprehension-rewrite/): the plans, the board, every panel report,
the lessons and the six drafts as patches. Delete that directory once this milestone ships. the lessons and the six drafts as patches. Delete that directory once this milestone ships.
The move chunk (#53) read 6, 6, 6 and 6, Navigation 7 at every seat, Locality 5, 5, 5 and 6, Shape 6
at every seat, Self-sufficiency 5, 6, 7 and 8. Every seat's Locality cause is the lifecycle cluster
the split below rewrites.
Every chunk below ships through `/larv-review`, and before it merges it also runs the Every chunk below ships through `/larv-review`, and before it merges it also runs the
comprehension-panel scoring run over the whole project. Each round: comprehension-panel scoring run over the whole project. Each round:
@@ -215,17 +219,13 @@ comprehension-panel scoring run over the whole project. Each round:
- let no dimension drop, and record the four scores and the overall in this paragraph. - let no dimension drop, and record the four scores and the overall in this paragraph.
Each chunk also closes the items further down that it absorbs, and its PR names them: `hold` and Each chunk also closes the items further down that it absorbs, and its PR names them: `hold` and
`refusing`, the three `defaults`, `ASCII`, `session-options.ts`, the flat `src/`, and the link that `refusing`, `ASCII`, and the link that dropped mid-rebind.
dropped mid-rebind.
Every chunk that changes the public API updates README's examples, MIGRATION.md, CHANGELOG.md, Every chunk that changes the public API updates README's examples, MIGRATION.md, CHANGELOG.md,
docs/decisions.md and the AGENTS.md map in its own PR, and deletes what it replaces; no old and new docs/decisions.md and the AGENTS.md map in its own PR, and deletes what it replaces; no old and new
file stand side by side. The architecture review of 2026-09-30 (ALIGN) set this order; its amendments file stand side by side. The architecture review of 2026-09-30 (ALIGN) set this order; its amendments
to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture-review-2026-09-30). to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture-review-2026-09-30).
- [ ] **Move without changing behaviour.** `defs/` becomes `codec/`, `options.ts` becomes one defaults
table, and `result.ts` absorbs `error-from.ts`. Every file with one target moves to its folder
now, and the "`src/` stays flat" decision is retired in the same PR.
- [ ] **Build `protocol/`: every multi-fact octet read once into a named plain type.** - [ ] **Build `protocol/`: every multi-fact octet read once into a named plain type.**
- `vocabulary.ts`: the glossary as types with one-line TSDoc. - `vocabulary.ts`: the glossary as types with one-line TSDoc.
- `data-coding.ts`: the `data_coding` table as rows, with a test that it equals today's function - `data-coding.ts`: the `data_coding` table as rows, with a test that it equals today's function
@@ -234,6 +234,8 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
reference counter goes to `SmppClient` in the lifecycle split. reference counter goes to `SmppClient` in the lifecycle split.
- The `gsm7` rename, A6 included: `'ASCII'` becomes `'GSM7'` in every export. - The `gsm7` rename, A6 included: `'ASCII'` becomes `'GSM7'` in every export.
- A test that fails on a spec citation without its sentence. - A test that fails on a spec citation without its sentence.
- Settle where `encodeBody()` and `decodeMessage()` land before splitting `message.ts`: `codec/pdu.ts`
reads them, so a home in `protocol/` is an upward edge.
- [ ] **Build `messages/` on a `BoundedStore` that enforces its own bounds.** Reassembly refuses at its - [ ] **Build `messages/` on a `BoundedStore` that enforces its own bounds.** Reassembly refuses at its
bound instead of evicting, recorded against goals 2 and 4; the receipt merge follows Q8 and its bound instead of evicting, recorded against goals 2 and 4; the receipt merge follows Q8 and its
spent set expires by age. README's bound text is updated. spent set expires by age. README's bound text is updated.
@@ -254,6 +256,17 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
### Correctness ### Correctness
- [ ] **Bound a rebind's wait when `responseTimeout` is 0.** `comeBackUp()` arms no timers until the
bind is answered, so an SMSC that takes the TCP connection and never answers leaves
`ReconnectLoop` attempting forever: no `close`, no next attempt. `answering()` already falls
back to the default for held messages; the bind has no such floor. From the 2026-09-30 scoring
run; confirm with a test first.
- [ ] **Type a parsed `short_message` as what the parser puts there.** `parsePdu()` replaces it with
a decoded string wherever no UDH is present, while `PduParams<'submit_sm'>['short_message']`
is `Buffer` and `isCommand()`'s docstring calls the narrowing sound. Public type; from the
2026-09-30 scoring run.
- [ ] **Refuse to open a link that dropped while its rebind was answered.** A peer sending - [ ] **Refuse to open a link that dropped while its rebind was answered.** A peer sending
`bind_resp` and FIN together can tear the link down before `comeBackUp()` resumes; it then `bind_resp` and FIN together can tear the link down before `comeBackUp()` resumes; it then
calls `link.open()` on a `down` link, `resetTimers()` skips, and `attempt()` reports success, calls `link.open()` on a `down` link, `resetTimers()` skips, and `attempt()` reports success,
@@ -277,7 +290,7 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
- [ ] **Return an `err` where `message` is not a string, rather than throwing.** - [ ] **Return an `err` where `message` is not a string, rather than throwing.**
`sendSms({ message: undefined })` — a forgotten property — reaches `value.replace()` in `sendSms({ message: undefined })` — a forgotten property — reaches `value.replace()` in
`defs/encodings.ts` through the alphabet detection `checkOptions()` runs, and the `TypeError` `codec/encodings.ts` through the alphabet detection `checkOptions()` runs, and the `TypeError`
escapes `submitSms()` into the caller's process; `NaN` and `12345` do the same. README promises escapes `submitSms()` into the caller's process; `NaN` and `12345` do the same. README promises
"Never throws. Every fallible call resolves to `{ err?, … }`" and AGENTS.md hard rule 1 says it "Never throws. Every fallible call resolves to `{ err?, … }`" and AGENTS.md hard rule 1 says it
again, so the docs are false for the likeliest caller mistake there is. From the stability again, so the docs are false for the likeliest caller mistake there is. From the stability
@@ -300,7 +313,7 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
Goals 2 then 3: `from: 1e21` is reported as sent to an address that reaches nobody, which is Goals 2 then 3: `from: 1e21` is reported as sent to an address that reaches nobody, which is
the wrong answer about what happened before it is laxness in what we send. That a number is the wrong answer about what happened before it is laxness in what we send. That a number is
accepted at all reaches a consumer in no sentence either: only the type comment at accepted at all reaches a consumer in no sentence either: only the type comment at
`defs/commands.ts:239`, and one CHANGELOG line that stops being visible when `codec/commands.ts`, and one CHANGELOG line that stops being visible when
0.7.0 is cut, while README's Building bullet reads as the whole rule for a text field. Whether 0.7.0 is cut, while README's Building bullet reads as the whole rule for a text field. Whether
this is a supported spelling or 0.4.0 tolerance decides whether that sentence lands in this is a supported spelling or 0.4.0 tolerance decides whether that sentence lands in
README.md or in MIGRATION.md — write it in the same change as the rule, so it is worded once. README.md or in MIGRATION.md — write it in the same change as the rule, so it is worded once.
@@ -319,7 +332,7 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
`responseTimeout`, `idleTimeout`, `shutdownTimeout` and `reassemblyTimeout` from below only, `responseTimeout`, `idleTimeout`, `shutdownTimeout` and `reassemblyTimeout` from below only,
and Node fires a larger delay after 1 ms. From the 2026-09-28 scoring run. and Node fires a larger delay after 1 ms. From the 2026-09-28 scoring run.
- [ ] **Keep a bare ESC out of GSM detection.** `gsmRegex` in `defs/encodings.ts` admits `\x1B`, - [ ] **Keep a bare ESC out of GSM detection.** `gsmRegex` in `codec/encodings.ts` admits `\x1B`,
so `"\x1B("` is detected as GSM, goes out as 0x1B 0x28 and arrives as `{`. From the so `"\x1B("` is detected as GSM, goes out as 0x1B 0x28 and arrives as `{`. From the
2026-09-28 scoring run. 2026-09-28 scoring run.
@@ -339,32 +352,31 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
### Shape — 6 today, and the gate is 7 ### Shape — 6 today, and the gate is 7
- [ ] **Give each meaning of "refusal" its own name.** `PduRefusedError`/`codec/refusal.ts` (a PDU
the codec would not read), reassembly's `Refusal` (a segment not kept), `LinkLife.refusal()`
(the session is closed), `OutgoingRequests.refuse()` (bad input) and `Session.refuse()`
(answering an unreadable PDU) are five things under one word. From the 2026-09-30 scoring run.
- [ ] **Collect a batch of sent PDUs in one function.** `collectReceipt()` in `session/sms.ts` and
`collectSent()` in `messages/submit.ts` are the same loop over `Result<{ pduObj }>[]`, counting
`UnansweredError`s and keeping the first failure. From the 2026-09-30 scoring run.
- [ ] **Say at `readParams()` that only `short_message` reads the length it is handed.** Every field's
`read` gets `sm_length`, and that is sound only because `sm_length` precedes `short_message` in
wire order and `buffer` is the one type that uses it. From the 2026-09-30 scoring run.
- [ ] **Answer "is this a bind command" in one place.** `bindCommands` (read by - [ ] **Answer "is this a bind command" in one place.** `bindCommands` (read by
`incoming-requests.ts`, `outgoing-requests.ts` and `test/session.test.ts`) and `session/incoming-requests.ts`, `session/outgoing-requests.ts` and `test/session.test.ts`) and
`bindTypeFromCommand()` (read by `server.ts` and `checkedBind()`) each list the three bind `bindTypeFromCommand()` (read by `server.ts` and `checkedBind()`) each list the three bind
commands, so a fourth added to one is missed by the other. Derive the list from the function, commands, so a fourth added to one is missed by the other. Derive the list from the function,
or the reverse. From the stability review of #42. or the reverse. From the stability review of #42.
- [ ] **Group `src/` into a second level, and retire whichever record loses.** 34 files on one
plane, where `src/defs/` at 7 proves the shape is known one level down. `docs/decisions.md`
says "`src/` stays flat until a module has to move for another reason. Valid while that map is
what a reader navigates by" — and both architects reported that the map is now AGENTS.md rather
than the tree, which is that premise failing. `todo.md` already carries the opposite
instruction under Worth doing. Two records, opposite answers; one has to go.
- [ ] **Split `test/session-extras.test.ts` by the question each block answers.** 3,010 lines, 19 - [ ] **Split `test/session-extras.test.ts` by the question each block answers.** 3,010 lines, 19
unrelated `describe` blocks whose names are already the file names they should be. With unrelated `describe` blocks whose names are already the file names they should be. With
`session.test.ts` it is 54% of all test code and 84% the size of `src/`. "extras" names neither `session.test.ts` it is 54% of all test code and 84% the size of `src/`. "extras" names neither
a question nor a module — it names the rest — and AGENTS.md's own convention forbids exactly a question nor a module — it names the rest — and AGENTS.md's own convention forbids exactly
that. `max-lines` covers `src/**` only, so nothing has stopped it growing. that. `max-lines` covers `src/**` only, so nothing has stopped it growing.
- [ ] **Collapse the three objects named `defaults`.** `client.ts`, `server.ts` and
`session-options.ts` each export or hold one; `port: 2775` is written twice and the idle
timeout is derived two ways to the same 40 000, and 64 MiB is both `defaultMaxOctets` and
`defaults.maxHeldOctets`. "What is the default for X" has three answers
depending on the entrypoint, and nothing fails when they drift. Named by both architects as the
most likely first bug a new contributor ships.
- [ ] **Give `hold` one meaning, and rename `IncomingRequests.refusing` for what it does.** - [ ] **Give `hold` one meaning, and rename `IncomingRequests.refusing` for what it does.**
`LinkLife.hold()` is a request's budget waiting for a link, `HeldMessages.hold()` a message the `LinkLife.hold()` is a request's budget waiting for a link, `HeldMessages.hold()` a message the
application owes an answer; `refusing` decides no refusal — `held.full()` does — and only makes application owes an answer; `refusing` decides no refusal — `held.full()` does — and only makes
@@ -377,12 +389,6 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
the breaking unit, so this is as cheap as it will ever be, and `todo.md` already requires the the breaking unit, so this is as cheap as it will ever be, and `todo.md` already requires the
`consts.ENCODING` names settled before the custom-encoding registry — this is the other half. `consts.ENCODING` names settled before the custom-encoding registry — this is the other half.
- [ ] **Split `session-options.ts` into the things it is.** Option types and their validator, the
`SessionEvents` map, and the bind-direction rules (`bindCommands`, `bindTypeFromCommand`,
`standsInFor`, `bindCarries`) are three questions in one file, and the `defaults` table mixes
option defaults with four hard bounds that are not options. Both architects named it as where
the codebase rots first: at 34-wide it is where anything session-shaped lands.
- [ ] **Name the base-versus-segment distinction in the message id types.** `Sms.smsId` is a base, - [ ] **Name the base-versus-segment distinction in the message id types.** `Sms.smsId` is a base,
`sendSms().smsIds[]` are segment ids, `Dlr.smsId` is a segment id and `MessageDlr.smsId` is a `sendSms().smsIds[]` are segment ids, `Dlr.smsId` is a segment id and `MessageDlr.smsId` is a
base again — four fields, one type, `string`. The whole multipart receipt mechanism turns on base again — four fields, one type, `string`. The whole multipart receipt mechanism turns on
@@ -464,9 +470,9 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
the fenced blocks at test time and assert each appears verbatim in the executed source, so an the fenced blocks at test time and assert each appears verbatim in the executed source, so an
edit to either fails the gate. edit to either fails the gate.
- [ ] **Narrow the `src/defs/*` lint exemption to the four table files.** Its stated reason — "the - [ ] **Narrow the `src/codec/` table lint exemption to the four table files.** Its stated reason — "the
spec tables are data: their length tracks the specification, not any complexity" — is false for spec tables are data: their length tracks the specification, not any complexity" — is false for
`defs/types.ts`, which is 595 lines of wire codec with 25 functions and is the file that parses `codec/types.ts`, which is 595 lines of wire codec with 25 functions and is the file that parses
hostile input from the network. It carries more over-budget methods than any other file in the hostile input from the network. It carries more over-budget methods than any other file in the
repo, under a suppression written for something else. repo, under a suppression written for something else.
@@ -524,7 +530,7 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
`idleTimeout: '5000'` is refused with `got 5000` — a value the reader reads as correct — where `idleTimeout: '5000'` is refused with `got 5000` — a value the reader reads as correct — where
`connectTimeout` quotes it. `namedValue()`'s four sites — `messagingMode`, `encoding`, the time `connectTimeout` quotes it. `namedValue()`'s four sites — `messagingMode`, `encoding`, the time
options and `smsIdFormat` — are the same defect once more: there `true` and `'true'` both print options and `smsIdFormat` — are the same defect once more: there `true` and `'true'` both print
as `true`. One fix closes all three, and `valueText()` in `defs/types.ts` is the quoted as `true`. One fix closes all three, and `valueText()` in `codec/types.ts` is the quoted
spelling to take it from. Raised by review, 2026-09-20. spelling to take it from. Raised by review, 2026-09-20.
- [ ] **Refuse a send the codec cannot build before it waits for a link and a window slot.** Today - [ ] **Refuse a send the codec cannot build before it waits for a link and a window slot.** Today
@@ -561,7 +567,7 @@ to the plan are [plan 3 §8](docs/comprehension-rewrite/plan-3.md#8-architecture
Maintainer's ask, 2026-09-14; not started until asked. Maintainer's ask, 2026-09-14; not started until asked.
- [ ] **Count what is left of a budget one way in `leftOf()` and `LinkLife`.** Today they are one - [ ] **Count what is left of a budget one way in `leftOf()` and `LinkLife`.** Today they are one
concept counted twice. `idle-waiters.ts` reads what is left of a budget as `Math.max(1, concept counted twice. `session/idle-waiters.ts` reads what is left of a budget as `Math.max(1,
deadline - now)`, because 0 means "forever" there; `link-life.ts` runs the same subtraction deadline - now)`, because 0 means "forever" there; `link-life.ts` runs the same subtraction
and calls `<= 0` expired. Neither is reachable from the other, so nothing can disagree today, and calls `<= 0` expired. Neither is reachable from the other, so nothing can disagree today,
but a reader who learns one and applies it to the other is wrong. A budget type both take but a reader who learns one and applies it to the other is wrong. A budget type both take
@@ -685,7 +691,7 @@ Each lands under goal 7: an option or a hook, with the call that passes none unc
### Encodings ### Encodings
- [ ] **Register a custom encoding.** Maintainer's ask, 2026-09-14. `EncodingName` is a closed union - [ ] **Register a custom encoding.** Maintainer's ask, 2026-09-14. `EncodingName` is a closed union
of three (`defs/encodings.ts`). An entry needs a name, a `data_coding`, `encode`, `decode`, of three (`codec/encodings.ts`). An entry needs a name, a `data_coding`, `encode`, `decode`,
`match`, whether `detect()` may pick it, and enough for `splitMessage()` to budget a segment `match`, whether `detect()` may pick it, and enough for `splitMessage()` to budget a segment
without halving a character. Take encodings as a client or server option rather than mutating a without halving a character. Take encodings as a client or server option rather than mutating a
module table as `smpp` does, so two sessions in one process cannot disagree about a name. A taken module table as `smpp` does, so two sessions in one process cannot disagree about a name. A taken