geonix 1.12.2 → 1.20.0

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/src/Gateway.js CHANGED
@@ -1,236 +1,258 @@
1
- import { connection } from './Connection.js'
2
- import { registry } from './Registry.js'
3
- import { createTCPServer, GeonixVersion, picoid, proxyHttp, sleep } from './Util.js'
4
- import express, { Router } from 'express'
5
- import { Request } from './Request.js'
6
- import { HEALTH_CHECK_ENDPOINT } from './WebServer.js'
7
- import expressWs from 'express-ws'
8
- import querystring from 'querystring'
9
- import semver from 'semver'
10
- import { WebSocket } from 'ws'
11
-
12
- const raw = express.raw({ limit: '100mb' })
13
-
14
- const DEBUG_ENDPOINT = '/lZ6jD2eC3iP0zB3jJ1yJ9pM8gG3yI3vS'
15
- const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/
1
+ import { connection } from "./Connection.js";
2
+ import { registry } from "./Registry.js";
3
+ import { createTCPServer, GeonixVersion, picoid, proxyHttp, sleep } from "./Util.js";
4
+ import express, { Router } from "express";
5
+ import { Request } from "./Request.js";
6
+ import { HEALTH_CHECK_ENDPOINT } from "./WebServer.js";
7
+ import expressWs from "express-ws";
8
+ import querystring from "querystring";
9
+ import semver from "semver";
10
+ import { WebSocket } from "ws";
11
+
12
+ const raw = express.raw({ limit: "100mb" });
13
+
14
+ const DEBUG_ENDPOINT = "/lZ6jD2eC3iP0zB3jJ1yJ9pM8gG3yI3vS";
15
+ const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
16
16
 
17
17
  const logger = (req, res, next) => {
18
- console.info(`HTTP ${req.method} ${req.url}`)
18
+ console.info(`HTTP ${req.method} ${req.url}`);
19
19
 
20
- next()
21
- }
20
+ next();
21
+ };
22
22
 
23
23
  const stats = {
24
24
  requests: 0,
25
25
  proxied: 0,
26
26
  proxied_over_nats: 0,
27
27
  debug_requests: 0
28
- }
28
+ };
29
+
30
+ const defaultOpts = {
31
+ beforeRequest: (_req, _res) => { },
32
+ afterRequest: (_req, _res) => { }
33
+ };
29
34
 
30
35
  export class Gateway {
31
36
 
32
- static start() {
33
- return new Gateway()
37
+ static start(opts) {
38
+ return new Gateway(opts);
34
39
  }
35
40
 
36
- #api = express()
37
- #router = (req, res, next) => next()
38
- #activeServers = []
39
- #port
41
+ #opts = defaultOpts;
42
+ #api = express();
43
+ #router = (req, res, next) => next();
44
+ #port;
40
45
 
41
- #rebuildRouter = false
42
- #buildRouterRunning = false
43
- #endpoints = []
46
+ #rebuildRouter = false;
47
+ #buildRouterRunning = false;
48
+ #endpoints = [];
44
49
 
45
- #registry = {}
50
+ #registry = {};
46
51
 
47
- constructor() {
48
- expressWs(this.#api)
52
+ constructor(opts) {
53
+ expressWs(this.#api);
49
54
 
50
- this.#start()
55
+ this.#opts = { ...this.#opts, ...opts };
56
+
57
+ this.#start();
51
58
  }
52
59
 
53
60
  async #start(port = 8080) {
54
- await connection.waitUntilReady()
61
+ await connection.waitUntilReady();
55
62
 
56
- this.#port = process.env.PORT || port
57
- const server = this.#api.listen(this.#port)
63
+ this.#port = process.env.PORT || port;
64
+ this.#api.listen(this.#port);
58
65
 
59
- console.debug(`geonix.gateway: listening on http://0.0.0.0:${this.#port}`)
66
+ console.debug(`geonix.gateway: listening on http://0.0.0.0:${this.#port}`);
60
67
 
61
68
  // logging
62
- this.#api.use(logger)
69
+ this.#api.use(logger);
63
70
 
64
71
  // cors
65
72
  this.#api.use((req, res, next) => {
66
- const origin = req.headers['origin']
67
- const allMethods = 'GET,PUT,POST,DELETE,OPTIONS,HEAD'
68
- const allHeaders = '*'
69
- const requestMethod = req.headers['access-control-request-method']
70
- const requestHeaders = req.headers['access-control-request-headers']
73
+ const origin = req.headers["origin"];
74
+ const allMethods = "GET,PUT,POST,DELETE,OPTIONS,HEAD";
75
+ const allHeaders = "*";
76
+ const requestMethod = req.headers["access-control-request-method"];
77
+ const requestHeaders = req.headers["access-control-request-headers"];
71
78
 
72
- res.set('Access-Control-Allow-Credentials', 'true')
73
- res.set('Access-Control-Allow-Origin', origin || '*')
74
- res.set('Access-Control-Allow-Methods', requestMethod || allMethods)
75
- res.set('Allow', requestMethod || allMethods)
76
- res.set('Access-Control-Allow-Headers', requestHeaders || allHeaders)
79
+ res.set("Access-Control-Allow-Credentials", "true");
80
+ res.set("Access-Control-Allow-Origin", origin || "*");
81
+ res.set("Access-Control-Allow-Methods", requestMethod || allMethods);
82
+ res.set("Allow", requestMethod || allMethods);
83
+ res.set("Access-Control-Allow-Headers", requestHeaders || allHeaders);
77
84
 
78
- next()
79
- })
85
+ next();
86
+ });
80
87
 
81
88
  // debug router (only available in non-production environments)
82
- if (process.env.NODE_ENV !== 'production')
83
- this.#api.use(DEBUG_ENDPOINT, this.#debugRouter())
89
+ if (process.env.NODE_ENV !== "production")
90
+ this.#api.use(DEBUG_ENDPOINT, this.#debugRouter());
91
+
92
+ this.#api.use((req, res, next) => {
93
+ if (this.#opts.beforeRequest) {
94
+ this.#opts.beforeRequest(req, res);
95
+ }
96
+ next();
97
+ });
84
98
 
