geonix 1.12.3 → 1.20.1

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