geonix 1.20.0 → 1.20.2
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 +36 -10
- package/package.json +2 -1
- package/src/Connection.js +35 -27
- package/src/Gateway.js +48 -42
- package/src/Logger.js +34 -0
- package/src/Registry.js +6 -3
- package/src/Request.js +18 -9
- package/src/RequestOptions.js +1 -0
- package/src/Service.js +70 -22
- package/src/Stream.js +63 -22
- package/src/Util.js +29 -8
- package/src/WebServer.js +37 -32
- package/test/gateway.js +5 -5
- 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.
|
|
@@ -77,21 +85,27 @@ declare class Registry {
|
|
|
77
85
|
export {};
|
|
78
86
|
export function Remote(service: string, ...context: (string | Stream | Object)[]): string | Stream | Object;
|
|
79
87
|
/**
|
|
88
|
+
* Send a request to a service
|
|
80
89
|
*
|
|
81
|
-
* @param {
|
|
82
|
-
* @param {
|
|
83
|
-
* @param {
|
|
90
|
+
* @param {string} service
|
|
91
|
+
* @param {string} method
|
|
92
|
+
* @param {any[]} args
|
|
93
|
+
* @param {any[]} context
|
|
94
|
+
* @param {object} options
|
|
84
95
|
* @returns
|
|
85
96
|
*/
|
|
86
|
-
export function Request(service:
|
|
97
|
+
export function Request(service: string, method: string, args: any[], context: any[], options: object): Promise<any>;
|
|
87
98
|
/**
|
|
99
|
+
* Send a request to a service
|
|
88
100
|
*
|
|
89
|
-
* @param {
|
|
90
|
-
* @param {
|
|
91
|
-
* @param {
|
|
101
|
+
* @param {string} identifier
|
|
102
|
+
* @param {string} method
|
|
103
|
+
* @param {any[]} args
|
|
104
|
+
* @param {any[]} context
|
|
105
|
+
* @param {object} options
|
|
92
106
|
* @returns
|
|
93
107
|
*/
|
|
94
|
-
export function directRequest(identifier:
|
|
108
|
+
export function directRequest(identifier: string, method: string, args: any[], context: any[], options: object, service: any): Promise<any>;
|
|
95
109
|
/**
|
|
96
110
|
* Publish payload to a subject
|
|
97
111
|
*
|
|
@@ -115,7 +129,6 @@ export function RequestOptions(options: any): RequestOptionsClass;
|
|
|
115
129
|
export class Service {
|
|
116
130
|
static serviceClasses: any[];
|
|
117
131
|
static start(options?: {}): void;
|
|
118
|
-
static static(): void;
|
|
119
132
|
connections: Map<any, any>;
|
|
120
133
|
$createConnection(streamId: any): Promise<boolean>;
|
|
121
134
|
$getEnv(): {
|
|
@@ -133,6 +146,17 @@ export class Service {
|
|
|
133
146
|
$getServiceInfo(): {};
|
|
134
147
|
#private;
|
|
135
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
|
+
};
|
|
136
160
|
/**
|
|
137
161
|
* Converts data to stream
|
|
138
162
|
*
|
|
@@ -146,12 +170,15 @@ export function getReadable(object: any): Promise<any>;
|
|
|
146
170
|
export function streamToBuffer(object: any): Promise<any>;
|
|
147
171
|
export function streamToString(object: any): Promise<any>;
|
|
148
172
|
export const stats: {};
|
|
173
|
+
export const activeStreams: {};
|
|
149
174
|
/**
|
|
150
175
|
* Parse nats:// URL
|
|
151
176
|
* @param {string} url
|
|
152
177
|
* @returns
|
|
153
178
|
*/
|
|
154
179
|
export function parseURL(url: string): any;
|
|
180
|
+
export function getFirstItemFromAsyncIterable(asyncIterable: any): Promise<any>;
|
|
181
|
+
export function getNetworkAddresses(): any[];
|
|
155
182
|
export function sleep(delay: number): Promise<any>;
|
|
156
183
|
export function picoid(size?: number): any;
|
|
157
184
|
export function hash(data: string | Buffer): any;
|
|
@@ -170,7 +197,6 @@ export function ServeStatic(root: any, options?: {}): any;
|
|
|
170
197
|
export const webserver: WebServer;
|
|
171
198
|
declare class WebServer {
|
|
172
199
|
start(): Promise<void>;
|
|
173
|
-
getAddresses(): any[];
|
|
174
200
|
getPort(): any;
|
|
175
201
|
router(): any;
|
|
176
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.2",
|
|
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>",
|
package/src/Connection.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { connect, JSONCodec } from "nats";
|
|
2
|
-
import { parseURL, picoid, sleep } from "./Util.js";
|
|
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";
|
|
5
6
|
|
|
6
7
|
// -------------------------------------------------------------------------------------------------
|
|
7
8
|
const CONNECTION_TIMEOUT = 10000;
|
|
@@ -9,6 +10,16 @@ const CONNECTION_TIMEOUT = 10000;
|
|
|
9
10
|
const defaultRequestOptions = {
|
|
10
11
|
timeout: 300000
|
|
11
12
|
};
|
|
13
|
+
|
|
14
|
+
const defaultConnectionOptions = {
|
|
15
|
+
timeout: CONNECTION_TIMEOUT,
|
|
16
|
+
reconnect: true,
|
|
17
|
+
debug: process.env.TRANSPORT_DEBUG === "true",
|
|
18
|
+
maxReconnectAttempts: 30,
|
|
19
|
+
pingInterval: 30000,
|
|
20
|
+
waitOnFirstConnect: true,
|
|
21
|
+
connections: 1
|
|
22
|
+
};
|
|
12
23
|
// -------------------------------------------------------------------------------------------------
|
|
13
24
|
|
|
14
25
|
/**
|
|
@@ -36,17 +47,8 @@ class Connection {
|
|
|
36
47
|
* @param {string} transport
|
|
37
48
|
*/
|
|
38
49
|
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
50
|
const options = {
|
|
49
|
-
...
|
|
51
|
+
...defaultConnectionOptions,
|
|
50
52
|
...parseURL(transport)
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -54,7 +56,7 @@ class Connection {
|
|
|
54
56
|
this.#connections.push(await connect(options));
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
logger.info("gx.connection.connected");
|
|
58
60
|
|
|
59
61
|
this.#ready = true;
|
|
60
62
|
|
|
@@ -64,7 +66,7 @@ class Connection {
|
|
|
64
66
|
|
|
65
67
|
async monitorStatus() {
|
|
66
68
|
for await (const event of this.#getConnection().status()) {
|
|
67
|
-
|
|
69
|
+
logger.info("gx.connection.status", JSON.stringify(event));
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -72,15 +74,17 @@ class Connection {
|
|
|
72
74
|
* Wait for the connection to be safely closed
|
|
73
75
|
*/
|
|
74
76
|
async waitUntilClosed() {
|
|
75
|
-
|
|
77
|
+
// wait for all connections to be closed
|
|
78
|
+
await Promise.all(this.#connections.map(connection => connection.closed()));
|
|
79
|
+
|
|
76
80
|
this.#closed = true;
|
|
77
|
-
|
|
81
|
+
logger.info("gx.connection.closed");
|
|
78
82
|
|
|
79
83
|
webserver.stop();
|
|
80
84
|
|
|
81
85
|
await sleep(5000);
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
logger.info("gx.terminate");
|
|
84
88
|
process.exit(1);
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -88,8 +92,9 @@ class Connection {
|
|
|
88
92
|
* Wait for the connection to be fully established
|
|
89
93
|
*/
|
|
90
94
|
async waitUntilReady() {
|
|
91
|
-
while (!this.#ready)
|
|
95
|
+
while (!this.#ready) {
|
|
92
96
|
await sleep(100);
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
/**
|
|
@@ -100,14 +105,16 @@ class Connection {
|
|
|
100
105
|
* @returns void
|
|
101
106
|
*/
|
|
102
107
|
async publish(subject, json) {
|
|
103
|
-
if (this.#draining || this.#closed)
|
|
108
|
+
if (this.#draining || this.#closed) {
|
|
104
109
|
return;
|
|
110
|
+
}
|
|
105
111
|
|
|
106
112
|
let payload = codec.encode(json);
|
|
107
113
|
|
|
108
114
|
// if payload is too big, convert it to Stream
|
|
109
|
-
if (payload.length > this.getMaxPayloadSize())
|
|
115
|
+
if (payload.length > this.getMaxPayloadSize()) {
|
|
110
116
|
payload = codec.encode(Stream(JSON.stringify(json)));
|
|
117
|
+
}
|
|
111
118
|
|
|
112
119
|
await this.#getConnection().publish(subject, payload);
|
|
113
120
|
}
|
|
@@ -120,8 +127,9 @@ class Connection {
|
|
|
120
127
|
* @returns void
|
|
121
128
|
*/
|
|
122
129
|
async publishRaw(subject, data) {
|
|
123
|
-
if (this.#draining || this.#closed)
|
|
130
|
+
if (this.#draining || this.#closed) {
|
|
124
131
|
return;
|
|
132
|
+
}
|
|
125
133
|
|
|
126
134
|
await this.#getConnection().publish(subject, data);
|
|
127
135
|
}
|
|
@@ -145,16 +153,17 @@ class Connection {
|
|
|
145
153
|
let payload = codec.encode({ $r: respondTo, p: json });
|
|
146
154
|
|
|
147
155
|
// if payload is too big, convert it to Stream
|
|
148
|
-
if (payload.length > this.getMaxPayloadSize())
|
|
156
|
+
if (payload.length > this.getMaxPayloadSize()) {
|
|
149
157
|
payload = codec.encode(Stream(JSON.stringify({ $r: respondTo, p: json })));
|
|
158
|
+
}
|
|
150
159
|
|
|
151
160
|
const nc = this.#getConnection();
|
|
152
161
|
let response = await nc.subscribe(respondTo, { max: 1, ...options });
|
|
153
162
|
|
|
154
163
|
await nc.publish(subject, payload);
|
|
155
164
|
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
const event = await getFirstItemFromAsyncIterable(response);
|
|
166
|
+
return codec.decode(event.data);
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
async subscribe(subject, options) {
|
|
@@ -181,9 +190,7 @@ class Connection {
|
|
|
181
190
|
async drain() {
|
|
182
191
|
this.#draining = true;
|
|
183
192
|
|
|
184
|
-
|
|
185
|
-
await connection.drain();
|
|
186
|
-
}
|
|
193
|
+
await Promise.all(this.#connections.map(connection => connection.drain()));
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
}
|
|
@@ -194,8 +201,9 @@ export const connection = new Connection();
|
|
|
194
201
|
connection.start();
|
|
195
202
|
|
|
196
203
|
export const stopConnection = () => {
|
|
197
|
-
if (!connection)
|
|
204
|
+
if (!connection) {
|
|
198
205
|
return;
|
|
206
|
+
}
|
|
199
207
|
|
|
200
208
|
connection.drain();
|
|
201
209
|
};
|
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,27 +157,29 @@ 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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
166
|
+
if (entry.a) {
|
|
167
|
+
for (let address of entry.a) {
|
|
168
|
+
try {
|
|
169
|
+
const ac = new AbortController();
|
|
170
|
+
const timeout = setTimeout(() => ac.abort(), 500);
|
|
171
|
+
const result = await (await fetch(`http://${address}${HEALTH_CHECK_ENDPOINT}`, { signal: ac.signal })).json();
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
if (result.status === "healthy" && result.services?.includes(entry.n)) {
|
|
174
|
+
backend = address;
|
|
175
|
+
logger.info(`${entry.n}@${entry.v} (#${entry.i}) directly reachable @ ${address}`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// silently ignore errors
|
|
176
180
|
}
|
|
177
|
-
} catch {
|
|
178
|
-
// silently ignore errors
|
|
179
181
|
}
|
|
182
|
+
}
|
|
180
183
|
|
|
181
184
|
let proxy;
|
|
182
185
|
if (!backend) {
|
|
@@ -188,7 +191,7 @@ export class Gateway {
|
|
|
188
191
|
try {
|
|
189
192
|
await this.#proxyHttpOverNats(streamId, entry, client);
|
|
190
193
|
} catch (e) {
|
|
191
|
-
|
|
194
|
+
logger.error("nats.proxy.error", e);
|
|
192
195
|
client.destroy();
|
|
193
196
|
}
|
|
194
197
|
}, 50000, 10000);
|
|
@@ -202,8 +205,9 @@ export class Gateway {
|
|
|
202
205
|
|
|
203
206
|
entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
|
|
204
207
|
|
|
205
|
-
if (entries.length > 0)
|
|
208
|
+
if (entries.length > 0) {
|
|
206
209
|
this.#rebuildRouter = true;
|
|
210
|
+
}
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
async #handleRemovedServices() {
|
|
@@ -213,7 +217,7 @@ export class Gateway {
|
|
|
213
217
|
for (let { entry, proxy } of localEntries) {
|
|
214
218
|
if (registryEntries[entry.i] === undefined) {
|
|
215
219
|
proxy?.server?.close();
|
|
216
|
-
|
|
220
|
+
logger.info(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`);
|
|
217
221
|
delete this.#registry[entry.i];
|
|
218
222
|
|
|
219
223
|
this.#rebuildRouter = true;
|
|
@@ -288,8 +292,7 @@ export class Gateway {
|
|
|
288
292
|
});
|
|
289
293
|
|
|
290
294
|
const dataLoop = async () => {
|
|
291
|
-
for await (const event of ingress)
|
|
292
|
-
client.write(event.data);
|
|
295
|
+
for await (const event of ingress) { client.write(event.data); }
|
|
293
296
|
};
|
|
294
297
|
|
|
295
298
|
dataLoop();
|
|
@@ -319,15 +322,16 @@ export class Gateway {
|
|
|
319
322
|
inbound.on("close", () => backend.close());
|
|
320
323
|
});
|
|
321
324
|
} catch (e) {
|
|
322
|
-
|
|
325
|
+
logger.error(e);
|
|
323
326
|
}
|
|
324
327
|
}
|
|
325
328
|
|
|
326
329
|
async #buildRouter() {
|
|
327
|
-
if (this.#buildRouterRunning)
|
|
330
|
+
if (this.#buildRouterRunning) {
|
|
328
331
|
return;
|
|
332
|
+
}
|
|
329
333
|
|
|
330
|
-
|
|
334
|
+
logger.debug("gateway.buildRouter");
|
|
331
335
|
|
|
332
336
|
this.#rebuildRouter = false;
|
|
333
337
|
this.#buildRouterRunning = true;
|
|
@@ -357,8 +361,8 @@ export class Gateway {
|
|
|
357
361
|
backend: [backend]
|
|
358
362
|
});
|
|
359
363
|
} catch (e) {
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
logger.error("gateway.buildRouter.error:", entry);
|
|
365
|
+
logger.error("gateway.buildRouter.error:", e);
|
|
362
366
|
}
|
|
363
367
|
}
|
|
364
368
|
}
|
|
@@ -370,12 +374,13 @@ export class Gateway {
|
|
|
370
374
|
const version = endpoints[index].version;
|
|
371
375
|
const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`;
|
|
372
376
|
|
|
373
|
-
for (let n = 0; n < index; n++)
|
|
377
|
+
for (let n = 0; n < index; n++) {
|
|
374
378
|
if (`${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url && endpoints[n].version === version) {
|
|
375
379
|
endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
|
|
376
380
|
endpoints.splice(index, 1);
|
|
377
381
|
break;
|
|
378
382
|
}
|
|
383
|
+
}
|
|
379
384
|
}
|
|
380
385
|
|
|
381
386
|
// sort endpoints by order, if there is one
|
|
@@ -395,21 +400,22 @@ export class Gateway {
|
|
|
395
400
|
router.ws(uri, (ws, req) => {
|
|
396
401
|
const url = req.originalUrl.replace(/\/\.websocket$/, "");
|
|
397
402
|
|
|
398
|
-
|
|
403
|
+
logger.debug("proxy.web.ws.to:", backend + req.originalUrl);
|
|
399
404
|
this.#proxyWebsocketOverNats(`ws://${backend}${url}`, ws, req);
|
|
400
405
|
});
|
|
401
|
-
} else
|
|
406
|
+
} else {
|
|
402
407
|
router[verb](uri, async (req, res, _next) => {
|
|
403
408
|
stats.proxied++;
|
|
404
409
|
backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
|
|
405
410
|
|
|
406
411
|
try {
|
|
407
|
-
|
|
412
|
+
logger.debug("proxy.web.to:", backend + req.originalUrl);
|
|
408
413
|
await proxyHttp(`http://${backend}`, req, res);
|
|
409
414
|
} catch (e) {
|
|
410
|
-
|
|
415
|
+
logger.error("proxy.web.error:", e);
|
|
411
416
|
}
|
|
412
417
|
});
|
|
418
|
+
}
|
|
413
419
|
}
|
|
414
420
|
|
|
415
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
|
@@ -44,8 +44,9 @@ class Registry extends EventEmitter {
|
|
|
44
44
|
timeout: Date.now() + REGISTRY_ENTRY_TIMEOUT
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
if (!exists)
|
|
47
|
+
if (!exists) {
|
|
48
48
|
this.emit("added", this.#registry[data.i]);
|
|
49
|
+
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -83,14 +84,16 @@ class Registry extends EventEmitter {
|
|
|
83
84
|
let matchVersion = version ? semver.satisfies(entry.v, version) : true;
|
|
84
85
|
let matchId = id ? entry.id === id : true;
|
|
85
86
|
|
|
86
|
-
if (matchName && matchVersion && matchId)
|
|
87
|
+
if (matchName && matchVersion && matchId) {
|
|
87
88
|
matches.push(entry);
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
if (matches.length > 0) {
|
|
91
93
|
// return instance id in case of id matching
|
|
92
|
-
if (id)
|
|
94
|
+
if (id) {
|
|
93
95
|
return matches[0].i;
|
|
96
|
+
}
|
|
94
97
|
|
|
95
98
|
// sort matched services in the registry by version
|
|
96
99
|
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
|
}
|