systemlynx 1.18.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 +43 -12
- package/systemlynx/Client/components/SocketDispatcher.js +80 -1
- package/systemlynx/Client/tests/Client.test.js +26 -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 +12 -3
- 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/API.md
CHANGED
|
@@ -1,200 +1,782 @@
|
|
|
1
|
-
**IN WORKING PROGRESS**
|
|
2
|
-
|
|
3
1
|
# SystemLynx API Documentation
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
SystemLynx is a Node.js framework for building modular, distributed web APIs. It lets you host plain JavaScript objects as **Modules** on a server and load them transparently into a client application, calling their methods over HTTP or WebSockets.
|
|
4
|
+
|
|
5
|
+
For a high-level introduction see the [README](./README.md).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
<summary><b><a href="https://github.com/Odion100/SystemLynx/blob/tasksjs2.0/API.md#app">App</a></b></summary>
|
|
9
|
-
|
|
10
|
-
- [**startService(options)**](https://github.com/Odion100/SystemLynx/blob/tasksjs2.0/API.md#appstartserviceoptions)
|
|
11
|
-
- [**loadService(name, url)**](https://github.com/Odion100/SystemLynx/blob/tasksjs2.0/API.md#apploadserviceurl)
|
|
12
|
-
- [**onLoad(callback)**](https://github.com/Odion100/SystemLynx/tasksjs2.0/API.md#apponloadcallback)
|
|
13
|
-
- [**module(name, constructor [,reserved_methods])**]()
|
|
14
|
-
- [**config(constructor)**](https://github.com/Odion100/SystemLynx/tasksjs2.0/API.md#appconfigconstructor)
|
|
15
|
-
- [**on(event, callback)**]()
|
|
16
|
-
- [**emit(event, payload)**]()
|
|
7
|
+
---
|
|
17
8
|
|
|
18
|
-
|
|
9
|
+
## Table of Contents
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- [
|
|
11
|
+
- [Service](#service)
|
|
12
|
+
- [ServerModule](#servermodule)
|
|
13
|
+
- [Middleware](#middleware)
|
|
14
|
+
- [App](#app)
|
|
15
|
+
- [Client](#client)
|
|
16
|
+
- [ClientModule](#clientmodule)
|
|
17
|
+
- [LoadBalancer](#loadbalancer)
|
|
18
|
+
- [HttpClient](#httpclient)
|
|
19
|
+
|
|
20
|
+
---
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
## Service
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
<summary><b><a href="https://github.com/Odion100/SystemLynx/tasksjs2.0/API.md#service">Service</a></b></summary>
|
|
29
|
-
|
|
30
|
-
- [**startService(options)**]()
|
|
31
|
-
- [**module(name, constructor [,options])**]()
|
|
32
|
-
- [**Server()**]()
|
|
33
|
-
- [**WebSocket()**]()
|
|
24
|
+
`Service` is used to host objects that can be loaded remotely by a SystemLynx `Client`.
|
|
34
25
|
|
|
35
|
-
|
|
26
|
+
```javascript
|
|
27
|
+
const { Service } = require("systemlynx");
|
|
28
|
+
```
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
<summary><b><a href="https://github.com/Odion100/SystemLynx/tasksjs2.0/API.md#service">LoadBalancer</a></b></summary>
|
|
39
|
-
|
|
40
|
-
- [**startService(options)**]()
|
|
41
|
-
- [**module(name, constructor [,options])**]()
|
|
42
|
-
- [**Server()**]()
|
|
43
|
-
- [**WebSocket()**]()
|
|
44
|
-
- [**clones**]()
|
|
45
|
-
- [**register(options)**]()
|
|
46
|
-
- [**dispatch(event)**]()
|
|
47
|
-
- [**assignDispatch(event)**]()
|
|
30
|
+
Alternatively, use `createService` to create an isolated instance:
|
|
48
31
|
|
|
49
|
-
|
|
32
|
+
```javascript
|
|
33
|
+
const { createService } = require("systemlynx");
|
|
34
|
+
const Service = createService();
|
|
35
|
+
```
|
|
50
36
|
|
|
51
37
|
---
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
39
|
+
### Service.module(name, constructor [, reserved_methods])
|
|
40
|
+
|
|
41
|
+
Registers an object or constructor function as a **ServerModule** hosted by this Service. Returns the constructed module.
|
|
42
|
+
|
|
43
|
+
| Parameter | Type | Description |
|
|
44
|
+
|:---|:---|:---|
|
|
45
|
+
| `name` | string | The name used to identify and route to this module |
|
|
46
|
+
| `constructor` | object \| function | The module object, or a constructor function where `this` is the module instance |
|
|
47
|
+
| `reserved_methods` | string[] | Method names to exclude from routing (not accessible remotely) |
|
|
48
|
+
|
|
49
|
+
**Object constructor:**
|
|
50
|
+
```javascript
|
|
51
|
+
const Users = {
|
|
52
|
+
add(data) {
|
|
53
|
+
return { message: "User added" };
|
|
54
|
+
},
|
|
55
|
+
find(query) {
|
|
56
|
+
return { message: "Users found" };
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
Service.module("Users", Users);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Function constructor:**
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
When a function is used, `this` inside it is the module instance. Methods added to `this` are exposed remotely. The constructor receives the Express server and Socket.io instance as arguments, useful for adding custom routes or listeners.
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
```javascript
|
|
68
|
+
Service.module("Orders", function (server, WebSocket) {
|
|
69
|
+
const Orders = this;
|
|
70
|
+
|
|
71
|
+
Orders.find = async function (query) {
|
|
72
|
+
return { results: [] };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
Orders.create = async function (data) {
|
|
76
|
+
return { created: true };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Scope middleware to specific methods
|
|
80
|
+
Orders.before("create", validateData);
|
|
81
|
+
Orders.after("find", formatResults);
|
|
82
|
+
});
|
|
83
|
+
```
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
**Excluding methods from routing:**
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
Service.module("Users", Users, ["internalMethod", "helperFn"]);
|
|
89
|
+
```
|
|
70
90
|
|
|
71
91
|
---
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
### Service.startService(options)
|
|
94
|
+
|
|
95
|
+
Starts the Express and Socket.io servers and sets up routing for all registered modules. Returns a promise that resolves with the service connection data once the server is listening.
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
await Service.startService({
|
|
99
|
+
route: "api/users",
|
|
100
|
+
port: 4400,
|
|
101
|
+
host: "localhost",
|
|
102
|
+
});
|
|
103
|
+
```
|
|
74
104
|
|
|
75
|
-
|
|
105
|
+
| Option | Type | Required | Description |
|
|
106
|
+
|:---|:---|:---:|:---|
|
|
107
|
+
| `route` | string | ✓ | The base route for this service (e.g. `"api/users"`) |
|
|
108
|
+
| `port` | number | ✓ | The port to listen on |
|
|
109
|
+
| `host` | string | | Hostname for the service URL. Default: `"localhost"` |
|
|
110
|
+
| `protocol` | string | | `"http"` or `"https"`. Inferred from `ssl` if not set |
|
|
111
|
+
| `ssl` | object | | `{ key, cert }` — file contents for HTTPS. When provided, an HTTPS server is created |
|
|
112
|
+
| `useREST` | boolean | | When `true`, module methods named `get`, `post`, `put`, or `delete` are also exposed as REST routes. Default: `false` |
|
|
76
113
|
|
|
114
|
+
**With SSL:**
|
|
77
115
|
```javascript
|
|
78
|
-
const
|
|
116
|
+
const fs = require("fs");
|
|
117
|
+
|
|
118
|
+
await Service.startService({
|
|
119
|
+
route: "api/users",
|
|
120
|
+
port: 443,
|
|
121
|
+
host: "example.com",
|
|
122
|
+
protocol: "https",
|
|
123
|
+
ssl: {
|
|
124
|
+
key: fs.readFileSync("/path/to/key.pem"),
|
|
125
|
+
cert: fs.readFileSync("/path/to/cert.pem"),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
79
128
|
```
|
|
80
129
|
|
|
81
|
-
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Service.before([name,] ...middleware)
|
|
82
133
|
|
|
83
|
-
|
|
134
|
+
Adds middleware that runs **before** a module method is called.
|
|
135
|
+
|
|
136
|
+
- With no name — runs before every method on every module
|
|
137
|
+
- With a module name — runs before every method on that module
|
|
138
|
+
- With `"ModuleName.methodName"` — runs before that specific method only
|
|
139
|
+
- With `"$all"` — same as no name, explicitly runs before everything
|
|
84
140
|
|
|
85
141
|
```javascript
|
|
86
|
-
|
|
142
|
+
// Before every method on every module
|
|
143
|
+
Service.before(authenticate);
|
|
87
144
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
// Same, explicit form
|
|
146
|
+
Service.before("$all", authenticate);
|
|
147
|
+
|
|
148
|
+
// Before every method on the Users module
|
|
149
|
+
Service.before("Users", requireAuth);
|
|
150
|
+
|
|
151
|
+
// Before only Users.delete
|
|
152
|
+
Service.before("Users.delete", requireAdmin);
|
|
153
|
+
|
|
154
|
+
// Multiple middleware in one call
|
|
155
|
+
Service.before("Users.edit", validate, sanitize);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Service.after([name,] ...middleware)
|
|
161
|
+
|
|
162
|
+
Adds middleware that runs **after** a module method returns, before the response is sent. Same scoping rules as `Service.before`.
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
// After every method
|
|
166
|
+
Service.after(logResponse);
|
|
167
|
+
|
|
168
|
+
// After every method on Orders
|
|
169
|
+
Service.after("Orders", formatResponse);
|
|
170
|
+
|
|
171
|
+
// After only Orders.find
|
|
172
|
+
Service.after("Orders.find", attachMetadata);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### Service.server
|
|
178
|
+
|
|
179
|
+
The underlying Express app instance. Use this to add custom Express routes, static file serving, or any Express middleware.
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
Service.module("Storage", function (server) {
|
|
183
|
+
const express = require("express");
|
|
184
|
+
server.use("/files", express.static("/path/to/files"));
|
|
185
|
+
|
|
186
|
+
this.list = () => { /* ... */ };
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Service.WebSocket
|
|
193
|
+
|
|
194
|
+
The Socket.io server instance.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## ServerModule
|
|
199
|
+
|
|
200
|
+
A `ServerModule` is the object returned by `Service.module()`. It is also the value of `this` inside a module constructor function. It has the following built-in methods in addition to whatever methods you define.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### module.on(eventName, callback [, options])
|
|
205
|
+
|
|
206
|
+
Listens for a WebSocket event emitted to this module's namespace. Returns an unsubscribe function.
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
const Users = Service.module("Users", function () {
|
|
210
|
+
this.on("user_connected", (data) => {
|
|
211
|
+
console.log("User connected:", data);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
| Option | Type | Description |
|
|
217
|
+
|:---|:---|:---|
|
|
218
|
+
| `eventId` | string | Stable key for this listener. Re-registering with the same `eventId` replaces the previous listener instead of adding a new one |
|
|
219
|
+
| `interval` | number | Throttle interval in milliseconds |
|
|
220
|
+
| `limit` | number | Max calls within the throttle interval |
|
|
95
221
|
|
|
96
|
-
|
|
97
|
-
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
### module.once(eventName, callback [, options])
|
|
225
|
+
|
|
226
|
+
Same as `on`, but the listener is automatically removed after firing once. Returns an unsubscribe function.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### module.emit(eventName, data)
|
|
231
|
+
|
|
232
|
+
Emits a WebSocket event to all connected clients listening on this module.
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
Service.module("Orders", function () {
|
|
236
|
+
this.create = function (data) {
|
|
237
|
+
const order = createOrder(data);
|
|
238
|
+
this.emit("order_created", order);
|
|
239
|
+
return order;
|
|
98
240
|
};
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This is also available in middleware via `req.module.emit(...)`:
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
function notifyClients(req, res, next) {
|
|
248
|
+
req.module.emit(`updated:${req.returnValue._id}`, req.returnValue);
|
|
249
|
+
next();
|
|
99
250
|
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### module.$clearEvent(eventName [, fn])
|
|
256
|
+
|
|
257
|
+
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.
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
module.$clearEvent("order_created"); // remove all
|
|
261
|
+
module.$clearEvent("order_created", myHandler); // remove by name
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### module.destroy()
|
|
267
|
+
|
|
268
|
+
Removes all event listeners on this module.
|
|
100
269
|
|
|
101
|
-
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### module.before([name,] ...middleware)
|
|
273
|
+
|
|
274
|
+
Adds middleware scoped to this module. Same as `Service.before("ModuleName", ...)` but callable from within the constructor.
|
|
102
275
|
|
|
103
|
-
|
|
276
|
+
```javascript
|
|
277
|
+
Service.module("Users", function () {
|
|
278
|
+
this.edit = editUser;
|
|
279
|
+
this.delete = deleteUser;
|
|
280
|
+
|
|
281
|
+
this.before("edit", validateEditData);
|
|
282
|
+
this.before("delete", requireAdmin);
|
|
283
|
+
this.after("$all", logAction);
|
|
284
|
+
});
|
|
104
285
|
```
|
|
105
286
|
|
|
106
|
-
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### module.after([name,] ...middleware)
|
|
290
|
+
|
|
291
|
+
Same as `module.before` but runs after the method returns.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Middleware
|
|
107
296
|
|
|
108
|
-
|
|
297
|
+
Middleware functions follow the Express convention: `(req, res, next) => {}`. They can be async.
|
|
298
|
+
|
|
299
|
+
SystemLynx adds the following properties to the request and response objects.
|
|
300
|
+
|
|
301
|
+
### Request properties
|
|
302
|
+
|
|
303
|
+
| Property | Type | Description |
|
|
304
|
+
|:---|:---|:---|
|
|
305
|
+
| `req.module_name` | string | Name of the module being called (e.g. `"Users"`) |
|
|
306
|
+
| `req.fn` | string | Name of the method being called (e.g. `"find"`) |
|
|
307
|
+
| `req.arguments` | array | The arguments passed by the client to the method |
|
|
308
|
+
| `req.module` | object | The ServerModule instance itself |
|
|
309
|
+
| `req.returnValue` | any | The value returned by the method — available in `after` middleware |
|
|
310
|
+
|
|
311
|
+
Any properties you add to `req` are accessible in subsequent middleware in the chain:
|
|
109
312
|
|
|
110
313
|
```javascript
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const host = "localhost";
|
|
314
|
+
function authenticate(req, res, next) {
|
|
315
|
+
const token = req.headers.authorization;
|
|
316
|
+
const session = verifyToken(token);
|
|
115
317
|
|
|
116
|
-
|
|
318
|
+
if (!session) return res.sendError({ status: 401, message: "Unauthorized" });
|
|
319
|
+
|
|
320
|
+
req.session = session;
|
|
321
|
+
next();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function requireAdmin(req, res, next) {
|
|
325
|
+
if (!req.session.isAdmin)
|
|
326
|
+
return res.sendError({ status: 403, message: "Forbidden" });
|
|
327
|
+
next();
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Response methods
|
|
332
|
+
|
|
333
|
+
| Method | Description |
|
|
334
|
+
|:---|:---|
|
|
335
|
+
| `res.sendError({ status, message })` | Sends an error response and stops the middleware chain |
|
|
336
|
+
| `res.sendResponse(value)` | Sends a successful response with the given value |
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
function validate(req, res, next) {
|
|
340
|
+
const [data] = req.arguments;
|
|
341
|
+
if (!data.name) return res.sendError({ status: 400, message: "name is required" });
|
|
342
|
+
next();
|
|
343
|
+
}
|
|
117
344
|
```
|
|
118
345
|
|
|
119
|
-
|
|
346
|
+
**Modifying the return value in `after` middleware:**
|
|
120
347
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
| staticRouting | boolean | O | The route from which the service can be loaded. |
|
|
130
|
-
| middleware | string | R | The route from which the service can be loaded. |
|
|
348
|
+
```javascript
|
|
349
|
+
function attachMeta(req, res, next) {
|
|
350
|
+
req.returnValue.timestamp = Date.now();
|
|
351
|
+
next();
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
131
356
|
|
|
132
357
|
## App
|
|
133
358
|
|
|
134
|
-
|
|
359
|
+
`App` combines the functionality of `Service` and `Client` into a single object with a chainable interface and lifecycle events. It is the recommended entry point for production services.
|
|
135
360
|
|
|
136
361
|
```javascript
|
|
137
362
|
const { App } = require("systemlynx");
|
|
138
363
|
```
|
|
139
364
|
|
|
140
|
-
|
|
365
|
+
To use a custom Express server or create isolated instances:
|
|
141
366
|
|
|
142
|
-
|
|
367
|
+
```javascript
|
|
368
|
+
const { createApp } = require("systemlynx");
|
|
369
|
+
const express = require("express");
|
|
143
370
|
|
|
144
|
-
|
|
145
|
-
|
|
371
|
+
const server = express();
|
|
372
|
+
const App = createApp(server);
|
|
373
|
+
```
|
|
146
374
|
|
|
147
|
-
|
|
375
|
+
---
|
|
148
376
|
|
|
149
|
-
|
|
377
|
+
### App.startService(options)
|
|
150
378
|
|
|
151
|
-
|
|
379
|
+
Same options as [`Service.startService`](#servicestartserviceoptions). Returns `App` for chaining.
|
|
152
380
|
|
|
153
|
-
|
|
381
|
+
```javascript
|
|
382
|
+
App.startService({ route: "api/profiles", port: 4400, host: "localhost" });
|
|
383
|
+
```
|
|
154
384
|
|
|
155
|
-
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
### App.module(name, constructor [, reserved_methods])
|
|
388
|
+
|
|
389
|
+
Same as [`Service.module`](#servicemodulename-constructor--reserved_methods). Returns `App` for chaining.
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
App.startService({ route: "api", port: 4400 })
|
|
393
|
+
.module("Users", UsersConstructor)
|
|
394
|
+
.module("Orders", OrdersConstructor);
|
|
395
|
+
```
|
|
156
396
|
|
|
157
397
|
---
|
|
158
398
|
|
|
159
|
-
|
|
399
|
+
### App.loadService(name, url)
|
|
400
|
+
|
|
401
|
+
Loads a remote SystemLynx Service and makes it available inside module constructors via `this.useService(name)`. Returns `App` for chaining.
|
|
160
402
|
|
|
161
|
-
|
|
403
|
+
```javascript
|
|
404
|
+
App.startService({ route: "api/basketball", port: 4401 })
|
|
405
|
+
.loadService("Profiles", "http://localhost:4400/api/profiles")
|
|
406
|
+
.module("Games", function () {
|
|
407
|
+
const Profiles = this.useService("Profiles");
|
|
408
|
+
|
|
409
|
+
this.create = async function (data) {
|
|
410
|
+
const profile = await Profiles.Users.get({ id: data.userId });
|
|
411
|
+
// ...
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
```
|
|
162
415
|
|
|
163
416
|
---
|
|
164
417
|
|
|
165
|
-
|
|
418
|
+
### App.onLoad(callback)
|
|
166
419
|
|
|
167
|
-
|
|
420
|
+
Fires a callback when the most recently chained `loadService` connects. Receives the loaded service object.
|
|
168
421
|
|
|
169
|
-
|
|
422
|
+
```javascript
|
|
423
|
+
App.loadService("Profiles", url)
|
|
424
|
+
.onLoad((Profiles) => {
|
|
425
|
+
console.log("Profiles service connected:", Object.keys(Profiles));
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### App.config(constructor)
|
|
432
|
+
|
|
433
|
+
Registers a configuration constructor that runs before modules are initialized. Use it to set up shared state or configuration accessible to all modules via `this.useConfig()`.
|
|
170
434
|
|
|
171
435
|
```javascript
|
|
172
|
-
|
|
436
|
+
App.config(function (next) {
|
|
437
|
+
this.dbConnection = connectToDatabase();
|
|
438
|
+
this.settings = loadSettings();
|
|
439
|
+
next();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
App.module("Users", function () {
|
|
443
|
+
const config = this.useConfig();
|
|
444
|
+
this.db = config.dbConnection;
|
|
445
|
+
});
|
|
173
446
|
```
|
|
174
447
|
|
|
175
|
-
|
|
448
|
+
---
|
|
176
449
|
|
|
177
|
-
|
|
178
|
-
- **_Service.startService(options)_** - Used
|
|
179
|
-
- **_Service.Server()_** - Returns the expressJS app instance used to handle routing to the _Services_.
|
|
180
|
-
- **_Service.WebSocket()_** - Returns socket.io WebSocket instance used to emit events from the _Services_.
|
|
450
|
+
### App.before / App.after
|
|
181
451
|
|
|
182
|
-
|
|
452
|
+
Same as [`Service.before`](#servicebeforename-middleware) and [`Service.after`](#serviceafterename-middleware).
|
|
183
453
|
|
|
184
|
-
|
|
185
|
-
|
|
454
|
+
```javascript
|
|
455
|
+
App.startService({ route: "api", port: 4400 })
|
|
456
|
+
.before("$all", authenticate)
|
|
457
|
+
.module("Users", Users)
|
|
458
|
+
.module("Orders", Orders);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
### App.on(eventName, callback)
|
|
464
|
+
|
|
465
|
+
Listens for a lifecycle event on the App. Returns an unsubscribe function.
|
|
466
|
+
|
|
467
|
+
| Event | Payload | Description |
|
|
468
|
+
|:---|:---|:---|
|
|
469
|
+
| `"ready"` | system object | Fires when the App has fully initialized — service started, all modules loaded, all remote services connected |
|
|
470
|
+
| `"service_loaded"` | service object | Fires each time a remote service finishes connecting |
|
|
471
|
+
| `"service_loaded:<name>"` | service object | Fires when the named service finishes connecting |
|
|
472
|
+
| `"failed_connection"` | `{ err, name, url }` | Fires when a remote service fails to connect |
|
|
186
473
|
|
|
187
474
|
```javascript
|
|
188
|
-
|
|
475
|
+
App.on("ready", function (system) {
|
|
476
|
+
console.log("App ready");
|
|
477
|
+
console.log("Modules:", system.modules.map(m => m.name));
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
App.on("service_loaded:Profiles", (Profiles) => {
|
|
481
|
+
console.log("Profiles connected");
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The `this` value inside a `"ready"` callback is the system context, giving access to `this.useModule()`, `this.useService()`, and `this.useConfig()`.
|
|
189
486
|
|
|
190
|
-
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
### App.emit(eventName, data)
|
|
490
|
+
|
|
491
|
+
Emits a lifecycle event on the App.
|
|
492
|
+
|
|
493
|
+
---
|
|
191
494
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
495
|
+
### App.use(plugin)
|
|
496
|
+
|
|
497
|
+
Registers a plugin function that runs before App initialization. The plugin receives `(App, system)` and can add modules, load services, or configure the App.
|
|
498
|
+
|
|
499
|
+
```javascript
|
|
500
|
+
const myPlugin = (App, system) => {
|
|
501
|
+
App.loadService("Analytics", "http://localhost:4500/analytics");
|
|
502
|
+
App.module("Tracker", TrackerConstructor);
|
|
195
503
|
};
|
|
504
|
+
|
|
505
|
+
App.use(myPlugin).startService({ route: "api", port: 4400 });
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### System context inside modules
|
|
511
|
+
|
|
512
|
+
Inside any module constructor (and `App.on("ready", ...)` callbacks), `this` includes:
|
|
513
|
+
|
|
514
|
+
| Method | Description |
|
|
515
|
+
|:---|:---|
|
|
516
|
+
| `this.useModule(name)` | Returns another module registered on this App by name |
|
|
517
|
+
| `this.useService(name)` | Returns a remote service loaded via `App.loadService(name, url)` |
|
|
518
|
+
| `this.useConfig()` | Returns the configuration object built by `App.config(constructor)` |
|
|
519
|
+
|
|
520
|
+
```javascript
|
|
521
|
+
App.module("Games", function () {
|
|
522
|
+
const Users = this.useModule("Users");
|
|
523
|
+
const Profiles = this.useService("Profiles");
|
|
524
|
+
|
|
525
|
+
this.create = async function (data) {
|
|
526
|
+
const user = await Users.get({ id: data.userId });
|
|
527
|
+
const profile = await Profiles.Players.get({ id: data.userId });
|
|
528
|
+
// ...
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Client
|
|
536
|
+
|
|
537
|
+
`Client` is used to load a remote SystemLynx Service and call its module methods from a client application.
|
|
538
|
+
|
|
539
|
+
```javascript
|
|
540
|
+
const { Client } = require("systemlynx");
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
### Client.loadService(url [, options])
|
|
546
|
+
|
|
547
|
+
Loads a remote SystemLynx Service. Returns a promise that resolves into an object containing all the modules hosted by that service, plus service-level methods.
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
const { Users, Orders } = await Client.loadService("http://localhost:4400/api");
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
| Option | Type | Description |
|
|
554
|
+
|:---|:---|:---|
|
|
555
|
+
| `forceReload` | boolean | When `true`, bypasses the cache and reconnects even if this URL was already loaded |
|
|
556
|
+
|
|
557
|
+
**Loading multiple services:**
|
|
558
|
+
|
|
559
|
+
```javascript
|
|
560
|
+
const ProfilesAPI = await Client.loadService("http://localhost:4400/api/profiles");
|
|
561
|
+
const BasketballAPI = await Client.loadService("http://localhost:4401/api/basketball");
|
|
562
|
+
|
|
563
|
+
const user = await ProfilesAPI.Users.get({ id: "abc123" });
|
|
564
|
+
const games = await BasketballAPI.Games.find({ userId: "abc123" });
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Service-level methods on the returned object:**
|
|
568
|
+
|
|
569
|
+
| Method | Description |
|
|
570
|
+
|:---|:---|
|
|
571
|
+
| `service.on(event, callback)` | Listen for service-level WebSocket events (`"connect"`, `"disconnect"`, `"reconnect"`) |
|
|
572
|
+
| `service.setHeaders(headers)` | Set default headers sent with every request from this service |
|
|
573
|
+
| `service.headers()` | Returns the current default headers |
|
|
574
|
+
| `service.resetConnection()` | Manually trigger a reconnection |
|
|
575
|
+
| `service.disconnect()` | Disconnect the WebSocket |
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## ClientModule
|
|
580
|
+
|
|
581
|
+
Each module on a loaded service is a `ClientModule`. Calling any method returns a promise that resolves with the method's return value.
|
|
582
|
+
|
|
583
|
+
```javascript
|
|
584
|
+
const { Users } = await Client.loadService(url);
|
|
585
|
+
|
|
586
|
+
const result = await Users.find({ city: "New York" });
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
File uploads are handled automatically — include a `file` (single) or `files` (array) property on any argument object:
|
|
590
|
+
|
|
591
|
+
```javascript
|
|
592
|
+
const result = await Storage.save({
|
|
593
|
+
file: fs.createReadStream("/path/to/photo.jpg"),
|
|
594
|
+
userId: "abc123",
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const result = await Storage.save({
|
|
598
|
+
files: [fs.createReadStream("a.jpg"), fs.createReadStream("b.jpg")],
|
|
599
|
+
albumId: "xyz",
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
### module.on(eventName, callback [, options])
|
|
606
|
+
|
|
607
|
+
Listens for WebSocket events emitted by the server-side module. Returns an unsubscribe function — useful for React `useEffect` cleanup.
|
|
608
|
+
|
|
609
|
+
```javascript
|
|
610
|
+
const unsubscribe = Orders.on("order_created", (data) => {
|
|
611
|
+
console.log("New order:", data);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Later, to clean up:
|
|
615
|
+
unsubscribe();
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
| Option | Type | Description |
|
|
619
|
+
|:---|:---|:---|
|
|
620
|
+
| `eventId` | string | Stable key for this listener. Re-registering with the same `eventId` replaces the previous listener. Useful in React re-renders |
|
|
621
|
+
| `interval` | number | Throttle interval in milliseconds |
|
|
622
|
+
| `limit` | number | Max calls within the throttle interval |
|
|
623
|
+
|
|
624
|
+
**React usage:**
|
|
625
|
+
```javascript
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
const unsubscribe = Orders.on("order_created", handleNewOrder, {
|
|
628
|
+
eventId: "order-list-listener",
|
|
629
|
+
});
|
|
630
|
+
return unsubscribe;
|
|
631
|
+
}, []);
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
### module.once(eventName, callback [, options])
|
|
637
|
+
|
|
638
|
+
Same as `on`, but fires only once then removes itself automatically. Returns an unsubscribe function.
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
### module.emit(eventName, data)
|
|
643
|
+
|
|
644
|
+
Emits a WebSocket event to the server-side module's namespace.
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
### module.$clearEvent(eventName [, fn])
|
|
649
|
+
|
|
650
|
+
Removes listeners. Without a function argument, removes all listeners for that event.
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
### module.destroy()
|
|
655
|
+
|
|
656
|
+
Removes all listeners on this module.
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
### module.setHeaders(headers)
|
|
661
|
+
|
|
662
|
+
Sets default HTTP headers sent with every request from this module. Module-level headers take priority over service-level headers.
|
|
663
|
+
|
|
664
|
+
```javascript
|
|
665
|
+
Users.setHeaders({ Authorization: `Bearer ${token}` });
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
### module.headers()
|
|
671
|
+
|
|
672
|
+
Returns the current module-level headers object.
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## LoadBalancer
|
|
679
|
+
|
|
680
|
+
`LoadBalancer` distributes requests across multiple clones of a Service. Clients connect to the LoadBalancer URL — the LoadBalancer handles routing transparently.
|
|
681
|
+
|
|
682
|
+
```javascript
|
|
683
|
+
const { LoadBalancer } = require("systemlynx");
|
|
196
684
|
```
|
|
197
685
|
|
|
198
|
-
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
### LoadBalancer.startService(options)
|
|
689
|
+
|
|
690
|
+
Same options as [`Service.startService`](#servicestartserviceoptions). Starts the LoadBalancer as a Service.
|
|
691
|
+
|
|
692
|
+
```javascript
|
|
693
|
+
await LoadBalancer.startService({ route: "api/users", port: 4400 });
|
|
694
|
+
```
|
|
199
695
|
|
|
200
696
|
---
|
|
697
|
+
|
|
698
|
+
### LoadBalancer.clones
|
|
699
|
+
|
|
700
|
+
The `clones` module manages clone registration and routing. It is itself a `ServerModule`, so clients can call its methods remotely.
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
#### clones.register(options, callback)
|
|
705
|
+
|
|
706
|
+
Registers a new clone of a service with the LoadBalancer. Once registered, the LoadBalancer will route requests to it using round-robin.
|
|
707
|
+
|
|
708
|
+
```javascript
|
|
709
|
+
const { clones } = await Client.loadService("http://localhost:4400/api/users");
|
|
710
|
+
|
|
711
|
+
clones.register({ host: "localhost", port: 4401, route: "/api/users" }, (err, result) => {
|
|
712
|
+
if (err) console.error("Registration failed:", err);
|
|
713
|
+
else console.log("Clone registered:", result);
|
|
714
|
+
});
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
| Option | Type | Required | Description |
|
|
718
|
+
|:---|:---|:---:|:---|
|
|
719
|
+
| `host` | string | ✓ | Hostname of the clone |
|
|
720
|
+
| `port` | number | ✓ | Port the clone is running on |
|
|
721
|
+
| `route` | string | ✓ | Route of the clone service |
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
#### clones.dispatch(event, callback)
|
|
726
|
+
|
|
727
|
+
Dispatches a named event to all registered clones.
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
#### clones.assignDispatch(event, callback)
|
|
732
|
+
|
|
733
|
+
Assigns handling of an event to a single clone, preventing duplicate handling across instances.
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## HttpClient
|
|
738
|
+
|
|
739
|
+
A lightweight HTTP client for making requests to SystemLynx services or any HTTP endpoint.
|
|
740
|
+
|
|
741
|
+
```javascript
|
|
742
|
+
const { HttpClient } = require("systemlynx");
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
### HttpClient.request(options)
|
|
748
|
+
|
|
749
|
+
Makes an HTTP request. Returns a promise.
|
|
750
|
+
|
|
751
|
+
```javascript
|
|
752
|
+
const data = await HttpClient.request({
|
|
753
|
+
url: "http://localhost:4400/api/users/Users/find",
|
|
754
|
+
method: "put",
|
|
755
|
+
body: { __arguments: [{ city: "New York" }] },
|
|
756
|
+
headers: { Authorization: "Bearer token" },
|
|
757
|
+
});
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
| Option | Type | Description |
|
|
761
|
+
|:---|:---|:---|
|
|
762
|
+
| `url` | string | Request URL |
|
|
763
|
+
| `method` | string | HTTP method (`"get"`, `"post"`, `"put"`, `"delete"`). Default: `"get"` |
|
|
764
|
+
| `body` | object | Request body |
|
|
765
|
+
| `headers` | object | Request headers |
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
### HttpClient.upload(options)
|
|
770
|
+
|
|
771
|
+
Uploads files using multipart form data. Returns a promise.
|
|
772
|
+
|
|
773
|
+
```javascript
|
|
774
|
+
const result = await HttpClient.upload({
|
|
775
|
+
url: "http://localhost:4400/sf/api/storage/Storage/save",
|
|
776
|
+
method: "put",
|
|
777
|
+
formData: {
|
|
778
|
+
__arguments: [{ message: "profile photo" }],
|
|
779
|
+
file: fs.createReadStream("/path/to/photo.jpg"),
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
```
|