Give up a connect after connectTimeout instead of waiting the OS out #12
@@ -193,8 +193,9 @@ decision under [The wire](docs/decisions.md#the-wire).
|
||||
`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.
|
||||
- A socket a test opens and never reads must be `resume()`d, and a `data` listener counts. An unread
|
||||
socket never processes the peer's FIN, so `server.close()` hangs forever — that is a test bug, not
|
||||
a library one.
|
||||
- Everything a test opens gets its teardown registered as it is opened, never closed on the test's
|
||||
last line: an assertion that throws skips that line, and the listener it leaves behind keeps
|
||||
`node --test` alive until CI's ten-minute cap. `test/teardown.ts` covers a session, a server and a
|
||||
@@ -214,6 +215,8 @@ Each file answers one question, and a fact belongs to the file whose question it
|
||||
- **README.md — what you can rely on, and where this is heading.** Observable behaviour, for
|
||||
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.
|
||||
- **CHANGELOG.md — what changed for a consumer, per release.** Written for the public, never for the
|
||||
next agent, and a line lands there as the work ships rather than at release.
|
||||
- **MIGRATION.md — what a 0.4.0 consumer has to change.** Renamed and removed surface, and the
|
||||
behaviour that changed on the wire.
|
||||
- **AGENTS.md — what may not change, and why.** Hard rules, architecture, conventions, and an index
|
||||
@@ -299,6 +302,8 @@ the file.
|
||||
- Coming up is not proof a link works, so only one that outlasted `maxDelay` resets the backoff.
|
||||
- `reconnect: { fromStart: true }` puts the first connect and bind through that same loop, and
|
||||
`client()` then resolves only once it is bound.
|
||||
- `connectTimeout` defaults to 10 s, bounds the whole connect including the TLS handshake, and
|
||||
`false` is the one way to turn it off.
|
||||
- A stream this library cannot frame is a dead link; one PDU it cannot parse is not.
|
||||
- A deliberate shutdown drains; an unusable link and an abort do not.
|
||||
- Every segment of a concatenated message is answered as it arrives, so `sendResp()` on one is the
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.0 (unreleased)
|
||||
|
||||
- `client()` now bounds each connect attempt at 10 seconds, the TLS handshake included, and reports
|
||||
one that expires as an ordinary connect failure, so `reconnect` retries it on its usual backoff.
|
||||
A connect previously waited the operating system out, around 130 s on Linux against a host that
|
||||
drops SYNs. `connectTimeout` retunes the bound, and `connectTimeout: false` restores the old wait.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
The TypeScript rewrite. 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).
|
||||
@@ -227,6 +227,7 @@ All optional. Timeouts and delays are milliseconds.
|
||||
| `interfaceVersion` | `0x34` | The SMPP version declared at bind. `0x50` for an SMSC that requires SMPP 5.0. |
|
||||
| `systemType`, `addressRange`, `addrTon`, `addrNpi` | `''`, `''`, `0`, `0` | The remaining bind fields, for operators that require them. |
|
||||
| `tls` | `false` | `true` for defaults, or a `tls.ConnectionOptions` object for a private CA or a client certificate. |
|
||||
| `connectTimeout` | `10000` | Give up on **each connect attempt** the SMSC never completes, the TLS handshake included, and report it as an ordinary connect failure, which `reconnect` then retries. It bounds the socket and the handshake — never the `client()` call, and never the wait for the bind response, which is `responseTimeout`. `false` waits the operating system out instead, around 130 s on Linux; `0` is refused. |
|
||||
| `enquireLinkInterval` | `20000` | Interval between `enquire_link` on a quiet link. |
|
||||
| `idleTimeout` | `2 × enquireLinkInterval` | Give up on a link the peer has stopped answering, and re-bind unless `reconnect` is `false`. |
|
||||
| `responseTimeout` | `30000` | How long to wait for a response, and how long a send with no link waits for the next one. `0` waits forever. |
|
||||
@@ -629,6 +630,10 @@ if (isCommand(pduObj, 'submit_sm')) {
|
||||
| Spec tables | `cmds`, `consts`, `encodings`, `errors`, `tlvs`, `types`, the `cmdsById`, `constsById`, `errorsById` and `tlvsById` maps, and all of them grouped as `defs`. `isCommandName`, `isErrorName`, `isEncodingName`, `commandNameById` and `errorNameById` narrow a value into them. |
|
||||
| Types | Every option, result, event payload and table entry has a named type: `ClientOptions`, `ServerOptions`, `SendSmsOptions`, `SendSmsResult`, `Sms`, `Dlr`, `MessageDlr`, `Receipt`, `PduObject`, `PduHeader`, `SmppLog`, `Result` and the rest in `dist/index.d.ts`. |
|
||||
|
||||
## What changed per release
|
||||
|
||||
See [CHANGELOG.md](https://gitea.larvit.se/larvit/smpp-js/src/branch/main/CHANGELOG.md).
|
||||
|
||||
## Migrating from larvitsmpp 0.4.0
|
||||
|
||||
See [MIGRATION.md](https://gitea.larvit.se/larvit/smpp-js/src/branch/main/MIGRATION.md).
|
||||
|
||||
@@ -524,6 +524,26 @@ rule and an index of the titles below.
|
||||
`LinkGate`'s hold is not — it is awaited with no other handle, so a process whose only work is
|
||||
`client()` would exit unbound.
|
||||
|
||||
- **`connectTimeout` defaults to 10 s, bounds the whole connect including the TLS handshake, and
|
||||
`false` is the one way to turn it off.** Maintainer's call, 2026-09-20, serving goal 5: a connect
|
||||
that never returns is one `reconnect` cannot retry, because the operating system holds the attempt
|
||||
for around 130 s at Linux's default `tcp_syn_retries` and nothing above it is counting — and no
|
||||
application chooses that, so it is a default rather than an option that switches on what the caller
|
||||
obviously wanted. Accepted: the far end sees roughly four times the SYNs against a dead host, one
|
||||
per ~40 s rather than one per ~160 s, which goal 4 tolerates because the backoff still caps the
|
||||
rate. `false` spells the operating system's wait, as it does for `reconnect`, and `0` is refused
|
||||
naming it, so one spelling reaches each result. What expires is reported as the ordinary connect
|
||||
failure, so the loop retries it like any other, and the message names the peer and whether the TCP
|
||||
connect or the TLS handshake stalled — different faults, different answers. It settles on
|
||||
`secureConnect` for a TLS socket, so a peer that accepts and then says nothing is bounded the same
|
||||
way a black-holed SYN is. Rejected: shipping the option with no default, which left goal 5's "an
|
||||
option does not switch on the thing the caller obviously wanted" unmet, and would have cost a
|
||||
second breaking minor plus a reversal of the `0` spelling to correct later. Rejected:
|
||||
`socket.setTimeout()`, an idle timeout that goes on arming once the link is up. Rejected: bounding
|
||||
it with `responseTimeout`, which names the wait for an answer on a link that already exists and
|
||||
would retune both at once. `server()` shares the checker and ignores the option, as it already
|
||||
ignores `reconnect` — nothing at that end connects out.
|
||||
|
||||
- **A stream this library cannot frame is a dead link; one PDU it cannot parse is not.**
|
||||
Maintainer's call, 2026-08-31, narrowed 2026-09-05 via the interop plan: a `command_length` below
|
||||
16 or above `maxPduLength` leaves nothing that can say where the next PDU starts, so it tears the
|
||||
|
||||
+30
-2
@@ -22,6 +22,7 @@ export type ClientOptions = {
|
||||
addrNpi?: number;
|
||||
addrTon?: number;
|
||||
bindType?: BindType;
|
||||
connectTimeout?: number | false;
|
||||
enquireLinkInterval?: number;
|
||||
host?: string;
|
||||
idleTimeout?: number;
|
||||
@@ -42,6 +43,7 @@ export type ClientOptions = {
|
||||
|
||||
const defaults = {
|
||||
bindType: 'transceiver',
|
||||
connectTimeout: 10_000,
|
||||
enquireLinkInterval: 20_000,
|
||||
host: 'localhost',
|
||||
/** The idle timeout is what notices a dead link, so it has to outlast one silent probe. */
|
||||
@@ -52,7 +54,30 @@ const defaults = {
|
||||
username: 'user',
|
||||
} as const;
|
||||
|
||||
function armConnectTimeout(
|
||||
sock: Socket,
|
||||
connectTimeout: number | false,
|
||||
target: { peer: string; secure: boolean },
|
||||
settle: (result: Result<{ sock: Socket }>) => void,
|
||||
): NodeJS.Timeout | undefined {
|
||||
if (connectTimeout === false) return undefined;
|
||||
|
||||
let phase = `connecting to ${target.peer}`;
|
||||
|
||||
if (target.secure) {
|
||||
sock.once('connect', () => { phase = `completing the TLS handshake with ${target.peer}`; });
|
||||
}
|
||||
|
||||
return setTimeout(() => {
|
||||
sock.destroy();
|
||||
settle({
|
||||
err: new Error(`Timed out ${phase} after ${String(connectTimeout)} ms; raise connectTimeout or set it to false`),
|
||||
});
|
||||
}, connectTimeout).unref();
|
||||
}
|
||||
|
||||
function openSocket(options: ClientOptions): Promise<Result<{ sock: Socket }>> {
|
||||
const connectTimeout = options.connectTimeout ?? defaults.connectTimeout;
|
||||
const host = options.host ?? defaults.host;
|
||||
const port = options.port ?? defaults.port;
|
||||
const secure = options.tls !== undefined && options.tls !== false;
|
||||
@@ -77,11 +102,14 @@ function openSocket(options: ClientOptions): Promise<Result<{ sock: Socket }>> {
|
||||
return;
|
||||
}
|
||||
|
||||
const settle = (result: Result<{ sock: Socket }>): void => {
|
||||
const timer = armConnectTimeout(sock, connectTimeout, { peer: `${host}:${String(port)}`, secure }, settle);
|
||||
|
||||
function settle(result: Result<{ sock: Socket }>): void {
|
||||
clearTimeout(timer);
|
||||
sock.removeListener('error', onError);
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
resolve(result);
|
||||
};
|
||||
}
|
||||
|
||||
function onError(err: Error): void {
|
||||
settle({ err });
|
||||
|
||||
+4
-2
@@ -9,7 +9,9 @@ export function errorFrom(reason: unknown): Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** String() throws on a null-prototype object or a symbol, so only a string or number is printed. */
|
||||
const printable: readonly string[] = ['boolean', 'number', 'string'];
|
||||
|
||||
/** String() throws on a null-prototype object, so anything but these is named by its type. */
|
||||
export function namedValue(value: unknown): string {
|
||||
return typeof value === 'string' || typeof value === 'number' ? String(value) : typeof value;
|
||||
return printable.includes(typeof value) ? String(value) : typeof value;
|
||||
}
|
||||
|
||||
+35
-8
@@ -144,14 +144,11 @@ export function checkSessionOptions(options: CheckableOptions): VoidResult {
|
||||
return { err: new Error('fromStart is part of the reconnect policy, spell it reconnect: { fromStart: true }') };
|
||||
}
|
||||
|
||||
const checked = checkLimits([
|
||||
['idleTimeout', options.idleTimeout ?? 0, 0],
|
||||
['maxOutstanding', options.maxOutstanding ?? defaults.maxOutstanding, 1],
|
||||
['maxReassembly', options.maxReassembly ?? defaults.maxReassembly, 1],
|
||||
['reassemblyTimeout', options.reassemblyTimeout ?? defaults.reassemblyTimeout, 0],
|
||||
['responseTimeout', options.responseTimeout ?? defaults.responseTimeout, 0],
|
||||
['shutdownTimeout', options.shutdownTimeout ?? defaults.shutdownTimeout, 0],
|
||||
]);
|
||||
const connect = checkConnectTimeout(options.connectTimeout);
|
||||
|
||||
if (connect.err) return connect;
|
||||
|
||||
const checked = checkLimits(limitsOf(options));
|
||||
|
||||
if (checked.err) return checked;
|
||||
|
||||
@@ -160,6 +157,35 @@ export function checkSessionOptions(options: CheckableOptions): VoidResult {
|
||||
return backoff.err ? backoff : checkSmsIdFormat(options.smsIdFormat);
|
||||
}
|
||||
|
||||
function limitsOf(options: CheckableOptions): [string, number, number][] {
|
||||
return [
|
||||
['idleTimeout', options.idleTimeout ?? 0, 0],
|
||||
['maxOutstanding', options.maxOutstanding ?? defaults.maxOutstanding, 1],
|
||||
['maxReassembly', options.maxReassembly ?? defaults.maxReassembly, 1],
|
||||
['reassemblyTimeout', options.reassemblyTimeout ?? defaults.reassemblyTimeout, 0],
|
||||
['responseTimeout', options.responseTimeout ?? defaults.responseTimeout, 0],
|
||||
['shutdownTimeout', options.shutdownTimeout ?? defaults.shutdownTimeout, 0],
|
||||
];
|
||||
}
|
||||
|
||||
const maxTimerDelay = 2_147_483_647;
|
||||
|
||||
function checkConnectTimeout(connectTimeout: unknown): VoidResult {
|
||||
if (connectTimeout === undefined || connectTimeout === false) return {};
|
||||
|
||||
const got = typeof connectTimeout === 'string' ? `"${connectTimeout}"` : namedValue(connectTimeout);
|
||||
|
||||
if (typeof connectTimeout !== 'number' || !Number.isInteger(connectTimeout) || connectTimeout < 1) {
|
||||
return { err: new Error(`connectTimeout must be a whole number of milliseconds, 1 or more, got ${got}; false waits the OS out instead`) };
|
||||
}
|
||||
|
||||
if (connectTimeout > maxTimerDelay) {
|
||||
return { err: new Error(`connectTimeout must be ${String(maxTimerDelay)} ms or less (about 24 days), got ${got}; false waits the OS out instead`) };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function checkLimits(limits: [string, number, number][]): VoidResult {
|
||||
for (const [name, value, min] of limits) {
|
||||
if (!Number.isInteger(value) || value < min) {
|
||||
@@ -238,6 +264,7 @@ function checkSmsIdFormat(smsIdFormat: unknown): VoidResult {
|
||||
|
||||
/** What the checker reads, as it arrives: a caller without types can put anything in it. */
|
||||
export type CheckableOptions = {
|
||||
connectTimeout?: unknown;
|
||||
/** Not an option: the one spelling is inside reconnect, and this is where the other is refused. */
|
||||
fromStart?: unknown;
|
||||
idleTimeout?: number | undefined;
|
||||
|
||||
+130
-15
@@ -84,6 +84,21 @@ function within<T>(ms: number, promise: Promise<T>): Promise<T | undefined> {
|
||||
return Promise.race([promise, delay(ms).then((): undefined => undefined)]);
|
||||
}
|
||||
|
||||
/** A client still retrying holds a socket and a timer nothing else releases. */
|
||||
function abortAfter(
|
||||
t: TestContext,
|
||||
controller: AbortController,
|
||||
connecting: ReturnType<typeof client>,
|
||||
): void {
|
||||
t.after(async () => {
|
||||
controller.abort();
|
||||
|
||||
const { session } = await connecting;
|
||||
|
||||
await session?.close({ signal: AbortSignal.abort() });
|
||||
});
|
||||
}
|
||||
|
||||
function submitPdu(seqNr: number, cmdStatus: ErrorName = 'ESME_ROK'): PduObject {
|
||||
return {
|
||||
cmdId: 0x00000004,
|
||||
@@ -798,21 +813,6 @@ describe('reconnect from the first bind', () => {
|
||||
return port;
|
||||
}
|
||||
|
||||
/** A client still retrying holds a socket and a timer nothing else releases. */
|
||||
function abortAfter(
|
||||
t: TestContext,
|
||||
controller: AbortController,
|
||||
connecting: ReturnType<typeof client>,
|
||||
): void {
|
||||
t.after(async () => {
|
||||
controller.abort();
|
||||
|
||||
const { session } = await connecting;
|
||||
|
||||
await session?.close({ signal: AbortSignal.abort() });
|
||||
});
|
||||
}
|
||||
|
||||
test('gives up on the first attempt where reconnect alone is asked for', async () => {
|
||||
const port = await closedPort();
|
||||
const spy = logSpy();
|
||||
@@ -1018,6 +1018,121 @@ describe('reconnect from the first bind', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectTimeout', () => {
|
||||
/** Accepts and then says nothing, so a TLS handshake started on it never completes. */
|
||||
async function stalledListener(t: TestContext): Promise<{ accepted: net.Socket[]; port: number }> {
|
||||
const accepted: net.Socket[] = [];
|
||||
const listener = net.createServer(sock => {
|
||||
accepted.push(sock);
|
||||
sock.resume();
|
||||
});
|
||||
|
||||
closeListenerAfter(t, listener, accepted);
|
||||
await new Promise<void>(resolve => { listener.listen(0, '127.0.0.1', resolve); });
|
||||
|
||||
const address = listener.address();
|
||||
|
||||
return { accepted, port: typeof address === 'object' && address !== null ? address.port : 0 };
|
||||
}
|
||||
|
||||
test('gives up on a connect the peer never completes', async t => {
|
||||
const { port } = await stalledListener(t);
|
||||
const settled = await within(2000, client({
|
||||
connectTimeout: 150,
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
reconnect: false,
|
||||
tls: true,
|
||||
}));
|
||||
|
||||
assert.ok(settled, 'a handshake nothing answers is what the OS wait would swallow for minutes');
|
||||
assert.ok(settled.err instanceof Error);
|
||||
assert.match(
|
||||
settled.err.message,
|
||||
new RegExp(`Timed out completing the TLS handshake with 127\\.0\\.0\\.1:${String(port)} after 150 ms; raise connectTimeout`),
|
||||
'a firewall and a peer that accepts then stalls need different answers, and whoever reads this has never heard of the option',
|
||||
);
|
||||
assert.equal(settled.session, undefined);
|
||||
});
|
||||
|
||||
// Waits the default out for real: node:test mock timers land in Node 20.4, and the floor is 18.
|
||||
test('bounds a connect nobody asked to bound, at the default ten seconds', async t => {
|
||||
const { port } = await stalledListener(t);
|
||||
const connecting = client({ host: '127.0.0.1', port, reconnect: false, tls: true });
|
||||
const settled = await within(13_000, connecting);
|
||||
|
||||
assert.ok(settled, 'no bound was armed, so nothing ever settled this connect');
|
||||
assert.ok(settled.err instanceof Error);
|
||||
assert.match(settled.err.message, /after 10000 ms/);
|
||||
});
|
||||
|
||||
test('retries a connect it timed out on, like any other failed attempt', async t => {
|
||||
const { accepted, port } = await stalledListener(t);
|
||||
const controller = new AbortController();
|
||||
const connecting = client({
|
||||
connectTimeout: 60,
|
||||
host: '127.0.0.1',
|
||||
port,
|
||||
reconnect: { fromStart: true, maxDelay: 40, minDelay: 10 },
|
||||
signal: controller.signal,
|
||||
tls: true,
|
||||
});
|
||||
|
||||
abortAfter(t, controller, connecting);
|
||||
await delay(400);
|
||||
|
||||
assert.ok(accepted.length >= 3, `the loop retried what timed out, got ${String(accepted.length)} attempts`);
|
||||
});
|
||||
|
||||
test('disarms on the connect that completed, rather than on the socket that follows it', async t => {
|
||||
const smpp = await startServer(t);
|
||||
const { session } = await connect(t, smpp, { connectTimeout: 200 });
|
||||
|
||||
assert.ok(session);
|
||||
await delay(300);
|
||||
|
||||
const probe = await session.send({ cmdName: 'enquire_link' });
|
||||
|
||||
assert.equal(probe.err, undefined);
|
||||
assert.equal(session.sock.destroyed, false);
|
||||
});
|
||||
|
||||
test('refuses a connect timeout that would turn itself off', async () => {
|
||||
assert.match(
|
||||
checkSessionOptions({ connectTimeout: 0 }).err?.message ?? '',
|
||||
/false waits the OS out/,
|
||||
'off is spelled false, so 0 may not stand in for it',
|
||||
);
|
||||
assert.equal(checkSessionOptions({ connectTimeout: false }).err, undefined);
|
||||
assert.match(checkSessionOptions({ connectTimeout: -1 }).err?.message ?? '', /connectTimeout/);
|
||||
assert.match(checkSessionOptions({ connectTimeout: 1.5 }).err?.message ?? '', /whole number/);
|
||||
assert.match(
|
||||
checkSessionOptions({ connectTimeout: '5000' }).err?.message ?? '',
|
||||
/got "5000"/,
|
||||
'an env var read without Number() is the commonest untyped value, and it is a correct number',
|
||||
);
|
||||
assert.match(checkSessionOptions({ connectTimeout: true }).err?.message ?? '', /got true/);
|
||||
assert.match(
|
||||
checkSessionOptions({ connectTimeout: 2_147_483_648 }).err?.message ?? '',
|
||||
/2147483647 ms or less \(about 24 days\), got 2147483648; false waits the OS out instead/,
|
||||
'a delay Node cannot hold in 32 bits fires after 1 ms, the inverse of what it asked for',
|
||||
);
|
||||
assert.equal(checkSessionOptions({ connectTimeout: 2_147_483_647 }).err, undefined);
|
||||
assert.equal(checkSessionOptions({ connectTimeout: 1000 }).err, undefined);
|
||||
|
||||
const refused = await client({ connectTimeout: 0, port: 1 });
|
||||
|
||||
assert.ok(refused.err instanceof Error);
|
||||
assert.match(refused.err.message, /false waits/, 'the socket may not be opened before the option is refused');
|
||||
assert.equal(refused.session, undefined);
|
||||
|
||||
const off = await client({ connectTimeout: false, port: 1 });
|
||||
|
||||
assert.ok(off.err instanceof Error);
|
||||
assert.match(off.err.message, /ECONNREFUSED/, 'false opts out of the bound without breaking the connect');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sends across a reconnect', () => {
|
||||
/** Answers every message after the first, which is left to hold the send window open. */
|
||||
function answerAfterTheFirst(smpp: SmppServer, arrived: string[]): Latch {
|
||||
|
||||
@@ -1493,7 +1493,6 @@ describe('robustness', () => {
|
||||
// 0.4.0 registered a listener per sequence number and waited forever, leaking one per call.
|
||||
test('gives up on a peer that never answers', async t => {
|
||||
const accepted: net.Socket[] = [];
|
||||
// resume() so the socket drains; an unread socket never notices the peer hanging up.
|
||||
const silent = net.createServer(sock => { accepted.push(sock); sock.resume(); });
|
||||
|
||||
await new Promise<void>(resolve => silent.listen(0, resolve));
|
||||
|
||||
@@ -179,16 +179,22 @@ the rewrite, for a dependency added later. Maintainer's call, 2026-09-14.
|
||||
|
||||
## Worth doing, not blocking
|
||||
|
||||
- [ ] **Give up a connect after `connectTimeout` instead of waiting the OS out.** `openSocket()` in
|
||||
`client.ts` settles only on `connect`/`secureConnect`, a socket `error`, or the caller's
|
||||
`signal`, and `reconnect-loop.ts` times nothing but its backoff — so a host that drops SYNs
|
||||
stalls every attempt for the OS TCP timeout, around 130 s on Linux at the default
|
||||
`tcp_syn_retries`, and `reconnect.fromStart` cannot retry what never returns. The option aborts
|
||||
the socket and returns the ordinary connect failure; absent, the wait stays exactly 0.5.0's, so
|
||||
a call passing no options is unchanged (goal 6). Whether a later major makes it a default is
|
||||
the open half. Found comparing 0.5.0 with the seven `larvitsmpp` forks, 2026-09-20 —
|
||||
[SwiftHero/larvitsmpp](https://github.com/SwiftHero/larvitsmpp) added a 10 s connect timeout in
|
||||
2017, and it is the one fork change with no equivalent here.
|
||||
- [ ] **Cut the three teardown sentences `test/teardown.ts` already says.** Under AGENTS.md's
|
||||
Conventions, "`test/teardown.ts` covers a session, a server and a listener" restates its two
|
||||
exported names, "Its close aborts rather than drains" restates `closeAfter`'s own doc comment,
|
||||
and the `net.Server.close()` sentence restates `closeListenerAfter`'s. Keep the registered-at-
|
||||
creation rule and the FIFO one, which nothing else states, and drop "CI's ten-minute cap" —
|
||||
that number lives in `.gitea/workflows/test.yaml`. Raised by the prose pass, 2026-09-20.
|
||||
|
||||
- [ ] **Refuse a delay Node's timers cannot hold, in `checkLimits`.** `idleTimeout`,
|
||||
`reassemblyTimeout`, `responseTimeout` and `shutdownTimeout` take any integer, and `setTimeout`
|
||||
fires after 1 ms for anything above 2147483647 — so a value in the wrong unit gets the inverse
|
||||
of what it asked for, explained only by a warning on stderr. `connectTimeout` refuses one
|
||||
already, which is the asymmetry to close. The same four print an untyped value bare, so
|
||||
`idleTimeout: '5000'` is refused with `got 5000` — a value the reader reads as correct — where
|
||||
`connectTimeout` quotes it. `namedValue()`'s four sites — `messagingMode`, `encoding`, the time
|
||||
options and `smsIdFormat` — are the same defect once more: there `true` and `'true'` both print
|
||||
as `true`. One fix closes all three. Raised by review, 2026-09-20.
|
||||
|
||||
- [ ] **A send the codec will refuse waits for a link and a window slot first.** `refuse()` in
|
||||
`outgoing-requests.ts` runs `misuse()` and the abort check before the wait, precisely so a call
|
||||
|
||||
Reference in New Issue
Block a user