geonix 1.31.0 → 1.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/benchmarks/transport.js +223 -0
- package/package.json +2 -1
- package/src/Gateway.js +10 -0
- package/src/Registry.js +64 -5
- package/src/Service.js +9 -1
- package/src/Util.js +9 -0
package/README.md
CHANGED
|
@@ -295,6 +295,7 @@ Each returned part has:
|
|
|
295
295
|
| `GX_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
|
296
296
|
| `GX_STREAM_TIMEOUT` | `90000` | Max wait for a stream consumer to connect (ms) |
|
|
297
297
|
| `GX_INACTIVITY_TIMEOUT` | `90000` | TCP proxy (HTTP-over-NATS) inactivity timeout (ms) |
|
|
298
|
+
| `GX_HEALTH_TIMEOUT` | `2000` | Registry health-probe timeout per advertised address (ms) |
|
|
298
299
|
| `GX_SECRET` | — | Encryption key: AES-256-GCM payloads + HMAC-SHA256 subjects. Services without the same key cannot communicate. |
|
|
299
300
|
| `GX_DEBUG_ENDPOINT` | — | Mount path for the debug router (e.g. `/_debug`). Disabled when unset. |
|
|
300
301
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Latency / throughput benchmark comparing NATS vs LocalBus.
|
|
2
|
+
//
|
|
3
|
+
// The parent process spawns a child per transport so each child gets a fresh
|
|
4
|
+
// module load (Connection.start() auto-runs on import and reads GX_TRANSPORT).
|
|
5
|
+
// Each child measures two paths and prints JSON; the parent tabulates.
|
|
6
|
+
//
|
|
7
|
+
// Service RPC end-to-end Remote(svc).method() — uses HTTP loopback
|
|
8
|
+
// for either transport (entries.length === 1 forces the
|
|
9
|
+
// HTTP-RPC branch in directRequest)
|
|
10
|
+
// connection.request raw pub/sub round-trip — measures the actual transport
|
|
11
|
+
//
|
|
12
|
+
// Run: `node benchmarks/transport.js`
|
|
13
|
+
// Options (env): BENCH_ITERATIONS=2000 BENCH_WARMUP=200 BENCH_PAYLOAD_SIZE=64
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const root = resolve(here, "..");
|
|
21
|
+
|
|
22
|
+
const ITERATIONS = parseInt(process.env.BENCH_ITERATIONS) || 2000;
|
|
23
|
+
const WARMUP = parseInt(process.env.BENCH_WARMUP) || 200;
|
|
24
|
+
const PAYLOAD_SIZE = parseInt(process.env.BENCH_PAYLOAD_SIZE) || 64;
|
|
25
|
+
|
|
26
|
+
if (process.env.BENCH_CHILD) {
|
|
27
|
+
await runChild();
|
|
28
|
+
} else {
|
|
29
|
+
await runParent();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Parent: spawn one child per transport, collect JSON, print a comparison
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
async function runParent() {
|
|
37
|
+
const transports = [
|
|
38
|
+
{ name: "NATS", url: "nats://localhost" },
|
|
39
|
+
{ name: "LocalBus", url: "local://" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
process.stderr.write(`Iterations: ${ITERATIONS}, warmup: ${WARMUP}, payload: ${PAYLOAD_SIZE} bytes\n\n`);
|
|
43
|
+
|
|
44
|
+
const results = [];
|
|
45
|
+
for (const t of transports) {
|
|
46
|
+
process.stderr.write(`Running ${t.name} (${t.url})... `);
|
|
47
|
+
const t0 = Date.now();
|
|
48
|
+
try {
|
|
49
|
+
const r = await runChildProcess(t.url);
|
|
50
|
+
process.stderr.write(`ok (${((Date.now() - t0) / 1000).toFixed(1)}s)\n`);
|
|
51
|
+
results.push({ ...t, ...r });
|
|
52
|
+
} catch (e) {
|
|
53
|
+
process.stderr.write(`FAILED: ${e.message}\n`);
|
|
54
|
+
results.push({ ...t, error: e.message });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
printTable(results);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runChildProcess(transport) {
|
|
62
|
+
return new Promise((res, rej) => {
|
|
63
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url)], {
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
GX_TRANSPORT: transport,
|
|
67
|
+
GX_LOG_LEVEL: "none",
|
|
68
|
+
BENCH_CHILD: "1",
|
|
69
|
+
},
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
71
|
+
cwd: root,
|
|
72
|
+
});
|
|
73
|
+
let stdout = "";
|
|
74
|
+
let stderr = "";
|
|
75
|
+
child.stdout.on("data", (c) => stdout += c.toString());
|
|
76
|
+
child.stderr.on("data", (c) => stderr += c.toString());
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
child.kill("SIGKILL");
|
|
79
|
+
rej(new Error(`child timed out after 120s; stderr=${stderr.slice(0, 500)}`));
|
|
80
|
+
}, 120_000);
|
|
81
|
+
child.on("exit", (code) => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (code !== 0) {
|
|
84
|
+
return rej(new Error(`exit ${code}; stderr=${stderr.slice(0, 500)}`));
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
res(JSON.parse(stdout));
|
|
88
|
+
} catch (_e) {
|
|
89
|
+
rej(new Error(`bad JSON from child; stdout=${stdout.slice(0, 500)}`));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function printTable(results) {
|
|
96
|
+
const header = ["transport", "path", "mean (ms)", "p50", "p95", "p99", "throughput (rps)"];
|
|
97
|
+
const rows = [];
|
|
98
|
+
for (const r of results) {
|
|
99
|
+
if (r.error) {
|
|
100
|
+
rows.push([r.name, "—", "error", "—", "—", "—", r.error.slice(0, 50)]);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const s = r.serviceRPC;
|
|
104
|
+
const c = r.connectionRequest;
|
|
105
|
+
rows.push([
|
|
106
|
+
r.name,
|
|
107
|
+
"Service RPC (Remote)",
|
|
108
|
+
s.mean.toFixed(3),
|
|
109
|
+
s.p50.toFixed(3),
|
|
110
|
+
s.p95.toFixed(3),
|
|
111
|
+
s.p99.toFixed(3),
|
|
112
|
+
Math.round(s.throughput).toString(),
|
|
113
|
+
]);
|
|
114
|
+
rows.push([
|
|
115
|
+
r.name,
|
|
116
|
+
"connection.request",
|
|
117
|
+
c.mean.toFixed(3),
|
|
118
|
+
c.p50.toFixed(3),
|
|
119
|
+
c.p95.toFixed(3),
|
|
120
|
+
c.p99.toFixed(3),
|
|
121
|
+
Math.round(c.throughput).toString(),
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const widths = header.map((h, i) =>
|
|
126
|
+
Math.max(h.length, ...rows.map((r) => String(r[i]).length))
|
|
127
|
+
);
|
|
128
|
+
const fmt = (row) => "│ " + row.map((v, i) => String(v).padEnd(widths[i])).join(" │ ") + " │";
|
|
129
|
+
const sep = (l, m, r) => l + widths.map((w) => "─".repeat(w + 2)).join(m) + r;
|
|
130
|
+
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(sep("┌", "┬", "┐"));
|
|
133
|
+
console.log(fmt(header));
|
|
134
|
+
console.log(sep("├", "┼", "┤"));
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
console.log(fmt(row));
|
|
137
|
+
}
|
|
138
|
+
console.log(sep("└", "┴", "┘"));
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Child: actually run the benchmark and print JSON to stdout
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
async function runChild() {
|
|
147
|
+
const { Service, connection, registry, Remote } = await import("../exports.js");
|
|
148
|
+
const { decode } = await import("../src/Codec.js");
|
|
149
|
+
const { uniqueName, waitFor } = await import("../tests/helpers.js");
|
|
150
|
+
|
|
151
|
+
await connection.waitUntilReady();
|
|
152
|
+
|
|
153
|
+
const SVC = uniqueName("BenchSvc");
|
|
154
|
+
class BenchSvc extends Service {
|
|
155
|
+
echo(v) { return v; }
|
|
156
|
+
}
|
|
157
|
+
BenchSvc.start({ name: SVC });
|
|
158
|
+
await waitFor(() => registry.getIdentifier(SVC), 5000);
|
|
159
|
+
|
|
160
|
+
const payload = "x".repeat(PAYLOAD_SIZE);
|
|
161
|
+
|
|
162
|
+
function percentile(arr, p) {
|
|
163
|
+
const sorted = arr.slice().sort((a, b) => a - b);
|
|
164
|
+
return sorted[Math.min(Math.floor(sorted.length * p), sorted.length - 1)];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function summary(latencies, totalSeconds) {
|
|
168
|
+
return {
|
|
169
|
+
iterations: latencies.length,
|
|
170
|
+
mean: latencies.reduce((a, b) => a + b, 0) / latencies.length,
|
|
171
|
+
p50: percentile(latencies, 0.50),
|
|
172
|
+
p95: percentile(latencies, 0.95),
|
|
173
|
+
p99: percentile(latencies, 0.99),
|
|
174
|
+
throughput: latencies.length / totalSeconds,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Path 1: Service RPC via Remote (HTTP-loopback under the hood)
|
|
179
|
+
const remote = Remote(SVC);
|
|
180
|
+
for (let i = 0; i < WARMUP; i++) {
|
|
181
|
+
await remote.echo(payload);
|
|
182
|
+
}
|
|
183
|
+
let latencies = [];
|
|
184
|
+
let runStart = process.hrtime.bigint();
|
|
185
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
186
|
+
const t0 = process.hrtime.bigint();
|
|
187
|
+
await remote.echo(payload);
|
|
188
|
+
const t1 = process.hrtime.bigint();
|
|
189
|
+
latencies.push(Number(t1 - t0) / 1e6);
|
|
190
|
+
}
|
|
191
|
+
const serviceRPC = summary(latencies, Number(process.hrtime.bigint() - runStart) / 1e9);
|
|
192
|
+
|
|
193
|
+
// Path 2: direct connection.request (pure transport pub/sub round-trip)
|
|
194
|
+
const subject = `bench.rr.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
195
|
+
const responder = await connection.subscribe(subject);
|
|
196
|
+
(async () => {
|
|
197
|
+
for await (const event of responder) {
|
|
198
|
+
const call = decode(event.data);
|
|
199
|
+
await connection.publish(call.$r, call.p);
|
|
200
|
+
}
|
|
201
|
+
})().catch(() => { /* ignore on shutdown */ });
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < WARMUP; i++) {
|
|
204
|
+
await connection.request(subject, payload);
|
|
205
|
+
}
|
|
206
|
+
latencies = [];
|
|
207
|
+
runStart = process.hrtime.bigint();
|
|
208
|
+
for (let i = 0; i < ITERATIONS; i++) {
|
|
209
|
+
const t0 = process.hrtime.bigint();
|
|
210
|
+
await connection.request(subject, payload);
|
|
211
|
+
const t1 = process.hrtime.bigint();
|
|
212
|
+
latencies.push(Number(t1 - t0) / 1e6);
|
|
213
|
+
}
|
|
214
|
+
const connectionRequest = summary(latencies, Number(process.hrtime.bigint() - runStart) / 1e9);
|
|
215
|
+
responder.unsubscribe();
|
|
216
|
+
|
|
217
|
+
process.stdout.write(JSON.stringify({
|
|
218
|
+
payloadSize: PAYLOAD_SIZE,
|
|
219
|
+
serviceRPC,
|
|
220
|
+
connectionRequest,
|
|
221
|
+
}));
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geonix",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"test:unit": "GX_LOG_LEVEL=none node --test --test-reporter=spec tests/unit/*.test.js",
|
|
14
14
|
"test:integration": "GX_LOG_LEVEL=none node --test --test-concurrency=1 --test-reporter=spec tests/integration/*.test.js",
|
|
15
15
|
"lint": "npx eslint src",
|
|
16
|
+
"bench": "node benchmarks/transport.js",
|
|
16
17
|
"deploy": "npm run build && npm publish"
|
|
17
18
|
},
|
|
18
19
|
"author": "Davor Tarandek <dtarandek@tria.hr>",
|
package/src/Gateway.js
CHANGED
|
@@ -223,6 +223,16 @@ export class Gateway {
|
|
|
223
223
|
const existing = this.#registry[entry.i];
|
|
224
224
|
|
|
225
225
|
if (existing !== undefined) {
|
|
226
|
+
const hasAddresses = entry.a?.length > 0;
|
|
227
|
+
// Graduate from a NATS-tunnel proxy to direct HTTP once addresses become reachable
|
|
228
|
+
if (existing.proxy && hasAddresses) {
|
|
229
|
+
logger.info(`gateway.upgradeToDirect: ${entry.n}@${entry.v} (#${entry.i})`);
|
|
230
|
+
existing.proxy.server?.close();
|
|
231
|
+
existing.proxy = undefined;
|
|
232
|
+
existing.knownAddressCount = entry.a.length;
|
|
233
|
+
this.#rebuildRouter = true;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
226
236
|
// trigger rebuild if a direct entry has gained new addresses since last seen
|
|
227
237
|
if (!existing.proxy && entry.a?.length !== existing.knownAddressCount) {
|
|
228
238
|
existing.knownAddressCount = entry.a.length;
|
package/src/Registry.js
CHANGED
|
@@ -8,6 +8,11 @@ import { logger } from "./Logger.js";
|
|
|
8
8
|
|
|
9
9
|
const REGISTRY_ENTRY_TIMEOUT = 5000;
|
|
10
10
|
const GARBAGE_COLLECTOR_INTERVAL = 500;
|
|
11
|
+
const HEALTH_TIMEOUT = 2000;
|
|
12
|
+
|
|
13
|
+
// Read via function so tests can mutate the env after module load. Mirrors the
|
|
14
|
+
// pattern used by getTransportTimeout / getStreamTimeout (per CLAUDE.md N6).
|
|
15
|
+
const getHealthTimeout = () => parseInt(process.env.GX_HEALTH_TIMEOUT, 10) || HEALTH_TIMEOUT;
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Maintains a local, in-process view of all services currently present on the NATS bus.
|
|
@@ -43,7 +48,7 @@ class Registry extends EventEmitter {
|
|
|
43
48
|
this.#isActive = false;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
async #checkHealth(address, addresses, onFirstHealthy, timeout =
|
|
51
|
+
async #checkHealth(address, addresses, onFirstHealthy, timeout = getHealthTimeout()) {
|
|
47
52
|
try {
|
|
48
53
|
const result = await (await fetchWithTimeout(`http://${address}/!!_gx/health`, {}, timeout)).json();
|
|
49
54
|
|
|
@@ -60,6 +65,9 @@ class Registry extends EventEmitter {
|
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
async #registerService(data) {
|
|
68
|
+
// Capture the full advertised list before probing collapses data.a to the healthy subset
|
|
69
|
+
const advertised = Array.isArray(data.a) ? [...data.a] : [];
|
|
70
|
+
|
|
63
71
|
if (data.a?.length > 0) {
|
|
64
72
|
const allAddresses = data.a;
|
|
65
73
|
data.a = [];
|
|
@@ -89,6 +97,7 @@ class Registry extends EventEmitter {
|
|
|
89
97
|
|
|
90
98
|
this.#registry[data.i] = {
|
|
91
99
|
...data,
|
|
100
|
+
advertised,
|
|
92
101
|
timeout: Date.now() + REGISTRY_ENTRY_TIMEOUT,
|
|
93
102
|
};
|
|
94
103
|
|
|
@@ -102,6 +111,56 @@ class Registry extends EventEmitter {
|
|
|
102
111
|
this.emit("added", this.#registry[data.i]);
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
// Returns true when both arrays contain the same addresses (order-insensitive).
|
|
115
|
+
#sameAddresses(a, b) {
|
|
116
|
+
if (a.length !== b.length) { return false; }
|
|
117
|
+
const set = new Set(a);
|
|
118
|
+
for (const x of b) {
|
|
119
|
+
if (!set.has(x)) { return false; }
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Single-flight re-probe: rebuilds `entry.a` from `entry.advertised`. If a new advertised
|
|
125
|
+
// list arrives while a probe is running, queues exactly one re-run via probeStale.
|
|
126
|
+
async #reprobe(entry) {
|
|
127
|
+
if (entry.probeInFlight) {
|
|
128
|
+
entry.probeStale = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
entry.probeInFlight = true;
|
|
132
|
+
try {
|
|
133
|
+
do {
|
|
134
|
+
entry.probeStale = false;
|
|
135
|
+
const candidates = [...(entry.advertised ?? [])];
|
|
136
|
+
const healthy = [];
|
|
137
|
+
await Promise.all(candidates.map((a) => this.#checkHealth(a, healthy, () => { })));
|
|
138
|
+
entry.a = healthy;
|
|
139
|
+
} while (entry.probeStale);
|
|
140
|
+
} finally {
|
|
141
|
+
entry.probeInFlight = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Refresh timeout for a known entry; kick off a re-probe when either (a) the advertised
|
|
146
|
+
// list changed, or (b) the entry's healthy `a` is empty and the source is still advertising
|
|
147
|
+
// addresses — that's the "all probes failed at registration" case that needs to retry.
|
|
148
|
+
#refreshKnown(entry, advertised) {
|
|
149
|
+
entry.timeout = Date.now() + REGISTRY_ENTRY_TIMEOUT;
|
|
150
|
+
if (!Array.isArray(advertised)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const changed = !this.#sameAddresses(entry.advertised ?? [], advertised);
|
|
154
|
+
const stuckEmpty = (entry.a?.length ?? 0) === 0 && advertised.length > 0;
|
|
155
|
+
if (!changed && !stuckEmpty) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (changed) {
|
|
159
|
+
entry.advertised = [...advertised];
|
|
160
|
+
}
|
|
161
|
+
this.#reprobe(entry).catch((e) => logger.error("registry.reprobe:", e));
|
|
162
|
+
}
|
|
163
|
+
|
|
105
164
|
async #beaconListener() {
|
|
106
165
|
const subscription = await connection.subscribe("gx2.beacon");
|
|
107
166
|
|
|
@@ -110,18 +169,18 @@ class Registry extends EventEmitter {
|
|
|
110
169
|
|
|
111
170
|
const exists = this.#registry[data.i] !== undefined;
|
|
112
171
|
|
|
113
|
-
//
|
|
172
|
+
// Short beacon (no `.n`). May still carry an `.a` we should react to.
|
|
114
173
|
if (!data.n) {
|
|
115
174
|
if (exists) {
|
|
116
|
-
this.#registry[data.i]
|
|
175
|
+
this.#refreshKnown(this.#registry[data.i], data.a);
|
|
117
176
|
continue;
|
|
118
177
|
}
|
|
119
178
|
data = await directRequest(data.i, "$getServiceInfo");
|
|
120
179
|
}
|
|
121
180
|
|
|
122
|
-
// known service
|
|
181
|
+
// Full beacon for an already-known service: refresh timeout and react to `.a` changes.
|
|
123
182
|
if (exists) {
|
|
124
|
-
this.#registry[data.i]
|
|
183
|
+
this.#refreshKnown(this.#registry[data.i], data.a);
|
|
125
184
|
continue;
|
|
126
185
|
}
|
|
127
186
|
|
package/src/Service.js
CHANGED
|
@@ -157,7 +157,15 @@ export class Service {
|
|
|
157
157
|
*/
|
|
158
158
|
async #beacon() {
|
|
159
159
|
while (this.#isActive) {
|
|
160
|
-
|
|
160
|
+
// Refresh addresses each tick so a service that came up before its network was
|
|
161
|
+
// fully configured self-heals on the next beacon. Only `.a` can change at runtime;
|
|
162
|
+
// identity fields (i, n, v, m, gx) are immutable post-startup.
|
|
163
|
+
this.#me.a = getNetworkAddresses().map((address) => `${address}:${webserver.getPort()}`);
|
|
164
|
+
|
|
165
|
+
const payload = this.#options.fullBeacon
|
|
166
|
+
? this.#me
|
|
167
|
+
: { i: this.#me.i, a: this.#me.a };
|
|
168
|
+
|
|
161
169
|
connection.publish("gx2.beacon", payload).catch((e) => logger.warn("beacon.publish:", e));
|
|
162
170
|
await sleep(BEACON_INTERVAL);
|
|
163
171
|
}
|
package/src/Util.js
CHANGED
|
@@ -211,6 +211,15 @@ export function getNetworkAddresses() {
|
|
|
211
211
|
for (const interfaceAddresses of Object.values(interfaces)) {
|
|
212
212
|
if (!interfaceAddresses) { continue; }
|
|
213
213
|
for (const addressObject of interfaceAddresses) {
|
|
214
|
+
// Skip IPv6 link-local addresses (fe80::/10). They require a zone identifier
|
|
215
|
+
// (e.g. %eth0) to route, which `fetch()` doesn't accept — advertising them
|
|
216
|
+
// just wastes a probe slot.
|
|
217
|
+
if (
|
|
218
|
+
addressObject.family === "IPv6" &&
|
|
219
|
+
addressObject.address.toLowerCase().startsWith("fe80:")
|
|
220
|
+
) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
214
223
|
const addr = addressObject.family === "IPv4"
|
|
215
224
|
? addressObject.address
|
|
216
225
|
: addressObject.family === "IPv6"
|