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/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
|
}
|