allserver 1.1.0 → 1.3.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 +130 -19
- package/package.json +4 -2
- package/src/client/AllserverClient.js +1 -0
- package/src/client/BullmqClientTransport.js +71 -0
- package/src/client/HttpClientTransport.js +3 -1
- package/src/index.js +9 -0
- package/src/server/Allserver.js +3 -3
- package/src/server/BullmqTransport.js +54 -0
- package/src/server/ExpressTransport.js +74 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Multi-transport and multi-protocol simple RPC server and (optional) client. Boilerplate-less. Opinionated. Minimalistic. DX-first.
|
|
4
4
|
|
|
5
|
-
Think HTTP, gRPC, GraphQL, WebSockets, Lambda, inter-process, unix sockets, etc
|
|
5
|
+
Think of Remote Procedure Calls using exactly the same client and server code but via multiple protocols/mechanisms such as HTTP, gRPC, GraphQL, WebSockets, job queues, Lambda, inter-process, (Web)Workers, unix sockets, etc etc etc.
|
|
6
6
|
|
|
7
7
|
Should be used in (micro)services where JavaScript is able to run - your computer, Docker, k8s, virtual machines, serverless functions (Lambdas, Google Cloud Functions, Azure Functions, etc), RaspberryPI, SharedWorker, thread, you name it.
|
|
8
8
|
|
|
@@ -12,6 +12,8 @@ Superpowers the `Allserver` gives you:
|
|
|
12
12
|
- Run your HTTP server as gRPC with a single line change (almost).
|
|
13
13
|
- Serve same logic via HTTP and gRPC (or more) simultaneously in the same node.js process.
|
|
14
14
|
- Deploy and run your HTTP server on AWS Lambda with no code changes.
|
|
15
|
+
- Embed same exact code into your existing Express.js, no need to implement any handlers/middleware.
|
|
16
|
+
- Use Redis-backed job queue [BullMQ](https://docs.bullmq.io) to call remote procedures reliably.
|
|
15
17
|
- And moar!
|
|
16
18
|
|
|
17
19
|
Superpowers the `AllserverClient` gives you:
|
|
@@ -88,6 +90,7 @@ When calling a remote procedure I want something which:
|
|
|
88
90
|
- Can be easily mapped to any language, any protocol. Especially to upstream GraphQL mutations.
|
|
89
91
|
- Is simple to read in the source code, just like a method/function call. Without thinking of protocol-level details for every damn call.
|
|
90
92
|
- Allows me to test gRPC server from my browser/Postman/curl (via HTTP!) by a simple one line config change.
|
|
93
|
+
- Replace flaky HTTP with Kafka
|
|
91
94
|
- Does not bring tons of npm dependencies with it.
|
|
92
95
|
|
|
93
96
|
Also, the main driving force was my vast experience splitting monolith servers onto (micro)services. Here is how I do it with much success.
|
|
@@ -141,15 +144,21 @@ Please note, that Allserver depends only on a single tiny npm module [`stampit`]
|
|
|
141
144
|
|
|
142
145
|
The default `HttpTransport` is using the [`micro`](http://npmjs.com/package/micro) npm module as an optional dependency.
|
|
143
146
|
|
|
144
|
-
```shell
|
|
147
|
+
```shell
|
|
145
148
|
npm i allserver micro
|
|
146
149
|
```
|
|
147
150
|
|
|
151
|
+
Alternatively, you can embed Allserver into your existing Express.js application:
|
|
152
|
+
|
|
153
|
+
```shell
|
|
154
|
+
npm i allserver express
|
|
155
|
+
```
|
|
156
|
+
|
|
148
157
|
#### Client
|
|
149
158
|
|
|
150
159
|
Optionally, you can use Allserver's built-in client:
|
|
151
160
|
|
|
152
|
-
```shell
|
|
161
|
+
```shell
|
|
153
162
|
npm i allserver node-fetch
|
|
154
163
|
```
|
|
155
164
|
|
|
@@ -161,7 +170,7 @@ Or do HTTP requests using any module you like.
|
|
|
161
170
|
|
|
162
171
|
No dependencies other than `allserver` itself.
|
|
163
172
|
|
|
164
|
-
```shell
|
|
173
|
+
```shell
|
|
165
174
|
npm i allserver
|
|
166
175
|
```
|
|
167
176
|
|
|
@@ -175,7 +184,7 @@ Same as the HTTP protocol client above.
|
|
|
175
184
|
|
|
176
185
|
The default `GrpcTransport` is using the standard the [`@grpc/grpc-js`](https://www.npmjs.com/package/@grpc/grpc-js) npm module as an optional dependency.
|
|
177
186
|
|
|
178
|
-
```shell
|
|
187
|
+
```shell
|
|
179
188
|
npm i allserver @grpc/grpc-js@1 @grpc/proto-loader@0.5
|
|
180
189
|
```
|
|
181
190
|
|
|
@@ -185,12 +194,32 @@ Note, with gRPC server and client you'd need to have your own `.proto` file. See
|
|
|
185
194
|
|
|
186
195
|
Optionally, you can use Allserver's built-in client:
|
|
187
196
|
|
|
188
|
-
```shell
|
|
197
|
+
```shell
|
|
189
198
|
npm i allserver @grpc/grpc-js@1 @grpc/proto-loader@0.5
|
|
190
199
|
```
|
|
191
200
|
|
|
192
201
|
Or do gRPC requests using any module you like.
|
|
193
202
|
|
|
203
|
+
### [BullMQ](https://docs.bullmq.io) job queue
|
|
204
|
+
|
|
205
|
+
#### Server
|
|
206
|
+
|
|
207
|
+
The default `BullmqTransport` is using the [`bullmq`](https://www.npmjs.com/package/bullmq) module as a dependency, connects to Redis using `Worker` class.
|
|
208
|
+
|
|
209
|
+
```shell
|
|
210
|
+
npm i allserver bullmq
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Client
|
|
214
|
+
|
|
215
|
+
Optionally, you can use Allserver's built-in client:
|
|
216
|
+
|
|
217
|
+
```shell
|
|
218
|
+
npm i allserver bullmq
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Or use the `bullmq` module directly. You don't need to use Allserver to call remote procedures. See code example below.
|
|
222
|
+
|
|
194
223
|
## Code examples
|
|
195
224
|
|
|
196
225
|
### Procedures
|
|
@@ -297,19 +326,36 @@ const procedures = {
|
|
|
297
326
|
async processEntity(_, ctx) {
|
|
298
327
|
const micro = ctx.allserver.transport.micro; // same as require("micro")
|
|
299
328
|
const req = ctx.http.req; // node.js Request
|
|
300
|
-
|
|
329
|
+
|
|
301
330
|
// as a string
|
|
302
331
|
const text = await micro.text(req);
|
|
303
332
|
// as a node.js buffer
|
|
304
333
|
const buffer = await micro.buffer(req);
|
|
305
|
-
|
|
306
|
-
// ... process the request here ...
|
|
334
|
+
|
|
335
|
+
// ... process the request here ...
|
|
307
336
|
},
|
|
308
337
|
};
|
|
309
338
|
```
|
|
310
339
|
|
|
311
340
|
More info can be found in the [`micro`](http://npmjs.com/package/micro) NPM module docs.
|
|
312
341
|
|
|
342
|
+
### HTTP server in Express.js
|
|
343
|
+
|
|
344
|
+
Doesn't require a dedicated client transport. Use the HTTP client below.
|
|
345
|
+
|
|
346
|
+
NB: not yet tested in production.
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
const { Allserver, ExpressTransport } = require("allserver");
|
|
350
|
+
|
|
351
|
+
const middleware = Allserver({
|
|
352
|
+
procedures,
|
|
353
|
+
transport: ExpressTransport(),
|
|
354
|
+
}).start();
|
|
355
|
+
|
|
356
|
+
app.use("/route-with-allsever", middleware);
|
|
357
|
+
```
|
|
358
|
+
|
|
313
359
|
### HTTP server in AWS Lambda
|
|
314
360
|
|
|
315
361
|
Doesn't require a dedicated client transport. Use the HTTP client below.
|
|
@@ -343,7 +389,7 @@ exports = Allserver({
|
|
|
343
389
|
|
|
344
390
|
You'd need to install `node-fetch` optional dependency.
|
|
345
391
|
|
|
346
|
-
```shell
|
|
392
|
+
```shell
|
|
347
393
|
npm i allserver node-fetch
|
|
348
394
|
```
|
|
349
395
|
|
|
@@ -354,7 +400,7 @@ const { AllserverClient } = require("allserver");
|
|
|
354
400
|
// or
|
|
355
401
|
const AllserverClient = require("allserver/Client");
|
|
356
402
|
|
|
357
|
-
const client = AllserverClient({ uri: "http://localhost:
|
|
403
|
+
const client = AllserverClient({ uri: "http://localhost:40000" });
|
|
358
404
|
|
|
359
405
|
const { success, code, message, user } = await client.updateUser({
|
|
360
406
|
id: "123412341234123412341234",
|
|
@@ -363,7 +409,7 @@ const { success, code, message, user } = await client.updateUser({
|
|
|
363
409
|
});
|
|
364
410
|
```
|
|
365
411
|
|
|
366
|
-
The `AllserverClient` will issue `HTTP POST` request to this URL: `http://localhost:
|
|
412
|
+
The `AllserverClient` will issue `HTTP POST` request to this URL: `http://localhost:40000/updateUser`.
|
|
367
413
|
The path of the URL is dynamically taken straight from the `client.updateUser` calling code using the ES6 [`Proxy`](https://stackoverflow.com/a/20147219/188475) class. In other words, `AllserverClient` intercepts non-existent property access.
|
|
368
414
|
|
|
369
415
|
#### Using any HTTP client (axios in this example)
|
|
@@ -373,7 +419,7 @@ It's a regular HTTP `POST` call with JSON request and response. URI is `/updateU
|
|
|
373
419
|
```js
|
|
374
420
|
import axios from "axios";
|
|
375
421
|
|
|
376
|
-
const response = await axios.post("http://localhost:
|
|
422
|
+
const response = await axios.post("http://localhost:40000/updateUser", {
|
|
377
423
|
id: "123412341234123412341234",
|
|
378
424
|
firstName: "Fred",
|
|
379
425
|
lastName: "Flinstone",
|
|
@@ -479,15 +525,80 @@ const { success, code, message, user } = data;
|
|
|
479
525
|
1. You can't have `import` statements in your `.proto` file. (Yet.)
|
|
480
526
|
1. Your server-side `.proto` file must include Allserver's [mandatory declarations](./mandatory.proto). (Yet.)
|
|
481
527
|
|
|
528
|
+
### BullMQ server side
|
|
529
|
+
|
|
530
|
+
Note that we are reusing the `procedures` from the example above.
|
|
531
|
+
|
|
532
|
+
Here is how your BullMQ server can look like:
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
const { Allserver, BullmqTransport } = require("allserver");
|
|
536
|
+
|
|
537
|
+
Allserver({
|
|
538
|
+
procedures,
|
|
539
|
+
transport: BullmqTransport({
|
|
540
|
+
connectionOptions: { host: "localhost", port: 6379 },
|
|
541
|
+
}),
|
|
542
|
+
}).start();
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### BullMQ client side
|
|
546
|
+
|
|
547
|
+
#### Using built-in client
|
|
548
|
+
|
|
549
|
+
Note, that this code is **same** as the HTTP client code example above! The only difference is the URI.
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
const { AllserverClient, BullmqClientTransport } = require("allserver");
|
|
553
|
+
// or
|
|
554
|
+
const AllserverClient = require("allserver/Client");
|
|
555
|
+
|
|
556
|
+
const client = AllserverClient({ uri: "bullmq://localhost:6379" });
|
|
557
|
+
// or
|
|
558
|
+
const client = AllserverClient({
|
|
559
|
+
transport: BullmqClientTransport({ uri: "redis://localhost:6379" }),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const { success, code, message, user } = await client.updateUser({
|
|
563
|
+
id: "123412341234123412341234",
|
|
564
|
+
firstName: "Fred",
|
|
565
|
+
lastName: "Flinstone",
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
The `bullmq://` schema uses same connection string as Redis: `bullmq://[[username:]password@]host[:port][/database]`
|
|
570
|
+
|
|
571
|
+
#### Using any BullMQ `Queue` class without Allserver
|
|
572
|
+
|
|
573
|
+
```js
|
|
574
|
+
const { Queue, QueueEvents } = require("bullmq");
|
|
575
|
+
|
|
576
|
+
const queue = new Queue("Allserver", {
|
|
577
|
+
connection: { host: "localhost", port },
|
|
578
|
+
});
|
|
579
|
+
const queueEvents = new QueueEvents("Allserver", {
|
|
580
|
+
connection: { host: "localhost", port },
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const job = await queue.add("updateUser", {
|
|
584
|
+
id: "123412341234123412341234",
|
|
585
|
+
firstName,
|
|
586
|
+
lastName,
|
|
587
|
+
});
|
|
588
|
+
const data = await job.waitUntilFinished(queueEvents, 30_000);
|
|
589
|
+
|
|
590
|
+
const { success, code, message, user } = data;
|
|
591
|
+
```
|
|
592
|
+
|
|
482
593
|
## `AllserverClient` options
|
|
483
594
|
|
|
484
595
|
**All the arguments are optional.** But either `uri` or `transport` must be provided. We are trying to keep the highest possible DX here.
|
|
485
596
|
|
|
486
597
|
- `uri`<br>
|
|
487
|
-
The remote server address string. Out of box supported schemas are: `http`, `https`, `grpc`. (More to come.)
|
|
598
|
+
The remote server address string. Out of box supported schemas are: `http`, `https`, `grpc`, `bullmq`. (More to come.)
|
|
488
599
|
|
|
489
600
|
- `transport`<br>
|
|
490
|
-
The transport implementation object. The `uri` is ignored if this option provided. If not given then it will be automatically created based on the `uri` schema. E.g. if it starts with `http://` or `https
|
|
601
|
+
The transport implementation object. The `uri` is ignored if this option provided. If not given then it will be automatically created based on the `uri` schema. E.g. if it starts with `http://` or `https://` then `HttpClientTransport` will be used. If starts with `grpc://` then `GrpcClientTransport` will be used. If starts with `bullmq://` then `BullmqClientTransport` is used.
|
|
491
602
|
|
|
492
603
|
- `neverThrow=true`<br>
|
|
493
604
|
Set it to `false` if you want to get exceptions when there are a network, or a server errors during a procedure call. Otherwise, the standard `{success,code,message}` object is returned from method calls. The Allserver error `code`s are always start with `"ALLSERVER_"`. E.g. `"ALLSERVER_CLIENT_MALFORMED_INTROSPECTION"`.
|
|
@@ -653,7 +764,7 @@ Yep.
|
|
|
653
764
|
const { AllserverClient } = require("allserver");
|
|
654
765
|
|
|
655
766
|
const client = AllserverClient({
|
|
656
|
-
uri: "http://example.com:
|
|
767
|
+
uri: "http://example.com:40000",
|
|
657
768
|
|
|
658
769
|
async before(ctx) {
|
|
659
770
|
console.log(ctx.procedureName, ctx.arg);
|
|
@@ -685,7 +796,7 @@ const { AllserverClient, HttpClientTransport } = require("allserver");
|
|
|
685
796
|
|
|
686
797
|
const client = AllserverClient({
|
|
687
798
|
transport: HttpClientTransport({
|
|
688
|
-
uri: "http://my-server:
|
|
799
|
+
uri: "http://my-server:40000",
|
|
689
800
|
headers: { authorization: "Basic my-token" },
|
|
690
801
|
}),
|
|
691
802
|
});
|
|
@@ -712,7 +823,7 @@ If something more sophisticated is needed - you would need to mangle the `ctx` i
|
|
|
712
823
|
const { AllserverClient } = require("allserver");
|
|
713
824
|
|
|
714
825
|
const client = AllserverClient({
|
|
715
|
-
uri: "http://my-server:
|
|
826
|
+
uri: "http://my-server:40000",
|
|
716
827
|
async before(ctx) {
|
|
717
828
|
console.log(ctx.procedureName, ctx.arg);
|
|
718
829
|
ctx.http.mode = "cors";
|
|
@@ -739,7 +850,7 @@ const MyAllserverClientWithAuth = AllserverClient.defaults({
|
|
|
739
850
|
});
|
|
740
851
|
|
|
741
852
|
const client = MyAllserverClientWithAuth({
|
|
742
|
-
uri: "http://my-server:
|
|
853
|
+
uri: "http://my-server:40000",
|
|
743
854
|
});
|
|
744
855
|
```
|
|
745
856
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "allserver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Multi-protocol simple RPC server and [optional] client. Boilerplate-less. Opinionated. Minimalistic. DX-first.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -34,10 +34,12 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@grpc/grpc-js": "^1.1.7",
|
|
36
36
|
"@grpc/proto-loader": "^0.5.5",
|
|
37
|
+
"bullmq": "^3.5.7",
|
|
37
38
|
"eslint": "^7.9.0",
|
|
39
|
+
"express": "^4.18.2",
|
|
38
40
|
"lambda-local": "^1.7.3",
|
|
39
41
|
"micro": "^9.3.4",
|
|
40
|
-
"mocha": "^
|
|
42
|
+
"mocha": "^10.2.0",
|
|
41
43
|
"node-fetch": "^2.6.1",
|
|
42
44
|
"nyc": "^15.1.0",
|
|
43
45
|
"prettier": "^2.1.1"
|
|
@@ -164,6 +164,7 @@ module.exports = require("stampit")({
|
|
|
164
164
|
http() { return require("./HttpClientTransport"); },
|
|
165
165
|
https() { return require("./HttpClientTransport"); },
|
|
166
166
|
grpc() { return require("./GrpcClientTransport"); },
|
|
167
|
+
bullmq() { return require("./BullmqClientTransport"); },
|
|
167
168
|
},
|
|
168
169
|
},
|
|
169
170
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module.exports = require("./ClientTransport").compose({
|
|
2
|
+
name: "BullmqClientTransport",
|
|
3
|
+
|
|
4
|
+
props: {
|
|
5
|
+
Queue: require("bullmq").Queue,
|
|
6
|
+
QueueEvents: require("bullmq").QueueEvents,
|
|
7
|
+
_timeout: 60000,
|
|
8
|
+
_queue: null,
|
|
9
|
+
_queueEvents: null,
|
|
10
|
+
_jobsOptions: null,
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
init({ queueName = "Allserver", connectionOptions, timeout, jobsOptions }) {
|
|
14
|
+
if (!connectionOptions) {
|
|
15
|
+
const bullmqUrl = new URL(this.uri);
|
|
16
|
+
connectionOptions = {
|
|
17
|
+
host: bullmqUrl.hostname,
|
|
18
|
+
port: bullmqUrl.port,
|
|
19
|
+
username: bullmqUrl.username,
|
|
20
|
+
password: bullmqUrl.password,
|
|
21
|
+
db: Number(bullmqUrl.pathname.substr(1)) || 0,
|
|
22
|
+
retryStrategy: null, // only one attempt to connect
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
this._timeout = timeout || this._timeout;
|
|
26
|
+
|
|
27
|
+
this._queue = new this.Queue(queueName, { connection: connectionOptions });
|
|
28
|
+
this._queue.on("error", () => {}); // The only reason we subscribe is to avoid bullmq to print errors to console
|
|
29
|
+
this._queueEvents = new this.QueueEvents(queueName, { connection: connectionOptions });
|
|
30
|
+
this._queueEvents.on("error", () => {}); // The only reason we subscribe is to avoid bullmq to print errors to console
|
|
31
|
+
|
|
32
|
+
this._jobsOptions = jobsOptions || {};
|
|
33
|
+
if (this._jobsOptions.removeOnComplete === undefined) this._jobsOptions.removeOnComplete = 100;
|
|
34
|
+
if (this._jobsOptions.removeOnFail === undefined) this._jobsOptions.removeOnFail = 100;
|
|
35
|
+
if (this._jobsOptions.sizeLimit === undefined) this._jobsOptions.sizeLimit = 524288; // max data JSON size is 512KB
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
methods: {
|
|
39
|
+
async introspect(ctx) {
|
|
40
|
+
ctx.procedureName = "introspect";
|
|
41
|
+
const jobReturnResult = await this.call(ctx);
|
|
42
|
+
// The server-side Transport will not have job result if introspection is not available on the server side,
|
|
43
|
+
// but the server itself is up and running processing calls.
|
|
44
|
+
if (jobReturnResult == null) throw new Error("The bullmq introspection job returned nothing");
|
|
45
|
+
return jobReturnResult;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async call({ procedureName, bullmq }) {
|
|
49
|
+
try {
|
|
50
|
+
await this._queue.waitUntilReady();
|
|
51
|
+
const job = await bullmq.queue.add(procedureName, bullmq.data, bullmq.jobsOptions);
|
|
52
|
+
return await job.waitUntilFinished(bullmq.queueEvents, this._timeout);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err.code === "ECONNREFUSED") err.noNetToServer = true;
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
createCallContext(defaultCtx) {
|
|
60
|
+
return {
|
|
61
|
+
...defaultCtx,
|
|
62
|
+
bullmq: {
|
|
63
|
+
data: defaultCtx.arg,
|
|
64
|
+
queue: this._queue,
|
|
65
|
+
queueEvents: this._queueEvents,
|
|
66
|
+
jobsOptions: this._jobsOptions,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -6,7 +6,9 @@ module.exports = require("./ClientTransport").compose({
|
|
|
6
6
|
props: {
|
|
7
7
|
// eslint-disable-next-line no-undef
|
|
8
8
|
fetch: (typeof self !== "undefined" && self.fetch) || require("node-fetch"),
|
|
9
|
-
headers: {
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
11
|
+
},
|
|
10
12
|
},
|
|
11
13
|
|
|
12
14
|
init({ headers, fetch }) {
|
package/src/index.js
CHANGED
|
@@ -11,12 +11,18 @@ module.exports = {
|
|
|
11
11
|
get HttpTransport() {
|
|
12
12
|
return require("./server/HttpTransport");
|
|
13
13
|
},
|
|
14
|
+
get ExpressTransport() {
|
|
15
|
+
return require("./server/ExpressTransport");
|
|
16
|
+
},
|
|
14
17
|
get LambdaTransport() {
|
|
15
18
|
return require("./server/LambdaTransport");
|
|
16
19
|
},
|
|
17
20
|
get GrpcTransport() {
|
|
18
21
|
return require("./server/GrpcTransport");
|
|
19
22
|
},
|
|
23
|
+
get BullmqTransport() {
|
|
24
|
+
return require("./server/BullmqTransport");
|
|
25
|
+
},
|
|
20
26
|
|
|
21
27
|
// client
|
|
22
28
|
|
|
@@ -32,4 +38,7 @@ module.exports = {
|
|
|
32
38
|
get GrpcClientTransport() {
|
|
33
39
|
return require("./client/GrpcClientTransport");
|
|
34
40
|
},
|
|
41
|
+
get BullmqClientTransport() {
|
|
42
|
+
return require("./client/BullmqClientTransport");
|
|
43
|
+
},
|
|
35
44
|
};
|
package/src/server/Allserver.js
CHANGED
|
@@ -65,7 +65,7 @@ module.exports = require("stampit")({
|
|
|
65
65
|
try {
|
|
66
66
|
result = await ctx.procedure(ctx.arg, ctx);
|
|
67
67
|
} catch (err) {
|
|
68
|
-
const code = err.code || "ALLSERVER_PROCEDURE_ERROR"
|
|
68
|
+
const code = err.code || "ALLSERVER_PROCEDURE_ERROR";
|
|
69
69
|
this.logger.error(code, err);
|
|
70
70
|
ctx.error = err;
|
|
71
71
|
ctx.result = {
|
|
@@ -103,7 +103,7 @@ module.exports = require("stampit")({
|
|
|
103
103
|
break;
|
|
104
104
|
}
|
|
105
105
|
} catch (err) {
|
|
106
|
-
const code = err.code || "ALLSERVER_MIDDLEWARE_ERROR";
|
|
106
|
+
const code = err.code || "ALLSERVER_MIDDLEWARE_ERROR";
|
|
107
107
|
this.logger.error(code, err);
|
|
108
108
|
ctx.error = err;
|
|
109
109
|
ctx.result = {
|
|
@@ -136,7 +136,7 @@ module.exports = require("stampit")({
|
|
|
136
136
|
// Warning! This call might overwrite an existing result.
|
|
137
137
|
await this._callMiddlewares(ctx, "after");
|
|
138
138
|
|
|
139
|
-
this.transport.reply(ctx);
|
|
139
|
+
return this.transport.reply(ctx);
|
|
140
140
|
},
|
|
141
141
|
|
|
142
142
|
start() {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module.exports = require("./Transport").compose({
|
|
2
|
+
name: "BullmqTransport",
|
|
3
|
+
|
|
4
|
+
props: {
|
|
5
|
+
Worker: require("bullmq").Worker,
|
|
6
|
+
_worker: null,
|
|
7
|
+
_queueName: null,
|
|
8
|
+
_connectionOptions: null,
|
|
9
|
+
_workerOptions: null,
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
init({ queueName = "Allserver", connectionOptions, workerOptions }) {
|
|
13
|
+
if (!connectionOptions) {
|
|
14
|
+
connectionOptions = { host: "localhost", port: 6379 };
|
|
15
|
+
}
|
|
16
|
+
this._queueName = queueName || this._queueName;
|
|
17
|
+
this._connectionOptions = connectionOptions || this._connectionOptions;
|
|
18
|
+
this._workerOptions = workerOptions || this._workerOptions || {};
|
|
19
|
+
if (this._workerOptions.autorun === undefined) this._workerOptions.autorun = true;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
methods: {
|
|
23
|
+
async startServer(defaultCtx) {
|
|
24
|
+
this._worker = new this.Worker(
|
|
25
|
+
this._queueName,
|
|
26
|
+
async (job) => {
|
|
27
|
+
const ctx = { ...defaultCtx, bullmq: { job }, arg: job.data };
|
|
28
|
+
return await ctx.allserver.handleCall(ctx);
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
connection: this._connectionOptions,
|
|
32
|
+
...this._workerOptions,
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
return await this._worker.waitUntilReady();
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
reply(ctx) {
|
|
39
|
+
return ctx.result;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async stopServer() {
|
|
43
|
+
return this._worker.close();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
getProcedureName(ctx) {
|
|
47
|
+
return ctx.bullmq.job.name;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
isIntrospection(ctx) {
|
|
51
|
+
return this.getProcedureName(ctx) === "introspect"; // could be conflicting with procedure name(s)
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { parse: parseUrl } = require("url");
|
|
2
|
+
|
|
3
|
+
module.exports = require("./Transport").compose({
|
|
4
|
+
name: "ExpressTransport",
|
|
5
|
+
|
|
6
|
+
props: {
|
|
7
|
+
_express: require("express"),
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
methods: {
|
|
11
|
+
async deserializeRequest(ctx) {
|
|
12
|
+
if (ctx.http.req.deserializationFailed) return false;
|
|
13
|
+
// If there is no body we will use request query (aka search params)
|
|
14
|
+
let arg = ctx.http.query;
|
|
15
|
+
if (ctx.http.req.body && Object.keys(ctx.http.req.body).length > 0) arg = ctx.http.req.body;
|
|
16
|
+
ctx.arg = arg;
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async _handleRequest(ctx) {
|
|
21
|
+
if (await this.deserializeRequest(ctx)) {
|
|
22
|
+
await ctx.allserver.handleCall(ctx);
|
|
23
|
+
} else {
|
|
24
|
+
// HTTP protocol request was malformed (not expected structure).
|
|
25
|
+
// We are not going to process it.
|
|
26
|
+
ctx.result = { success: false, code: "ALLSERVER_BAD_REQUEST", message: "Can't parse JSON" };
|
|
27
|
+
ctx.http.statusCode = 400;
|
|
28
|
+
this.reply(ctx);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
startServer(defaultCtx) {
|
|
33
|
+
return [
|
|
34
|
+
this._express.json(),
|
|
35
|
+
(err, req, res, next) => {
|
|
36
|
+
if (err.statusCode) req.deserializationFailed = err; // overriding the error, processing the request as normal
|
|
37
|
+
next();
|
|
38
|
+
},
|
|
39
|
+
async (req, res) => {
|
|
40
|
+
const ctx = {
|
|
41
|
+
...defaultCtx,
|
|
42
|
+
http: { req, res, query: req.query || {} },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
await this._handleRequest(ctx);
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
getProcedureName(ctx) {
|
|
51
|
+
return parseUrl(ctx.http.req.url).pathname.substr(1);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
isIntrospection(ctx) {
|
|
55
|
+
return this.getProcedureName(ctx) === "";
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
prepareNotFoundReply(ctx) {
|
|
59
|
+
ctx.http.statusCode = 404;
|
|
60
|
+
},
|
|
61
|
+
prepareProcedureErrorReply(ctx) {
|
|
62
|
+
// Generic exception.
|
|
63
|
+
ctx.http.statusCode = 500;
|
|
64
|
+
|
|
65
|
+
// nodejs assert() exception. In HTTP world this likely means 400 "Bad Request".
|
|
66
|
+
if (ctx.error.code === "ERR_ASSERTION") ctx.http.statusCode = 400;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
reply(ctx) {
|
|
70
|
+
if (!ctx.http.statusCode) ctx.http.statusCode = 200;
|
|
71
|
+
ctx.http.res.status(ctx.http.statusCode).json(ctx.result);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|