85
99
  // handle mapped endpoints as service calls
86
100
  this.#api.use(raw, (req, res, next) => {
87
- stats.requests++
101
+ stats.requests++;
88
102
 
89
103
  if (this.#router)
90
- this.#router(req, res, next)
104
+ this.#router(req, res, next);
91
105
  else
92
- next()
93
- })
106
+ next();
107
+ });
108
+
109
+ this.#api.use((req, res, next) => {
110
+ if (this.#opts.afterRequest) {
111
+ this.#opts.afterRequest(req, res);
112
+ }
113
+ next();
114
+ });
94
115
 
95
116
  // config
96
- this.#api.disable('x-powered-by')
97
- this.#api.disable('etag')
117
+ this.#api.disable("x-powered-by");
118
+ this.#api.disable("etag");
98
119
 
99
120
  // default answer
100
- this.#api.all('*', (req, res) => {
121
+ this.#api.all("*", (req, res) => {
101
122
  res.status(404).send({
102
123
  error: 404,
103
- source: 'gw'
104
- })
105
- })
124
+ source: "gw"
125
+ });
126
+ });
106
127
 
107
128
  setInterval(() => {
108
129
  if (this.#rebuildRouter)
109
- this.#buildRouter()
110
- }, 1000)
130
+ this.#buildRouter();
131
+ }, 1000);
111
132
 
112
133
  while (true) {
113
134
  try {
114
135
  // send keeplive to check if connection is still alive
115
- await connection.publish('gx.gateway.keepalive', Date.now())
136
+ await connection.publish("gx.gateway.keepalive", Date.now());
116
137
 
117
- await this.#handleAddedServics()
118
- await this.#handleRemovedServices()
119
- await sleep(1000)
138
+ await this.#handleAddedServics();
139
+ await this.#handleRemovedServices();
140
+ await sleep(1000);
120
141
 
121
- if (connection.isClosed())
122
- break
142
+ if (connection.isClosed()) {
143
+ break;
144
+ }
123
145
  } catch (e) {
124
- console.error(e)
146
+ console.error(e);
125
147
  }
126
148
  }
127
149
 
128
150
  // terminate process
129
- console.debug(`geonix.gateway: stopped`)
130
- process.exit(0)
151
+ console.debug("geonix.gateway: stopped");
152
+ process.exit(0);
131
153
  }
132
154
 
133
155
  async #handleAddedServics() {
134
- let entries = Object.values(registry.getEntries())
156
+ let entries = Object.values(registry.getEntries());
135
157
 
