Fit every concatenated segment in an SMS, and close the encoding helpers' throw (#96)
* Regression tests for the concatenated segment budget and the encoding-name door * Budget unpacked segments at 134 octets, and export isEncodingName * Name the length a Latin-1 message starts costing more segments at * Fold the totality clause into hard rule 1, and log the unrepresentable-alphabet finding * Make the maxSegments regression discriminate, and drop the narrating comments * State the per-alphabet capacities accurately, and log the invalid-date path
This commit is contained in:
@@ -52,7 +52,9 @@ These are not preferences. Breaking one is a defect.
|
||||
1. **Nothing throws.** Every fallible function returns (or resolves to) a DTO carrying an optional
|
||||
`err`. No `throw`, no rejected promises, no exceptions as control flow. Node APIs that throw are
|
||||
wrapped at the boundary and converted into a result. Programmer errors (bad arguments) are
|
||||
results too.
|
||||
results too, wherever the types admit one: a function whose argument types are a closed set is
|
||||
guarded by the compiler and stays total, which is why the encoding helpers return plainly, and
|
||||
the check belongs at whichever boundary the argument arrives untyped at.
|
||||
2. **Log messages are static strings.** Every dynamic value goes into the log metadata. Never
|
||||
interpolate, never concatenate.
|
||||
- GOOD: `log.debug('sendSms() - splitting message', { parts: msgs.length, to });`
|
||||
@@ -182,12 +184,12 @@ so a peer that dispatches one request at a time is never left waiting on us.
|
||||
|
||||
Over SMPP the ESME puts one GSM character per octet in `short_message` and the SMSC packs it into
|
||||
septets. The 140-octet limit applies to that packed result, not to what goes on the wire here, which
|
||||
is why a concatenated segment is 153 characters plus a 6-octet UDH — 159 octets in `short_message`,
|
||||
and entirely correct. Do not "fix" this to 134; that number is the packed payload size and would
|
||||
truncate every long message by a fifth.
|
||||
is why a concatenated GSM segment is 153 characters plus a 6-octet UDH — 159 octets in
|
||||
`short_message`, and entirely correct. Do not "fix" that to 134; that number is the packed payload
|
||||
size and would truncate every long GSM message by a fifth.
|
||||
|
||||
UCS2 is not packed, so there the two coincide: 67 characters = 134 octets, plus the 6-octet UDH is
|
||||
exactly 140.
|
||||
GSM 7-bit is the only alphabet it applies to. What each of the three is budgeted, and why, is a
|
||||
decision under [The wire](#the-wire).
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -301,6 +303,21 @@ Grouped by what each one constrains.
|
||||
a cast and types nothing it carries. Accepted: a second copy of the package installed alongside
|
||||
this one defeats `instanceof`, where `err.name` still reads `PduRefusedError`.
|
||||
|
||||
- **`bitCount()`, `encodeMessage()` and `splitMessage()` keep their total signatures, because
|
||||
`EncodingName` is what keeps an alphabet with no codec away from them.** Maintainer's call,
|
||||
2026-09-09, from the architecture review of [#95](https://github.com/larvit/larvitsmpp/pull/95):
|
||||
all three index `encodings` by name and would throw on one it has no codec for, which hard rule 1
|
||||
forbids. That PR left no such name to pass — `encodings` is a `Record<EncodingName, Encoding>`, so
|
||||
every member of the union has a codec and one added without a codec, or without a segment budget,
|
||||
fails to compile in four places. What was missing is the door for a caller holding a name at
|
||||
runtime: `Object.hasOwn(encodings, x)` is the only test the published surface offered and it
|
||||
narrows nothing, so `isEncodingName()` is exported beside `isCommandName()` and `isErrorName()`,
|
||||
which serve their own tables that way. Rejected: a `Result` signature on all three, which costs
|
||||
every typed consumer a narrow forever — goal 6, and the tag is the last cheap chance to spend it —
|
||||
to guard a state the compiler refuses. Where the domain really is open the check is already there:
|
||||
`sendSms()` takes its options as `unknown` and refuses `encoding` by name, which is what a caller
|
||||
without types gets.
|
||||
|
||||
### The wire
|
||||
|
||||
- **The declared interface version is an option on both `client()` and `server()`, and is not the
|
||||
@@ -557,6 +574,18 @@ Grouped by what each one constrains.
|
||||
the base instead would break that pair. The option is on `client()` only, since a `server()` session
|
||||
writes both ids itself.
|
||||
|
||||
- **A concatenated segment is budgeted at 134 octets, which is 153 septets where the SMSC packs them
|
||||
and 134 octets of anything it does not.** Maintainer's call, 2026-09-09, from the architecture
|
||||
review of [#95](https://github.com/larvit/larvitsmpp/pull/95): `segmentUnits` handed 153 to
|
||||
everything but UCS2, so a long `encoding: 'LATIN1'` message went out as segments of 153 octets plus
|
||||
a 6-octet UDH — 159 on the air where GSM 03.40 carries 140, which no SMSC can deliver. Goal 1 owns
|
||||
it. There is one budget, 140 less the UDH, and the alphabet decides only what it is counted in, so
|
||||
Latin-1 and UCS2 both take those 134 octets — 134 characters and 67 — and it is GSM 7-bit's 153
|
||||
that is the odd number rather than the other way round. `Record<EncodingName, number>` is what makes
|
||||
a fourth alphabet state its own. Rejected: 134 for GSM 7-bit too, which is the mistake the
|
||||
unpacked-alphabet section above exists to stop. Accepted: a Latin-1 message past the 140 characters
|
||||
one SMS holds now costs more segments than it did, and `smsIds` is that much longer.
|
||||
|
||||
### The session's life
|
||||
|
||||
- **A close arriving after our own `unbind` is a clean unbind, not an error.** Maintainer's call,
|
||||
|
||||
Reference in New Issue
Block a user