UNPKG

31.7 kBJavaScriptView Raw
1"use strict";
2//
3// Shared microservices framework.
4//
5var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6 return new (P || (P = Promise))(function (resolve, reject) {
7 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
8 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
9 function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
10 step((generator = generator.apply(thisArg, _arguments || [])).next());
11 });
12};
13var __generator = (this && this.__generator) || function (thisArg, body) {
14 var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
15 return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
16 function verb(n) { return function (v) { return step([n, v]); }; }
17 function step(op) {
18 if (f) throw new TypeError("Generator is already executing.");
19 while (_) try {
20 if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
21 if (y = 0, t) op = [op[0] & 2, t.value];
22 switch (op[0]) {
23 case 0: case 1: t = op; break;
24 case 4: _.label++; return { value: op[1], done: false };
25 case 5: _.label++; y = op[1]; op = [0]; continue;
26 case 7: op = _.ops.pop(); _.trys.pop(); continue;
27 default:
28 if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
29 if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
30 if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
31 if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
32 if (t[2]) _.ops.pop();
33 _.trys.pop(); continue;
34 }
35 op = body.call(thisArg, _);
36 } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
37 if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
38 }
39};
40var __values = (this && this.__values) || function (o) {
41 var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0;
42 if (m) return m.call(o);
43 return {
44 next: function () {
45 if (o && i >= o.length) o = void 0;
46 return { value: o && o[i++], done: !o };
47 }
48 };
49};
50Object.defineProperty(exports, "__esModule", { value: true });
51var express = require("express");
52var amqp = require("amqplib");
53var axios_1 = require("axios");
54var request = require("request");
55var utils_1 = require("./utils");
56exports.asyncHandler = utils_1.asyncHandler;
57exports.retry = utils_1.retry;
58exports.sleep = utils_1.sleep;
59exports.verifyBodyParam = utils_1.verifyBodyParam;
60exports.verifyQueryParam = utils_1.verifyQueryParam;
61var morganBody = require('morgan-body');
62var http = require("http");
63var bodyParser = require("body-parser");
64var uuid = require("uuid");
65var perf_hooks_1 = require("perf_hooks");
66var inProduction = process.env.NODE_ENV === "production";
67var enableMorgan = !inProduction || process.env.ENABLE_MORGAN === "true";
68process.on('exit', function (code) {
69 if (code === 0) {
70 console.log("Process exited with code: " + code);
71 }
72 else {
73 console.error("Process exited with error code: " + code);
74 }
75});
76process.on('uncaughtException', function (err) {
77 console.error("Uncaught exception: " + (err && err.stack || err));
78 process.exit(1);
79});
80process.on('unhandledRejection', function (reason, promise) {
81 console.error('Unhandled promise rejection at:', reason.stack || reason);
82 process.exit(1);
83});
84//
85// Used to register an event handler to be setup after messaging system has started.
86//
87var EventHandler = /** @class */ (function () {
88 function EventHandler(eventName, eventHandlerFn) {
89 this.eventName = eventName;
90 this.eventHandlerFn = eventHandlerFn;
91 }
92 return EventHandler;
93}());
94//
95// Class that represents a particular microservice instance.
96//
97var MicroService = /** @class */ (function () {
98 function MicroService(config) {
99 var _this = this;
100 //
101 // The unique ID for the particular instance of the microservice.
102 //
103 this.id = uuid.v4();
104 //
105 // Event handlers that have been registered to be setup once
106 // connection to message queue is established.
107 ///
108 this.registeredEventHandlers = new Set();
109 /**
110 * Reference to the timer interface.
111 * Allows code to be timed for performance.
112 */
113 this.timer = {
114 start: function (timerName) {
115 // Just a stub for the moment.
116 },
117 stop: function (timerName) {
118 // Just a stub for the moment.
119 },
120 };
121 /**
122 * Reference to the metrics interface.
123 * Allows a service to output metrics.
124 */
125 this.metrics = {
126 discrete: function (name, value) {
127 // Just a stub for the moment.
128 },
129 continuous: function (name, value) {
130 // Just a stub for the moment.
131 },
132 };
133 this.config = Object.assign({}, config);
134 this.expressApp = express();
135 this.httpServer = new http.Server(this.expressApp);
136 //
137 // Allow middleware, but only under certain conditions.
138 //
139 function unless(pred, middleware) {
140 return function (req, res, next) {
141 if (pred(req)) {
142 next(); // Skip this middleware.
143 }
144 else {
145 middleware(req, res, next); // Invoke middleware.
146 }
147 };
148 }
149 this.expressApp.use(unless(function (req) { return req.method === "PUT"; }, bodyParser.json()));
150 this.expressApp.use(unless(function (req) { return req.method === "PUT"; }, bodyParser.urlencoded({ extended: true })));
151 if (enableMorgan) {
152 console.log("Enabling Morgan request tracing.");
153 morganBody(this.expressApp, {
154 noColors: true,
155 });
156 }
157 this.expressApp.get("/is-alive", function (req, res) {
158 res.json({ ok: true });
159 });
160 this.rest = {
161 /**
162 * Create a handler for incoming HTTP GET requests.
163 * Implemented by Express under the hood.
164 */
165 get: function (route, requestHandler) {
166 _this.expressApp.get(route, function (req, res) {
167 _this.verbose("Handling GET " + route);
168 var startTime = perf_hooks_1.performance.now();
169 requestHandler(req, res)
170 .then(function () {
171 _this.verbose("HTTP GET handler for " + route + " finished in " + (perf_hooks_1.performance.now() - startTime).toFixed(2) + "ms");
172 })
173 .catch(function (err) {
174 console.error("Error from handler: HTTP GET " + route);
175 console.error(err && err.stack || err);
176 if (!res.headersSent) {
177 res.sendStatus(500);
178 }
179 });
180 });
181 },
182 /**
183 * Create a handler for incoming HTTP POST requests.
184 * Implemented by Express under the hood.
185 */
186 post: function (route, requestHandler) {
187 _this.expressApp.post(route, function (req, res) {
188 _this.verbose("Handling POST " + route);
189 var startTime = perf_hooks_1.performance.now();
190 requestHandler(req, res)
191 .then(function () {
192 _this.verbose("HTTP POST handler for " + route + " finished in " + (perf_hooks_1.performance.now() - startTime).toFixed(2) + "ms");
193 })
194 .catch(function (err) {
195 console.error("Error from handler: HTTP POST " + route);
196 console.error(err && err.stack || err);
197 if (!res.headersSent) {
198 res.sendStatus(500);
199 }
200 });
201 });
202 },
203 /**
204 * Create a handler for incoming HTTP PUT requests.
205 * Implemented by Express under the hood
206 *
207 * @param route
208 * @param requestHandler
209 */
210 put: function (route, requestHandler) {
211 _this.expressApp.put(route, function (req, res) {
212 _this.verbose("Handling PUT " + route);
213 var startTime = perf_hooks_1.performance.now();
214 requestHandler(req, res)
215 .then(function () {
216 _this.verbose("HTTP PUT handler for " + route + " finished in " + (perf_hooks_1.performance.now() - startTime).toFixed(2) + "ms");
217 })
218 .catch(function (err) {
219 console.error("Error from handler: HTTP PUT " + route);
220 console.error(err && err.stack || err);
221 if (!res.headersSent) {
222 res.sendStatus(500);
223 }
224 });
225 });
226 },
227 /**
228 * Setup serving of static files.
229 *
230 * @param dirPath The path to the directory that contains static files.
231 */
232 static: function (dirPath) {
233 console.log("Serving static files from " + dirPath);
234 _this.expressApp.use(express.static(dirPath));
235 },
236 };
237 this.request = {
238 /**
239 * Make a HTTP get request to another service.
240 *
241 * @param serviceName The name (logical or host) of the service.
242 * @param route The HTTP route on the service to make the request to.
243 * @param params Query parameters for the request.
244 */
245 get: function (serviceName, route, body) { return __awaiter(_this, void 0, void 0, function () {
246 var url;
247 return __generator(this, function (_a) {
248 switch (_a.label) {
249 case 0:
250 url = "http://" + serviceName + route;
251 return [4 /*yield*/, axios_1.default.get(url, body)];
252 case 1: return [2 /*return*/, _a.sent()];
253 }
254 });
255 }); },
256 /**
257 * Make a HTTP get request to another service.
258 *
259 * @param serviceName The name (logical or host) of the service.
260 * @param route The HTTP route on the service to make the request to.
261 * @param params Query parameters for the request.
262 */
263 post: function (serviceName, route, body) { return __awaiter(_this, void 0, void 0, function () {
264 var url;
265 return __generator(this, function (_a) {
266 switch (_a.label) {
267 case 0:
268 url = "http://" + serviceName + route;
269 return [4 /*yield*/, axios_1.default.post(url, body)];
270 case 1: return [2 /*return*/, _a.sent()];
271 }
272 });
273 }); },
274 /**
275 * Forward HTTP get request to another named service.
276 * The response from the forward requests is automatically piped into the passed in response.
277 *
278 * @param serviceName The name of the service to forward the request to.
279 * @param route The HTTP GET route to forward to.
280 * @param params Query parameters for the request.
281 * @param res The stream to pipe response to.
282 */
283 forward: function (serviceName, route, req, res) {
284 var url = "http://" + serviceName + route;
285 req.pipe(request(url)).pipe(res);
286 }
287 };
288 }
289 //
290 // Helper method for verbose logging.
291 //
292 MicroService.prototype.verbose = function (msg) {
293 if (this.config.verbose) {
294 console.log(msg);
295 }
296 };
297 /**
298 * Get the name that identifies the service.
299 */
300 MicroService.prototype.getServiceName = function () {
301 return this.config.serviceName;
302 };
303 /**
304 * Get the instance ID for the particular instance of the service.
305 */
306 MicroService.prototype.getInstanceId = function () {
307 return this.id;
308 };
309 /**
310 * Returns true if the messaging system is currently available.
311 */
312 MicroService.prototype.isMessagingAvailable = function () {
313 return !!this.messagingConnection;
314 };
315 //
316 // Setup a RabbitMQ message handler.
317 //
318 MicroService.prototype.internalOn = function (eventHandler) {
319 return __awaiter(this, void 0, void 0, function () {
320 var eventName, queueName, messagingChannel, consumeCallback;
321 var _this = this;
322 return __generator(this, function (_a) {
323 switch (_a.label) {
324 case 0:
325 eventName = eventHandler.eventName;
326 // http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertExchange
327 return [4 /*yield*/, this.messagingChannel.assertExchange(eventName, "fanout", { durable: true })];
328 case 1:
329 // http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertExchange
330 _a.sent();
331 return [4 /*yield*/, this.messagingChannel.assertQueue("", { durable: true, exclusive: true })];
332 case 2:
333 queueName = (_a.sent()).queue;
334 eventHandler.queueName = queueName;
335 this.messagingChannel.bindQueue(queueName, eventName, "");
336 messagingChannel = this.messagingChannel;
337 this.verbose("Bound queue " + queueName + " to " + eventName + ".");
338 consumeCallback = function (msg) { return __awaiter(_this, void 0, void 0, function () {
339 var args, eventResponse;
340 return __generator(this, function (_a) {
341 switch (_a.label) {
342 case 0:
343 this.verbose("Handling event " + eventName);
344 args = JSON.parse(msg.content.toString());
345 eventResponse = {
346 ack: function () {
347 return __awaiter(this, void 0, void 0, function () {
348 return __generator(this, function (_a) {
349 messagingChannel.ack(msg);
350 return [2 /*return*/];
351 });
352 });
353 }
354 };
355 return [4 /*yield*/, eventHandler.eventHandlerFn(args, eventResponse)];
356 case 1:
357 _a.sent();
358 this.verbose(eventName + " handler done.");
359 return [2 /*return*/];
360 }
361 });
362 }); };
363 // http://www.squaremobius.net/amqp.node/channel_api.html#channel_consume
364 this.messagingChannel.consume(queueName, utils_1.asyncHandler(this, "ASYNC: " + eventName, consumeCallback), {
365 noAck: false,
366 });
367 this.verbose("Receiving events on queue " + eventName);
368 return [2 /*return*/];
369 }
370 });
371 });
372 };
373 //
374 // Unwind a RabbitMQ message handler.
375 //
376 MicroService.prototype.internalOff = function (eventHandler) {
377 this.messagingChannel.unbindQueue(eventHandler.queueName, eventHandler.eventName, "");
378 delete eventHandler.queueName;
379 };
380 /**
381 * Create an ongoing handler for a named incoming event.
382 * Implemented by Rabbitmq under the hood for reliable messaging.
383 *
384 * @param eventName The name of the event to handle.
385 * @param eventHandler Callback to be invoke when the incoming event is received.
386 */
387 MicroService.prototype.on = function (eventName, eventHandlerFn) {
388 return __awaiter(this, void 0, void 0, function () {
389 var eventHandler;
390 return __generator(this, function (_a) {
391 switch (_a.label) {
392 case 0:
393 eventHandler = new EventHandler(eventName, eventHandlerFn);
394 this.registeredEventHandlers.add(eventHandler);
395 if (!this.messagingConnection) return [3 /*break*/, 2];
396 //
397 // Message system already started.
398 //
399 return [4 /*yield*/, this.internalOn(eventHandler)];
400 case 1:
401 //
402 // Message system already started.
403 //
404 _a.sent();
405 _a.label = 2;
406 case 2: return [2 /*return*/, eventHandler];
407 }
408 });
409 });
410 };
411 /**
412 * Unregister a previously register event handler.
413 *
414 * @param handler The event handler to unregister.
415 */
416 MicroService.prototype.off = function (handler) {
417 var eventHandler = handler;
418 if (this.messagingConnection) {
419 this.internalOff(eventHandler);
420 }
421 this.registeredEventHandlers.delete(eventHandler);
422 };
423 /**
424 * Create a once-off handler for a named incoming event.
425 * The event handler will only be invoke once before the event is unregistered.
426 * Implemented by Rabbitmq under the hood for reliable messaging.
427 *
428 * @param eventName The name of the event to handle.
429 * @param eventHandler Callback to be invoke when the incoming event is received.
430 */
431 MicroService.prototype.once = function (eventName, eventHandlerFn) {
432 return __awaiter(this, void 0, void 0, function () {
433 var eventHandler;
434 var _this = this;
435 return __generator(this, function (_a) {
436 switch (_a.label) {
437 case 0: return [4 /*yield*/, this.on(eventName, function (args, res) { return __awaiter(_this, void 0, void 0, function () {
438 return __generator(this, function (_a) {
439 switch (_a.label) {
440 case 0:
441 this.off(eventHandler); // Unregister before we receive any more events.
442 return [4 /*yield*/, eventHandlerFn(args, res)];
443 case 1:
444 _a.sent(); // Trigger user callback.
445 return [2 /*return*/];
446 }
447 });
448 }); })];
449 case 1:
450 eventHandler = _a.sent();
451 return [2 /*return*/];
452 }
453 });
454 });
455 };
456 /***
457 * Wait for a single incoming event, returns the events arguments and then unregister the event handler.
458 *
459 * @param eventName The name of the event to handle.
460 *
461 * @returns A promise to resolve the incoming event's arguments.
462 */
463 MicroService.prototype.waitForOneEvent = function (eventName) {
464 var _this = this;
465 return new Promise(function (resolve) {
466 _this.once(eventName, function (args, res) { return __awaiter(_this, void 0, void 0, function () {
467 return __generator(this, function (_a) {
468 res.ack(); // Ack the response.
469 resolve(args); // Resolve event args through the promise.
470 return [2 /*return*/];
471 });
472 }); });
473 });
474 };
475 /**
476 * Emit a named outgoing event.
477 * Implemented by Rabbitmq under the hood for reliable messaging.
478 *
479 * @param eventName The name of the event to emit.
480 * @param eventArgs Event args to publish with the event and be received at the other end.
481 */
482 MicroService.prototype.emit = function (eventName, eventArgs) {
483 return __awaiter(this, void 0, void 0, function () {
484 return __generator(this, function (_a) {
485 switch (_a.label) {
486 case 0:
487 if (!this.messagingConnection) {
488 throw new Error("Messaging system currently unavailable.");
489 }
490 // http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertExchange
491 return [4 /*yield*/, this.messagingChannel.assertExchange(eventName, "fanout", { durable: true, })];
492 case 1:
493 // http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertExchange
494 _a.sent();
495 this.verbose("Sending message: " + eventName);
496 this.verbose(JSON.stringify(eventArgs, null, 4));
497 // http://www.squaremobius.net/amqp.node/channel_api.html#channel_publish
498 this.messagingChannel.publish(eventName, '', new Buffer(JSON.stringify(eventArgs)), {
499 persistent: true,
500 }); //TODO: Probably a more efficient way to do this! Maybe BSON?
501 return [2 /*return*/];
502 }
503 });
504 });
505 };
506 /**
507 * Start the HTTP server.
508 * ! No need to call this if you already called 'start'.
509 */
510 MicroService.prototype.startHttpServer = function () {
511 return __awaiter(this, void 0, void 0, function () {
512 var _this = this;
513 return __generator(this, function (_a) {
514 return [2 /*return*/, new Promise(function (resolve, reject) {
515 var host;
516 if (_this.config.host !== undefined) {
517 host = _this.config.host;
518 }
519 else {
520 host = process.env.HOST || '0.0.0.0';
521 }
522 var port;
523 if (_this.config.port !== undefined) {
524 port = _this.config.port;
525 }
526 else {
527 port = (process.env.PORT && parseInt(process.env.PORT)) || 3000;
528 }
529 _this.httpServer.listen(port, host, function (err) {
530 if (err) {
531 reject(err);
532 }
533 else {
534 _this.verbose("Running on http://" + port + ":" + host);
535 resolve();
536 }
537 });
538 })];
539 });
540 });
541 };
542 /**
543 * Start the RabbitMQ message queue.
544 * ! No need to call this if you already called 'start'.
545 */
546 MicroService.prototype.startMessaging = function () {
547 return __awaiter(this, void 0, void 0, function () {
548 var initMessaging;
549 var _this = this;
550 return __generator(this, function (_a) {
551 switch (_a.label) {
552 case 0:
553 initMessaging = function () { return __awaiter(_this, void 0, void 0, function () {
554 var e_1, _a, messagingHost, _b, _c, _d, _e, eventHandler, e_1_1;
555 var _this = this;
556 return __generator(this, function (_f) {
557 switch (_f.label) {
558 case 0:
559 if (this.config.messagingHost) {
560 messagingHost = this.config.messagingHost;
561 }
562 else {
563 messagingHost = process.env.MESSAGING_HOST || "amqp://guest:guest@localhost:5672";
564 }
565 this.verbose("Connecting to messaging server at: " + messagingHost);
566 _b = this;
567 return [4 /*yield*/, utils_1.retry(function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
568 switch (_a.label) {
569 case 0: return [4 /*yield*/, amqp.connect(messagingHost)];
570 case 1: return [2 /*return*/, _a.sent()];
571 }
572 }); }); }, 10000, 1000)];
573 case 1:
574 _b.messagingConnection = _f.sent();
575 this.messagingConnection.on("error", function (err) {
576 console.error("Error from message system.");
577 console.error(err && err.stack || err);
578 });
579 this.messagingConnection.on("close", function (err) {
580 _this.messagingConnection = undefined;
581 _this.messagingChannel = undefined;
582 console.log("Lost connection to rabbit, waiting for restart.");
583 initMessaging()
584 .then(function () { return console.log("Restarted messaging."); })
585 .catch(function (err) {
586 console.error("Failed to restart messaging.");
587 console.error(err && err.stack || err);
588 });
589 });
590 _c = this;
591 return [4 /*yield*/, this.messagingConnection.createChannel()];
592 case 2:
593 _c.messagingChannel = _f.sent();
594 _f.label = 3;
595 case 3:
596 _f.trys.push([3, 8, 9, 10]);
597 _d = __values(this.registeredEventHandlers.values()), _e = _d.next();
598 _f.label = 4;
599 case 4:
600 if (!!_e.done) return [3 /*break*/, 7];
601 eventHandler = _e.value;
602 return [4 /*yield*/, this.internalOn(eventHandler)];
603 case 5:
604 _f.sent();
605 _f.label = 6;
606 case 6:
607 _e = _d.next();
608 return [3 /*break*/, 4];
609 case 7: return [3 /*break*/, 10];
610 case 8:
611 e_1_1 = _f.sent();
612 e_1 = { error: e_1_1 };
613 return [3 /*break*/, 10];
614 case 9:
615 try {
616 if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
617 }
618 finally { if (e_1) throw e_1.error; }
619 return [7 /*endfinally*/];
620 case 10: return [2 /*return*/];
621 }
622 });
623 }); };
624 return [4 /*yield*/, initMessaging()];
625 case 1:
626 _a.sent();
627 return [2 /*return*/];
628 }
629 });
630 });
631 };
632 /**
633 * Starts the microservice.
634 * It starts listening for incoming HTTP requests and events.
635 */
636 MicroService.prototype.start = function () {
637 return __awaiter(this, void 0, void 0, function () {
638 return __generator(this, function (_a) {
639 switch (_a.label) {
640 case 0: return [4 /*yield*/, this.startHttpServer()];
641 case 1:
642 _a.sent();
643 return [4 /*yield*/, this.startMessaging()];
644 case 2:
645 _a.sent();
646 return [2 /*return*/];
647 }
648 });
649 });
650 };
651 return MicroService;
652}());
653exports.MicroService = MicroService;
654/**
655 * Instantiates a microservice.
656 *
657 * @param [config] Optional configuration for the microservice.
658 */
659function micro(config) {
660 return new MicroService(config);
661}
662exports.micro = micro;
663//# sourceMappingURL=index.js.map
\No newline at end of file