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