Refuse what we cannot send: an alphabet that cannot carry the message, and a time nobody can read (#97)
* Regression tests for a named alphabet and a time that cannot carry the send * Refuse a message the named alphabet cannot carry, and a time nobody can read * Close the architecture review: a builder that cannot drop a time, and docs that hold * Refuse a time of no known kind, and clamp a relative period to what the format holds * Reflow the decision record and name the clamp in its test
This commit is contained in:
@@ -210,9 +210,9 @@ decision under [The wire](#the-wire).
|
||||
helpers each file carries are copies, tolerated because a wrong one fails that file's own tests and
|
||||
nothing else, and a helper that only names the parameters of one `objToPdu()` call is on that same
|
||||
footing — it encodes no wire fact `objToPdu()` does not already own. So is a stub standing in for a
|
||||
collaborator the type system already keeps in step: `recordingDeps()` in `messaging-mode.test.ts`
|
||||
and `message-class.test.ts` is one `SendSmsDeps.send` that answers nothing, and a field added to
|
||||
that type fails to compile in both copies at once.
|
||||
collaborator the type system already keeps in step: `recordingDeps()` in `messaging-mode.test.ts`,
|
||||
`message-class.test.ts` and `unsendable.test.ts` is one `SendSmsDeps.send` that answers nothing,
|
||||
and a field added to that type fails to compile in every copy at once.
|
||||
- `message_id` values the library generates are UUID v7.
|
||||
- A test that needs a dummy peer must `resume()` its sockets. An unread socket never processes the
|
||||
peer's FIN, so `server.close()` hangs forever — that is a test bug, not a library one.
|
||||
@@ -316,7 +316,8 @@ Grouped by what each one constrains.
|
||||
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.
|
||||
without types gets. `smppTime.encode()` is where that reasoning lands the other way and is recorded
|
||||
under [The wire](#the-wire): `Date | number | string` is not a closed set, so it is a `Result`.
|
||||
|
||||
### The wire
|
||||
|
||||
@@ -586,6 +587,41 @@ Grouped by what each one constrains.
|
||||
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.
|
||||
|
||||
- **An alphabet the caller named has to carry the message, and a time the format cannot express is
|
||||
refused, both before a segment goes out.** Maintainer's call, 2026-09-09, from the architecture and
|
||||
stability reviews of [#96](https://github.com/larvit/larvitsmpp/pull/96): `encoding: 'LATIN1'` on
|
||||
`あいう` put `42 44 46` — `"BDF"` — on the wire and returned success, `encoding: 'ASCII'`
|
||||
flattened every character outside 03.38 to a space, and `validityPeriod: new Date('nope')` wrote
|
||||
`NaNNaNNaNNaNNaNNaNNaN00+` into the PDU. Goal 2 owns all three: bytes that do not say what the
|
||||
caller asked, reported as sent. `unencodable()` is the single answer to whether an alphabet can
|
||||
carry a message, as `messageClassOf()` is to whether a `data_coding` carries a class, and it asks
|
||||
the codec — `decode(encode(c)) === c` per code point — rather than restating the tables beside it,
|
||||
so the guard cannot drift from what the encoder writes for any one character, and a fourth
|
||||
alphabet answers by having a codec at all. It is exported for the reason `concatOf()` is: a caller
|
||||
composing a `submit_sm` through `send()` and `encodeMessage()` would otherwise rewrite the read
|
||||
this fixed. `match()` cannot be that answer — it doubles as the auto-selection policy `detect()`
|
||||
reads, where LATIN1 is hardcoded false so nothing picks it, and using it would refuse the 8-bit
|
||||
binary body Latin-1 is kept for. The guard is
|
||||
on the named branch alone, so an unspecified send is untouched: every alphabet `detect()` returns
|
||||
carries every character it was picked for, over the whole code point range. `smppTime.encode()`
|
||||
returns a `Result`, where the three encoding helpers stayed total: that argument was that
|
||||
`EncodingName` is a closed set the compiler guards, and `Date | number | string` is not — an
|
||||
invalid `Date` and `NaN` inhabit it, which hard rule 1 makes a result "wherever the types admit
|
||||
one", and `decode()` has been fallible for the same reason since it was written. Rejected:
|
||||
transcoding to UCS2, which overrides the one option the caller wrote in order to override a choice
|
||||
— the same reason a flash Latin-1 message is refused rather than promoted, and an operator that
|
||||
accepts only `data_coding` 0x03 would be handed something it never agreed to take. Rejected:
|
||||
guarding `sendSms()` alone and leaving `smppTime.encode()` writing `NaN`s, which leaves this
|
||||
library's own published helper composing the garbage the guard exists to stop. Rejected: a
|
||||
`holds()` member beside `match()` on `Encoding`, a second per-alphabet table to keep in step with
|
||||
the codec. Rejected: validating the `string` spelling of a time, which is a stamp the caller
|
||||
formatted for a peer whose format is theirs to name, its width included, where SMPP 3.4 gives the
|
||||
field 1 or 17 octets. Accepted: `Infinity` seconds is refused rather than clamped, where a real
|
||||
period past the 99d 23:59:59 the format holds is still clamped to it. Accepted: GSM's 0x1B is an
|
||||
extension prefix rather than a character, so a bare ESC beside one of the ten extension bases is
|
||||
the one input a per-character reading passes and the encoder then writes as the extended character
|
||||
— the only composition in any of the three codecs, and not a character a message is written in.
|
||||
|
||||
### 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