Plan failover, an optional store for pooling and restarts, shipping src, and Gitea runners for macOS and Windows
Test / lint (pull_request) Successful in 19s
Test / test (18) (pull_request) Successful in 18s
Test / test (20) (pull_request) Successful in 18s
Test / test (22) (pull_request) Successful in 19s
Test / test (24) (pull_request) Successful in 17s
Test / test (26) (pull_request) Successful in 18s
Test / lint (pull_request) Successful in 19s
Test / test (18) (pull_request) Successful in 18s
Test / test (20) (pull_request) Successful in 18s
Test / test (22) (pull_request) Successful in 19s
Test / test (24) (pull_request) Successful in 17s
Test / test (26) (pull_request) Successful in 18s
This commit is contained in:
@@ -41,12 +41,13 @@ one wins. They do not override the hard rules below.
|
|||||||
published. A new option has to beat "the application can do this itself", and has to keep a
|
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
|
promise this library can verify. The low-level surface is a passthrough: policy binds what the
|
||||||
library composes, never what the caller wrote.
|
library composes, never what the caller wrote.
|
||||||
8. **Nothing that needs state wider than one session.** No persistence across a restart, no
|
8. **State wider than one session goes through one store.** A pool of sessions, a limit shared
|
||||||
coordination between processes, no pool of sessions — and no seam handing the application state to
|
between processes, and what has to survive a restart — receipts still awaited, a message half
|
||||||
persist for one of those either, which commits to the same scope through the back door and
|
reassembled — are held through a store interface and never beside it. Without a store the
|
||||||
publishes an internal shape to do it. A limit wider than a session, such as an account's rate
|
application supplies, that state is in memory and ends with the process, and the defaults need
|
||||||
limit, is the application's to hold, behind a hook the library calls (goal 6). This is the scope
|
none. The interface carries the library's own versioned records, never an internal shape handed
|
||||||
floor, and it is why an otherwise reasonable feature is declined without a fresh argument each time.
|
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
|
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
|
dependencies, the Node 18 floor verified in CI rather than asserted, every README example executed
|
||||||
by the suite.
|
by the suite.
|
||||||
|
|||||||
@@ -261,12 +261,12 @@ Each lands under AGENTS.md goal 6: an option or a hook, with the call that passe
|
|||||||
- [ ] **A limiter hook, with a messages-per-second cap built on it.** Maintainer's call, 2026-09-14,
|
- [ ] **A limiter hook, with a messages-per-second cap built on it.** Maintainer's call, 2026-09-14,
|
||||||
reversing the earlier decline: Kannel, Jasmin, go-smpp and `smpp-js-sdk` all limit throughput.
|
reversing the earlier decline: Kannel, Jasmin, go-smpp and `smpp-js-sdk` all limit throughput.
|
||||||
Count PDUs, not `sendSms()` calls — a long message is one `submit_sm` per segment and the
|
Count PDUs, not `sendSms()` calls — a long message is one `submit_sm` per segment and the
|
||||||
operator counts those, which an application wrapping `sendSms()` cannot see. The hook is where an
|
operator counts those, which an application wrapping `sendSms()` cannot see. The hook takes a
|
||||||
account-wide limit lives: a bucket the application holds across sessions or processes, which
|
limiter the application already runs; the built-in cap counts per session until the store at the
|
||||||
goal 8 keeps out of here. The built-in cap is the per-session case; the default stays uncapped.
|
bottom lets it span sessions and processes. The default stays uncapped. Open: the hook's shape (a
|
||||||
Open: the hook's shape (a wait that resolves when a PDU may go, cut short by the send's
|
wait that resolves when a PDU may go, cut short by the send's `signal`), which requests it gates
|
||||||
`signal`), which requests it gates — messages, never `enquire_link`, `unbind` or a response — and
|
— messages, never `enquire_link`, `unbind` or a response — and whether its wait counts against
|
||||||
whether its wait counts against `responseTimeout`.
|
`responseTimeout`.
|
||||||
|
|
||||||
- [ ] **Back off and resend on `ESME_RTHROTTLED`.** The SMSC refused the PDU, so resending cannot
|
- [ ] **Back off and resend on `ESME_RTHROTTLED`.** The SMSC refused the PDU, so resending cannot
|
||||||
duplicate it and goal 2 holds, and the retry needs nothing wider than the session. Needs a
|
duplicate it and goal 2 holds, and the retry needs nothing wider than the session. Needs a
|
||||||
@@ -287,11 +287,10 @@ Each lands under AGENTS.md goal 6: an option or a hook, with the call that passe
|
|||||||
SMSCs take only `sar_*` or `message_payload`; php-smpp offers all three. A 16-bit reference also
|
SMSCs take only `sar_*` or `message_payload`; php-smpp offers all three. A 16-bit reference also
|
||||||
makes a collision rarer: the 8-bit one wraps every 255 sends on a session.
|
makes a collision rarer: the 8-bit one wraps every 255 sends on a session.
|
||||||
|
|
||||||
- [ ] **Failover across SMSC hosts.** `client()` takes one `host` and `port`; Kannel, Jasmin and
|
- [ ] **Failover across SMSC hosts.** Maintainer's call, 2026-09-14. `client()` takes one `host` and
|
||||||
php-smpp take several. Asked 2026-09-14 whether this needs state wider than a session: a list the
|
`port`; Kannel, Jasmin and php-smpp take several. A list the reconnect loop walks holds only
|
||||||
reconnect loop walks holds only which host the one session is on, so goal 8 does not decline it,
|
which host the one session is on, so it needs no store. Open: in order or round robin, and when
|
||||||
unlike a pool (Declined). Needs a decision: in order or round robin, and when to try the first
|
to try the first host again.
|
||||||
host again.
|
|
||||||
|
|
||||||
### The server
|
### The server
|
||||||
|
|
||||||
@@ -368,11 +367,11 @@ Each lands under AGENTS.md goal 6: an option or a hook, with the call that passe
|
|||||||
|
|
||||||
### Packaging, tests and CI
|
### Packaging, tests and CI
|
||||||
|
|
||||||
- [ ] **The source maps point at files the package does not ship.** `sourceMap` and `declarationMap`
|
- [ ] **Ship `src`, so the source maps lead somewhere.** Maintainer's call, 2026-09-14. `sourceMap`
|
||||||
write maps whose `sources` are `../src/*.ts`, and `files` publishes only `dist`, so 225 KB of the
|
and `declarationMap` write maps whose `sources` are `../src/*.ts`, but `files` publishes only
|
||||||
555 KB package leads nowhere. Add `src` to `files`, which makes the maps work — go to definition
|
`dist`, so 82 of the package's 167 files, 225 KB of its 555 KB unpacked, point at nothing. Adding
|
||||||
lands in the TypeScript — or drop both maps from the build. `inlineSources` would fix only the
|
`src` (41 files, 209 KB) makes go to definition land in the TypeScript, and lets a debugger or
|
||||||
`.js.map` files.
|
`--enable-source-maps` show it.
|
||||||
|
|
||||||
- [ ] **A coverage report and a floor in the gate.** `node --test --experimental-test-coverage
|
- [ ] **A coverage report and a floor in the gate.** `node --test --experimental-test-coverage
|
||||||
--test-coverage-include='src/**' test/*.test.ts` on Node 24.18.0, 2026-09-14: 98.77% lines,
|
--test-coverage-include='src/**' test/*.test.ts` on Node 24.18.0, 2026-09-14: 98.77% lines,
|
||||||
@@ -381,14 +380,21 @@ Each lands under AGENTS.md goal 6: an option or a hook, with the call that passe
|
|||||||
of its own on 24, since the matrix runs compiled JavaScript — and add it to `main`'s required
|
of its own on 24, since the matrix runs compiled JavaScript — and add it to `main`'s required
|
||||||
checks. Raise the floor as coverage rises; never lower it.
|
checks. Raise the floor as coverage rises; never lower it.
|
||||||
|
|
||||||
- [ ] **Tests on macOS and Windows.** GitHub's hosted `macos-*` and `windows-*` runners are free for
|
- [ ] **Tests on macOS and Windows, on Gitea runners.** Gitea 1.26.4 runs the Ubuntu jobs on
|
||||||
public repositories and `actions/setup-node` runs on both, so the job is a `.github/workflows`
|
`dedicated-runner`; a Windows and a macOS host each running `act_runner` in host mode, under
|
||||||
file with a matrix. It cannot gate: pull requests and required checks live on Gitea, which has no
|
labels such as `windows-2025:host` and `macos-15:host`, run the same `setup-node` matrix and can
|
||||||
such runners without a self-hosted machine of each, so on the mirror it reports after merge. It
|
be required checks on `main`. Registered ephemeral — the runner API already carries the flag — a
|
||||||
runs Node on the runner, as the Ubuntu jobs already do — GitHub's macOS runners have no Docker
|
runner takes one job and leaves, so its host can be reset from a snapshot and registered again;
|
||||||
and its Windows runners run no Linux containers — so goal 9 needs no new exception. On Windows
|
that loop is ours to script. CI already installs Node with `setup-node` rather than through
|
||||||
the scripts' `*.test.*` globs reach Node unexpanded, which Node 21 and later expand themselves;
|
`compose.yaml`, so goal 9 needs no new exception.
|
||||||
Node 18 and 20 cannot run there as the scripts stand. Waits for "Retire the GitHub repository".
|
- **Windows:** a KVM VM on our metal, or `dockur/windows`, the same VM packaged as a container,
|
||||||
|
which needs `/dev/kvm`. Either needs a Windows licence; the evaluation editions expire. Node
|
||||||
|
on `PATH`, since host mode runs `actions/checkout` and `actions/setup-node` with the host's
|
||||||
|
`node`. `cmd` hands the scripts' `*.test.*` globs to Node unexpanded, which Node 21 and later
|
||||||
|
expand themselves, so Node 18 and 20 cannot run there as the scripts stand.
|
||||||
|
- **macOS:** Apple hardware. Apple's licence allows macOS guests only on Apple hardware, so not
|
||||||
|
on our metal: a Mac mini running `act_runner` itself, or Tart VMs on it, two macOS guests at a
|
||||||
|
time.
|
||||||
|
|
||||||
- [ ] **Mutation testing.** `@stryker-mutator/tap-runner` runs `node:test` suites and measures whether
|
- [ ] **Mutation testing.** `@stryker-mutator/tap-runner` runs `node:test` suites and measures whether
|
||||||
a test notices a change, which coverage cannot; `@semyonf/smpp` runs Stryker in CI. The session
|
a test notices a change, which coverage cannot; `@semyonf/smpp` runs Stryker in CI. The session
|
||||||
@@ -401,16 +407,32 @@ Each lands under AGENTS.md goal 6: an option or a hook, with the call that passe
|
|||||||
|
|
||||||
## Declined
|
## Declined
|
||||||
|
|
||||||
- **Merge state surviving a process restart.** Declined by AGENTS.md goal 8, maintainer's call,
|
|
||||||
2026-09-02. A restart loses every incomplete receipt group and a peer has no reason to resend one it
|
|
||||||
already had answered, so the loss is real — but surviving it means handing the application the merge
|
|
||||||
state to persist, which the scope floor covers as squarely as holding the state here would, and
|
|
||||||
which publishes the shape of `DlrMerger`'s groups against goal 7. Nothing is foreclosed: the seam
|
|
||||||
can still be added after 0.5.0 as a minor.
|
|
||||||
|
|
||||||
- **A pool of sessions.** Declined by AGENTS.md goal 8, maintainer's call, 2026-09-14: sends shared
|
|
||||||
across several sessions need state wider than any one of them. `node-smpp-next`'s `createPool()` is
|
|
||||||
the example. An application can run several clients and choose between them.
|
|
||||||
|
|
||||||
- **CommonJS.** ESM only, maintainer's call reaffirmed 2026-09-14, though `node-smpp-next` ships both.
|
- **CommonJS.** ESM only, maintainer's call reaffirmed 2026-09-14, though `node-smpp-next` ships both.
|
||||||
`require()` of an ES module works unflagged from Node 20.19 and 22.12.
|
`require()` of an ES module works unflagged from Node 20.19 and 22.12.
|
||||||
|
|
||||||
|
## An optional store
|
||||||
|
|
||||||
|
- [ ] **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 —
|
||||||
|
and AGENTS.md goal 8 was rewritten for it. Big: design before code.
|
||||||
|
- **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
|
||||||
|
concatenation reference, so a restart does not reuse one. For a pool, the ids every session
|
||||||
|
sent, since an SMSC may deliver a receipt on any bind of the account, and the
|
||||||
|
messages-per-second budget the sessions share.
|
||||||
|
- **What it cannot hold.** A response belongs to the link its request arrived on, so a message
|
||||||
|
left unanswered at a restart stays unanswerable; the peer's own timeout settles it.
|
||||||
|
- **Pooling.** Several sessions, in one process or many, behind one send: a message goes to a
|
||||||
|
bound session with a free window slot, all its segments on that one. In one process the
|
||||||
|
in-memory store is enough; across processes the application supplies one.
|
||||||
|
- **The interface.** Narrow, with keys and records of the library's own making, versioned, with
|
||||||
|
expiry: a record from an older version is read or refused, never misread, and `DlrMerger`'s
|
||||||
|
group shape is never published (goal 7). What processes share needs an atomic operation —
|
||||||
|
compare-and-set or increment — since get-then-set races.
|
||||||
|
- **Adapters live elsewhere.** Redis, Postgres or SQLite stores are packages of their own; this
|
||||||
|
one ships the interface and the in-memory store, and no runtime dependency (goal 9).
|
||||||
|
- **When the store fails.** Open: a send whose awaited receipt cannot be recorded is refused, or
|
||||||
|
sent and reported as undetermined (goal 2); a pool whose store is down stops, or falls back to
|
||||||
|
memory. Either way, an application that supplied no store never waits on one.
|
||||||
|
- **One spelling.** A store-backed cap and the limiter hook both reach a limit shared between
|
||||||
|
processes; settle which owns that case before building the second.
|
||||||
|
|||||||
Reference in New Issue
Block a user