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/Service.js CHANGED
@@ -1,57 +1,53 @@
1
- import { codec, connection } from './Connection.js'
2
- import { picoid, sleep, hash, getSecondsSinceMidnight, OverlayObject, GeonixVersion, getNetworkAddresses } from './Util.js'
3
- import { webserver } from './WebServer.js'
4
- import { createConnection } from 'net'
5
- import { EOL } from 'os'
6
- import cookieParser from 'cookie-parser'
7
- import express from 'express'
8
- import { isStream, streamToString } from './Stream.js'
9
-
10
- const protectedMethodNames = ['constructor', 'onStart']
11
- const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|SUB|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/
12
- const isEndpointFilter = methodName => endpointMatcher.test(methodName)
13
- const ERROR_BEGIN_DELIMITER = '-'.repeat(10)
14
- const ERROR_END_DELIMITER = '-'.repeat(40)
15
-
16
- const json = express.json({ limit: '100mb' })
17
- const raw = express.raw({ type: '*/*', limit: '100mb' })
1
+ import { codec, connection } from "./Connection.js";
2
+ import { picoid, sleep, hash, getSecondsSinceMidnight, OverlayObject, GeonixVersion, getFirstItemFromAsyncIterable } from "./Util.js";
3
+ import { webserver } from "./WebServer.js";
4
+ import { createConnection } from "net";
5
+ import { EOL } from "os";
6
+ import cookieParser from "cookie-parser";
7
+ import express from "express";
8
+ import { isStream, streamToString } from "./Stream.js";
9
+
10
+ const protectedMethodNames = ["constructor", "onStart"];
11
+ const endpointMatcher = /^((?<options>.+)\|)?(?<verb>WS|SUB|GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS|ALL)\s(?<url>.*)/;
12
+ const isEndpointFilter = methodName => endpointMatcher.test(methodName);
13
+ const ERROR_BEGIN_DELIMITER = "-".repeat(10);
14
+ const ERROR_END_DELIMITER = "-".repeat(40);
15
+
16
+ const json = express.json({ limit: "100mb" });
17
+ const raw = express.raw({ type: "*/*", limit: "100mb" });
18
18
 
19
19
  export class Service {
20
20
 
21
- static serviceClasses = []
21
+ static serviceClasses = [];
22
22
 
23
23
  static start(options = {}) {
24
24
  if (!this.serviceClasses.includes(this.prototype.constructor.name))
25
- this.serviceClasses.push(this.prototype.constructor.name)
26
-
27
- const instance = new this()
28
- instance.#start(options)
29
- }
30
-
31
- static static() {
25
+ this.serviceClasses.push(this.prototype.constructor.name);
32
26
 
27
+ const instance = new this();
28
+ instance.#start(options);
33
29
  }
34
30
 
35
31
  // ---------------------------------------------------------------------------------------------
36
32
 
37
- #me = {}
38
- #options = {}
33
+ #me = {};
34
+ #options = {};
39
35
 
40
36
  async #start(options = {}) {
41
- this.#options = options
37
+ this.#options = options;
42
38
 
43
- await webserver.waitUntilReady()
44
- await connection.waitUntilReady()
39
+ await webserver.waitUntilReady();
40
+ await connection.waitUntilReady();
45
41
 
46
- const fields = Object.getOwnPropertyNames(this).filter(isEndpointFilter).concat(Object.getOwnPropertyNames(this.constructor.prototype))
42
+ const fields = Object.getOwnPropertyNames(this).filter(isEndpointFilter).concat(Object.getOwnPropertyNames(this.constructor.prototype));
47
43
 
48
44
  // preserve order of endpoints as defined in the source
49
- const serviceSource = this.constructor.toString().split('\n')
45
+ const serviceSource = this.constructor.toString().split("\n");
50
46
  fields.sort((a, b, ia = -1, ib = -1) => {
51
- for (let line = 0; line < serviceSource.length; line++) ia = serviceSource[line].includes(a) ? line : ia
52
- for (let line = 0; line < serviceSource.length; line++) ib = serviceSource[line].includes(b) ? line : ib
53
- return ia - ib
54
- })
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;
49
+ return ia - ib;
50
+ });
55
51
 
