geonix 1.20.1 → 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 CHANGED
@@ -14,7 +14,8 @@ export default [
14
14
  }
15
15
  },
16
16
  rules: {
17
- // "no-console": "error",
17
+ "curly": "error",
18
+ "no-console": "error",
18
19
  semi: "error",
19
20
  "no-unused-vars": ["error", { argsIgnorePattern: "^_.*" }],
20
21
  "no-constant-condition": ["error", { checkLoops: false }],
package/index.d.ts CHANGED
@@ -65,6 +65,14 @@ export class Gateway {
65
65
  constructor(opts: any);
66
66
  #private;
67
67
  }
68
+ export class Logger {
69
+ constructor(options: any);
70
+ info(...args: any[]): void;
71
+ error(...args: any[]): void;
72
+ debug(...args: any[]): void;
73
+ #private;
74
+ }
75
+ export const logger: Logger;
68
76
  export const registry: Registry;
69
77
  /**
70
78
  * Registry maintains a local list of available services and their versions.
@@ -138,6 +146,17 @@ export class Service {
138
146
  $getServiceInfo(): {};
139
147
  #private;
140
148
  }
149
+ export type ServiceOptions = {
150
+ middleware: {
151
+ json: boolean;
152
+ raw: boolean;
153
+ cookies: boolean;
154
+ };
155
+ /**
156
+ * Enable full beacon
157
+ */
158
+ fullBeacon: boolean;
159
+ };
141
160
  /**
142
161
  * Converts data to stream
143
162
  *
@@ -151,6 +170,7 @@ export function getReadable(object: any): Promise<any>;
151
170
  export function streamToBuffer(object: any): Promise<any>;
152
171
  export function streamToString(object: any): Promise<any>;
153
172
  export const stats: {};
173
+ export const activeStreams: {};
154
174
  /**
155
175
  * Parse nats:// URL
156
176
  * @param {string} url
@@ -158,6 +178,7 @@ export const stats: {};
158
178
  */
159
179
  export function parseURL(url: string): any;
160
180
  export function getFirstItemFromAsyncIterable(asyncIterable: any): Promise<any>;
181
+ export function getNetworkAddresses(): any[];
161
182
  export function sleep(delay: number): Promise<any>;
162
183
  export function picoid(size?: number): any;
163
184
  export function hash(data: string | Buffer): any;
@@ -176,7 +197,6 @@ export function ServeStatic(root: any, options?: {}): any;
176
197
  export const webserver: WebServer;
