systemlynx 1.20.0 → 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.
package/API.md CHANGED
@@ -252,6 +252,27 @@ function notifyClients(req, res, next) {
252
252
 
253
253
  ---
254
254
 
255
+ ### Built-in `"error"` event
256
+
257
+ SystemLynx emits a local `"error"` event on a module whenever one of its methods sends an error back to the client (a thrown error, a rejected promise, or a middleware error). This is a **server-side, local-only** event — it is *not* broadcast over WebSockets to clients (the failing client already receives the error over HTTP). It lets observers monitor failures that bypass `after` middleware.
258
+
259
+ | Event | Payload | Description |
260
+ |:---|:---|:---|
261
+ | `"error"` | `{ module_name, fn, arguments, status, message, error }` | Fires when a method on this module returns an error to the client |
262
+
263
+ ```javascript
264
+ Service.module("Orders", function () {
265
+ this.on("error", (info) => {
266
+ console.log(`${info.module_name}.${info.fn} failed (${info.status}): ${info.message}`);
267
+ console.log("called with:", info.arguments);
268
+ });
269
+ });
270
+ ```
271
+
272
+ A plugin can subscribe across every module via [`App.getModules()`](#appgetmodulename--appgetmodules).
273
+
274
+ ---
275
+
255
276
  ### module.$clearEvent(eventName [, fn])
256
277
 
257
278
  Removes event listeners. If a function is provided, removes only the listener matching that function's name. If no function is provided, removes all listeners for that event.
@@ -447,6 +468,26 @@ App.module("Users", function () {
447
468
 
448
469
  ---
449
470
 
471
+ ### App.getModule(name) / App.getModules()
472
+
473
+ Return the live module objects hosted on this App. `getModule(name)` returns a single module (or `undefined`); `getModules()` returns an object keyed by module name (`{ [name]: module }`).
474
+
475
+ Modules are constructed during initialization, so the live references only exist **after** the `"ready"` event. Before that, `getModule` returns `undefined` and `getModules` returns an empty object. Call them inside an `App.on("ready", ...)` handler — this is how a plugin (e.g. a monitoring/observability plugin) gets a handle on modules to observe or decorate them.
476
+
477
+ ```javascript
478
+ App.use((App) => {
479
+ App.on("ready", () => {
480
+ Object.entries(App.getModules()).forEach(([name, module]) => {
481
+ module.on("error", (info) => {
482
+ console.log(`[${name}] ${info.module_name}.${info.fn} failed:`, info.message);
483
+ });
484
+ });
485
+ });
486
+ });
487
+ ```
488
+
489
+ ---
490
+
450
491
  ### App.before / App.after
451
492
 
452
493
  Same as [`Service.before`](#servicebeforename-middleware) and [`Service.after`](#serviceafterename-middleware).
@@ -0,0 +1,135 @@
1
+ # RFC: Module Handle + Internal Error Events
2
+
3
+ ## Context
4
+
5
+ SystemView is a SystemLynx plugin (installed via `App.use`) that wants to act as the
6
+ observability/logging layer for a SystemLynx service — monitoring requests and errors
7
+ under the hood, and letting modules log through SystemView.
8
+
9
+ Today the plugin gets `(App, system)` but has no *supported* way to reach the live module
10
+ objects, and SystemLynx emits **no internal events** on the request/error lifecycle. This
11
+ RFC adds the minimal SystemLynx-side surface to enable that. **All SystemView-specific
12
+ behavior (the log function, the monitoring sink) lives in the SystemView plugin, not in
13
+ SystemLynx.** SystemLynx only exposes access + an emit point.
14
+
15
+ ---
16
+
17
+ ## Two gaps, two changes
18
+
19
+ ### Gap 1 — No supported handle on the live modules
20
+
21
+ A plugin receives `App` and `system`, and live modules already exist at
22
+ `system.modules[i].module` after construction. But:
23
+
24
+ - Reaching into `system.modules[].module` couples the plugin to internal shape.
25
+ - At `plugin.apply` time the modules are **not built yet** — `system.modules` holds only
26
+ `{ name, __constructor }`. The live `.module` is assigned later in `loadModules`, right
27
+ before `App.emit("ready", system)`.
28
+
29
+ **Change:** Add `App.getModules()` (and `App.getModule(name)`) returning the live module
30
+ objects. Returns them after `ready`; empty/undefined before.
31
+
32
+ ### Gap 2 — Errors bypass the lifecycle
33
+
34
+ `before`/`after` middleware already let a plugin observe the request happy-path. But every
35
+ error to the client funnels through `res.sendError` in `Router.js`, which writes the HTTP
36
+ response and **does not call `next()`** — so afterware and the response middleware are
37
+ skipped. `after` hooks never see failures.
38
+
39
+ **Change:** Emit a **local-only** `"error"` event on the module from inside `sendError`.
40
+ The failing client already receives the error over HTTP; this event is purely for
41
+ server-side observers. Use `$emit` (local) **not** `emit` (which would broadcast the error
42
+ over websockets to every connected client).
43
+
44
+ ---
45
+
46
+ ## Files to Change
47
+
48
+ ### 1. `systemlynx/App/App.js`
49
+
50
+ Add module accessors. The live module is at `system.modules[i].module` once built.
51
+
52
+ ```js
53
+ App.getModule = (name) => {
54
+ const found = system.modules.find((m) => m.name === name);
55
+ return found ? found.module : undefined;
56
+ };
57
+
58
+ App.getModules = () =>
59
+ system.modules.reduce((obj, { name, module }) => {
60
+ if (module) obj[name] = module;
61
+ return obj;
62
+ }, {});
63
+ ```
64
+
65
+ `getModules()` returns an object **keyed by module name** (`{ [name]: module }`) —
66
+ consistent with how SystemLynx exposes service modules elsewhere (`useService(name)`
67
+ returns named module keys). `getModule(name)` returns a single live module. Both only
68
+ return live modules after the `ready` event (before that, `module` is undefined). Plugins
69
+ should call these inside an `App.on("ready", ...)` handler.
70
+
71
+ ### 2. `systemlynx/ServerManager/components/Router.js`
72
+
73
+ Emit a local `"error"` event from the single error chokepoint, `sendError` (inside
74
+ `parseRequest`). `req.Module` is the live module dispatcher.
75
+
76
+ ```js
77
+ const sendError = (error) => {
78
+ const status = (error || {}).status || 500;
79
+ const message = (error || {}).message || unhandledMessage;
80
+ if (req.Module && typeof req.Module.$emit === "function")
81
+ req.Module.$emit("error", {
82
+ module_name,
83
+ fn,
84
+ arguments: req.arguments,
85
+ status,
86
+ message,
87
+ error,
88
+ });
89
+ res.status(status).json({ ...presets, ...error, status, message, SystemLynxService: true });
90
+ };
91
+ ```
92
+
93
+ The payload carries the call identity (`module_name` + `fn`) and the inputs
94
+ (`arguments`) so an observer can reconstruct what was called and with what. The raw
95
+ Express `req` is intentionally **not** included — args-only keeps the event lean; if a
96
+ consumer later needs headers we can revisit.
97
+
98
+ `$emit` is the local-only emitter installed by `SocketEmitter` (see `SocketEmitter.js` —
99
+ `Emitter.$emit = Emitter.emit` before `emit` is overridden to broadcast). Guard with a
100
+ typeof check so a module without SocketEmitter applied doesn't throw.
101
+
102
+ ---
103
+
104
+ ## Files NOT Changed
105
+
106
+ - `systemlynx/ServerManager/components/SocketEmitter.js` — `$emit` already exists; we only
107
+ consume it.
108
+ - `systemlynx/Service/Service.js` — module construction unchanged.
109
+ - HTTP response shape — unchanged; the `"error"` event is additive and server-side only.
110
+
111
+ ---
112
+
113
+ ## Plugin-side usage (for reference — lives in SystemView, not this repo)
114
+
115
+ ```js
116
+ App.use((App, system) => {
117
+ App.on("ready", () => {
118
+ Object.entries(App.getModules()).forEach(([name, module]) => {
119
+ module.on("error", (info) => { /* SystemView records the failure */ });
120
+ module.log = (...args) => { /* SystemView log routing */ };
121
+ });
122
+ });
123
+ });
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Verification
129
+
130
+ 1. `npm test` — existing suite stays green (changes are additive).
131
+ 2. New test: register a module whose method throws, call it over HTTP, assert the module's
132
+ local `"error"` event fired with `{ module_name, fn, status, message, error }` and that
133
+ the event did **not** go out over the websocket.
134
+ 3. New test: `App.getModule(name)` returns the live module after `ready`, undefined before.
135
+ 4. Confirm the failing HTTP response body is unchanged from current behavior.
package/index.test.js CHANGED
@@ -46,7 +46,9 @@ describe("SystemLynx Objects", () => {
46
46
  "onLoad",
47
47
  "config",
48
48
  "server",
49
- "WebSocket"
49
+ "WebSocket",
50
+ "getModule",
51
+ "getModules"
50
52
  )
51
53
  .that.respondsTo("module")
52
54
  .that.respondsTo("on")
@@ -58,7 +60,9 @@ describe("SystemLynx Objects", () => {
58
60
  .that.respondsTo("startService")
59
61
  .that.respondsTo("loadService")
60
62
  .that.respondsTo("onLoad")
61
- .that.respondsTo("config");
63
+ .that.respondsTo("config")
64
+ .that.respondsTo("getModule")
65
+ .that.respondsTo("getModules");
62
66
  });
63
67
 
64
68
  it("should return a SystemLynx Client", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systemlynx",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "browser": {
@@ -43,6 +43,20 @@ module.exports = function createApp(server, WebSocket, customClient) {
43
43
  return App;
44
44
  };
45
45
 
46
+ // Live module accessors. Modules are constructed during initialization, so the
47
+ // `module` reference only exists after the "ready" event — before that these
48
+ // return undefined / an empty list.
49
+ App.getModule = (name) => {
50
+ const found = system.modules.find((mod) => mod.name === name);
51
+ return found ? found.module : undefined;
52
+ };
53
+
54
+ App.getModules = () =>
55
+ system.modules.reduce((obj, { name, module }) => {
56
+ if (module) obj[name] = module;
57
+ return obj;
58
+ }, {});
59
+
46
60
  App.before = (...args) => {
47
61
  system.Service.before(...args);
48
62
  return App;
@@ -24,9 +24,13 @@ describe("createApp()", () => {
24
24
  "onLoad",
25
25
  "config",
26
26
  "server",
27
- "WebSocket"
27
+ "WebSocket",
28
+ "getModule",
29
+ "getModules"
28
30
  )
29
31
  .that.respondsTo("module")
32
+ .that.respondsTo("getModule")
33
+ .that.respondsTo("getModules")
30
34
  .that.respondsTo("on")
31
35
  .that.respondsTo("emit")
32
36
  .that.respondsTo("$clearEvent")
@@ -393,6 +397,66 @@ describe("App SystemObjects: Initializing Modules, Modules and configurations",
393
397
  );
394
398
  });
395
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
+
396
460
  it("should be able to use App.config(constructor) to construct a configuration module", async () => {
397
461
  const App = createApp();
398
462
 
@@ -58,6 +58,18 @@ module.exports = function createRouter(server, config) {
58
58
  const sendError = (error) => {
59
59
  const status = (error || {}).status || 500;
60
60
  const message = (error || {}).message || unhandledMessage;
61
+ // Emit a local-only "error" event on the module so server-side observers
62
+ // (e.g. a SystemView plugin) can monitor failures. $emit does not broadcast
63
+ // over websockets — the failing client already receives the error over HTTP.
64
+ if (Module && typeof Module.$emit === "function")
65
+ Module.$emit("error", {
66
+ module_name,
67
+ fn,
68
+ arguments: req.arguments,
69
+ status,
70
+ message,
71
+ error,
72
+ });
61
73
  res.status(status).json({
62
74
  ...presets,
63
75
  ...error,