geonix 1.20.1 → 1.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.js +2 -1
- package/index.d.ts +21 -1
- package/package.json +4 -3
- package/src/Codec.js +16 -0
- package/src/Connection.js +37 -30
- package/src/Gateway.js +36 -32
- package/src/Logger.js +34 -0
- package/src/Registry.js +9 -5
- package/src/Request.js +18 -9
- package/src/RequestOptions.js +1 -0
- package/src/Service.js +74 -25
- package/src/Stream.js +65 -25
- package/src/Util.js +29 -8
- package/src/WebServer.js +37 -32
- package/test/context.js +35 -0
- package/test/gateway.js +5 -5
- package/test/simple.js +29 -0
- package/test/stream.js +28 -23
package/src/Service.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { picoid, sleep, hash, getSecondsSinceMidnight, OverlayObject, GeonixVersion, getFirstItemFromAsyncIterable } from "./Util.js";
|
|
1
|
+
import { connection } from "./Connection.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";
|
|
10
|
+
import { decode } from "./Codec.js";
|
|
9
11
|
|
|
10
12
|
const protectedMethodNames = ["constructor", "onStart"];
|
|
11
13
|
const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|SUB|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
|
|
@@ -15,14 +17,33 @@ const ERROR_END_DELIMITER = "-".repeat(40);
|
|
|
15
17
|
|
|
16
18
|
const json = express.json({ limit: "100mb" });
|
|
17
19
|
const raw = express.raw({ type: "*/*", limit: "100mb" });
|
|
20
|
+
const cookies = cookieParser();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} ServiceOptions
|
|
24
|
+
* @property {Object} middleware
|
|
25
|
+
* @property {boolean} middleware.json Enable JSON middleware
|
|
26
|
+
* @property {boolean} middleware.raw Enable RAW middleware
|
|
27
|
+
* @property {boolean} middleware.cookies Enable cookies middleware
|
|
28
|
+
* @property {boolean} fullBeacon Enable full beacon
|
|
29
|
+
*/
|
|
30
|
+
const defaultServiceOptions = {
|
|
31
|
+
middleware: {
|
|
32
|
+
json: true,
|
|
33
|
+
raw: true,
|
|
34
|
+
cookies: true
|
|
35
|
+
},
|
|
36
|
+
fullBeacon: true
|
|
37
|
+
};
|
|
18
38
|
|
|
19
39
|
export class Service {
|
|
20
40
|
|
|
21
41
|
static serviceClasses = [];
|
|
22
42
|
|
|
23
43
|
static start(options = {}) {
|
|
24
|
-
if (!this.serviceClasses.includes(this.prototype.constructor.name))
|
|
44
|
+
if (!this.serviceClasses.includes(this.prototype.constructor.name)) {
|
|
25
45
|
this.serviceClasses.push(this.prototype.constructor.name);
|
|
46
|
+
}
|
|
26
47
|
|
|
27
48
|
const instance = new this();
|
|
28
49
|
instance.#start(options);
|
|
@@ -31,21 +52,28 @@ export class Service {
|
|
|
31
52
|
// ---------------------------------------------------------------------------------------------
|
|
32
53
|
|
|
33
54
|
#me = {};
|
|
34
|
-
#options =
|
|
55
|
+
#options = defaultServiceOptions;
|
|
35
56
|
|
|
36
57
|
async #start(options = {}) {
|
|
37
|
-
this.#options = options;
|
|
58
|
+
this.#options = { ...this.#options, ...options };
|
|
38
59
|
|
|
39
60
|
await webserver.waitUntilReady();
|
|
40
61
|
await connection.waitUntilReady();
|
|
41
62
|
|
|
42
|
-
const fields = Object.getOwnPropertyNames(this)
|
|
63
|
+
const fields = Object.getOwnPropertyNames(this)
|
|
64
|
+
.filter(isEndpointFilter)
|
|
65
|
+
.concat(Object.getOwnPropertyNames(this.constructor.prototype));
|
|
43
66
|
|
|
44
67
|
// preserve order of endpoints as defined in the source
|
|
45
68
|
const serviceSource = this.constructor.toString().split("\n");
|
|
46
69
|
fields.sort((a, b, ia = -1, ib = -1) => {
|
|
47
|
-
for (let line = 0; line < serviceSource.length; line++)
|
|
48
|
-
|
|
70
|
+
for (let line = 0; line < serviceSource.length; line++) {
|
|
71
|
+
ia = serviceSource[line].includes(a) ? line : ia;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let line = 0; line < serviceSource.length; line++) {
|
|
75
|
+
ib = serviceSource[line].includes(b) ? line : ib;
|
|
76
|
+
}
|
|
49
77
|
return ia - ib;
|
|
50
78
|
});
|
|
51
79
|
|
|
@@ -65,7 +93,7 @@ export class Service {
|
|
|
65
93
|
// geonix version
|
|
66
94
|
gx: GeonixVersion,
|
|
67
95
|
// IP addresses
|
|
68
|
-
a:
|
|
96
|
+
a: getNetworkAddresses().map(address => `${address}:${webserver.getPort()}`)
|
|
69
97
|
};
|
|
70
98
|
|
|
71
99
|
// check if method takes context as first argument
|
|
@@ -79,7 +107,7 @@ export class Service {
|
|
|
79
107
|
this.#directListener();
|
|
80
108
|
this.#webserver();
|
|
81
109
|
|
|
82
|
-
|
|
110
|
+
logger.info("gx.service.start", this.#me.n, this.#me.v);
|
|
83
111
|
|
|
84
112
|
// execute onStart method, if present
|
|
85
113
|
if (this.onStart) {
|
|
@@ -93,7 +121,11 @@ export class Service {
|
|
|
93
121
|
*/
|
|
94
122
|
async #beacon() {
|
|
95
123
|
while (true) {
|
|
96
|
-
|
|
124
|
+
if (this.#options.fullBeacon) {
|
|
125
|
+
connection.publish("gx2.beacon", this.#me);
|
|
126
|
+
} else {
|
|
127
|
+
connection.publish("gx2.beacon", { i: this.#me.i });
|
|
128
|
+
}
|
|
97
129
|
await sleep(1000);
|
|
98
130
|
}
|
|
99
131
|
}
|
|
@@ -106,13 +138,11 @@ export class Service {
|
|
|
106
138
|
const subscription = await connection.subscribe(`gx2.service.${hash(identifier)}`, { queue: identifier });
|
|
107
139
|
|
|
108
140
|
for await (let event of subscription) {
|
|
109
|
-
let call =
|
|
141
|
+
let call = decode(event.data);
|
|
110
142
|
|
|
111
|
-
if (isStream(call))
|
|
112
|
-
call = JSON.parse(await streamToString(call));
|
|
143
|
+
if (isStream(call)) { call = JSON.parse(await streamToString(call)); }
|
|
113
144
|
|
|
114
|
-
if (call.$r && call.p)
|
|
115
|
-
this.#onCall(call.p, (json) => connection.publish(call.$r, json));
|
|
145
|
+
if (call.$r && call.p) { this.#onCall(call.p, (json) => connection.publish(call.$r, json)); }
|
|
116
146
|
}
|
|
117
147
|
}
|
|
118
148
|
|
|
@@ -124,13 +154,15 @@ export class Service {
|
|
|
124
154
|
const subscription = await connection.subscribe(`gx2.service.${hash(identifier)}`, { queue: identifier });
|
|
125
155
|
|
|
126
156
|
for await (let event of subscription) {
|
|
127
|
-
let call =
|
|
157
|
+
let call = decode(event.data);
|
|
128
158
|
|
|
129
|
-
if (isStream(call))
|
|
159
|
+
if (isStream(call)) {
|
|
130
160
|
call = JSON.parse(await streamToString(call));
|
|
161
|
+
}
|
|
131
162
|
|
|
132
|
-
if (call.$r && call.p)
|
|
163
|
+
if (call.$r && call.p) {
|
|
133
164
|
this.#onCall(call.p, (json) => connection.publish(call.$r, json));
|
|
165
|
+
}
|
|
134
166
|
}
|
|
135
167
|
}
|
|
136
168
|
|
|
@@ -142,11 +174,24 @@ export class Service {
|
|
|
142
174
|
const endpoints = this.#me.m
|
|
143
175
|
.filter(isEndpointFilter);
|
|
144
176
|
|
|
145
|
-
if (!endpoints || endpoints.length === 0)
|
|
177
|
+
if (!endpoints || endpoints.length === 0) {
|
|
146
178
|
return;
|
|
179
|
+
}
|
|
147
180
|
|
|
148
181
|
const router = webserver.router();
|
|
149
|
-
|
|
182
|
+
|
|
183
|
+
// setup defualt middlewares
|
|
184
|
+
if (this.#options.middleware.json) {
|
|
185
|
+
router.use(json);
|
|
186
|
+
}
|
|
187
|
+
if (this.#options.middleware.raw) {
|
|
188
|
+
router.use(raw);
|
|
189
|
+
}
|
|
190
|
+
if (this.#options.middleware.cookies) {
|
|
191
|
+
router.use(cookies);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// register endpoints
|
|
150
195
|
for (let endpoint of endpoints) {
|
|
151
196
|
let { verb, url: uri } = endpointMatcher.exec(endpoint)?.groups || {};
|
|
152
197
|
verb = verb.toLowerCase();
|
|
@@ -158,8 +203,9 @@ export class Service {
|
|
|
158
203
|
|
|
159
204
|
handlers = [...handlersBefore, ...handlers, ...handlersAfter];
|
|
160
205
|
|
|
161
|
-
for (let n = 0; n < handlers.length; n++)
|
|
206
|
+
for (let n = 0; n < handlers.length; n++) {
|
|
162
207
|
handlers[n] = handlers[n].bind(this);
|
|
208
|
+
}
|
|
163
209
|
|
|
164
210
|
switch (verb) {
|
|
165
211
|
case "ws":
|
|
@@ -180,8 +226,9 @@ export class Service {
|
|
|
180
226
|
async #sub(subject, handler) {
|
|
181
227
|
const subscription = await connection.subscribe(`gx.sub.${subject}`);
|
|
182
228
|
const processor = async () => {
|
|
183
|
-
for await (const event of subscription)
|
|
229
|
+
for await (const event of subscription) {
|
|
184
230
|
handler(event.data);
|
|
231
|
+
}
|
|
185
232
|
};
|
|
186
233
|
processor();
|
|
187
234
|
}
|
|
@@ -198,13 +245,15 @@ export class Service {
|
|
|
198
245
|
const method = this[methodName];
|
|
199
246
|
let _args = args;
|
|
200
247
|
|
|
201
|
-
if (!method)
|
|
248
|
+
if (!method) {
|
|
202
249
|
return respond({ e: `unknown method (${this.#me.n}.${methodName})` });
|
|
250
|
+
}
|
|
203
251
|
|
|
204
252
|
try {
|
|
205
253
|
// inject context as first argument
|
|
206
|
-
if (method.takesContext)
|
|
254
|
+
if (method.takesContext) {
|
|
207
255
|
_args = [OverlayObject(context, { caller, me: this.#me }), ..._args];
|
|
256
|
+
}
|
|
208
257
|
|
|
209
258
|
respond({ r: await method.apply(this, _args) });
|
|
210
259
|
} 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,17 @@ 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
|
-
export async function streamToString(object) {
|
|
92
|
-
|
|
93
|
-
if (isStream(readable))
|
|
94
|
-
readable = await getReadable(readable);
|
|
95
|
-
|
|
96
|
-
return Buffer.concat(await readable.toArray()).toString();
|
|
132
|
+
export async function streamToString(object, encoding) {
|
|
133
|
+
return streamToBuffer(object).toString(encoding);
|
|
97
134
|
}
|
|
98
135
|
|
|
136
|
+
export async function streamToJSON(object) {
|
|
137
|
+
return JSON.parse(await streamToBuffer(object));
|
|
138
|
+
}
|
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/context.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Remote, Service } from "../exports.js";
|
|
2
|
+
import { sleep } from "../src/Util.js";
|
|
3
|
+
|
|
4
|
+
class TimeService extends Service {
|
|
5
|
+
|
|
6
|
+
#timestamp() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getCurrentTime() {
|
|
11
|
+
const [prefix] = this.context;
|
|
12
|
+
|
|
13
|
+
return `${prefix} ${this.#timestamp()}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class ApplicationService extends Service {
|
|
19
|
+
|
|
20
|
+
#timeService = Remote("TimeService", "prefix");
|
|
21
|
+
|
|
22
|
+
async onStart() {
|
|
23
|
+
while (true) {
|
|
24
|
+
const time = await this.#timeService.getCurrentTime();
|
|
25
|
+
|
|
26
|
+
console.log("TIME =", time);
|
|
27
|
+
|
|
28
|
+
await sleep(1000);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
TimeService.start();
|
|
35
|
+
ApplicationService.start();
|
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/simple.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Remote, Service } from "../exports.js";
|
|
2
|
+
import { sleep } from "../src/Util.js";
|
|
3
|
+
|
|
4
|
+
class TimeService extends Service {
|
|
5
|
+
|
|
6
|
+
getCurrentTime() {
|
|
7
|
+
return new Date().toISOString();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class ApplicationService extends Service {
|
|
13
|
+
|
|
14
|
+
#timeService = Remote("TimeService");
|
|
15
|
+
|
|
16
|
+
async onStart() {
|
|
17
|
+
while (true) {
|
|
18
|
+
const time = await this.#timeService.getCurrentTime();
|
|
19
|
+
|
|
20
|
+
console.log("TIME =", time);
|
|
21
|
+
|
|
22
|
+
await sleep(1000);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
TimeService.start();
|
|
29
|
+
ApplicationService.start();
|