geonix 1.12.3 → 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,250 +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
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
165
+ let backend;
158
166
  for (let address of entry.a)
159
167
  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
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;
168
176
  }
169
- } catch (e) {
177
+ } catch {
170
178
  // silently ignore errors
171
179
  }
172
180
 
173
- let proxy
181
+ let proxy;
174
182
  if (!backend) {
175
183
  // create proxy over nats
176
184
  proxy = await createTCPServer(async (client) => {
177
- const streamId = picoid()
178
- stats.proxied_over_nats++
185
+ const streamId = picoid();
186
+ stats.proxied_over_nats++;
179
187
 
180
188
  try {
181
- await this.#proxyHttpOverNats(streamId, entry, client)
189
+ await this.#proxyHttpOverNats(streamId, entry, client);
182
190
  } catch (e) {
183
- console.error('nats.proxy.error', e)
184
- client.destroy()
191
+ console.error("nats.proxy.error", e);
192
+ client.destroy();
185
193
  }
186
- }, 50000, 10000)
187
- backend = `127.0.0.1:${proxy.port}`
194
+ }, 50000, 10000);
195
+ backend = `127.0.0.1:${proxy.port}`;
188
196
  }
189
197
 
190
- this.#registry[entry.i] = { entry, proxy, backend }
198
+ this.#registry[entry.i] = { entry, proxy, backend };
191
199
 
192
- return true
193
- }
200
+ return true;
201
+ };
194
202
 
195
- entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true)
203
+ entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
196
204
 
197
205
  if (entries.length > 0)
198
- this.#rebuildRouter = true
206
+ this.#rebuildRouter = true;
199
207
  }
200
208
 
201
209
  async #handleRemovedServices() {
202
- const localEntries = Object.values(this.#registry)
203
- const registryEntries = registry.getEntries()
210
+ const localEntries = Object.values(this.#registry);
211
+ const registryEntries = registry.getEntries();
204
212
 
205
213
  for (let { entry, proxy } of localEntries) {
206
214
  if (registryEntries[entry.i] === undefined) {
207
- proxy?.server?.close()
208
- console.log(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`)
209
- delete this.#registry[entry.i]
215
+ proxy?.server?.close();
216
+ console.log(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`);
217
+ delete this.#registry[entry.i];
210
218
 
211
- this.#rebuildRouter = true
219
+ this.#rebuildRouter = true;
212
220
  }
213
221
  }
214
222
  }
215
223
 
216
224
  #debugRouter() {
217
- const router = Router()
225
+ const router = Router();
218
226
 
219
227
  router.use((req, res, next) => {
220
- stats.debug_requests++
228
+ stats.debug_requests++;
221
229
 
222
- next()
223
- })
230
+ next();
231
+ });
224
232
 
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
- })
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
+ });
230
238
 
231
- router.get('/endpoints', (req, res) => {
232
- res.send(this.#endpoints)
233
- })
239
+ router.get("/endpoints", (req, res) => {
240
+ res.send(this.#endpoints);
241
+ });
234
242
 
235
- router.get('/router-registry', (req, res) => {
236
- res.send(this.#registry)
237
- })
243
+ router.get("/router-registry", (req, res) => {
244
+ res.send(this.#registry);
245
+ });
238
246
 
239
- router.get('/registry', (req, res) => {
240
- res.send(registry.getEntries())
241
- })
247
+ router.get("/registry", (req, res) => {
248
+ res.send(registry.getEntries());
249
+ });
242
250
 
243
- router.get('/stats', (req, res) => {
244
- res.send(stats)
245
- })
251
+ router.get("/stats", (req, res) => {
252
+ res.send(stats);
253
+ });
246
254
 
247
- router.get('/info', (req, res) => {
255
+ router.get("/info", (req, res) => {
248
256
  res.send({
249
257
  geonix: GeonixVersion,
250
258
  node: {
@@ -256,39 +264,35 @@ export class Gateway {
256
264
  mem: process.memoryUsage(),
257
265
  rss: process.memoryUsage.rss(),
258
266
  cpu: process.cpuUsage()
259
- })
260
- })
261
-
262
- router.get('/rebuild', (req, res) => {
263
- this.#rebuildRouter = true
264
- res.send({ result: 'ok' })
265
- })
267
+ });
268
+ });
266
269
 
267
- router.use('/', express.static('src/status'))
268
- router.all('*', (req, res) => {
269
- res.status(404).send({ error: 404 })
270
- })
270
+ router.all("*", (req, res) => {
271
+ res.status(404).send({ error: 404 });
272
+ });
271
273
 
272
- return router
274
+ return router;
273
275
  }
274
276
 
275
277
  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) => { })
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
+ });
285
289
 
286
290
  const dataLoop = async () => {
287
291
  for await (const event of ingress)
288
- client.write(event.data)
289
- }
292
+ client.write(event.data);
293
+ };
290
294
 
291
- dataLoop()
295
+ dataLoop();
292
296
  }
293
297
  }
294
298
 