136
158
  const processEntry = async (entry) => {
137
159
  if (this.#registry[entry.i] !== undefined)
138
- return false
160
+ return false;
139
161
 
140
- console.log(`gateway.onServiceAdded: ${entry.n}@${entry.v} (#${entry.i})`)
162
+ console.log(`gateway.onServiceAdded: ${entry.n}@${entry.v} (#${entry.i})`);
141
163
 
142
164
  // figure out if endpoints is reachable via direct http call
143
- let backend
165
+ let backend;
144
166
  for (let address of entry.a)
145
167
  try {
146
- const ac = new AbortController()
147
- const timeout = setTimeout(() => ac.abort(), 500)
148
- const result = await (await fetch(`http://${address}${HEALTH_CHECK_ENDPOINT}`, { signal: ac.signal })).json()
149
- clearTimeout(timeout)
150
- if (result.status === 'healthy' && result.services?.includes(entry.n)) {
151
- backend = address
152
- console.log(`${serviceTag} directly reachable @ ${address}`)
153
- break
168
+ const ac = new AbortController();
169
+ const timeout = setTimeout(() => ac.abort(), 500);
170
+ const result = await (await fetch(`http://${address}${HEALTH_CHECK_ENDPOINT}`, { signal: ac.signal })).json();
171
+ clearTimeout(timeout);
172
+ if (result.status === "healthy" && result.services?.includes(entry.n)) {
173
+ backend = address;
174
+ console.log(`${entry.n}@${entry.v} (#${entry.i}) directly reachable @ ${address}`);
175
+ break;
154
176
  }
155
- } catch (e) {
177
+ } catch {
156
178
  // silently ignore errors
157
179
  }
158
180
 
159
- let proxy
181
+ let proxy;
160
182
  if (!backend) {
161
183
  // create proxy over nats
162
184
  proxy = await createTCPServer(async (client) => {
163
- const streamId = picoid()
164
- stats.proxied_over_nats++
185
+ const streamId = picoid();
186
+ stats.proxied_over_nats++;
165
187
 
166
188
  try {
167
- await this.#proxyHttpOverNats(streamId, entry, client)
189
+ await this.#proxyHttpOverNats(streamId, entry, client);
168
190
  } catch (e) {
169
- console.error('nats.proxy.error', e)
170
- client.destroy()
191
+ console.error("nats.proxy.error", e);
192
+ client.destroy();
171
193
  }
172
- }, 50000, 10000)
173
- backend = `127.0.0.1:${proxy.port}`
194
+ }, 50000, 10000);
195
+ backend = `127.0.0.1:${proxy.port}`;
174
196
  }
175
197
 
176
- this.#registry[entry.i] = { entry, proxy, backend }
198
+ this.#registry[entry.i] = { entry, proxy, backend };
177
199
 
178
- return true
179
- }
200
+ return true;
201
+ };
180
202
 
181
- entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true)
203
+ entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
182
204
 
183
205
  if (entries.length > 0)
184
- this.#rebuildRouter = true
206
+ this.#rebuildRouter = true;
185
207
  }
186
208
 
187
209
  async #handleRemovedServices() {
188
- const localEntries = Object.values(this.#registry)
189
- const registryEntries = registry.getEntries()
210
+ const localEntries = Object.values(this.#registry);
211
+ const registryEntries = registry.getEntries();
190
212
 
191
213
  for (let { entry, proxy } of localEntries) {
192
214
  if (registryEntries[entry.i] === undefined) {
193
- proxy?.server?.close()
194
- console.log(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`)
195
- delete this.#registry[entry.i]
215
+ proxy?.server?.close();
216
+ console.log(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`);
217
+ delete this.#registry[entry.i];
196
218
 
197
- this.#rebuildRouter = true
219
+ this.#rebuildRouter = true;
198
220
  }
199
221
  }
200
222
  }
201
223
 
202
224
  #debugRouter() {
203
- const router = Router()
225
+ const router = Router();
204
226
 
205
227
  router.use((req, res, next) => {
206
- stats.debug_requests++
228
+ stats.debug_requests++;
207
229
 
208
- next()
209
- })
230
+ next();
231
+ });
210
232
 
211
- router.get('/services', (req, res) => {
212
- const services = Object.values(registry.getEntries()).map(e => (`${e.n}@${e.v}`))
213
- services.sort()
214
- res.send(services)
215
- })
233
+ router.get("/services", (req, res) => {
234
+ const services = Object.values(registry.getEntries()).map(e => (`${e.n}@${e.v}`));
235
+ services.sort();
236
+ res.send(services);
237
+ });
216
238
 
