allserver 1.1.0 → 1.2.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,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 Remote Procedure Calls using exactly the same client and server code.
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,7 @@ 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
+ - Use Redis-backed job queue [BullMQ](https://docs.bullmq.io) to call remote procedures reliably.
15
16
  - And moar!
16
17
 
17
18
  Superpowers the `AllserverClient` gives you:
@@ -88,6 +89,7 @@ When calling a remote procedure I want something which:
88
89
  - Can be easily mapped to any language, any protocol. Especially to upstream GraphQL mutations.
89
90
  - 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
91
  - Allows me to test gRPC server from my browser/Postman/curl (via HTTP!) by a simple one line config change.
92
+ - Replace flaky HTTP with Kafka
91
93
  - Does not bring tons of npm dependencies with it.
92
94
 
93
95
  Also, the main driving force was my vast experience splitting monolith servers onto (micro)services. Here is how I do it with much success.
@@ -191,6 +193,26 @@ npm i allserver @grpc/grpc-js@1 @grpc/proto-loader@0.5
191
193
 
192
194
  Or do gRPC requests using any module you like.
193
195
 
196
+ ### [BullMQ](https://docs.bullmq.io) job queue
197
+
198
+ #### Server
199
+
200
+ The default `BullmqTransport` is using the [`bullmq`](https://www.npmjs.com/package/bullmq) module as a dependency, connects to Redis using `Worker` class.
201
+
202
+ ```shell script
203
+ npm i allserver bullmq
204
+ ```
205
+
206
+ #### Client
207
+
208
+ Optionally, you can use Allserver's built-in client:
209
+
210
+ ```shell script
211
+ npm i allserver bullmq
212
+ ```
213
+
214
+ Or use the `bullmq` module directly. You don't need to use Allserver to call remote procedures. See code example below.
215
+
194
216
  ## Code examples
195
217
 
196
218
  ### Procedures
@@ -297,13 +319,13 @@ const procedures = {
297
319
  async processEntity(_, ctx) {
298
320
  const micro = ctx.allserver.transport.micro; // same as require("micro")
299
321
  const req = ctx.http.req; // node.js Request
300
-
322
+
301
323
  // as a string
302
324
  const text = await micro.text(req);
303
325
  // as a node.js buffer
304
326
  const buffer = await micro.buffer(req);
305
-
306
- // ... process the request here ...
327
+
328
+ // ... process the request here ...
307
329
  },
308
330
  };
309
331
  ```
@@ -354,7 +376,7 @@ const { AllserverClient } = require("allserver");
354
376
  // or
355
377
  const AllserverClient = require("allserver/Client");
356
378
 
357
- const client = AllserverClient({ uri: "http://localhost:4000" });
379
+ const client = AllserverClient({ uri: "http://localhost:40000" });
358
380
 
359
381
  const { success, code, message, user } = await client.updateUser({
360
382
  id: "123412341234123412341234",
@@ -363,7 +385,7 @@ const { success, code, message, user } = await client.updateUser({
363
385
  });
364
386
  ```
365
387
 
366
- The `AllserverClient` will issue `HTTP POST` request to this URL: `http://localhost:4000/updateUser`.
388
+ The `AllserverClient` will issue `HTTP POST` request to this URL: `http://localhost:40000/updateUser`.
367
389
  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
390
 
369
391
  #### Using any HTTP client (axios in this example)
@@ -373,7 +395,7 @@ It's a regular HTTP `POST` call with JSON request and response. URI is `/updateU
373
395
  ```js
374
396
  import axios from "axios";
375
397
 
376
- const response = await axios.post("http://localhost:4000/updateUser", {
398
+ const response = await axios.post("http://localhost:40000/updateUser", {
377
399
  id: "123412341234123412341234",
378
400
  firstName: "Fred",
379
401
  lastName: "Flinstone",
@@ -479,15 +501,80 @@ const { success, code, message, user } = data;
479
501
  1. You can't have `import` statements in your `.proto` file. (Yet.)
480
502
  1. Your server-side `.proto` file must include Allserver's [mandatory declarations](./mandatory.proto). (Yet.)
481
503
 
504
+ ### BullMQ server side
505
+
506
+ Note that we are reusing the `procedures` from the example above.
507
+
508
+ Here is how your BullMQ server can look like:
509
+
510
+ ```js
511
+ const { Allserver, BullmqTransport } = require("allserver");
512
+
513
+ Allserver({
514
+ procedures,
515
+ transport: BullmqTransport({
516
+ connectionOptions: { host: "localhost", port: 6379 },
517
+ }),
518
+ }).start();
519
+ ```
520
+
521
+ ### BullMQ client side
522
+
523
+ #### Using built-in client
524
+
525
+ Note, that this code is **same** as the HTTP client code example above! The only difference is the URI.
526
+
527
+ ```js
528
+ const { AllserverClient, BullmqClientTransport } = require("allserver");
529
+ // or
530
+ const AllserverClient = require("allserver/Client");
531
+
532
+ const client = AllserverClient({ uri: "bullmq://localhost:6379" });
533
+ // or
534
+ const client = AllserverClient({
535
+ transport: BullmqClientTransport({ uri: "redis://localhost:6379" }),
536
+ });
537
+
538
+ const { success, code, message, user } = await client.updateUser({
539
+ id: "123412341234123412341234",
540
+ firstName: "Fred",
541
+ lastName: "Flinstone",
542
+ });
543
+ ```
544
+
545
+ The `bullmq://` schema uses same connection string as Redis: `bullmq://[[username:]password@]host[:port][/database]`
546
+
547
+ #### Using any BullMQ `Queue` class without Allserver
548
+
549
+ ```js
550
+ const { Queue, QueueEvents } = require("bullmq");
551
+
552
+ const queue = new Queue("Allserver", {
553
+ connection: { host: "localhost", port },
554
+ });
555
+ const queueEvents = new QueueEvents("Allserver", {
556
+ connection: { host: "localhost", port },
557
+ });
558
+
559
+ const job = await queue.add("updateUser", {
560
+ id: "123412341234123412341234",
561
+ firstName,
562
+ lastName,
563
+ });
564
+ const data = await job.waitUntilFinished(queueEvents, 30_000);
565
+
566
+ const { success, code, message, user } = data;
567
+ ```
568
+
482
569
  ## `AllserverClient` options
483
570
 
484
571
  **All the arguments are optional.** But either `uri` or `transport` must be provided. We are trying to keep the highest possible DX here.
485
572
 
486
573
  - `uri`<br>
487
- The remote server address string. Out of box supported schemas are: `http`, `https`, `grpc`. (More to come.)
574
+ The remote server address string. Out of box supported schemas are: `http`, `https`, `grpc`, `bullmq`. (More to come.)
488
575
 
489
576
  - `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:/` then `HttpClientTransport` will be used. If starts with `grpc://` then `GrpcClientTransport` will be used.
577
+ 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
578
 
492
579
  - `neverThrow=true`<br>
493
580
  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 +740,7 @@ Yep.
653
740
  const { AllserverClient } = require("allserver");
654
741
 
655
742
  const client = AllserverClient({
656
- uri: "http://example.com:4000",
743
+ uri: "http://example.com:40000",
657
744
 
658
745
  async before(ctx) {
659
746
  console.log(ctx.procedureName, ctx.arg);
@@ -685,7 +772,7 @@ const { AllserverClient, HttpClientTransport } = require("allserver");
685
772
 
686
773
  const client = AllserverClient({
687
774
  transport: HttpClientTransport({
688
- uri: "http://my-server:4000",
775
+ uri: "http://my-server:40000",
689
776
  headers: { authorization: "Basic my-token" },
690
777
  }),
691
778
  });
@@ -712,7 +799,7 @@ If something more sophisticated is needed - you would need to mangle the `ctx` i
712
799
  const { AllserverClient } = require("allserver");
713
800
 
714
801
  const client = AllserverClient({
715
- uri: "http://my-server:4000",
802
+ uri: "http://my-server:40000",
716
803
  async before(ctx) {
717
804
  console.log(ctx.procedureName, ctx.arg);
718
805
  ctx.http.mode = "cors";
@@ -739,7 +826,7 @@ const MyAllserverClientWithAuth = AllserverClient.defaults({
739
826
  });
740
827
 
741
828
  const client = MyAllserverClientWithAuth({
742
- uri: "http://my-server:4000",
829
+ uri: "http://my-server:40000",
743
830
  });
744
831
  ```
745
832
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allserver",
3
- "version": "1.1.0",
3
+ "version": "1.2.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,11 @@
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",
38
39
  "lambda-local": "^1.7.3",
39
40
  "micro": "^9.3.4",
40
- "mocha": "^8.1.3",
41
+ "mocha": "^10.2.0",
41
42
  "node-fetch": "^2.6.1",
42
43
  "nyc": "^15.1.0",
43
44
  "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
+ });
package/src/index.js CHANGED
@@ -17,6 +17,9 @@ module.exports = {
17
17
  get GrpcTransport() {
18
18
  return require("./server/GrpcTransport");
19
19
  },
20
+ get BullmqTransport() {
21
+ return require("./server/BullmqTransport");
22
+ },
20
23
 
21
24
  // client
22
25
 
@@ -32,4 +35,7 @@ module.exports = {
32
35
  get GrpcClientTransport() {
33
36
  return require("./client/GrpcClientTransport");
34
37
  },
38
+ get BullmqClientTransport() {
39
+ return require("./client/BullmqClientTransport");
40
+ },
35
41
  };
@@ -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
+ });