Give up a connect after connectTimeout instead of waiting the OS out
Mirror / push (push) Successful in 4s
Test / lint (pull_request) Successful in 21s
Test / test (18) (pull_request) Successful in 22s
Test / test (20) (pull_request) Successful in 18s
Test / test (22) (pull_request) Successful in 18s
Test / test (24) (pull_request) Successful in 19s
Test / test (26) (pull_request) Successful in 27s

This commit is contained in:
2026-09-20 00:42:09 +02:00
parent d874448273
commit 17447bbe3a
8 changed files with 175 additions and 33 deletions
+20
View File
@@ -22,6 +22,7 @@ export type ClientOptions = {
addrNpi?: number;
addrTon?: number;
bindType?: BindType;
connectTimeout?: number;
enquireLinkInterval?: number;
host?: string;
idleTimeout?: number;
@@ -52,7 +53,22 @@ const defaults = {
username: 'user',
} as const;
function armConnectTimeout(
sock: Socket,
connectTimeout: number | undefined,
settle: (result: Result<{ sock: Socket }>) => void,
): NodeJS.Timeout | undefined {
if (connectTimeout === undefined) return undefined;
// The connecting socket is what holds the process, so this wait never has to.
return setTimeout(() => {
sock.destroy();
settle({ err: new Error(`Timed out connecting after ${String(connectTimeout)} ms`) });
}, connectTimeout).unref();
}
function openSocket(options: ClientOptions): Promise<Result<{ sock: Socket }>> {
const connectTimeout = options.connectTimeout;
const host = options.host ?? defaults.host;
const port = options.port ?? defaults.port;
const secure = options.tls !== undefined && options.tls !== false;
@@ -78,6 +94,8 @@ function openSocket(options: ClientOptions): Promise<Result<{ sock: Socket }>> {
}
const settle = (result: Result<{ sock: Socket }>): void => {
if (timer) clearTimeout(timer);
sock.removeListener('error', onError);
signal?.removeEventListener('abort', onAbort);
resolve(result);
@@ -97,6 +115,8 @@ function openSocket(options: ClientOptions): Promise<Result<{ sock: Socket }>> {
sock.once(secure ? 'secureConnect' : 'connect', () => {
settle({ sock });
});
const timer = armConnectTimeout(sock, connectTimeout, settle);
});
}
+28 -8
View File
@@ -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,28 @@ 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],
];
}
/** Leaving it out is how the wait stays the OS's, so 0 would be a second spelling for that. */
function checkConnectTimeout(connectTimeout: number | undefined): VoidResult {
if (connectTimeout === undefined) return {};
if (Number.isInteger(connectTimeout) && connectTimeout >= 1) return {};
const got = String(connectTimeout);
return { err: new Error(`connectTimeout must be 1 or more, got ${got}; omit it to wait the OS out`) };
}
function checkLimits(limits: [string, number, number][]): VoidResult {
for (const [name, value, min] of limits) {
if (!Number.isInteger(value) || value < min) {
@@ -238,6 +257,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?: number | undefined;
/** Not an option: the one spelling is inside reconnect, and this is where the other is refused. */
fromStart?: unknown;
idleTimeout?: number | undefined;