217
- router.get('/endpoints', (req, res) => {
218
- res.send(this.#endpoints)
219
- })
239
+ router.get("/endpoints", (req, res) => {
240
+ res.send(this.#endpoints);
241
+ });
220
242
 
221
- router.get('/router-registry', (req, res) => {
222
- res.send(this.#registry)
223
- })
243
+ router.get("/router-registry", (req, res) => {
244
+ res.send(this.#registry);
245
+ });
224
246
 
225
- router.get('/registry', (req, res) => {
226
- res.send(registry.getEntries())
227
- })
247
+ router.get("/registry", (req, res) => {
248
+ res.send(registry.getEntries());
249
+ });
228
250
 
229
- router.get('/stats', (req, res) => {
230
- res.send(stats)
231
- })
251
+ router.get("/stats", (req, res) => {
252
+ res.send(stats);
253
+ });
232
254
 
233
- router.get('/info', (req, res) => {
255
+ router.get("/info", (req, res) => {
234
256
  res.send({
235
257
  geonix: GeonixVersion,
236
258
  node: {
@@ -242,39 +264,35 @@ export class Gateway {
242
264
  mem: process.memoryUsage(),
243
265
  rss: process.memoryUsage.rss(),
244
266
  cpu: process.cpuUsage()
245
- })
246
- })
247
-
248
- router.get('/rebuild', (req, res) => {
249
- this.#rebuildRouter = true
250
- res.send({ result: 'ok' })
251
- })
267
+ });
268
+ });
252
269
 
253
- router.use('/', express.static('src/status'))
254
- router.all('*', (req, res) => {
255
- res.status(404).send({ error: 404 })
256
- })
270
+ router.all("*", (req, res) => {
271
+ res.status(404).send({ error: 404 });
272
+ });
257
273
 
258
- return router
274
+ return router;
259
275
  }
260
276
 
261
277
  async #proxyHttpOverNats(streamId, entry, client) {
262
- if (await Request(entry.n, 'SYS_createConnection', [streamId])) {
263
- const ingress = await connection.subscribe(`gx2.stream.${streamId}.a`)
264
-
265
- client.on('data', (chunk) => connection.publishRaw(`gx2.stream.${streamId}.b`, chunk))
266
- client.on('close', () => {
267
- connection.unsubscribe(ingress)
268
- connection.publish(`gx2.stream.${streamId}.c`, Buffer.from('end'))
269
- })
270
- client.on('error', (error) => { })
278
+ if (await Request(entry.n, "SYS_createConnection", [streamId])) {
279
+ const ingress = await connection.subscribe(`gx2.stream.${streamId}.a`);
280
+
281
+ client.on("data", (chunk) => connection.publishRaw(`gx2.stream.${streamId}.b`, chunk));
282
+ client.on("close", () => {
283
+ connection.unsubscribe(ingress);
284
+ connection.publish(`gx2.stream.${streamId}.c`, Buffer.from("end"));
285
+ });
286
+ client.on("error", (_error) => {
287
+ // silently ignore errors
288
+ });
271
289
 
272
290
  const dataLoop = async () => {
273
291
  for await (const event of ingress)
274
- client.write(event.data)
275
- }
292
+ client.write(event.data);
293
+ };
276
294
 
277
- dataLoop()
295
+ dataLoop();
278
296
  }
279
297
  }
280
298
 
@@ -291,45 +309,44 @@ export class Gateway {
291
309
  headers: {
292
310
  ...req.headers
293
311
  }
294
- })
312
+ });
295
313
 
296
- backend.on('open', () => {
297
- backend.on('message', (data, isBinary) => inbound.send(isBinary ? data : data.toString()))
298
- inbound.on('message', data => backend.send(data))
314
+ backend.on("open", () => {
315
+ backend.on("message", (data, isBinary) => inbound.send(isBinary ? data : data.toString()));
316
+ inbound.on("message", data => backend.send(data));
299
317
 
300
- backend.on('close', () => inbound.close())
301
- inbound.on('close', () => backend.close())
302
- })
318
+ backend.on("close", () => inbound.close());
319
+ inbound.on("close", () => backend.close());
320
+ });
303
321
  } catch (e) {
304
- console.error(e)
322
+ console.error(e);
305
323
  }
306
324
  }
307
325
 
308
-
309
326
  async #buildRouter() {
