UNPKG

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