geonix 1.20.1 → 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 +21 -1
- package/package.json +2 -1
- package/src/Connection.js +31 -23
- package/src/Gateway.js +36 -32
- 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.
|
|
@@ -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.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
|
|
|
@@ -76,13 +78,13 @@ class Connection {
|
|
|
76
78
|
await Promise.all(this.#connections.map(connection => connection.closed()));
|
|
77
79
|
|
|
78
80
|
this.#closed = true;
|
|
79
|
-
|
|
81
|
+
logger.info("gx.connection.closed");
|
|
80
82
|
|
|
81
83
|
webserver.stop();
|
|
82
84
|
|
|
83
85
|
await sleep(5000);
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
logger.info("gx.terminate");
|
|
86
88
|
process.exit(1);
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -90,8 +92,9 @@ class Connection {
|
|
|
90
92
|
* Wait for the connection to be fully established
|
|
91
93
|
*/
|
|
92
94
|
async waitUntilReady() {
|
|
93
|
-
while (!this.#ready)
|
|
95
|
+
while (!this.#ready) {
|
|
94
96
|
await sleep(100);
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
/**
|
|
@@ -102,14 +105,16 @@ class Connection {
|
|
|
102
105
|
* @returns void
|
|
103
106
|
*/
|
|
104
107
|
async publish(subject, json) {
|
|
105
|
-
if (this.#draining || this.#closed)
|
|
108
|
+
if (this.#draining || this.#closed) {
|
|
106
109
|
return;
|
|
110
|
+
}
|
|
107
111
|
|
|
108
112
|
let payload = codec.encode(json);
|
|
109
113
|
|
|
110
114
|
// if payload is too big, convert it to Stream
|
|
111
|
-
if (payload.length > this.getMaxPayloadSize())
|
|
115
|
+
if (payload.length > this.getMaxPayloadSize()) {
|
|
112
116
|
payload = codec.encode(Stream(JSON.stringify(json)));
|
|
117
|
+
}
|
|
113
118
|
|
|
114
119
|
await this.#getConnection().publish(subject, payload);
|
|
115
120
|
}
|
|
@@ -122,8 +127,9 @@ class Connection {
|
|
|
122
127
|
* @returns void
|
|
123
128
|
*/
|
|
124
129
|
async publishRaw(subject, data) {
|
|
125
|
-
if (this.#draining || this.#closed)
|
|
130
|
+
if (this.#draining || this.#closed) {
|
|
126
131
|
return;
|
|
132
|
+
}
|
|
127
133
|
|
|
128
134
|
await this.#getConnection().publish(subject, data);
|
|
129
135
|
}
|
|
@@ -147,16 +153,17 @@ class Connection {
|
|
|
147
153
|
let payload = codec.encode({ $r: respondTo, p: json });
|
|
148
154
|
|
|
149
155
|
// if payload is too big, convert it to Stream
|
|
150
|
-
if (payload.length > this.getMaxPayloadSize())
|
|
156
|
+
if (payload.length > this.getMaxPayloadSize()) {
|
|
151
157
|
payload = codec.encode(Stream(JSON.stringify({ $r: respondTo, p: json })));
|
|
158
|
+
}
|
|
152
159
|
|
|
153
160
|
const nc = this.#getConnection();
|
|
154
161
|
let response = await nc.subscribe(respondTo, { max: 1, ...options });
|
|
155
162
|
|
|
156
163
|
await nc.publish(subject, payload);
|
|
157
164
|
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
const event = await getFirstItemFromAsyncIterable(response);
|
|
166
|
+
return codec.decode(event.data);
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
async subscribe(subject, options) {
|
|
@@ -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,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
|
@@ -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
|
}
|
package/src/RequestOptions.js
CHANGED
package/src/Service.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { codec, connection } from "./Connection.js";
|
|
2
|
-
import { picoid, sleep, hash, getSecondsSinceMidnight, OverlayObject, GeonixVersion, getFirstItemFromAsyncIterable } from "./Util.js";
|
|
2
|
+
import { picoid, sleep, hash, getSecondsSinceMidnight, OverlayObject, GeonixVersion, getFirstItemFromAsyncIterable, getNetworkAddresses } from "./Util.js";
|
|
3
3
|
import { webserver } from "./WebServer.js";
|
|
4
4
|
import { createConnection } from "net";
|
|
5
5
|
import { EOL } from "os";
|
|
6
6
|
import cookieParser from "cookie-parser";
|
|
7
7
|
import express from "express";
|
|
8
8
|
import { isStream, streamToString } from "./Stream.js";
|
|
9
|
+
import { logger } from "./Logger.js";
|
|
9
10
|
|
|
10
11
|
const protectedMethodNames = ["constructor", "onStart"];
|
|
11
12
|
const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|SUB|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
|
|
@@ -15,14 +16,33 @@ const ERROR_END_DELIMITER = "-".repeat(40);
|
|
|
15
16
|
|
|
16
17
|
const json = express.json({ limit: "100mb" });
|
|
17
18
|
const raw = express.raw({ type: "*/*", limit: "100mb" });
|
|
19
|
+
const cookies = cookieParser();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ServiceOptions
|
|
23
|
+
* @property {Object} middleware
|
|
24
|
+
* @property {boolean} middleware.json Enable JSON middleware
|
|
25
|
+
* @property {boolean} middleware.raw Enable RAW middleware
|
|
26
|
+
* @property {boolean} middleware.cookies Enable cookies middleware
|
|
27
|
+
* @property {boolean} fullBeacon Enable full beacon
|
|
28
|
+
*/
|
|
29
|
+
const defaultServiceOptions = {
|
|
30
|
+
middleware: {
|
|
31
|
+
json: true,
|
|
32
|
+
raw: true,
|
|
33
|
+
cookies: true
|
|
34
|
+
},
|
|
35
|
+
fullBeacon: true
|
|
36
|
+
};
|
|
18
37
|
|
|
19
38
|
export class Service {
|
|
20
39
|
|
|
21
40
|
static serviceClasses = [];
|
|
22
41
|
|
|
23
42
|
static start(options = {}) {
|
|
24
|
-
if (!this.serviceClasses.includes(this.prototype.constructor.name))
|
|
43
|
+
if (!this.serviceClasses.includes(this.prototype.constructor.name)) {
|
|
25
44
|
this.serviceClasses.push(this.prototype.constructor.name);
|
|
45
|
+
}
|
|
26
46
|
|
|
27
47
|
const instance = new this();
|
|
28
48
|
instance.#start(options);
|
|
@@ -31,21 +51,28 @@ export class Service {
|
|
|
31
51
|
// ---------------------------------------------------------------------------------------------
|
|
32
52
|
|
|
33
53
|
#me = {};
|
|
34
|
-
#options =
|
|
54
|
+
#options = defaultServiceOptions;
|
|
35
55
|
|
|
36
56
|
async #start(options = {}) {
|
|
37
|
-
this.#options = options;
|
|
57
|
+
this.#options = { ...this.#options, ...options };
|
|
38
58
|
|
|
39
59
|
await webserver.waitUntilReady();
|
|
40
60
|
await connection.waitUntilReady();
|
|
41
61
|
|
|
42
|
-
const fields = Object.getOwnPropertyNames(this)
|
|
62
|
+
const fields = Object.getOwnPropertyNames(this)
|
|
63
|
+
.filter(isEndpointFilter)
|
|
64
|
+
.concat(Object.getOwnPropertyNames(this.constructor.prototype));
|
|
43
65
|
|
|
44
66
|
// preserve order of endpoints as defined in the source
|
|
45
67
|
const serviceSource = this.constructor.toString().split("\n");
|
|
46
68
|
fields.sort((a, b, ia = -1, ib = -1) => {
|
|
47
|
-
for (let line = 0; line < serviceSource.length; line++)
|
|
48
|
-
|
|
69
|
+
for (let line = 0; line < serviceSource.length; line++) {
|
|
70
|
+
ia = serviceSource[line].includes(a) ? line : ia;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let line = 0; line < serviceSource.length; line++) {
|
|
74
|
+
ib = serviceSource[line].includes(b) ? line : ib;
|
|
75
|
+
}
|
|
49
76
|
return ia - ib;
|
|
50
77
|
});
|
|
51
78
|
|
|
@@ -65,7 +92,7 @@ export class Service {
|
|
|
65
92
|
// geonix version
|
|
66
93
|
gx: GeonixVersion,
|
|
67
94
|
// IP addresses
|
|
68
|
-
a:
|
|
95
|
+
a: getNetworkAddresses().map(address => `${address}:${webserver.getPort()}`)
|
|
69
96
|
};
|
|
70
97
|
|
|
71
98
|
// check if method takes context as first argument
|
|
@@ -79,7 +106,7 @@ export class Service {
|
|
|
79
106
|
this.#directListener();
|
|
80
107
|
this.#webserver();
|
|
81
108
|
|
|
82
|
-
|
|
109
|
+
logger.info("gx.service.start", this.#me.n, this.#me.v);
|
|
83
110
|
|
|
84
111
|
// execute onStart method, if present
|
|
85
112
|
if (this.onStart) {
|
|
@@ -93,7 +120,11 @@ export class Service {
|
|
|
93
120
|
*/
|
|
94
121
|
async #beacon() {
|
|
95
122
|
while (true) {
|
|
96
|
-
|
|
123
|
+
if (this.#options.fullBeacon) {
|
|
124
|
+
connection.publish("gx2.beacon", this.#me);
|
|
125
|
+
} else {
|
|
126
|
+
connection.publish("gx2.beacon", { i: this.#me.i });
|
|
127
|
+
}
|
|
97
128
|
await sleep(1000);
|
|
98
129
|
}
|
|
99
130
|
}
|
|
@@ -108,11 +139,9 @@ export class Service {
|
|
|
108
139
|
for await (let event of subscription) {
|
|
109
140
|
let call = codec.decode(event.data);
|
|
110
141
|
|
|
111
|
-
if (isStream(call))
|
|
112
|
-
call = JSON.parse(await streamToString(call));
|
|
142
|
+
if (isStream(call)) { call = JSON.parse(await streamToString(call)); }
|
|
113
143
|
|
|
114
|
-
if (call.$r && call.p)
|
|
115
|
-
this.#onCall(call.p, (json) => connection.publish(call.$r, json));
|
|
144
|
+
if (call.$r && call.p) { this.#onCall(call.p, (json) => connection.publish(call.$r, json)); }
|
|
116
145
|
}
|
|
117
146
|
}
|
|
118
147
|
|
|
@@ -126,11 +155,13 @@ export class Service {
|
|
|
126
155
|
for await (let event of subscription) {
|
|
127
156
|
let call = codec.decode(event.data);
|
|
128
157
|
|
|
129
|
-
if (isStream(call))
|
|
158
|
+
if (isStream(call)) {
|
|
130
159
|
call = JSON.parse(await streamToString(call));
|
|
160
|
+
}
|
|
131
161
|
|
|
132
|
-
if (call.$r && call.p)
|
|
162
|
+
if (call.$r && call.p) {
|
|
133
163
|
this.#onCall(call.p, (json) => connection.publish(call.$r, json));
|
|
164
|
+
}
|
|
134
165
|
}
|
|
135
166
|
}
|
|
136
167
|
|
|
@@ -142,11 +173,24 @@ export class Service {
|
|
|
142
173
|
const endpoints = this.#me.m
|
|
143
174
|
.filter(isEndpointFilter);
|
|
144
175
|
|
|
145
|
-
if (!endpoints || endpoints.length === 0)
|
|
176
|
+
if (!endpoints || endpoints.length === 0) {
|
|
146
177
|
return;
|
|
178
|
+
}
|
|
147
179
|
|
|
148
180
|
const router = webserver.router();
|
|
149
|
-
|
|
181
|
+
|
|
182
|
+
// setup defualt middlewares
|
|
183
|
+
if (this.#options.middleware.json) {
|
|
184
|
+
router.use(json);
|
|
185
|
+
}
|
|
186
|
+
if (this.#options.middleware.raw) {
|
|
187
|
+
router.use(raw);
|
|
188
|
+
}
|
|
189
|
+
if (this.#options.middleware.cookies) {
|
|
190
|
+
router.use(cookies);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// register endpoints
|
|
150
194
|
for (let endpoint of endpoints) {
|
|
151
195
|
let { verb, url: uri } = endpointMatcher.exec(endpoint)?.groups || {};
|
|
152
196
|
verb = verb.toLowerCase();
|
|
@@ -158,8 +202,9 @@ export class Service {
|
|
|
158
202
|
|
|
159
203
|
handlers = [...handlersBefore, ...handlers, ...handlersAfter];
|
|
160
204
|
|
|
161
|
-
for (let n = 0; n < handlers.length; n++)
|
|
205
|
+
for (let n = 0; n < handlers.length; n++) {
|
|
162
206
|
handlers[n] = handlers[n].bind(this);
|
|
207
|
+
}
|
|
163
208
|
|
|
164
209
|
switch (verb) {
|
|
165
210
|
case "ws":
|
|
@@ -180,8 +225,9 @@ export class Service {
|
|
|
180
225
|
async #sub(subject, handler) {
|
|
181
226
|
const subscription = await connection.subscribe(`gx.sub.${subject}`);
|
|
182
227
|
const processor = async () => {
|
|
183
|
-
for await (const event of subscription)
|
|
228
|
+
for await (const event of subscription) {
|
|
184
229
|
handler(event.data);
|
|
230
|
+
}
|
|
185
231
|
};
|
|
186
232
|
processor();
|
|
187
233
|
}
|
|
@@ -198,13 +244,15 @@ export class Service {
|
|
|
198
244
|
const method = this[methodName];
|
|
199
245
|
let _args = args;
|
|
200
246
|
|
|
201
|
-
if (!method)
|
|
247
|
+
if (!method) {
|
|
202
248
|
return respond({ e: `unknown method (${this.#me.n}.${methodName})` });
|
|
249
|
+
}
|
|
203
250
|
|
|
204
251
|
try {
|
|
205
252
|
// inject context as first argument
|
|
206
|
-
if (method.takesContext)
|
|
253
|
+
if (method.takesContext) {
|
|
207
254
|
_args = [OverlayObject(context, { caller, me: this.#me }), ..._args];
|
|
255
|
+
}
|
|
208
256
|
|
|
209
257
|
respond({ r: await method.apply(this, _args) });
|
|
210
258
|
} catch (e) {
|
package/src/Stream.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Readable } from "stream";
|
|
2
2
|
import { connection } from "./Connection.js";
|
|
3
|
-
import { picoid, StreamChunker } from "./Util.js";
|
|
3
|
+
import { getFirstItemFromAsyncIterable, getNetworkAddresses, picoid, StreamChunker } from "./Util.js";
|
|
4
|
+
import { logger } from "./Logger.js";
|
|
5
|
+
import { webserver } from "./WebServer.js";
|
|
4
6
|
|
|
5
7
|
const CHUNK_SIZE = 1024 * 128;
|
|
6
8
|
|
|
7
9
|
export const stats = {};
|
|
10
|
+
export const activeStreams = {};
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Converts data to stream
|
|
@@ -14,15 +17,17 @@ export const stats = {};
|
|
|
14
17
|
* @returns
|
|
15
18
|
*/
|
|
16
19
|
export function Stream(data, tag = "_") {
|
|
17
|
-
if (isStream(data))
|
|
20
|
+
if (isStream(data)) {
|
|
18
21
|
return data;
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
const id = picoid();
|
|
21
25
|
let readable = data;
|
|
22
26
|
|
|
23
27
|
// convert Buffer or string to a Readable
|
|
24
|
-
if (!(readable.pipe && readable.readable))
|
|
28
|
+
if (!(readable.pipe && readable.readable)) {
|
|
25
29
|
readable = Readable.from(Buffer.from(data));
|
|
30
|
+
}
|
|
26
31
|
|
|
27
32
|
// split the stream is smaller chunks
|
|
28
33
|
const transform = StreamChunker(Math.min(connection.getMaxPayloadSize(), CHUNK_SIZE));
|
|
@@ -30,23 +35,38 @@ export function Stream(data, tag = "_") {
|
|
|
30
35
|
readable = transform;
|
|
31
36
|
|
|
32
37
|
stats[tag] = stats[tag] !== undefined ? stats[tag] + 1 : 1;
|
|
38
|
+
activeStreams[id] = readable;
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
// NATS handler
|
|
41
|
+
(async () => {
|
|
35
42
|
const control = await connection.subscribe(`gx2.stream.${id}.a`, { max: 1 });
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
const event = await getFirstItemFromAsyncIterable(control);
|
|
45
|
+
if (activeStreams[id] !== undefined && event.data.length === 0) {
|
|
46
|
+
// remove stream from the list
|
|
47
|
+
delete activeStreams[id];
|
|
48
|
+
|
|
49
|
+
// kickstart the stream
|
|
50
|
+
readable.on("data", chunk => connection.publishRaw(`gx2.stream.${id}.b`, chunk));
|
|
51
|
+
readable.on("close", () => {
|
|
52
|
+
connection.publishRaw(`gx2.stream.${id}.b`);
|
|
53
|
+
stats[tag]--;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
|
|
58
|
+
const result = {
|
|
59
|
+
$: "stream",
|
|
60
|
+
id
|
|
45
61
|
};
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
// get the port and addresses of the webserver
|
|
64
|
+
const addresses = webserver.getPort() ? getNetworkAddresses().map(address => `${address}:${webserver.getPort()}`) : undefined;
|
|
65
|
+
if (addresses) {
|
|
66
|
+
result.a = addresses;
|
|
67
|
+
}
|
|
48
68
|
|
|
49
|
-
return
|
|
69
|
+
return result;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
export function isStream(object) {
|
|
@@ -54,23 +74,43 @@ export function isStream(object) {
|
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
export async function getReadable(object) {
|
|
57
|
-
if (!isStream(object))
|
|
77
|
+
if (!isStream(object)) {
|
|
58
78
|
return object;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// get stream via HTTP
|
|
82
|
+
if (object.a.length > 0) {
|
|
83
|
+
for (const address of object.a) {
|
|
84
|
+
try {
|
|
85
|
+
const uri = `http://${address}/!!_____stream/${object.id}`;
|
|
86
|
+
const response = await fetch(uri);
|
|
87
|
+
|
|
88
|
+
if (response.status === 200) {
|
|
89
|
+
return Readable.fromWeb(response.body);
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore errors
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
59
96
|
|
|
97
|
+
// get stream via NATS
|
|
60
98
|
const readable = new Readable({ read: () => null });
|
|
61
99
|
const subscription = await connection.subscribe(`gx2.stream.${object.id}.b`);
|
|
62
100
|
|
|
63
101
|
const dataHandler = async () => {
|
|
64
|
-
for await (const event of subscription)
|
|
102
|
+
for await (const event of subscription) {
|
|
65
103
|
try {
|
|
66
104
|
if (event.data.length === 0) {
|
|
67
105
|
readable.push(null);
|
|
68
106
|
subscription.drain();
|
|
69
|
-
} else
|
|
107
|
+
} else {
|
|
70
108
|
readable.push(event.data);
|
|
109
|
+
}
|
|
71
110
|
} catch (e) {
|
|
72
|
-
|
|
111
|
+
logger.error("Stream.getReadable.dataHandler.error:", e);
|
|
73
112
|
}
|
|
113
|
+
}
|
|
74
114
|
};
|
|
75
115
|
dataHandler();
|
|
76
116
|
|
|
@@ -82,17 +122,18 @@ export async function getReadable(object) {
|
|
|
82
122
|
|
|
83
123
|
export async function streamToBuffer(object) {
|
|
84
124
|
let readable = object;
|
|
85
|
-
if (isStream(readable))
|
|
125
|
+
if (isStream(readable)) {
|
|
86
126
|
readable = await getReadable(readable);
|
|
127
|
+
}
|
|
87
128
|
|
|
88
129
|
return Buffer.concat(await readable.toArray());
|
|
89
130
|
}
|
|
90
131
|
|
|
91
132
|
export async function streamToString(object) {
|
|
92
133
|
let readable = object;
|
|
93
|
-
if (isStream(readable))
|
|
134
|
+
if (isStream(readable)) {
|
|
94
135
|
readable = await getReadable(readable);
|
|
136
|
+
}
|
|
95
137
|
|
|
96
138
|
return Buffer.concat(await readable.toArray()).toString();
|
|
97
|
-
}
|
|
98
|
-
|
|
139
|
+
}
|
package/src/Util.js
CHANGED
|
@@ -3,6 +3,7 @@ import { URL, fileURLToPath } from "url";
|
|
|
3
3
|
import { readFile } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { Transform } from "node:stream";
|
|
6
|
+
import { networkInterfaces } from "os";
|
|
6
7
|
import * as http from "http";
|
|
7
8
|
import * as https from "https";
|
|
8
9
|
import * as net from "net";
|
|
@@ -60,7 +61,6 @@ export const createServerAtPort = (port, pkg, handler) =>
|
|
|
60
61
|
new Promise((resolve) => {
|
|
61
62
|
const server = pkg.createServer(handler);
|
|
62
63
|
server.on("error", (_error) => {
|
|
63
|
-
// console.log('error', error.message)
|
|
64
64
|
resolve(null);
|
|
65
65
|
});
|
|
66
66
|
server.listen(port, () => {
|
|
@@ -78,14 +78,16 @@ export const createServerAtPort = (port, pkg, handler) =>
|
|
|
78
78
|
* @returns
|
|
79
79
|
*/
|
|
80
80
|
export const createServerAtFreePort = async (pkg, handler, start = 30000, poolSize = 20000) => {
|
|
81
|
-
for (let port = start; port < start + poolSize; port++)
|
|
81
|
+
for (let port = start; port < start + poolSize; port++) {
|
|
82
82
|
try {
|
|
83
83
|
const result = await createServerAtPort(port, pkg, handler);
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
if (result) {
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
86
87
|
} catch {
|
|
87
88
|
// silenty ignore errors
|
|
88
89
|
}
|
|
90
|
+
}
|
|
89
91
|
};
|
|
90
92
|
|
|
91
93
|
/**
|
|
@@ -121,10 +123,11 @@ export const getFunctionParams = (fn) => {
|
|
|
121
123
|
const endParenthesisPosition = code.indexOf(")");
|
|
122
124
|
let params;
|
|
123
125
|
|
|
124
|
-
if (endParenthesisPosition != -1)
|
|
126
|
+
if (endParenthesisPosition != -1) {
|
|
125
127
|
params = code.substring(code.indexOf("(") + 1, endParenthesisPosition);
|
|
126
|
-
else
|
|
128
|
+
} else {
|
|
127
129
|
params = code.substring(0, code.indexOf("=>"));
|
|
130
|
+
}
|
|
128
131
|
|
|
129
132
|
params = params
|
|
130
133
|
// cleanup spaces
|
|
@@ -135,8 +138,9 @@ export const getFunctionParams = (fn) => {
|
|
|
135
138
|
// remove potential default values
|
|
136
139
|
for (let index = 0; index < params.length; index++) {
|
|
137
140
|
const defaultValueAssignmentPosition = params[index].indexOf("=");
|
|
138
|
-
if (defaultValueAssignmentPosition != -1)
|
|
141
|
+
if (defaultValueAssignmentPosition != -1) {
|
|
139
142
|
params[index] = params[index].substring(0, defaultValueAssignmentPosition - 1);
|
|
143
|
+
}
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
return params;
|
|
@@ -153,8 +157,10 @@ export const proxyHttp = (target, req, res) =>
|
|
|
153
157
|
const protocol = req.protocol === "https" ? https : http;
|
|
154
158
|
const proxyReq = protocol.request(remoteTarget, options, (proxyRes) => {
|
|
155
159
|
res.status(proxyRes.statusCode);
|
|
156
|
-
for (const header in proxyRes.headers)
|
|
160
|
+
for (const header in proxyRes.headers) {
|
|
157
161
|
res.set(header, proxyRes.headers[header]);
|
|
162
|
+
}
|
|
163
|
+
|
|
158
164
|
proxyRes.pipe(res);
|
|
159
165
|
});
|
|
160
166
|
proxyReq.on("error", (error) => reject(error));
|
|
@@ -207,4 +213,19 @@ export async function getFirstItemFromAsyncIterable(asyncIterable) {
|
|
|
207
213
|
const iterator = asyncIterable[Symbol.asyncIterator]();
|
|
208
214
|
const result = await iterator.next();
|
|
209
215
|
return result.value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getNetworkAddresses() {
|
|
219
|
+
const list = [];
|
|
220
|
+
const interfaces = networkInterfaces();
|
|
221
|
+
|
|
222
|
+
for (let interfaceAddresses of Object.values(interfaces)) {
|
|
223
|
+
for (let addressObject of interfaceAddresses) {
|
|
224
|
+
if (addressObject.family === "IPv4") {
|
|
225
|
+
list.push(addressObject.address);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return list;
|
|
210
231
|
}
|
package/src/WebServer.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import "express-async-errors";
|
|
2
2
|
import express, { Router } from "express";
|
|
3
3
|
import expressWs from "express-ws";
|
|
4
|
-
import { networkInterfaces } from "os";
|
|
5
4
|
import { createServerAtFreePort, createServerAtPort, sleep } from "./Util.js";
|
|
6
5
|
import * as http from "http";
|
|
7
6
|
import { Service } from "./Service.js";
|
|
8
7
|
import * as path from "path";
|
|
8
|
+
import { logger } from "./Logger.js";
|
|
9
|
+
import { activeStreams } from "./Stream.js";
|
|
9
10
|
|
|
10
11
|
export const HEALTH_CHECK_ENDPOINT = "/pA4vY7fT9oG5aI8cA4yV3qW5fP9qR1vI";
|
|
11
12
|
|
|
@@ -16,12 +17,14 @@ export const ServeStatic = (root, options = {}) => {
|
|
|
16
17
|
router.use((req, res, next) => {
|
|
17
18
|
if (options.root) {
|
|
18
19
|
// remove trailing slash
|
|
19
|
-
if (options.root.endsWith("/"))
|
|
20
|
+
if (options.root.endsWith("/")) {
|
|
20
21
|
options.root = options.root.slice(0, -1);
|
|
22
|
+
}
|
|
21
23
|
|
|
22
24
|
// replace root prefix
|
|
23
|
-
if (req.url.startsWith(options.root))
|
|
25
|
+
if (req.url.startsWith(options.root)) {
|
|
24
26
|
req.url = req.url.replace(options.root, "");
|
|
27
|
+
}
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
next();
|
|
@@ -29,11 +32,12 @@ export const ServeStatic = (root, options = {}) => {
|
|
|
29
32
|
|
|
30
33
|
router.use(express.static(root, options));
|
|
31
34
|
|
|
32
|
-
if (options.indexOn404)
|
|
35
|
+
if (options.indexOn404) {
|
|
33
36
|
router.get("*", (req, res) => {
|
|
34
|
-
|
|
37
|
+
logger.info(path.join(absoluteRoot, "index.html"));
|
|
35
38
|
res.sendFile(path.join(absoluteRoot, "index.html"));
|
|
36
39
|
});
|
|
40
|
+
}
|
|
37
41
|
|
|
38
42
|
return router;
|
|
39
43
|
};
|
|
@@ -47,31 +51,43 @@ class WebServer {
|
|
|
47
51
|
#started = false;
|
|
48
52
|
|
|
49
53
|
async start() {
|
|
50
|
-
if (this.#started)
|
|
51
|
-
return;
|
|
54
|
+
if (this.#started) { return; }
|
|
52
55
|
|
|
53
56
|
this.#started = true;
|
|
54
57
|
|
|
55
|
-
let
|
|
58
|
+
let srv;
|
|
56
59
|
if (process.env.LOCAL_PORT) {
|
|
57
|
-
|
|
60
|
+
srv = await createServerAtPort(process.env.LOCAL_PORT, http, this.#app);
|
|
58
61
|
} else {
|
|
59
|
-
|
|
62
|
+
srv = await createServerAtFreePort(http, this.#app);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!srv) {
|
|
66
|
+
throw new Error("gx.webserver.start: unable to start");
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
this.#server = server;
|
|
63
|
-
this.#port = port;
|
|
69
|
+
this.#server = srv.server;
|
|
70
|
+
this.#port = srv.port;
|
|
64
71
|
|
|
65
|
-
expressWs(this.#app, server);
|
|
72
|
+
expressWs(this.#app, srv.server);
|
|
73
|
+
|
|
74
|
+
// stream endpoint
|
|
75
|
+
this.#app.get("/!!_____stream/:id", (req, res) => {
|
|
76
|
+
const id = req.params.id;
|
|
77
|
+
|
|
78
|
+
if (activeStreams[id]) {
|
|
79
|
+
res.status(200);
|
|
80
|
+
activeStreams[id].pipe(res);
|
|
81
|
+
delete activeStreams[id];
|
|
82
|
+
} else {
|
|
83
|
+
res.status(404).send({ error: 404 });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
66
86
|
|
|
67
87
|
this.#app.get(HEALTH_CHECK_ENDPOINT, (req, res) => {
|
|
68
88
|
res.send({ status: "healthy", services: Service.serviceClasses });
|
|
69
89
|
});
|
|
70
90
|
|
|
71
|
-
// this.#app.use((req, res, next) => {
|
|
72
|
-
// next()
|
|
73
|
-
// })
|
|
74
|
-
|
|
75
91
|
// wait for 2 seconds and then set fall-through handler
|
|
76
92
|
// this should provide more than enough time to start all the services
|
|
77
93
|
setTimeout(() => {
|
|
@@ -88,23 +104,11 @@ class WebServer {
|
|
|
88
104
|
this.#app.disable("x-powered-by");
|
|
89
105
|
this.#app.disable("etag");
|
|
90
106
|
|
|
91
|
-
|
|
107
|
+
logger.info(`gx.webserver.start: listening on http://127.0.0.1:${this.#port}`);
|
|
92
108
|
|
|
93
109
|
this.#ready = true;
|
|
94
110
|
}
|
|
95
111
|
|
|
96
|
-
getAddresses() {
|
|
97
|
-
const list = [];
|
|
98
|
-
const interfaces = networkInterfaces();
|
|
99
|
-
|
|
100
|
-
for (let interfaceAddresses of Object.values(interfaces))
|
|
101
|
-
for (let addressObject of interfaceAddresses)
|
|
102
|
-
if (addressObject.family === "IPv4")
|
|
103
|
-
list.push(addressObject.address);
|
|
104
|
-
|
|
105
|
-
return list;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
112
|
getPort() {
|
|
109
113
|
return this.#port;
|
|
110
114
|
}
|
|
@@ -118,14 +122,15 @@ class WebServer {
|
|
|
118
122
|
async waitUntilReady() {
|
|
119
123
|
await this.start();
|
|
120
124
|
|
|
121
|
-
while (!this.#ready)
|
|
125
|
+
while (!this.#ready) {
|
|
122
126
|
await sleep(100);
|
|
127
|
+
}
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
stop() {
|
|
126
131
|
if (this.#server) {
|
|
127
132
|
this.#server.close();
|
|
128
|
-
|
|
133
|
+
logger.info("gx.webserver.stop");
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
|
package/test/gateway.js
CHANGED
|
@@ -2,14 +2,14 @@ import { Gateway, Service } from "../exports.js";
|
|
|
2
2
|
|
|
3
3
|
class TestService extends Service {
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
res.send(
|
|
5
|
+
"GET /"(req, res) {
|
|
6
|
+
res.send("Hello World");
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
TestService.start()
|
|
10
|
+
TestService.start();
|
|
11
11
|
Gateway.start({
|
|
12
12
|
beforeRequest: (req, res) => {
|
|
13
|
-
res.set(
|
|
13
|
+
res.set("X-Test", "Test");
|
|
14
14
|
}
|
|
15
|
-
})
|
|
15
|
+
});
|
package/test/stream.js
CHANGED
|
@@ -1,38 +1,43 @@
|
|
|
1
|
-
import { randomBytes } from
|
|
2
|
-
import { Stream, getReadable, connection } from
|
|
3
|
-
import { createWriteStream, readFileSync } from
|
|
4
|
-
import { createHash } from
|
|
5
|
-
import { pipeline } from
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { Stream, getReadable, connection } from "geonix";
|
|
3
|
+
import { createWriteStream, readFileSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { webserver } from "../src/WebServer.js";
|
|
6
7
|
|
|
7
|
-
await connection.waitUntilReady()
|
|
8
|
+
await connection.waitUntilReady();
|
|
8
9
|
|
|
9
|
-
const hash = data => createHash(
|
|
10
|
+
const hash = data => createHash("sha512").update(data).digest("base64");
|
|
10
11
|
|
|
11
|
-
const PAYLOAD_SIZE = 1024 * 1024 * 1024
|
|
12
|
-
const TEMP_FILE =
|
|
12
|
+
const PAYLOAD_SIZE = 1024 * 1024 * 1024;
|
|
13
|
+
const TEMP_FILE = "/tmp/geonix.stream_test";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
await webserver.start();
|
|
16
|
+
|
|
17
|
+
console.time("test");
|
|
15
18
|
try {
|
|
16
|
-
const payload = randomBytes(PAYLOAD_SIZE)
|
|
17
|
-
const sourceHash = hash(payload)
|
|
19
|
+
const payload = randomBytes(PAYLOAD_SIZE);
|
|
20
|
+
const sourceHash = hash(payload);
|
|
21
|
+
|
|
22
|
+
const stream = Stream(payload);
|
|
18
23
|
|
|
19
|
-
const source = await getReadable(
|
|
20
|
-
const dest = createWriteStream(TEMP_FILE)
|
|
24
|
+
const source = await getReadable(stream);
|
|
25
|
+
const dest = createWriteStream(TEMP_FILE);
|
|
21
26
|
|
|
22
|
-
await pipeline(source, dest)
|
|
27
|
+
await pipeline(source, dest);
|
|
23
28
|
|
|
24
|
-
const check = readFileSync(TEMP_FILE)
|
|
25
|
-
const destHash = hash(check)
|
|
29
|
+
const check = readFileSync(TEMP_FILE);
|
|
30
|
+
const destHash = hash(check);
|
|
26
31
|
|
|
27
32
|
if (sourceHash == destHash) {
|
|
28
|
-
console.log(
|
|
33
|
+
console.log("MATCH");
|
|
29
34
|
} else {
|
|
30
|
-
console.error(
|
|
31
|
-
console.log(payload)
|
|
32
|
-
console.log(check)
|
|
35
|
+
console.error("Destination does not match the source!");
|
|
36
|
+
console.log("P =", payload);
|
|
37
|
+
console.log("C =", check);
|
|
33
38
|
}
|
|
34
39
|
} catch (e) {
|
|
35
|
-
console.error(e)
|
|
40
|
+
console.error(e);
|
|
36
41
|
} finally {
|
|
37
|
-
console.timeEnd(
|
|
42
|
+
console.timeEnd("test");
|
|
38
43
|
}
|