Measure throughput, make it goal 6, and gate a release on it #14
@@ -660,23 +660,29 @@ one wins. They do not override the hard rules below.
|
|||||||
about a message the application sent reaches it as a report rather than as an inbound message, and
|
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
|
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.
|
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
|
6. **Fast enough that this library is never the bottleneck.** Throughput over a few sessions rather
|
||||||
|
than many idle ones, which is why this exists on Node at all. On four cores or more, one bound
|
||||||
|
session sustains at least 5,000 `submit_sm`/s at a send window of 1, 20,000 at the default window
|
||||||
|
of 10, and 30,000 at 50 or above, and asking for delivery receipts costs nothing measurable.
|
||||||
|
Memory is bounded per session, never per process. [benchmarks/](benchmarks/README.md) is how those
|
||||||
|
floors are checked; every release re-measures them and asks what it would take to go faster.
|
||||||
|
7. **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
|
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
|
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
|
options stays exactly as easy and as safe, and a hook is a seam the library calls, never a way into
|
||||||
its internals.
|
its internals.
|
||||||
7. **A small, stable public surface over reshapeable internals.** Only what `src/index.ts` exports is
|
8. **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
|
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. **State wider than one session goes through one store.** A pool of sessions, a limit shared
|
9. **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
|
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
|
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
|
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
|
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
|
to the application to persist. Coordinating processes any other way is declined without a fresh
|
||||||
argument each time.
|
argument each time.
|
||||||
9. **It builds, tests and runs the same everywhere.** Container-only toolchain, no runtime
|
10. **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.
|
||||||
|
|
||||||
@@ -693,6 +699,11 @@ Who depends on this library, and what they may rely on.
|
|||||||
they do in practice outranks what the specification says they should do.
|
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
|
- **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.
|
link. A peer they have to complain about is a defect however well the library reads.
|
||||||
|
- **The developer building the SMPP edge of something else.** What binds to `server()` in practice is
|
||||||
|
an aggregator's customer-facing edge, a bridge putting SMPP in front of a modern transport, or a
|
||||||
|
test double standing in for an SMSC. This is not a store-and-forward SMSC and will not become one:
|
||||||
|
spooling, scheduling, retry policy and billing belong to whatever this is the edge of, and state
|
||||||
|
shared between instances goes through the store in goal 9.
|
||||||
- **Pre-1.0, so the minor is the breaking unit** and a patch never breaks. What a 0.4.0 consumer has
|
- **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).
|
to change is in [MIGRATION.md](https://gitea.larvit.se/larvit/smpp-js/src/branch/main/MIGRATION.md).
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,26 @@ docker compose -f compose.yaml -f interop-tests/compose.jasmin.yaml run --rm nod
|
|||||||
--count=20000 --concurrency=50
|
--count=20000 --concurrency=50
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`GATE=1` fails the run when a window falls under goal 6's floor, and refuses to judge at all on
|
||||||
|
fewer than four cores.
|
||||||
|
|
||||||
**A short run measures the JIT, not the library.** At 2,000 messages per point the same build
|
**A short run measures the JIT, not the library.** At 2,000 messages per point the same build
|
||||||
reported 16k/s where 100,000 messages reported 40k/s. Give each point several seconds.
|
reported 16k/s where 100,000 messages reported 40k/s. Give each point several seconds.
|
||||||
|
|
||||||
|
**Cores matter, though the library is single-threaded.** The run is two Node processes, and past
|
||||||
|
them V8 marks and compiles on threads of its own while the kernel carries loopback TCP. Window 200,
|
||||||
|
100,000 messages:
|
||||||
|
|
||||||
|
| Cores | msgs/s |
|
||||||
|
| --- | --- |
|
||||||
|
| 1 | 20,476 |
|
||||||
|
| 2 | 29,603 |
|
||||||
|
| 4 | 32,869 |
|
||||||
|
| 8 | 40,046 |
|
||||||
|
|
||||||
|
A window of 1 is unmoved by any of it (7,280–8,659 throughout) because it waits on the round trip
|
||||||
|
rather than the CPU. The windowed figures are for the pair: one process alone reaches about half.
|
||||||
|
|
||||||
## Results, 2026-09-20
|
## Results, 2026-09-20
|
||||||
|
|
||||||
Single host, 8 cores, Node 24.18.0 in the project container, loopback. Client and SMSC are separate
|
Single host, 8 cores, Node 24.18.0 in the project container, loopback. Client and SMSC are separate
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
|
import { availableParallelism } from 'node:os';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { once } from 'node:events';
|
import { once } from 'node:events';
|
||||||
import { createInterface } from 'node:readline';
|
import { createInterface } from 'node:readline';
|
||||||
|
|
||||||
|
/** Goal 6's floors, per send window. Set GATE=1 to fail the run instead of only reporting. */
|
||||||
|
const floors: Record<number, number> = { 1: 5000, 10: 20_000, 50: 30_000, 200: 30_000 };
|
||||||
|
|
||||||
|
/** Below this, client and sink contend for one core and the windowed floors are unreachable. */
|
||||||
|
const gateCores = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Node rather than Python, unlike the repo's other standalone scripts: it spawns the two processes
|
* Node rather than Python, unlike the repo's other standalone scripts: it spawns the two processes
|
||||||
* it measures, and they must run on the same runtime this library is measured under.
|
* it measures, and they must run on the same runtime this library is measured under.
|
||||||
@@ -61,3 +68,28 @@ for (const row of rows) {
|
|||||||
|
|
||||||
process.stdout.write(`${dlr}${window}${rate}${String(row.seconds)}\n`);
|
process.stdout.write(`${dlr}${window}${rate}${String(row.seconds)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cores = availableParallelism();
|
||||||
|
|
||||||
|
process.stdout.write(`\n${String(cores)} cores\n`);
|
||||||
|
|
||||||
|
if (process.env.GATE !== '1') process.exit(0);
|
||||||
|
|
||||||
|
if (cores < gateCores) {
|
||||||
|
process.stdout.write(`refusing to gate on ${String(cores)} cores; goal 6 states four or more\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const short = rows.filter(row => {
|
||||||
|
const floor = floors[Number(row.concurrency)];
|
||||||
|
|
||||||
|
return floor !== undefined && Number(row.perSecond) < floor;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const row of short) {
|
||||||
|
const floor = String(floors[Number(row.concurrency)]);
|
||||||
|
|
||||||
|
process.stdout.write(`below goal 6: window ${String(row.concurrency)} ran ${String(row.perSecond)}/s, floor is ${floor}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(short.length === 0 ? 0 : 1);
|
||||||
|
|||||||
+10
-10
@@ -36,7 +36,7 @@ rule and an index of the titles below.
|
|||||||
- **`PduRefusedError` is exported, and `sessionError` names it in the event's type.** Maintainer's
|
- **`PduRefusedError` is exported, and `sessionError` names it in the event's type.** Maintainer's
|
||||||
call, 2026-09-05, from a product review: one event carries both a PDU the peer malformed and the
|
call, 2026-09-05, from a product review: one event carries both a PDU the peer malformed and the
|
||||||
session's own failure, and `instanceof` is the only way to separate them that hard rule 4 allows —
|
session's own failure, and `instanceof` is the only way to separate them that hard rule 4 allows —
|
||||||
without the class as a value an application is left string-matching `err.message`. Goal 7 is paid by
|
without the class as a value an application is left string-matching `err.message`. Goal 8 is paid by
|
||||||
exporting the discriminant and the struct it carries and nothing else: `PduHeader` is named because
|
exporting the discriminant and the struct it carries and nothing else: `PduHeader` is named because
|
||||||
an application that logs or forwards a header wants a name for it, `PduRefusalReason` is not
|
an application that logs or forwards a header wants a name for it, `PduRefusalReason` is not
|
||||||
because `reason` is compared against string literals, and an accessor
|
because `reason` is compared against string literals, and an accessor
|
||||||
@@ -63,7 +63,7 @@ rule and an index of the titles below.
|
|||||||
runtime: `Object.hasOwn(encodings, x)` is the only test the published surface offered and it
|
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()`,
|
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
|
which serve their own tables that way. Rejected: a `Result` signature on all three, which costs
|
||||||
every typed consumer a narrow forever — goal 7, and the tag is the last cheap chance to spend it —
|
every typed consumer a narrow forever — goal 8, 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:
|
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
|
`sendSms()` takes its options as `unknown` and refuses `encoding` by name, which is what a caller
|
||||||
without types gets. `smppTime.encode()` is where that reasoning lands the other way and is recorded
|
without types gets. `smppTime.encode()` is where that reasoning lands the other way and is recorded
|
||||||
@@ -168,13 +168,13 @@ rule and an index of the titles below.
|
|||||||
the suite ran took the 0x40 this library sends on a concatenated segment, but Route Mobile and
|
the suite ran took the 0x40 this library sends on a concatenated segment, but Route Mobile and
|
||||||
Kaleyra both document `esm_class` 0x43 for one, and a caller facing either had to hand-build every
|
Kaleyra both document `esm_class` 0x43 for one, and a caller facing either had to hand-build every
|
||||||
segment through `send()` — giving up the split, the per-segment ids, the send window and the
|
segment through `send()` — giving up the split, the per-segment ids, the send window and the
|
||||||
receipt merge, which is what goal 7 means by beating "the application can do this itself". The four
|
receipt merge, which is what goal 8 means by beating "the application can do this itself". The four
|
||||||
modes of SMPP 3.4 5.2.12 are a `MESSAGING_MODE` constant group and the option takes one of their
|
modes of SMPP 3.4 5.2.12 are a `MESSAGING_MODE` constant group and the option takes one of their
|
||||||
names, so 0x43 is a composition this library makes rather than a value a caller states, and the UDH
|
names, so 0x43 is a composition this library makes rather than a value a caller states, and the UDH
|
||||||
indicator a segment carrying a header needs cannot be cleared by anything the option can express.
|
indicator a segment carrying a header needs cannot be cleared by anything the option can express.
|
||||||
It takes three of those four: 2.10.3 carries transaction mode on `data_sm` alone, and none goes out
|
It takes three of those four: 2.10.3 carries transaction mode on `data_sm` alone, and none goes out
|
||||||
of here, so `FORWARD` stays in the group that mirrors the spec table and `sendSms()` refuses it by
|
of here, so `FORWARD` stays in the group that mirrors the spec table and `sendSms()` refuses it by
|
||||||
that reason rather than as an unknown name — a mode this library cannot deliver is a promise goal 7
|
that reason rather than as an unknown name — a mode this library cannot deliver is a promise goal 8
|
||||||
will not let it make. `DATAGRAM` with `dlr: true` is refused on the same footing: 2.10.2 defines the
|
will not let it make. `DATAGRAM` with `dlr: true` is refused on the same footing: 2.10.2 defines the
|
||||||
report away, so arming `DlrMerger` for one is goal 2's wrong answer, where the mode alone and a
|
report away, so arming `DlrMerger` for one is goal 2's wrong answer, where the mode alone and a
|
||||||
report under any other mode both go out untouched. Those three names left `ESM_CLASS`, where they
|
report under any other mode both go out untouched. Those three names left `ESM_CLASS`, where they
|
||||||
@@ -231,7 +231,7 @@ rule and an index of the titles below.
|
|||||||
`encodingByDataCoding()` reads the alphabet off that same test rather than repeating the group
|
`encodingByDataCoding()` reads the alphabet off that same test rather than repeating the group
|
||||||
masks beside it. It is exported for the reason `concatOf()` is — an application that needs a class
|
masks beside it. It is exported for the reason `concatOf()` is — an application that needs a class
|
||||||
other than 0 would otherwise rewrite the read this fixed. Rejected: a `messageClass` field on the
|
other than 0 would otherwise rewrite the read this fixed. Rejected: a `messageClass` field on the
|
||||||
`sms` event, which pays goal 7 for three classes nothing here acts on, where the boolean the
|
`sms` event, which pays goal 8 for three classes nothing here acts on, where the boolean the
|
||||||
application already had covers the one it does. Compressed text is out of scope and stays out —
|
application already had covers the one it does. Compressed text is out of scope and stays out —
|
||||||
nothing here implements 3GPP TS 23.042, so a compressed body reaches the application as whatever
|
nothing here implements 3GPP TS 23.042, so a compressed body reaches the application as whatever
|
||||||
its declared alphabet makes of it — but bit 5 does not move the class bits, so 0x30 is read as
|
its declared alphabet makes of it — but bit 5 does not move the class bits, so 0x30 is read as
|
||||||
@@ -413,14 +413,14 @@ rule and an index of the titles below.
|
|||||||
returned `42 44 46` — `"BDF"` — reported as built, while a string `message_payload` was cut to its
|
returned `42 44 46` — `"BDF"` — reported as built, while a string `message_payload` was cut to its
|
||||||
low octets whatever `data_coding` said. Goal 2 owns it, as it owns the `sendSms()` guard above.
|
low octets whatever `data_coding` said. Goal 2 owns it, as it owns the `sendSms()` guard above.
|
||||||
The line falls at the string: a `Buffer` is octets the caller already chose and goes out as given
|
The line falls at the string: a `Buffer` is octets the caller already chose and goes out as given
|
||||||
under any `data_coding`, which is what keeps goal 7's escape hatch open — the raw UDH, 8-bit binary
|
under any `data_coding`, which is what keeps goal 8's escape hatch open — the raw UDH, 8-bit binary
|
||||||
and deliberately malformed bodies `interop-tests/` builds are all still buildable — and a string
|
and deliberately malformed bodies `interop-tests/` builds are all still buildable — and a string
|
||||||
with no `data_coding` is untouched, detection carrying every character it was picked for. The
|
with no `data_coding` is untouched, detection carrying every character it was picked for. The
|
||||||
guard is `unencodable()` again rather than a second reading, and `unencodableText()` is the
|
guard is `unencodable()` again rather than a second reading, and `unencodableText()` is the
|
||||||
character, its code point and its index said once for both refusals — unexported where
|
character, its code point and its index said once for both refusals — unexported where
|
||||||
`unencodable()` is published, since wording `{ char, index }` into a sentence rewrites no read a
|
`unencodable()` is published, since wording `{ char, index }` into a sentence rewrites no read a
|
||||||
caller would get wrong, where asking the codec is, and publishing it would freeze this library's
|
caller would get wrong, where asking the codec is, and publishing it would freeze this library's
|
||||||
error prose as API for an application whose own refusal should read like itself. Goal 7, from the
|
error prose as API for an application whose own refusal should read like itself. Goal 8, from the
|
||||||
architecture review of [#99](https://github.com/larvit/larvitsmpp/pull/99), 2026-09-09. It is
|
architecture review of [#99](https://github.com/larvit/larvitsmpp/pull/99), 2026-09-09. It is
|
||||||
reached through
|
reached through
|
||||||
`encodeBody()` in `message.ts`, which is where the `data_coding`-to-text pair already lives:
|
`encodeBody()` in `message.ts`, which is where the `data_coding`-to-text pair already lives:
|
||||||
@@ -595,7 +595,7 @@ rule and an index of the titles below.
|
|||||||
dropped with the link — is traffic the peer will not send again, so each one reaches `sessionError`
|
dropped with the link — is traffic the peer will not send again, so each one reaches `sessionError`
|
||||||
as well as the log. Rejected there: an exported `MessageLostError` carrying the group, on the
|
as well as the log. Rejected there: an exported `MessageLostError` carrying the group, on the
|
||||||
`PduRefusedError` pattern — no `sms` ever fired for that group, so there is nothing in it the
|
`PduRefusedError` pattern — no `sms` ever fired for that group, so there is nothing in it the
|
||||||
application could act on, and goal 7 does not buy a second exported class to make a count
|
application could act on, and goal 8 does not buy a second exported class to make a count
|
||||||
distinguishable. Accepted: a completing segment whose own answer the socket would not carry still
|
distinguishable. Accepted: a completing segment whose own answer the socket would not carry still
|
||||||
reaches the application, because the message is whole and correct and the failed answer is on
|
reaches the application, because the message is whole and correct and the failed answer is on
|
||||||
`sessionError` — a peer that re-sends after the drop is the smaller risk than dropping a message
|
`sessionError` — a peer that re-sends after the drop is the smaller risk than dropping a message
|
||||||
@@ -607,7 +607,7 @@ rule and an index of the titles below.
|
|||||||
of the multipart change: `server()` filled the session's only `onRequest` slot, so the escape hatch
|
of the multipart change: `server()` filled the session's only `onRequest` slot, so the escape hatch
|
||||||
the error above names was reachable only by hand-wiring a `Session` over a raw socket, giving up
|
the error above names was reachable only by hand-wiring a `Session` over a raw socket, giving up
|
||||||
bind acceptance, `authenticate`, the session set and the drain `close()` runs over it — which is
|
bind acceptance, `authenticate`, the session set and the drain `close()` runs over it — which is
|
||||||
what goal 7 means by beating "the application can do this itself". What the library verifies is the
|
what goal 8 means by beating "the application can do this itself". What the library verifies is the
|
||||||
ordering rather than the hook's honesty about answering: the hook is consulted only for a non-bind
|
ordering rather than the hook's honesty about answering: the hook is consulted only for a non-bind
|
||||||
request on a session already bound, so no bind — a second one on a live session included — and
|
request on a session already bound, so no bind — a second one on a live session included — and
|
||||||
nothing a peer sends before one can be intercepted however the hook is written. One
|
nothing a peer sends before one can be intercepted however the hook is written. One
|
||||||
@@ -788,5 +788,5 @@ rule and an index of the titles below.
|
|||||||
call, 2026-09-14. Nothing verifies either platform, so the code avoids what is known to differ there:
|
call, 2026-09-14. Nothing verifies either platform, so the code avoids what is known to differ there:
|
||||||
shelling out, a path joined by hand, a signal Windows does not deliver, a Unix socket or a file mode.
|
shelling out, a path joined by hand, a signal Windows does not deliver, a Unix socket or a file mode.
|
||||||
That binds what `dist/` runs; the container tooling, `interop-tests/` and the `package.json` scripts
|
That binds what `dist/` runs; the container tooling, `interop-tests/` and the `package.json` scripts
|
||||||
run on Linux by goal 9. Rejected: macOS and Windows runners, on GitHub's mirror or as Gitea
|
run on Linux by goal 10. Rejected: macOS and Windows runners, on GitHub's mirror or as Gitea
|
||||||
host-mode runners on a Windows VM and a Mac.
|
host-mode runners on a Windows VM and a Mac.
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ and is also what the panel ranked hardest — two methods, one answer.
|
|||||||
reassembly dispatch, `onRequest` ordering and bind-direction refusal have no unit test because
|
reassembly dispatch, `onRequest` ordering and bind-direction refusal have no unit test because
|
||||||
the class cannot be built without a live socket. Carry the eight members as `IncomingDeps`,
|
the class cannot be built without a live socket. Carry the eight members as `IncomingDeps`,
|
||||||
exactly as `sendPastDrain` is carried now. No public surface changes. **Do this before the
|
exactly as `sendPastDrain` is carried now. No public surface changes. **Do this before the
|
||||||
store (goal 8), or the back-edge is baked into the store's published interface.**
|
store (goal 9), or the back-edge is baked into the store's published interface.**
|
||||||
|
|
||||||
- [ ] **Route `sms.ts` through its handlers, all of it.** `createSms()` already injects
|
- [ ] **Route `sms.ts` through its handlers, all of it.** `createSms()` already injects
|
||||||
`handlers.send`, and then reaches `sms.session.sendReturn()`, `sms.session.bindAllows()` and
|
`handlers.send`, and then reaches `sms.session.sendReturn()`, `sms.session.bindAllows()` and
|
||||||
@@ -340,7 +340,7 @@ and is also what the panel ranked hardest — two methods, one answer.
|
|||||||
|
|
||||||
### Doc claims this review falsified
|
### Doc claims this review falsified
|
||||||
|
|
||||||
- [ ] **Make "every README example is executed by the suite" true, or stop claiming it.** Goal 9 and
|
- [ ] **Make "every README example is executed by the suite" true, or stop claiming it.** Goal 10 and
|
||||||
the Done table both promise it; `test/readme.test.ts` transcribes the examples by hand and has
|
the Done table both promise it; `test/readme.test.ts` transcribes the examples by hand and has
|
||||||
drifted — 15 fenced `javascript` blocks in the README against 10 tests, and the test named "the
|
drifted — 15 fenced `javascript` blocks in the README against 10 tests, and the test named "the
|
||||||
documented sending options" passes none of the five options the README's example passes. Read
|
documented sending options" passes none of the five options the README's example passes. Read
|
||||||
@@ -459,7 +459,7 @@ and is also what the panel ranked hardest — two methods, one answer.
|
|||||||
|
|
||||||
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 goal 6: an option or a hook, with the call that passes none unchanged.
|
Each lands under goal 7: an option or a hook, with the call that passes none unchanged.
|
||||||
|
|
||||||
### Sending
|
### Sending
|
||||||
|
|
||||||
@@ -621,7 +621,7 @@ Each lands under goal 6: an option or a hook, with the call that passes none unc
|
|||||||
|
|
||||||
- [ ] **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 goal 8 was rewritten for it. Big: design before code.
|
and goal 9 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
|
||||||
@@ -634,10 +634,10 @@ Each lands under goal 6: an option or a hook, with the call that passes none unc
|
|||||||
in-memory store is enough; across processes the application supplies one.
|
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
|
- **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
|
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 —
|
group shape is never published (goal 8). What processes share needs an atomic operation —
|
||||||
compare-and-set or increment — since get-then-set races.
|
compare-and-set or increment — since get-then-set races.
|
||||||
- **Adapters live elsewhere.** Redis, Postgres or SQLite stores are packages of their own; this
|
- **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).
|
one ships the interface and the in-memory store, and no runtime dependency (goal 10).
|
||||||
- **When the store fails.** Open: a send whose awaited receipt cannot be recorded is refused, or
|
- **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
|
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.
|
memory. Either way, an application that supplied no store never waits on one.
|
||||||
|
|||||||
Reference in New Issue
Block a user