@@ -305,45 +309,44 @@ export class Gateway {
305
309
  headers: {
306
310
  ...req.headers
307
311
  }
308
- })
312
+ });
309
313
 
310
- backend.on('open', () => {
311
- backend.on('message', (data, isBinary) => inbound.send(isBinary ? data : data.toString()))
312
- 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));
313
317
 
314
- backend.on('close', () => inbound.close())
315
- inbound.on('close', () => backend.close())
316
- })
318
+ backend.on("close", () => inbound.close());
319
+ inbound.on("close", () => backend.close());
320
+ });
317
321
  } catch (e) {
318
- console.error(e)
322
+ console.error(e);
319
323
  }
320
324
  }
321
325
 
322
-
323
326
  async #buildRouter() {
324
327
  if (this.#buildRouterRunning)
325
- return
328
+ return;
326
329
 
327
- console.debug('gateway.buildRouter')
330
+ console.debug("gateway.buildRouter");
328
331
 
329
- this.#rebuildRouter = false
330
- this.#buildRouterRunning = true
332
+ this.#rebuildRouter = false;
333
+ this.#buildRouterRunning = true;
331
334
 
332
335
  try {
333
- const router = Router()
334
- const entries = Object.values(this.#registry)
336
+ const router = Router();
337
+ const entries = Object.values(this.#registry);
335
338
 
336
- const endpoints = []
339
+ const endpoints = [];
337
340
 
338
341
  for (let { entry, backend } of entries) {
339
342
  // generate global endpoint list
340
343
  for (let e of entry.m) {
341
344
  if (endpointMatcher.test(e)) {
342
- const endpoint = endpointMatcher.exec(e).groups
345
+ const endpoint = endpointMatcher.exec(e).groups;
343
346
  let options = {
344
347
  order: 100,
345
348
  ...(endpoint.options ? querystring.parse(endpoint.options) : {})
346
- }
349
+ };
347
350
 
348
351
  try {
349
352
  endpoints.push({
@@ -352,67 +355,66 @@ export class Gateway {
352
355
  options,
353
356
  endpoint,
354
357
  backend: [backend]
355
- })
358
+ });
356
359
  } catch (e) {
357
- console.error('gateway.buildRouter.error:', entry)
358
- console.error('gateway.buildRouter.error:', e)
360
+ console.error("gateway.buildRouter.error:", entry);
361
+ console.error("gateway.buildRouter.error:", e);
359
362
  }
360
363
  }
361
364
  }
362
365
  }
363
366
 
364
367
  // handle duplicates (round-robin)
365
- let index = endpoints.length
368
+ let index = endpoints.length;
366
369
  while (index--) {
367
- const version = endpoints[index].version
368
- 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}`;
369
372
 
370
373
  for (let n = 0; n < index; n++)
371
374
  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
375
+ endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
376
+ endpoints.splice(index, 1);
377
+ break;
375
378
  }
376
379
  }
377
380
 
378
381
  // 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))
382
+ endpoints.sort((a, b) => semver.rcompare(a.version, b.version));
383
+ endpoints.sort((a, b) => parseInt(a.options.order) - parseInt(b.options.order));
381
384
 
382
- this.#endpoints = endpoints
385
+ this.#endpoints = endpoints;
383
386
 
384
387
  // build the router
385
388
  for (let endpoint of endpoints) {
386
- let { verb, url: uri } = endpoint.endpoint
387
- 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];
388
391
 
389
- verb = verb.toLowerCase()
392
+ verb = verb.toLowerCase();
390
393
 
391
- if (verb === 'ws') {
394
+ if (verb === "ws") {
392
395
  router.ws(uri, (ws, req) => {
393
- const url = req.originalUrl.replace(/\/\.websocket$/, '')
396
+ const url = req.originalUrl.replace(/\/\.websocket$/, "");
394
397
 
395
- console.debug(`proxy.web.ws.to:`, backend + req.originalUrl)
396
- this.#proxyWebsocketOverNats(`ws://${backend}${url}`, ws, req)
397
- })
398
+ console.debug("proxy.web.ws.to:", backend + req.originalUrl);
399
+ this.#proxyWebsocketOverNats(`ws://${backend}${url}`, ws, req);
400
+ });
398
401
  } else
399
- router[verb](uri, async (req, res, next) => {
400
- stats.proxied++
401
- 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];
402
405
 
403
406
  try {
404
- console.debug(`proxy.web.to:`, backend + req.originalUrl)
405
- await proxyHttp(`http://${backend}`, req, res)
407
+ console.debug("proxy.web.to:", backend + req.originalUrl);
408
+ await proxyHttp(`http://${backend}`, req, res);
406
409
  } catch (e) {
407
- console.error('proxy.web.error:', e)
408
- } finally {
410
+ console.error("proxy.web.error:", e);
409
411
  }
410
- })
412
+ });
411
413
  }
412
414
 
413
- this.#router = router
415
+ this.#router = router;
414
416
  } finally {
415
- this.#buildRouterRunning = false
417
+ this.#buildRouterRunning = false;
416
418
  }
417
419
  }
418
420