56
52
  this.#me = {
57
53
  id: options?.id,
@@ -59,33 +55,36 @@ export class Service {
59
55
  // instance
60
56
  i: picoid(),
61
57
  // name
62
- n: this.constructor.name,
58
+ n: options.name ?? this.constructor.name,
63
59
  // version
64
60
  v: process.env.VERSION || process.env.version || options?.version || this.version || `999.999.${getSecondsSinceMidnight()}`,
65
61
  // methods
66
62
  m: fields
67
63
  .filter(methodName => !protectedMethodNames.includes(methodName))
68
- .filter(methodName => !methodName.startsWith('$')),
64
+ .filter(methodName => !methodName.startsWith("$")),
65
+ // geonix version
66
+ gx: GeonixVersion,
69
67
  // IP addresses
70
- a: getNetworkAddresses().map(address => `${address}:${webserver.getPort()}`)
71
- }
68
+ a: webserver.getAddresses().map(address => `${address}:${webserver.getPort()}`)
69
+ };
72
70
 
73
71
  // check if method takes context as first argument
74
72
  for (let methodName of this.#me.m) {
75
- const method = this[methodName]
76
- method.takesContext = method.toString()?.match(/\((?<args>.*)\)/)?.groups?.args.startsWith('$')
73
+ const method = this[methodName];
74
+ method.takesContext = method.toString()?.match(/\((?<args>.*)\)/)?.groups?.args.startsWith("$");
77
75
  }
78
76
 
79
- this.#beacon()
80
- this.#queueListener()
81
- this.#directListener()
82
- this.#webserver()
77
+ this.#beacon();
78
+ this.#queueListener();
79
+ this.#directListener();
80
+ this.#webserver();
83
81
 
