geonix 1.20.1 → 1.20.3
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/eslint.config.js +2 -1
- package/index.d.ts +21 -1
- package/package.json +4 -3
- package/src/Codec.js +16 -0
- package/src/Connection.js +37 -30
- package/src/Gateway.js +36 -32
- package/src/Logger.js +34 -0
- package/src/Registry.js +9 -5
- package/src/Request.js +18 -9
- package/src/RequestOptions.js +1 -0
- package/src/Service.js +74 -25
- package/src/Stream.js +65 -25
- package/src/Util.js +29 -8
- package/src/WebServer.js +37 -32
- package/test/context.js +35 -0
- package/test/gateway.js +5 -5
- package/test/simple.js +29 -0
- package/test/stream.js +28 -23
package/eslint.config.js
CHANGED
|
@@ -14,7 +14,8 @@ export default [
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
rules: {
|
|
17
|
-
|
|
17
|
+
"curly": "error",
|
|
18
|
+
"no-console": "error",
|
|
18
19
|
semi: "error",
|
|
19
20
|
"no-unused-vars": ["error", { argsIgnorePattern: "^_.*" }],
|
|
20
21
|
"no-constant-condition": ["error", { checkLoops: false }],
|
package/index.d.ts
CHANGED
|
@@ -65,6 +65,14 @@ export class Gateway {
|
|
|
65
65
|
constructor(opts: any);
|
|
66
66
|
#private;
|
|
67
67
|
}
|
|
68
|
+
export class Logger {
|
|
69
|
+
constructor(options: any);
|
|
70
|
+
info(...args: any[]): void;
|
|
71
|
+
error(...args: any[]): void;
|
|
72
|
+
debug(...args: any[]): void;
|
|
73
|
+
#private;
|
|
74
|
+
}
|
|
75
|
+
export const logger: Logger;
|
|
68
76
|
export const registry: Registry;
|
|
69
77
|
/**
|
|
70
78
|
* Registry maintains a local list of available services and their versions.
|
|
@@ -138,6 +146,17 @@ export class Service {
|
|
|
138
146
|
$getServiceInfo(): {};
|
|
139
147
|
#private;
|
|
140
148
|
}
|
|
149
|
+
export type ServiceOptions = {
|
|
150
|
+
middleware: {
|
|
151
|
+
json: boolean;
|
|
152
|
+
raw: boolean;
|
|
153
|
+
cookies: boolean;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Enable full beacon
|
|
157
|
+
*/
|
|
158
|
+
fullBeacon: boolean;
|
|
159
|
+
};
|
|
141
160
|
/**
|
|
142
161
|
* Converts data to stream
|
|
143
162
|
*
|
|
@@ -151,6 +170,7 @@ export function getReadable(object: any): Promise<any>;
|
|
|
151
170
|
export function streamToBuffer(object: any): Promise<any>;
|
|
152
171
|
export function streamToString(object: any): Promise<any>;
|
|
153
172
|
export const stats: {};
|
|
173
|
+
export const activeStreams: {};
|
|
154
174
|
/**
|
|
155
175
|
* Parse nats:// URL
|
|
156
176
|
* @param {string} url
|
|
@@ -158,6 +178,7 @@ export const stats: {};
|
|
|
158
178
|
*/
|
|
159
179
|
export function parseURL(url: string): any;
|
|
160
180
|
export function getFirstItemFromAsyncIterable(asyncIterable: any): Promise<any>;
|
|
181
|
+
export function getNetworkAddresses(): any[];
|
|
161
182
|
export function sleep(delay: number): Promise<any>;
|
|
162
183
|
export function picoid(size?: number): any;
|
|
163
184
|
export function hash(data: string | Buffer): any;
|
|
@@ -176,7 +197,6 @@ export function ServeStatic(root: any, options?: {}): any;
|
|
|
176
197
|
export const webserver: WebServer;
|
|
177
198
|
declare class WebServer {
|
|
178
199
|
start(): Promise<void>;
|
|
179
|
-
getAddresses(): any[];
|
|
180
200
|
getPort(): any;
|
|
181
201
|
router(): any;
|
|
182
202
|
waitUntilReady(): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geonix",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
12
|
"build": "npx tsc && cat build/* > index.d.ts && rm -rf build",
|
|
13
|
+
"lint": "npx eslint src",
|
|
13
14
|
"deploy": "npm run build && npm publish"
|
|
14
15
|
},
|
|
15
16
|
"author": "Davor Tarandek <dtarandek@tria.hr>",
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
"registry": "https://registry.npmjs.org/"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
|
-
"eslint": "^9.
|
|
32
|
+
"eslint": "^9.10.0",
|
|
32
33
|
"typescript": "^5.5.4"
|
|
33
34
|
}
|
|
34
|
-
}
|
|
35
|
+
}
|
package/src/Codec.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { JSONCodec } from "nats";
|
|
2
|
+
|
|
3
|
+
export const codec = JSONCodec();
|
|
4
|
+
|
|
5
|
+
export function encode(data) {
|
|
6
|
+
return codec.encode(data);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function decode(data) {
|
|
10
|
+
// check if data is json
|
|
11
|
+
if (Buffer.isBuffer(data) && data.readUInt8(0) === "{".charCodeAt(0)) {
|
|
12
|
+
return codec.decode(data);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
throw new Error("Codec.decode: unknown data type");
|
|
16
|
+
}
|
package/src/Connection.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { connect
|
|
2
|
-
import { parseURL, picoid, sleep } from "./Util.js";
|
|
1
|
+
import { connect } from "nats";
|
|
2
|
+
import { getFirstItemFromAsyncIterable, parseURL, picoid, sleep } from "./Util.js";
|
|
3
3
|
import { Stream } from "./Stream.js";
|
|
4
4
|
import { webserver } from "./WebServer.js";
|
|
5
|
+
import { logger } from "./Logger.js";
|
|
6
|
+
import { decode, encode } from "./Codec.js";
|
|
5
7
|
|
|
6
8
|
// -------------------------------------------------------------------------------------------------
|
|
7
9
|
const CONNECTION_TIMEOUT = 10000;
|
|
@@ -9,6 +11,16 @@ const CONNECTION_TIMEOUT = 10000;
|
|
|
9
11
|
const defaultRequestOptions = {
|
|
10
12
|
timeout: 300000
|
|
11
13
|
};
|
|
14
|
+
|
|
15
|
+
const defaultConnectionOptions = {
|
|
16
|
+
timeout: CONNECTION_TIMEOUT,
|
|
17
|
+
reconnect: true,
|
|
18
|
+
debug: process.env.TRANSPORT_DEBUG === "true",
|
|
19
|
+
maxReconnectAttempts: 30,
|
|
20
|
+
pingInterval: 30000,
|
|
21
|
+
waitOnFirstConnect: true,
|
|
22
|
+
connections: 1
|
|
23
|
+
};
|
|
12
24
|
// -------------------------------------------------------------------------------------------------
|
|
13
25
|
|
|
14
26
|
/**
|
|
@@ -36,17 +48,8 @@ class Connection {
|
|
|
36
48
|
* @param {string} transport
|
|
37
49
|
*/
|
|
38
50
|
async start(transport = process.env.TRANSPORT || "nats://localhost") {
|
|
39
|
-
const defaults = {
|
|
40
|
-
timeout: CONNECTION_TIMEOUT,
|
|
41
|
-
reconnect: true,
|
|
42
|
-
debug: process.env.TRANSPORT_DEBUG === "true",
|
|
43
|
-
maxReconnectAttempts: 30,
|
|
44
|
-
pingInterval: 30000,
|
|
45
|
-
waitOnFirstConnect: true,
|
|
46
|
-
connections: 1
|
|
47
|
-
};
|
|
48
51
|
const options = {
|
|
49
|
-
...
|
|
52
|
+
...defaultConnectionOptions,
|
|
50
53
|
...parseURL(transport)
|
|
51
54
|
};
|
|
52
55
|
|
|
@@ -54,7 +57,7 @@ class Connection {
|
|
|
54
57
|
this.#connections.push(await connect(options));
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
logger.info("gx.connection.connected");
|
|
58
61
|
|
|
59
62
|
this.#ready = true;
|
|
60
63
|
|
|
@@ -64,7 +67,7 @@ class Connection {
|
|
|
64
67
|
|
|
65
68
|
async monitorStatus() {
|
|
66
69
|
for await (const event of this.#getConnection().status()) {
|
|
67
|
-
|
|
70
|
+
logger.info("gx.connection.status", JSON.stringify(event));
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -76,13 +79,13 @@ class Connection {
|
|
|
76
79
|
await Promise.all(this.#connections.map(connection => connection.closed()));
|
|
77
80
|
|
|
78
81
|
this.#closed = true;
|
|
79
|
-
|
|
82
|
+
logger.info("gx.connection.closed");
|
|
80
83
|
|
|
81
84
|
webserver.stop();
|
|
82
85
|
|
|
83
86
|
await sleep(5000);
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
logger.info("gx.terminate");
|
|
86
89
|
process.exit(1);
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -90,8 +93,9 @@ class Connection {
|
|
|
90
93
|
* Wait for the connection to be fully established
|
|
91
94
|
*/
|
|
92
95
|
async waitUntilReady() {
|
|
93
|
-
while (!this.#ready)
|
|
96
|
+
while (!this.#ready) {
|
|
94
97
|
await sleep(100);
|
|
98
|
+
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
/**
|
|
@@ -102,14 +106,16 @@ class Connection {
|
|
|
102
106
|
* @returns void
|
|
103
107
|
*/
|
|
104
108
|
async publish(subject, json) {
|
|
105
|
-
if (this.#draining || this.#closed)
|
|
109
|
+
if (this.#draining || this.#closed) {
|
|
106
110
|
return;
|
|
111
|
+
}
|
|
107
112
|
|
|
108
|
-
let payload =
|
|
113
|
+
let payload = encode(json);
|
|
109
114
|
|
|
110
115
|
// if payload is too big, convert it to Stream
|
|
111
|
-
if (payload.length > this.getMaxPayloadSize())
|
|
112
|
-
payload =
|
|
116
|
+
if (payload.length > this.getMaxPayloadSize()) {
|
|
117
|
+
payload = encode(Stream(JSON.stringify(json)));
|
|
118
|
+
}
|
|
113
119
|
|
|
114
120
|
await this.#getConnection().publish(subject, payload);
|
|
115
121
|
}
|
|
@@ -122,8 +128,9 @@ class Connection {
|
|
|
122
128
|
* @returns void
|
|
123
129
|
*/
|
|
124
130
|
async publishRaw(subject, data) {
|
|
125
|
-
if (this.#draining || this.#closed)
|
|
131
|
+
if (this.#draining || this.#closed) {
|
|
126
132
|
return;
|
|
133
|
+
}
|
|
127
134
|
|
|
128
135
|
await this.#getConnection().publish(subject, data);
|
|
129
136
|
}
|
|
@@ -144,19 +151,20 @@ class Connection {
|
|
|
144
151
|
|
|
145
152
|
const respondTo = `gx2.r.${picoid(16)}`;
|
|
146
153
|
|
|
147
|
-
let payload =
|
|
154
|
+
let payload = encode({ $r: respondTo, p: json });
|
|
148
155
|
|
|
149
156
|
// if payload is too big, convert it to Stream
|
|
150
|
-
if (payload.length > this.getMaxPayloadSize())
|
|
151
|
-
payload =
|
|
157
|
+
if (payload.length > this.getMaxPayloadSize()) {
|
|
158
|
+
payload = encode(Stream(JSON.stringify({ $r: respondTo, p: json })));
|
|
159
|
+
}
|
|
152
160
|
|
|
153
161
|
const nc = this.#getConnection();
|
|
154
162
|
let response = await nc.subscribe(respondTo, { max: 1, ...options });
|
|
155
163
|
|
|
156
164
|
await nc.publish(subject, payload);
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
const event = await getFirstItemFromAsyncIterable(response);
|
|
167
|
+
return decode(event.data);
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
async subscribe(subject, options) {
|
|
@@ -188,14 +196,13 @@ class Connection {
|
|
|
188
196
|
|
|
189
197
|
}
|
|
190
198
|
|
|
191
|
-
export const codec = JSONCodec();
|
|
192
|
-
|
|
193
199
|
export const connection = new Connection();
|
|
194
200
|
connection.start();
|
|
195
201
|
|
|
196
202
|
export const stopConnection = () => {
|
|
197
|
-
if (!connection)
|
|
203
|
+
if (!connection) {
|
|
198
204
|
return;
|
|
205
|
+
}
|
|
199
206
|
|
|
200
207
|
connection.drain();
|
|
201
208
|
};
|
package/src/Gateway.js
CHANGED
|
@@ -8,14 +8,15 @@ import expressWs from "express-ws";
|
|
|
8
8
|
import querystring from "querystring";
|
|
9
9
|
import semver from "semver";
|
|
10
10
|
import { WebSocket } from "ws";
|
|
11
|
+
import { logger } from "./Logger.js";
|
|
11
12
|
|
|
12
13
|
const raw = express.raw({ limit: "100mb" });
|
|
13
14
|
|
|
14
15
|
const DEBUG_ENDPOINT = "/lZ6jD2eC3iP0zB3jJ1yJ9pM8gG3yI3vS";
|
|
15
16
|
const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const requestLogger = (req, res, next) => {
|
|
19
|
+
logger.info(`HTTP ${req.method} ${req.url}`);
|
|
19
20
|
|
|
20
21
|
next();
|
|
21
22
|
};
|
|
@@ -63,10 +64,10 @@ export class Gateway {
|
|
|
63
64
|
this.#port = process.env.PORT || port;
|
|
64
65
|
this.#api.listen(this.#port);
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
logger.debug(`geonix.gateway: listening on http://0.0.0.0:${this.#port}`);
|
|
67
68
|
|
|
68
69
|
// logging
|
|
69
|
-
this.#api.use(
|
|
70
|
+
this.#api.use(requestLogger);
|
|
70
71
|
|
|
71
72
|
// cors
|
|
72
73
|
this.#api.use((req, res, next) => {
|
|
@@ -86,8 +87,9 @@ export class Gateway {
|
|
|
86
87
|
});
|
|
87
88
|
|
|
88
89
|
// debug router (only available in non-production environments)
|
|
89
|
-
if (process.env.NODE_ENV !== "production")
|
|
90
|
+
if (process.env.NODE_ENV !== "production") {
|
|
90
91
|
this.#api.use(DEBUG_ENDPOINT, this.#debugRouter());
|
|
92
|
+
}
|
|
91
93
|
|
|
92
94
|
this.#api.use((req, res, next) => {
|
|
93
95
|
if (this.#opts.beforeRequest) {
|
|
@@ -100,10 +102,8 @@ export class Gateway {
|
|
|
100
102
|
this.#api.use(raw, (req, res, next) => {
|
|
101
103
|
stats.requests++;
|
|
102
104
|
|
|
103
|
-
if (this.#router)
|
|
104
|
-
|
|
105
|
-
else
|
|
106
|
-
next();
|
|
105
|
+
if (this.#router) { this.#router(req, res, next); }
|
|
106
|
+
else { next(); }
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
this.#api.use((req, res, next) => {
|
|
@@ -126,8 +126,9 @@ export class Gateway {
|
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
setInterval(() => {
|
|
129
|
-
if (this.#rebuildRouter)
|
|
129
|
+
if (this.#rebuildRouter) {
|
|
130
130
|
this.#buildRouter();
|
|
131
|
+
}
|
|
131
132
|
}, 1000);
|
|
132
133
|
|
|
133
134
|
while (true) {
|
|
@@ -143,12 +144,12 @@ export class Gateway {
|
|
|
143
144
|
break;
|
|
144
145
|
}
|
|
145
146
|
} catch (e) {
|
|
146
|
-
|
|
147
|
+
logger.error(e);
|
|
147
148
|
}
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
// terminate process
|
|
151
|
-
|
|
152
|
+
logger.debug("geonix.gateway: stopped");
|
|
152
153
|
process.exit(0);
|
|
153
154
|
}
|
|
154
155
|
|
|
@@ -156,15 +157,14 @@ export class Gateway {
|
|
|
156
157
|
let entries = Object.values(registry.getEntries());
|
|
157
158
|
|
|
158
159
|
const processEntry = async (entry) => {
|
|
159
|
-
if (this.#registry[entry.i] !== undefined)
|
|
160
|
-
return false;
|
|
160
|
+
if (this.#registry[entry.i] !== undefined) { return false; }
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
logger.info(`gateway.onServiceAdded: ${entry.n}@${entry.v} (#${entry.i})`);
|
|
163
163
|
|
|
164
164
|
// figure out if endpoints is reachable via direct http call
|
|
165
165
|
let backend;
|
|
166
166
|
if (entry.a) {
|
|
167
|
-
for (let address of entry.a)
|
|
167
|
+
for (let address of entry.a) {
|
|
168
168
|
try {
|
|
169
169
|
const ac = new AbortController();
|
|
170
170
|
const timeout = setTimeout(() => ac.abort(), 500);
|
|
@@ -172,12 +172,13 @@ export class Gateway {
|
|
|
172
172
|
clearTimeout(timeout);
|
|
173
173
|
if (result.status === "healthy" && result.services?.includes(entry.n)) {
|
|
174
174
|
backend = address;
|
|
175
|
-
|
|
175
|
+
logger.info(`${entry.n}@${entry.v} (#${entry.i}) directly reachable @ ${address}`);
|
|
176
176
|
break;
|
|
177
177
|
}
|
|
178
178
|
} catch {
|
|
179
179
|
// silently ignore errors
|
|
180
180
|
}
|
|
181
|
+
}
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
let proxy;
|
|
@@ -190,7 +191,7 @@ export class Gateway {
|
|
|
190
191
|
try {
|
|
191
192
|
await this.#proxyHttpOverNats(streamId, entry, client);
|
|
192
193
|
} catch (e) {
|
|
193
|
-
|
|
194
|
+
logger.error("nats.proxy.error", e);
|
|
194
195
|
client.destroy();
|
|
195
196
|
}
|
|
196
197
|
}, 50000, 10000);
|
|
@@ -204,8 +205,9 @@ export class Gateway {
|
|
|
204
205
|
|
|
205
206
|
entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
|
|
206
207
|
|
|
207
|
-
if (entries.length > 0)
|
|
208
|
+
if (entries.length > 0) {
|
|
208
209
|
this.#rebuildRouter = true;
|
|
210
|
+
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
async #handleRemovedServices() {
|
|
@@ -215,7 +217,7 @@ export class Gateway {
|
|
|
215
217
|
for (let { entry, proxy } of localEntries) {
|
|
216
218
|
if (registryEntries[entry.i] === undefined) {
|
|
217
219
|
proxy?.server?.close();
|
|
218
|
-
|
|
220
|
+
logger.info(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`);
|
|
219
221
|
delete this.#registry[entry.i];
|
|
220
222
|
|
|
221
223
|
this.#rebuildRouter = true;
|
|
@@ -290,8 +292,7 @@ export class Gateway {
|
|
|
290
292
|
});
|
|
291
293
|
|
|
292
294
|
const dataLoop = async () => {
|
|
293
|
-
for await (const event of ingress)
|
|
294
|
-
client.write(event.data);
|
|
295
|
+
for await (const event of ingress) { client.write(event.data); }
|
|
295
296
|
};
|
|
296
297
|
|
|
297
298
|
dataLoop();
|
|
@@ -321,15 +322,16 @@ export class Gateway {
|
|
|
321
322
|
inbound.on("close", () => backend.close());
|
|
322
323
|
});
|
|
323
324
|
} catch (e) {
|
|
324
|
-
|
|
325
|
+
logger.error(e);
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
async #buildRouter() {
|
|
329
|
-
if (this.#buildRouterRunning)
|
|
330
|
+
if (this.#buildRouterRunning) {
|
|
330
331
|
return;
|
|
332
|
+
}
|
|
331
333
|
|
|
332
|
-
|
|
334
|
+
logger.debug("gateway.buildRouter");
|
|
333
335
|
|
|
334
336
|
this.#rebuildRouter = false;
|
|
335
337
|
this.#buildRouterRunning = true;
|
|
@@ -359,8 +361,8 @@ export class Gateway {
|
|
|
359
361
|
backend: [backend]
|
|
360
362
|
});
|
|
361
363
|
} catch (e) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
+
logger.error("gateway.buildRouter.error:", entry);
|
|
365
|
+
logger.error("gateway.buildRouter.error:", e);
|
|
364
366
|
}
|
|
365
367
|
}
|
|
366
368
|
}
|
|
@@ -372,12 +374,13 @@ export class Gateway {
|
|
|
372
374
|
const version = endpoints[index].version;
|
|
373
375
|
const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`;
|
|
374
376
|
|
|
375
|
-
for (let n = 0; n < index; n++)
|
|
377
|
+
for (let n = 0; n < index; n++) {
|
|
376
378
|
if (`${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url && endpoints[n].version === version) {
|
|
377
379
|
endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
|
|
378
380
|
endpoints.splice(index, 1);
|
|
379
381
|
break;
|
|
380
382
|
}
|
|
383
|
+
}
|
|
381
384
|
}
|
|
382
385
|
|
|
383
386
|
// sort endpoints by order, if there is one
|
|
@@ -397,21 +400,22 @@ export class Gateway {
|
|
|
397
400
|
router.ws(uri, (ws, req) => {
|
|
398
401
|
const url = req.originalUrl.replace(/\/\.websocket$/, "");
|
|
399
402
|
|
|
400
|
-
|
|
403
|
+
logger.debug("proxy.web.ws.to:", backend + req.originalUrl);
|
|
401
404
|
this.#proxyWebsocketOverNats(`ws://${backend}${url}`, ws, req);
|
|
402
405
|
});
|
|
403
|
-
} else
|
|
406
|
+
} else {
|
|
404
407
|
router[verb](uri, async (req, res, _next) => {
|
|
405
408
|
stats.proxied++;
|
|
406
409
|
backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
|
|
407
410
|
|
|
408
411
|
try {
|
|
409
|
-
|
|
412
|
+
logger.debug("proxy.web.to:", backend + req.originalUrl);
|
|
410
413
|
await proxyHttp(`http://${backend}`, req, res);
|
|
411
414
|
} catch (e) {
|
|
412
|
-
|
|
415
|
+
logger.error("proxy.web.error:", e);
|
|
413
416
|
}
|
|
414
417
|
});
|
|
418
|
+
}
|
|
415
419
|
}
|
|
416
420
|
|
|
417
421
|
this.#router = router;
|
package/src/Logger.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const defaultLoggerOptions = {
|
|
2
|
+
timestamp: true
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export class Logger {
|
|
6
|
+
|
|
7
|
+
#options = defaultLoggerOptions;
|
|
8
|
+
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.#options = { ...this.#options, ...options };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#log(...args) {
|
|
14
|
+
const ts = this.#options.timestamp ? new Date().toISOString() : undefined;
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.log(...[ts, ...args].filter($ => $));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
info(...args) {
|
|
21
|
+
this.#log("INF", ...args);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
error(...args) {
|
|
25
|
+
this.#log("ERR", ...args);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
debug(...args) {
|
|
29
|
+
this.#log("DBG", ...args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const logger = new Logger();
|
package/src/Registry.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { connection
|
|
1
|
+
import { connection } from "./Connection.js";
|
|
2
2
|
import { sleep } from "./Util.js";
|
|
3
3
|
import semver from "semver";
|
|
4
4
|
import EventEmitter from "events";
|
|
5
5
|
import { directRequest } from "./Request.js";
|
|
6
|
+
import { decode } from "./Codec.js";
|
|
6
7
|
|
|
7
8
|
const REGISTRY_ENTRY_TIMEOUT = 5000;
|
|
8
9
|
|
|
@@ -30,7 +31,7 @@ class Registry extends EventEmitter {
|
|
|
30
31
|
const subscription = await connection.subscribe("gx2.beacon");
|
|
31
32
|
|
|
32
33
|
for await (const event of subscription) {
|
|
33
|
-
let data =
|
|
34
|
+
let data = decode(event.data);
|
|
34
35
|
|
|
35
36
|
const exists = this.#registry[data.i] !== undefined;
|
|
36
37
|
|
|
@@ -44,8 +45,9 @@ class Registry extends EventEmitter {
|
|
|
44
45
|
timeout: Date.now() + REGISTRY_ENTRY_TIMEOUT
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
if (!exists)
|
|
48
|
+
if (!exists) {
|
|
48
49
|
this.emit("added", this.#registry[data.i]);
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -83,14 +85,16 @@ class Registry extends EventEmitter {
|
|
|
83
85
|
let matchVersion = version ? semver.satisfies(entry.v, version) : true;
|
|
84
86
|
let matchId = id ? entry.id === id : true;
|
|
85
87
|
|
|
86
|
-
if (matchName && matchVersion && matchId)
|
|
88
|
+
if (matchName && matchVersion && matchId) {
|
|
87
89
|
matches.push(entry);
|
|
90
|
+
}
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
if (matches.length > 0) {
|
|
91
94
|
// return instance id in case of id matching
|
|
92
|
-
if (id)
|
|
95
|
+
if (id) {
|
|
93
96
|
return matches[0].i;
|
|
97
|
+
}
|
|
94
98
|
|
|
95
99
|
// sort matched services in the registry by version
|
|
96
100
|
matches.sort((a, b) => semver.rcompare(a.v, b.v));
|
package/src/Request.js
CHANGED
|
@@ -5,6 +5,7 @@ import { hash, sleep } from "./Util.js";
|
|
|
5
5
|
import { RequestOptionsClass } from "./RequestOptions.js";
|
|
6
6
|
import { isStream, streamToString } from "./Stream.js";
|
|
7
7
|
import { inspect } from "node:util";
|
|
8
|
+
import { logger } from "./Logger.js";
|
|
8
9
|
|
|
9
10
|
const REGISTRY_TIMEOUT = 300000;
|
|
10
11
|
|
|
@@ -44,8 +45,9 @@ function getOriginator() {
|
|
|
44
45
|
for (const item of stack) {
|
|
45
46
|
const typeName = item.getTypeName();
|
|
46
47
|
|
|
47
|
-
if (Service.serviceClasses.includes(typeName))
|
|
48
|
+
if (Service.serviceClasses.includes(typeName)) {
|
|
48
49
|
return `${typeName}.${item.getMethodName()}`;
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -64,8 +66,9 @@ export async function Request(service, method, args, context, options) {
|
|
|
64
66
|
const { name, version, id } = match.groups;
|
|
65
67
|
|
|
66
68
|
// allow passing RequestOptions as first arg
|
|
67
|
-
if (args?.length > 0 && args[0] instanceof RequestOptionsClass)
|
|
69
|
+
if (args?.length > 0 && args[0] instanceof RequestOptionsClass) {
|
|
68
70
|
options = (args.shift())?.options;
|
|
71
|
+
}
|
|
69
72
|
|
|
70
73
|
let identifier = null;
|
|
71
74
|
|
|
@@ -75,8 +78,9 @@ export async function Request(service, method, args, context, options) {
|
|
|
75
78
|
let retries = Math.floor(registryTimeout / delay);
|
|
76
79
|
while (identifier == null && retries-- > 0) {
|
|
77
80
|
identifier = registry.getIdentifier(name, version, id);
|
|
78
|
-
if (!identifier)
|
|
81
|
+
if (!identifier) {
|
|
79
82
|
await sleep(delay);
|
|
83
|
+
}
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
return directRequest(identifier, method, args, context, options, service);
|
|
@@ -108,10 +112,11 @@ export async function directRequest(identifier, method, args, context, options,
|
|
|
108
112
|
options);
|
|
109
113
|
|
|
110
114
|
// automatically process streamed response
|
|
111
|
-
if (isStream(response))
|
|
115
|
+
if (isStream(response)) {
|
|
112
116
|
response = JSON.parse(await streamToString(response));
|
|
117
|
+
}
|
|
113
118
|
} catch (e) {
|
|
114
|
-
|
|
119
|
+
logger.debug("GxError: directRequest", inspect({
|
|
115
120
|
originator, service: service ?? identifier, method, args, context, options,
|
|
116
121
|
error: e, duration: Date.now() - requestBegin
|
|
117
122
|
}));
|
|
@@ -119,12 +124,14 @@ export async function directRequest(identifier, method, args, context, options,
|
|
|
119
124
|
throw e;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
if (!response)
|
|
127
|
+
if (!response) {
|
|
123
128
|
throw Error("Request: invalid response");
|
|
129
|
+
}
|
|
124
130
|
|
|
125
131
|
// got error?
|
|
126
|
-
if (response.e)
|
|
132
|
+
if (response.e) {
|
|
127
133
|
throw Error(`Request: remote error: ${response.e}`);
|
|
134
|
+
}
|
|
128
135
|
|
|
129
136
|
return response.r;
|
|
130
137
|
}
|
|
@@ -147,10 +154,12 @@ export async function Publish(subject, payload) {
|
|
|
147
154
|
* @returns
|
|
148
155
|
*/
|
|
149
156
|
export async function Subscribe(subject, callback) {
|
|
150
|
-
if (typeof callback !== "function")
|
|
157
|
+
if (typeof callback !== "function") {
|
|
151
158
|
return;
|
|
159
|
+
}
|
|
152
160
|
|
|
153
161
|
const subscription = await connection.subscribe(`gx.sub.${subject}`);
|
|
154
|
-
for await (const event of subscription)
|
|
162
|
+
for await (const event of subscription) {
|
|
155
163
|
callback(event.data);
|
|
164
|
+
}
|
|
156
165
|
}
|