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