Move the goals to README.md and state the audience
Mirror / push (push) Successful in 5s
Test / lint (pull_request) Successful in 21s
Test / test (18) (pull_request) Successful in 21s
Test / test (20) (pull_request) Successful in 19s
Test / test (22) (pull_request) Successful in 20s
Test / test (24) (pull_request) Successful in 20s
Test / test (26) (pull_request) Successful in 20s

This commit is contained in:
2026-09-19 00:48:21 +02:00
parent aab74e4cb3
commit 0f13951e8c
3 changed files with 81 additions and 47 deletions
+9 -42
View File
@@ -12,45 +12,11 @@ not for structure or style.
## Goals ## Goals
In priority order, and the order is the point: where two of them pull against each other, the earlier The nine goals, in priority order, live in
one wins. They do not override the hard rules below. [README.md](https://gitea.larvit.se/larvit/smpp-js/src/branch/main/README.md#goals) — they say where this library is heading, which an outside
reader judges it by. The README states the audience alongside them. Everything below cites a goal by
number.
1. **Correct on the wire.** SMPP 3.4 as SMSCs actually run it. Every other goal yields to this one;
the defect table below is what the alternative costs.
2. **Never give the application a wrong answer about what happened.** An outcome we cannot determine
is reported as undetermined rather than guessed; a report the peer marked as not final settles
nothing, so nothing the library concludes may rest on one; a request the peer may already have
taken is never re-sent on the library's own initiative; work the peer has no reason to send again
is not dropped.
3. **Strict in what we send, generous in what we read.** The library's own senders follow 3.4, and
the codec parses whatever arrives. Where the letter of the spec would discard traffic a real SMSC
sends, keep the traffic.
4. **A peer an operator never has to complain about.** No bind flooding, nothing a bind direction
forbids, no optional parameters to a peer that declared none, nothing held without a bound.
5. **The session layer is in here, and its defaults are what most applications should run.**
Keepalive, reconnect, the send window, reassembly and receipt correlation. What the network says
about a message the application sent reaches it as a report rather than as an inbound message, and
says whether it is final, so nothing has to read the PDU to tell those apart. An option retunes a
default or opts out of it; an option does not switch on the thing the caller obviously wanted.
6. **Configurable and extendable, never at the defaults' expense.** Where an application needs other
than the default and cannot build it from what is exported — a rate limit counted per PDU, an
alphabet, a receipt format — it gets an option or a hook rather than a fork. A call that passes no
options stays exactly as easy and as safe, and a hook is a seam the library calls, never a way into
its internals.
7. **A small, stable public surface over reshapeable internals.** Only what `src/index.ts` exports is
published. A new option has to beat "the application can do this itself", and has to keep a
promise this library can verify. The low-level surface is a passthrough: policy binds what the
library composes, never what the caller wrote.
8. **State wider than one session goes through one store.** A pool of sessions, a limit shared
between processes, and what has to survive a restart — receipts still awaited, a message half
reassembled — are held through a store interface and never beside it. Without a store the
application supplies, that state is in memory and ends with the process, and the defaults need
none. The interface carries the library's own versioned records, never an internal shape handed
to the application to persist. Coordinating processes any other way is declined without a fresh
argument each time.
9. **It builds, tests and runs the same everywhere.** Container-only toolchain, no runtime
dependencies, the Node 18 floor verified in CI rather than asserted, every README example executed
by the suite.
## Hard rules ## Hard rules
@@ -245,12 +211,13 @@ decision under [The wire](#the-wire).
Each file answers one question, and a fact belongs to the file whose question it answers: Each file answers one question, and a fact belongs to the file whose question it answers:
- **README.md — what you can rely on.** Observable behaviour, for someone using the package. It - **README.md — what you can rely on, and where this is heading.** Observable behaviour, for
carries a reason only where the reason changes how you would call the thing. someone using the package, plus the goals and the audience. It carries a reason only where the
reason changes how you would call the thing.
- **MIGRATION.md — what a 0.4.0 consumer has to change.** Renamed and removed surface, and the - **MIGRATION.md — what a 0.4.0 consumer has to change.** Renamed and removed surface, and the
behaviour that changed on the wire. behaviour that changed on the wire.
- **AGENTS.md — what may not change, and why.** Goals, hard rules, architecture, conventions, and the - **AGENTS.md — what may not change, and why.** Hard rules, architecture, conventions, and the
decisions the goals do not already settle. It does not restate behaviour README states. decisions the goals do not already settle. It does not restate behaviour or goals README states.
- **todo.md** is a working file that sets its own rules; nothing here governs it. - **todo.md** is a working file that sets its own rules; nothing here governs it.
A sentence living in two of them is a defect: delete the copy in the file whose question it does not A sentence living in two of them is a defect: delete the copy in the file whose question it does not
+68 -1
View File
@@ -22,7 +22,8 @@ window, long messages and delivery receipts. TypeScript, ESM, no dependencies.
[Send options](#send-options) · [Session](#session) · [Receiving in depth](#receiving-in-depth) · [Send options](#send-options) · [Session](#session) · [Receiving in depth](#receiving-in-depth) ·
[Server in depth](#server-in-depth) · [Logging](#logging) · [Server in depth](#server-in-depth) · [Logging](#logging) ·
[PDUs and the low-level API](#pdus-and-the-low-level-api) · [PDUs and the low-level API](#pdus-and-the-low-level-api) ·
[Migrating from 0.4.0](#migrating-from-larvitsmpp-040) · [Development](#development) [Migrating from 0.4.0](#migrating-from-larvitsmpp-040) · [Goals](#goals) ·
[Audience](#audience) · [Development](#development)
## Install ## Install
@@ -632,6 +633,72 @@ if (isCommand(pduObj, 'submit_sm')) {
See [MIGRATION.md](https://gitea.larvit.se/larvit/smpp-js/src/branch/main/MIGRATION.md). See [MIGRATION.md](https://gitea.larvit.se/larvit/smpp-js/src/branch/main/MIGRATION.md).
## Goals
In priority order, and the order is the point: where two of them pull against each other, the earlier
one wins. They do not override the hard rules below.
1. **Correct on the wire.** SMPP 3.4 as SMSCs actually run it. Every other goal yields to this one;
the defect table below is what the alternative costs.
2. **Never give the application a wrong answer about what happened.** An outcome we cannot determine
is reported as undetermined rather than guessed; a report the peer marked as not final settles
nothing, so nothing the library concludes may rest on one; a request the peer may already have
taken is never re-sent on the library's own initiative; work the peer has no reason to send again
is not dropped.
3. **Strict in what we send, generous in what we read.** The library's own senders follow 3.4, and
the codec parses whatever arrives. Where the letter of the spec would discard traffic a real SMSC
sends, keep the traffic.
4. **A peer an operator never has to complain about.** No bind flooding, nothing a bind direction
forbids, no optional parameters to a peer that declared none, nothing held without a bound.
5. **The session layer is in here, and its defaults are what most applications should run.**
Keepalive, reconnect, the send window, reassembly and receipt correlation. What the network says
about a message the application sent reaches it as a report rather than as an inbound message, and
says whether it is final, so nothing has to read the PDU to tell those apart. An option retunes a
default or opts out of it; an option does not switch on the thing the caller obviously wanted.
6. **Configurable and extendable, never at the defaults' expense.** Where an application needs other
than the default and cannot build it from what is exported — a rate limit counted per PDU, an
alphabet, a receipt format — it gets an option or a hook rather than a fork. A call that passes no
options stays exactly as easy and as safe, and a hook is a seam the library calls, never a way into
its internals.
7. **A small, stable public surface over reshapeable internals.** Only what `src/index.ts` exports is
published. A new option has to beat "the application can do this itself", and has to keep a
promise this library can verify. The low-level surface is a passthrough: policy binds what the
library composes, never what the caller wrote.
8. **State wider than one session goes through one store.** A pool of sessions, a limit shared
between processes, and what has to survive a restart — receipts still awaited, a message half
reassembled — are held through a store interface and never beside it. Without a store the
application supplies, that state is in memory and ends with the process, and the defaults need
none. The interface carries the library's own versioned records, never an internal shape handed
to the application to persist. Coordinating processes any other way is declined without a fresh
argument each time.
9. **It builds, tests and runs the same everywhere.** Container-only toolchain, no runtime
dependencies, the Node 18 floor verified in CI rather than asserted, every README example executed
by the suite.
## Audience
Who depends on this library, and what they may rely on.
- **The public npm audience, not only larvit's own applications.** Only what `src/index.ts` exports
is public; everything behind it is reshaped freely.
- **Node 18 and newer, ESM only, no runtime dependencies.** The floor is verified in CI rather than
asserted, so the library drops into a service or a container without pulling a tree behind it.
- **Real SMSCs and ESMEs as operators actually run them**, not a reference implementation. Jasmin,
SMPPSim, Kannel, jsmpp, Cloudhopper, python-smpplib and php-smpp are the interop targets, and what
they do in practice outranks what the specification says they should do.
- **The SMSC operator on the far end**, who never sees this API but carries what it does to their
link. A peer they have to complain about is a defect however well the library reads.
- **Pre-1.0, so the minor is the breaking unit** and a patch never breaks. What a 0.4.0 consumer has
to change is in [MIGRATION.md](https://gitea.larvit.se/larvit/smpp-js/src/branch/main/MIGRATION.md).
Personas this README serves, in order:
1. The application developer sending or receiving SMS on the defaults, who should need no options.
2. The application developer who needs one thing retuned — an alphabet, a rate limit, a receipt
format — through an option or a hook rather than a fork.
3. The operator coordinating sessions across processes through a store.
4. The developer migrating from `larvitsmpp` 0.4.0.
## Development ## Development
Everything runs in the container; nothing is installed on the host. Everything runs in the container; nothing is installed on the host.
+4 -4
View File
@@ -1,7 +1,7 @@
# todo.md # todo.md
Remaining work for `@larvit/smpp`. Read [AGENTS.md](AGENTS.md) first — the goals and hard rules Remaining work for `@larvit/smpp`. Read [README.md](README.md)'s goals and [AGENTS.md](AGENTS.md)'s
there constrain every item below. hard rules first — they constrain every item below.
This is a working file that sets its own rules. The documentation conventions in AGENTS.md do not This is a working file that sets its own rules. The documentation conventions in AGENTS.md do not
govern it, and nothing here is a source anything else may cite. govern it, and nothing here is a source anything else may cite.
@@ -262,7 +262,7 @@ the rewrite, for a dependency added later. Maintainer's call, 2026-09-14.
From comparing 0.5.0 with `smpp`, `@semyonf/smpp`, `@leissner/node-red-smpp`, `node-smpp-next`, From comparing 0.5.0 with `smpp`, `@semyonf/smpp`, `@leissner/node-red-smpp`, `node-smpp-next`,
`smpp-js-sdk`, `smppjs`, cloudhopper-smpp, jsmpp, go-smpp, Kannel, Jasmin and php-smpp, 2026-09-14. `smpp-js-sdk`, `smppjs`, cloudhopper-smpp, jsmpp, go-smpp, Kannel, Jasmin and php-smpp, 2026-09-14.
Each lands under AGENTS.md goal 6: an option or a hook, with the call that passes none unchanged. Each lands under goal 6: an option or a hook, with the call that passes none unchanged.
### Sending ### Sending
@@ -424,7 +424,7 @@ Each lands under AGENTS.md goal 6: an option or a hook, with the call that passe
- [ ] **Pooling, and state that survives a restart, through an optional store.** Maintainer's call, - [ ] **Pooling, and state that survives a restart, through an optional store.** Maintainer's call,
2026-09-14. It replaces two declines — merge state surviving a restart, and a pool of sessions — 2026-09-14. It replaces two declines — merge state surviving a restart, and a pool of sessions —
and AGENTS.md goal 8 was rewritten for it. Big: design before code. and goal 8 was rewritten for it. Big: design before code.
- **What it holds.** Receipts still awaited and the groups `DlrMerger` collects. Segments of a - **What it holds.** Receipts still awaited and the groups `DlrMerger` collects. Segments of a
message already answered but not yet whole, which the peer will not send again (goal 2). The message already answered but not yet whole, which the peer will not send again (goal 2). The
concatenation reference, so a restart does not reuse one. For a pool, the ids every session concatenation reference, so a restart does not reuse one. For a pool, the ids every session