systemlynx 1.19.12 → 1.20.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 +700 -118
- package/README.md +106 -3
- package/RFCs/001-websocket-named-events-room-scoping.md +133 -0
- package/index.test.js +4 -0
- package/package.json +1 -1
- package/systemlynx/App/tests/App.test.js +73 -56
- 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 +1 -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
|
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",
|
|
@@ -96,8 +98,10 @@ describe("SystemLynx Objects", () => {
|
|
|
96
98
|
"before",
|
|
97
99
|
"after",
|
|
98
100
|
"on",
|
|
101
|
+
"once",
|
|
99
102
|
"emit",
|
|
100
103
|
"$clearEvent",
|
|
104
|
+
"destroy",
|
|
101
105
|
"clones",
|
|
102
106
|
"register",
|
|
103
107
|
"dispatch",
|
package/package.json
CHANGED
|
@@ -12,10 +12,12 @@ 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",
|
|
@@ -62,7 +64,9 @@ describe("App: Loading Services", () => {
|
|
|
62
64
|
.that.has.all.keys(
|
|
63
65
|
"emit",
|
|
64
66
|
"on",
|
|
67
|
+
"once",
|
|
65
68
|
"$clearEvent",
|
|
69
|
+
"destroy",
|
|
66
70
|
"resetConnection",
|
|
67
71
|
"disconnect",
|
|
68
72
|
"headers",
|
|
@@ -103,7 +107,9 @@ describe("App: Loading Services", () => {
|
|
|
103
107
|
.that.has.all.keys(
|
|
104
108
|
"emit",
|
|
105
109
|
"on",
|
|
110
|
+
"once",
|
|
106
111
|
"$clearEvent",
|
|
112
|
+
"destroy",
|
|
107
113
|
"resetConnection",
|
|
108
114
|
"disconnect",
|
|
109
115
|
"headers",
|
|
@@ -136,48 +142,52 @@ describe("App: Loading Services", () => {
|
|
|
136
142
|
|
|
137
143
|
await new Promise((resolve) => {
|
|
138
144
|
const App = createApp();
|
|
139
|
-
App.loadService("test", url)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
145
|
+
App.loadService("test", url);
|
|
146
|
+
App.on("service_loaded", (test) => {
|
|
147
|
+
expect(test)
|
|
148
|
+
.to.be.an("object")
|
|
149
|
+
.that.has.all.keys(
|
|
150
|
+
"emit",
|
|
151
|
+
"on",
|
|
152
|
+
"once",
|
|
153
|
+
"$clearEvent",
|
|
154
|
+
"destroy",
|
|
155
|
+
"resetConnection",
|
|
156
|
+
"disconnect",
|
|
157
|
+
"headers",
|
|
158
|
+
"setHeaders",
|
|
159
|
+
"mod"
|
|
160
|
+
)
|
|
161
|
+
.that.respondsTo("emit")
|
|
162
|
+
.that.respondsTo("$clearEvent")
|
|
163
|
+
.that.respondsTo("on")
|
|
164
|
+
.that.respondsTo("resetConnection")
|
|
165
|
+
.that.respondsTo("headers")
|
|
166
|
+
.that.respondsTo("setHeaders");
|
|
167
|
+
});
|
|
168
|
+
App.on("service_loaded:test", (test) => {
|
|
169
|
+
expect(test)
|
|
170
|
+
.to.be.an("object")
|
|
171
|
+
.that.has.all.keys(
|
|
172
|
+
"emit",
|
|
173
|
+
"on",
|
|
174
|
+
"once",
|
|
175
|
+
"$clearEvent",
|
|
176
|
+
"destroy",
|
|
177
|
+
"resetConnection",
|
|
178
|
+
"disconnect",
|
|
179
|
+
"headers",
|
|
180
|
+
"setHeaders",
|
|
181
|
+
"mod"
|
|
182
|
+
)
|
|
183
|
+
.that.respondsTo("emit")
|
|
184
|
+
.that.respondsTo("$clearEvent")
|
|
185
|
+
.that.respondsTo("on")
|
|
186
|
+
.that.respondsTo("resetConnection")
|
|
187
|
+
.that.respondsTo("headers")
|
|
188
|
+
.that.respondsTo("setHeaders");
|
|
189
|
+
resolve();
|
|
190
|
+
});
|
|
181
191
|
});
|
|
182
192
|
});
|
|
183
193
|
|
|
@@ -204,7 +214,9 @@ describe("App: Loading Services", () => {
|
|
|
204
214
|
.that.has.all.keys(
|
|
205
215
|
"emit",
|
|
206
216
|
"on",
|
|
217
|
+
"once",
|
|
207
218
|
"$clearEvent",
|
|
219
|
+
"destroy",
|
|
208
220
|
"resetConnection",
|
|
209
221
|
"disconnect",
|
|
210
222
|
"headers",
|
|
@@ -236,8 +248,10 @@ describe("App SystemObjects: Initializing Modules, Modules and configurations",
|
|
|
236
248
|
"useService",
|
|
237
249
|
"useConfig",
|
|
238
250
|
"on",
|
|
251
|
+
"once",
|
|
239
252
|
"emit",
|
|
240
253
|
"$clearEvent",
|
|
254
|
+
"destroy",
|
|
241
255
|
"before",
|
|
242
256
|
"after"
|
|
243
257
|
)
|
|
@@ -257,8 +271,10 @@ describe("App SystemObjects: Initializing Modules, Modules and configurations",
|
|
|
257
271
|
"useService",
|
|
258
272
|
"useConfig",
|
|
259
273
|
"on",
|
|
274
|
+
"once",
|
|
260
275
|
"emit",
|
|
261
276
|
"$clearEvent",
|
|
277
|
+
"destroy",
|
|
262
278
|
"before",
|
|
263
279
|
"after"
|
|
264
280
|
)
|
|
@@ -472,19 +488,19 @@ describe("SystemContext", () => {
|
|
|
472
488
|
.that.respondsTo("useConfig");
|
|
473
489
|
this.configPassed = true;
|
|
474
490
|
next();
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
491
|
+
});
|
|
492
|
+
App.on("ready", function () {
|
|
493
|
+
expect(this)
|
|
494
|
+
.to.be.an("object")
|
|
495
|
+
.that.respondsTo("useService")
|
|
496
|
+
.that.respondsTo("useModule")
|
|
497
|
+
.that.respondsTo("useConfig");
|
|
498
|
+
const mod1 = this.useModule("mod1");
|
|
499
|
+
const config = this.useConfig();
|
|
500
|
+
expect(mod1.testPassed).to.equal(true);
|
|
501
|
+
expect(config.configPassed).to.equal(true);
|
|
502
|
+
});
|
|
503
|
+
App.on("ready", function () {
|
|
488
504
|
expect(this)
|
|
489
505
|
.to.be.an("object")
|
|
490
506
|
.that.respondsTo("useService")
|
|
@@ -543,7 +559,8 @@ describe("SystemContext", () => {
|
|
|
543
559
|
expect(event.type).to.equal("WebSocket");
|
|
544
560
|
resolve();
|
|
545
561
|
});
|
|
546
|
-
|
|
562
|
+
// small delay so "subscribe" WebSocket message is processed before the HTTP call triggers the emit
|
|
563
|
+
setTimeout(() => EventTesterModule.sendEvent(eventName), 100);
|
|
547
564
|
})
|
|
548
565
|
);
|
|
549
566
|
});
|
|
@@ -17,13 +17,13 @@ const extractFilesFromArguments = (__arguments) => {
|
|
|
17
17
|
|
|
18
18
|
__arguments.forEach((arg) => {
|
|
19
19
|
if (isObject(arg)) {
|
|
20
|
-
if (
|
|
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 (
|
|
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
|
-
|
|
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",
|