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/README.md CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  # SystemLynx JS ![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-blue.svg) ![JS 100%](https://img.shields.io/badge/JavaScript-100%25-green)
4
4
 
5
- SystemLynx is a NodeJS framework for building modular web APIs, built on top of ExpressJS and Socket.io. It allows you to create objects and load them from a server into a client application.
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 `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.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systemlynx",
3
- "version": "1.19.12",
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;