310
327
  if (this.#buildRouterRunning)
311
- return
328
+ return;
312
329
 
313
- console.debug('gateway.buildRouter')
330
+ console.debug("gateway.buildRouter");
314
331
 
315
- this.#rebuildRouter = false
316
- this.#buildRouterRunning = true
332
+ this.#rebuildRouter = false;
333
+ this.#buildRouterRunning = true;
317
334
 
318
335
  try {
319
- const router = Router()
320
- const entries = Object.values(this.#registry)
336
+ const router = Router();
337
+ const entries = Object.values(this.#registry);
321
338
 
322
- const endpoints = []
339
+ const endpoints = [];
323
340
 
324
341
  for (let { entry, backend } of entries) {
325
342
  // generate global endpoint list
326
343
  for (let e of entry.m) {
327
344
  if (endpointMatcher.test(e)) {
328
- const endpoint = endpointMatcher.exec(e).groups
345
+ const endpoint = endpointMatcher.exec(e).groups;
329
346
  let options = {
330
347
  order: 100,
331
348
  ...(endpoint.options ? querystring.parse(endpoint.options) : {})
332
- }
349
+ };
333
350
 
334
351
  try {
335
352
  endpoints.push({
@@ -338,67 +355,66 @@ export class Gateway {
338
355
  options,
339
356
  endpoint,
340
357
  backend: [backend]
341
- })
358
+ });
342
359
  } catch (e) {
343
- console.error('gateway.buildRouter.error:', entry)
344
- console.error('gateway.buildRouter.error:', e)
360
+ console.error("gateway.buildRouter.error:", entry);
361
+ console.error("gateway.buildRouter.error:", e);
345
362
  }
346
363
  }
347
364
  }
348
365
  }
349
366
 
350
367
  // handle duplicates (round-robin)
351
- let index = endpoints.length
368
+ let index = endpoints.length;
352
369
  while (index--) {
353
- const version = endpoints[index].version
354
- const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`
370
+ const version = endpoints[index].version;
371
+ const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`;
355
372
 
356
373
  for (let n = 0; n < index; n++)
357
374
  if (`${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url && endpoints[n].version === version) {
358
- endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend)
359
- endpoints.splice(index, 1)
360
- break
375
+ endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
376
+ endpoints.splice(index, 1);
377
+ break;
361
378
  }
362
379
  }
363
380
 
364
381
  // sort endpoints by order, if there is one
365
- endpoints.sort((a, b) => semver.rcompare(a.version, b.version))
366
- endpoints.sort((a, b) => parseInt(a.options.order) - parseInt(b.options.order))
382
+ endpoints.sort((a, b) => semver.rcompare(a.version, b.version));
383
+ endpoints.sort((a, b) => parseInt(a.options.order) - parseInt(b.options.order));
367
384
 
368
- this.#endpoints = endpoints
385
+ this.#endpoints = endpoints;
369
386
 
370
387
  // build the router
371
388
  for (let endpoint of endpoints) {
372
- let { verb, url: uri } = endpoint.endpoint
373
- let backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length]
389
+ let { verb, url: uri } = endpoint.endpoint;
390
+ let backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
374
391
 
375
- verb = verb.toLowerCase()
392
+ verb = verb.toLowerCase();
376
393
 
377
- if (verb === 'ws') {
394
+ if (verb === "ws") {
378
395
  router.ws(uri, (ws, req) => {
379
- const url = req.originalUrl.replace(/\/\.websocket$/, '')
396
+ const url = req.originalUrl.replace(/\/\.websocket$/, "");
380
397
 
381
- console.debug(`proxy.web.ws.to:`, backend + req.originalUrl)
382
- this.#proxyWebsocketOverNats(`ws://${backend}${url}`, ws, req)
383
- })
398
+ console.debug("proxy.web.ws.to:", backend + req.originalUrl);
399
+ this.#proxyWebsocketOverNats(`ws://${backend}${url}`, ws, req);
400
+ });
384
401
  } else
385
- router[verb](uri, async (req, res, next) => {
386
- stats.proxied++
387
- backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length]
402
+ router[verb](uri, async (req, res, _next) => {
403
+ stats.proxied++;
404
+ backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
388
405
 
389
406
  try {
390
- console.debug(`proxy.web.to:`, backend + req.originalUrl)
391
- await proxyHttp(`http://${backend}`, req, res)
407
+ console.debug("proxy.web.to:", backend + req.originalUrl);
408
+ await proxyHttp(`http://${backend}`, req, res);
392
409
  } catch (e) {
393
- console.error('proxy.web.error:', e)
394
- } finally {
410
+ console.error("proxy.web.error:", e);
395
411
  }
396
- })
412
+ });
397
413
  }
398
414
 
399
- this.#router = router
415
+ this.#router = router;
400
416
  } finally {
401
- this.#buildRouterRunning = false
417
+ this.#buildRouterRunning = false;
402
418
  }
403
419
  }
404
420