geonix 1.30.2 → 1.30.4
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/package.json +2 -2
- package/src/Codec.js +1 -1
- package/src/Connection.js +76 -19
- package/src/Crypto.js +23 -9
- package/src/Gateway.js +37 -28
- package/src/Logger.js +16 -7
- package/src/Registry.js +19 -13
- package/src/Remote.js +12 -8
- package/src/Request.js +30 -14
- package/src/Service.js +66 -39
- package/src/Stream.js +12 -6
- package/src/Util.js +51 -44
- package/src/WebServer.js +11 -12
- package/.vscode/settings.json +0 -11
package/package.json
CHANGED
package/src/Codec.js
CHANGED
package/src/Connection.js
CHANGED
|
@@ -10,7 +10,7 @@ import { encryptPayload, decryptPayload, encryptSubject, wrapSubscription } from
|
|
|
10
10
|
const CONNECTION_TIMEOUT = 10000;
|
|
11
11
|
|
|
12
12
|
const defaultRequestOptions = {
|
|
13
|
-
timeout: 300000
|
|
13
|
+
timeout: 300000,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const DEFAULT_CONNECTION_COUNT = 1;
|
|
@@ -21,7 +21,16 @@ const defaultConnectionOptions = {
|
|
|
21
21
|
debug: (process.env.GX_TRANSPORT_DEBUG || process.env.TRANSPORT_DEBUG) === "true",
|
|
22
22
|
maxReconnectAttempts: 30,
|
|
23
23
|
pingInterval: 30000,
|
|
24
|
-
waitOnFirstConnect: true
|
|
24
|
+
waitOnFirstConnect: true,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TRANSPORT_TIMEOUT = 60_000;
|
|
28
|
+
const WATCHDOG_INTERVAL = 1000;
|
|
29
|
+
|
|
30
|
+
// env read via a function so tests can set GX_TRANSPORT_TIMEOUT after this module is imported
|
|
31
|
+
const getTransportTimeout = () => {
|
|
32
|
+
const raw = parseInt(process.env.GX_TRANSPORT_TIMEOUT, 10);
|
|
33
|
+
return Number.isFinite(raw) ? raw : TRANSPORT_TIMEOUT;
|
|
25
34
|
};
|
|
26
35
|
// -------------------------------------------------------------------------------------------------
|
|
27
36
|
|
|
@@ -32,12 +41,13 @@ const defaultConnectionOptions = {
|
|
|
32
41
|
* encryption are activated by setting the `GX_SECRET` environment variable.
|
|
33
42
|
*/
|
|
34
43
|
class Connection {
|
|
35
|
-
|
|
36
44
|
#draining = false;
|
|
37
45
|
#closed = false;
|
|
38
46
|
#ready = false;
|
|
39
47
|
#connections = [];
|
|
40
48
|
#connectionRoundRobin = 0;
|
|
49
|
+
#disconnectedSince = Date.now();
|
|
50
|
+
#watchdogInterval = null;
|
|
41
51
|
|
|
42
52
|
#getConnection() {
|
|
43
53
|
return this.#connections[this.#connectionRoundRobin++ % this.#connections.length];
|
|
@@ -54,20 +64,52 @@ class Connection {
|
|
|
54
64
|
async start(transport = process.env.GX_TRANSPORT || process.env.TRANSPORT || "nats://localhost") {
|
|
55
65
|
const { connections: _connectionCount, ...natsOptions } = {
|
|
56
66
|
...defaultConnectionOptions,
|
|
57
|
-
...parseURL(transport)
|
|
67
|
+
...parseURL(transport),
|
|
58
68
|
};
|
|
59
69
|
const connectionCount = parseInt(_connectionCount) || DEFAULT_CONNECTION_COUNT;
|
|
60
70
|
|
|
71
|
+
this.#startWatchdog();
|
|
72
|
+
|
|
61
73
|
for (let i = 0; i < connectionCount; i++) {
|
|
62
74
|
this.#connections.push(await connect(natsOptions));
|
|
63
75
|
}
|
|
64
76
|
|
|
77
|
+
this.#disconnectedSince = null;
|
|
78
|
+
|
|
65
79
|
logger.info("gx.connection.connected");
|
|
66
80
|
|
|
67
81
|
this.#ready = true;
|
|
68
82
|
|
|
69
83
|
this.monitorStatus();
|
|
70
|
-
this.waitUntilClosed().catch(e => logger.error("gx.connection.waitUntilClosed:", e));
|
|
84
|
+
this.waitUntilClosed().catch((e) => logger.error("gx.connection.waitUntilClosed:", e));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Polls the disconnect timestamp; exits the process if the transport has been
|
|
88
|
+
// unreachable longer than GX_TRANSPORT_TIMEOUT (default 60s, 0 disables).
|
|
89
|
+
#startWatchdog() {
|
|
90
|
+
if (this.#watchdogInterval) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.#watchdogInterval = setInterval(() => {
|
|
95
|
+
const limit = getTransportTimeout();
|
|
96
|
+
if (limit === 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (this.#disconnectedSince === null) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const elapsed = Date.now() - this.#disconnectedSince;
|
|
104
|
+
if (elapsed >= limit) {
|
|
105
|
+
logger.error(
|
|
106
|
+
`gx.connection: transport unreachable for ${Math.round(elapsed / 1000)}s (limit ${limit}ms), exiting`,
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}, WATCHDOG_INTERVAL);
|
|
111
|
+
|
|
112
|
+
this.#watchdogInterval.unref?.();
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
/**
|
|
@@ -78,9 +120,17 @@ class Connection {
|
|
|
78
120
|
for (const conn of this.#connections) {
|
|
79
121
|
(async () => {
|
|
80
122
|
for await (const event of conn.status()) {
|
|
123
|
+
if (event.type === "disconnect") {
|
|
124
|
+
if (this.#disconnectedSince === null) {
|
|
125
|
+
this.#disconnectedSince = Date.now();
|
|
126
|
+
}
|
|
127
|
+
} else if (event.type === "reconnect") {
|
|
128
|
+
this.#disconnectedSince = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
81
131
|
logger.debug("gx.connection.status", JSON.stringify(event));
|
|
82
132
|
}
|
|
83
|
-
})().catch(e => logger.error("gx.connection.status:", e));
|
|
133
|
+
})().catch((e) => logger.error("gx.connection.status:", e));
|
|
84
134
|
}
|
|
85
135
|
}
|
|
86
136
|
|
|
@@ -92,11 +142,16 @@ class Connection {
|
|
|
92
142
|
*/
|
|
93
143
|
async waitUntilClosed() {
|
|
94
144
|
// wait for all connections to be closed
|
|
95
|
-
await Promise.all(this.#connections.map(connection => connection.closed()));
|
|
145
|
+
await Promise.all(this.#connections.map((connection) => connection.closed()));
|
|
96
146
|
|
|
97
147
|
this.#closed = true;
|
|
98
148
|
logger.info("gx.connection.closed");
|
|
99
149
|
|
|
150
|
+
if (this.#watchdogInterval) {
|
|
151
|
+
clearInterval(this.#watchdogInterval);
|
|
152
|
+
this.#watchdogInterval = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
100
155
|
webserver.stop();
|
|
101
156
|
|
|
102
157
|
await sleep(5000);
|
|
@@ -118,9 +173,9 @@ class Connection {
|
|
|
118
173
|
|
|
119
174
|
/**
|
|
120
175
|
* Publish JSON
|
|
121
|
-
*
|
|
122
|
-
* @param {string} subject
|
|
123
|
-
* @param {object} json
|
|
176
|
+
*
|
|
177
|
+
* @param {string} subject
|
|
178
|
+
* @param {object} json
|
|
124
179
|
* @returns void
|
|
125
180
|
*/
|
|
126
181
|
async publish(subject, json) {
|
|
@@ -150,21 +205,24 @@ class Connection {
|
|
|
150
205
|
return;
|
|
151
206
|
}
|
|
152
207
|
|
|
153
|
-
await this.#getConnection().publish(
|
|
208
|
+
await this.#getConnection().publish(
|
|
209
|
+
encryptSubject(subject),
|
|
210
|
+
encryptPayload(data != null ? Buffer.from(data) : Buffer.alloc(0)),
|
|
211
|
+
);
|
|
154
212
|
}
|
|
155
213
|
|
|
156
214
|
/**
|
|
157
215
|
* Request/Reply pattern on top of pub/sub
|
|
158
|
-
*
|
|
159
|
-
* @param {string} subject
|
|
160
|
-
* @param {object} json
|
|
161
|
-
* @param {object} options
|
|
216
|
+
*
|
|
217
|
+
* @param {string} subject
|
|
218
|
+
* @param {object} json
|
|
219
|
+
* @param {object} options
|
|
162
220
|
* @returns any
|
|
163
221
|
*/
|
|
164
222
|
async request(subject, json, opts = {}) {
|
|
165
223
|
const options = {
|
|
166
224
|
...defaultRequestOptions,
|
|
167
|
-
...opts
|
|
225
|
+
...opts,
|
|
168
226
|
};
|
|
169
227
|
|
|
170
228
|
const respondTo = `gx2.r.${picoid(16)}`;
|
|
@@ -235,9 +293,8 @@ class Connection {
|
|
|
235
293
|
async drain() {
|
|
236
294
|
this.#draining = true;
|
|
237
295
|
|
|
238
|
-
await Promise.all(this.#connections.map(connection => connection.drain()));
|
|
296
|
+
await Promise.all(this.#connections.map((connection) => connection.drain()));
|
|
239
297
|
}
|
|
240
|
-
|
|
241
298
|
}
|
|
242
299
|
|
|
243
300
|
/**
|
|
@@ -247,7 +304,7 @@ class Connection {
|
|
|
247
304
|
* @type {Connection}
|
|
248
305
|
*/
|
|
249
306
|
export const connection = new Connection();
|
|
250
|
-
connection.start().catch(e => {
|
|
307
|
+
connection.start().catch((e) => {
|
|
251
308
|
logger.error("gx.connection.start:", e);
|
|
252
309
|
process.exit(1);
|
|
253
310
|
});
|
package/src/Crypto.js
CHANGED
|
@@ -3,7 +3,11 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes }
|
|
|
3
3
|
const _secret = process.env.GX_SECRET || null;
|
|
4
4
|
|
|
5
5
|
// Subject key is derived separately so HMAC-subject and AES-payload never share key material.
|
|
6
|
-
const _subjectKey = _secret
|
|
6
|
+
const _subjectKey = _secret
|
|
7
|
+
? createHash("sha256")
|
|
8
|
+
.update(_secret + "\x00subject")
|
|
9
|
+
.digest()
|
|
10
|
+
: null;
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* AES-256-GCM key derived from `GX_SECRET`, or `null` when encryption is disabled.
|
|
@@ -11,7 +15,11 @@ const _subjectKey = _secret ? createHash("sha256").update(_secret + "\x00subject
|
|
|
11
15
|
*
|
|
12
16
|
* @type {Buffer|null}
|
|
13
17
|
*/
|
|
14
|
-
export const _payloadKey = _secret
|
|
18
|
+
export const _payloadKey = _secret
|
|
19
|
+
? createHash("sha256")
|
|
20
|
+
.update(_secret + "\x00payload")
|
|
21
|
+
.digest()
|
|
22
|
+
: null;
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Encrypts `data` with AES-256-GCM using {@link _payloadKey}. Returns `data` unchanged when
|
|
@@ -21,7 +29,9 @@ export const _payloadKey = _secret ? createHash("sha256").update(_secret + "\x00
|
|
|
21
29
|
* @returns {Buffer}
|
|
22
30
|
*/
|
|
23
31
|
export function encryptPayload(data) {
|
|
24
|
-
if (!_payloadKey) {
|
|
32
|
+
if (!_payloadKey) {
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
25
35
|
const buf = data != null ? Buffer.from(data) : Buffer.alloc(0);
|
|
26
36
|
const iv = randomBytes(12);
|
|
27
37
|
const cipher = createCipheriv("aes-256-gcm", _payloadKey, iv);
|
|
@@ -42,7 +52,9 @@ export function encryptPayload(data) {
|
|
|
42
52
|
* @returns {Buffer}
|
|
43
53
|
*/
|
|
44
54
|
export function decryptPayload(data) {
|
|
45
|
-
if (!_payloadKey) {
|
|
55
|
+
if (!_payloadKey) {
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
46
58
|
const buf = Buffer.from(data);
|
|
47
59
|
const decipher = createDecipheriv("aes-256-gcm", _payloadKey, buf.subarray(0, 12));
|
|
48
60
|
decipher.setAuthTag(buf.subarray(12, 28));
|
|
@@ -64,10 +76,12 @@ export function encryptSubject(subject) {
|
|
|
64
76
|
return subject;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
|
-
return subject
|
|
68
|
-
(
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
return subject
|
|
80
|
+
.split(".")
|
|
81
|
+
.map((seg) =>
|
|
82
|
+
seg === "*" || seg === ">" ? seg : createHmac("sha256", _subjectKey).update(seg).digest("hex").slice(0, 32),
|
|
83
|
+
)
|
|
84
|
+
.join(".");
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
/**
|
|
@@ -94,7 +108,7 @@ export function wrapSubscription(sub) {
|
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
return { value: { ...value, data: decryptPayload(value.data) }, done: false };
|
|
97
|
-
}
|
|
111
|
+
},
|
|
98
112
|
};
|
|
99
113
|
},
|
|
100
114
|
drain: () => sub.drain(),
|
package/src/Gateway.js
CHANGED
|
@@ -15,12 +15,14 @@ const MAX_SESSIONS = 16384;
|
|
|
15
15
|
// Real client IP — checks well-known CDN/proxy headers before falling back to socket address
|
|
16
16
|
function getClientIp(req) {
|
|
17
17
|
const header =
|
|
18
|
-
req.headers["cf-connecting-ip"] ||
|
|
19
|
-
req.headers["true-client-ip"] ||
|
|
20
|
-
req.headers["x-real-ip"] ||
|
|
21
|
-
req.headers["x-forwarded-for"];
|
|
18
|
+
req.headers["cf-connecting-ip"] || // Cloudflare
|
|
19
|
+
req.headers["true-client-ip"] || // Cloudflare Enterprise / Akamai
|
|
20
|
+
req.headers["x-real-ip"] || // nginx
|
|
21
|
+
req.headers["x-forwarded-for"]; // standard (may be comma-separated list)
|
|
22
22
|
|
|
23
|
-
if (header) {
|
|
23
|
+
if (header) {
|
|
24
|
+
return header.split(",")[0].trim();
|
|
25
|
+
}
|
|
24
26
|
return req.socket?.remoteAddress || "unknown";
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -39,12 +41,12 @@ const stats = {
|
|
|
39
41
|
requests: 0,
|
|
40
42
|
proxied: 0,
|
|
41
43
|
proxied_over_nats: 0,
|
|
42
|
-
debug_requests: 0
|
|
44
|
+
debug_requests: 0,
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
const defaultOpts = {
|
|
46
|
-
beforeRequest: (_req, _res) => {
|
|
47
|
-
afterRequest: (_req, _res) => {
|
|
48
|
+
beforeRequest: (_req, _res) => {},
|
|
49
|
+
afterRequest: (_req, _res) => {},
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
/**
|
|
@@ -53,7 +55,6 @@ const defaultOpts = {
|
|
|
53
55
|
* dynamically as services join and leave the bus.
|
|
54
56
|
*/
|
|
55
57
|
export class Gateway {
|
|
56
|
-
|
|
57
58
|
/**
|
|
58
59
|
* Creates and starts a new Gateway instance.
|
|
59
60
|
*
|
|
@@ -85,7 +86,7 @@ export class Gateway {
|
|
|
85
86
|
|
|
86
87
|
this.#opts = { ...this.#opts, ...opts };
|
|
87
88
|
|
|
88
|
-
this.#start().catch(e => logger.error("gx.gateway.start:", e));
|
|
89
|
+
this.#start().catch((e) => logger.error("gx.gateway.start:", e));
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async #start(port = 8080) {
|
|
@@ -179,7 +180,7 @@ export class Gateway {
|
|
|
179
180
|
this.#api.all("*", (req, res) => {
|
|
180
181
|
res.status(404).send({
|
|
181
182
|
error: 404,
|
|
182
|
-
source: "gw"
|
|
183
|
+
source: "gw",
|
|
183
184
|
});
|
|
184
185
|
});
|
|
185
186
|
|
|
@@ -253,7 +254,7 @@ export class Gateway {
|
|
|
253
254
|
return true;
|
|
254
255
|
};
|
|
255
256
|
|
|
256
|
-
entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
|
|
257
|
+
entries = (await Promise.all(entries.map(processEntry))).filter((result) => result === true);
|
|
257
258
|
|
|
258
259
|
if (entries.length > 0) {
|
|
259
260
|
this.#rebuildRouter = true;
|
|
@@ -285,7 +286,7 @@ export class Gateway {
|
|
|
285
286
|
});
|
|
286
287
|
|
|
287
288
|
router.get("/services", (req, res) => {
|
|
288
|
-
const services = Object.values(registry.getEntries()).map(e =>
|
|
289
|
+
const services = Object.values(registry.getEntries()).map((e) => `${e.n}@${e.v}`);
|
|
289
290
|
services.sort();
|
|
290
291
|
res.send(services);
|
|
291
292
|
});
|
|
@@ -312,11 +313,11 @@ export class Gateway {
|
|
|
312
313
|
node: {
|
|
313
314
|
version: process.version,
|
|
314
315
|
platform: process.platform,
|
|
315
|
-
arch: process.arch
|
|
316
|
+
arch: process.arch,
|
|
316
317
|
},
|
|
317
318
|
mem: process.memoryUsage(),
|
|
318
319
|
rss: process.memoryUsage.rss(),
|
|
319
|
-
cpu: process.cpuUsage()
|
|
320
|
+
cpu: process.cpuUsage(),
|
|
320
321
|
});
|
|
321
322
|
});
|
|
322
323
|
|
|
@@ -341,19 +342,21 @@ export class Gateway {
|
|
|
341
342
|
});
|
|
342
343
|
|
|
343
344
|
const dataLoop = async () => {
|
|
344
|
-
for await (const event of ingress) {
|
|
345
|
+
for await (const event of ingress) {
|
|
346
|
+
client.write(event.data);
|
|
347
|
+
}
|
|
345
348
|
};
|
|
346
349
|
|
|
347
|
-
dataLoop().catch(e => logger.error("nats.proxy.dataLoop:", e));
|
|
350
|
+
dataLoop().catch((e) => logger.error("nats.proxy.dataLoop:", e));
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
|
|
351
354
|
/**
|
|
352
355
|
* Proxies websocket connection
|
|
353
|
-
*
|
|
354
|
-
* @param {string} target
|
|
355
|
-
* @param {Readable} inbound
|
|
356
|
-
* @param {Request} req
|
|
356
|
+
*
|
|
357
|
+
* @param {string} target
|
|
358
|
+
* @param {Readable} inbound
|
|
359
|
+
* @param {Request} req
|
|
357
360
|
*/
|
|
358
361
|
#proxyWebsocket(target, inbound, req) {
|
|
359
362
|
try {
|
|
@@ -366,7 +369,7 @@ export class Gateway {
|
|
|
366
369
|
|
|
367
370
|
backend.on("open", () => {
|
|
368
371
|
backend.on("message", (data, isBinary) => inbound.send(isBinary ? data : data.toString()));
|
|
369
|
-
inbound.on("message", data => backend.send(data));
|
|
372
|
+
inbound.on("message", (data) => backend.send(data));
|
|
370
373
|
|
|
371
374
|
backend.on("close", () => inbound.close());
|
|
372
375
|
inbound.on("close", () => backend.close());
|
|
@@ -398,18 +401,22 @@ export class Gateway {
|
|
|
398
401
|
? `127.0.0.1:${proxy.port}`
|
|
399
402
|
: entry.a?.[Math.floor(Math.random() * entry.a.length)];
|
|
400
403
|
|
|
401
|
-
if (!backendAddr) {
|
|
404
|
+
if (!backendAddr) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
402
407
|
|
|
403
408
|
// generate global endpoint list
|
|
404
409
|
for (let e of entry.m) {
|
|
405
410
|
const endpointMatch = endpointMatcher.exec(e);
|
|
406
411
|
if (endpointMatch) {
|
|
407
412
|
const endpoint = endpointMatch.groups;
|
|
408
|
-
const parsed = endpoint.options
|
|
413
|
+
const parsed = endpoint.options
|
|
414
|
+
? Object.fromEntries(new URLSearchParams(endpoint.options))
|
|
415
|
+
: {};
|
|
409
416
|
const parsedOrder = parseInt(parsed.order, 10);
|
|
410
417
|
let options = {
|
|
411
418
|
...parsed,
|
|
412
|
-
order: Number.isNaN(parsedOrder) ? 100 : parsedOrder
|
|
419
|
+
order: Number.isNaN(parsedOrder) ? 100 : parsedOrder,
|
|
413
420
|
};
|
|
414
421
|
|
|
415
422
|
try {
|
|
@@ -418,7 +425,7 @@ export class Gateway {
|
|
|
418
425
|
version: semver.coerce(entry.v).version,
|
|
419
426
|
options,
|
|
420
427
|
endpoint,
|
|
421
|
-
backend: [backendAddr]
|
|
428
|
+
backend: [backendAddr],
|
|
422
429
|
});
|
|
423
430
|
} catch (e) {
|
|
424
431
|
logger.error("gateway.buildRouter.error:", entry);
|
|
@@ -435,7 +442,10 @@ export class Gateway {
|
|
|
435
442
|
const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`;
|
|
436
443
|
|
|
437
444
|
for (let n = 0; n < index; n++) {
|
|
438
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
`${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url &&
|
|
447
|
+
endpoints[n].version === version
|
|
448
|
+
) {
|
|
439
449
|
endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
|
|
440
450
|
endpoints.splice(index, 1);
|
|
441
451
|
break;
|
|
@@ -509,5 +519,4 @@ export class Gateway {
|
|
|
509
519
|
this.#isActive = false;
|
|
510
520
|
clearInterval(this.#rebuildRouterInterval);
|
|
511
521
|
}
|
|
512
|
-
|
|
513
522
|
}
|
package/src/Logger.js
CHANGED
|
@@ -5,12 +5,16 @@ const LEVEL = LEVELS[process.env.GX_LOG_LEVEL] ?? LEVELS.info;
|
|
|
5
5
|
const FORMAT = process.env.GX_LOG_FORMAT === "json" ? "json" : "text";
|
|
6
6
|
|
|
7
7
|
const defaultLoggerOptions = {
|
|
8
|
-
timestamp: true
|
|
8
|
+
timestamp: true,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
function serialize(val) {
|
|
12
|
-
if (val instanceof Error) {
|
|
13
|
-
|
|
12
|
+
if (val instanceof Error) {
|
|
13
|
+
return val.stack || val.message;
|
|
14
|
+
}
|
|
15
|
+
if (typeof val === "object" && val !== null) {
|
|
16
|
+
return JSON.stringify(val);
|
|
17
|
+
}
|
|
14
18
|
return String(val);
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -22,7 +26,6 @@ function serialize(val) {
|
|
|
22
26
|
* The output format is controlled by `GX_LOG_FORMAT` (`text` | `json`, default `text`).
|
|
23
27
|
*/
|
|
24
28
|
export class Logger {
|
|
25
|
-
|
|
26
29
|
#options = defaultLoggerOptions;
|
|
27
30
|
#level = LEVEL;
|
|
28
31
|
#format = FORMAT;
|
|
@@ -46,10 +49,17 @@ export class Logger {
|
|
|
46
49
|
#log(level, ...args) {
|
|
47
50
|
const stream = level === "error" ? process.stderr : process.stdout;
|
|
48
51
|
if (this.#format === "json") {
|
|
49
|
-
stream.write(
|
|
52
|
+
stream.write(
|
|
53
|
+
JSON.stringify({ time: new Date().toISOString(), level, msg: args.map(serialize).join(" ") }) + "\n",
|
|
54
|
+
);
|
|
50
55
|
} else {
|
|
51
56
|
const ts = this.#options.timestamp ? new Date().toISOString() : undefined;
|
|
52
|
-
stream.write(
|
|
57
|
+
stream.write(
|
|
58
|
+
[ts, TAGS[level], ...args]
|
|
59
|
+
.filter(($) => $ !== undefined)
|
|
60
|
+
.map(serialize)
|
|
61
|
+
.join(" ") + "\n",
|
|
62
|
+
);
|
|
53
63
|
}
|
|
54
64
|
}
|
|
55
65
|
|
|
@@ -104,7 +114,6 @@ export class Logger {
|
|
|
104
114
|
setFormat(format) {
|
|
105
115
|
this.#format = format === "json" ? "json" : "text";
|
|
106
116
|
}
|
|
107
|
-
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
package/src/Registry.js
CHANGED
|
@@ -17,7 +17,6 @@ const GARBAGE_COLLECTOR_INTERVAL = 500;
|
|
|
17
17
|
* @extends EventEmitter
|
|
18
18
|
*/
|
|
19
19
|
class Registry extends EventEmitter {
|
|
20
|
-
|
|
21
20
|
#isActive = false;
|
|
22
21
|
#registry = {};
|
|
23
22
|
#byIdentifier = new Map();
|
|
@@ -25,15 +24,15 @@ class Registry extends EventEmitter {
|
|
|
25
24
|
constructor() {
|
|
26
25
|
super();
|
|
27
26
|
|
|
28
|
-
this.#start().catch(e => logger.error("registry.start:", e));
|
|
27
|
+
this.#start().catch((e) => logger.error("registry.start:", e));
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
async #start() {
|
|
32
31
|
this.#isActive = true;
|
|
33
32
|
await connection.waitUntilReady();
|
|
34
33
|
|
|
35
|
-
this.#beaconListener().catch(e => logger.error("registry.beaconListener:", e));
|
|
36
|
-
this.#garbageCollector().catch(e => logger.error("registry.garbageCollector:", e));
|
|
34
|
+
this.#beaconListener().catch((e) => logger.error("registry.beaconListener:", e));
|
|
35
|
+
this.#garbageCollector().catch((e) => logger.error("registry.garbageCollector:", e));
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/**
|
|
@@ -69,14 +68,17 @@ class Registry extends EventEmitter {
|
|
|
69
68
|
let firstFound = false;
|
|
70
69
|
const onFirstHealthy = () => {
|
|
71
70
|
if (!firstFound) {
|
|
72
|
-
firstFound = true;
|
|
71
|
+
firstFound = true;
|
|
72
|
+
resolveFirst();
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
// all checks run in parallel; background ones push into data.a,
|
|
77
77
|
// which is the same array reference spread into the registry entry below
|
|
78
78
|
// resolve promiseFirst when all checks are done so we don't wait on the timeout
|
|
79
|
-
Promise.all(allAddresses.map(a => this.#checkHealth(a, data.a, onFirstHealthy))).then(() =>
|
|
79
|
+
Promise.all(allAddresses.map((a) => this.#checkHealth(a, data.a, onFirstHealthy))).then(() =>
|
|
80
|
+
resolveFirst(),
|
|
81
|
+
);
|
|
80
82
|
|
|
81
83
|
try {
|
|
82
84
|
await withTimeout(promiseFirst, 5000);
|
|
@@ -87,7 +89,7 @@ class Registry extends EventEmitter {
|
|
|
87
89
|
|
|
88
90
|
this.#registry[data.i] = {
|
|
89
91
|
...data,
|
|
90
|
-
timeout: Date.now() + REGISTRY_ENTRY_TIMEOUT
|
|
92
|
+
timeout: Date.now() + REGISTRY_ENTRY_TIMEOUT,
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
const nameVersion = `${data.n}@${data.v}`;
|
|
@@ -124,7 +126,7 @@ class Registry extends EventEmitter {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// new service — dispatch registration without awaiting so the beacon loop stays unblocked
|
|
127
|
-
this.#registerService(data).catch(e => logger.error("registry.registerService:", e));
|
|
129
|
+
this.#registerService(data).catch((e) => logger.error("registry.registerService:", e));
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
@@ -144,7 +146,9 @@ class Registry extends EventEmitter {
|
|
|
144
146
|
const set = this.#byIdentifier.get(nameVersion);
|
|
145
147
|
if (set) {
|
|
146
148
|
set.delete(entry.i);
|
|
147
|
-
if (set.size === 0) {
|
|
149
|
+
if (set.size === 0) {
|
|
150
|
+
this.#byIdentifier.delete(nameVersion);
|
|
151
|
+
}
|
|
148
152
|
}
|
|
149
153
|
this.#byIdentifier.delete(entry.i);
|
|
150
154
|
|
|
@@ -174,8 +178,11 @@ class Registry extends EventEmitter {
|
|
|
174
178
|
*/
|
|
175
179
|
getEntriesForIdentifier(identifier) {
|
|
176
180
|
const ids = this.#byIdentifier.get(identifier);
|
|
177
|
-
if (!ids) {
|
|
178
|
-
|
|
181
|
+
if (!ids) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return [...ids].map((i) => this.#registry[i]).filter(Boolean);
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
/**
|
|
@@ -215,7 +222,6 @@ class Registry extends EventEmitter {
|
|
|
215
222
|
return `${matches[0].n}@${matches[0].v}`;
|
|
216
223
|
}
|
|
217
224
|
}
|
|
218
|
-
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
/**
|
|
@@ -223,4 +229,4 @@ class Registry extends EventEmitter {
|
|
|
223
229
|
*
|
|
224
230
|
* @type {Registry}
|
|
225
231
|
*/
|
|
226
|
-
export const registry = new Registry();
|
|
232
|
+
export const registry = new Registry();
|
package/src/Remote.js
CHANGED
|
@@ -10,12 +10,16 @@ import { Request } from "./Request.js";
|
|
|
10
10
|
* @param {...any} context - Optional context values forwarded to the remote method.
|
|
11
11
|
* @returns {Proxy} A proxy whose properties are async functions that call the remote service.
|
|
12
12
|
*/
|
|
13
|
-
export const Remote = (service, ...context) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
export const Remote = (service, ...context) =>
|
|
14
|
+
new Proxy(
|
|
15
|
+
{},
|
|
16
|
+
{
|
|
17
|
+
get: (_target, method) => {
|
|
18
|
+
if (typeof method !== "string" || method === "then") {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
+
return async (...args) => Request(service, method, args, context);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
package/src/Request.js
CHANGED
|
@@ -23,7 +23,7 @@ function waitForIdentifier(name, version, id, timeout) {
|
|
|
23
23
|
return Promise.resolve(found);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
return new Promise(resolve => {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
27
|
const timer = setTimeout(() => {
|
|
28
28
|
registry.removeListener("added", onAdded);
|
|
29
29
|
resolve(null);
|
|
@@ -109,11 +109,15 @@ export async function directRequest(identifier, method, args, context, options,
|
|
|
109
109
|
|
|
110
110
|
let httpResponse;
|
|
111
111
|
try {
|
|
112
|
-
const res = await fetchWithTimeout(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
const res = await fetchWithTimeout(
|
|
113
|
+
url,
|
|
114
|
+
{
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "content-type": contentType },
|
|
117
|
+
body: fetchBody,
|
|
118
|
+
},
|
|
119
|
+
options?.httpTimeout ?? 5000,
|
|
120
|
+
);
|
|
117
121
|
if (res.ok) {
|
|
118
122
|
httpResponse = _payloadKey
|
|
119
123
|
? JSON.parse(decryptPayload(Buffer.from(await res.arrayBuffer())))
|
|
@@ -124,7 +128,9 @@ export async function directRequest(identifier, method, args, context, options,
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
if (httpResponse) {
|
|
127
|
-
if (httpResponse.e) {
|
|
131
|
+
if (httpResponse.e) {
|
|
132
|
+
throw Error(`Request: remote error: ${httpResponse.e}`);
|
|
133
|
+
}
|
|
128
134
|
if (isStream(httpResponse.r)) {
|
|
129
135
|
return JSON.parse(await streamToString(httpResponse.r));
|
|
130
136
|
}
|
|
@@ -142,19 +148,29 @@ export async function directRequest(identifier, method, args, context, options,
|
|
|
142
148
|
m: method,
|
|
143
149
|
a: args,
|
|
144
150
|
c: context,
|
|
145
|
-
o: originator
|
|
151
|
+
o: originator,
|
|
146
152
|
},
|
|
147
|
-
options
|
|
153
|
+
options,
|
|
154
|
+
);
|
|
148
155
|
|
|
149
156
|
// automatically process streamed response
|
|
150
157
|
if (isStream(response)) {
|
|
151
158
|
response = JSON.parse(await streamToString(response));
|
|
152
159
|
}
|
|
153
160
|
} catch (e) {
|
|
154
|
-
logger.debug(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
logger.debug(
|
|
162
|
+
"GxError: directRequest",
|
|
163
|
+
inspect({
|
|
164
|
+
originator,
|
|
165
|
+
service: service ?? identifier,
|
|
166
|
+
method,
|
|
167
|
+
args,
|
|
168
|
+
context,
|
|
169
|
+
options,
|
|
170
|
+
error: e,
|
|
171
|
+
duration: Date.now() - requestBegin,
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
158
174
|
|
|
159
175
|
throw e;
|
|
160
176
|
}
|
|
@@ -199,4 +215,4 @@ export async function Subscribe(subject, callback) {
|
|
|
199
215
|
for await (const event of subscription) {
|
|
200
216
|
callback(event.data);
|
|
201
217
|
}
|
|
202
|
-
}
|
|
218
|
+
}
|
package/src/Service.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { connection } from "./Connection.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
picoid,
|
|
4
|
+
sleep,
|
|
5
|
+
hash,
|
|
6
|
+
getSecondsSinceMidnight,
|
|
7
|
+
OverlayObject,
|
|
8
|
+
GeonixVersion,
|
|
9
|
+
getFirstItemFromAsyncIterable,
|
|
10
|
+
getNetworkAddresses,
|
|
11
|
+
deepMerge,
|
|
12
|
+
} from "./Util.js";
|
|
3
13
|
import { webserver } from "./WebServer.js";
|
|
4
14
|
import { createConnection } from "net";
|
|
5
15
|
import { EOL } from "os";
|
|
@@ -18,7 +28,7 @@ const getInactivityTimeout = () => parseInt(process.env.GX_INACTIVITY_TIMEOUT) |
|
|
|
18
28
|
|
|
19
29
|
const protectedMethodNames = ["constructor", "onStart"];
|
|
20
30
|
const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|SUB|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
|
|
21
|
-
const isEndpointFilter = methodName => endpointMatcher.test(methodName);
|
|
31
|
+
const isEndpointFilter = (methodName) => endpointMatcher.test(methodName);
|
|
22
32
|
const ERROR_BEGIN_DELIMITER = "-".repeat(10);
|
|
23
33
|
const ERROR_END_DELIMITER = "-".repeat(40);
|
|
24
34
|
|
|
@@ -33,9 +43,9 @@ const defaultServiceOptions = {
|
|
|
33
43
|
middleware: {
|
|
34
44
|
json: true,
|
|
35
45
|
raw: true,
|
|
36
|
-
cookies: true
|
|
46
|
+
cookies: true,
|
|
37
47
|
},
|
|
38
|
-
fullBeacon: true
|
|
48
|
+
fullBeacon: true,
|
|
39
49
|
};
|
|
40
50
|
|
|
41
51
|
/**
|
|
@@ -43,7 +53,6 @@ const defaultServiceOptions = {
|
|
|
43
53
|
* the service on the NATS bus, expose HTTP endpoints, and begin sending beacons to the registry.
|
|
44
54
|
*/
|
|
45
55
|
export class Service {
|
|
46
|
-
|
|
47
56
|
/**
|
|
48
57
|
* Creates a new instance of the subclass and starts it on the NATS bus.
|
|
49
58
|
*
|
|
@@ -52,10 +61,10 @@ export class Service {
|
|
|
52
61
|
*/
|
|
53
62
|
static start(options = {}) {
|
|
54
63
|
const instance = new this();
|
|
55
|
-
instance.#start(options).catch(e => logger.error("gx.service.start:", e));
|
|
64
|
+
instance.#start(options).catch((e) => logger.error("gx.service.start:", e));
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
// ---------------------------------------------------------------------------------------------
|
|
67
|
+
// ---------------------------------------------------------------------------------------------
|
|
59
68
|
|
|
60
69
|
#isActive = false;
|
|
61
70
|
#me = {};
|
|
@@ -78,7 +87,10 @@ export class Service {
|
|
|
78
87
|
const lineMap = new Map();
|
|
79
88
|
for (const name of fields) {
|
|
80
89
|
const quoted = JSON.stringify(name);
|
|
81
|
-
lineMap.set(
|
|
90
|
+
lineMap.set(
|
|
91
|
+
name,
|
|
92
|
+
serviceSource.findIndex((line) => line.includes(quoted)),
|
|
93
|
+
);
|
|
82
94
|
}
|
|
83
95
|
fields.sort((a, b) => lineMap.get(a) - lineMap.get(b));
|
|
84
96
|
|
|
@@ -90,15 +102,21 @@ export class Service {
|
|
|
90
102
|
// name
|
|
91
103
|
n: options.name ?? this.constructor.name,
|
|
92
104
|
// version
|
|
93
|
-
v:
|
|
105
|
+
v:
|
|
106
|
+
process.env.GX_VERSION ||
|
|
107
|
+
process.env.VERSION ||
|
|
108
|
+
process.env.version ||
|
|
109
|
+
options?.version ||
|
|
110
|
+
this.version ||
|
|
111
|
+
`999.999.${getSecondsSinceMidnight()}`,
|
|
94
112
|
// methods
|
|
95
113
|
m: fields
|
|
96
|
-
.filter(methodName => !protectedMethodNames.includes(methodName))
|
|
97
|
-
.filter(methodName => !methodName.startsWith("$")),
|
|
114
|
+
.filter((methodName) => !protectedMethodNames.includes(methodName))
|
|
115
|
+
.filter((methodName) => !methodName.startsWith("$")),
|
|
98
116
|
// geonix version
|
|
99
117
|
gx: GeonixVersion,
|
|
100
118
|
// IP addresses
|
|
101
|
-
a: getNetworkAddresses().map(address => `${address}:${webserver.getPort()}`)
|
|
119
|
+
a: getNetworkAddresses().map((address) => `${address}:${webserver.getPort()}`),
|
|
102
120
|
};
|
|
103
121
|
|
|
104
122
|
// check if method takes context as first argument
|
|
@@ -106,21 +124,20 @@ export class Service {
|
|
|
106
124
|
const method = this[methodName];
|
|
107
125
|
this.#methodTakesContext.set(
|
|
108
126
|
method,
|
|
109
|
-
method
|
|
127
|
+
method
|
|
128
|
+
.toString()
|
|
129
|
+
?.match(/\((?<args>.*)\)/)
|
|
130
|
+
?.groups?.args.startsWith("$"),
|
|
110
131
|
);
|
|
111
132
|
}
|
|
112
133
|
|
|
113
|
-
this.#beacon()
|
|
114
|
-
.catch(e => logger.error("gx.beacon:", e));
|
|
134
|
+
this.#beacon().catch((e) => logger.error("gx.beacon:", e));
|
|
115
135
|
|
|
116
|
-
this.#callListener(`${this.#me.n}@${this.#me.v}`)
|
|
117
|
-
.catch(e => logger.error("gx.queueListener:", e));
|
|
136
|
+
this.#callListener(`${this.#me.n}@${this.#me.v}`).catch((e) => logger.error("gx.queueListener:", e));
|
|
118
137
|
|
|
119
|
-
this.#callListener(this.#me.i)
|
|
120
|
-
.catch(e => logger.error("gx.directListener:", e));
|
|
138
|
+
this.#callListener(this.#me.i).catch((e) => logger.error("gx.directListener:", e));
|
|
121
139
|
|
|
122
|
-
this.#webserver()
|
|
123
|
-
.catch(e => logger.error("gx.webserver:", e));
|
|
140
|
+
this.#webserver().catch((e) => logger.error("gx.webserver:", e));
|
|
124
141
|
|
|
125
142
|
logger.info("gx.service.start", this.#me.n, this.#me.v);
|
|
126
143
|
|
|
@@ -141,7 +158,7 @@ export class Service {
|
|
|
141
158
|
async #beacon() {
|
|
142
159
|
while (this.#isActive) {
|
|
143
160
|
const payload = this.#options.fullBeacon ? this.#me : { i: this.#me.i };
|
|
144
|
-
connection.publish("gx2.beacon", payload).catch(e => logger.warn("beacon.publish:", e));
|
|
161
|
+
connection.publish("gx2.beacon", payload).catch((e) => logger.warn("beacon.publish:", e));
|
|
145
162
|
await sleep(BEACON_INTERVAL);
|
|
146
163
|
}
|
|
147
164
|
}
|
|
@@ -168,11 +185,10 @@ export class Service {
|
|
|
168
185
|
|
|
169
186
|
/**
|
|
170
187
|
* Register local endpoints with express instance
|
|
171
|
-
* @returns
|
|
188
|
+
* @returns
|
|
172
189
|
*/
|
|
173
190
|
async #webserver() {
|
|
174
|
-
const endpoints = this.#me.m
|
|
175
|
-
.filter(isEndpointFilter);
|
|
191
|
+
const endpoints = this.#me.m.filter(isEndpointFilter);
|
|
176
192
|
|
|
177
193
|
const router = webserver.router();
|
|
178
194
|
|
|
@@ -181,7 +197,9 @@ export class Service {
|
|
|
181
197
|
router.post(`/!!_gx/rpc/${hash(this.#me.i)}`, raw, async (req, res) => {
|
|
182
198
|
const body = _payloadKey
|
|
183
199
|
? JSON.parse(decryptPayload(req.body))
|
|
184
|
-
:
|
|
200
|
+
: Buffer.isBuffer(req.body)
|
|
201
|
+
? JSON.parse(req.body.toString())
|
|
202
|
+
: req.body;
|
|
185
203
|
await this.#onCall(body, (result) => {
|
|
186
204
|
const payload = Buffer.from(JSON.stringify(result));
|
|
187
205
|
if (_payloadKey) {
|
|
@@ -214,7 +232,7 @@ export class Service {
|
|
|
214
232
|
let { verb, url: uri } = endpointMatcher.exec(endpoint)?.groups || {};
|
|
215
233
|
verb = verb.toLowerCase();
|
|
216
234
|
|
|
217
|
-
let handlers =
|
|
235
|
+
let handlers = Array.isArray(this[endpoint]) ? this[endpoint] : [this[endpoint]];
|
|
218
236
|
|
|
219
237
|
const handlersBefore = this.#options?.handlers?.before ?? [];
|
|
220
238
|
const handlersAfter = this.#options?.handlers?.after ?? [];
|
|
@@ -232,7 +250,14 @@ export class Service {
|
|
|
232
250
|
// handlersBefore run as route-scoped middleware before the upgrade;
|
|
233
251
|
// handlersAfter does not apply to WebSocket connections.
|
|
234
252
|
if (handlersBefore.length > 0) {
|
|
235
|
-
router.use(
|
|
253
|
+
router.use(
|
|
254
|
+
uri,
|
|
255
|
+
...handlersBefore.map(
|
|
256
|
+
(h) =>
|
|
257
|
+
(...args) =>
|
|
258
|
+
h.apply(this, args),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
236
261
|
}
|
|
237
262
|
router.ws(uri, this[endpoint].bind(this));
|
|
238
263
|
break;
|
|
@@ -258,14 +283,14 @@ export class Service {
|
|
|
258
283
|
handler(event.data);
|
|
259
284
|
}
|
|
260
285
|
};
|
|
261
|
-
processor().catch(e => logger.error("$sub.processor:", e));
|
|
286
|
+
processor().catch((e) => logger.error("$sub.processor:", e));
|
|
262
287
|
}
|
|
263
288
|
|
|
264
289
|
/**
|
|
265
290
|
* Handle individual call
|
|
266
|
-
* @param {Object} call
|
|
267
|
-
* @param {Function} respond
|
|
268
|
-
* @returns
|
|
291
|
+
* @param {Object} call
|
|
292
|
+
* @param {Function} respond
|
|
293
|
+
* @returns
|
|
269
294
|
*/
|
|
270
295
|
async #onCall(call, respond) {
|
|
271
296
|
const { m: methodName, a: args, c: context, o: caller } = call;
|
|
@@ -302,7 +327,7 @@ export class Service {
|
|
|
302
327
|
|
|
303
328
|
this.#connections.set(streamId, {
|
|
304
329
|
client,
|
|
305
|
-
sub: ingress
|
|
330
|
+
sub: ingress,
|
|
306
331
|
});
|
|
307
332
|
|
|
308
333
|
const cleanup = () => {
|
|
@@ -338,8 +363,11 @@ export class Service {
|
|
|
338
363
|
cleanup();
|
|
339
364
|
};
|
|
340
365
|
|
|
341
|
-
incomingLoop().catch(e => {
|
|
342
|
-
|
|
366
|
+
incomingLoop().catch((e) => {
|
|
367
|
+
logger.error("$createConnection.incomingLoop:", e);
|
|
368
|
+
cleanup();
|
|
369
|
+
});
|
|
370
|
+
controlLoop().catch((e) => logger.error("$createConnection.controlLoop:", e));
|
|
343
371
|
|
|
344
372
|
return true;
|
|
345
373
|
}
|
|
@@ -350,11 +378,11 @@ export class Service {
|
|
|
350
378
|
node: {
|
|
351
379
|
version: process.version,
|
|
352
380
|
platform: process.platform,
|
|
353
|
-
arch: process.arch
|
|
381
|
+
arch: process.arch,
|
|
354
382
|
},
|
|
355
383
|
mem: process.memoryUsage(),
|
|
356
384
|
rss: process.memoryUsage.rss(),
|
|
357
|
-
cpu: process.cpuUsage()
|
|
385
|
+
cpu: process.cpuUsage(),
|
|
358
386
|
};
|
|
359
387
|
}
|
|
360
388
|
|
|
@@ -369,5 +397,4 @@ export class Service {
|
|
|
369
397
|
$stop() {
|
|
370
398
|
this.#isActive = false;
|
|
371
399
|
}
|
|
372
|
-
|
|
373
|
-
}
|
|
400
|
+
}
|
package/src/Stream.js
CHANGED
|
@@ -52,7 +52,7 @@ export function Stream(data) {
|
|
|
52
52
|
|
|
53
53
|
const event = await Promise.race([
|
|
54
54
|
getFirstItemFromAsyncIterable(control),
|
|
55
|
-
new Promise(resolve => setTimeout(() => resolve(null), getStreamTimeout()))
|
|
55
|
+
new Promise((resolve) => setTimeout(() => resolve(null), getStreamTimeout())),
|
|
56
56
|
]);
|
|
57
57
|
|
|
58
58
|
if (!event) {
|
|
@@ -68,20 +68,22 @@ export function Stream(data) {
|
|
|
68
68
|
delete activeStreams[id];
|
|
69
69
|
|
|
70
70
|
// kickstart the stream
|
|
71
|
-
readable.on("data", chunk => connection.publishRaw(`gx2.stream.${id}.b`, chunk));
|
|
71
|
+
readable.on("data", (chunk) => connection.publishRaw(`gx2.stream.${id}.b`, chunk));
|
|
72
72
|
readable.on("close", () => {
|
|
73
73
|
connection.publishRaw(`gx2.stream.${id}.b`);
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
})().catch(e => logger.error("stream.nats.handler:", e));
|
|
76
|
+
})().catch((e) => logger.error("stream.nats.handler:", e));
|
|
77
77
|
|
|
78
78
|
const result = {
|
|
79
79
|
$: "stream",
|
|
80
|
-
id
|
|
80
|
+
id,
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
// get the port and addresses of the webserver
|
|
84
|
-
const addresses = webserver.getPort()
|
|
84
|
+
const addresses = webserver.getPort()
|
|
85
|
+
? getNetworkAddresses().map((address) => `${address}:${webserver.getPort()}`)
|
|
86
|
+
: undefined;
|
|
85
87
|
if (addresses) {
|
|
86
88
|
result.a = addresses;
|
|
87
89
|
}
|
|
@@ -151,7 +153,11 @@ export async function getReadable(object) {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
};
|
|
154
|
-
dataHandler().catch(e => {
|
|
156
|
+
dataHandler().catch((e) => {
|
|
157
|
+
logger.error("stream.dataHandler:", e);
|
|
158
|
+
subscription.unsubscribe();
|
|
159
|
+
readable.destroy();
|
|
160
|
+
});
|
|
155
161
|
|
|
156
162
|
// kickstart remote stream with a blank message
|
|
157
163
|
await connection.publishRaw(`gx2.stream.${object.id}.a`);
|
package/src/Util.js
CHANGED
|
@@ -14,22 +14,22 @@ import { tmpdir } from "os";
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Wait for {delay} ms
|
|
17
|
-
* @param {number} delay
|
|
18
|
-
* @returns
|
|
17
|
+
* @param {number} delay
|
|
18
|
+
* @returns
|
|
19
19
|
*/
|
|
20
|
-
export const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));
|
|
20
|
+
export const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Wait for next tick
|
|
24
|
-
*
|
|
25
|
-
* @returns
|
|
24
|
+
*
|
|
25
|
+
* @returns
|
|
26
26
|
*/
|
|
27
|
-
export const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve));
|
|
27
|
+
export const yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Parse nats:// URL
|
|
31
|
-
* @param {string} url
|
|
32
|
-
* @returns
|
|
31
|
+
* @param {string} url
|
|
32
|
+
* @returns
|
|
33
33
|
*/
|
|
34
34
|
export function parseURL(url) {
|
|
35
35
|
const parsed = new URL(url);
|
|
@@ -38,12 +38,12 @@ export function parseURL(url) {
|
|
|
38
38
|
servers: `${parsed.hostname}:${parsed.port || 4222}`,
|
|
39
39
|
user: parsed.password ? parsed.username : "",
|
|
40
40
|
pass: parsed.password,
|
|
41
|
-
token: parsed.username && !parsed.password ? parsed.username : undefined
|
|
41
|
+
token: parsed.username && !parsed.password ? parsed.username : undefined,
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
45
|
...basic,
|
|
46
|
-
...Object.fromEntries(parsed.searchParams)
|
|
46
|
+
...Object.fromEntries(parsed.searchParams),
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -51,7 +51,9 @@ const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
51
51
|
const LOG256_LOG62 = Math.log(256) / Math.log(62); // ≈ 1.3437
|
|
52
52
|
|
|
53
53
|
export function encodeBase62(buffer) {
|
|
54
|
-
if (buffer.length === 0) {
|
|
54
|
+
if (buffer.length === 0) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
55
57
|
const len = Math.ceil(buffer.length * LOG256_LOG62);
|
|
56
58
|
let n = BigInt("0x" + buffer.toString("hex"));
|
|
57
59
|
const chars = new Array(len);
|
|
@@ -73,17 +75,17 @@ export const picoid = (size = 16) => encodeBase62(randomBytes(size));
|
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Get SHA256 hash of a string or a buffer
|
|
76
|
-
* @param {string|Buffer} data
|
|
77
|
-
* @returns
|
|
78
|
+
* @param {string|Buffer} data
|
|
79
|
+
* @returns
|
|
78
80
|
*/
|
|
79
81
|
export const hash = (data) => createHash("sha256").update(data).digest("hex");
|
|
80
82
|
|
|
81
83
|
/**
|
|
82
84
|
* Create TCP or HTTP server at specified port
|
|
83
|
-
* @param {number} port
|
|
84
|
-
* @param {Object} pkg
|
|
85
|
-
* @param {Function} handler
|
|
86
|
-
* @returns
|
|
85
|
+
* @param {number} port
|
|
86
|
+
* @param {Object} pkg
|
|
87
|
+
* @param {Function} handler
|
|
88
|
+
* @returns
|
|
87
89
|
*/
|
|
88
90
|
export const createServerAtPort = (port, pkg, handler) =>
|
|
89
91
|
new Promise((resolve) => {
|
|
@@ -131,7 +133,7 @@ export const proxyHttp = (target, req, res) =>
|
|
|
131
133
|
const remoteTarget = `${target}${req.originalUrl}`;
|
|
132
134
|
const options = {
|
|
133
135
|
method: req.method,
|
|
134
|
-
headers: req.headers
|
|
136
|
+
headers: req.headers,
|
|
135
137
|
};
|
|
136
138
|
|
|
137
139
|
const protocol = req.protocol === "https" ? https : http;
|
|
@@ -151,11 +153,12 @@ export const proxyHttp = (target, req, res) =>
|
|
|
151
153
|
|
|
152
154
|
/**
|
|
153
155
|
* Create a object proxy that overlays overlay object
|
|
154
|
-
* @param {*} object
|
|
155
|
-
* @param {*} overlay
|
|
156
|
-
* @returns
|
|
156
|
+
* @param {*} object
|
|
157
|
+
* @param {*} overlay
|
|
158
|
+
* @returns
|
|
157
159
|
*/
|
|
158
|
-
export const OverlayObject = (object, overlay) =>
|
|
160
|
+
export const OverlayObject = (object, overlay) =>
|
|
161
|
+
new Proxy(object, { get: (t, p) => (overlay[p] !== undefined ? overlay[p] : t[p]) });
|
|
159
162
|
|
|
160
163
|
/**
|
|
161
164
|
* The version string of the currently installed Geonix package, read from `package.json` at
|
|
@@ -173,24 +176,25 @@ export const GeonixVersion = (() => {
|
|
|
173
176
|
|
|
174
177
|
/**
|
|
175
178
|
* Chunk a stream into smaller chunks
|
|
176
|
-
*
|
|
177
|
-
* @param {*} chunkSize
|
|
178
|
-
* @returns
|
|
179
|
+
*
|
|
180
|
+
* @param {*} chunkSize
|
|
181
|
+
* @returns
|
|
179
182
|
*/
|
|
180
|
-
export const StreamChunker = (chunkSize = 65536) =>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
done
|
|
192
|
-
|
|
193
|
-
}
|
|
183
|
+
export const StreamChunker = (chunkSize = 65536) =>
|
|
184
|
+
new Transform({
|
|
185
|
+
transform(chunk, _encoding, done) {
|
|
186
|
+
let offset = 0;
|
|
187
|
+
while (offset < chunk.length) {
|
|
188
|
+
const sliceSize = Math.min(chunkSize, chunk.length - offset);
|
|
189
|
+
this.push(chunk.slice(offset, offset + sliceSize));
|
|
190
|
+
offset += sliceSize;
|
|
191
|
+
}
|
|
192
|
+
done();
|
|
193
|
+
},
|
|
194
|
+
flush(done) {
|
|
195
|
+
done();
|
|
196
|
+
},
|
|
197
|
+
});
|
|
194
198
|
|
|
195
199
|
export async function getFirstItemFromAsyncIterable(asyncIterable) {
|
|
196
200
|
const iterator = asyncIterable[Symbol.asyncIterator]();
|
|
@@ -246,7 +250,7 @@ export async function parseMultipart(req, _options) {
|
|
|
246
250
|
const END_OF_HEADERS = Buffer.from("\r\n\r\n");
|
|
247
251
|
const options = {
|
|
248
252
|
useMemory: false,
|
|
249
|
-
..._options
|
|
253
|
+
..._options,
|
|
250
254
|
};
|
|
251
255
|
const parts = [];
|
|
252
256
|
let stream = req;
|
|
@@ -308,7 +312,7 @@ export async function parseMultipart(req, _options) {
|
|
|
308
312
|
headers: {},
|
|
309
313
|
bodyFile: options.useMemory ? undefined : bodyFile,
|
|
310
314
|
body: options.useMemory ? [] : createWriteStream(bodyFile, { flags: "wx" }),
|
|
311
|
-
size: 0
|
|
315
|
+
size: 0,
|
|
312
316
|
};
|
|
313
317
|
parts.push(activePart);
|
|
314
318
|
};
|
|
@@ -341,7 +345,9 @@ export async function parseMultipart(req, _options) {
|
|
|
341
345
|
break;
|
|
342
346
|
}
|
|
343
347
|
|
|
344
|
-
const isLastBoundary =
|
|
348
|
+
const isLastBoundary =
|
|
349
|
+
combined[boundaryIndex + boundary.length] === 45 &&
|
|
350
|
+
combined[boundaryIndex + boundary.length + 1] === 45;
|
|
345
351
|
|
|
346
352
|
if (boundaryIndex > 0) {
|
|
347
353
|
write(combined.subarray(0, boundaryIndex));
|
|
@@ -360,7 +366,8 @@ export async function parseMultipart(req, _options) {
|
|
|
360
366
|
}
|
|
361
367
|
|
|
362
368
|
activePart.headers = combined
|
|
363
|
-
.subarray(boundaryIndex + boundary.length + 2, endOfHeaders)
|
|
369
|
+
.subarray(boundaryIndex + boundary.length + 2, endOfHeaders)
|
|
370
|
+
.toString()
|
|
364
371
|
.split("\r\n")
|
|
365
372
|
.reduce((acc, val) => {
|
|
366
373
|
const [header, value] = val.split(": ");
|
|
@@ -404,7 +411,7 @@ export async function parseMultipart(req, _options) {
|
|
|
404
411
|
try {
|
|
405
412
|
await unlink(part.bodyFile);
|
|
406
413
|
} catch {
|
|
407
|
-
// ignore errors
|
|
414
|
+
// ignore errors
|
|
408
415
|
}
|
|
409
416
|
});
|
|
410
417
|
}
|
package/src/WebServer.js
CHANGED
|
@@ -50,7 +50,6 @@ export const ServeStatic = (root, options = {}) => {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
class WebServer {
|
|
53
|
-
|
|
54
53
|
#app = express();
|
|
55
54
|
#server;
|
|
56
55
|
#port;
|
|
@@ -60,7 +59,9 @@ class WebServer {
|
|
|
60
59
|
#routers = [];
|
|
61
60
|
|
|
62
61
|
async start() {
|
|
63
|
-
if (this.#started) {
|
|
62
|
+
if (this.#started) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
64
65
|
|
|
65
66
|
this.#started = true;
|
|
66
67
|
|
|
@@ -116,15 +117,14 @@ class WebServer {
|
|
|
116
117
|
router = await new Promise((resolve, reject) => {
|
|
117
118
|
currentResolve = resolve;
|
|
118
119
|
|
|
119
|
-
router(req, res,
|
|
120
|
-
(error
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
120
|
+
router(req, res, (error, _req, _res, _next) => {
|
|
121
|
+
if (error) {
|
|
122
|
+
return reject(error);
|
|
123
|
+
}
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
// go to next router
|
|
126
|
+
resolve(routers.shift());
|
|
127
|
+
});
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -138,7 +138,7 @@ class WebServer {
|
|
|
138
138
|
this.#app.all("*", (req, res) => {
|
|
139
139
|
res.status(404).send({
|
|
140
140
|
error: 404,
|
|
141
|
-
source: "ws"
|
|
141
|
+
source: "ws",
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
144
|
|
|
@@ -175,7 +175,6 @@ class WebServer {
|
|
|
175
175
|
logger.info("gx.webserver.stop");
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
-
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
export const webserver = new WebServer();
|