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
package/systemlynx/App/App.js
CHANGED
|
@@ -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,
|