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.
- package/API.md +741 -118
- package/README.md +106 -3
- package/RFCs/001-websocket-named-events-room-scoping.md +133 -0
- package/RFCs/002-module-handle-and-internal-error-events.md +135 -0
- package/index.test.js +10 -2
- package/package.json +1 -1
- package/systemlynx/App/App.js +14 -0
- package/systemlynx/App/tests/App.test.js +138 -57
- package/systemlynx/Client/components/ServiceRequestHandler.js +2 -2
- package/systemlynx/Client/components/SocketDispatcher.js +80 -1
- package/systemlynx/Client/tests/Client.test.js +4 -0
- package/systemlynx/Client/tests/SocketDispatcher.test.js +12 -6
- package/systemlynx/Dispatcher/Dispatcher.js +71 -27
- package/systemlynx/Dispatcher/Dispatcher.test.js +87 -4
- package/systemlynx/LoadBalancer/tests/LoadBalancer.test.js +4 -0
- package/systemlynx/ServerManager/components/Router.js +13 -0
- package/systemlynx/ServerManager/components/SocketEmitter.js +7 -1
- package/systemlynx/ServerManager/tests/SocketEmitter.test.js +12 -9
- package/systemlynx/Service/Service.test.js +3 -1
package/README.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
# SystemLynx JS   
|
|
4
4
|
|
|
5
|
-
SystemLynx is a
|
|
5
|
+
SystemLynx is a Node.js framework for building modular, distributed web applications using an RPC-style architecture. It enables services to expose structured modules whose methods can be invoked remotely from client applications, abstracting away much of the complexity of client-to-service communication.
|
|
6
|
+
|
|
7
|
+
Built on top of Express and Socket.io, SystemLynx supports both request-response and real-time communication patterns, allowing developers to build scalable APIs and event-driven systems with a unified interface.
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
SystemLynx comes with the following objects that are used for web app development:
|
|
8
11
|
|
|
@@ -39,7 +42,7 @@ Service.module("Users", Users);
|
|
|
39
42
|
|
|
40
43
|
In the code above we assigned an object to the variable `Users` and gave it an add method. The `Service.module(name, constructor/object)` function takes the name assigned to the object as the first argument and the object itself as the second argument.
|
|
41
44
|
|
|
42
|
-
Alternatively, you can use a constructor function instead of an object as the second argument. In the example below we create another **Module** called "Orders". This time we use a constructor function as the second argument of the to **Service.module** function. The
|
|
45
|
+
Alternatively, you can use a constructor function instead of an object as the second argument. In the example below we create another **Module** called "Orders". This time we use a constructor function as the second argument of the to **Service.module** function. The `this` value is the initial instance of the **Module** object. Every method added to the `this` value will be accessible when the object is loaded by a **SystemLynx Client**. Note: **Module** methods can be synchronous or asynchronous functions.
|
|
43
46
|
|
|
44
47
|
```javascript
|
|
45
48
|
const { Service } = require("systemlynx");
|
|
@@ -156,8 +159,8 @@ const Users = {};
|
|
|
156
159
|
|
|
157
160
|
Users.add = function (data) {
|
|
158
161
|
console.log(data);
|
|
159
|
-
return { message: "You have successfully called the Users.add method" };
|
|
160
162
|
this.emit("new_user", { message: "new_user event test" });
|
|
163
|
+
return { message: "You have successfully called the Users.add method" };
|
|
161
164
|
};
|
|
162
165
|
|
|
163
166
|
Service.module("Users", Users);
|
|
@@ -173,3 +176,103 @@ Service.module("Orders", function () {
|
|
|
173
176
|
|
|
174
177
|
Service.startService({ route: "test/service", port: "4400", host: "localhost" });
|
|
175
178
|
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
# Middleware
|
|
183
|
+
|
|
184
|
+
SystemLynx supports middleware that runs before or after a module method is called. This is useful for things like authentication, logging, or validating requests.
|
|
185
|
+
|
|
186
|
+
Use `Service.before` to add a middleware function that runs before any method is invoked, and `Service.after` to run one after the response is ready.
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
const { Service } = require("systemlynx");
|
|
190
|
+
|
|
191
|
+
Service.before(function (req, res, next) {
|
|
192
|
+
console.log("Incoming request:", req.fn);
|
|
193
|
+
next();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
Service.module("Users", Users);
|
|
197
|
+
Service.module("Orders", Orders);
|
|
198
|
+
|
|
199
|
+
Service.startService({ route: "api", port: 4400, host: "localhost" });
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
You can also scope middleware to a specific module or method so it only runs where you need it.
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
// Runs before every method on the Users module
|
|
206
|
+
Service.before("Users", authMiddleware);
|
|
207
|
+
|
|
208
|
+
// Runs only before Users.delete
|
|
209
|
+
Service.before("Users.delete", requireAdminMiddleware);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
See the [API Documentation](https://github.com/Odion100/SystemLynx/blob/master/API.md#tasksjs-api-documentation) for the full middleware API.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
# Architecture: Monolith to Microservices
|
|
217
|
+
|
|
218
|
+
One of the core ideas behind SystemLynx is that your module code doesn't change depending on how you deploy it. You can start with everything in a single service and scale it out later — without rewriting anything.
|
|
219
|
+
|
|
220
|
+
## Start as a monolith
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
const { Service } = require("systemlynx");
|
|
224
|
+
|
|
225
|
+
Service.module("Users", Users);
|
|
226
|
+
Service.module("Orders", Orders);
|
|
227
|
+
Service.module("Products", Products);
|
|
228
|
+
|
|
229
|
+
Service.startService({ route: "api", port: 4400, host: "localhost" });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Your client loads everything from one place:
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
const { Users, Orders, Products } = await Client.loadService("http://localhost:4400/api");
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Scale out: move modules to their own services
|
|
239
|
+
|
|
240
|
+
When you're ready to scale, pull any module into its own service. The module code is unchanged — just move it and update the URL the client loads from.
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
// users-service.js
|
|
244
|
+
Service.module("Users", Users);
|
|
245
|
+
Service.startService({ route: "users", port: 4401, host: "localhost" });
|
|
246
|
+
|
|
247
|
+
// orders-service.js
|
|
248
|
+
Service.module("Orders", Orders);
|
|
249
|
+
Service.startService({ route: "orders", port: 4402, host: "localhost" });
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
// client
|
|
254
|
+
const { Users } = await Client.loadService("http://localhost:4401/users");
|
|
255
|
+
const { Orders } = await Client.loadService("http://localhost:4402/orders");
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Because every module can be hosted independently, you can scale horizontally (run multiple instances of a service) or vertically (split a busy module into its own service) as your needs grow.
|
|
259
|
+
|
|
260
|
+
## Load Balancing
|
|
261
|
+
|
|
262
|
+
The **LoadBalancer** lets you run multiple clones of a service and distribute requests across them. Clones register themselves with the LoadBalancer, which handles routing — your client just talks to the LoadBalancer URL and doesn't need to know how many instances are running behind it.
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const { LoadBalancer } = require("systemlynx");
|
|
266
|
+
|
|
267
|
+
LoadBalancer.startService({ route: "users", port: 4400, host: "localhost" });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Each clone registers on startup:
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
const { Client } = require("systemlynx");
|
|
274
|
+
|
|
275
|
+
const { clones } = await Client.loadService("http://localhost:4400/users");
|
|
276
|
+
|
|
277
|
+
await clones.register({ host: "localhost", port: 4401, route: "/users" });
|
|
278
|
+
```
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# RFC: WebSocket Named Events + Room-Based Scoping
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Currently all server-to-client WebSocket traffic flows through a single event named `"dispatch"`. The server uses `socket.emit("dispatch", {id, name, data, type})` on the namespace, which broadcasts to **every connected client** in the namespace. Each client then checks `event.name` and routes or discards client-side. This wastes bandwidth — if 10 clients are connected and only 1 subscribed to `"orderCreated"`, all 10 still receive the packet.
|
|
6
|
+
|
|
7
|
+
The goal is to use Socket.io's native named events + room-based scoping so that each event is only delivered to clients that actually subscribed to it.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Architecture Change
|
|
12
|
+
|
|
13
|
+
**Before:**
|
|
14
|
+
```
|
|
15
|
+
Server: namespace.emit("dispatch", { id, name: "orderCreated", data, type })
|
|
16
|
+
→ ALL clients in namespace receive it
|
|
17
|
+
→ each client checks event.name, ignores if not listening
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**After:**
|
|
21
|
+
```
|
|
22
|
+
Client subscribes: socket.emit("subscribe", "orderCreated")
|
|
23
|
+
Server joins room: clientSocket.join("orderCreated")
|
|
24
|
+
Server emits: namespace.to("orderCreated").emit("orderCreated", { id, data, type })
|
|
25
|
+
→ ONLY clients in room "orderCreated" receive it
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The user-facing API is **unchanged** — `module.on("eventName", cb)` still works the same.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Files to Change
|
|
33
|
+
|
|
34
|
+
### 1. `systemlynx/ServerManager/components/SocketEmitter.js`
|
|
35
|
+
|
|
36
|
+
Two changes:
|
|
37
|
+
- Add a `"connection"` handler on the namespace to manage room membership per connected client
|
|
38
|
+
- Change the emit to target the room instead of broadcasting to all
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// Add after creating namespace socket:
|
|
42
|
+
socket.on("connection", (clientSocket) => {
|
|
43
|
+
clientSocket.on("subscribe", (name) => clientSocket.join(name));
|
|
44
|
+
clientSocket.on("unsubscribe", (name) => clientSocket.leave(name));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Change the emit:
|
|
48
|
+
// OLD: socket.emit("dispatch", { id, name, data, type });
|
|
49
|
+
// NEW: socket.to(name).emit(name, { id, data, type });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. `systemlynx/Client/components/SocketDispatcher.js`
|
|
53
|
+
|
|
54
|
+
Three changes:
|
|
55
|
+
|
|
56
|
+
**a) Replace `socket.on("dispatch", ...)` with `socket.onAny(...)`** to receive named events. Reconstruct the event object explicitly (no spread — avoids payload fields bleeding into event shape). `name` is kept on the event object since a single callback can handle multiple events and may need to know which fired:
|
|
57
|
+
```javascript
|
|
58
|
+
socket.onAny((name, payload) => {
|
|
59
|
+
const event = { id: payload.id, name, data: payload.data, type: payload.type };
|
|
60
|
+
dispatcher.emit(name, payload.data, event);
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**b) Track subscription counts and sync with server rooms** by overriding `on`, `once`, `$clearEvent`, and `destroy`. Use a `subscriptionCounts` Map. On first listener for an event → emit `"subscribe"` to server. When last listener removed → emit `"unsubscribe"`. Use a `RESERVED` set for socket.io internal events (`connect`, `disconnect`, `error`, `connect_error`) that must never be treated as subscriptions.
|
|
65
|
+
|
|
66
|
+
**c) Re-subscribe on reconnect** — inside the `"connect"` handler, re-emit `"subscribe"` for all events currently tracked in `subscriptionCounts`.
|
|
67
|
+
|
|
68
|
+
Subscription reference-counting sketch:
|
|
69
|
+
```javascript
|
|
70
|
+
const subscriptionCounts = new Map();
|
|
71
|
+
const RESERVED = new Set(["connect", "disconnect", "error", "connect_error"]);
|
|
72
|
+
|
|
73
|
+
const trackSubscribe = (name) => {
|
|
74
|
+
const n = (subscriptionCounts.get(name) || 0) + 1;
|
|
75
|
+
subscriptionCounts.set(name, n);
|
|
76
|
+
if (n === 1) socket.emit("subscribe", name);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const trackUnsubscribe = (name) => {
|
|
80
|
+
const n = (subscriptionCounts.get(name) || 0) - 1;
|
|
81
|
+
if (n <= 0) { subscriptionCounts.delete(name); socket.emit("unsubscribe", name); }
|
|
82
|
+
else subscriptionCounts.set(name, n);
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Override `on`: call original, if not RESERVED → `trackSubscribe`, return unsub that also calls `trackUnsubscribe`.
|
|
87
|
+
Override `once`: same but auto-`trackUnsubscribe` when the callback fires (use a `done` flag to prevent double-unsubscribe if unsub is called before event fires).
|
|
88
|
+
Override `$clearEvent`: call original, if tracked → clear count and emit `"unsubscribe"`.
|
|
89
|
+
Override `destroy`: emit `"unsubscribe"` for all tracked events, clear map, call original.
|
|
90
|
+
|
|
91
|
+
### 3. `systemlynx/Client/tests/SocketDispatcher.test.js`
|
|
92
|
+
|
|
93
|
+
Two changes:
|
|
94
|
+
|
|
95
|
+
**a) Add subscription handling to test server setup** (mirrors what SocketEmitter does internally):
|
|
96
|
+
```javascript
|
|
97
|
+
socket.on("connection", (clientSocket) => {
|
|
98
|
+
clientSocket.on("subscribe", (name) => clientSocket.join(name));
|
|
99
|
+
clientSocket.on("unsubscribe", (name) => clientSocket.leave(name));
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**b) Change server-side emit in test** from namespace broadcast to room-targeted named event:
|
|
104
|
+
```javascript
|
|
105
|
+
// OLD:
|
|
106
|
+
socket.emit("dispatch", { name: eventName, data: { testPassed: true } })
|
|
107
|
+
// NEW:
|
|
108
|
+
socket.to(eventName).emit(eventName, { id: "test-id", data: { testPassed: true }, type: "WebSocket" })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Update the `event` deep-equal assertion in the `SocketDispatcher.apply()` test to match the new reconstructed shape `{ id, name, data, type }`.
|
|
112
|
+
|
|
113
|
+
### 4. `/Users/odionedwards/SystemLynx-client/systemlynx/Client/components/SocketDispatcher.mjs`
|
|
114
|
+
|
|
115
|
+
The `systemlynx-client` package (separate repo at `/Users/odionedwards/SystemLynx-client/`, used by `systemview`) has its own `SocketDispatcher.mjs` that is identical in logic to `SocketDispatcher.js` but uses ES module syntax (`import`/`export default`). Apply the exact same changes — subscription count Map, `on`/`once`/`$clearEvent`/`destroy` overrides, `socket.onAny`, reconnect re-subscription — written with ES module syntax.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Files NOT Changed
|
|
120
|
+
|
|
121
|
+
- `systemlynx/ServerManager/components/Router.js` — HTTP method calls, unaffected
|
|
122
|
+
- `systemlynx/Client/components/ServiceRequestHandler.js` — HTTP method calls, unaffected
|
|
123
|
+
- `systemlynx/Client/tests/Client.test.js` — event shape `has.all.keys("id", "name", "data", "type")` still matches ✓
|
|
124
|
+
- `systemlynx/App/tests/App.test.js` — event shape not tested here ✓
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Verification
|
|
129
|
+
|
|
130
|
+
1. `npm test` — all existing tests should pass
|
|
131
|
+
2. The `SocketDispatcher.test.js` "emit and handle events" tests confirm room delivery works
|
|
132
|
+
3. The `Client.test.js` "receive events emitted from backend" test verifies end-to-end: server `eventTester.emit(...)` → only subscribed client receives it
|
|
133
|
+
4. Manually: connect two clients to the same service, subscribe one to `"eventA"`, confirm only that client gets the event when the server emits it
|
|
@@ -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
|
@@ -34,8 +34,10 @@ describe("SystemLynx Objects", () => {
|
|
|
34
34
|
.that.has.all.keys(
|
|
35
35
|
"module",
|
|
36
36
|
"on",
|
|
37
|
+
"once",
|
|
37
38
|
"emit",
|
|
38
39
|
"$clearEvent",
|
|
40
|
+
"destroy",
|
|
39
41
|
"before",
|
|
40
42
|
"after",
|
|
41
43
|
"use",
|
|
@@ -44,7 +46,9 @@ describe("SystemLynx Objects", () => {
|
|
|
44
46
|
"onLoad",
|
|
45
47
|
"config",
|
|
46
48
|
"server",
|
|
47
|
-
"WebSocket"
|
|
49
|
+
"WebSocket",
|
|
50
|
+
"getModule",
|
|
51
|
+
"getModules"
|
|
48
52
|
)
|
|
49
53
|
.that.respondsTo("module")
|
|
50
54
|
.that.respondsTo("on")
|
|
@@ -56,7 +60,9 @@ describe("SystemLynx Objects", () => {
|
|
|
56
60
|
.that.respondsTo("startService")
|
|
57
61
|
.that.respondsTo("loadService")
|
|
58
62
|
.that.respondsTo("onLoad")
|
|
59
|
-
.that.respondsTo("config")
|
|
63
|
+
.that.respondsTo("config")
|
|
64
|
+
.that.respondsTo("getModule")
|
|
65
|
+
.that.respondsTo("getModules");
|
|
60
66
|
});
|
|
61
67
|
|
|
62
68
|
it("should return a SystemLynx Client", () => {
|
|
@@ -96,8 +102,10 @@ describe("SystemLynx Objects", () => {
|
|
|
96
102
|
"before",
|
|
97
103
|
"after",
|
|
98
104
|
"on",
|
|
105
|
+
"once",
|
|
99
106
|
"emit",
|
|
100
107
|
"$clearEvent",
|
|
108
|
+
"destroy",
|
|
101
109
|
"clones",
|
|
102
110
|
"register",
|
|
103
111
|
"dispatch",
|
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;
|