84
- console.log('gx.service.start', this.#me.n, this.#me.v)
82
+ console.log("gx.service.start", this.#me.n, this.#me.v);
85
83
 
86
84
  // execute onStart method, if present
87
- if (this.onStart)
88
- this.onStart()
85
+ if (this.onStart) {
86
+ this.onStart();
87
+ }
89
88
  }
90
89
 
91
90
  /**
@@ -94,8 +93,8 @@ export class Service {
94
93
  */
95
94
  async #beacon() {
96
95
  while (true) {
97
- connection.publish('gx2.beacon', this.#me)
98
- await sleep(1000)
96
+ connection.publish("gx2.beacon", { i: this.#me.i });
97
+ await sleep(1000);
99
98
  }
100
99
  }
101
100
 
@@ -103,17 +102,17 @@ export class Service {
103
102
  * Wait and respond to remote call events (queue)
104
103
  */
105
104
  async #queueListener() {
106
- const identifier = `${this.#me.n}@${this.#me.v}`
107
- const subscription = await connection.subscribe(`gx2.service.${hash(identifier)}`, { queue: identifier })
105
+ const identifier = `${this.#me.n}@${this.#me.v}`;
106
+ const subscription = await connection.subscribe(`gx2.service.${hash(identifier)}`, { queue: identifier });
108
107
 
109
108
  for await (let event of subscription) {
110
- let call = codec.decode(event.data)
109
+ let call = codec.decode(event.data);
111
110
 
112
111
  if (isStream(call))
113
- call = JSON.parse(await streamToString(call))
112
+ call = JSON.parse(await streamToString(call));
114
113
 
115
114
  if (call.$r && call.p)
116
- this.#onCall(call.p, (json) => connection.publish(call.$r, json))
115
+ this.#onCall(call.p, (json) => connection.publish(call.$r, json));
117
116
  }
118
117
  }
119
118
 
@@ -121,17 +120,17 @@ export class Service {
121
120
  * Wait and respond to remote call events (queue)
122
121
  */
123
122
  async #directListener() {
124
- const identifier = this.#me.i
125
- const subscription = await connection.subscribe(`gx2.service.${hash(identifier)}`, { queue: identifier })
123
+ const identifier = this.#me.i;
124
+ const subscription = await connection.subscribe(`gx2.service.${hash(identifier)}`, { queue: identifier });
126
125
 
127
126
  for await (let event of subscription) {
128
- let call = codec.decode(event.data)
127
+ let call = codec.decode(event.data);
129
128
 
130
129
  if (isStream(call))
131
- call = JSON.parse(await streamToString(call))
130
+ call = JSON.parse(await streamToString(call));
132
131
 
133
132
  if (call.$r && call.p)
134
- this.#onCall(call.p, (json) => connection.publish(call.$r, json))
133
+ this.#onCall(call.p, (json) => connection.publish(call.$r, json));
135
134
  }
136
135
  }
137
136
 
@@ -141,48 +140,52 @@ export class Service {
141
140
  */
142
141
  async #webserver() {
143
142
  const endpoints = this.#me.m
144
- .filter(isEndpointFilter)
143
+ .filter(isEndpointFilter);
145
144
 
146
145
  if (!endpoints || endpoints.length === 0)
147
- return
146
+ return;
148
147
 
149
- const router = webserver.router()
150
- router.use(json, raw, cookieParser())
148
+ const router = webserver.router();
149
+ router.use(json, raw, cookieParser());
151
150
  for (let endpoint of endpoints) {
152
- let { verb, url: uri } = endpointMatcher.exec(endpoint)?.groups || {}
153
- verb = verb.toLowerCase()
151
+ let { verb, url: uri } = endpointMatcher.exec(endpoint)?.groups || {};
152
+ verb = verb.toLowerCase();
154
153
 
155
- let handlers = (Array.isArray(this[endpoint]) ? this[endpoint] : [this[endpoint]])
154
+ let handlers = (Array.isArray(this[endpoint]) ? this[endpoint] : [this[endpoint]]);
156
155
 
157
- if (Array.isArray(this.#options?.handlers?.before))
158
- handlers = [...this.#options?.handlers?.before, ...handlers]
159
- if (Array.isArray(this.#options?.handlers?.after))
160
- handlers = [...handlers, ...this.#options?.handlers?.after]
156
+ const handlersBefore = this.#options?.handlers?.before ?? [];
157
+ const handlersAfter = this.#options?.handlers?.after ?? [];
158
+
159
+ handlers = [...handlersBefore, ...handlers, ...handlersAfter];
161
160
 
162
161
  for (let n = 0; n < handlers.length; n++)
163
- handlers[n] = handlers[n].bind(this)
162
+ handlers[n] = handlers[n].bind(this);
164
163
 
165
164
  switch (verb) {
166
- case 'ws':
167
- router.ws(uri, this[endpoint].bind(this))
168
- break
169
-
170
- case 'sub':
171
- const subscription = await connection.subscribe(`gx.sub.${uri}`)
172
- const processor = async () => {
173
- for await (const event of subscription)
174
- this[endpoint](event.data)
175
- }
176
- processor()
177
- break
165
+ case "ws":
166
+ router.ws(uri, this[endpoint].bind(this));
167
+ break;
168
+
169
+ case "sub":
170
+ this.#sub(uri, this[endpoint]);
171
+ break;
178
172
 
179
173
  default:
180
- router[verb](uri, ...handlers)
181
- break
174
+ router[verb](uri, ...handlers);
175
+ break;
182
176
  }
183
177
  }
184
178
  }
185
179
 
180
+ async #sub(subject, handler) {
181
+ const subscription = await connection.subscribe(`gx.sub.${subject}`);
182
+ const processor = async () => {
183
+ for await (const event of subscription)
184
+ handler(event.data);
185
+ };
186
+ processor();
187
+ }
188
+
186
189
  /**
187
190
  * Handle individual call
188
191
  * @param {Object} call
@@ -190,69 +193,69 @@ export class Service {
190
193
  * @returns
191
194
  */
192
195
  async #onCall(call, respond) {
193
- const { m: methodName, a: args, c: context, o: caller } = call
196
+ const { m: methodName, a: args, c: context, o: caller } = call;
194
197
 
195
- const method = this[methodName]
196
- let _args = args
198
+ const method = this[methodName];
199
+ let _args = args;
197
200
 
198
201
  if (!method)
199
- return respond({ e: `unknown method (${this.#me.n}.${methodName})` })
202
+ return respond({ e: `unknown method (${this.#me.n}.${methodName})` });
200
203
 
201
204
  try {
202
205
  // inject context as first argument
203
206
  if (method.takesContext)
204
- _args = [OverlayObject(context, { caller, me: this.#me }), ..._args]
207
+ _args = [OverlayObject(context, { caller, me: this.#me }), ..._args];
205
208
 
206
- respond({ r: await method.apply(this, _args) })
209
+ respond({ r: await method.apply(this, _args) });
207
210
  } catch (e) {
208
- respond({ e: `${EOL}${ERROR_BEGIN_DELIMITER} ${this.#me.n}${EOL}${e.stack}${EOL}${ERROR_END_DELIMITER}` })
211
+ respond({ e: `${EOL}${ERROR_BEGIN_DELIMITER} ${this.#me.n}${EOL}${e.stack}${EOL}${ERROR_END_DELIMITER}` });
209
212
  }
210
213
  }
211
214
 
212
- connections = new Map()
213
- async SYS_createConnection(streamId) {
214
- const ingress = await connection.subscribe(`gx2.stream.${streamId}.b`)
215
- const control = await connection.subscribe(`gx2.stream.${streamId}.c`)
215
+ connections = new Map();
216
+ async $createConnection(streamId) {
217
+ const ingress = await connection.subscribe(`gx2.stream.${streamId}.b`);
218
+ const control = await connection.subscribe(`gx2.stream.${streamId}.c`);
216
219
 
217
- const client = createConnection({ port: webserver.getPort() })
218
- client.on('data', (chunk) => connection.publishRaw(`gx2.stream.${streamId}.a`, chunk))
219
- client.on('close', () => connection.unsubscribe(ingress))
220
+ const client = createConnection({ port: webserver.getPort() });
221
+ client.on("data", (chunk) => connection.publishRaw(`gx2.stream.${streamId}.a`, chunk));
222
+ client.on("close", () => connection.unsubscribe(ingress));
220
223
 
221
224
  this.connections.set(streamId, {
222
225
  client,
223
226
  sub: ingress
224
- })
227
+ });
225
228
 
226
229
  const incomingLoop = async () => {
227
230
  for await (const event of ingress) {
228
- client.write(Buffer.from(event.data))
231
+ client.write(Buffer.from(event.data));
229
232
  }
230
- }
233
+ };
231
234
 
232
235
  const controlLoop = async () => {
233
- // eslint-disable-next-line
234
- for await (const event of control) {
235
- const _connection = this.connections.get(streamId)
236
- if (!_connection) {
237
- return
238
- }
236
+ // wait for first control message to arrive
237
+ await getFirstItemFromAsyncIterable(control);
239
238
 
240
- connection.unsubscribe(ingress)
241
- connection.unsubscribe(control)
239
+ const _connection = this.connections.get(streamId);
240
+ if (!_connection) {
241
+ return;
242
+ }
242
243
 
243
- _connection.client.destroy()
244
+ connection.unsubscribe(ingress);
245
+ connection.unsubscribe(control);
244
246
 
245
- this.connections.delete(streamId)
246
- }
247
- }
247
+ _connection.client.destroy();
248
248
 
249
- incomingLoop()
250
- controlLoop()
249
+ this.connections.delete(streamId);
250
+ };
251
251
 
252
- return true
252
+ incomingLoop();
253
+ controlLoop();
254
+
255
+ return true;
253
256
  }
254
257
 
255
- async SYS_getEnv() {
258
+ $getEnv() {
256
259
  return {
257
260
  geonix: GeonixVersion,
258
261
  node: {
@@ -264,7 +267,11 @@ export class Service {
264
267
  mem: process.memoryUsage(),
265
268
  rss: process.memoryUsage.rss(),
266
269
  cpu: process.cpuUsage()
267
- }
270
+ };
271
+ }
272
+
273
+ $getServiceInfo() {
274
+ return this.#me;
268
275
  }
269
276
 
270
277
  }
package/src/Stream.js CHANGED
@@ -1,189 +1,98 @@
1
- import { Readable } from 'stream'
2
- import { connection } from './Connection.js'
3
- import { abortableAsyncGenerator, createServerAtFreePort, getNetworkAddresses, picoid, StreamChunker } from './Util.js'
4
- import http from 'http'
5
-
6
- const CHUNK_SIZE = 1024 * 128
7
- const STREAM_TIMEOUT = 120000
8
-
9
- export const stats = {}
10
-
11
- // HTTP stream server
12
- const streams = {}
13
- const { port: streamServerPort } = await createServerAtFreePort(http, (req, res) => {
14
- const stream = streams[decodeURIComponent(req.url.substring(1))]
15
- if (stream) {
16
- stream.pipe(res)
17
- } else {
18
- res.statusCode = 404
19
- res.end()
20
- return
21
- }
22
- }, 40000)
1
+ import { Readable } from "stream";
2
+ import { connection } from "./Connection.js";
3
+ import { picoid, StreamChunker } from "./Util.js";
4
+
5
+ const CHUNK_SIZE = 1024 * 128;
6
+
7
+ export const stats = {};
23
8
 
24
9
  /**
25
- * Converts input data to stream
10
+ * Converts data to stream
26
11
  *
27
12
  * @param {*} data
28
13
  * @param {*} automated
29
14
  * @returns
30
15
  */
31
- export function Stream(data, tag = '_') {
16
+ export function Stream(data, tag = "_") {
32
17
  if (isStream(data))
33
- return data
18
+ return data;
34
19
 
35
- const id = picoid()
36
- const result = { $: 'stream', id }
37
- let readable = data
20
+ const id = picoid();
21
+ let readable = data;
38
22
 
39
23
  // convert Buffer or string to a Readable
40
24
  if (!(readable.pipe && readable.readable))
41
- readable = Readable.from(Buffer.from(data))
25
+ readable = Readable.from(Buffer.from(data));
42
26
 
43
27
  // split the stream is smaller chunks
44
- const transform = StreamChunker(Math.min(connection.getMaxPayloadSize(), CHUNK_SIZE))
45
- readable.pipe(transform)
46
- readable = transform
47
-
48
- stats[tag] = stats[tag] !== undefined ? stats[tag] + 1 : 1
49
-
50
- const acDeliveryOverNATS = new AbortController();
28
+ const transform = StreamChunker(Math.min(connection.getMaxPayloadSize(), CHUNK_SIZE));
29
+ readable.pipe(transform);
30
+ readable = transform;
51
31
 
52
- const deliverOverNATS = async () => {
53
- const control = await connection.subscribe(`gx2.stream.${id}.a`, { max: 1 })
32
+ stats[tag] = stats[tag] !== undefined ? stats[tag] + 1 : 1;
54
33
 
55
- // abort if no request to start streaming
56
- const timeout = setTimeout(() => {
57
- acDeliveryOverNATS.abort()
58
- }, STREAM_TIMEOUT)
34
+ const controlHandler = async () => {
35
+ const control = await connection.subscribe(`gx2.stream.${id}.a`, { max: 1 });
59
36
 
60
- // deliver the stream
61
- for await (const event of abortableAsyncGenerator(control, acDeliveryOverNATS.signal)) {
37
+ for await (const event of control)
62
38
  if (event.data.length === 0) {
63
- readable.on('data', chunk => connection.publishRaw(`gx2.stream.${id}.b`, chunk))
64
- readable.on('close', () => {
65
- connection.publishRaw(`gx2.stream.${id}.b`)
66
- stats[tag]--
67
- })
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
+ });
68
44
  }
69
- }
70
-
71
- // cleanup
72
- clearTimeout(timeout)
73
- await control.drain()
74
- await connection.unsubscribe(control)
75
- }
76
-
77
- const deliverOverHTTP = async () => {
78
- // stop listening for NATS request
79
- acDeliveryOverNATS.abort()
80
-
81
- // register stream as available over HTTP
82
- streams[id] = readable
45
+ };
83
46
 
84
- // cleanup
85
- readable.on('finish', () => {
86
- delete streams[id]
87
- stats[tag]--
88
- })
89
- }
47
+ controlHandler();
90
48
 
91
- deliverOverNATS()
92
-
93
- if (streamServerPort != -1) {
94
- result.a = getNetworkAddresses()
95
- result.p = streamServerPort;
96
- deliverOverHTTP()
97
- }
98
-
99
- return result
49
+ return { $: "stream", id };
100
50
  }
101
51
 
102
52
  export function isStream(object) {
103
- return object && typeof object === 'object' && object.$ === 'stream'
53
+ return object && typeof object === "object" && object.$ === "stream";
104
54
  }
105
55
 
106
- export async function getReadable(object, forceNats = false) {
56
+ export async function getReadable(object) {
107
57
  if (!isStream(object))
108
- return object
109
-
110
- if (forceNats) {
111
- return getReadableOverNATS(object);
112
- }
113
-
114
- try {
115
- return await getReadableOverHTTP(object);
116
- } catch (e) {
117
- return getReadableOverNATS(object);
118
- }
119
- }
120
-
121
- export async function getReadableOverHTTP(object) {
122
- if (!object.a) {
123
- throw new Error('Stream is not available over HTTP')
124
- }
125
-
126
- for (const address of object.a) {
127
- try {
128
- const response = await new Promise((resolve, reject) => {
129
- const request = http.request(`http://${address}:${object.p}/${object.id}`, { method: 'GET', timeout: 5000 })
130
- request.on('response', (response) => {
131
- resolve(response)
132
- })
133
-
134
- request.on('error', (e) => {
135
- reject(e)
136
- })
137
-
138
- request.end()
139
- })
140
-
141
- return response
142
- } catch (e) {
143
- console.error(`Stream.getReadableOverHTTP.error:`, e)
144
- }
145
- }
146
-
147
- throw new Error('No data')
148
- }
58
+ return object;
149
59
 
150
- export async function getReadableOverNATS(object) {
151
- const readable = new Readable({ read: () => null })
152
- const subscription = await connection.subscribe(`gx2.stream.${object.id}.b`)
60
+ const readable = new Readable({ read: () => null });
61
+ const subscription = await connection.subscribe(`gx2.stream.${object.id}.b`);
153
62
 
154
63
  const dataHandler = async () => {
155
64
  for await (const event of subscription)
156
65
  try {
157
66
  if (event.data.length === 0) {
158
- readable.push(null)
159
- subscription.drain()
67
+ readable.push(null);
68
+ subscription.drain();
160
69
  } else
161
- readable.push(event.data)
70
+ readable.push(event.data);
162
71
  } catch (e) {
163
- console.error(`Stream.getReadable.dataHandler.error:`, e)
72
+ console.error("Stream.getReadable.dataHandler.error:", e);
164
73
  }
165
- }
166
- dataHandler()
74
+ };
75
+ dataHandler();
167
76
 
168
77
  // kickstart remote stream with a blank message
169
- await connection.publishRaw(`gx2.stream.${object.id}.a`)
78
+ await connection.publishRaw(`gx2.stream.${object.id}.a`);
170
79
 
171
- return readable
80
+ return readable;
172
81
  }
173
82
 
174
- export async function streamToBuffer(object, forceNats = false) {
175
- let readable = object
83
+ export async function streamToBuffer(object) {
84
+ let readable = object;
176
85
  if (isStream(readable))
177
- readable = await getReadable(readable, forceNats)
86
+ readable = await getReadable(readable);
178
87
 
179
- return Buffer.concat(await readable.toArray())
88
+ return Buffer.concat(await readable.toArray());
180
89
  }
181
90
 
182
91
  export async function streamToString(object) {
183
- let readable = object
92
+ let readable = object;
184
93
  if (isStream(readable))
185
- readable = await getReadable(readable)
94
+ readable = await getReadable(readable);
186
95
 
187
- return Buffer.concat(await readable.toArray()).toString()
96
+ return Buffer.concat(await readable.toArray()).toString();
188
97
  }
189
98