├── .travis.yml ├── .gitignore ├── test ├── server.js ├── request.js ├── rcoil.js └── rcoil-server.js ├── src ├── devnull-logger.js ├── console-logger.js ├── rcoil.js ├── execution-context.js ├── execution-director.js ├── request.js └── players.js ├── index.js ├── package.json ├── LICENSE └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | - "4.0" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Documentation 36 | out -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var bodyParser = require('body-parser'); 3 | var app = express(); 4 | 5 | app.use(bodyParser.json()); 6 | 7 | app.get("/users", function(req, res) { 8 | res.send([ 9 | { 10 | username : "user1", 11 | password: "xxxxx" 12 | }, 13 | { 14 | username : "user2", 15 | password: "xxxxx" 16 | }, 17 | { 18 | username : "user3", 19 | password: "xxxxx" 20 | } 21 | ]); 22 | }); 23 | 24 | app.get("/users/user1", function(req, res) { 25 | res.send({ 26 | username : "user1", 27 | password: "xxxxx", 28 | address: [ 29 | "the rainy corner", 30 | "42 Galaxy way", 31 | "planet earth" 32 | ] 33 | }); 34 | }); 35 | 36 | app.post("/users", function(req, res) { 37 | res.send({ 38 | id: req.body.id 39 | }); 40 | }); 41 | 42 | app.get("/headers", function(req, res) { 43 | res.send({ 44 | headers: req.headers 45 | }); 46 | }); 47 | 48 | module.exports = function() { 49 | app.listen(3000); 50 | } -------------------------------------------------------------------------------- /src/devnull-logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var colors = require('colors'); 18 | var moment = require('moment'); 19 | 20 | var DevNullLogger = function () { 21 | 22 | }; 23 | 24 | DevNullLogger.prototype.info = function (message) { 25 | return; 26 | }; 27 | 28 | DevNullLogger.prototype.debug = function (message) { 29 | return; 30 | }; 31 | 32 | DevNullLogger.prototype.warn = function (message) { 33 | return; 34 | }; 35 | 36 | DevNullLogger.prototype.error = function (message) { 37 | return; 38 | }; 39 | 40 | module.exports = DevNullLogger; 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var Rcoil = require('./src/rcoil'); 18 | var ExecutionDirector = require('./src/execution-director'); 19 | var Request = require('./src/request'); 20 | var ConsoleLogger = require('./src/console-logger'); 21 | var DevNullLogger = require('./src/devnull-logger'); 22 | var Players = require('./src/players'); 23 | 24 | module.exports = { 25 | Rcoil : Rcoil, 26 | ExecutionDirector : ExecutionDirector, 27 | Request : Request, 28 | ConsoleLogger : ConsoleLogger, 29 | DevNullLogger : DevNullLogger, 30 | Players: Players 31 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rcoil", 3 | "version": "0.0.3", 4 | "description": "Rcoil is an orchestration library that makes it easy to call multiple APIs or AWS Lambda functions and aggregate the output into a single response.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --reporter spec" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/SAPessi/rcoil.git" 12 | }, 13 | "keywords": [ 14 | "Amazon API Gateway", 15 | "API Gateway", 16 | "AWS", 17 | "Lambda", 18 | "API", 19 | "orchestration", 20 | "backend" 21 | ], 22 | "author": "Stefano Buliani", 23 | "license": "Apache-2.0", 24 | "bugs": { 25 | "url": "https://github.com/SAPessi/rcoil/issues" 26 | }, 27 | "homepage": "https://github.com/SAPessi/rcoil#readme", 28 | "dependencies": { 29 | "aws-sdk": "^2.2.33", 30 | "cli-table": "^0.3.1", 31 | "colors": "^1.1.2", 32 | "moment": "^2.11.1", 33 | "rwlock": "^5.0.0", 34 | "valid-url": "^1.0.9" 35 | }, 36 | "devDependencies": { 37 | "chai": "^3.5.0", 38 | "mocha": "^2.4.5", 39 | "express": "^4.13.4", 40 | "body-parser": "^1.14.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/request.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | 3 | var R = require("../src/request"); 4 | 5 | var apiUrl = "http://api.com/test"; 6 | 7 | describe("Test request object", function() { 8 | describe("Static utility functions", function() { 9 | it("Create a request with wrong url", function() { 10 | var fn = function() { R.get("test", "resd/asd*ads") }; 11 | expect(fn).to.throw(Error); 12 | }); 13 | it("Creates a GET request", function() { 14 | var req = R.get("test", apiUrl); 15 | expect(req).not.to.be.null; 16 | expect(req.settings.method).to.equal(R.HttpVerb.GET); 17 | expect(req.name).to.equal("test"); 18 | expect(req.settings.host).to.equal("api.com"); 19 | }); 20 | it("Creates a POST request", function() { 21 | var req = R.post("test", apiUrl); 22 | expect(req.settings.method).to.equal(R.HttpVerb.POST); 23 | }); 24 | it("Request with invalid configuration", function() { 25 | var fn = function() { R.get("test", { 26 | host: "api.com", 27 | method: "GET" 28 | })}; 29 | expect(fn).to.throw(Error); 30 | }); 31 | it("Set input function", function() { 32 | var req = R.get("test", "http://api.com/test").onInput(function(context, requestData) { 33 | var data = context.requestData("test", "test"); 34 | return null; 35 | }); 36 | 37 | expect(req.inputFunc).not.to.be.null; 38 | }); 39 | it("Lambda function setup", function() { 40 | var lambdaArn = "arn:aws:xxxxxxxx:lambda:asd"; 41 | var qualifier = "dev" 42 | var req = R.lambda("test", lambdaArn, qualifier); 43 | 44 | expect(req.settings.function).to.equal(lambdaArn); 45 | expect(req.settings.qualifier).to.equal(qualifier); 46 | }); 47 | it("Lambda function without qualifier", function() { 48 | var lambdaArn = "arn:aws:xxxxxxxx:lambda:asd"; 49 | var req = R.lambda("test", lambdaArn); 50 | 51 | expect(req.settings.function).to.equal(lambdaArn); 52 | expect(req.settings.qualifier).to.equal("$LATEST"); 53 | }); 54 | }); 55 | }); -------------------------------------------------------------------------------- /src/console-logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var colors = require('colors'); 18 | var moment = require('moment'); 19 | 20 | /** 21 | * Logger object that sends output to the stdout (console.log) and uses 22 | * the colors package to highlight info, debug, warn, and error lines. 23 | * 24 | * @class 25 | * @constructor 26 | */ 27 | var ConsoleLogger = function () { 28 | 29 | }; 30 | 31 | /** 32 | * Return a formatted timestamp string for the logs 33 | * 34 | * @function 35 | * @return {String} A formatted timestamp string as [YYYY-MM-DD HH:mm:ss.SS] 36 | */ 37 | ConsoleLogger.prototype.timestamp = function () { 38 | return "[" + moment().format('YYYY-MM-DD HH:mm:ss.SS') + "]: "; 39 | }; 40 | 41 | /** 42 | * Logs an info message 43 | * 44 | * @function 45 | * @param {String} message - The message 46 | */ 47 | ConsoleLogger.prototype.info = function (message) { 48 | console.log(this.timestamp() + message.blue); 49 | }; 50 | 51 | /** 52 | * Logs a debug message 53 | * 54 | * @function 55 | * @param {String} message - The message 56 | */ 57 | ConsoleLogger.prototype.debug = function (message) { 58 | console.log(this.timestamp() + message.magenta); 59 | }; 60 | 61 | /** 62 | * Logs a warn message 63 | * 64 | * @function 65 | * @param {String} message - The message 66 | */ 67 | ConsoleLogger.prototype.warn = function (message) { 68 | console.log(this.timestamp() + message.yellow); 69 | }; 70 | 71 | /** 72 | * Logs an error message 73 | * 74 | * @function 75 | * @param {String} message - The message 76 | */ 77 | ConsoleLogger.prototype.error = function (message) { 78 | console.log(this.timestamp() + message.red); 79 | }; 80 | 81 | module.exports = ConsoleLogger; 82 | -------------------------------------------------------------------------------- /test/rcoil.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | 3 | var Rcoil = require("../src/rcoil"); 4 | var R = require("../src/request"); 5 | 6 | // new coil 7 | var coil = new Rcoil(); 8 | 9 | describe("Rcoil tree structure", function() { 10 | describe("Start a tree structure", function() { 11 | it("Adds a request without creating a group", function() { 12 | var fn = function() { coil.addRequest(R.get("test", "http://api.com/test")) }; 13 | expect(fn).to.throw(Error); 14 | }); 15 | it("Start a new group", function() { 16 | coil = coil.startGroup("testGroup"); 17 | expect(coil._tmpPositionId).to.equal("testGroup"); 18 | expect(coil.calls[0].id).to.equal("testGroup"); 19 | }); 20 | it("Looks up request group in a simple tree", function() { 21 | expect(coil._findRequestGroup(coil.calls, "testGroup")).not.to.be.null; 22 | }); 23 | it("Looks up a non existing request group", function() { 24 | expect(coil._findRequestGroup(coil.calls, "testGroupFake")).to.be.null; 25 | }); 26 | it("Add a request", function() { 27 | coil = coil.addRequest(R.get("test", "http://api.com/test")); 28 | expect(coil.calls[0].requests[0]).not.to.be.null; 29 | expect(coil.calls[0].requests[0].type).to.equal(R.RequestType.HTTP); 30 | }); 31 | it("Count of request groups", function() { 32 | coil.startGroup("secondGroup"); 33 | expect(coil._tmpPositionId).to.equal("secondGroup"); 34 | expect(coil._totalRequestGroups).to.equal(2); 35 | }); 36 | it("Lookup request in a 2 level tree", function() { 37 | expect(coil._findRequestGroup(coil.calls, "secondGroup")).not.to.be.null; 38 | }); 39 | it("Verify tree structure", function() { 40 | expect(coil.calls[0].children.length).to.equal(1); 41 | expect(coil.calls[0].children[0].id).to.equal("secondGroup"); 42 | }); 43 | it("Create empty group", function() { 44 | var group = coil._createEmptyRequestGroup("empty"); 45 | expect(group).to.be.instanceof(Object); 46 | expect(group.id).to.equal("empty"); 47 | expect(group.requests).to.be.instanceof(Array); 48 | expect(group.requests.length).to.equal(0); 49 | expect(group.children).to.be.instanceof(Array); 50 | expect(group.children.length).to.equal(0); 51 | }); 52 | it("Send invalid request object", function() { 53 | var fn = function() { 54 | coil.addRequest({hello:"World"}); 55 | } 56 | expect(fn).to.throw(Error); 57 | }); 58 | it("Reset position", function() { 59 | coil.fromTheBeginning(); 60 | expect(coil._tmpPositionId).to.equal(""); 61 | }); 62 | it("Group count", function() { 63 | var count = coil.requestGroupsCount() 64 | expect(count).to.equal(2); 65 | }); 66 | }); 67 | }); -------------------------------------------------------------------------------- /test/rcoil-server.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | 3 | var Rcoil = require('../src/rcoil'); 4 | var ExecutionDirector = require('../src/execution-director'); 5 | var R = require('../src/request'); 6 | var DevNullLogger = require('../src/devnull-logger'); 7 | var server = require("./server"); 8 | 9 | server(); 10 | 11 | function singleCallCoil(groupName, requestName) { 12 | var coil = new Rcoil(); 13 | coil.startGroup(groupName).addRequest(R.get(requestName, "http://localhost:3000/users")); 14 | var director = new ExecutionDirector(coil, { 15 | logger: new DevNullLogger(), 16 | debug: true 17 | }); 18 | return director; 19 | } 20 | 21 | function sequentialCallCoil() { 22 | var coil = new Rcoil(); 23 | coil.startGroup("group1").addRequest(R.get("request1", "http://localhost:3000/users")); 24 | coil.startGroup("group2").addRequest(R.get("request2", "http://localhost:3000/users/user1")); 25 | var director = new ExecutionDirector(coil, { 26 | logger: new DevNullLogger(), 27 | debug: true 28 | }); 29 | return director; 30 | } 31 | 32 | function cancelCallCoil() { 33 | var coil = new Rcoil(); 34 | coil.startGroup("group1").addRequest(R.get("request1", "http://localhost:3000/users").onInput(function(ctx) { 35 | return false; 36 | })); 37 | var director = new ExecutionDirector(coil, { 38 | logger: new DevNullLogger(), 39 | debug: true 40 | }); 41 | return director; 42 | } 43 | 44 | 45 | describe("Test Rcoil against local express server", function () { 46 | describe("Users API", function () { 47 | it("calls a single api", function () { 48 | var director = singleCallCoil("first", "local"); 49 | director.start(function (context) { 50 | var responseData = context.requestData("first", "local"); 51 | var output = JSON.parse(responseData.body); 52 | 53 | expect(output.length).to.equal(3); 54 | }); 55 | }); 56 | it("group with space in id", function() { 57 | var director = singleCallCoil("first group with space", "local"); 58 | director.start(function(context) { 59 | var responseData = context.requestData("first group with space", "local"); 60 | var output = JSON.parse(responseData.body); 61 | 62 | expect(output.length).to.equal(3); 63 | }); 64 | }); 65 | it("request with space in id", function() { 66 | var director = singleCallCoil("first group with space", "local request"); 67 | director.start(function(context) { 68 | var responseData = context.requestData("first group with space", "local request"); 69 | var output = JSON.parse(responseData.body); 70 | 71 | expect(output.length).to.equal(3); 72 | }); 73 | }); 74 | it("sequential requests", function() { 75 | var director = sequentialCallCoil(); 76 | 77 | director.on("groupStart", function(group, context) { 78 | if (group.id == "group1") { 79 | expect(context.requestData("group2", "request2")).to.be.null; 80 | } 81 | if (group.id == "group2") { 82 | expect(context.requestData("group1", "request1")).not.to.be.null; 83 | } 84 | }); 85 | director.start(function(context) { 86 | var responseData = context.responseData("group2", "request2"); 87 | var output = JSON.parse(responseData); 88 | expect(output.username).to.equal("user1"); 89 | }); 90 | }); 91 | it("Custom headers in request", function() { 92 | var coil = new Rcoil(); 93 | coil.startGroup("test").addRequest( 94 | R.get("test", "http://localhost:3000/users").onInput(function(context, request) { 95 | request.setHeader("x-custom", "custom"); 96 | return null; 97 | })); 98 | var director = new ExecutionDirector(coil, { 99 | logger: new DevNullLogger(), 100 | debug: true 101 | }); 102 | director.start(function(context) { 103 | var body = JSON.parse(context.responseData("test", "test")); 104 | expect(body.headers["x-custom"]).not.to.be.null; 105 | }); 106 | 107 | }); 108 | it("flow control methods - cancel coil", function() { 109 | var director = cancelCallCoil(); 110 | 111 | director.start(function(ctx) { 112 | resp = ctx.responseData("group1", "request1"); 113 | expect(resp.isCanceled).to.equal(true); 114 | }) 115 | }) 116 | }); 117 | }); -------------------------------------------------------------------------------- /src/rcoil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var Table = require('cli-table'); 18 | var colors = require('colors'); 19 | 20 | const Request = require('./request'); 21 | 22 | /** 23 | * The main Rcoil object. This is a tree structure made of request groups and requests. 24 | * Multiple requests and request groups can be executed simulatneously. 25 | * 26 | * An Rcoil is meant to be interacted with in a more "human" way using the startGroup, 27 | * afterGroup, and addRequest methods. 28 | * 29 | * @class 30 | * @constructor 31 | */ 32 | function Rcoil() { 33 | /** 34 | * The tree structure containing request groups and requests. 35 | * 36 | * @property {Array} 37 | */ 38 | this.calls = []; 39 | 40 | /** 41 | * temporary variable to store the current position in the tree during the 42 | * creation by the start/after/add methods 43 | * 44 | * @property {String} 45 | */ 46 | this._tmpPositionId = ""; 47 | 48 | /** 49 | * count of the number of request groups in the Rcoil. This is used by the 50 | * director to confirm when the execution is completed 51 | * 52 | * @property {int} 53 | */ 54 | this._totalRequestGroups = 0; 55 | }; 56 | 57 | /** 58 | * the process function needs to return an object with 2 properties: 59 | * a boolean to tell the walker to return and a value to return. 60 | * 61 | * @example 62 | * function(key, value) { 63 | * return { shouldReturn: false, value: value } 64 | * } 65 | * 66 | * @callback treeWalkerCallback 67 | * @param {*} key - The key of the current property in the tree 68 | * @param {*} value - The value for the property 69 | */ 70 | 71 | /** 72 | * Walks the tree to find an object. It applies the process function 73 | * to each object in the tree. 74 | * 75 | * @function 76 | * @private 77 | * @param {Object} o - The tree to walk 78 | * @param {treeWalkerCallback} process - A callback function to process the node 79 | * 80 | * @returns {*} Whatever the process function decides to return 81 | */ 82 | Rcoil.prototype._traverseTree = function (o, process) { 83 | var result; 84 | for (var i in o) { 85 | if (typeof (o[i]) !== 'object') 86 | continue; 87 | 88 | result = process(i, o[i]); 89 | if (result.shouldReturn) { 90 | return result.value; 91 | } 92 | 93 | if (o[i] !== null && typeof (o[i]) === 'object') { 94 | var subObject = this._traverseTree(o[i], process); 95 | if (subObject != null) 96 | return subObject; 97 | } 98 | } 99 | 100 | return null; 101 | }; 102 | 103 | /** 104 | * Finds and returns a request group inside a tree 105 | * 106 | * @Function 107 | * @private 108 | * @param {Object} o - The tree to walk 109 | * @param {String} groupId - The request group id to look for in the tree 110 | * 111 | * @returns {Object} A request group object 112 | */ 113 | Rcoil.prototype._findRequestGroup = function (o, groupId) { 114 | return this._traverseTree(o, function (key, value) { 115 | var output = { 116 | shouldReturn: false, 117 | value: null 118 | }; 119 | 120 | if (typeof (value) === 'object' && value != null && value.id == groupId) { 121 | output.shouldReturn = true; 122 | output.value = value; 123 | } 124 | 125 | return output; 126 | }); 127 | }; 128 | 129 | /** 130 | * Creates a new empty request group object with the given name 131 | * 132 | * @function 133 | * @private 134 | * @param {String} name - The name for the new request group 135 | * 136 | * @return {Object} a populated empty request group object 137 | */ 138 | Rcoil.prototype._createEmptyRequestGroup = function (groupId) { 139 | return { 140 | id: groupId, 141 | requests: [], 142 | children: [] 143 | }; 144 | }; 145 | 146 | /** 147 | * Starts a new request group in the tree, the group will be a child of the 148 | * current group (or root of the tree). Request groups created as children are 149 | * executed sequentially. 150 | * 151 | * To create two parallel request groups use the afterGroup method to return to 152 | * the previous node of the tree. 153 | * @example 154 | * rcoil 155 | * .startGroup("first") 156 | * .startGroup("firstChild") 157 | * .afterGroup("first") // going back to the first request group 158 | * .startGroup("secondChild"); 159 | * // requests within firstChild and secondChild will be execute simultaneously 160 | * 161 | * @function 162 | * @param {String} groupId - A unique id for the request group in the tree 163 | * 164 | * @return {Object} The Rcoil object 165 | */ 166 | Rcoil.prototype.startGroup = function (groupId) { 167 | if (this._findRequestGroup(this.calls, groupId) != null) { 168 | throw new Error("Request Group " + groupId + " already exists in your Symphony"); 169 | } 170 | 171 | if (this._tmpPositionId != "") { 172 | this._findRequestGroup(this.calls, this._tmpPositionId).children.push(this._createEmptyRequestGroup(groupId)) 173 | } else { 174 | this.calls.push(this._createEmptyRequestGroup(groupId)); 175 | } 176 | this._tmpPositionId = groupId; 177 | this._totalRequestGroups++; 178 | 179 | return this; 180 | }, 181 | 182 | /** 183 | * Returns to the given position in the tree. 184 | * 185 | * @function 186 | * @param {String} groupId - The name of the node we need to walk back to 187 | * 188 | * @return {Object} The symphony object 189 | */ 190 | Rcoil.prototype.afterGroup = function (groupId) { 191 | this._tmpPositionId = groupId; 192 | 193 | return this; 194 | }, 195 | 196 | /** 197 | * Adds a request to the current request group (started by startGroup or moved to by 198 | * afterGroup). 199 | * 200 | * @example 201 | * rcoil 202 | * .startGroup("testGroup") 203 | * .addRequest(Request.get("listUsers", "https://api.com/listUsers")); 204 | * 205 | * @function 206 | * @param {Request} request - A configured request object 207 | * 208 | * @return {Object} The symphony object 209 | */ 210 | Rcoil.prototype.addRequest = function (request) { 211 | if (this._tmpPositionId == "") { 212 | throw new Error("Cannot add requests without starting a group first"); 213 | } 214 | 215 | if (!(request instanceof Request)) { 216 | throw new Error("Invalid Request object"); 217 | } 218 | 219 | this._findRequestGroup(this.calls, this._tmpPositionId).requests.push(request); 220 | 221 | return this; 222 | }, 223 | 224 | /** 225 | * Returns the walker to the root node of the tree 226 | * 227 | * @function 228 | * @return {Object} The symphony object 229 | */ 230 | Rcoil.prototype.fromTheBeginning = function () { 231 | this._tmpPositionId = ""; 232 | 233 | return this; 234 | }, 235 | 236 | /** 237 | * Retrieves the total number of request groups in the this symphony 238 | * 239 | * @function 240 | * @return {int} The number of request groups 241 | */ 242 | Rcoil.prototype.requestGroupsCount = function () { 243 | return this._totalRequestGroups; 244 | } 245 | 246 | /** 247 | * Uses console log and unicode tables to print out the structure of the coil 248 | * 249 | * @function 250 | */ 251 | Rcoil.prototype.printCoil = function() { 252 | console.log("Rcoil execution plan".white.bold); 253 | 254 | for (var i = 0; i < this.calls.length; i++) { 255 | this._printRequestGroup(this.calls[i], 1); 256 | } 257 | } 258 | 259 | /** 260 | * Prints a request group as a unicode table to the console 261 | * 262 | * @function 263 | * @private 264 | * @param {Object} group - The request group to be printed 265 | * @param {int} level - The depth of the current group in the coil, this sets the spacing from the right 266 | */ 267 | Rcoil.prototype._printRequestGroup = function(group, level) { 268 | var spacer = ""; 269 | for (var i = 0; i < level; i++) spacer += " "; 270 | // TODO: Spacer doesn't work nicely with the Table here. 271 | console.log((spacer + "↪ Request Group: " + group.id).blue.bold); 272 | 273 | var table = new Table({ 274 | chars: { 'top': '═' , 'top-mid': '╤' , 'top-left': '╔' , 'top-right': '╗' 275 | , 'bottom': '═' , 'bottom-mid': '╧' , 'bottom-left': '╚' , 'bottom-right': '╝' 276 | , 'left': '║' , 'left-mid': '╟' , 'mid': '─' , 'mid-mid': '┼' 277 | , 'right': '║' , 'right-mid': '╢' , 'middle': '│' } 278 | }); 279 | 280 | for (var i = 0; i < group.requests.length; i++) { 281 | var request = group.requests[i]; 282 | table.push( 283 | [request.name, request.type, request.getUrl()] 284 | ); 285 | } 286 | 287 | var tableString = table.toString(); 288 | tableString = tableString.replace(new RegExp(/^/gm), spacer); 289 | console.log(tableString); 290 | 291 | for (var i = 0; i < group.children.length; i++) { 292 | this._printRequestGroup(group.children[i], level+1); 293 | } 294 | } 295 | 296 | module.exports = Rcoil; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rcoil 2 | Rcoil is an orchestration library that makes it easy to call multiple APIs or AWS Lambda functions and aggregate the output into a single response. 3 | 4 | Rcoil was originally built to help Amazon API Gateway customers orchestrate requests to complex backends into a single API call. 5 | 6 | [![Build Status](https://travis-ci.org/SAPessi/rcoil.svg?branch=master)](https://travis-ci.org/SAPessi/rcoil) 7 | 8 | # Installation 9 | To install Rcoil run 10 | ``` 11 | npm install rcoil 12 | ``` 13 | 14 | # Usage 15 | The main object is Rcoil. Rcoil is a tree structure of request groups and requests. Request groups can contain multiple requests and have child group. 16 | 17 | Requests within a request group are executed simultaneously and. Similarly, request groups can have multiple request groups as children, the child request groups are also executed simultaneously. 18 | 19 | The `ExecutionDirector` object runs an Rcoil and the callback passed to the `start()` method has access to the aggregated results in an `ExecutionContext` object. 20 | 21 | The first step is to import the Rcoil module. The Rcoil module includes multiple packages, the main `Rcoil` object, the `ExecutionDirector`, and the `Request` object. 22 | 23 | ```javascript 24 | var rcoil = require('rcoil'); 25 | 26 | var Rcoil = rcoil.Rcoil; 27 | var ExecutionDirector = rcoil.ExecutionDirector; 28 | var Request = rcoil.Request; 29 | var ConsoleLogger = rcoil.ConsoleLogger; 30 | ``` 31 | 32 | Once you have imported the module you can setup the first Rcoil 33 | 34 | ```javascript 35 | var coil = new Rcoil(); 36 | 37 | coil 38 | // start a new group of requests, these will be executed simultaneously 39 | .startGroup("firstRequestGroup") 40 | .addRequest(Request.get("firstSimultaneousRequest", "http://localhost:3000/users")) 41 | .addRequest(Request.get("secondSimultaneousRequest", "http://localhost:3000/pets")) 42 | // adds a child group to the firstRequestGroup. These requests will be executed 43 | // once the requests from the firstRequestGroup are completed. 44 | .startGroup("childRequestGroup") 45 | .addRequest( 46 | Request.post("createPetRequest", "http://localhost:3000/pets") 47 | // this callback is executed when the createPetRequest needs an input body. 48 | // the context variable contains all of the requests and responses from the 49 | // previous calls 50 | .onInput(function(context) { 51 | var responseData = JSON.parse(context.responseData("firstRequestGroup", "firstSimultaneouRequest").body); 52 | return responseData.id; 53 | 54 | // You can also return false from this method to cancel the execution of the request 55 | }); 56 | ); 57 | 58 | var director = new ExecutionDirector(coil, { 59 | logger: new ConsoleLogger(), 60 | debug: true 61 | }); 62 | 63 | // The callback function is called once the director has finished executing the coil 64 | director.start(function(context) { 65 | var responseData = context.responseData("childRequestGroup", "createPetRequest"); 66 | var output = JSON.parse(responseData.body); 67 | 68 | lambdaContext.succeed(output); 69 | }); 70 | ``` 71 | 72 | ## The Rcoil object 73 | Rcoil is the main data structure that contains all of the request groups and their requests. Data within the `Rcoil` object is stored as a tree inside the `calls` array. The `calls` property is an array of request groups. Request group in turn can contain multiple `Request` objects and other groups in the children property. 74 | 75 | ```javascript 76 | var requestGroup = { 77 | id: "requestGroupId", 78 | requests: [], 79 | children: [] 80 | }; 81 | ``` 82 | 83 | Each request group can contain multiple request groups. All of the children of a request group will be executed simultaneously. 84 | 85 | The `Rcoil` object exposes the `startGroup` method to begin a new group. The new group is added as a child of the current group being manipulated. Starting a group with an empty `Rcoil` object will result in this structure: 86 | ```javascript 87 | var calls = [ 88 | { 89 | id: "group1", 90 | requests: [], 91 | children: [] 92 | } 93 | ]``` 94 | Starting another group right after the first one will add a new group as a child to the one just created. 95 | ```javascript 96 | var calls = [ 97 | { 98 | id: "group1", 99 | requests: [], 100 | children: [ 101 | { 102 | id: "group2", 103 | requests: [], 104 | children: [] 105 | } 106 | ] 107 | } 108 | ] 109 | ``` 110 | 111 | If we called startGroup again now we'd be adding a children to the "group2" group. If we want to add a new group as a child of "group1", at the same level as "group2", we can use the `afterGroup("group1")` method. This will reset the position of tree walker to "group1". 112 | 113 | ```javascript 114 | var calls = [ 115 | { 116 | id: "group1", 117 | requests: [], 118 | children: [ 119 | { 120 | id: "group2", 121 | requests: [], 122 | children: [] 123 | }, 124 | { 125 | id: "group3", 126 | requests: [], 127 | children: [] 128 | } 129 | ] 130 | } 131 | ] 132 | ``` 133 | 134 | With this structure the `ExecutionDirector` would first execute "group1", and then run "group2" and "group3" simultaneously. 135 | 136 | To create this structure with the object we would do: 137 | ```javascript 138 | var coil = new Rcoil(); 139 | coil 140 | .startGroup("group1") 141 | .startGroup("group2") 142 | .afterGroup("group1") 143 | .startGroup("group3"); 144 | ``` 145 | 146 | You can use the `printCoil` method to show the structure of the coil in a readable format in the console 147 | 148 | ```javascript 149 | coil.printCoil(); 150 | ``` 151 | 152 | ## The Request object 153 | Request groups in the `Rcoil` object contain requests. The `Request` object represents an individual call to an HTTP method or AWS Lambda function. The `Rcoil` object exposes the `addRequest(request)` method which inserts a request in the current group. 154 | 155 | Request objects can be initialized with a simple url, or with a full configuration structure 156 | 157 | ```javascript 158 | // using a simple url 159 | Request.get("firstSimultaneousRequest", "http://localhost:3000/users") 160 | 161 | // passing the configuration object 162 | Request.get("firstSimultaneousRequest", { 163 | host: "myapi.com", 164 | path: "/test", 165 | port: "80", 166 | protocol: "http:", 167 | method: "GET" 168 | }); 169 | ``` 170 | 171 | Requests can also interact with AWS Lambda functions. 172 | 173 | ```javascript 174 | Request.lambda("firstLambdaRequest", "arn:aws:lambda:us-west-2:account-id:function:FunctionName"); 175 | 176 | // you can also pass a version or alias qualifier 177 | Request.lambda("firstLambdaRequest", "arn:aws:lambda:us-west-2:account-id:function:FunctionName", "prod"); 178 | ``` 179 | 180 | The `onInput` callback triggered before each request. Use the `onInput` callback to generate a request 181 | body for the backend. The `ExecutionContext` is passed to the function, all previous requests and responses 182 | are available in the context object. 183 | 184 | ```javascript 185 | request.onInput(function(context, requestObject) { 186 | var responseData = JSON.parse(context.responseData("firstRequestGroup", "firstSimultaneouRequest").body); 187 | 188 | // you can manipulate the http.ClientRequest object to inject custom headers 189 | requestObject.setHeader("x-custom-auth", responseData.authToken); 190 | 191 | // return the body for the request. This can be an Object or a string. 192 | var newRequest = { 193 | id: responseData.id, 194 | staticValue: "value" 195 | }; 196 | return newRequest; 197 | }); 198 | ``` 199 | 200 | To cancel the execution of the request you can simply return `false` from the `onInput` method. If the request 201 | execution is canceled you will see the `isCanceled` flag in the response data of the `ExecutionContext` for the 202 | request set to `true`. 203 | 204 | ```javascript 205 | request.onInput(function(context, requestObject) { 206 | var responseData = JSON.parse(context.responseData("firstRequestGroup", "firstSimultaneouRequest").body); 207 | 208 | // Basic flow control. If the previous request did not retrieve a valid auth token then 209 | // cancel this request 210 | if (responseData.authToken == null) { 211 | return false; 212 | } 213 | 214 | // return the body for the request. This can be an Object or a string. 215 | var newRequest = { 216 | id: responseData.id, 217 | staticValue: "value" 218 | }; 219 | return newRequest; 220 | }); 221 | ``` 222 | 223 | ## The ExecutionContext object 224 | The ExecutionContext object is used throughout the execution of a coil to track all requests sent and responses received. The object is passed to all callbacks, such as the `onInput` callback for requests, and events. 225 | 226 | The `ExecutionContext` object uses a mutex to synchronize access to the requests and response structures. For this reason the data should only be accessed through the `requestData` and `responseData` methods. Requests and responses contain all of the data sent and received including headers and status codes. 227 | 228 | ```javascript 229 | // get the request data for a particular request 230 | var request = context.requestData("requestGroupId", "requestName"); 231 | // the request object looks like this: 232 | // { 233 | // config: requestConfig, 234 | // body: "requestBodyString", 235 | // startTime: timestamp, 236 | // 237 | // // if it's an HTTP request then it will also contain the following values 238 | // headers: {}, 239 | // url: "http://myurl.com", 240 | // method: "GET" 241 | // } 242 | 243 | var response = context.responseData("requestGroupId", "requestName"); 244 | // the response object looks like this: 245 | // { 246 | // body: "responseBodyString", 247 | // endTime: timestamp, 248 | // 249 | // // if it's an HTTP request then it will also contain the following values 250 | // headers: {}, 251 | // httpVersion: "http://myurl.com", 252 | // method: "GET", 253 | // statusCode: 200, 254 | // statusMessage: "", 255 | // 256 | // // if it's a Lambda request 257 | // err: "Error message from Lambda" 258 | // } 259 | ``` 260 | 261 | ## The ExecutionDirector object 262 | The `ExecutionDirector` object takes an `Rcoil` structure and executes all of the requests in the correct order. Throughout the execution state is kept in the `ExecutionContext` object. The context is passed to all callbacks to allow access to all data exchanged, including requests and responses. 263 | 264 | The constructor for the `ExecutionDirector` receives an `Rcoil` object as well as a set of options. The `start` method begins the execution of a coil once the director is initialized. The `start` method can receive a callback. The callback is triggered once the execution is completed and receives the populated `ExecutionContext`. 265 | 266 | ```javascript 267 | var director = new ExecutionDirector(coil, { 268 | logger: new ConsoleLogger(), 269 | debug: true 270 | }); 271 | director.start(function(context) { 272 | // do something with the results 273 | }); 274 | ``` 275 | 276 | ### Events 277 | The `ExecutionDirector` object exposes a number of events to manage the lifcycle of an execution. 278 | 279 | | Event | Parameters | Description | 280 | |-------|------------|-------------| 281 | | abort | context | abort is triggered when a program calls the `abort` method of the `ExecutionDirector`, and all active requests complete their execution. | 282 | | groupStart | group, context | groupStart is triggered when the director starts executing a request group. The event is passed the request group object as well as the `ExecutionContext` populated with all requests executed so far. | 283 | | groupEnd | group, context | groupEnd is triggered when the director completes the execution of a group. The event is passed the request group object as well as the `ExecutionContext` populated with all requests executed so far. | 284 | | requestStart | groupId, request, context | requestStart is triggered when the `RequestPlayer` starts the execution of a request. The event is passed the groupId that the request belongs to, the `Request` object, and the `ExecutionContext`. | 285 | | requestEnd | groupId, request, context | requestEnd, is triggered when the `RequestPlayer` completes the execution of a request or the execution is canceled. The event is passed the groupId that the request belongs to, the `Request` object, and the `ExecutionContext`. | 286 | 287 | Use the `on` method to subscribe to events. 288 | 289 | ```javascript 290 | director.on("requestStart", function(groupId, request, context) { 291 | if (request.name == "firstSimultaneousRequest") { 292 | // do something 293 | } 294 | }); 295 | ``` 296 | -------------------------------------------------------------------------------- /src/execution-context.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // external modules 18 | var ReadWriteLock = require('rwlock'); 19 | 20 | const groupsLockName = "requestGroups"; 21 | const requestsLockName = "requests"; 22 | const requestDataLockName = "requestData"; 23 | const responseDataLockName = "responseData"; 24 | 25 | /** 26 | * The ExecutionContext object saves the state of the symphony execution, this includes all 27 | * of the active request groups and requests as well as all request and response data. 28 | * Requests and responses are organized by group id and request name and are 29 | * accessible through the responseData and requestData. 30 | * 31 | * The ExecutionContext object uses the rwlock module to synchronize access to the internal 32 | * data structures. 33 | * 34 | * @class 35 | * @constructor 36 | */ 37 | function ExecutionContext() { 38 | /** 39 | * The mutex used to synchronize reads and writes 40 | * 41 | * @property {ReadWriteLock} _lock 42 | * @private 43 | */ 44 | this._lock = new ReadWriteLock(); 45 | /** 46 | * The temporary list of active request groups. This should never be access directly 47 | * and only updated through its register and unregister methods. 48 | * 49 | * @property {Array.} _activeGroups 50 | * @private 51 | */ 52 | this._activeGroups = []; 53 | /** 54 | * The temporary list of active requests. This should never be access directly 55 | * and only updated through its register and unregister methods. 56 | * 57 | * @property {Array.} _activeRequests 58 | * @private 59 | */ 60 | this._activeRequests = []; 61 | /** 62 | * The data structure containing all requests sent during the execution of a 63 | * symphony. The object contains group ids as keys and then request names 64 | * as keys. 65 | * 66 | * This structure should be accessed through the requestData method. 67 | * @example 68 | * { 69 | * firstGroup: { 70 | * firstRequest: { 71 | * config: {}, 72 | * inputFunc: function(), 73 | * method: GET, 74 | * body: "" 75 | * } 76 | * } 77 | * } 78 | * 79 | * var tmpResp = context.responseData("testGroup", "listUsers"); 80 | * return JSON.parse(tmpResp.body); 81 | * 82 | * @property {Object} _requests 83 | * @private 84 | */ 85 | this._requests = {}; 86 | /** 87 | * 88 | */ 89 | this._responses = {}; 90 | } 91 | 92 | /** 93 | * Registers a request group as currently being executed with the execution context 94 | * 95 | * @function 96 | * @param {Object} group - The request group object to be registered 97 | */ 98 | ExecutionContext.prototype.registerActiveGroup = function (group) { 99 | this._lock.writeLock(groupsLockName, (function (release) { 100 | this._activeGroups.push(group); 101 | release(); 102 | }).bind(this)); 103 | }; 104 | 105 | /** 106 | * Removes a request group from the list fo groups currently being executed 107 | * 108 | * @function 109 | * @param {Object} group - The request group to be removed from the list 110 | */ 111 | ExecutionContext.prototype.unregisterActiveGroup = function (group) { 112 | this._lock.writeLock(groupsLockName, (function (release) { 113 | for (var i = 0; i < this._activeGroups.length; i++) { 114 | if (this._activeGroups[i].id == group.id) { 115 | this._activeGroups.splice(i, 1); 116 | } 117 | } 118 | 119 | release(); 120 | }).bind(this)); 121 | }; 122 | 123 | /** 124 | * Looks up an returns a request group from the list of currently active groups. 125 | * 126 | * @function 127 | * @param {String} groupId - The id of the group to look up 128 | * @return {Object} The group object from the active ones, null if it cannot be found 129 | */ 130 | ExecutionContext.prototype.getActiveGroup = function (groupId) { 131 | var group = null; 132 | this._lock.readLock(groupsLockName, (function (release) { 133 | for (var i = 0; i < this._activeGroups.length; i++) { 134 | if (this._activeGroups[i].id == groupId) { 135 | group = this._activeGroups[i]; 136 | } 137 | } 138 | 139 | release(); 140 | }).bind(this)); 141 | 142 | return group; 143 | }; 144 | 145 | /** 146 | * Performs a deep copy of the request group list and returns it. 147 | * 148 | * @function 149 | * @return {Array.} The full list of currently active request groups. 150 | */ 151 | ExecutionContext.prototype.getActiveGroups = function () { 152 | var tmpActive = null; 153 | this._lock.readLock(groupsLockName, (function (release) { 154 | tmpActive = JSON.parse(JSON.stringify(this._activeGroups)); 155 | release(); 156 | }).bind(this)); 157 | return tmpActive; 158 | }, 159 | 160 | /** 161 | * Registers a request as currently being executed 162 | * 163 | * @function 164 | * @param {Request} requestConfig - A valid request object 165 | */ 166 | ExecutionContext.prototype.registerActiveRequest = function (requestConfig) { 167 | this._lock.writeLock(requestsLockName, (function (release) { 168 | this._activeGroups.push(requestConfig); 169 | release(); 170 | }).bind(this)); 171 | }; 172 | 173 | /** 174 | * removed a request from the list of active requests 175 | * 176 | * @function 177 | * @param {Request} requestConfig - A request configuration 178 | */ 179 | ExecutionContext.prototype.unregisterActiveRequests = function (requestConfig) { 180 | this._lock.writeLock(requestsLockName, (function (release) { 181 | for (var i = 0; i < this._activeRequests.length; i++) { 182 | if (this._activeRequests[i].name == requestConfig.name) { 183 | this._activeRequests.splice(i, 1); 184 | } 185 | } 186 | 187 | release(); 188 | }).bind(this)); 189 | }; 190 | 191 | /** 192 | * Retrieves a request from the list of active requests 193 | * 194 | * @function 195 | * @param {String} requestName - The name of the request 196 | * @return {Request} The request config 197 | */ 198 | ExecutionContext.prototype.getActiveRequest = function (requestName) { 199 | var request = null; 200 | this._lock.readLock(requestsLockName, (function (release) { 201 | for (var req in this._activeRequests) { 202 | if (req.name == requestName) { 203 | request = req; 204 | } 205 | } 206 | 207 | release(); 208 | }).bind(this)); 209 | 210 | return request; 211 | }; 212 | 213 | /** 214 | * Performs a deep copy of the list of active requests and return it 215 | * 216 | * @function 217 | * @return {Array.} The list of active requests 218 | */ 219 | ExecutionContext.prototype.getActiveRequests = function () { 220 | var tmpActive = null; 221 | 222 | this._lock.readLock(requestsLockName, (function (release) { 223 | tmpActive = JSON.parse(JSON.stringify(this._activeRequests)); 224 | release(); 225 | }).bind(this)); 226 | 227 | return tmpActive; 228 | }; 229 | 230 | /** 231 | * Reads the request data generated for a specific request during the execution 232 | * of the rcoil. 233 | * 234 | * @example 235 | * var data = context.requestData("group1", "request1"); 236 | * console.log(JSON.stringify(data, null, 2)); 237 | * { 238 | * config: requestConfig, 239 | * headers: requestObject.headers, 240 | * url: requestObject.url, 241 | * method: requestObject.method, 242 | * statusCode: requestObject.statusCode, 243 | * body: requestBodyString, 244 | * startTime: Date.now() 245 | * } 246 | * 247 | * @function 248 | * @param {String} groupId - The request group id 249 | * @param {String} requestName - The name of the request 250 | * @return {Object} the request data sent to the remote server 251 | */ 252 | ExecutionContext.prototype.requestData = function (groupId, requestName) { 253 | var data = null; 254 | this._lock.readLock(requestDataLockName, (function (release) { 255 | if (this._requests[groupId] !== undefined && this._requests[groupId][requestName] !== undefined) { 256 | data = this._requests[groupId][requestName]; 257 | } 258 | 259 | release(); 260 | }).bind(this)); 261 | return data; 262 | }; 263 | 264 | /** 265 | * Saves a request data to the context. Request data should match this pattern: 266 | * @example 267 | * { 268 | * config: requestConfig, 269 | * headers: requestObject.headers, 270 | * url: requestObject.url, 271 | * method: requestObject.method, 272 | * statusCode: requestObject.statusCode, 273 | * body: requestBodyString, 274 | * startTime: Date.now() 275 | * } 276 | * 277 | * @function 278 | * @param {String} groupId - A request group id 279 | * @param {String} requestName - A request name 280 | * @param {Object} requestData - The request data 281 | */ 282 | ExecutionContext.prototype.setRequestData = function (groupId, requestName, requestData) { 283 | this._lock.writeLock(requestDataLockName, (function (release) { 284 | if (this._requests[groupId] === undefined) 285 | this._requests[groupId] = {}; 286 | 287 | this._requests[groupId][requestName] = requestData; 288 | 289 | release(); 290 | }).bind(this)); 291 | }, 292 | 293 | /** 294 | * Retrieves the response data for a specific request. 295 | * 296 | * @example 297 | * var data = context.responseData("group1", "request1"); 298 | * console.log(JSON.stringify(data, null, 2)); 299 | * { 300 | * headers: resp.headers, 301 | * httpVersion: resp.httpVersion, 302 | * method: resp.method, 303 | * statusCode: resp.statusCode, 304 | * statusMessage: resp.statusMessage, 305 | * body: responseOutputString, 306 | * endTime: Date.now(), 307 | * isCanceled: false 308 | * } 309 | * 310 | * @function 311 | * @param {String} groupId - The request group id 312 | * @param {String} requestName - The request name 313 | */ 314 | ExecutionContext.prototype.responseData = function (groupId, requestName) { 315 | var data = null; 316 | 317 | this._lock.readLock(responseDataLockName, (function (release) { 318 | if (this._responses[groupId] !== undefined && this._responses[groupId][requestName] !== undefined) { 319 | data = this._responses[groupId][requestName]; 320 | } 321 | 322 | release(); 323 | }).bind(this)); 324 | return data; 325 | }; 326 | 327 | /** 328 | * Saves a response data in the context. Response data should match this pattern: 329 | * 330 | * @example 331 | * { 332 | * headers: resp.headers, 333 | * httpVersion: resp.httpVersion, 334 | * method: resp.method, 335 | * statusCode: resp.statusCode, 336 | * statusMessage: resp.statusMessage, 337 | * body: responseOutputString, 338 | * endTime: Date.now() 339 | * } 340 | * 341 | * @function 342 | * @param {String} groupId - The request group id 343 | * @param {String} requestName - The request name 344 | * @param {Object} responseData - The response data object 345 | */ 346 | ExecutionContext.prototype.setResponseData = function (groupId, requestName, responseData) { 347 | this._lock.writeLock(responseDataLockName, (function (release) { 348 | if (this._responses[groupId] === undefined) 349 | this._responses[groupId] = {}; 350 | 351 | this._responses[groupId][requestName] = responseData; 352 | 353 | release(); 354 | }).bind(this)); 355 | }; 356 | 357 | /** 358 | * Retrieves the full request and response data structure for all requests and groups 359 | * in a coil. 360 | * 361 | * @example 362 | * { 363 | * req: { // all requests 364 | * group1: { // grouped by request group 365 | * request1 : { 366 | * config: requestConfig, 367 | * headers: requestObject.headers, 368 | * url: requestObject.url, 369 | * method: requestObject.method, 370 | * statusCode: requestObject.statusCode, 371 | * body: requestBodyString, 372 | * startTime: Date.now() 373 | * }, 374 | * ... 375 | * }, 376 | * ... 377 | * }, 378 | * res: { // all responses 379 | * group1: { // grouped by request group 380 | * request1: { 381 | * headers: resp.headers, 382 | * httpVersion: resp.httpVersion, 383 | * method: resp.method, 384 | * statusCode: resp.statusCode, 385 | * statusMessage: resp.statusMessage, 386 | * body: responseOutputString, 387 | * endTime: Date.now(), 388 | * isCanceled: false 389 | * }, 390 | * ... 391 | * }, 392 | * ... 393 | * } 394 | * } 395 | * 396 | * @function 397 | * @return {Object} The full list of requests and responses 398 | */ 399 | ExecutionContext.prototype.getData = function () { 400 | return { 401 | req: this._requests, 402 | res: this._responses 403 | }; 404 | }; 405 | 406 | module.exports = ExecutionContext; 407 | -------------------------------------------------------------------------------- /src/execution-director.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // core modules 18 | const util = require('util'); 19 | const events = require('events'); 20 | 21 | // local modules 22 | var RequestGroupPlayer = require('./players').RequestGroupPlayer; 23 | var ExecutionContext = require('./execution-context'); 24 | var Rcoil = require('./rcoil'); 25 | var DevNullLogger = require('./devnull-logger'); 26 | var Request = require('./request'); 27 | 28 | /** 29 | * The ExecutionDirector receives and executes an Rcoil tree. 30 | * 31 | * Options available are: 32 | * @example 33 | * { 34 | * debug: false, 35 | * logger: new ConsoleLogger(); 36 | * } 37 | * 38 | * @class 39 | * @constructor 40 | * @param {Rcoil} rcoil - An initialized Rcoil object 41 | * @param {Object} options - Configuration options for the director 42 | * 43 | * @fires ExecutionDirector#end 44 | * @fires ExecutionDirector#groupStart 45 | * @fires ExecutionDirector#groupEnd 46 | * @fires ExecutionDirector#requestStart 47 | * @fires ExecutionDirector#requestEnd 48 | */ 49 | function ExecutionDirector(rcoil, options) { 50 | /** 51 | * The Rcoil object 52 | * 53 | * @property {Rcoil} 54 | */ 55 | this.rcoil = rcoil; 56 | /** 57 | * The execution context, this is updated during the execution of the Rcoil 58 | * and passed to all callbacks and events. 59 | * 60 | * @property {ExecutionContext} 61 | */ 62 | this.executionContext = new ExecutionContext(); 63 | 64 | /** 65 | * Configuration options for the director. These are passed to the RequestGroup 66 | * and Request players. 67 | * 68 | * @property {Object} 69 | */ 70 | this.options = { 71 | debug: false, 72 | logger: new DevNullLogger(), 73 | awsConfig: null 74 | }; 75 | util._extend(this.options, options); 76 | 77 | /** 78 | * Total number of reuqest groups in the coil 79 | * 80 | * @property {int} 81 | * @private 82 | */ 83 | this._totalGroups = 0; 84 | /** 85 | * Temporary counter used to track how many groups have been executed. 86 | * 87 | * @property {int} 88 | * @private 89 | */ 90 | this._tmpGroupsCounter = 0; 91 | 92 | /** 93 | * This tells the director that the execution was aborted and should not start 94 | * any new requests 95 | * 96 | * @property {bool} 97 | * @private 98 | */ 99 | this._aborted = false; 100 | 101 | /** 102 | * The callback passed to the start method of the ExecutionDirector. This is called when the 103 | * ExecutionDirector successfully completes the execution of an Rcoil object. If the execution 104 | * is aborted the abort event is trggered. 105 | * 106 | * @property {function} 107 | * @private 108 | */ 109 | this._endCallback = null; 110 | 111 | events.EventEmitter.call(this); 112 | } 113 | util.inherits(ExecutionDirector, events.EventEmitter); 114 | 115 | /** 116 | * The abort event is fired whenever the abort method is called on a director and 117 | * all running requests are completed. 118 | * 119 | * @event ExecutionDirector#abort 120 | * @type {Object} 121 | * @property {ExecutionContext} context - The completed execution context 122 | */ 123 | 124 | /** 125 | * The groupStart event is fired every time the director starts the execution of a 126 | * new request group 127 | * 128 | * @event ExecutionDirector#groupStart 129 | * @type {Object} 130 | * @property {Object} group - The request group being started 131 | * @property {ExecutionContext} context - The updated execution context 132 | */ 133 | 134 | /** 135 | * The groupEnd event is fired every time the director completes the execution of a 136 | * request group 137 | * 138 | * @event ExecutionDirector#groupEnd 139 | * @type {Object} 140 | * @property {Object} group - The request group being started 141 | * @property {ExecutionContext} context - The updated execution context 142 | */ 143 | 144 | /** 145 | * The requestStart event is fired every time the director starts the execution of a 146 | * request 147 | * 148 | * @event ExecutionDirector#requestStart 149 | * @type {Object} 150 | * @property {String} groupId - The request group id the request belongs to 151 | * @property {Request} request - The request being started 152 | * @property {ExecutionContext} context - The updated execution context 153 | */ 154 | 155 | /** 156 | * The requestEnd event is fired every time the director completes the execution of a 157 | * request 158 | * 159 | * @event ExecutionDirector#requestEnd 160 | * @type {Object} 161 | * @property {String} groupId - The request group id the request belongs to 162 | * @property {Request} request - The completed request 163 | * @property {ExecutionContext} context - The updated execution context 164 | */ 165 | 166 | /** 167 | * Private method used to respond to the start event from the RequestGroupPlayer 168 | * 169 | * @function 170 | * @private 171 | * @param {Object} requestGroup - The request group 172 | */ 173 | ExecutionDirector.prototype._requestGroupStarted = function (requestGroup) { 174 | requestGroup.startTime = Date.now(); 175 | this.executionContext.registerActiveGroup(requestGroup); 176 | this.emit("groupStart", requestGroup, this.executionContext); 177 | this.options.logger.info("Request group " + requestGroup.id + " started"); 178 | }; 179 | 180 | /** 181 | * Checks if the execution of the Rcoil was aborted. If it was checks that all active 182 | * requests have completed executing and then fires the "abort" event before returning. 183 | * 184 | * @function 185 | * @private 186 | * @return {bool} true if the execution was aborted, false otherwise. 187 | */ 188 | ExecutionDirector.prototype._isAborted = function() { 189 | if (this._aborted) { 190 | // all requests have completed the execution, we can emit the event 191 | if (this.executionContext.getActiveRequests().length == 0) { 192 | this.emit("abort", this.executionContext); 193 | } 194 | return true; 195 | } 196 | 197 | return false; 198 | } 199 | 200 | /** 201 | * Private method used to respond to the end event from the RequestGroupPlayer 202 | * 203 | * @function 204 | * @private 205 | * @param {Object} requestGroup - The request group 206 | */ 207 | ExecutionDirector.prototype._requestGroupDone = function (requestGroup) { 208 | requestGroup.endTime = Date.now(); 209 | this.executionContext.unregisterActiveGroup(requestGroup); 210 | this._tmpGroupsCounter++; 211 | 212 | if (this._isAborted()) return; 213 | 214 | this.emit("groupEnd", requestGroup, this.executionContext); 215 | 216 | var execTime = requestGroup.endTime - requestGroup.startTime; 217 | this.options.logger.info("Request group " + requestGroup.id + " finished in: " + execTime); 218 | 219 | for (var i = 0; i < requestGroup.children.length; i++) { 220 | var childPlayer = this._getRequestGroupPlayer(requestGroup.children[i]) 221 | childPlayer.start(); 222 | } 223 | 224 | if (requestGroup.children.length == 0 && this._tmpGroupsCounter == this._totalGroups) { 225 | if (this._endCallback != null) 226 | this._endCallback(this.executionContext); 227 | } 228 | }; 229 | 230 | /** 231 | * Private method used to respond to the requestStart event from the RequestGroupPlayer 232 | * 233 | * @function 234 | * @private 235 | * @property {Object} requestGroup - The completed request group object 236 | * @property {Request} requestConfig - The request configuration passed to the RequestPlayer 237 | * @property {http.ClientRequest} requestObject - The node.js http/s request object 238 | * @property {*} input - The input generated for the request by the callback function 239 | */ 240 | ExecutionDirector.prototype._requestStarted = function (requestGroup, requestConfig, requestObject, input) { 241 | this.executionContext.registerActiveRequest(requestConfig); 242 | 243 | var request = { 244 | config: requestConfig, 245 | body: input, 246 | startTime: Date.now() 247 | }; 248 | 249 | switch (requestConfig.type) { 250 | case Request.RequestType.HTTP: 251 | request.headers = requestObject.headers; 252 | request.url = requestObject.url; 253 | request.method = requestObject.method; 254 | break; 255 | } 256 | 257 | this.executionContext.setRequestData(requestGroup.id, requestConfig.name, request); //util.inspect(requestObject, {showHidden:true, depth: null}) 258 | 259 | this.emit("requestStart", requestGroup.id, requestConfig, this.executionContext); 260 | 261 | this.options.logger.info("Request " + requestConfig.name + " started"); 262 | }; 263 | 264 | /** 265 | * Private method used to respond to the requestEnd event from the RequestGroupPlayer 266 | * 267 | * @function 268 | * @private 269 | * @property {Object} requestGroup - The completed request group object 270 | * @property {Request} requestConfig - The request configuration passed to the RequestPlayer 271 | * @property {http.IncomingMessage} response - The response object from the node.js http/s client 272 | * @property {*} output - The output returned from the remote server 273 | */ 274 | ExecutionDirector.prototype._requestDone = function (requestGroup, requestConfig, resp, output) { 275 | this.executionContext.unregisterActiveRequests(requestConfig); 276 | 277 | var response = { 278 | body: output, 279 | endTime: Date.now(), 280 | isCanceled: false 281 | }; 282 | 283 | switch (requestConfig.type) { 284 | case Request.RequestType.HTTP: 285 | response.headers = resp.headers; 286 | response.httpVersion = resp.httpVersion; 287 | response.method = resp.method; 288 | response.statusCode = resp.statusCode; 289 | response.statusMessage = resp.statusMessage; 290 | break; 291 | case Request.RequestType.LAMBDA: 292 | response.err = resp; 293 | break; 294 | } 295 | 296 | this.executionContext.setResponseData(requestGroup.id, requestConfig.name, response); 297 | this.options.logger.info("Request " + requestConfig.name + " finished"); 298 | 299 | this.emit("requestEnd", requestGroup.id, requestConfig, this.executionContext); 300 | 301 | if (this.options.debug) { 302 | var execTime = response.endTime - this.executionContext.requestData(requestGroup.id, requestConfig.name).startTime; 303 | this.options.logger.debug("Request " + requestConfig.name + " executed in: " + execTime + "ms"); 304 | } 305 | }; 306 | 307 | /** 308 | * Handles requests being canceled by returning false from the onInput function 309 | * 310 | * @function 311 | * @private 312 | * @property {Object} requestGroup - The completed request group object 313 | * @property {Request} requestConfig - The request configuration passed to the RequestPlayer 314 | */ 315 | ExecutionDirector.prototype._requestCanceled = function(requestGroup, requestConfig) { 316 | this.executionContext.unregisterActiveRequests(requestConfig); 317 | 318 | var response = { 319 | endTime: Date.now(), 320 | isCanceled: true 321 | }; 322 | 323 | this.executionContext.setResponseData(requestGroup.id, requestConfig.name, response); 324 | this.options.logger.info("Request " + requestConfig.name + " canceled"); 325 | 326 | this.emit("requestEnd", requestGroup.id, requestConfig, this.executionContext); 327 | } 328 | 329 | /** 330 | * Returns an initialized RequestGroupPlayer with all events associated to the private methods 331 | * of the director. 332 | * 333 | * @function 334 | * @private 335 | * @param {Object} requestGroup - The request group object for the player 336 | */ 337 | ExecutionDirector.prototype._getRequestGroupPlayer = function (requestGroup) { 338 | var player = new RequestGroupPlayer(requestGroup, this.executionContext, this.options); 339 | 340 | player.on("start", this._requestGroupStarted.bind(this)); 341 | player.on("end", this._requestGroupDone.bind(this)); 342 | player.on("requestStart", this._requestStarted.bind(this)); 343 | player.on("requestEnd", this._requestDone.bind(this)); 344 | player.on("requestCancel", this._requestCanceled.bind(this)); 345 | return player; 346 | }; 347 | 348 | /** 349 | * Begins the execution of the Rcoil object 350 | * 351 | * @function 352 | * @return {ExecutionContext} The execution context object that the director will keep updating 353 | * throughout the execution 354 | */ 355 | ExecutionDirector.prototype.start = function (callback) { 356 | this._totalGroups = this.rcoil.requestGroupsCount(); 357 | this._tmpGroupsCounter = 0; 358 | this._endCallback = callback; 359 | 360 | for (var i = 0; i < this.rcoil.calls.length; i++) { 361 | var player = this._getRequestGroupPlayer(this.rcoil.calls[i]); 362 | player.start(); 363 | } 364 | 365 | return this.executionContext; 366 | }; 367 | 368 | /** 369 | * Aborts the execution of an Rcoil. 370 | * 371 | * @function 372 | */ 373 | ExecutionDirector.prototype.abort = function () { 374 | this._aborted = true; 375 | return; 376 | }; 377 | 378 | module.exports = ExecutionDirector; 379 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var url = require('url'); 18 | var validUrl = require('valid-url'); 19 | 20 | /** 21 | * The Request object contains the configuration for a single backend requests. 22 | * The request object can represent both an HTTP request and a Lambda function invocation. 23 | * 24 | * HTTP requests can be created using the static shortcuts or the instance methods. Request 25 | * creation methods can receive either a target url or a full request configuration. 26 | * 27 | * @example 28 | * // To quickly create a get request 29 | * var req = Request.get("name", "http://myapi.com/test"); 30 | * 31 | * // to sepcify a full configuration 32 | * var req = Request.get("name", { 33 | * host: "myapi.com", 34 | * path: "/test", 35 | * port: "80", 36 | * protocol: "http:", 37 | * method: "GET" 38 | * }); 39 | * 40 | * // Request body can be set using the onInput callback 41 | * req.onInput(function(context, requestObject) { 42 | * return { 43 | * request: "body" 44 | * }; 45 | * }); 46 | * 47 | * @class 48 | * @constructor 49 | * @param {String} name - A name to uniquely identify the request object 50 | */ 51 | function Request(name) { 52 | this.type = ""; 53 | this.name = name; 54 | this.settings = {}; 55 | this.inputFunc = null; 56 | this.outputFunc = null; 57 | } 58 | 59 | /** 60 | * the input function generates and returns request bodies for the coil's requests. 61 | * The callback is triggered by the RequestPlayer before it starts each request. When 62 | * starting an HTTP request the input callback is triggered with the ExecutionContext 63 | * and the generated http.ClientRequest object. When invoking a Lambda function the 64 | * callback simply receives the context. 65 | * 66 | * @example 67 | * // for HTTP requests 68 | * function(executionContext, httpRequest) { 69 | * httpRequest.setHeader("x-customHeader", "customHeaderValue"); 70 | * 71 | * return { 72 | * requestBodyParam1 : "value", 73 | * requestBodyParam2 : "value2" 74 | * } 75 | * } 76 | * 77 | * // for Lambda invocations 78 | * function(executionContext) { 79 | * return { 80 | * lambdaEventProperty1: "value1", 81 | * } 82 | * } 83 | * 84 | * @callback inputCallback 85 | * @param {ExecutionContext} executionContext - The populated execution context object 86 | * @param {http.ClientRequest} httpRequest - The generated HTTP request 87 | */ 88 | 89 | /** 90 | * An object to simplify the creation of HTTP requests 91 | * 92 | * @example 93 | * var r = new Request("requestName"); 94 | * r.setHttpRequest(Request.HttpVerb.GET, "http://api.com/test"); 95 | * 96 | * @roperty {Object} 97 | */ 98 | Request.HttpVerb = { 99 | GET: "GET", 100 | POST: "POST", 101 | PUT: "PUT", 102 | PATCH: "PATCH", 103 | HEAD: "HEAD", 104 | DELETE: "DELETE", 105 | OPTIONS: "OPTIONS" 106 | }; 107 | 108 | Request.RequestType = { 109 | HTTP: "http", 110 | LAMBDA: "lambda" 111 | }; 112 | 113 | /** 114 | * Creates a GET HTTP request. Can receive a url for the request or a full configuration object. 115 | * 116 | * @function 117 | * @param {String} name - A name that uniquely identifies the request 118 | * @param {String|Object} config - A url string or a request configuration 119 | * @return {Request} An initialized request object 120 | */ 121 | Request.get = function (name, config) { 122 | return new Request(name).setGet(config); 123 | }; 124 | /** 125 | * Creates a POST HTTP request. Can receive a url for the request or a full configuration object. 126 | * 127 | * @function 128 | * @param {String} name - A name that uniquely identifies the request 129 | * @param {String|Object} config - A url string or a request configuration 130 | * @return {Request} An initialized request object 131 | */ 132 | Request.post = function (name, config) { 133 | return new Request(name).setPost(config); 134 | }; 135 | /** 136 | * Creates a PUT HTTP request. Can receive a url for the request or a full configuration object. 137 | * 138 | * @function 139 | * @param {String} name - A name that uniquely identifies the request 140 | * @param {String|Object} config - A url string or a request configuration 141 | * @return {Request} An initialized request object 142 | */ 143 | Request.put = function (name, config) { 144 | return new Request(name).setPut(config); 145 | }; 146 | /** 147 | * Creates a PATCH HTTP request. Can receive a url for the request or a full configuration object. 148 | * 149 | * @function 150 | * @param {String} name - A name that uniquely identifies the request 151 | * @param {String|Object} config - A url string or a request configuration 152 | * @return {Request} An initialized request object 153 | */ 154 | Request.patch = function (name, config) { 155 | return new Request(name).setPatch(config); 156 | }; 157 | /** 158 | * Creates a HEAD HTTP request. Can receive a url for the request or a full configuration object. 159 | * 160 | * @function 161 | * @param {String} name - A name that uniquely identifies the request 162 | * @param {String|Object} config - A url string or a request configuration 163 | * @return {Request} An initialized request object 164 | */ 165 | Request.head = function (name, config) { 166 | return new Request(name).setHead(config); 167 | }; 168 | /** 169 | * Creates a DELETE HTTP request. Can receive a url for the request or a full configuration object. 170 | * 171 | * @function 172 | * @param {String} name - A name that uniquely identifies the request 173 | * @param {String|Object} config - A url string or a request configuration 174 | * @return {Request} An initialized request object 175 | */ 176 | Request.delete = function (name, config) { 177 | return new Request(name).setDelete(config); 178 | }; 179 | /** 180 | * Creates a OPTIONS HTTP request. Can receive a url for the request or a full configuration object. 181 | * 182 | * @function 183 | * @param {String} name - A name that uniquely identifies the request 184 | * @param {String|Object} config - A url string or a request configuration 185 | * @return {Request} An initialized request object 186 | */ 187 | Request.options = function (name, config) { 188 | return new Request(name).setOptions(config); 189 | }; 190 | 191 | /** 192 | * Creates a Lambda request give a Lambda function ARN. 193 | * 194 | * @function 195 | * @param {String} name - A name that uniquely identifies the request 196 | * @param {String|Object} config - A valid Lambda function ARN 197 | * @return {Request} An initialized request object 198 | */ 199 | Request.lambda = function(name, functionArn, qualifier) { 200 | return new Request(name).setLambda(functionArn, qualifier); 201 | }; 202 | 203 | /** 204 | * Sets up the request object for a Lambda function. This private method is used by 205 | * the public shortcuts. 206 | * 207 | * @function 208 | * @private 209 | * @param {String} functionArn - A valid Lambda function ARN 210 | * @param {String} qualifier - A Lambda function version or alias 211 | */ 212 | Request.prototype._createLambdaRequest = function (functionArn, qualifier) { 213 | this.type = Request.RequestType.LAMBDA; 214 | this.settings = { 215 | function: functionArn, 216 | qualifier: (qualifier === undefined || qualifier == null || qualifier == ""?"$LATEST":qualifier) 217 | }; 218 | }; 219 | 220 | /** 221 | * Sets up the request as an HTTP request. This private method is used by the public 222 | * shortcuts. 223 | * 224 | * @function 225 | * @private 226 | * @param {String} httpVerb - The HTTP verb of the request. 227 | * @param {String|Object} config - This can be a url for the request or a full request object 228 | * configuration 229 | */ 230 | Request.prototype._createHttpRequest = function (httpVerb, config) { 231 | this.type = Request.RequestType.HTTP, 232 | this.settings = {} 233 | 234 | if (typeof config === 'object') { 235 | if (!config.hasOwnProperty('host') || 236 | !config.hasOwnProperty('path') || 237 | !config.hasOwnProperty('protocol') || 238 | !config.hasOwnProperty('method')) { 239 | throw new Error("Invalid request configuration. Missing parameters"); 240 | } 241 | this.settings = config; 242 | } else { 243 | if (!validUrl.isUri(config)) { 244 | throw new Error("Invalid request url: " + config); 245 | } 246 | 247 | var parsedUrl = url.parse(config); 248 | 249 | this.settings = { 250 | host: parsedUrl.hostname, 251 | path: (parsedUrl.path != null ? parsedUrl.path : "") + (parsedUrl.hash != null ? parsedUrl.hash : ""), 252 | port: parsedUrl.port, 253 | protocol: parsedUrl.protocol, 254 | method: httpVerb.toUpperCase() 255 | }; 256 | } 257 | }; 258 | 259 | /** 260 | * constructs the full URL of the request from the settings object. For HTTP requests it returns the full 261 | * request url, for LAMBDA requests it returns the full function ARN + qualifier. 262 | * 263 | * @function 264 | * @return {String} The URL for the function 265 | */ 266 | Request.prototype.getUrl = function() { 267 | switch (this.type) { 268 | case Request.RequestType.HTTP: 269 | return this.settings.protocol + "//" + this.settings.host + ":" + this.settings.port + this.settings.path; 270 | break; 271 | case Request.RequestType.LAMBDA: 272 | return this.settings.function + ":" + this.settings.qualifier; 273 | break; 274 | } 275 | } 276 | 277 | /** 278 | * Sets up the request object as an HTTP request 279 | * 280 | * @function 281 | * @param {String} httpVerb - The HTTP verb of the request. 282 | * @param {String|Object} config - This can be a url for the request or a full request object 283 | * configuration 284 | * @return {Request} The updated request object 285 | */ 286 | Request.prototype.setHttpRequest = function (httpVerb, config) { 287 | this._createHttpRequest(httpVerb, config); 288 | return this; 289 | }; 290 | 291 | /** 292 | * Sets up the request object as an HTTP GET request. 293 | * 294 | * @function 295 | * @param {String|Object} config - This can be a url for the request or a full request object 296 | * @return {Request} The updated request object 297 | */ 298 | Request.prototype.setGet = function (config) { 299 | return this.setHttpRequest(Request.HttpVerb.GET, config); 300 | }; 301 | /** 302 | * Sets up the request object as an HTTP POST request. 303 | * 304 | * @function 305 | * @param {String|Object} config - This can be a url for the request or a full request object 306 | * @return {Request} The updated request object 307 | */ 308 | Request.prototype.setPost = function (config) { 309 | return this.setHttpRequest(Request.HttpVerb.POST, config); 310 | }; 311 | /** 312 | * Sets up the request object as an HTTP PUT request. 313 | * 314 | * @function 315 | * @param {String|Object} config - This can be a url for the request or a full request object 316 | * @return {Request} The updated request object 317 | */ 318 | Request.prototype.setPut = function (config) { 319 | return this.setHttpRequest(Request.HttpVerb.PUT, config); 320 | }; 321 | /** 322 | * Sets up the request object as an HTTP PATCH request. 323 | * 324 | * @function 325 | * @param {String|Object} config - This can be a url for the request or a full request object 326 | * @return {Request} The updated request object 327 | */ 328 | Request.prototype.setPatch = function (config) { 329 | return this.setHttpRequest(Request.HttpVerb.PATCH, config); 330 | }; 331 | /** 332 | * Sets up the request object as an HTTP DELETE request. 333 | * 334 | * @function 335 | * @param {String|Object} config - This can be a url for the request or a full request object 336 | * @return {Request} The updated request object 337 | */ 338 | Request.prototype.setDelete = function (config) { 339 | return this.setHttpRequest(Request.HttpVerb.DELETE, config); 340 | }; 341 | /** 342 | * Sets up the request object as an HTTP HEAD request. 343 | * 344 | * @function 345 | * @param {String|Object} config - This can be a url for the request or a full request object 346 | * @return {Request} The updated request object 347 | */ 348 | Request.prototype.setHead = function (config) { 349 | return this.setHttpRequest(Request.HttpVerb.HEAD, config); 350 | }; 351 | /** 352 | * Sets up the request object as an HTTP OPTIONS request. 353 | * 354 | * @function 355 | * @param {String|Object} config - This can be a url for the request or a full request object 356 | * @return {Request} The updated request object 357 | */ 358 | Request.prototype.setOptions = function (config) { 359 | return this.setHttpRequest(Request.HttpVerb.OPTIONS, config); 360 | }; 361 | /** 362 | * Sets up the request object as a Lambda function invocation 363 | * 364 | * @function 365 | * @param {String} functionArn - A valid Lambda function ARN 366 | * @param {String} qualifier - A Lambda function version or alias 367 | * @return {Request} The updated request object 368 | */ 369 | Request.prototype.setLambda = function (functionArn, qualifier) { 370 | this._createLambdaRequest.call(this, functionArn, qualifier); 371 | return this; 372 | }; 373 | 374 | /** 375 | * Adds a callback function that will be executed just before the request is executed. 376 | * Use the function to return an input for the request. The function will receive the context 377 | * as a paramter. 378 | * 379 | * If the Request is an HTTP request then the input function will receive both the context 380 | * and the populated http.ClientRequest object. If it's a Lambda invoke the callback will 381 | * only receive the context. 382 | * 383 | * @example 384 | * Request.post("createUser2", "http://myapi.com/createUser2") 385 | * .onInput(function(context, requestObject) { 386 | * // use the context object to read the response from a previous request 387 | * var tmpResp = context.responseData("test", "createUser1"); 388 | * return JSON.parse(tmpResp.body); 389 | * }); 390 | */ 391 | Request.prototype.onInput = function (inputFunc) { 392 | this.inputFunc = inputFunc; 393 | return this; 394 | }; 395 | 396 | 397 | module.exports = Request; 398 | -------------------------------------------------------------------------------- /src/players.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Stefano Buliani (@sapessi) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // core modules 18 | var http = require('http'); 19 | var https = require('https'); 20 | var util = require('util'); 21 | var events = require('events'); 22 | 23 | // external modules 24 | var AWS = require('aws-sdk'); 25 | 26 | // local modules 27 | var Request = require('./request'); 28 | 29 | /** 30 | * RequestPlayer executes requests configured in a request group. The RequestPlayer 31 | * can execute both HTTP and Lambda requests. This object is used internally by the 32 | * RequestGroupPlayer and there should be no need to interact with it from the outside. 33 | * 34 | * RequestPlayer is an EventEmitter. 35 | * 36 | * @class 37 | * @constructor 38 | * @param {Object} request - A configured Request object 39 | * @param {Object} context - An initialized ExecutionContext object to keep state across multiple executions 40 | * @param {Object} options - The options passed to the director 41 | * 42 | * @fires RequestPlayer#start 43 | * @fires RequestPlayer#end 44 | * @fires RequestPlayer#abort 45 | */ 46 | function RequestPlayer(request, context, options) { 47 | this.request = request; 48 | this.options = options; 49 | this.requestObject = null; 50 | 51 | this._context = context; 52 | 53 | events.EventEmitter.call(this); 54 | } 55 | util.inherits(RequestPlayer, events.EventEmitter); 56 | 57 | /** 58 | * The start event is fired whenever the RequestPlayer begins executing a request 59 | * 60 | * @event RequestPlayer#start 61 | * @type {Object} 62 | * @property {Object} requestConfig - The original configuration for the request object 63 | * @property {Object} request - The initialized http.ClientRequest object 64 | * @property {*} input - The input generated by the callback function configured in the request object 65 | */ 66 | 67 | /** 68 | * The end event is fired whenever the RequestPlayer completes the execution of a request 69 | * 70 | * @event RequestPlayer#end 71 | * @type {Object} 72 | * @property {Object} requestConfig - The original configuration for the request object 73 | * @property {Object} response - The received http.IncomingMessage object 74 | * @property {String} output - The response body 75 | */ 76 | 77 | /** 78 | * The abort event is fired whenever the RequestPlayer aborts an HTTP request 79 | * 80 | * @event RequestPlayer#abort 81 | * @type {Object} 82 | * @property {Object} requestConfig - The original configuration for the request object 83 | */ 84 | 85 | /** 86 | * The cancel event is fired whenever the onInput method of the requestConfig returns false 87 | * 88 | * @event RequestPlayer#cancel 89 | * @type {Object} 90 | * @property {Object} requestConfig - The original configuration for the request object 91 | */ 92 | 93 | /** 94 | * Executes a Request object. This method runs the input callback to retrieve the request data 95 | * then fires the relevant executor based on the request type. 96 | * 97 | * @function 98 | * @private 99 | * @param {Object} requestConfig - An initialized Request object 100 | */ 101 | RequestPlayer.prototype._executeRequest = function (requestConfig) { 102 | if (this.options.debug) this.options.logger.debug("starting execution for request: " + requestConfig.name); 103 | 104 | switch (requestConfig.type) { 105 | case Request.RequestType.HTTP: 106 | this._executeHttpRequest(requestConfig); 107 | break; 108 | case Request.RequestType.LAMBDA: 109 | this._executeLambdaRequest(requestConfig); 110 | break; 111 | } 112 | }; 113 | 114 | /** 115 | * Uses the callback function configured in the request configuration (onInput) to 116 | * generate the request body. If it's an HTTP request the callback receives both the 117 | * context and the http.ClientRequest object. For LAMBDA invokes it only sends the 118 | * context. 119 | * 120 | * @function 121 | * @private 122 | * @param {Request} requestConfig - An initialized Request object 123 | * @param {http.ClientRequest} requestObject - The request object generated by the HTTP client 124 | */ 125 | RequestPlayer.prototype._getRequestInput = function(requestConfig, requestObject) { 126 | var requestInput = null; 127 | 128 | if (requestConfig.inputFunc !== undefined && requestConfig.inputFunc != null) { 129 | var tmpTimer = Date.now(); 130 | switch (requestConfig.type) { 131 | case Request.RequestType.HTTP: 132 | requestInput = requestConfig.inputFunc(this._context, requestObject); 133 | break; 134 | case Request.RequestType.LAMBDA: 135 | requestInput = requestConfig.inputFunc(this._context); 136 | break; 137 | } 138 | tmpTimer = Date.now() - tmpTimer; 139 | 140 | if (this.options.debug) this.options.logger.debug("Input function for " + requestConfig.name + " execution: " + tmpTimer + "ms"); 141 | 142 | return requestInput; 143 | } 144 | } 145 | 146 | /** 147 | * Executes a Lambda function using the AWS SDK for Node.js. The SDK can be initialized 148 | * using a configuration object passed to the ExecutionDirector in the awsConfig property 149 | * of the options. 150 | * 151 | * @function 152 | * @private 153 | * @param {Request} requestConfig - A Request object containing the Lambda function settings 154 | */ 155 | RequestPlayer.prototype._executeLambdaRequest = function(requestConfig) { 156 | if (this.options.awsConfig != null) { 157 | AWS.config = this.options.awsConfig; 158 | } 159 | 160 | var input = this._getRequestInput(requestConfig) 161 | 162 | if (input === false) { 163 | localThis.emit("cancel", requestConfig); 164 | return; 165 | } 166 | 167 | var lambda = AWS.Lambda({apiVersion: '2015-03-31'}); 168 | var functionConfig = { 169 | FunctionName: requestConfig.settings.function, 170 | InvocationType: "RequestResponse", 171 | Payload: new Buffer(input), 172 | Qualifier: requestConfig.settings.qualifier 173 | }; 174 | var localThis = this; 175 | 176 | lambda.invoke(functionConfig, function(err, data) { 177 | if (err) // TODO: is this the right thing to do?! 178 | data = err; 179 | 180 | localThis.emit("end", requestConfig, err, data); 181 | }); 182 | } 183 | 184 | /** 185 | * Executes an HTTP Request object. This method uses the default nodejs http and https modules 186 | * to execute HTTP requests. Once the request is completed this method fires the RequestPlayer#end 187 | * event. 188 | * 189 | * @function 190 | * @private 191 | * @param {Object} requestConfig - An initialized Request object 192 | */ 193 | RequestPlayer.prototype._executeHttpRequest = function (requestConfig) { 194 | var httpClient = http; 195 | if (requestConfig.settings.protocol == "https:") { 196 | httpClient = https; 197 | } 198 | 199 | var localThis = this; 200 | var tmpTimer = Date.now(); 201 | 202 | this.requestObject = httpClient.request(requestConfig.settings, function (response) { 203 | var resp = ""; 204 | response.on("data", function (chunk) { 205 | resp += chunk; 206 | }); 207 | 208 | response.on("end", function () { 209 | tmpTimer = Date.now() - tmpTimer; 210 | 211 | localThis.emit("end", requestConfig, response, resp); 212 | }) 213 | }); 214 | 215 | var input = this._getRequestInput(requestConfig, this.requestObject); 216 | if (input === false) { 217 | localThis.emit("cancel", requestConfig); 218 | return; 219 | } 220 | 221 | if (input != null) { 222 | this.requestObject.write((typeof input === 'object') ? JSON.stringify(input) : input); 223 | } 224 | tmpTimer = Date.now(); 225 | this.emit("start", requestConfig, this.requestObject, input); 226 | 227 | this.requestObject.on("abort", function () { 228 | localThis.emit("abort", this.requestObject); 229 | }) 230 | this.requestObject.end(); 231 | }; 232 | 233 | /** 234 | * Starts the execution of the Request 235 | * 236 | * @function 237 | */ 238 | RequestPlayer.prototype.start = function () { 239 | this._executeRequest(this.request); 240 | }; 241 | 242 | /** 243 | * Aborts the execution of the request 244 | * 245 | * @function 246 | */ 247 | RequestPlayer.prototype.abort = function () { 248 | if (this.requestObject != null) { 249 | this.requestObject.abort(); 250 | } 251 | }; 252 | 253 | 254 | /** 255 | * The request group player monitors the execution of a groups of requests and reports 256 | * the status through its events. The ExecutionDirector object receives the notifications 257 | * from the RequestGroupPlayer and updates the ExecutionContext 258 | * 259 | * @class 260 | * @constructor 261 | * @param {Object} requestGroup - The request group object to be executed 262 | * @param {ExecutionContext} context - The current execution context 263 | * @param {Object} options - Configuration options for the player 264 | * 265 | * @fires RequestGroupPlayer#start 266 | * @fires RequestGroupPlayer#end 267 | * @fires RequestGroupPlayer#requestStart 268 | * @fires RequestGroupPlayer#requestEnd 269 | */ 270 | function RequestGroupPlayer(requestGroup, context, options) { 271 | this.requestGroup = requestGroup; 272 | this.options = options; 273 | 274 | this._context = context; 275 | this._requestsCount = 0; 276 | this._tmpRequests = 0; 277 | this._requests = []; 278 | 279 | events.EventEmitter.call(this); 280 | } 281 | util.inherits(RequestGroupPlayer, events.EventEmitter); 282 | 283 | /** 284 | * The start event is fired whenever the RequestGroupPlayer begins executing a group 285 | * 286 | * @event RequestGroupPlayer#start 287 | * @type {Object} 288 | * @property {Object} requestGroup - The request group object that is being executed 289 | */ 290 | 291 | /** 292 | * The end event is fired whenever the RequestGroupPlayer completes the execution of a group 293 | * 294 | * @event RequestGroupPlayer#end 295 | * @type {Object} 296 | * @property {Object} requestGroup - The completed request group object 297 | */ 298 | 299 | /** 300 | * The requestStart event is fired whenever the RequestGroupPlayer sends the start command 301 | * to a RequestPlayer object 302 | * 303 | * @event RequestGroupPlayer#requestStart 304 | * @type {Object} 305 | * @property {Object} requestGroup - The completed request group object 306 | * @property {Request} requestConfig - The request configuration passed to the RequestPlayer 307 | * @property {http.ClientRequest} requestObject - The node.js http/s request object 308 | * @property {*} input - The input generated for the request by the callback function 309 | */ 310 | 311 | /** 312 | * The requestEnd event is fired whenever the RequestPlayer completes the execution 313 | * of a request 314 | * 315 | * @event RequestGroupPlayer#requestEnd 316 | * @type {Object} 317 | * @property {Object} requestGroup - The completed request group object 318 | * @property {Request} requestConfig - The request configuration passed to the RequestPlayer 319 | * @property {http.IncomingMessage} response - The response object from the node.js http/s client 320 | * @property {*} output - The output returned from the remote server 321 | */ 322 | 323 | /** 324 | * The requestCancel event is fired whenever the onInput function for a request returns false 325 | * 326 | * @event RequestGroupPlayer#requestCancel 327 | * @type {Object} 328 | * @property {Object} requestGroup - The completed request group object 329 | * @property {Request} requestConfig - the request configuration for the request that is being canceled 330 | */ 331 | 332 | /** 333 | * Returns an intialized RequestPlayer object with all of the events connected to callbacks. The 334 | * connections here are used to propagate events up to the ExecutionDirector. 335 | * 336 | * @function 337 | * @private 338 | * @param {Request} request - The request configuration 339 | * @param {ExecutionContext} context - The current execution context 340 | * @param {Object} options - The options for the RequestPlayer object 341 | * 342 | * @return {RequestPlayer} An initliazed RequestPlayer object 343 | */ 344 | RequestGroupPlayer.prototype._getRequestPlayer = function (request, context, options) { 345 | var newRequest = new RequestPlayer(request, context, options); 346 | var localThis = this; 347 | newRequest.on("start", function (requestConfig, requestObject, input) { 348 | localThis.emit("requestStart", localThis.requestGroup, requestConfig, requestObject, input); 349 | }); 350 | newRequest.on("end", function (requestConfig, response, output) { 351 | localThis.emit("requestEnd", localThis.requestGroup, requestConfig, response, output); 352 | 353 | localThis._incrementRequests(); 354 | }); 355 | newRequest.on("cancel", function(requestConfig) { 356 | localThis.emit("requestCancel", localThis.requestGroup, requestConfig); 357 | 358 | localThis._incrementRequests(); 359 | }); 360 | 361 | return newRequest; 362 | }; 363 | 364 | /** 365 | * Utility method that increments the local counter of executed requests and emits the end 366 | * event for the request group if necessary 367 | * 368 | * @function 369 | * @private 370 | */ 371 | RequestGroupPlayer.prototype._incrementRequests = function() { 372 | this._tmpRequests++; 373 | 374 | if (this._tmpRequests == this._requestsCount) { 375 | this.emit("end", this.requestGroup); 376 | } 377 | } 378 | 379 | /** 380 | * Starts the execution of a request group. All of the request objects configured within the group are 381 | * started at the same time. 382 | * 383 | * @function 384 | * @private 385 | * @param {Object} requestGroup - The request group object from the Rcoil tree. 386 | */ 387 | RequestGroupPlayer.prototype._startRequestGroup = function (requestGroup) { 388 | this.emit("start", requestGroup); 389 | this._tmpRequests = 0; 390 | this._requestsCount = requestGroup.requests.length; 391 | 392 | for (var i = 0; i < requestGroup.requests.length; i++) { 393 | var newRequest = this._getRequestPlayer(requestGroup.requests[i], this._context, this.options); 394 | 395 | this._requests.push(newRequest); 396 | newRequest.start(); 397 | } 398 | }; 399 | 400 | /** 401 | * Starts the execution of a request group object from the Rcoil tree. 402 | */ 403 | RequestGroupPlayer.prototype.start = function () { 404 | this._startRequestGroup(this.requestGroup); 405 | } 406 | 407 | module.exports = { 408 | RequestPlayer: RequestPlayer, 409 | RequestGroupPlayer: RequestGroupPlayer 410 | } 411 | --------------------------------------------------------------------------------