Files
NetMesh/electron/bridges/tcpConnectLatency.cjs

162 lines
4.6 KiB
JavaScript
Raw Permalink Normal View History

"use strict";
const dns = require("node:dns");
const DEFAULT_TIMEOUT_MS = 3000;
const DEFAULT_CACHE_TTL_MS = 30000;
const DEFAULT_FAILURE_CACHE_TTL_MS = 5000;
const MAX_CACHE_ENTRIES = 256;
const MAX_CONCURRENT_PROBES = 8;
const MAX_QUEUED_PROBES = 64;
function createTcpConnectLatencyProbe({
net,
lookup = dns.lookup,
now = () => performance.now(),
cacheNow = () => Date.now(),
cacheTtlMs = DEFAULT_CACHE_TTL_MS,
failureCacheTtlMs = DEFAULT_FAILURE_CACHE_TTL_MS,
maxCacheEntries = MAX_CACHE_ENTRIES,
maxConcurrentProbes = MAX_CONCURRENT_PROBES,
maxQueuedProbes = MAX_QUEUED_PROBES,
setTimer = setTimeout,
clearTimer = clearTimeout,
}) {
const resultCache = new Map();
const inFlight = new Map();
const queue = [];
let activeProbeCount = 0;
const concurrentLimit = Math.max(1, Math.floor(maxConcurrentProbes));
const queueLimit = Math.max(0, Math.floor(maxQueuedProbes));
function runProbe({ hostname, port, timeoutMs }) {
return new Promise((resolve) => {
let startedAt = net.isIP?.(hostname) ? now() : null;
let settled = false;
let socket = null;
const finish = (latencyMs) => {
if (settled) return;
settled = true;
try { socket?.destroy(); } catch { /* best-effort cleanup */ }
resolve(latencyMs);
};
try {
const connectOptions = { host: hostname, port };
if (startedAt === null) {
connectOptions.lookup = (host, options, callback) => {
lookup(host, options, (err, address, family) => {
startedAt = now();
callback(err, address, family);
});
};
}
socket = net.createConnection(connectOptions, () => {
const connectedAt = now();
finish(Math.max(0, Math.round(connectedAt - (startedAt ?? connectedAt))));
});
socket.once("error", () => finish(null));
socket.setTimeout(timeoutMs, () => finish(null));
} catch {
finish(null);
}
});
}
function startScheduledProbe(task) {
activeProbeCount += 1;
let result;
try {
result = Promise.resolve(task());
} catch (err) {
result = Promise.reject(err);
}
return result.finally(() => {
activeProbeCount -= 1;
while (activeProbeCount < concurrentLimit && queue.length > 0) {
const next = queue.shift();
clearTimer(next.timer);
const remainingMs = next.deadline - cacheNow();
if (remainingMs <= 0) {
next.resolve(null);
continue;
}
startScheduledProbe(() => next.task(remainingMs)).then(next.resolve, next.reject);
}
});
}
function scheduleProbe(task, timeoutMs) {
if (activeProbeCount < concurrentLimit) return startScheduledProbe(() => task(timeoutMs));
if (queue.length >= queueLimit) return Promise.resolve(null);
return new Promise((resolve, reject) => {
const entry = {
task,
resolve,
reject,
deadline: cacheNow() + timeoutMs,
timer: null,
};
entry.timer = setTimer(() => {
const index = queue.indexOf(entry);
if (index >= 0) queue.splice(index, 1);
resolve(null);
}, timeoutMs);
queue.push(entry);
});
}
function cacheResult(key, value) {
resultCache.delete(key);
resultCache.set(key, {
value,
expiresAt: cacheNow() + (value === null ? failureCacheTtlMs : cacheTtlMs),
});
while (resultCache.size > maxCacheEntries) {
resultCache.delete(resultCache.keys().next().value);
}
}
return function measureTcpConnectLatency({ hostname, port, timeoutMs = DEFAULT_TIMEOUT_MS }) {
if (!hostname || !Number.isInteger(port) || port < 1 || port > 65535) {
return Promise.resolve(null);
}
const key = `${String(hostname).toLowerCase()}\u0000${port}`;
const pending = inFlight.get(key);
if (pending) return pending;
const cached = resultCache.get(key);
const checkedAt = cacheNow();
if (cached && cached.expiresAt > checkedAt) {
resultCache.delete(key);
resultCache.set(key, cached);
return Promise.resolve(cached.value);
}
resultCache.delete(key);
const promise = scheduleProbe(
(remainingMs) => runProbe({ hostname, port, timeoutMs: remainingMs }),
timeoutMs,
)
.catch(() => null)
.then((value) => {
inFlight.delete(key);
cacheResult(key, value);
return value;
});
inFlight.set(key, promise);
return promise;
};
}
module.exports = {
createTcpConnectLatencyProbe,
DEFAULT_TIMEOUT_MS,
DEFAULT_CACHE_TTL_MS,
MAX_CONCURRENT_PROBES,
MAX_QUEUED_PROBES,
};