177
198
  declare class WebServer {
178
199
  start(): Promise<void>;
179
- getAddresses(): any[];
180
200
  getPort(): any;
181
201
  router(): any;
182
202
  waitUntilReady(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geonix",
3
- "version": "1.20.1",
3
+ "version": "1.20.2",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "scripts": {
11
11
  "test": "echo \"Error: no test specified\" && exit 1",
12
12
  "build": "npx tsc && cat build/* > index.d.ts && rm -rf build",
13
+ "lint": "npx eslint src",
13
14
  "deploy": "npm run build && npm publish"
14
15
  },
15
16
  "author": "Davor Tarandek <dtarandek@tria.hr>",
package/src/Connection.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { connect, JSONCodec } from "nats";
2
- import { parseURL, picoid, sleep } from "./Util.js";
2
+ import { getFirstItemFromAsyncIterable, parseURL, picoid, sleep } from "./Util.js";
3
3
  import { Stream } from "./Stream.js";
4
4
  import { webserver } from "./WebServer.js";
5
+ import { logger } from "./Logger.js";
5
6
 
6
7
  // -------------------------------------------------------------------------------------------------
7
8
  const CONNECTION_TIMEOUT = 10000;
@@ -9,6 +10,16 @@ const CONNECTION_TIMEOUT = 10000;
9
10
  const defaultRequestOptions = {
10
11
  timeout: 300000
11
12
  };
13
+
14
+ const defaultConnectionOptions = {
15
+ timeout: CONNECTION_TIMEOUT,
16
+ reconnect: true,
17
+ debug: process.env.TRANSPORT_DEBUG === "true",
18
+ maxReconnectAttempts: 30,
19
+ pingInterval: 30000,
20
+ waitOnFirstConnect: true,
21
+ connections: 1
22
+ };
12
23
  // -------------------------------------------------------------------------------------------------
13
24
 
14
25
  /**
@@ -36,17 +47,8 @@ class Connection {
36
47
  * @param {string} transport
37
48
  */
38
49
  async start(transport = process.env.TRANSPORT || "nats://localhost") {
39
- const defaults = {
40
- timeout: CONNECTION_TIMEOUT,
41
- reconnect: true,
42
- debug: process.env.TRANSPORT_DEBUG === "true",
43
- maxReconnectAttempts: 30,
44
- pingInterval: 30000,
45
- waitOnFirstConnect: true,
46
- connections: 1
47
- };
48
50
  const options = {
49
- ...defaults,
51
+ ...defaultConnectionOptions,
50
52
  ...parseURL(transport)
51
53
  };
52
54
 
@@ -54,7 +56,7 @@ class Connection {
54
56
  this.#connections.push(await connect(options));
55
57
  }
56
58
 
57
- console.log("gx.connection.connected");
59
+ logger.info("gx.connection.connected");
58
60
 
59
61
  this.#ready = true;
60
62
 
@@ -64,7 +66,7 @@ class Connection {
64
66
 
65
67
  async monitorStatus() {
66
68
  for await (const event of this.#getConnection().status()) {
67
- console.log("gx.connection.status", JSON.stringify(event));
69
+ logger.info("gx.connection.status", JSON.stringify(event));
68
70
  }
69
71
  }
70
72
 
@@ -76,13 +78,13 @@ class Connection {
76
78
  await Promise.all(this.#connections.map(connection => connection.closed()));
77
79
 
78
80
  this.#closed = true;
79
- console.log("gx.connection.closed");
81
+ logger.info("gx.connection.closed");
80
82
 
81
83
  webserver.stop();
82
84
 
83
85
  await sleep(5000);
84
86
 
85
- console.log("gx.terminate");
87
+ logger.info("gx.terminate");
86
88
  process.exit(1);
87
89
  }
88
90
 
@@ -90,8 +92,9 @@ class Connection {
90
92
  * Wait for the connection to be fully established
91
93
  */
92
94
  async waitUntilReady() {
93
- while (!this.#ready)
95
+ while (!this.#ready) {
94
96
  await sleep(100);
97
+ }
95
98
  }
96
99
 
97
100
  /**
@@ -102,14 +105,16 @@ class Connection {
102
105
  * @returns void
103
106
  */
104
107
  async publish(subject, json) {
105
- if (this.#draining || this.#closed)
108
+ if (this.#draining || this.#closed) {
106
109
  return;
110
+ }
107
111
 
108
112
  let payload = codec.encode(json);
109
113
 
110
114
  // if payload is too big, convert it to Stream
111
- if (payload.length > this.getMaxPayloadSize())
115
+ if (payload.length > this.getMaxPayloadSize()) {
112
116
  payload = codec.encode(Stream(JSON.stringify(json)));
117
+ }
113
118
 
114
119
  await this.#getConnection().publish(subject, payload);
115
120
  }
@@ -122,8 +127,9 @@ class Connection {
122
127
  * @returns void
123
128
  */
124
129
  async publishRaw(subject, data) {
125
- if (this.#draining || this.#closed)
130
+ if (this.#draining || this.#closed) {
126
131
  return;
132
+ }
127
133
 
128
134
  await this.#getConnection().publish(subject, data);
129
135
  }
@@ -147,16 +153,17 @@ class Connection {
147
153
  let payload = codec.encode({ $r: respondTo, p: json });
148
154
 
149
155
  // if payload is too big, convert it to Stream
150
- if (payload.length > this.getMaxPayloadSize())
156
+ if (payload.length > this.getMaxPayloadSize()) {
151
157
  payload = codec.encode(Stream(JSON.stringify({ $r: respondTo, p: json })));
158
+ }
152
159
 
153
160
  const nc = this.#getConnection();
154
161
  let response = await nc.subscribe(respondTo, { max: 1, ...options });
155
162
 
156
163
  await nc.publish(subject, payload);
157
164
 
158
- for await (let event of response)
159
- return codec.decode(event.data);
165
+ const event = await getFirstItemFromAsyncIterable(response);
166
+ return codec.decode(event.data);
160
167
  }
161
168
 
162
169
  async subscribe(subject, options) {
@@ -194,8 +201,9 @@ export const connection = new Connection();
194
201
  connection.start();
195
202
 
196
203
  export const stopConnection = () => {
197
- if (!connection)
204
+ if (!connection) {
198
205
  return;
206
+ }
199
207
 
200
208
  connection.drain();
201
209
  };
package/src/Gateway.js CHANGED
@@ -8,14 +8,15 @@ import expressWs from "express-ws";
8
8
  import querystring from "querystring";
9
9
  import semver from "semver";
10
10
  import { WebSocket } from "ws";
11
+ import { logger } from "./Logger.js";
11
12
 
12
13
  const raw = express.raw({ limit: "100mb" });
13
14
 
14
15
  const DEBUG_ENDPOINT = "/lZ6jD2eC3iP0zB3jJ1yJ9pM8gG3yI3vS";
15
16
  const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
16
17
 
17
- const logger = (req, res, next) => {
18
- console.info(`HTTP ${req.method} ${req.url}`);
18
+ const requestLogger = (req, res, next) => {
19
+ logger.info(`HTTP ${req.method} ${req.url}`);
19
20
 
20
21
  next();
21
22
  };
@@ -63,10 +64,10 @@ export class Gateway {
63
64
  this.#port = process.env.PORT || port;
64
65
  this.#api.listen(this.#port);
65
66
 
66
- console.debug(`geonix.gateway: listening on http://0.0.0.0:${this.#port}`);
67
+ logger.debug(`geonix.gateway: listening on http://0.0.0.0:${this.#port}`);
67
68
 
68
69
  // logging
69
- this.#api.use(logger);
70
+ this.#api.use(requestLogger);
70
71
 
71
72
  // cors
72
73
  this.#api.use((req, res, next) => {
@@ -86,8 +87,9 @@ export class Gateway {
86
87
  });
87
88
 
88
89
  // debug router (only available in non-production environments)
89
- if (process.env.NODE_ENV !== "production")
90
+ if (process.env.NODE_ENV !== "production") {
90
91
  this.#api.use(DEBUG_ENDPOINT, this.#debugRouter());
92
+ }
91
93
 
92
94
  this.#api.use((req, res, next) => {
93
95
  if (this.#opts.beforeRequest) {
@@ -100,10 +102,8 @@ export class Gateway {
100
102
  this.#api.use(raw, (req, res, next) => {
101
103
  stats.requests++;
102
104
 
103
- if (this.#router)
104
- this.#router(req, res, next);
105
- else
106
- next();
105
+ if (this.#router) { this.#router(req, res, next); }
106
+ else { next(); }
107
107
  });
108
108
 
109
109
  this.#api.use((req, res, next) => {
@@ -126,8 +126,9 @@ export class Gateway {
126
126
  });
127
127
 
128
128
  setInterval(() => {
129
- if (this.#rebuildRouter)
129
+ if (this.#rebuildRouter) {
130
130
  this.#buildRouter();
131
+ }
131
132
  }, 1000);
132
133
 
133
134
  while (true) {
@@ -143,12 +144,12 @@ export class Gateway {
143
144
  break;
144
145
  }
145
146
  } catch (e) {
146
- console.error(e);
147
+ logger.error(e);
147
148
  }
148
149
  }
149
150
 
150
151
  // terminate process
151
- console.debug("geonix.gateway: stopped");
152
+ logger.debug("geonix.gateway: stopped");
152
153
  process.exit(0);
153
154
  }
154
155
 
@@ -156,15 +157,14 @@ export class Gateway {
156
157
  let entries = Object.values(registry.getEntries());
157
158
 
158
159
  const processEntry = async (entry) => {
159
- if (this.#registry[entry.i] !== undefined)
160
- return false;
160
+ if (this.#registry[entry.i] !== undefined) { return false; }
161
161
 
162
- console.log(`gateway.onServiceAdded: ${entry.n}@${entry.v} (#${entry.i})`);
162
+ logger.info(`gateway.onServiceAdded: ${entry.n}@${entry.v} (#${entry.i})`);
163
163
 
164
164
  // figure out if endpoints is reachable via direct http call
165
165
  let backend;
166
166
  if (entry.a) {
167
- for (let address of entry.a)
167
+ for (let address of entry.a) {
168
168
  try {
169
169
  const ac = new AbortController();
170
170
  const timeout = setTimeout(() => ac.abort(), 500);
@@ -172,12 +172,13 @@ export class Gateway {
172
172
  clearTimeout(timeout);
173
173
  if (result.status === "healthy" && result.services?.includes(entry.n)) {
174
174
  backend = address;
175
- console.log(`${entry.n}@${entry.v} (#${entry.i}) directly reachable @ ${address}`);
175
+ logger.info(`${entry.n}@${entry.v} (#${entry.i}) directly reachable @ ${address}`);
176
176
  break;
177
177
  }
178
178
  } catch {
179
179
  // silently ignore errors
180
180
  }
181
+ }
181
182
  }
182
183
 
183
184
  let proxy;
@@ -190,7 +191,7 @@ export class Gateway {
190
191
  try {
191
192
  await this.#proxyHttpOverNats(streamId, entry, client);
192
193
  } catch (e) {
193
- console.error("nats.proxy.error", e);
194
+ logger.error("nats.proxy.error", e);
194
195
  client.destroy();
195
196
  }
196
197
  }, 50000, 10000);
@@ -204,8 +205,9 @@ export class Gateway {
204
205
 
205
206
  entries = (await Promise.all(entries.map(processEntry))).filter(result => result === true);
206
207
 
207
- if (entries.length > 0)
208
+ if (entries.length > 0) {
208
209
  this.#rebuildRouter = true;
210
+ }
209
211
  }
210
212
 
211
213
  async #handleRemovedServices() {
@@ -215,7 +217,7 @@ export class Gateway {
215
217
  for (let { entry, proxy } of localEntries) {
216
218
  if (registryEntries[entry.i] === undefined) {
217
219
  proxy?.server?.close();
218
- console.log(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`);
220
+ logger.info(`gateway.onServiceRemoved: ${entry.n}@${entry.v}`);
219
221
  delete this.#registry[entry.i];
220
222
 
221
223
  this.#rebuildRouter = true;
@@ -290,8 +292,7 @@ export class Gateway {
290
292
  });
291
293
 
292
294
  const dataLoop = async () => {
293
- for await (const event of ingress)
294
- client.write(event.data);
295
+ for await (const event of ingress) { client.write(event.data); }
295
296
  };
296
297
 
297
298
  dataLoop();
@@ -321,15 +322,16 @@ export class Gateway {
321
322
  inbound.on("close", () => backend.close());
322
323
  });
323
324
  } catch (e) {
324
- console.error(e);
325
+ logger.error(e);
325
326
  }
326
327
  }
327
328
 
328
329
  async #buildRouter() {
329
- if (this.#buildRouterRunning)
330
+ if (this.#buildRouterRunning) {
330
331
  return;
332
+ }
331
333
 
332
- console.debug("gateway.buildRouter");
334
+ logger.debug("gateway.buildRouter");
333
335
 
334
336
  this.#rebuildRouter = false;
335
337
  this.#buildRouterRunning = true;
@@ -359,8 +361,8 @@ export class Gateway {
359
361
  backend: [backend]
360
362
  });
361
363
  } catch (e) {
362
- console.error("gateway.buildRouter.error:", entry);
363
- console.error("gateway.buildRouter.error:", e);
364
+ logger.error("gateway.buildRouter.error:", entry);
365
+ logger.error("gateway.buildRouter.error:", e);
364
366
  }
365
367
  }
366
368
  }
@@ -372,12 +374,13 @@ export class Gateway {
372
374
  const version = endpoints[index].version;
373
375
  const url = `${endpoints[index].endpoint.verb} ${endpoints[index].endpoint.url}`;
374
376
 
375
- for (let n = 0; n < index; n++)
377
+ for (let n = 0; n < index; n++) {
376
378
  if (`${endpoints[n].endpoint.verb} ${endpoints[n].endpoint.url}` === url && endpoints[n].version === version) {
377
379
  endpoints[n].backend = endpoints[n].backend.concat(endpoints[index].backend);
378
380
  endpoints.splice(index, 1);
379
381
  break;
380
382
  }
383
+ }
381
384
  }
382
385
 
383
386
  // sort endpoints by order, if there is one
@@ -397,21 +400,22 @@ export class Gateway {
397
400
  router.ws(uri, (ws, req) => {
398
401
  const url = req.originalUrl.replace(/\/\.websocket$/, "");
399
402
 
400
- console.debug("proxy.web.ws.to:", backend + req.originalUrl);
403
+ logger.debug("proxy.web.ws.to:", backend + req.originalUrl);
401
404
  this.#proxyWebsocketOverNats(`ws://${backend}${url}`, ws, req);
402
405
  });
403
- } else
406
+ } else {
404
407
  router[verb](uri, async (req, res, _next) => {
405
408
  stats.proxied++;
406
409
  backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
407
410
 
408
411
  try {
409
- console.debug("proxy.web.to:", backend + req.originalUrl);
412
+ logger.debug("proxy.web.to:", backend + req.originalUrl);
410
413
  await proxyHttp(`http://${backend}`, req, res);
411
414
  } catch (e) {
412
- console.error("proxy.web.error:", e);
415
+ logger.error("proxy.web.error:", e);
413
416
  }
414
417
  });
418
+ }
415
419
  }
416
420
 
417
421
  this.#router = router;
package/src/Logger.js ADDED
@@ -0,0 +1,34 @@
1
+ const defaultLoggerOptions = {
2
+ timestamp: true
3
+ };
4
+
5
+ export class Logger {
6
+
7
+ #options = defaultLoggerOptions;
8
+
9
+ constructor(options) {
10
+ this.#options = { ...this.#options, ...options };
11
+ }
12
+
13
+ #log(...args) {
14
+ const ts = this.#options.timestamp ? new Date().toISOString() : undefined;
15
+
16
+ // eslint-disable-next-line no-console
17
+ console.log(...[ts, ...args].filter($ => $));
18
+ }
19
+
20
+ info(...args) {
21
+ this.#log("INF", ...args);
22
+ }
23
+
24
+ error(...args) {
25
+ this.#log("ERR", ...args);
26
+ }
27
+
28
+ debug(...args) {
29
+ this.#log("DBG", ...args);
30
+ }
31
+
32
+ }
33
+
34
+ export const logger = new Logger();
package/src/Registry.js CHANGED
@@ -44,8 +44,9 @@ class Registry extends EventEmitter {
44
44
  timeout: Date.now() + REGISTRY_ENTRY_TIMEOUT
45
45
  };
46
46
 
47
- if (!exists)
47
+ if (!exists) {
48
48
  this.emit("added", this.#registry[data.i]);
49
+ }
49
50
  }
50
51
  }
51
52
 
@@ -83,14 +84,16 @@ class Registry extends EventEmitter {
83
84
  let matchVersion = version ? semver.satisfies(entry.v, version) : true;
84
85
  let matchId = id ? entry.id === id : true;
85
86
 
86
- if (matchName && matchVersion && matchId)
87
+ if (matchName && matchVersion && matchId) {
87
88
  matches.push(entry);
89
+ }
88
90
  }
89
91
 
90
92
  if (matches.length > 0) {
91
93
  // return instance id in case of id matching
92
- if (id)
94
+ if (id) {
93
95
  return matches[0].i;
96
+ }
94
97
 
95
98
  // sort matched services in the registry by version
96
99
  matches.sort((a, b) => semver.rcompare(a.v, b.v));
package/src/Request.js CHANGED
@@ -5,6 +5,7 @@ import { hash, sleep } from "./Util.js";
5
5
  import { RequestOptionsClass } from "./RequestOptions.js";
6
6
  import { isStream, streamToString } from "./Stream.js";
7
7
  import { inspect } from "node:util";
8
+ import { logger } from "./Logger.js";
8
9
 
9
10
  const REGISTRY_TIMEOUT = 300000;
10
11
 
@@ -44,8 +45,9 @@ function getOriginator() {
44
45
  for (const item of stack) {
45
46
  const typeName = item.getTypeName();
46
47
 
47
- if (Service.serviceClasses.includes(typeName))
48
+ if (Service.serviceClasses.includes(typeName)) {
48
49
  return `${typeName}.${item.getMethodName()}`;
50
+ }
49
51
  }
50
52
  }
51
53
 
@@ -64,8 +66,9 @@ export async function Request(service, method, args, context, options) {
64
66
  const { name, version, id } = match.groups;
65
67
 
66
68
  // allow passing RequestOptions as first arg
67
- if (args?.length > 0 && args[0] instanceof RequestOptionsClass)
69
+ if (args?.length > 0 && args[0] instanceof RequestOptionsClass) {
68
70
  options = (args.shift())?.options;
71
+ }
69
72
 
70
73
  let identifier = null;
71
74
 
@@ -75,8 +78,9 @@ export async function Request(service, method, args, context, options) {
75
78
  let retries = Math.floor(registryTimeout / delay);
76
79
  while (identifier == null && retries-- > 0) {
77
80
  identifier = registry.getIdentifier(name, version, id);
78
- if (!identifier)
81
+ if (!identifier) {
79
82
  await sleep(delay);
83
+ }
80
84
  }
81
85
 
82
86
  return directRequest(identifier, method, args, context, options, service);
@@ -108,10 +112,11 @@ export async function directRequest(identifier, method, args, context, options,
108
112
  options);
109
113
 
110
114
  // automatically process streamed response
111
- if (isStream(response))
115
+ if (isStream(response)) {
112
116
  response = JSON.parse(await streamToString(response));
117
+ }
113
118
  } catch (e) {
114
- console.debug("GxError: directRequest", inspect({
119
+ logger.debug("GxError: directRequest", inspect({
115
120
  originator, service: service ?? identifier, method, args, context, options,
116
121
  error: e, duration: Date.now() - requestBegin
117
122
  }));
@@ -119,12 +124,14 @@ export async function directRequest(identifier, method, args, context, options,
119
124
  throw e;
120
125
  }
121
126
 
122
- if (!response)
127
+ if (!response) {
123
128
  throw Error("Request: invalid response");
129
+ }
124
130
 
125
131
  // got error?
126
- if (response.e)
132
+ if (response.e) {
127
133
  throw Error(`Request: remote error: ${response.e}`);
134
+ }
128
135
 
129
136
  return response.r;
130
137
  }
@@ -147,10 +154,12 @@ export async function Publish(subject, payload) {
147
154
  * @returns
148
155
  */
149
156
  export async function Subscribe(subject, callback) {
150
- if (typeof callback !== "function")
157
+ if (typeof callback !== "function") {
151
158
  return;
159
+ }
152
160
 
153
161
  const subscription = await connection.subscribe(`gx.sub.${subject}`);
154
- for await (const event of subscription)
162
+ for await (const event of subscription) {
155
163
  callback(event.data);
164
+ }
156
165
  }
@@ -1,5 +1,6 @@
1
1
  export class RequestOptionsClass {
2
2
  options;
3
+
3
4
  constructor(options) {
4
5
  this.options = options;
5
6
  }
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).filter(isEndpointFilter).concat(Object.getOwnPropertyNames(this.constructor.prototype));
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++) ia = serviceSource[line].includes(a) ? line : ia;
48
- for (let line = 0; line < serviceSource.length; line++) ib = serviceSource[line].includes(b) ? line : ib;
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: webserver.getAddresses().map(address => `${address}:${webserver.getPort()}`)
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
- console.log("gx.service.start", this.#me.n, this.#me.v);
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
- connection.publish("gx2.beacon", { i: this.#me.i });
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
- router.use(json, raw, cookieParser());
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
- const controlHandler = async () => {
40
+ // NATS handler
41
+ (async () => {
35
42
  const control = await connection.subscribe(`gx2.stream.${id}.a`, { max: 1 });
36
43
 
37
- for await (const event of control)
38
- if (event.data.length === 0) {
39
- readable.on("data", chunk => connection.publishRaw(`gx2.stream.${id}.b`, chunk));
40
- readable.on("close", () => {
41
- connection.publishRaw(`gx2.stream.${id}.b`);
42
- stats[tag]--;
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
- controlHandler();
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 { $: "stream", id };
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
- console.error("Stream.getReadable.dataHandler.error:", e);
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
- // console.log(port, '=', result)
85
- if (result) return result;
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
- console.log(path.join(absoluteRoot, "index.html"));
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 port, server;
58
+ let srv;
56
59
  if (process.env.LOCAL_PORT) {
57
- ({ server, port } = await createServerAtPort(process.env.LOCAL_PORT, http, this.#app));
60
+ srv = await createServerAtPort(process.env.LOCAL_PORT, http, this.#app);
58
61
  } else {
59
- ({ server, port } = await createServerAtFreePort(http, this.#app));
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
- console.log(`gx.webserver.start: listening on http://127.0.0.1:${this.#port}`);
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
- console.log("gx.webserver.stop");
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
- 'GET /'(req, res) {
6
- res.send('Hello World')
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('X-Test', 'Test')
13
+ res.set("X-Test", "Test");
14
14
  }
15
- })
15
+ });
package/test/stream.js CHANGED
@@ -1,38 +1,43 @@
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'
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('sha512').update(data).digest('base64')
10
+ const hash = data => createHash("sha512").update(data).digest("base64");
10
11
 
11
- const PAYLOAD_SIZE = 1024 * 1024 * 1024
12
- const TEMP_FILE = '/tmp/geonix.stream_test'
12
+ const PAYLOAD_SIZE = 1024 * 1024 * 1024;
13
+ const TEMP_FILE = "/tmp/geonix.stream_test";
13
14
 
14
- console.time('test')
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(Stream(payload))
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('MATCH')
33
+ console.log("MATCH");
29
34
  } else {
30
- console.error('Destination does not match the source!')
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('test')
42
+ console.timeEnd("test");
38
43
  }