systemlynx 1.19.12 → 1.21.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.
@@ -12,19 +12,25 @@ describe("createApp()", () => {
12
12
  .that.has.all.keys(
13
13
  "module",
14
14
  "on",
15
+ "once",
15
16
  "emit",
16
17
  "before",
17
18
  "after",
18
19
  "$clearEvent",
20
+ "destroy",
19
21
  "use",
20
22
  "startService",
21
23
  "loadService",
22
24
  "onLoad",
23
25
  "config",
24
26
  "server",
25
- "WebSocket"
27
+ "WebSocket",
28
+ "getModule",
29
+ "getModules"
26
30
  )
27
31
  .that.respondsTo("module")
32
+ .that.respondsTo("getModule")
33
+ .that.respondsTo("getModules")
28
34
  .that.respondsTo("on")
29
35
  .that.respondsTo("emit")
30
36
  .that.respondsTo("$clearEvent")
@@ -62,7 +68,9 @@ describe("App: Loading Services", () => {
62
68
  .that.has.all.keys(
63
69
  "emit",
64
70
  "on",
71
+ "once",
65
72
  "$clearEvent",
73
+ "destroy",
66
74
  "resetConnection",
67
75
  "disconnect",
68
76
  "headers",
@@ -103,7 +111,9 @@ describe("App: Loading Services", () => {
103
111
  .that.has.all.keys(
104
112
  "emit",
105
113
  "on",
114
+ "once",
106
115
  "$clearEvent",
116
+ "destroy",
107
117
  "resetConnection",
108
118
  "disconnect",
109
119
  "headers",
@@ -136,48 +146,52 @@ describe("App: Loading Services", () => {
136
146
 
137
147
  await new Promise((resolve) => {
138
148
  const App = createApp();
139
- App.loadService("test", url)
140
- .on("service_loaded", (test) => {
141
- expect(test)
142
- .to.be.an("object")
143
- .that.has.all.keys(
144
- "emit",
145
- "on",
146
- "$clearEvent",
147
- "resetConnection",
148
- "disconnect",
149
- "headers",
150
- "setHeaders",
151
- "mod"
152
- )
153
- .that.respondsTo("emit")
154
- .that.respondsTo("$clearEvent")
155
- .that.respondsTo("on")
156
- .that.respondsTo("resetConnection")
157
- .that.respondsTo("headers")
158
- .that.respondsTo("setHeaders");
159
- })
160
- .on("service_loaded:test", (test) => {
161
- expect(test)
162
- .to.be.an("object")
163
- .that.has.all.keys(
164
- "emit",
165
- "on",
166
- "$clearEvent",
167
- "resetConnection",
168
- "disconnect",
169
- "headers",
170
- "setHeaders",
171
- "mod"
172
- )
173
- .that.respondsTo("emit")
174
- .that.respondsTo("$clearEvent")
175
- .that.respondsTo("on")
176
- .that.respondsTo("resetConnection")
177
- .that.respondsTo("headers")
178
- .that.respondsTo("setHeaders");
179
- resolve();
180
- });
149
+ App.loadService("test", url);
150
+ App.on("service_loaded", (test) => {
151
+ expect(test)
152
+ .to.be.an("object")
153
+ .that.has.all.keys(
154
+ "emit",
155
+ "on",
156
+ "once",
157
+ "$clearEvent",
158
+ "destroy",
159
+ "resetConnection",
160
+ "disconnect",
161
+ "headers",
162
+ "setHeaders",
163
+ "mod"
164
+ )
165
+ .that.respondsTo("emit")
166
+ .that.respondsTo("$clearEvent")
167
+ .that.respondsTo("on")
168
+ .that.respondsTo("resetConnection")
169
+ .that.respondsTo("headers")
170
+ .that.respondsTo("setHeaders");
171
+ });
172
+ App.on("service_loaded:test", (test) => {
173
+ expect(test)
174
+ .to.be.an("object")
175
+ .that.has.all.keys(
176
+ "emit",
177
+ "on",
178
+ "once",
179
+ "$clearEvent",
180
+ "destroy",
181
+ "resetConnection",
182
+ "disconnect",
183
+ "headers",
184
+ "setHeaders",
185
+ "mod"
186
+ )
187
+ .that.respondsTo("emit")
188
+ .that.respondsTo("$clearEvent")
189
+ .that.respondsTo("on")
190
+ .that.respondsTo("resetConnection")
191
+ .that.respondsTo("headers")
192
+ .that.respondsTo("setHeaders");
193
+ resolve();
194
+ });
181
195
  });
182
196
  });
183
197
 
@@ -204,7 +218,9 @@ describe("App: Loading Services", () => {
204
218
  .that.has.all.keys(
205
219
  "emit",
206
220
  "on",
221
+ "once",
207
222
  "$clearEvent",
223
+ "destroy",
208
224
  "resetConnection",
209
225
  "disconnect",
210
226
  "headers",
@@ -236,8 +252,10 @@ describe("App SystemObjects: Initializing Modules, Modules and configurations",
236
252
  "useService",
237
253
  "useConfig",
238
254
  "on",
255
+ "once",
239
256
  "emit",
240
257
  "$clearEvent",
258
+ "destroy",
241
259
  "before",
242
260
  "after"
243
261
  )
@@ -257,8 +275,10 @@ describe("App SystemObjects: Initializing Modules, Modules and configurations",
257
275
  "useService",
258
276
  "useConfig",
259
277
  "on",
278
+ "once",
260
279
  "emit",
261
280
  "$clearEvent",
281
+ "destroy",
262
282
  "before",
263
283
  "after"
264
284
  )
@@ -377,6 +397,66 @@ describe("App SystemObjects: Initializing Modules, Modules and configurations",
377
397
  );
378
398
  });
379
399
 
400
+ it("should expose live modules via App.getModule(name) and App.getModules() after ready", async () => {
401
+ const App = createApp();
402
+ const route = "test-service";
403
+ const port = "8495";
404
+
405
+ App.module("mod", function () {
406
+ this.test = () => {};
407
+ }).module("mod2", function () {
408
+ this.test = () => {};
409
+ });
410
+
411
+ // before initialization the live module reference does not exist yet
412
+ expect(App.getModule("mod")).to.equal(undefined);
413
+ expect(App.getModules()).to.be.an("object").that.is.empty;
414
+
415
+ await new Promise((resolve) => App.startService({ route, port }).on("ready", resolve));
416
+
417
+ const mod = App.getModule("mod");
418
+ expect(mod).to.be.an("object").that.respondsTo("on").that.respondsTo("emit");
419
+ expect(App.getModule("missing")).to.equal(undefined);
420
+
421
+ const modules = App.getModules();
422
+ expect(modules).to.be.an("object").that.has.all.keys("mod", "mod2");
423
+ expect(modules.mod).to.respondTo("on").that.respondsTo("emit");
424
+ });
425
+
426
+ it("should emit a local 'error' event on the module when a method throws back to the client", async () => {
427
+ const App = createApp();
428
+ const route = "test-service";
429
+ const port = "8496";
430
+
431
+ App.module("errMod", function () {
432
+ this.boom = () => {
433
+ throw { status: 400, message: "boom went the method" };
434
+ };
435
+ });
436
+
437
+ await new Promise((resolve) => App.startService({ route, port }).on("ready", resolve));
438
+
439
+ const errMod = App.getModule("errMod");
440
+ const errorEvent = new Promise((resolve) => errMod.on("error", resolve));
441
+
442
+ const url = `http://localhost:${port}/${route}/errMod/boom`;
443
+ try {
444
+ await HttpClient.request({ method: "POST", url, body: { __arguments: [{ x: 1 }] } });
445
+ } catch (e) {
446
+ // expected — the method throws and the error is returned over HTTP
447
+ }
448
+
449
+ const info = await errorEvent;
450
+ expect(info)
451
+ .to.be.an("object")
452
+ .that.has.all.keys("module_name", "fn", "arguments", "status", "message", "error");
453
+ expect(info.module_name).to.equal("errMod");
454
+ expect(info.fn).to.equal("boom");
455
+ expect(info.status).to.equal(400);
456
+ expect(info.message).to.equal("boom went the method");
457
+ expect(info.arguments).to.deep.equal([{ x: 1 }]);
458
+ });
459
+
380
460
  it("should be able to use App.config(constructor) to construct a configuration module", async () => {
381
461
  const App = createApp();
382
462
 
@@ -472,19 +552,19 @@ describe("SystemContext", () => {
472
552
  .that.respondsTo("useConfig");
473
553
  this.configPassed = true;
474
554
  next();
475
- })
476
- .on("ready", function () {
477
- expect(this)
478
- .to.be.an("object")
479
- .that.respondsTo("useService")
480
- .that.respondsTo("useModule")
481
- .that.respondsTo("useConfig");
482
- const mod1 = this.useModule("mod1");
483
- const config = this.useConfig();
484
- expect(mod1.testPassed).to.equal(true);
485
- expect(config.configPassed).to.equal(true);
486
- })
487
- .on("ready", function () {
555
+ });
556
+ App.on("ready", function () {
557
+ expect(this)
558
+ .to.be.an("object")
559
+ .that.respondsTo("useService")
560
+ .that.respondsTo("useModule")
561
+ .that.respondsTo("useConfig");
562
+ const mod1 = this.useModule("mod1");
563
+ const config = this.useConfig();
564
+ expect(mod1.testPassed).to.equal(true);
565
+ expect(config.configPassed).to.equal(true);
566
+ });
567
+ App.on("ready", function () {
488
568
  expect(this)
489
569
  .to.be.an("object")
490
570
  .that.respondsTo("useService")
@@ -543,7 +623,8 @@ describe("SystemContext", () => {
543
623
  expect(event.type).to.equal("WebSocket");
544
624
  resolve();
545
625
  });
546
- EventTesterModule.sendEvent(eventName);
626
+ // small delay so "subscribe" WebSocket message is processed before the HTTP call triggers the emit
627
+ setTimeout(() => EventTesterModule.sendEvent(eventName), 100);
547
628
  })
548
629
  );
549
630
  });
@@ -17,13 +17,13 @@ const extractFilesFromArguments = (__arguments) => {
17
17
 
18
18
  __arguments.forEach((arg) => {
19
19
  if (isObject(arg)) {
20
- if ("file" in arg) {
20
+ if (arg.file) {
21
21
  if (foundFile)
22
22
  throw new Error("Only one file or files allowed across arguments.");
23
23
  foundFile = arg.file;
24
24
  fileType = "file";
25
25
  arg.file = "__file__";
26
- } else if ("files" in arg) {
26
+ } else if (arg.files) {
27
27
  if (foundFile)
28
28
  throw new Error("Only one file or files allowed across arguments.");
29
29
  foundFile = arg.files;
@@ -2,6 +2,8 @@
2
2
  const io = require("socket.io-client");
3
3
  const createDispatcher = require("../../Dispatcher/Dispatcher");
4
4
 
5
+ const RESERVED = new Set(["connect", "disconnect", "error", "connect_error"]);
6
+
5
7
  module.exports = function SocketDispatcher(
6
8
  { namespace, socketPath: path },
7
9
  events = {},
@@ -13,8 +15,82 @@ module.exports = function SocketDispatcher(
13
15
  : createDispatcher.apply(this, [events, systemContext]);
14
16
 
15
17
  const socket = io.connect(namespace, { path });
18
+ const subscriptionCounts = new Map();
19
+
20
+ const trackSubscribe = (name) => {
21
+ const n = (subscriptionCounts.get(name) || 0) + 1;
22
+ subscriptionCounts.set(name, n);
23
+ if (n === 1) socket.emit("subscribe", name);
24
+ };
25
+
26
+ const trackUnsubscribe = (name) => {
27
+ const n = (subscriptionCounts.get(name) || 0) - 1;
28
+ if (n <= 0) {
29
+ subscriptionCounts.delete(name);
30
+ socket.emit("unsubscribe", name);
31
+ } else {
32
+ subscriptionCounts.set(name, n);
33
+ }
34
+ };
35
+
36
+ const originalOn = dispatcher.on.bind(dispatcher);
37
+ dispatcher.on = function (name, cb, options) {
38
+ const unsub = originalOn(name, cb, options);
39
+ if (!RESERVED.has(name)) {
40
+ trackSubscribe(name);
41
+ return function () {
42
+ unsub();
43
+ trackUnsubscribe(name);
44
+ };
45
+ }
46
+ return unsub;
47
+ };
16
48
 
17
- socket.on("dispatch", (event) => dispatcher.emit(event.name, event.data, event));
49
+ const originalOnce = dispatcher.once.bind(dispatcher);
50
+ dispatcher.once = function (name, cb, options) {
51
+ if (RESERVED.has(name)) return originalOnce(name, cb, options);
52
+ let done = false;
53
+ trackSubscribe(name);
54
+ const unsub = originalOnce(
55
+ name,
56
+ function (...args) {
57
+ if (!done) {
58
+ done = true;
59
+ trackUnsubscribe(name);
60
+ cb(...args);
61
+ }
62
+ },
63
+ options
64
+ );
65
+ return function () {
66
+ if (!done) {
67
+ done = true;
68
+ unsub();
69
+ trackUnsubscribe(name);
70
+ }
71
+ };
72
+ };
73
+
74
+ const originalClearEvent = dispatcher.$clearEvent.bind(dispatcher);
75
+ dispatcher.$clearEvent = function (name) {
76
+ originalClearEvent(name);
77
+ if (subscriptionCounts.has(name)) {
78
+ subscriptionCounts.delete(name);
79
+ socket.emit("unsubscribe", name);
80
+ }
81
+ };
82
+
83
+ const originalDestroy = dispatcher.destroy.bind(dispatcher);
84
+ dispatcher.destroy = function () {
85
+ subscriptionCounts.forEach((_, name) => socket.emit("unsubscribe", name));
86
+ subscriptionCounts.clear();
87
+ originalDestroy();
88
+ };
89
+
90
+ socket.onAny((name, payload) => {
91
+ const event = { id: payload.id, name, data: payload.data, type: payload.type };
92
+ dispatcher.emit(name, payload.data, event);
93
+ });
18
94
 
19
95
  socket.on("disconnect", () => {
20
96
  socket.disconnect();
@@ -22,6 +98,9 @@ module.exports = function SocketDispatcher(
22
98
  });
23
99
 
24
100
  socket.on("connect", () => {
101
+ subscriptionCounts.forEach((count, name) => {
102
+ if (count > 0) socket.emit("subscribe", name);
103
+ });
25
104
  dispatcher.emit("connect");
26
105
  });
27
106
 
@@ -46,7 +46,9 @@ describe("Client", () => {
46
46
  .that.has.all.keys(
47
47
  "emit",
48
48
  "on",
49
+ "once",
49
50
  "$clearEvent",
51
+ "destroy",
50
52
  "resetConnection",
51
53
  "disconnect",
52
54
  "headers",
@@ -66,7 +68,9 @@ describe("Client", () => {
66
68
  .that.has.all.keys(
67
69
  "emit",
68
70
  "on",
71
+ "once",
69
72
  "$clearEvent",
73
+ "destroy",
70
74
  "disconnect",
71
75
  "headers",
72
76
  "setHeaders",
@@ -11,6 +11,10 @@ const socket = WebSocket.of(route);
11
11
  socket.on("connect", ({ id }) => {
12
12
  console.log(`socket connected with id:${id}`);
13
13
  });
14
+ socket.on("connection", (clientSocket) => {
15
+ clientSocket.on("subscribe", (name) => clientSocket.join(name));
16
+ clientSocket.on("unsubscribe", (name) => clientSocket.leave(name));
17
+ });
14
18
  SocketServer.listen(port);
15
19
 
16
20
  describe("SocketDispatcher", () => {
@@ -19,7 +23,7 @@ describe("SocketDispatcher", () => {
19
23
  it("should return an EventDispatcher object with methods on and emit", async () => {
20
24
  expect(dispatcher)
21
25
  .to.be.an("object")
22
- .that.has.all.keys("on", "emit", "$clearEvent", "disconnect")
26
+ .that.has.all.keys("on", "once", "emit", "$clearEvent", "destroy", "disconnect")
23
27
  .that.respondsTo("on")
24
28
  .that.respondsTo("emit")
25
29
  .that.respondsTo("$clearEvent")
@@ -32,7 +36,7 @@ describe("SocketDispatcher", () => {
32
36
  });
33
37
  dispatcher.on("connect", () => console.log(`I'm all the way connected!`));
34
38
  setTimeout(
35
- () => socket.emit("dispatch", { name: eventName, data: { testPassed: true } }),
39
+ () => socket.to(eventName).emit(eventName, { id: "test-id", data: { testPassed: true }, type: "WebSocket" }),
36
40
  500
37
41
  );
38
42
  });
@@ -44,7 +48,7 @@ describe("SocketDispatcher.apply()", () => {
44
48
  it("should return an EventDispatcher object with methods on and emit", async () => {
45
49
  expect(dispatcher)
46
50
  .to.be.an("object")
47
- .that.has.all.keys("on", "emit", "$clearEvent", "disconnect")
51
+ .that.has.all.keys("on", "once", "emit", "$clearEvent", "destroy", "disconnect")
48
52
  .that.respondsTo("on")
49
53
  .that.respondsTo("emit")
50
54
  .that.respondsTo("$clearEvent")
@@ -53,14 +57,16 @@ describe("SocketDispatcher.apply()", () => {
53
57
  it("Should be able to emit and handle events", (done) => {
54
58
  dispatcher.on(eventName, (data, event) => {
55
59
  expect(data).to.deep.equal({ testPassed: true });
56
- expect(event).to.deep.equal({ name: "testing-event", data: { testPassed: true } });
57
- //console.log(event);
60
+ expect(event).to.be.an("object").that.has.all.keys("id", "name", "data", "type");
61
+ expect(event.name).to.equal(eventName);
62
+ expect(event.data).to.deep.equal({ testPassed: true });
63
+ expect(event.type).to.equal("WebSocket");
58
64
  console.log(`I'm all the way connected too!`);
59
65
  done();
60
66
  });
61
67
 
62
68
  setTimeout(
63
- () => socket.emit("dispatch", { name: eventName, data: { testPassed: true } }),
69
+ () => socket.to(eventName).emit(eventName, { id: "test-id", data: { testPassed: true }, type: "WebSocket" }),
64
70
  500
65
71
  );
66
72
  });
@@ -1,49 +1,93 @@
1
+ "use strict";
1
2
  const throttle = require("../../utils/throttle");
2
3
 
3
- module.exports = function createDispatcher(events = {}, systemContext) {
4
+ module.exports = function createDispatcher(_, systemContext) {
5
+ const events = new Map();
4
6
  const Dispatcher = this || {};
5
7
 
6
- Dispatcher.emit = (eventName, data, event) => {
7
- if (events[eventName])
8
- events[eventName].forEach((callback) =>
9
- callback.apply(systemContext, [data, event])
10
- );
8
+ Dispatcher.emit = function (eventName, data, event) {
9
+ const registry = events.get(eventName);
10
+ if (!registry) return Dispatcher;
11
+ for (const listener of registry.values()) {
12
+ listener(data, event);
13
+ }
11
14
  return Dispatcher;
12
15
  };
13
16
 
14
- Dispatcher.on = (eventName, callback, { limit, interval } = {}) => {
17
+ Dispatcher.on = function (eventName, callback, { limit, interval, eventId } = {}) {
15
18
  if (typeof callback !== "function") return Dispatcher;
16
- const name = callback.name;
17
- if (typeof interval === "number") callback = throttle(callback, limit, interval);
18
- if (!events[eventName]) events[eventName] = [];
19
-
20
- if (name) {
21
- //if the function has a name and it already present don't add it
22
- const i = events[eventName].findIndex((fn) => fn.name === callback.name);
23
- if (i === -1) events[eventName].push(callback);
24
- else events[eventName][i] = callback;
25
- } else events[eventName].push(callback);
26
- return Dispatcher;
19
+
20
+ const key = eventId || Symbol();
21
+ if (!events.has(eventName)) events.set(eventName, new Map());
22
+ const registry = events.get(eventName);
23
+ if (registry.has(key)) registry.delete(key);
24
+
25
+ let fn = typeof interval === "number" ? throttle(callback, limit, interval) : callback;
26
+ if (systemContext) fn = fn.bind(systemContext);
27
+ registry.set(key, fn);
28
+
29
+ return function () {
30
+ const currentRegistry = events.get(eventName);
31
+ if (!currentRegistry) return;
32
+ currentRegistry.delete(key);
33
+ if (currentRegistry.size === 0) events.delete(eventName);
34
+ };
27
35
  };
28
36
 
29
- Dispatcher.$clearEvent = (eventName, fn) => {
30
- if (!events[eventName]) return Dispatcher;
37
+ Dispatcher.once = function (eventName, callback, { limit, interval, eventId } = {}) {
38
+ if (typeof callback !== "function") return function () {};
39
+
40
+ const key = eventId || Symbol();
41
+ if (!events.has(eventName)) events.set(eventName, new Map());
42
+ const registry = events.get(eventName);
43
+ if (registry.has(key)) registry.delete(key);
44
+
45
+ const throttled =
46
+ typeof interval === "number" ? throttle(callback, limit, interval) : callback;
47
+
48
+ const boundFn = function (...args) {
49
+ registry.delete(key);
50
+ if (registry.size === 0) events.delete(eventName);
51
+ return throttled.apply(systemContext, args);
52
+ };
53
+
54
+ registry.set(key, boundFn);
55
+
56
+ return function () {
57
+ const currentRegistry = events.get(eventName);
58
+ if (!currentRegistry) return;
59
+ currentRegistry.delete(key);
60
+ if (currentRegistry.size === 0) events.delete(eventName);
61
+ };
62
+ };
63
+
64
+ Dispatcher.$clearEvent = function (eventName, fn) {
65
+ if (!events.get(eventName)) return Dispatcher;
31
66
 
32
67
  if (!fn) {
33
- // Clear all listeners for the given event
34
- delete events[eventName];
68
+ events.delete(eventName);
35
69
  } else if (typeof fn === "function") {
36
- // Remove the listener function with the specified name from the event's listener array
37
- events[eventName] = events[eventName].filter((callback) => {
38
- return callback.name !== fn.name;
39
- });
70
+ const registry = events.get(eventName);
71
+ for (const [key, listener] of registry.entries()) {
72
+ if (listener.name === fn.name) {
73
+ registry.delete(key);
74
+ break;
75
+ }
76
+ }
77
+ if (registry.size === 0) events.delete(eventName);
40
78
  } else {
41
79
  console.error(
42
- "SystemLynxError: the second parameter of the Dispatcher.$clearEvent takes the original function to the event"
80
+ "SystemLynxError: the second parameter of the Dispatcher.$clearEvent takes the original function to the event"
43
81
  );
44
82
  }
45
83
 
46
84
  return Dispatcher;
47
85
  };
86
+
87
+ Dispatcher.destroy = function () {
88
+ events.clear();
89
+ return Dispatcher;
90
+ };
91
+
48
92
  return Dispatcher;
49
93
  };