diff --git a/.gitignore b/.gitignore index afabb7538..f24b757af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist +npm-debug.log node_modules scripts/scratch* test/integration/yaml_suite/log @@ -6,4 +7,5 @@ test/integration/yaml_suite/log ## generated files scripts/last_rest_spec_update.sha test/browser_integration/yaml_tests.js -test/**/test-output-*.xml +test-output-*.xml +coverage.html diff --git a/test/browser_integration/console.shim.js b/CONTRIBUTING.md similarity index 100% rename from test/browser_integration/console.shim.js rename to CONTRIBUTING.md diff --git a/Gruntfile.js b/Gruntfile.js index fc077079f..378e8f9c4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -3,17 +3,6 @@ module.exports = function (grunt) { - var _ = require('lodash'); - - var sharedBrowserfyExclusions = [ - 'when', - 'src/lib/connectors/http.js', - 'src/lib/loggers/file.js', - 'src/lib/loggers/stdio.js', - 'src/lib/loggers/stream.js', - 'src/lib/loggers/stream.js' - ]; - // Project configuration. grunt.initConfig({ distDir: 'dist', @@ -31,20 +20,6 @@ module.exports = function (grunt) { src: ['<%= distDir %>'] } }, - mochaTest: { - unit: 'test/unit/**/*.test.js', - yaml_suite: { - src: 'test/integration/yaml_suite/index.js', - options: { - reporter: require('./test/integration/yaml_suite/reporter') - } - }, - options: { - require: 'should', - reporter: 'dot', - timeout: 11e3 - } - }, jshint: { source: { src: [ @@ -58,42 +33,6 @@ module.exports = function (grunt) { } } }, - watch: { - source: { - files: [ - 'src/**/*', - 'test/**/*', - 'Gruntfile.js' - ], - tasks: [ - 'jshint:source' - ] - }, - options: { - interupt: true - } - }, - run: { - generate_js_api: { - args: [ - 'scripts/generate/js_api' - ] - }, - generate_yaml_tests: { - args: [ - 'scripts/generate/yaml_tests' - ] - }, - integration_server: { - args: [ - 'test/browser_integration/server.js' - ], - options: { - wait: false, - ready: /server listening/ - } - } - }, browserify: { client: { files: { @@ -101,43 +40,34 @@ module.exports = function (grunt) { }, options: { standalone: 'elasticsearch', - ignore: _.union(sharedBrowserfyExclusions, [ + ignore: [ 'src/lib/connectors/jquery.js', 'src/lib/connectors/angular.js' - ]) + ] } }, - angular_client: { + angular: { files: { '<%= distDir %>/elasticsearch.angular.js': ['src/elasticsearch.angular.js'] }, options: { - standalone: 'elasticsearch', - ignore: _.union(sharedBrowserfyExclusions, [ + ignore: [ 'src/lib/connectors/jquery.js', - 'src/lib/connectors/xhr.js' - ]) - } - }, - yaml_suite: { - files: { - 'test/browser_integration/yaml_tests.js': ['test/integration/yaml_suite/index.js'] - }, - options: { - external: [ - 'optimist' + 'src/lib/connectors/xhr.js', + 'when' ] } - } - }, - concat: { - dist_banners: { + }, + jquery: { files: { - '<%= distDir %>/elasticsearch.js': ['<%= distDir %>/elasticsearch.js'], - '<%= distDir %>/elasticsearch.angular.js': ['<%= distDir %>/elasticsearch.angular.js'] + '<%= distDir %>/elasticsearch.jquery.js': ['src/elasticsearch.jquery.js'] }, options: { - banner: '<%= meta.banner %>' + ignore: [ + 'src/lib/connectors/angular.js', + 'src/lib/connectors/xhr.js', + 'when' + ] } } }, @@ -145,145 +75,41 @@ module.exports = function (grunt) { dist: { files: { '<%= distDir %>/elasticsearch.min.js': '<%= distDir %>/elasticsearch.js', - '<%= distDir %>/elasticsearch.angular.min.js': '<%= distDir %>/elasticsearch.angular.js' + '<%= distDir %>/elasticsearch.angular.min.js': '<%= distDir %>/elasticsearch.angular.js', + '<%= distDir %>/elasticsearch.jquery.min.js': '<%= distDir %>/elasticsearch.jquery.js' + } + } + }, + concat: { + dist_banners: { + files: { + '<%= distDir %>/elasticsearch.angular.js': '<%= distDir %>/elasticsearch.angular.js', + '<%= distDir %>/elasticsearch.angular.min.js': '<%= distDir %>/elasticsearch.angular.min.js', + '<%= distDir %>/elasticsearch.jquery.js': '<%= distDir %>/elasticsearch.jquery.js', + '<%= distDir %>/elasticsearch.jquery.min.js': '<%= distDir %>/elasticsearch.jquery.min.js', + '<%= distDir %>/elasticsearch.js': '<%= distDir %>/elasticsearch.js', + '<%= distDir %>/elasticsearch.min.js': '<%= distDir %>/elasticsearch.min.js' }, options: { - report: 'min', banner: '<%= meta.banner %>' - }, - global_defs: { - process: { - browser: true - } } } } }); // load plugins - grunt.loadNpmTasks('grunt-run'); - grunt.loadNpmTasks('grunt-open'); grunt.loadNpmTasks('grunt-browserify'); - grunt.loadNpmTasks('grunt-mocha-test'); grunt.loadNpmTasks('grunt-contrib-clean'); - grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-jshint'); - // Default task. + // Default task runs the build process. grunt.registerTask('default', [ - 'generate', - 'test', - 'build' + 'clean:dist', + 'browserify', + 'uglify:dist', + 'concat:dist_banners' ]); - // generates the parts of the yaml test suite and api. - grunt.registerTask('generate', [ - 'run:generate_yaml_tests', - 'run:generate_js_api' - ]); - - // runs the tests, must be run after generate - grunt.registerTask('test', function () { - grunt.task.requires('generate'); - grunt.task.run([ - 'jshint', - 'mochaTest:unit', - 'mochaTest:yaml_suite' - ]); - }); - - // runs the build process. - grunt.registerTask('build', function () { - grunt.task.requires('generate'); - grunt.task.run([ - 'clean:dist', - 'browserify', - 'uglify:dist', - 'concat:dist_banners' - ]); - }); - - var browsers = { - safari: { - darwin: 'Safari' - }, - chrome: { - darwin: 'Google Chrome', - win32: 'Google Chrome', - executable: 'google-chrome' - }, - chromium: { - executable: 'chromium-browser', - }, - firefox: { - darwin: 'Firefox', - win32: 'Firefox', - executable: 'firefox' - }, - opera: { - darwin: 'Opera', - win32: 'Opera', - executable: 'opera' - } - }; - - // creates browser_tests:{{browser}} tasks, for the browsers listed directly above - Object.keys(browsers).forEach(function (browser) { - var appName = browsers[browser][process.platform]; - // on other platforms, open expects app to be the name of the executale... - if (!appName && process.platform !== 'darwin' && process.platform !== 'win32') { - appName = browsers[browser].executable; - } - - if (!appName) { - // this browser doesn't run on this arch - return; - } - - grunt.config.set('__open_browser_tests.' + browser, { - appName: appName - }); - - grunt.registerTask('browser_tests:' + browser, [ - 'generate', - 'build', - 'run:integration_server', - '__open_browser_tests:' + browser - ]); - }); - - /** - * USE browser_tests:{{browser}} to run this task - * - * Change the port/host that the client connects to with the ES_HOST and ES_PORT environment variables - * - * You must always run the build task first, to ensure that the lastest API and yaml tests are available. - * This is run in the default and browser_tests:{{browser}} tests. - */ - grunt.registerMultiTask('__open_browser_tests', function () { - var host = grunt.option('host') || 'localhost'; - var port = grunt.option('port') || 9200; - var taskData = this.data; - - grunt.task.requires([ - 'generate', - 'build', - 'run:integration_server' - ]); - - grunt.config.set('open.yaml_suite_' + this.target, { - path: 'http://localhost:8888?es_hostname=' + encodeURIComponent(host) + - '&es_port=' + encodeURIComponent(port) + - '&browser=' + encodeURIComponent(this.target), - app: taskData.appName - }); - - grunt.task.run([ - 'open:yaml_suite_' + this.target, - 'wait:integration_server' - ]); - }); - }; diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..e69de29bb diff --git a/README.md b/README.md index a1e1003ab..2880c3f4b 100644 --- a/README.md +++ b/README.md @@ -60,200 +60,9 @@ bower install elasticsearch-jquery ``` ## Docs - - [Configuration](#configuration) + - [Configuration](docs/configuration.md) + - [Examples](docs/examples.md) - [API](docs/api.md) - [Replacing Core Components](docs/replacing_core_components.md) - [Errors](docs/errors.md) - - [Setting Up Logging](docs/setting_up_logging.md) - - [FAQ](#faq) - -## Configuration - -The `Client` constructor accepts a single object as it's argument, and the following keys can be used to configure that client instance: - -```js -var elasticsearch = require('elasticsearch'); -var es = new elasticsearch.Client({ - ... -}); -``` - -### hosts -Type: `String`, `String[]` or `Object[]` - -Default: -```js -hosts: [ - { - host: 'localhost', port: '9200', protocol: 'http' - } -] -``` - -Specify the list of hosts that this client will connect to. If sniffing is enabled, or you call sniff, this list will be used as seeds for discovery of the rest of the cluster. - -### log -Type: `String`, `String[]`, `Object`, `Object[]`, or `Constructor` - -Default: -```js -log: { - type: 'stdio', - levels: ['error', 'warning'] -} -``` - -Unless a constructor is specified, this sets the output settings for the bundled logger. See [setting up logging](docs/setting_up_logging.md) for more information. - -### connectionClass -Type: `String`, `Constructor` - -Default: - - Node: `'http'` - - Browser: `'xhr'` - - Angular Build: `'angular'` - - jQuery Build: `'jquery'` - -Defines the class that will be used to create connections. If you are looking to implement a protocol besides HTTP you will probably start by writing a Connection class and specifying it here. - -### selector -Type: `String`, `Function` - -Default: `'roundRobin'` - -Options: - - `'roundRobin'` - - `'random'` - -This function will be used to select a connection from the ConnectionPool. It should received a single argument, the list of "active" connections, and return the connection to use. Use this selector to implement special logic for your client such as preferring nodes in a certain rack or data-center. - -To make this function asynchronous, accept a second argument which will be the callback to use. The callback should be called Node-style, with a possible error like `cb(err, selectedConnection)`. - -### sniffOnStart -Type: `Boolean` - -Default: `false` - -Should the client attempt to detect the rest of the cluster when it is first instantiated? - -### sniffAfterRequests -Type: `Number` or `false` - -Default: `false` - -After `n` requests, perform a sniff operation and ensure our list of nodes is up to date. - - -### sniffOnConnectionFail -Type: `Boolean` - -Default: `false` - -Should the client immediately sniff for a more current list of nodes when a connection dies? (see [node death](#node-death)) - -### maxRetries -Type: `Number` - -Defailt: `3` - -How many times should the client try to connect to other nodes before returning a [ConnectionFault](docs/error.md#connectionfault) error. (see [node death](#node-death)) - -### timeout -Type: `Number` - -Default: 10000 - -How many milliseconds can the connection take before the request is aborted and retried. (TODO: timeout errors shouldn't cause a retry). - -### deadTimeout -Type: `Number` - -Default: 30000 - -How many milliseconds should a dead connection/node sit and wait before it is ping-ed? (see [node death](#node-death)) - -### maxSockets -Type: `Number` - -Default: 10 - -How many sockets should a connection keep to it's corresponding Elasticsearch node? These sockets are currently kept alive ***forever*** (not like nodes current "keep alive" sockets). - -### nodesToHostCallback -Type: `Function` - -Default: simple, not much going on [here](src/lib/client_config.js#L65). - -This function will receive a list of nodes received during a sniff. The list of nodes should be transformed into an array of objects which will each be used to create [Host](src/lib/host.js) objects. (TODO: allow this function to be async). - -## API - -To maintain consistency across all the low-level clients ([PHP](https://github.com/elasticsearch/elasticsearch-php), [Python](https://github.com/elasticsearch/elasticsearch-ph), [Ruby](https://github.com/elasticsearch/elasticsearch-ruby), [Perl](https://github.com/elasticsearch/elasticsearch-perl)) all API methods accept an object with parameters and a callback. If you don't pass the callback, the functions will return a promise. - -For full details on the API, check out [api.md](docs/api.md). - -### Examples - -#### create the client -```js -var es = new elasticsearch.Client({ - hosts: [ - 'localhost:9200' - ], - log: 'trace', - sniffOnStart: true -}); -``` - -#### call an endpoint -```js -es.cluster.nodeInfo({ - clear: true, - jvm: true, - os: ture -}, function (err, resp, status) { - // do your thing -}) -``` - -#### skip the callback to get a promise back -```js -es.search({ - q: 'pants' -}).then(function (resp) { - // use resp.body and resp.status -}, function (err) { - // freak out! -}) -``` - -#### abort a request -```js -var req = es.search({ - q: 'robots' -}, function (err, body, status) { - clearTimeout(timeout); - // do something -}); - -var timeout = setTimeout(function () { - req.abort(); -}, 200); -``` - -#### or just use the timeout param -```js -es.search({ - q: '*', - timeout: 200 -}).then(function (resp) { - // Iterate all the hits -}) -``` - -## FAQ - -### dead nodes -Q: When is a connection/node considered dead? - -A: A connection is considered dead when a request to it does not complete properly. If the server responds with any status, even 500, it is not considered dead. + - [Customize Logging](docs/customize_logging.md) diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 000000000..d488598fc --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,117 @@ + +The `Client` constructor accepts a single object as it's argument, and the following keys can be used to configure that client instance: + +```js +var elasticsearch = require('elasticsearch'); +var es = new elasticsearch.Client({ + ... +}); +``` + +### hosts +Type: `String`, `String[]` or `Object[]` + +Default: +```js +hosts: [ + { + host: 'localhost', port: '9200', protocol: 'http' + } +] +``` + +Specify the list of hosts that this client will connect to. If sniffing is enabled, or you call sniff, this list will be used as seeds for discovery of the rest of the cluster. + +### log +Type: `String`, `String[]`, `Object`, `Object[]`, or `Constructor` + +Default: +```js +log: { + type: 'stdio', + levels: ['error', 'warning'] +} +``` + +Unless a constructor is specified, this sets the output settings for the bundled logger. See [setting up logging](docs/setting_up_logging.md) for more information. + +### connectionClass +Type: `String`, `Constructor` + +Default: + - Node: `'http'` + - Browser: `'xhr'` + - Angular Build: `'angular'` + - jQuery Build: `'jquery'` + +Defines the class that will be used to create connections. If you are looking to implement a protocol besides HTTP you will probably start by writing a Connection class and specifying it here. + +### selector +Type: `String`, `Function` + +Default: `'roundRobin'` + +Options: + - `'roundRobin'` + - `'random'` + +This function will be used to select a connection from the ConnectionPool. It should received a single argument, the list of "active" connections, and return the connection to use. Use this selector to implement special logic for your client such as preferring nodes in a certain rack or data-center. + +To make this function asynchronous, accept a second argument which will be the callback to use. The callback should be called Node-style, with a possible error like `cb(err, selectedConnection)`. + +### sniffOnStart +Type: `Boolean` + +Default: `false` + +Should the client attempt to detect the rest of the cluster when it is first instantiated? + +### sniffAfterRequests +Type: `Number` or `false` + +Default: `false` + +After `n` requests, perform a sniff operation and ensure our list of nodes is up to date. + + +### sniffOnConnectionFail +Type: `Boolean` + +Default: `false` + +Should the client immediately sniff for a more current list of nodes when a connection dies? (see [node death](#node-death)) + +### maxRetries +Type: `Number` + +Defailt: `3` + +How many times should the client try to connect to other nodes before returning a [ConnectionFault](docs/error.md#connectionfault) error. (see [node death](#node-death)) + +### timeout +Type: `Number` + +Default: 10000 + +How many milliseconds can the connection take before the request is aborted and retried. (TODO: timeout errors shouldn't cause a retry). + +### deadTimeout +Type: `Number` + +Default: 30000 + +How many milliseconds should a dead connection/node sit and wait before it is ping-ed? (see [node death](#node-death)) + +### maxSockets +Type: `Number` + +Default: 10 + +How many sockets should a connection keep to it's corresponding Elasticsearch node? These sockets are currently kept alive ***forever*** (not like nodes current "keep alive" sockets). + +### nodesToHostCallback +Type: `Function` + +Default: simple, not much going on [here](src/lib/client_config.js#L65). + +This function will receive a list of nodes received during a sniff. The list of nodes should be transformed into an array of objects which will each be used to create [Host](src/lib/host.js) objects. (TODO: allow this function to be async). diff --git a/docs/setting_up_logging.md b/docs/customize_logging.md similarity index 70% rename from docs/setting_up_logging.md rename to docs/customize_logging.md index 7f3cdbf82..f599c789a 100644 --- a/docs/setting_up_logging.md +++ b/docs/customize_logging.md @@ -1,3 +1,3 @@ -# Setting Up Logging +# Customize Logging TODO: what are loggers, how to use bunyan/winston diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 000000000..0a2a78498 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,52 @@ +### Examples +#### create the client +```js +var es = new elasticsearch.Client({ + hosts: [ + 'localhost:9200' + ], + log: 'trace', + sniffOnStart: true +}); +``` +#### call an endpoint +```js +es.cluster.nodeInfo({ + clear: true, + jvm: true, + os: ture +}, function (err, resp, status) { + // do your thing +}) +``` +#### skip the callback to get a promise back +```js +es.search({ + q: 'pants' +}).then(function (resp) { + // use resp.body and resp.status +}, function (err) { + // freak out! +}) +``` +#### abort a request +```js +var req = es.search({ + q: 'robots' +}, function (err, body, status) { + clearTimeout(timeout); + // do something +}); +var timeout = setTimeout(function () { + req.abort(); +}, 200); +``` +#### or just use the timeout param +```js +es.search({ + q: '*', + timeout: 200 +}).then(function (resp) { + // Iterate all the hits +}) +``` diff --git a/package.json b/package.json index b0f48a02a..f6d62f88b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,10 @@ "name": "elasticsearch-js", "homepage": "https://github.com/elasticsearch/elasticsearch-js", "version": "0.0.1", + "browser": { + "./src/lib/connectors/index.js": "./src/lib/connectors/browser_index.js", + "./src/lib/loggers/index.js": "./src/lib/loggers/browser_index.js" + }, "devDependencies": { "tar": "~0.1.18", "mocha": "~1.14.0", @@ -25,19 +29,34 @@ "grunt-contrib-clean": "~0.5.0", "grunt-contrib-uglify": "~0.2.7", "grunt-contrib-concat": "~0.3.0", - "grunt-open": "~0.2.2", - "grunt-run": "~0.1.0", "xmlbuilder": "~0.4.3", - "grunt-contrib-watch": "~0.5.3" + "grunt-contrib-watch": "~0.5.3", + "coveralls": "~2.3.0", + "mocha-lcov-reporter": "0.0.1", + "blanket": "~1.1.5", + "sinon": "~1.7.3", + "nock": "~0.23.0", + "open": "0.0.4", + "testling": "https://github.com/spenceralger/testling/tarball/master" }, "license": "Apache License", "dependencies": { "when": "~2.6.0", "lodash": "~2.3.0", "agentkeepalive": "~0.1", - "chalk": "~0.3.0" + "chalk": "~0.3.0", + "inherits": "~2.0.1" }, "scripts": { - "test": "grunt generate test" + "test": "node scripts/run_tests.js", + "build": "grunt", + "generate": "node scripts/generate/js_api && node scripts/generate/yaml_tests", + "blanket": { + "pattern": "src" + } + }, + "testling": { + "harness": "mocha", + "files": "test/unit/test_!(http_connection|stdio_logger).js" } } diff --git a/scripts/generate/js_api/actions.js b/scripts/generate/js_api/actions.js index 606227b4f..0ccab70e1 100644 --- a/scripts/generate/js_api/actions.js +++ b/scripts/generate/js_api/actions.js @@ -95,9 +95,13 @@ function transformFile(entry) { }); }); - spec.urls = _.map(_.sortBy(urls, 'sortOrder'), function (url) { - return _.omit(url, 'sortOrder'); - }); + if (urls.length > 1) { + spec.urls = _.map(_.sortBy(urls, 'sortOrder'), function (url) { + return _.omit(url, 'sortOrder'); + }); + } else { + spec.url = urls[0]; + } spec.params = _.transform(spec.params, function (note, param, name) { // param.name = name; @@ -106,6 +110,10 @@ function transformFile(entry) { ]); }, {}); + if (_.size(spec.params) === 0) { + delete spec.params; + } + // escape method names with "special" keywords var location = spec.name.split('.').join('.prototype.') .replace(/(^|\.)(delete|default)(\.|$)/g, '[\'$2\']'); @@ -114,6 +122,7 @@ function transformFile(entry) { spec: _.pick(spec, [ 'methods', 'params', + 'url', 'urls', 'needBody', 'bulkBody', @@ -126,6 +135,61 @@ function transformFile(entry) { allParams: allParams }; + function hasMethod(/* ...methods */) { + for (var i = 0; i < arguments.length; i++) { + if (~action.spec.methods.indexOf(arguments[i])) { + continue; + } else { + return false; + } + } + return true; + } + function methodsAre(/* ...methods */) { + return hasMethod.apply(null, arguments) && arguments.length === action.spec.methods.length; + } + + var method; + + if (action.spec.methods.length === 1) { + method = action.spec.methods[0]; + } else { + // we need to define what the default method(s) will be + if (hasMethod('DELETE', 'POST')) { + method = 'POST'; + } + else if (methodsAre('DELETE')) { + method = 'DELETE'; + } + else if (methodsAre('POST', 'PUT')) { + method = 'POST'; + } + else if (methodsAre('GET', 'POST')) { + method = 'POST'; + } + else if (methodsAre('GET', 'HEAD')) { + if (action.spec.castExists) { + method = 'HEAD'; + } else { + method = 'GET'; + } + } + } + + if (method) { + if (method !== 'GET') { + action.spec.method = method; + } + delete action.spec.methods; + } else { + throw new Error('unable to pick a method for ' + JSON.stringify(action, null, ' ')); + } + + if (action.name === 'create') { + action.proxy = 'index'; + action.transformBody = 'params.op_type = \'create\';'; + } + if (actions.push(action) === specCount && doneParsing) { module.exports.emit('ready', action); } diff --git a/scripts/generate/js_api/index.js b/scripts/generate/js_api/index.js index 95790a535..8409e904a 100644 --- a/scripts/generate/js_api/index.js +++ b/scripts/generate/js_api/index.js @@ -17,12 +17,21 @@ function download() { } })); + // seperate the proxy actions + var groups = _.groupBy(actions, function (action) { + return action.proxy ? 'proxies' : 'normal'; + }); + clean(outputPath); + console.log('writing', actions.length, 'api actions to', outputPath); + fs.writeFileSync(outputPath, templates.apiFile({ - actions: actions, + actions: groups.normal, + proxies: groups.proxies, namespaces: _.unique(namespaces.sort(), true) })); + fs.writeFileSync(docOutputPath, templates.apiDocs({ actions: actions })); @@ -30,7 +39,7 @@ function download() { } restSpecUpdated(function (err, updated) { - if (process.env.FORCE_GEN || err || updated) { + if (process.env.FORCE_GEN || process.env.npm_config_force || err || updated) { download(); } }); diff --git a/scripts/generate/js_api/templates/action.tmpl b/scripts/generate/js_api/templates/action.tmpl deleted file mode 100644 index a059f5671..000000000 --- a/scripts/generate/js_api/templates/action.tmpl +++ /dev/null @@ -1,73 +0,0 @@ -var _ = require('<%= path2lib %>utils'); -var errors = require('<%= path2lib %>errors');<% - - -if (_.keys(enumOptions).length) { - %> -<% _.each(enumOptions, function(options, name) { - %> -var <%= name %>Options = <%= stringify(options) %>;<% - }); -} -%> - -/** - * Perform an elasticsearch [<%= name %>](<%= docUrl %>) request - * - * @for Client - * @method <%= name %> - * @param {Object} params - An object with parameters used to carry out this action<% _.each(params, function(param, paramName) { %> - * @param {<%= paramType(param.type) %>} <%= paramWithDefault('params.' + paramName, param.default) %><% if (param.description) { %> - <%= param.description %><% } %><% -}) -%> - */ -function do<%= _.studlyCase(name) %>(params, cb) { - if (typeof params === 'function') { - cb = params; - params = {}; - } else { - params = params || {}; - cb = typeof cb === 'function' ? cb : _.noop; - } - - var request = { -<%= writeRequestObjectBody(4, name, body, methods) %> - }; - var parts = {}; - var query = {}; - var responseOpts = {}; -<% - -if (methods.length > 1) { %> - // figure out the method - if (params.method = _.toUpperString(params.method)) { - if (<%= _.map(methods, function (method) { return 'params.method === ' + stringify(method) }).join(' || ') %>) { - request.method = params.method; - } else { - throw new TypeError('Invalid method: should be one of <%= methods.join(', ') %>'); - } - } else {<% - if (_.contains(methods, 'GET')) { - var nonGet = _.find(methods, function (m) {return m !== 'GET'; });%> - request.method = params.body ? <%= stringify(nonGet) %> : 'GET';<% - } else {%> - request.method = <%= stringify(methods[0]) %>;<% - }%> - }<% -} -%> - - // find the paths's params -<%= writeParams(2, urlParts, 'parts.') %> - - // build the path -<%= writeUrls(2, urls, urlParts, params) %> - - // build the query string -<%= writeParams(2, params, 'query.') %> - request.path = request.path + _.makeQueryString(query); - -<%= returnStatement(2, name) %> -} - -module.exports = do<%= _.studlyCase(name) %>; diff --git a/scripts/generate/js_api/templates/api_file.tmpl b/scripts/generate/js_api/templates/api_file.tmpl index 25d1efde9..cffffac5d 100644 --- a/scripts/generate/js_api/templates/api_file.tmpl +++ b/scripts/generate/js_api/templates/api_file.tmpl @@ -12,16 +12,16 @@ _.each(actions, function (action) { var className = _.studlyCase(namespace) + 'NS'; %> -api.<%= namespace %> = function <%= className %>(client) { - if (this instanceof <%= className %>) { - this.client = client; - } else { - return new <%= className %>(client); - } +api.<%= namespace %> = function <%= className %>(transport) { + this.transport = transport; };<% - } -%> + }%> <%= partials.client_action(action) %><% -}); %> +}); + +_.each(proxies, function (action) {%> +<%= partials.client_action_proxy(action) %><% +}); +%> diff --git a/scripts/generate/js_api/templates/client_action_proxy.tmpl b/scripts/generate/js_api/templates/client_action_proxy.tmpl new file mode 100644 index 000000000..d708f1401 --- /dev/null +++ b/scripts/generate/js_api/templates/client_action_proxy.tmpl @@ -0,0 +1,19 @@ +/** + * Perform a [<%= name %>](<%= docUrl %>) request + * + * @param {Object} params - An object with parameters used to carry out this action<% +_.each(allParams, function(param, paramName) { %> + * @param {<%= paramType(param.type) %>} <%= paramWithDefault('params.' + paramName, param.default) %><% + if (param.description) { + %> - <%= param.description %><% + } +%><% }) %> + */ +api<%= (location[0] === '[' ? '' : '.') + location %> = ca.proxy(<%= 'api' + (proxy[0] === '[' ? '' : '.') + proxy %><% +if (typeof transformBody === 'string') { %>, { + transform: function (params) { +<%= indent(transformBody, 4) %> + } +}<% +} +%>); diff --git a/scripts/generate/js_api/templates/index.js b/scripts/generate/js_api/templates/index.js index 2f2f91573..30ce1b230 100644 --- a/scripts/generate/js_api/templates/index.js +++ b/scripts/generate/js_api/templates/index.js @@ -42,6 +42,13 @@ var templateGlobals = { _: _, + indent: function (block, spaces) { + var indent = _.repeat(' ', spaces); + return block.split('\n').map(function (line) { + return indent + line; + }).join('\n'); + }, + paramType: function (type) { switch (type && type.toLowerCase ? type.toLowerCase() : 'any') { case 'time': diff --git a/scripts/generate/logs/index.js b/scripts/generate/logs/index.js index 0f14f14ae..834417265 100644 --- a/scripts/generate/logs/index.js +++ b/scripts/generate/logs/index.js @@ -34,7 +34,8 @@ var es = require('../../../src/elasticsearch'), endingMoment = moment().endOf('day').add('days', days), clientConfig = { log: { - level: 'error' + level: 'trace', + type: 'stdio' } }; @@ -95,7 +96,7 @@ fillIndecies(function () { actions.push(event); if (actions.length === 3000 || i === count - 1) { - client.config.log.info('writing', actions.length / 2, 'documents'); + console.info('writing', actions.length / 2, 'documents'); client.bulk({ body: actions }, done); @@ -176,10 +177,11 @@ function fillIndecies(cb) { async.parallel(indexPushActions, function (err, responses) { if (err) { - client.config.log.error(err.message = 'Unable to create indicies: ' + err.message); + console.error(err.message = 'Unable to create indicies: ' + err.message); + console.error(err.stack); } else { _.each(_.groupBy(responses), function (list, did) { - client.config.log.info(list.length, 'indicies', did); + console.info(list.length, 'indicies', did); }); cb(); } diff --git a/scripts/generate/yaml_tests/index.js b/scripts/generate/yaml_tests/index.js index 5828f2020..9bd88996a 100644 --- a/scripts/generate/yaml_tests/index.js +++ b/scripts/generate/yaml_tests/index.js @@ -32,7 +32,7 @@ function download() { restSpecUpdated(function (err, updated) { - if (process.env.FORCE_GEN || err || updated) { + if (process.env.FORCE_GEN || process.env.npm_config_force || err || updated) { download(); } }); diff --git a/test/make_j_unit_xml.js b/scripts/make_j_unit_xml.js similarity index 100% rename from test/make_j_unit_xml.js rename to scripts/make_j_unit_xml.js diff --git a/scripts/rest_spec_updated.js b/scripts/rest_spec_updated.js index b17c458e8..0f615f662 100644 --- a/scripts/rest_spec_updated.js +++ b/scripts/rest_spec_updated.js @@ -1,5 +1,11 @@ var https = require('https'); -var lastCommitUrl = 'https://api.github.com/repos/elasticsearch/elasticsearch-rest-api-spec/commits/HEAD'; +var request = { + hostname: 'api.github.com', + path: '/repos/elasticsearch/elasticsearch-rest-api-spec/commits/HEAD', + headers: { + 'User-Agent': 'spenceralger' + } +}; var fs = require('fs'); var lastRestSpecUpdateFile = __dirname + '/last_rest_spec_update.sha'; @@ -10,7 +16,7 @@ if (fs.existsSync(lastRestSpecUpdateFile)) { lastRestSpecUpdate = fs.readFileSync(lastRestSpecUpdateFile, 'utf8'); } -var req = https.get(lastCommitUrl, function (incoming) { +var req = https.get(request, function (incoming) { if (incoming.statusCode !== 200) { req.abort(); console.error('request for last commit failed', incoming.statusCode, incoming.headers); diff --git a/scripts/run_browser_integration_suite/index.js b/scripts/run_browser_integration_suite/index.js new file mode 100644 index 000000000..cbb2159d2 --- /dev/null +++ b/scripts/run_browser_integration_suite/index.js @@ -0,0 +1,146 @@ +var server = require('./server'); +var child_process = require('child_process'); +var _ = require('lodash'); +var open = require('open'); +var fs = require('fs'); +var path = require('path'); +var async = require('async'); + +var yamlTestSourceFile = path.join(__dirname, '../../test/integration/yaml_suite/index.js'); +var yamlTestBundleFile = path.join(__dirname, '../../test/browser_integration/yaml_tests.js'); +var clientEntryFile = path.join(__dirname, '../../src/elasticsearch.js'); + +var browsers = _.transform({ + safari: { + darwin: 'Safari' + }, + chrome: { + darwin: 'Google Chrome', + win32: 'Google Chrome', + executable: 'google-chrome' + }, + chromium: { + + executable: 'chromium-browser', + }, + firefox: { + darwin: 'Firefox', + win32: 'Firefox', + executable: 'firefox' + }, + opera: { + darwin: 'Opera', + win32: 'Opera', + executable: 'opera' + } +}, function (browsers, config, name) { + if (config[process.platform]) { + browsers[name] = config[process.platform]; + return; + } + + if (process.platform !== 'darwin' && process.platform !== 'win32' && config.executable) { + browsers[name] = config.executable; + return; + } +}, {}); + +var argv = require('optimist') + .default('browser', 'chrome') + .default('force_gen', false) + .boolean('force_gen') + .alias('f', 'force_gen') + .default('host', 'localhost') + .default('port', 9200) + .argv; + +var browserAppName; + +async.series([ + function (done) { + if (browsers.hasOwnProperty(argv.browser)) { + browserAppName = browsers[argv.browser]; + done(); + } else { + done('--browser must be set to one of ' + _.keys(browsers).join(', ') + ' on this platform'); + } + }, + function (done) { + fs.exists('dist', function (yes) { + if (!argv.force_gen && yes) { + done(); + return; + } + + console.log('generating client with "grunt build"'); + child_process.spawn('grunt', ['build'], { + stdio: 'inherit' + }).on('close', function (status) { + done(status && 'grunt closed with a status code of ' + status + '. aborting.'); + }); + }); + }, + function (done) { + fs.exists(yamlTestBundleFile, function (yes) { + if (!argv.force_gen && yes) { + done(); + return; + } + + console.log('generating browser\'s yaml_tests.js bundle'); + var b = require('browserify')(); + + b.add(yamlTestSourceFile); + var bundle = b.bundle({ + external: [ + 'optimist' + ], + ignore: [ + 'test/integration/yaml_suite/reporter', + clientEntryFile + ] + }); + var file = fs.createWriteStream(yamlTestBundleFile, { + flags: 'w', + encoding: 'utf8', + mode: 0666 + }); + + bundle.pipe(file); + + file.once('error', function (err) { + done(err); + }); + + bundle.once('error', function (err) { + done(err); + }); + + bundle.once('end', function () { + done(); + }); + + }); + } +], function (err) { + if (err) { + console.error(err); + process.exit(1); + } else { + server.listen(0, function () { + var port = server.address().port; + console.log('server listening on port', port); + + open('http://localhost:' + port + '?es_hostname=' + encodeURIComponent(argv.host) + + '&es_port=' + encodeURIComponent(argv.port) + + '&browser=' + encodeURIComponent(argv.browser), browserAppName); + }); + + server.on('tests done', function (success) { + console.log('test completed', success ? 'successfully' : 'but failed'); + process.exit(success ? 0 : 1); + }); + } + +}); + diff --git a/test/browser_integration/server.js b/scripts/run_browser_integration_suite/server.js similarity index 78% rename from test/browser_integration/server.js rename to scripts/run_browser_integration_suite/server.js index 8a1b1f6ee..8a3a93055 100644 --- a/test/browser_integration/server.js +++ b/scripts/run_browser_integration_suite/server.js @@ -2,22 +2,43 @@ var http = require('http'); var url = require('url'); var path = require('path'); var fs = require('fs'); -var crypto = require('crypto'); var _ = require('lodash'); -var util = require('util'); var chalk = require('chalk'); -var moment = require('moment'); var makeJUnitXml = require('../make_j_unit_xml'); chalk.enabled = true; -var browserify = require('browserify'); -var port = process.argv[2] || 8888; - var middleware = []; Error.stackTraceLimit = Infinity; -var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; +var chars = 'abcdefghijklmnopqrstuvwxyz'; + +var server = http.createServer(function (req, resp) { + var parsedUrl = url.parse(req.url, true); + req.uri = parsedUrl.pathname; + req.query = parsedUrl.query; + req.filename = path.join(__dirname, '../../test/browser_integration/', req.uri); + + var end = resp.end; + resp.end = function () { + console.log(chalk[this.statusCode < 300 ? 'green' : 'red'](this.statusCode), req.uri); + end.apply(resp, arguments); + }; + + var middleIndex = -1; + + function next() { + middleIndex++; + if (middleIndex < middleware.length) { + middleware[middleIndex](req, resp, next); + } else { + resp.writeHead(500); + resp.end('500 Bad Gateway\n'); + } + } + next(); +}); + function rand(length) { var str = ''; while (str.length < length) { @@ -26,31 +47,10 @@ function rand(length) { return str; } -function sendBundle(req, resp, files, opts, extend) { - resp.setHeader('Content-Type', 'application/javascript'); - resp.writeHead(200); - - var b = browserify(files); - - if (typeof extend === 'function') { - extend(b); - } - - var out = b.bundle(opts); - - out.on('data', function (chunk) { - resp.write(chunk); - }); - - out.on('end', function () { - resp.end(); - }); -} - function collectTestResults(req, resp) { var body = ''; var browser = req.query.browser; - var logFilename = path.join(__dirname, '../test-output-' + browser + '.xml'); + var logFilename = path.join(__dirname, '../../test-output-' + browser + '.xml'); req.on('data', function (chunk) { body += chunk; @@ -79,40 +79,15 @@ function collectTestResults(req, resp) { if (err) { console.log('unable to save test-output to', err.message); console.trace(); - process.exit(1); + server.emit('tests done', false); } else { console.log('test output written to', logFilename); - process.exit(testDetails.stats.failures ? 1 : 0); + server.emit('tests done', !testDetails.stats.failures); } }); }); } -var server = http.createServer(function (req, resp) { - var parsedUrl = url.parse(req.url, true); - req.uri = parsedUrl.pathname; - req.query = parsedUrl.query; - req.filename = path.join(__dirname, req.uri); - - var end = resp.end; - resp.end = function () { - console.log(chalk[this.statusCode < 300 ? 'green' : 'red'](this.statusCode), req.uri); - end.apply(resp, arguments); - }; - - var middleIndex = -1; - - function next() { - middleIndex++; - if (middleIndex < middleware.length) { - middleware[middleIndex](req, resp, next); - } else { - resp.writeHead(500); - resp.end('500 Bad Gateway\n'); - } - } - next(); -}); middleware.push(function (req, resp, next) { // resolve filenames @@ -145,14 +120,14 @@ middleware.push(function (req, resp, next) { resp.end(); } else { if (stats.isDirectory()) { - req.filename = path.join(req.filename, './index.html'); + req.filename = path.join(req.filename, '../../test/browser_integration/index.html'); } next(); } }); }); -middleware.push(function (req, resp, next) { +middleware.push(function (req, resp) { // static files var reader = fs.createReadStream(req.filename); var data = ''; @@ -201,7 +176,7 @@ middleware.push(function (req, resp, next) { es_hostname: 'localhost', es_port: 9200, browser: 'unknown', - ts: rand(5) + ts: 'no'//rand(5) }))); } else { resp.end(data); @@ -211,6 +186,5 @@ middleware.push(function (req, resp, next) { } }); -server.listen(parseInt(port, 10), function () { - console.log('server listening on port', server.address().port); -}); + +module.exports = server; diff --git a/scripts/run_tests.js b/scripts/run_tests.js new file mode 100644 index 000000000..9bce02273 --- /dev/null +++ b/scripts/run_tests.js @@ -0,0 +1,81 @@ +var async = require('async'); +var cp = require('child_process'); +var chalk = require('chalk'); +var argv = require('optimist') + .default({ + 'check-upstream': false, + 'in-node': true, + 'in-browser': true, + 'not-in-node': false, + 'not-in-browser': false, + 'unit': true, + 'integration': true + }) + .alias({ + u: 'unit', + i: 'integration', + b: 'in-browser', + n: 'in-node', + }) + .parse(JSON.parse(process.env.npm_config_argv).original); + +if (argv['not-in-browser']) { + argv.b = argv['in-browser'] = false; +} +if (argv['not-in-node']) { + argv.n = argv['in-node'] = false; +} + +var commands = []; + +if (argv['check-upstream']) { + commands.push(['node', 'scripts/generate/yaml_tests/index.js']); +} + +if (argv.unit) { + if (argv['in-node']) { + commands.push(['mocha', 'test/unit/test_*.js', '--require=should']); + } + if (argv['in-browser']) { + commands.push(['testling', '.']); + } +} + +if (argv.integration) { + if (argv['in-node']) { + commands.push(['mocha', 'test/integration/yaml_suite/index.js', '-b', '--require=should']); + } + if (argv['in-browser']) { + commands.push(['node', 'scripts/run_browser_integration_suite/index.js']); + } +} + +if (commands.length) { + async.forEachSeries(commands, function (args, done) { + var command = args.shift(); + console.log(chalk.gray('\n\n' + '# ' + command + ' ' + args.join(' '))); + var proc = cp.spawn(command, args, { + stdio: 'inherit' + }); + + proc.on('error', function (err) { + proc.removeAllListeners(); + done(err); + }); + + proc.on('exit', function (status) { + proc.removeAllListeners(); + done(status ? new Error(command + ' exited with status ' + status) : void 0); + }); + }, function (err) { + if (err) { + console.error(err.message); + process.exit(1); + } else { + process.exit(0); + } + }); +} else { + console.log('Arguments resulted in no tests to run.'); + console.log('Try combining test types with environments'); +} diff --git a/scripts/server.js b/scripts/server.js deleted file mode 100644 index 5ae66c3c4..000000000 --- a/scripts/server.js +++ /dev/null @@ -1,28 +0,0 @@ -var http = require('http'); - -var server = http.createServer(function (req, resp) { - var closed, count = 0; - - resp.on('close', function () { - closed = true; - console.log('response was closed'); - }); - - process.removeAllListeners(); - - var interval = setInterval(function () { - if (count > 99 || resp.closed || closed) { - clearInterval(interval); - console.log('done writing', resp.socket.bytesWritten, 'bytes'); - resp.end(); - } else { - process.stdout.write('->'); - resp.write('line of data, more to come... slowly!'); - count++; - } - }, 100); -}); - -server.listen(7500, function () { - console.log('server listening at', server.address()); -}); diff --git a/src/elasticsearch.angular.js b/src/elasticsearch.angular.js index 94ce21c9a..6e889b27f 100644 --- a/src/elasticsearch.angular.js +++ b/src/elasticsearch.angular.js @@ -5,40 +5,27 @@ * It will also instruct the client to use Angular's $http service for it's ajax requests */ var AngularConnector = require('./lib/connectors/angular'); -var Transport = require('./lib/transport'); var Client = require('./lib/client'); +process.angular_build = true; + /* global angular */ angular.module('elasticsearch.client', []) .factory('esFactory', ['$http', '$q', function ($http, $q) { - AngularConnector.prototype.$http = $http; + AngularConnector.prototype.$http = $http; - // store the original request function - Transport.prototype._request = Transport.prototype.request; + var factory = function (config) { + config = config || {}; + config.connectionClass = AngularConnector; + config.createDefer = function () { + return $q.defer(); + }; + return new Client(config); + }; - // overwrite the request function to return a promise - // and support the callback - Transport.prototype.request = function (params, cb) { - var deferred = $q.defer(); - this._request(params, function (err, body, status) { - if (typeof cb === 'function') { - cb(err, body, status); - } + factory.errors = require('./lib/errors'); + factory.ConnectionPool = require('./lib/connection_pool'); + factory.Transport = require('./lib/transport'); - if (err) { - deferred.reject(err); - } else { - deferred.resolve({ body: body, status: status }); - } - }); - - return deferred.promise; - }; - - return function (config) { - config = config || {}; - config.connectionClass = AngularConnector; - return new Client(config); - }; -}]); + }]); diff --git a/src/elasticsearch.jquery.js b/src/elasticsearch.jquery.js new file mode 100644 index 000000000..4ec2474d6 --- /dev/null +++ b/src/elasticsearch.jquery.js @@ -0,0 +1,4 @@ +process.jquery_build = true; + +/* global jQuery */ +jQuery.es = require('./elasticsearch'); diff --git a/src/elasticsearch.js b/src/elasticsearch.js index 899bdc956..8f2a39e20 100644 --- a/src/elasticsearch.js +++ b/src/elasticsearch.js @@ -1,5 +1,8 @@ var es = { - Client: require('./lib/client') + Client: require('./lib/client'), + errors: require('./lib/errors'), + ConnectionPool: require('./lib/connection_pool'), + Transport: require('./lib/transport') }; -module.exports = es; \ No newline at end of file +module.exports = es; diff --git a/src/lib/api.js b/src/lib/api.js index d641c067e..20a6e804e 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -16,10 +16,6 @@ api._namespaces = ['cluster', 'indices']; * @param {String} params.index - Default index for items which don't provide one */ api.bulk = ca({ - methods: [ - 'POST', - 'PUT' - ], params: { consistency: { type: 'enum', @@ -68,7 +64,8 @@ api.bulk = ca({ fmt: '/_bulk' } ], - bulkBody: true + bulkBody: true, + method: 'POST' }); @@ -79,29 +76,21 @@ api.bulk = ca({ * @param {String or String[] or Boolean} params.scrollId - A comma-separated list of scroll IDs to clear */ api.clearScroll = ca({ - methods: [ - 'DELETE' - ], - params: {}, - urls: [ - { - fmt: '/_search/scroll/<%=scrollId%>', - req: { - scrollId: { - type: 'list' - } + url: { + fmt: '/_search/scroll/<%=scrollId%>', + req: { + scrollId: { + type: 'list' } - } - ] + }, + sortOrder: -1 + }, + method: 'DELETE' }); -api.cluster = function ClusterNS(client) { - if (this instanceof ClusterNS) { - this.client = client; - } else { - return new ClusterNS(client); - } +api.cluster = function ClusterNS(transport) { + this.transport = transport; }; /** @@ -110,15 +99,9 @@ api.cluster = function ClusterNS(client) { * @param {Object} params - An object with parameters used to carry out this action */ api.cluster.prototype.getSettings = ca({ - methods: [ - 'GET' - ], - params: {}, - urls: [ - { - fmt: '/_cluster/settings' - } - ] + url: { + fmt: '/_cluster/settings' + } }); @@ -137,9 +120,6 @@ api.cluster.prototype.getSettings = ca({ * @param {String} params.index - Limit the information returned to a specific index */ api.cluster.prototype.health = ca({ - methods: [ - 'GET' - ], params: { level: { type: 'enum', @@ -210,9 +190,6 @@ api.cluster.prototype.health = ca({ * @param {String or String[] or Boolean} params.nodeId - A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes */ api.cluster.prototype.nodeHotThreads = ca({ - methods: [ - 'GET' - ], params: { interval: { type: 'time' @@ -267,9 +244,6 @@ api.cluster.prototype.nodeHotThreads = ca({ * @param {String or String[] or Boolean} params.nodeId - A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes */ api.cluster.prototype.nodeInfo = ca({ - methods: [ - 'GET' - ], params: { all: { type: 'boolean' @@ -334,9 +308,6 @@ api.cluster.prototype.nodeInfo = ca({ * @param {String or String[] or Boolean} params.nodeId - A comma-separated list of node IDs or names to perform the operation on; use `_local` to perform the operation on the node you're connected to, leave empty to perform the operation on all nodes */ api.cluster.prototype.nodeShutdown = ca({ - methods: [ - 'POST' - ], params: { delay: { type: 'time' @@ -357,7 +328,8 @@ api.cluster.prototype.nodeShutdown = ca({ { fmt: '/_shutdown' } - ] + ], + method: 'POST' }); @@ -382,9 +354,6 @@ api.cluster.prototype.nodeShutdown = ca({ * @param {String or String[] or Boolean} params.nodeId - A comma-separated list of node IDs or names to limit the returned information; use `_local` to return information from the node you're connecting to, leave empty to get information from all nodes */ api.cluster.prototype.nodeStats = ca({ - methods: [ - 'GET' - ], params: { all: { type: 'boolean' @@ -446,15 +415,10 @@ api.cluster.prototype.nodeStats = ca({ * @param {Object} params - An object with parameters used to carry out this action */ api.cluster.prototype.putSettings = ca({ - methods: [ - 'PUT' - ], - params: {}, - urls: [ - { - fmt: '/_cluster/settings' - } - ] + url: { + fmt: '/_cluster/settings' + }, + method: 'PUT' }); @@ -466,9 +430,6 @@ api.cluster.prototype.putSettings = ca({ * @param {Boolean} params.filterMetadata - Don't return cluster state metadata (default: false) */ api.cluster.prototype.reroute = ca({ - methods: [ - 'POST' - ], params: { dryRun: { type: 'boolean', @@ -479,11 +440,10 @@ api.cluster.prototype.reroute = ca({ name: 'filter_metadata' } }, - urls: [ - { - fmt: '/_cluster/reroute' - } - ] + url: { + fmt: '/_cluster/reroute' + }, + method: 'POST' }); @@ -501,9 +461,6 @@ api.cluster.prototype.reroute = ca({ * @param {Date or Number} params.masterTimeout - Specify timeout for connection to master */ api.cluster.prototype.state = ca({ - methods: [ - 'GET' - ], params: { filterBlocks: { type: 'boolean', @@ -537,11 +494,9 @@ api.cluster.prototype.state = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/_cluster/state' - } - ] + url: { + fmt: '/_cluster/state' + } }); @@ -558,10 +513,6 @@ api.cluster.prototype.state = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of types to restrict the results */ api.count = ca({ - methods: [ - 'POST', - 'GET' - ], params: { ignoreIndices: { type: 'enum', @@ -609,114 +560,8 @@ api.count = ca({ { fmt: '/_count' } - ] -}); - - -/** - * Perform a [create](http://elasticsearch.org/guide/reference/api/index_/) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.consistency - Explicit write consistency setting for the operation - * @param {String} params.id - Document ID - * @param {String} params.parent - ID of the parent document - * @param {String} params.percolate - Percolator queries to execute while indexing the document - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} [params.replication=sync] - Specific replication type - * @param {String} params.routing - Specific routing value - * @param {Date or Number} params.timeout - Explicit operation timeout - * @param {Date or Number} params.timestamp - Explicit timestamp for the document - * @param {Duration} params.ttl - Expiration time for the document - * @param {Number} params.version - Explicit version number for concurrency control - * @param {String} params.versionType - Specific version type - * @param {String} params.index - The name of the index - * @param {String} params.type - The type of the document - */ -api.create = ca({ - methods: [ - 'POST', - 'PUT' ], - params: { - consistency: { - type: 'enum', - options: [ - 'one', - 'quorum', - 'all' - ] - }, - id: { - type: 'string' - }, - parent: { - type: 'string' - }, - percolate: { - type: 'string' - }, - refresh: { - type: 'boolean' - }, - replication: { - type: 'enum', - 'default': 'sync', - options: [ - 'sync', - 'async' - ] - }, - routing: { - type: 'string' - }, - timeout: { - type: 'time' - }, - timestamp: { - type: 'time' - }, - ttl: { - type: 'duration' - }, - version: { - type: 'number' - }, - versionType: { - type: 'enum', - options: [ - 'internal', - 'external' - ], - name: 'version_type' - } - }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>/_create', - req: { - index: { - type: 'string' - }, - type: { - type: 'string' - }, - id: { - type: 'string' - } - } - }, - { - fmt: '/<%=index%>/<%=type%>', - req: { - index: { - type: 'string' - }, - type: { - type: 'string' - } - } - } - ] + method: 'POST' }); @@ -737,9 +582,6 @@ api.create = ca({ * @param {String} params.type - The type of the document */ api['delete'] = ca({ - methods: [ - 'DELETE' - ], params: { consistency: { type: 'enum', @@ -781,22 +623,22 @@ api['delete'] = ca({ name: 'version_type' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>', - req: { - index: { - type: 'string' - }, - type: { - type: 'string' - }, - id: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/<%=id%>', + req: { + index: { + type: 'string' + }, + type: { + type: 'string' + }, + id: { + type: 'string' } - } - ] + }, + sortOrder: -3 + }, + method: 'DELETE' }); @@ -818,9 +660,6 @@ api['delete'] = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of types to restrict the operation */ api.deleteByQuery = ca({ - methods: [ - 'DELETE' - ], params: { analyzer: { type: 'string' @@ -895,7 +734,8 @@ api.deleteByQuery = ca({ } } } - ] + ], + method: 'DELETE' }); @@ -913,9 +753,6 @@ api.deleteByQuery = ca({ * @param {String} [params.type=_all] - The type of the document (use `_all` to fetch the first document matching the ID across all types) */ api.exists = ca({ - methods: [ - 'HEAD' - ], params: { parent: { type: 'string' @@ -933,26 +770,26 @@ api.exists = ca({ type: 'string' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>', - opt: { - type: { - type: 'string', - 'default': '_all' - } - }, - req: { - index: { - type: 'string' - }, - id: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/<%=id%>', + opt: { + type: { + type: 'string', + 'default': '_all' } - } - ], - castExists: true + }, + req: { + index: { + type: 'string' + }, + id: { + type: 'string' + } + }, + sortOrder: -2 + }, + castExists: true, + method: 'HEAD' }); @@ -979,10 +816,6 @@ api.exists = ca({ * @param {String} params.type - The type of the document */ api.explain = ca({ - methods: [ - 'GET', - 'POST' - ], params: { analyzeWildcard: { type: 'boolean', @@ -1038,22 +871,22 @@ api.explain = ca({ name: '_source_include' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>/_explain', - req: { - index: { - type: 'string' - }, - type: { - type: 'string' - }, - id: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/<%=id%>/_explain', + req: { + index: { + type: 'string' + }, + type: { + type: 'string' + }, + id: { + type: 'string' } - } - ] + }, + sortOrder: -3 + }, + method: 'POST' }); @@ -1075,9 +908,6 @@ api.explain = ca({ * @param {String} [params.type=_all] - The type of the document (use `_all` to fetch the first document matching the ID across all types) */ api.get = ca({ - methods: [ - 'GET' - ], params: { fields: { type: 'list' @@ -1110,25 +940,24 @@ api.get = ca({ name: '_source_include' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>', - opt: { - type: { - type: 'string', - 'default': '_all' - } - }, - req: { - index: { - type: 'string' - }, - id: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/<%=id%>', + opt: { + type: { + type: 'string', + 'default': '_all' } - } - ] + }, + req: { + index: { + type: 'string' + }, + id: { + type: 'string' + } + }, + sortOrder: -2 + } }); @@ -1148,9 +977,6 @@ api.get = ca({ * @param {String} [params.type=_all] - The type of the document; use `_all` to fetch the first document matching the ID across all types */ api.getSource = ca({ - methods: [ - 'GET' - ], params: { exclude: { type: 'list' @@ -1174,25 +1000,24 @@ api.getSource = ca({ type: 'string' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>/_source', - opt: { - type: { - type: 'string', - 'default': '_all' - } - }, - req: { - index: { - type: 'string' - }, - id: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/<%=id%>/_source', + opt: { + type: { + type: 'string', + 'default': '_all' } - } - ] + }, + req: { + index: { + type: 'string' + }, + id: { + type: 'string' + } + }, + sortOrder: -2 + } }); @@ -1217,10 +1042,6 @@ api.getSource = ca({ * @param {String} params.type - The type of the document */ api.index = ca({ - methods: [ - 'POST', - 'PUT' - ], params: { consistency: { type: 'enum', @@ -1306,16 +1127,13 @@ api.index = ca({ } } } - ] + ], + method: 'POST' }); -api.indices = function IndicesNS(client) { - if (this instanceof IndicesNS) { - this.client = client; - } else { - return new IndicesNS(client); - } +api.indices = function IndicesNS(transport) { + this.transport = transport; }; /** @@ -1332,10 +1150,6 @@ api.indices = function IndicesNS(client) { * @param {String} [params.format=detailed] - Format of the output */ api.indices.prototype.analyze = ca({ - methods: [ - 'GET', - 'POST' - ], params: { analyzer: { type: 'string' @@ -1380,7 +1194,8 @@ api.indices.prototype.analyze = ca({ { fmt: '/_analyze' } - ] + ], + method: 'POST' }); @@ -1401,10 +1216,6 @@ api.indices.prototype.analyze = ca({ * @param {Boolean} params.recycler - Clear the recycler cache */ api.indices.prototype.clearCache = ca({ - methods: [ - 'POST', - 'GET' - ], params: { fieldData: { type: 'boolean', @@ -1462,7 +1273,8 @@ api.indices.prototype.clearCache = ca({ { fmt: '/_cache/clear' } - ] + ], + method: 'POST' }); @@ -1475,9 +1287,6 @@ api.indices.prototype.clearCache = ca({ * @param {String} params.index - The name of the index */ api.indices.prototype.close = ca({ - methods: [ - 'POST' - ], params: { timeout: { type: 'time' @@ -1487,16 +1296,16 @@ api.indices.prototype.close = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/<%=index%>/_close', - req: { - index: { - type: 'string' - } + url: { + fmt: '/<%=index%>/_close', + req: { + index: { + type: 'string' } - } - ] + }, + sortOrder: -1 + }, + method: 'POST' }); @@ -1509,10 +1318,6 @@ api.indices.prototype.close = ca({ * @param {String} params.index - The name of the index */ api.indices.prototype.create = ca({ - methods: [ - 'PUT', - 'POST' - ], params: { timeout: { type: 'time' @@ -1522,16 +1327,16 @@ api.indices.prototype.create = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/<%=index%>', - req: { - index: { - type: 'string' - } + url: { + fmt: '/<%=index%>', + req: { + index: { + type: 'string' } - } - ] + }, + sortOrder: -1 + }, + method: 'POST' }); @@ -1544,9 +1349,6 @@ api.indices.prototype.create = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of indices to delete; use `_all` or empty string to delete all indices */ api.indices.prototype['delete'] = ca({ - methods: [ - 'DELETE' - ], params: { timeout: { type: 'time' @@ -1568,7 +1370,8 @@ api.indices.prototype['delete'] = ca({ { fmt: '/' } - ] + ], + method: 'DELETE' }); @@ -1582,9 +1385,6 @@ api.indices.prototype['delete'] = ca({ * @param {String} params.name - The name of the alias to be deleted */ api.indices.prototype.deleteAlias = ca({ - methods: [ - 'DELETE' - ], params: { timeout: { type: 'time' @@ -1594,19 +1394,19 @@ api.indices.prototype.deleteAlias = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/<%=index%>/_alias/<%=name%>', - req: { - index: { - type: 'string' - }, - name: { - type: 'string' - } + url: { + fmt: '/<%=index%>/_alias/<%=name%>', + req: { + index: { + type: 'string' + }, + name: { + type: 'string' } - } - ] + }, + sortOrder: -2 + }, + method: 'DELETE' }); @@ -1619,28 +1419,25 @@ api.indices.prototype.deleteAlias = ca({ * @param {String} params.type - The name of the document type to delete */ api.indices.prototype.deleteMapping = ca({ - methods: [ - 'DELETE' - ], params: { masterTimeout: { type: 'time', name: 'master_timeout' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>', - req: { - index: { - type: 'list' - }, - type: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>', + req: { + index: { + type: 'list' + }, + type: { + type: 'string' } - } - ] + }, + sortOrder: -2 + }, + method: 'DELETE' }); @@ -1653,9 +1450,6 @@ api.indices.prototype.deleteMapping = ca({ * @param {String} params.name - The name of the template */ api.indices.prototype.deleteTemplate = ca({ - methods: [ - 'DELETE' - ], params: { timeout: { type: 'time' @@ -1665,16 +1459,16 @@ api.indices.prototype.deleteTemplate = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/_template/<%=name%>', - req: { - name: { - type: 'string' - } + url: { + fmt: '/_template/<%=name%>', + req: { + name: { + type: 'string' } - } - ] + }, + sortOrder: -1 + }, + method: 'DELETE' }); @@ -1688,9 +1482,6 @@ api.indices.prototype.deleteTemplate = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types to register warmer for; use `_all` or empty string to perform the operation on all types */ api.indices.prototype.deleteWarmer = ca({ - methods: [ - 'DELETE' - ], params: { masterTimeout: { type: 'time', @@ -1731,7 +1522,8 @@ api.indices.prototype.deleteWarmer = ca({ } } } - ] + ], + method: 'DELETE' }); @@ -1742,21 +1534,17 @@ api.indices.prototype.deleteWarmer = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of indices to check */ api.indices.prototype.exists = ca({ - methods: [ - 'HEAD' - ], - params: {}, - urls: [ - { - fmt: '/<%=index%>', - req: { - index: { - type: 'list' - } + url: { + fmt: '/<%=index%>', + req: { + index: { + type: 'list' } - } - ], - castExists: true + }, + sortOrder: -1 + }, + castExists: true, + method: 'HEAD' }); @@ -1769,9 +1557,6 @@ api.indices.prototype.exists = ca({ * @param {String or String[] or Boolean} params.name - A comma-separated list of alias names to return */ api.indices.prototype.existsAlias = ca({ - methods: [ - 'HEAD' - ], params: { ignoreIndices: { type: 'enum', @@ -1804,7 +1589,8 @@ api.indices.prototype.existsAlias = ca({ } } ], - castExists: true + castExists: true, + method: 'HEAD' }); @@ -1817,9 +1603,6 @@ api.indices.prototype.existsAlias = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types to check */ api.indices.prototype.existsType = ca({ - methods: [ - 'HEAD' - ], params: { ignoreIndices: { type: 'enum', @@ -1831,20 +1614,20 @@ api.indices.prototype.existsType = ca({ name: 'ignore_indices' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>', - req: { - index: { - type: 'list' - }, - type: { - type: 'list' - } + url: { + fmt: '/<%=index%>/<%=type%>', + req: { + index: { + type: 'list' + }, + type: { + type: 'list' } - } - ], - castExists: true + }, + sortOrder: -2 + }, + castExists: true, + method: 'HEAD' }); @@ -1859,10 +1642,6 @@ api.indices.prototype.existsType = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string for all indices */ api.indices.prototype.flush = ca({ - methods: [ - 'POST', - 'GET' - ], params: { force: { type: 'boolean' @@ -1895,7 +1674,8 @@ api.indices.prototype.flush = ca({ { fmt: '/_flush' } - ] + ], + method: 'POST' }); @@ -1908,9 +1688,6 @@ api.indices.prototype.flush = ca({ * @param {String or String[] or Boolean} params.name - A comma-separated list of alias names to return */ api.indices.prototype.getAlias = ca({ - methods: [ - 'GET' - ], params: { ignoreIndices: { type: 'enum', @@ -1954,9 +1731,6 @@ api.indices.prototype.getAlias = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names to filter aliases */ api.indices.prototype.getAliases = ca({ - methods: [ - 'GET' - ], params: { timeout: { type: 'time' @@ -1988,9 +1762,6 @@ api.indices.prototype.getAliases = ca({ * @param {String or String[] or Boolean} params.field - A comma-separated list of fields */ api.indices.prototype.getFieldMapping = ca({ - methods: [ - 'GET' - ], params: { includeDefaults: { type: 'boolean', @@ -2043,10 +1814,6 @@ api.indices.prototype.getFieldMapping = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types */ api.indices.prototype.getMapping = ca({ - methods: [ - 'GET' - ], - params: {}, urls: [ { fmt: '/<%=index%>/<%=type%>/_mapping', @@ -2081,10 +1848,6 @@ api.indices.prototype.getMapping = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices */ api.indices.prototype.getSettings = ca({ - methods: [ - 'GET' - ], - params: {}, urls: [ { fmt: '/<%=index%>/_settings', @@ -2108,10 +1871,6 @@ api.indices.prototype.getSettings = ca({ * @param {String} params.name - The name of the template */ api.indices.prototype.getTemplate = ca({ - methods: [ - 'GET' - ], - params: {}, urls: [ { fmt: '/_template/<%=name%>', @@ -2137,10 +1896,6 @@ api.indices.prototype.getTemplate = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types to restrict the operation; leave empty to perform the operation on all types */ api.indices.prototype.getWarmer = ca({ - methods: [ - 'GET' - ], - params: {}, urls: [ { fmt: '/<%=index%>/<%=type%>/_warmer/<%=name%>', @@ -2188,9 +1943,6 @@ api.indices.prototype.getWarmer = ca({ * @param {String} params.index - The name of the index */ api.indices.prototype.open = ca({ - methods: [ - 'POST' - ], params: { timeout: { type: 'time' @@ -2200,16 +1952,16 @@ api.indices.prototype.open = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/<%=index%>/_open', - req: { - index: { - type: 'string' - } + url: { + fmt: '/<%=index%>/_open', + req: { + index: { + type: 'string' } - } - ] + }, + sortOrder: -1 + }, + method: 'POST' }); @@ -2227,10 +1979,6 @@ api.indices.prototype.open = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices */ api.indices.prototype.optimize = ca({ - methods: [ - 'POST', - 'GET' - ], params: { flush: { type: 'boolean' @@ -2275,7 +2023,8 @@ api.indices.prototype.optimize = ca({ { fmt: '/_optimize' } - ] + ], + method: 'POST' }); @@ -2289,9 +2038,6 @@ api.indices.prototype.optimize = ca({ * @param {String} params.name - The name of the alias to be created or updated */ api.indices.prototype.putAlias = ca({ - methods: [ - 'PUT' - ], params: { timeout: { type: 'time' @@ -2332,7 +2078,8 @@ api.indices.prototype.putAlias = ca({ { fmt: '/_alias' } - ] + ], + method: 'PUT' }); @@ -2347,10 +2094,6 @@ api.indices.prototype.putAlias = ca({ * @param {String} params.type - The name of the document type */ api.indices.prototype.putMapping = ca({ - methods: [ - 'PUT', - 'POST' - ], params: { ignoreConflicts: { type: 'boolean', @@ -2364,19 +2107,19 @@ api.indices.prototype.putMapping = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/_mapping', - req: { - index: { - type: 'list' - }, - type: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/_mapping', + req: { + index: { + type: 'list' + }, + type: { + type: 'string' } - } - ] + }, + sortOrder: -2 + }, + method: 'POST' }); @@ -2388,9 +2131,6 @@ api.indices.prototype.putMapping = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices */ api.indices.prototype.putSettings = ca({ - methods: [ - 'PUT' - ], params: { masterTimeout: { type: 'time', @@ -2409,7 +2149,8 @@ api.indices.prototype.putSettings = ca({ { fmt: '/_settings' } - ] + ], + method: 'PUT' }); @@ -2423,10 +2164,6 @@ api.indices.prototype.putSettings = ca({ * @param {String} params.name - The name of the template */ api.indices.prototype.putTemplate = ca({ - methods: [ - 'PUT', - 'POST' - ], params: { order: { type: 'number' @@ -2439,16 +2176,16 @@ api.indices.prototype.putTemplate = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/_template/<%=name%>', - req: { - name: { - type: 'string' - } + url: { + fmt: '/_template/<%=name%>', + req: { + name: { + type: 'string' } - } - ] + }, + sortOrder: -1 + }, + method: 'POST' }); @@ -2462,9 +2199,6 @@ api.indices.prototype.putTemplate = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types to register the warmer for; leave empty to perform the operation on all types */ api.indices.prototype.putWarmer = ca({ - methods: [ - 'PUT' - ], params: { masterTimeout: { type: 'time', @@ -2497,7 +2231,8 @@ api.indices.prototype.putWarmer = ca({ } } } - ] + ], + method: 'PUT' }); @@ -2510,10 +2245,6 @@ api.indices.prototype.putWarmer = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices */ api.indices.prototype.refresh = ca({ - methods: [ - 'POST', - 'GET' - ], params: { ignoreIndices: { type: 'enum', @@ -2540,7 +2271,8 @@ api.indices.prototype.refresh = ca({ { fmt: '/_refresh' } - ] + ], + method: 'POST' }); @@ -2553,9 +2285,6 @@ api.indices.prototype.refresh = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices */ api.indices.prototype.segments = ca({ - methods: [ - 'GET' - ], params: { ignoreIndices: { type: 'enum', @@ -2594,9 +2323,6 @@ api.indices.prototype.segments = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string for all indices */ api.indices.prototype.snapshotIndex = ca({ - methods: [ - 'POST' - ], params: { ignoreIndices: { type: 'enum', @@ -2620,7 +2346,8 @@ api.indices.prototype.snapshotIndex = ca({ { fmt: '/_gateway/snapshot' } - ] + ], + method: 'POST' }); @@ -2654,9 +2381,6 @@ api.indices.prototype.snapshotIndex = ca({ * @param {String or String[] or Boolean} params.searchGroups - A comma-separated list of search groups to include in the `search` statistics */ api.indices.prototype.stats = ca({ - methods: [ - 'GET' - ], params: { all: { type: 'boolean' @@ -2756,9 +2480,6 @@ api.indices.prototype.stats = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names; use `_all` or empty string to perform the operation on all indices */ api.indices.prototype.status = ca({ - methods: [ - 'GET' - ], params: { ignoreIndices: { type: 'enum', @@ -2804,9 +2525,6 @@ api.indices.prototype.status = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names to filter aliases */ api.indices.prototype.updateAliases = ca({ - methods: [ - 'POST' - ], params: { timeout: { type: 'time' @@ -2816,11 +2534,10 @@ api.indices.prototype.updateAliases = ca({ name: 'master_timeout' } }, - urls: [ - { - fmt: '/_aliases' - } - ] + url: { + fmt: '/_aliases' + }, + method: 'POST' }); @@ -2837,10 +2554,6 @@ api.indices.prototype.updateAliases = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types to restrict the operation; leave empty to perform the operation on all types */ api.indices.prototype.validateQuery = ca({ - methods: [ - 'GET', - 'POST' - ], params: { explain: { type: 'boolean' @@ -2887,7 +2600,8 @@ api.indices.prototype.validateQuery = ca({ { fmt: '/_validate/query' } - ] + ], + method: 'POST' }); @@ -2897,16 +2611,9 @@ api.indices.prototype.validateQuery = ca({ * @param {Object} params - An object with parameters used to carry out this action */ api.info = ca({ - methods: [ - 'GET', - 'HEAD' - ], - params: {}, - urls: [ - { - fmt: '/' - } - ] + url: { + fmt: '/' + } }); @@ -2925,10 +2632,6 @@ api.info = ca({ * @param {String} params.type - The type of the document */ api.mget = ca({ - methods: [ - 'GET', - 'POST' - ], params: { fields: { type: 'list' @@ -2978,7 +2681,8 @@ api.mget = ca({ { fmt: '/_mget' } - ] + ], + method: 'POST' }); @@ -3010,10 +2714,6 @@ api.mget = ca({ * @param {String} params.type - The type of the document (use `_all` to fetch the first document matching the ID across all types) */ api.mlt = ca({ - methods: [ - 'GET', - 'POST' - ], params: { boostTerms: { type: 'number', @@ -3091,22 +2791,22 @@ api.mlt = ca({ name: 'stop_words' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>/_mlt', - req: { - index: { - type: 'string' - }, - type: { - type: 'string' - }, - id: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/<%=id%>/_mlt', + req: { + index: { + type: 'string' + }, + type: { + type: 'string' + }, + id: { + type: 'string' } - } - ] + }, + sortOrder: -3 + }, + method: 'POST' }); @@ -3119,10 +2819,6 @@ api.mlt = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types to use as default */ api.msearch = ca({ - methods: [ - 'GET', - 'POST' - ], params: { searchType: { type: 'enum', @@ -3161,7 +2857,8 @@ api.msearch = ca({ fmt: '/_msearch' } ], - bulkBody: true + bulkBody: true, + method: 'POST' }); @@ -3174,29 +2871,25 @@ api.msearch = ca({ * @param {String} params.type - The document type */ api.percolate = ca({ - methods: [ - 'GET', - 'POST' - ], params: { preferLocal: { type: 'boolean', name: 'prefer_local' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/_percolate', - req: { - index: { - type: 'string' - }, - type: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/_percolate', + req: { + index: { + type: 'string' + }, + type: { + type: 'string' } - } - ] + }, + sortOrder: -2 + }, + method: 'POST' }); @@ -3208,10 +2901,6 @@ api.percolate = ca({ * @param {String} params.scrollId - The scroll ID */ api.scroll = ca({ - methods: [ - 'GET', - 'POST' - ], params: { scroll: { type: 'duration' @@ -3233,7 +2922,8 @@ api.scroll = ca({ { fmt: '/_search/scroll' } - ] + ], + method: 'POST' }); @@ -3273,10 +2963,6 @@ api.scroll = ca({ * @param {String or String[] or Boolean} params.type - A comma-separated list of document types to search; leave empty to perform the operation on all types */ api.search = ca({ - methods: [ - 'GET', - 'POST' - ], params: { analyzer: { type: 'string' @@ -3424,7 +3110,8 @@ api.search = ca({ } } } - ] + ], + method: 'POST' }); @@ -3439,10 +3126,6 @@ api.search = ca({ * @param {String or String[] or Boolean} params.index - A comma-separated list of index names to restrict the operation; use `_all` or empty string to perform the operation on all indices */ api.suggest = ca({ - methods: [ - 'POST', - 'GET' - ], params: { ignoreIndices: { type: 'enum', @@ -3475,7 +3158,8 @@ api.suggest = ca({ { fmt: '/_suggest' } - ] + ], + method: 'POST' }); @@ -3503,9 +3187,6 @@ api.suggest = ca({ * @param {String} params.type - The type of the document */ api.update = ca({ - methods: [ - 'POST' - ], params: { consistency: { type: 'enum', @@ -3567,21 +3248,46 @@ api.update = ca({ name: 'version_type' } }, - urls: [ - { - fmt: '/<%=index%>/<%=type%>/<%=id%>/_update', - req: { - index: { - type: 'string' - }, - type: { - type: 'string' - }, - id: { - type: 'string' - } + url: { + fmt: '/<%=index%>/<%=type%>/<%=id%>/_update', + req: { + index: { + type: 'string' + }, + type: { + type: 'string' + }, + id: { + type: 'string' } - } - ] + }, + sortOrder: -3 + }, + method: 'POST' +}); + +/** + * Perform a [create](http://elasticsearch.org/guide/reference/api/index_/) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String} params.consistency - Explicit write consistency setting for the operation + * @param {String} params.id - Document ID + * @param {String} params.parent - ID of the parent document + * @param {String} params.percolate - Percolator queries to execute while indexing the document + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} [params.replication=sync] - Specific replication type + * @param {String} params.routing - Specific routing value + * @param {Date or Number} params.timeout - Explicit operation timeout + * @param {Date or Number} params.timestamp - Explicit timestamp for the document + * @param {Duration} params.ttl - Expiration time for the document + * @param {Number} params.version - Explicit version number for concurrency control + * @param {String} params.versionType - Specific version type + * @param {String} params.index - The name of the index + * @param {String} params.type - The type of the document + */ +api.create = ca.proxy(api.index, { + transform: function (params) { + params.op_type = 'create'; + } }); diff --git a/src/lib/client.js b/src/lib/client.js index 71655f7a5..54b67192a 100755 --- a/src/lib/client.js +++ b/src/lib/client.js @@ -22,33 +22,20 @@ * * @class Client * @constructor - * @param {Object} [config={}] - Configuration for the transport - * @param {Object} [config.transport] - Transport settings passed to {{#crossLink "Transport"}}Transport Constructor{{/crossLink}} - * @param {String|Array} [config.log] - Log output settings {{#crossLink "Log"}}Log Constructor{{/crossLink}} - * @param {Object} [config.trace=false] - Create a log output to stdio that only tracks trace logs */ module.exports = Client; -var _ = require('./utils'); -var ClientConfig = require('./client_config'); var api = require('./api.js'); +var ca = require('./client_action'); +var Transport = require('./transport'); function Client(config) { - this.client = this; + this.transport = new Transport(config); - // setup the config.. this config will be passed EVERYWHERE so for good measure it is locked down - Object.defineProperty(this, 'config', { - configurable: false, - enumerable: false, - writable: false, - value: !config || _.isPlainObject(config) ? new ClientConfig(config) : config, - }); - this.config.client = this; - - // instansiate the api's namespaces + // instantiate the api's namespaces for (var i = 0; i < this._namespaces.length; i++) { - this[this._namespaces[i]] = new this[this._namespaces[i]](this); + this[this._namespaces[i]] = new this[this._namespaces[i]](this.transport); } } @@ -60,19 +47,14 @@ Client.prototype = api; * @param {Object} params - Currently just a placeholder, no params used at this time * @param {Function} cb - callback */ -Client.prototype.ping = function (params, cb) { - if (typeof params === 'function') { - cb = params; - params = {}; - } - - this.config.transport.request({ - method: 'HEAD', - path: '/', - timeout: 100, - }, cb); -}; +Client.prototype.ping = ca({ + method: 'HEAD', + url: { + fmt: '/' + }, + timeout: 100 +}); Client.prototype.close = function () { - this.config.close(); + this.transport.close(); }; diff --git a/src/lib/client_action.js b/src/lib/client_action.js index 395ff6059..1db455ef5 100644 --- a/src/lib/client_action.js +++ b/src/lib/client_action.js @@ -2,21 +2,50 @@ * Constructs a function that can be called to make a request to ES * @type {[type]} */ -module.exports = function ClientAction(spec, client) { - return function (params, cb) { - return exec((client || this.client).config.transport, spec, params, cb); - }; -}; +module.exports = ClientAction; var _ = require('./utils'); +var when = require('when'); + +function ClientAction(spec) { + if (!_.isPlainObject(spec.params)) { + spec.params = {}; + } + + if (!spec.method) { + spec.method = 'GET'; + } + + return function (params, cb) { + if (typeof params === 'function') { + cb = params; + params = {}; + } else { + params = params || {}; + cb = typeof cb === 'function' ? cb : null; + } + + try { + return exec(this.transport, spec, params, cb); + } catch (e) { + if (typeof cb === 'function') { + _.nextTick(cb, e); + } else { + return when.reject(e); + } + } + }; +} var castType = { enum: function (param, val, name) { - if (_.contains(param.options, val)) { - return val; - } else { - throw new TypeError('Invalid ' + name + ': expected one of ' + param.options.join(',')); + /* jshint eqeqeq: false */ + for (var i = 0; i < param.options.length; i++) { + if (param.options[i] == val) { + return param.options[i]; + } } + throw new TypeError('Invalid ' + name + ': expected one of ' + param.options.join(',')); }, duration: function (param, val, name) { if (_.isNumeric(val) || _.isInterval(val)) { @@ -30,17 +59,16 @@ var castType = { }, list: function (param, val, name) { switch (typeof val) { + case 'number': case 'string': return val; case 'object': if (_.isArray(val)) { return val.join(','); - } else { - throw new TypeError('Invalid ' + name + ': expected be a comma seperated list, array, or boolean.'); } - break; + /* falls through */ default: - return !!val; + throw new TypeError('Invalid ' + name + ': expected be a comma seperated list, array, number or string.'); } }, boolean: function (param, val) { @@ -55,18 +83,25 @@ var castType = { } }, string: function (param, val, name) { - if (typeof val !== 'object' && val) { + switch (typeof val) { + case 'number': + case 'string': return '' + val; - } else { + default: throw new TypeError('Invalid ' + name + ': expected a string.'); } }, time: function (param, val, name) { - if (typeof val === 'string' || _.isNumeric(val)) { + if (typeof val === 'string') { return val; - } else if (val instanceof Date) { - return val.getTime(); - } else { + } + else if (_.isNumeric(val)) { + return '' + val; + } + else if (val instanceof Date) { + return '' + val.getTime(); + } + else { throw new TypeError('Invalid ' + name + ': expected some sort of time.'); } } @@ -88,8 +123,12 @@ function resolveUrl(url, params) { // missing a required param return false; } else { - // copy param vals into vars - vars[key] = params[key]; + // cast of copy required param + if (castType[url.req[key].type]) { + vars[key] = castType[url.req[key].type](url.req[key], params[key], key); + } else { + vars[key] = params[key]; + } } } } @@ -127,60 +166,27 @@ function resolveUrl(url, params) { }, {})); } -function exec(transport, spec, params, cb) { - if (typeof params === 'function') { - cb = params; - params = {}; - } else { - params = params || {}; - cb = typeof cb === 'function' ? cb : _.noop; - } +// export so that we can test this +ClientAction.resolveUrl = resolveUrl; - var request = {}; +function exec(transport, spec, params, cb) { + var request = { + method: spec.method, + timeout: spec.timeout || 10000 + }; var query = {}; var i; + // verify that we have the body if needed if (spec.needsBody && !params.body) { - return _.nextTick(cb, new TypeError('A request body is required.')); + throw new TypeError('A request body is required.'); } - if (params.timeout === void 0) { - request.timeout = 10000; - } else { - request.timeout = params.timeout; - } - - // copy over some properties from the spec - params.body && (request.body = params.body); - params.ignore && (request.ignore = _.isArray(params.ignore) ? params.ignore : [params.ignore]); + // control params spec.bulkBody && (request.bulkBody = true); spec.castExists && (request.castExists = true); - if (spec.methods.length === 1) { - request.method = spec.methods[0]; - } else { - // if set, uppercase the user's choice, other wise returns "" - request.method = _.toUpperString(params.method); - - if (request.method) { - // use the one specified as long as it's a valid option - if (!_.contains(spec.methods, request.method)) { - return _.nextTick(cb, new TypeError('Invalid method: should be one of ' + spec.methods.join(', '))); - } - } else { - // pick a method - if (request.body) { - // first method that isn't "GET" - request.method = spec.methodWithBody || ( - spec.methodWithBody = _.find(spec.methods, function (m) { return m !== 'GET'; }) - ); - } else { - // just use the first option - request.method = spec.methods[0]; - } - } - } - + // pick the url if (spec.url) { // only one url option request.path = resolveUrl(spec.url, params); @@ -194,12 +200,9 @@ function exec(transport, spec, params, cb) { if (!request.path) { // there must have been some mimimun requirements that were not met - return _.nextTick( - cb, - new TypeError( - 'Unable to build a path with those params. Supply at least ' + - _.keys(spec.urls[spec.urls.length - 1].req).join(', ') - ) + throw new TypeError( + 'Unable to build a path with those params. Supply at least ' + + _.keys(spec.urls[spec.urls.length - 1].req).join(', ') ); } @@ -207,24 +210,56 @@ function exec(transport, spec, params, cb) { if (!spec.paramKeys) { // build a key list on demand spec.paramKeys = _.keys(spec.params); - } - var key, param, name; - for (i = 0; i < spec.paramKeys.length; i++) { - key = spec.paramKeys[i]; - param = spec.params[key]; - // param keys don't always match the param name, in those cases it's stored in the param def as "name" - name = param.name || key; - try { - if (params[key] != null) { - query[name] = castType[param.type] ? castType[param.type](param, params[key], key) : params[key]; - if (param['default'] && query[name] === param['default']) { - delete query[name]; - } - } else if (param.required) { - throw new TypeError('Missing required parameter ' + key); + spec.requireParamKeys = _.transform(spec.params, function (req, param, key) { + if (param.required) { + req.push(key); } - } catch (e) { - return _.nextTick(cb, e); + }, []); + } + + var key, paramSpec; + + for (key in params) { + if (params.hasOwnProperty(key) && params[key] != null) { + switch (key) { + case 'body': + request.body = params.body; + break; + case 'ignore': + request.ignore = _.isArray(params.ignore) ? params.ignore : [params.ignore]; + break; + case 'timeout': + request.timeout = params.timeout; + break; + case 'method': + request.method = _.toUpperString(params.method); + break; + default: + paramSpec = spec.params[key]; + if (paramSpec) { + // param keys don't always match the param name, in those cases it's stored in the param def as "name" + paramSpec.name = paramSpec.name || key; + if (params[key] != null) { + if (castType[paramSpec.type]) { + query[paramSpec.name] = castType[paramSpec.type](paramSpec, params[key], key); + } else { + query[paramSpec.name] = params[key]; + } + + if (paramSpec['default'] && query[paramSpec.name] === paramSpec['default']) { + delete query[paramSpec.name]; + } + } + } else { + query[key] = params[key]; + } + } + } + } + + for (i = 0; i < spec.requireParamKeys.length; i ++) { + if (!query.hasOwnProperty(spec.requireParamKeys[i])) { + throw new TypeError('Missing required parameter ' + spec.requireParamKeys[i]); } } @@ -232,3 +267,23 @@ function exec(transport, spec, params, cb) { return transport.request(request, cb); } + + + +ClientAction.proxy = function (fn, spec) { + return function (params, cb) { + if (typeof params === 'function') { + cb = params; + params = {}; + } else { + params = params || {}; + cb = typeof cb === 'function' ? cb : null; + } + + if (spec.transform) { + spec.transform(params); + } + + return fn.call(this, params, cb); + }; +}; diff --git a/src/lib/client_config.js b/src/lib/client_config.js deleted file mode 100644 index 0e843661b..000000000 --- a/src/lib/client_config.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Manages the configuration of the client. - * - * @class ClientConfig - * @type {Function} - */ -module.exports = ClientConfig; - -var _ = require('./utils'); -var Host = require('./host'); -var selectors = require('./selectors'); - -var connectors = {}; -if (process.browser) { - connectors.Xhr = require('./connectors/xhr'); - connectors.Angular = require('./connectors/angular'); - connectors.jQuery = require('./connectors/jquery'); -} else { - connectors.Http = require('./connectors/http'); -} - -// remove connectors that have been excluded in the build -_.each(connectors, function (conn, name) { - if (typeof conn !== 'function') { - delete connectors[name]; - } -}); - -var serializers = { - Json: require('./serializers/json') -}; - -var extractHostPartsRE = /\[([^:]+):(\d+)\]/; - -var defaultClasses = { - log: require('./log'), - serializer: serializers.Json, - connectionPool: require('./connection_pool'), - transport: require('./transport'), -}; - -var defaultConfig = { - loggers: [ - { - level: 'warning' - } - ], - hosts: [ - { - host: 'localhost', - port: 9200, - protocol: 'http' - } - ], - connectionClass: process.browser ? connectors.Xhr : connectors.Http, - selector: selectors.roundRobin, - sniffOnStart: false, - sniffAfterRequests: null, - sniffOnConnectionFail: false, - maxRetries: 3, - timeout: 10000, - deadTimeout: 60000, - maxSockets: 10, - // transforms the response from /_cluster/nodes - nodesToHostCallback: function (nodes) { - var hosts = []; - _.each(nodes, function (node, id) { - var hostnameMatches = extractHostPartsRE.exec(node.http_address); - hosts.push({ - host: hostnameMatches[1], - port: hostnameMatches[2], - _meta: { - id: id, - name: node.name, - hostname: node.hostname, - version: node.version - } - }); - }); - return hosts; - } -}; - -// remove connector classes that were not included in the build -connectors = _.transform(connectors, function (note, connector, name) { - if (connector) { - note[name] = connector; - } -}, {}); - -function ClientConfig(config) { - _.extend(this, defaultConfig, config); - - if (this.log) { - // treat log as an alias for loggers in the config. - this.loggers = this.log; - delete this.log; - } - - // validate connectionClass - if (typeof this.connectionClass === 'string') { - this.connectionClass = connectors[_.studlyCase(this.connectionClass)]; - } - if (typeof this.connectionClass !== 'function') { - throw new TypeError('Invalid connectionClass "' + this.connectionClass + '". ' + - 'Expected a constructor or one of ' + _.keys(connectors).join(', ')); - } - - // validate selector - if (typeof this.selector === 'string') { - this.selector = selectors[_.camelCase(this.selector)]; - } - if (typeof this.selector !== 'function') { - throw new TypeError('Invalid Selector "' + this.selector + '". ' + - 'Expected a function or one of ' + _.keys(selectors).join(', ')); - } - - _.each(defaultClasses, function (DefaultClass, prop) { - this[prop] = typeof this[prop] === 'function' ? new this[prop](this) : new DefaultClass(this); - }, this); - - // populate the connection pool - this.connectionPool.setNodes(this.prepareHosts(this.hosts)); - - // nodes are completely managed by the connection pool, remove traces of the config - // value to prevent confusion - delete this.hosts; -} - -ClientConfig.prototype.prepareHosts = function (hosts) { - if (!_.isArray(hosts)) { - hosts = [hosts]; - } - - return _.map(hosts, function (host) { - return new Host(host); - }); -}; - -/** - * Shutdown the connectionPool, log outputs, and clear timers - */ -ClientConfig.prototype.close = function () { - this.log.close(); - this.connectionPool.close(); -}; diff --git a/src/lib/connection.js b/src/lib/connection.js index dbe1ad134..284d7151e 100644 --- a/src/lib/connection.js +++ b/src/lib/connection.js @@ -9,13 +9,19 @@ var EventEmitter = require('events').EventEmitter; * @constructor */ function ConnectionAbstract(host, config) { + config = _.defaults(config || {}, { + deadTimeout: 30000 + }); EventEmitter.call(this); - this.config = config; - this.host = host; + this.deadTimeout = config.deadTimeout; this.requestCount = 0; - if (!this.host) { - throw new Error('Missing host config'); + if (!host) { + throw new TypeError('Missing host'); + } else if (host.makeUrl) { + this.host = host; + } else { + throw new TypeError('Invalid host'); } _.makeBoundMethods(this); @@ -36,12 +42,11 @@ ConnectionAbstract.prototype.request = function () { throw new Error('Connection#request must be overwritten by the Connector'); }; -ConnectionAbstract.prototype.ping = function (params, cb) { - if (typeof params === 'function') { - cb = params; - } else if (typeof cb !== 'function') { +ConnectionAbstract.prototype.ping = function (cb) { + if (typeof cb !== 'function') { throw new TypeError('Callback must be a function'); } + return this.request({ path: '/', method: 'HEAD', @@ -54,16 +59,20 @@ ConnectionAbstract.prototype.setStatus = function (status) { this.status = status; - if (status === 'dead' || status === 'closed') { - if (this.__deadTimeout) { - clearTimeout(this.__deadTimeout); - } - if (status === 'dead') { - this.__deadTimeout = setTimeout(this.bound.resuscitate, this.config.deadTimeout); - } + if (this._deadTimeoutId) { + clearTimeout(this._deadTimeoutId); + this._deadTimeoutId = null; } - this.emit('status changed', status, origStatus, this); + if (status === 'dead') { + this._deadTimeoutId = setTimeout(this.bound.resuscitate, this.deadTimeout); + } + + this.emit('status set', status, origStatus, this); + + if (status === 'closed') { + this.removeAllListeners(); + } }; ConnectionAbstract.prototype.resuscitate = _.scheduled(function () { @@ -74,7 +83,7 @@ ConnectionAbstract.prototype.resuscitate = _.scheduled(function () { if (!err) { self.setStatus('alive'); } else { - self.emit('dead'); + self.setStatus('dead'); } }); } diff --git a/src/lib/connection_pool.js b/src/lib/connection_pool.js index bc8c41000..a5add3553 100644 --- a/src/lib/connection_pool.js +++ b/src/lib/connection_pool.js @@ -3,33 +3,66 @@ * before providing them to the application * * @class ConnectionPool - * @param {Client} client - The client this pool belongs to + * @constructor + * @param {Object} config - The config object passed to the transport. */ module.exports = ConnectionPool; var _ = require('./utils'); -var Host = require('./host'); +var Log = require('./log'); function ConnectionPool(config) { _.makeBoundMethods(this); - this.config = config; + + this.log = config.log; + if (!this.log) { + this.log = new Log(); + } + + // get the selector config var + this.selector = _.funcEnum(config, 'selector', ConnectionPool.selectors, ConnectionPool.defaultSelectors); + // get the connection class + this.Connection = _.funcEnum(config, 'connectionClass', ConnectionPool.connectionClasses, + ConnectionPool.defaultConnectionClass); + + // a map of connections to their "id" property, used when sniffing this.index = {}; + this.connections = { alive: [], dead: [] }; } +// selector options +ConnectionPool.selectors = require('./selectors'); +ConnectionPool.defaultSelectors = 'round_robin'; + +// get the connection options +ConnectionPool.connectionClasses = require('./connectors'); +ConnectionPool.defaultConnectionClass = ConnectionPool.connectionClasses._default; +delete ConnectionPool.connectionClasses._default; + +/** + * Selects a connection from the list using the this.selector + * Features: + * - detects if the selector is async or not + * - sync selectors should still return asynchronously + * - catches errors in sync selectors + * - automatically selects the first dead connection when there no living connections + * + * @param {Function} cb [description] + * @return {[type]} [description] + */ ConnectionPool.prototype.select = function (cb) { if (this.connections.alive.length) { - if (this.config.selector.length > 1) { - this.config.selector(this.connections.alive, cb); + if (this.selector.length > 1) { + this.selector(this.connections.alive, cb); } else { try { - _.nextTick(cb, null, this.config.selector(this.connections.alive)); + _.nextTick(cb, null, this.selector(this.connections.alive)); } catch (e) { - this.config.log.error(e); cb(e); } } @@ -38,7 +71,7 @@ ConnectionPool.prototype.select = function (cb) { } }; -ConnectionPool.prototype.onStatusChanged = _.handler(function (status, oldStatus, connection) { +ConnectionPool.prototype.onStatusSet = _.handler(function (status, oldStatus, connection) { var from, to, index; if (oldStatus === status) { @@ -48,8 +81,6 @@ ConnectionPool.prototype.onStatusChanged = _.handler(function (status, oldStatus } else { return true; } - } else { - this.config.log.info('connection id:', connection.id, 'is', status); } switch (status) { @@ -86,46 +117,56 @@ ConnectionPool.prototype.onStatusChanged = _.handler(function (status, oldStatus }); ConnectionPool.prototype.addConnection = function (connection) { + if (!connection.id) { + connection.id = connection.host.toString(); + } + if (!this.index[connection.id]) { + this.log.info('Adding connection to', connection.id); this.index[connection.id] = connection; - connection.on('status changed', this.bound.onStatusChanged); + connection.on('status set', this.bound.onStatusSet); connection.setStatus('alive'); } }; ConnectionPool.prototype.removeConnection = function (connection) { + if (!connection.id) { + connection.id = connection.host.toString(); + } + if (this.index[connection.id]) { delete this.index[connection.id]; connection.setStatus('closed'); - connection.removeListener('status changed', this.bound.onStatusChanged); + connection.removeListener('status set', this.bound.onStatusSet); } }; -ConnectionPool.prototype.setNodes = function (nodeConfigs) { +ConnectionPool.prototype.setHosts = function (hosts) { var connection; var i; var id; - var node; + var host; var toRemove = _.clone(this.index); - for (i = 0; i < nodeConfigs.length; i++) { - node = nodeConfigs[i]; - if (node instanceof Host) { - id = node.toString(); - if (this.index[id]) { - delete toRemove[id]; - } else { - connection = new this.config.connectionClass(node, this.config); - connection.id = id; - this.addConnection(connection); - } + for (i = 0; i < hosts.length; i++) { + host = hosts[i]; + id = host.toString(); + if (this.index[id]) { + delete toRemove[id]; + } else { + connection = new this.Connection(host); + connection.id = id; + this.addConnection(connection); } } - _.each(toRemove, this.removeConnection, this); + var removeIds = _.keys(toRemove); + for (i = 0; i < removeIds.length; i++) { + this.removeConnection(this.index[removeIds[i]]); + } }; ConnectionPool.prototype.close = function () { - this.setNodes([]); + this.setHosts([]); }; ConnectionPool.prototype.empty = ConnectionPool.prototype.close; diff --git a/src/lib/connectors/browser_index.js b/src/lib/connectors/browser_index.js new file mode 100644 index 000000000..592f787f3 --- /dev/null +++ b/src/lib/connectors/browser_index.js @@ -0,0 +1,24 @@ +var opts = { + xhr: require('./xhr'), + jquery: require('./jquery'), + angular: require('./angular') +}; +var _ = require('../utils'); + +// remove modules that have been ignored by browserify +_.each(opts, function (conn, name) { + if (typeof conn !== 'function') { + delete opts[name]; + } +}); + +// custom __default specification +if (opts.xhr) { + opts._default = 'xhr'; +} else if (opts.angular) { + opts._default = 'angular'; +} else { + opts._default = 'jquery'; +} + +module.exports = opts; diff --git a/src/lib/connectors/http.js b/src/lib/connectors/http.js index d9a1adeaa..75123a05d 100644 --- a/src/lib/connectors/http.js +++ b/src/lib/connectors/http.js @@ -12,56 +12,64 @@ var handles = { http: require('http'), https: require('https') }; +var Log = require('../log'); var _ = require('../utils'); var errors = require('../errors'); var qs = require('querystring'); -var KeepAliveAgent = require('agentkeepalive/lib/agent'); +var KeepAliveAgent = require('agentkeepalive'); var ConnectionAbstract = require('../connection'); - +/** + * Connector used to talk to an elasticsearch node via HTTP + * + * @param {Host} host - The host object representing the elasticsearch node we will be talking to + * @param {Object} [config] - Configuration options (extends the configuration options for ConnectionAbstract) + * @param {Number} [config.maxSockets=10] - the maximum number of sockets that will be opened to this node + * @param {Number} [config.maxFreeSockets=10] - this maximum number of sockets that can sit idle to this node + * @param {Number} [config.maxKeepAliveTime=300000] - an idle timeout for the connections to this node. If your + * maxSockets is much higher than your average concurrent usage, this timeout will cause sockets to close which + * can be interpreted as "bad" behavior for clients. + */ function HttpConnector(host, config) { + config = _.defaults(config || {}, { + maxSockets: 10, + maxFreeSockets: 10, + maxKeepAliveTime: 3e5 // 5 minutes + }); ConnectionAbstract.call(this, host, config); this.hand = handles[this.host.protocol]; this.agent = new KeepAliveAgent({ - maxSockets: 1, - maxKeepAliveRequests: 0, // max requests per keepalive socket, default is 0, no limit. - maxKeepAliveTime: 30000 // keepalive for 30 seconds + keepAlive: true, + maxSockets: config.maxSockets, + maxFreeSockets: config.maxFreeSockets || this.hand.Agent.defaultMaxSockets, + keepAliveMsecs: config.keepAliveMsecs }); - this.on('closed', this.bound.onClosed); - this.on('alive', this.bound.onAlive); + this.log = config.log; + if (!_.isObject(this.log)) { + this.log = new Log(); + } } _.inherits(HttpConnector, ConnectionAbstract); -HttpConnector.prototype.onClosed = _.handler(function () { - this.agent.destroy(); - this.removeAllListeners(); -}); - -HttpConnector.prototype.onAlive = _.handler(function () { - // only set the agents max agents config once the connection is verified to be alive - this.agent.maxSockets = this.config.maxSockets; -}); - HttpConnector.prototype.makeReqParams = function (params) { + params = params || {}; + var host = this.host; + var reqParams = { - method: params.method, - protocol: this.host.protocol + ':', - auth: this.host.auth, - hostname: this.host.host, - port: this.host.port, - pathname: this.host.path + params.path, - headers: this.host.headers, + method: params.method || 'GET', + protocol: host.protocol + ':', + auth: host.auth, + hostname: host.host, + port: host.port, + path: (host.path || '') + (params.path || ''), + headers: host.headers, agent: this.agent }; var query = this.host.query ? this.host.query : null; - if (typeof query === 'string') { - query = qs.parse(query); - } - if (params.query) { query = _.defaults({}, typeof params.query === 'string' ? qs.parse(params.query) : params.query, @@ -70,10 +78,7 @@ HttpConnector.prototype.makeReqParams = function (params) { } if (query) { - reqParams.query = query; - reqParams.path = reqParams.pathname + '?' + qs.stringify(query); - } else { - reqParams.path = reqParams.pathname; + reqParams.path = reqParams.path + '?' + qs.stringify(query); } return reqParams; @@ -86,7 +91,7 @@ HttpConnector.prototype.request = function (params, cb) { var response; var status = 0; var timeout = params.timeout || this.config.timeout; - var log = this.config.log; + var log = this.log; var reqParams = this.makeReqParams(params); diff --git a/src/lib/connectors/index.js b/src/lib/connectors/index.js new file mode 100644 index 000000000..e35375cae --- /dev/null +++ b/src/lib/connectors/index.js @@ -0,0 +1,4 @@ +module.exports = { + http: require('./http'), + _default: 'http' +}; diff --git a/src/lib/errors.js b/src/lib/errors.js index 1536f047d..36686debd 100644 --- a/src/lib/errors.js +++ b/src/lib/errors.js @@ -1,5 +1,5 @@ -var _ = require('./utils'), - errors = module.exports; +var _ = require('./utils'); +var errors = module.exports; function ErrorAbstract(msg, constructor) { this.message = msg; @@ -11,6 +11,7 @@ function ErrorAbstract(msg, constructor) { Error.captureStackTrace(this, constructor); } } +errors._Abstract = ErrorAbstract; _.inherits(ErrorAbstract, Error); /** diff --git a/src/lib/host.js b/src/lib/host.js index 8fb50490e..956f7be9c 100644 --- a/src/lib/host.js +++ b/src/lib/host.js @@ -1,5 +1,5 @@ /** - * Class to wrap URLS, formatting them and maintaining their seperate details + * Class to wrap URLS, formatting them and maintaining their separate details * @type {[type]} */ module.exports = Host; @@ -16,42 +16,76 @@ var defaultPort = { https: 443 }; +var urlParseFields = [ + 'protocol', 'hostname', 'pathname', 'port', 'auth', 'query' +]; + +var simplify = ['host', 'path']; + function Host(config) { - if (this instanceof Host) { - if (typeof config === 'string') { - return Host.fromString(config); - } else { - _.extend(this, config || {}); + config = config || {}; + + // defaults + this.protocol = 'http'; + this.host = 'localhost'; + this.port = 9200; + this.auth = null; + this.query = null; + + if (typeof config === 'string') { + if (!startsWithProtocolRE.test(config)) { + config = 'http://' + config; } + config = _.pick(url.parse(config, false, true), urlParseFields); + } + + if (_.isObject(config)) { + // move hostname/portname to host/port semi-intelligently. + _.each(simplify, function (to) { + var from = to + 'name'; + if (config[from] && config[to]) { + if (config[to].indexOf(config[from]) === 0) { + config[to] = config[from]; + } + } else if (config[from]) { + config[to] = config[from]; + } + delete config[from]; + }); } else { - return new Host(config); + config = {}; + } + + _.assign(this, config); + + // make sure the query string is parsed + if (this.query === null) { + // majority case + this.query = {}; + } else if (!_.isPlainObject(this.query)) { + this.query = qs.parse(this.query); + } + + // make sure that the port is a number + if (typeof this.port !== 'number') { + this.port = parseInt(this.port, 10); + if (isNaN(this.port)) { + this.port = 9200; + } + } + + // make sure the path starts with a leading slash + // and that empty paths convert to '/' + if (!this.path || this.path.charAt(0) !== '/') { + this.path = '/' + (this.path || ''); + } + + // strip trailing ':' on the protocol (when config comes from url.parse) + if (this.protocol.substr(-1) === ':') { + this.protocol = this.protocol.substring(0, this.protocol.length - 1); } } -Host.fromString = function (urlString) { - if (!startsWithProtocolRE.test(urlString)) { - urlString = 'http://' + urlString; - } - var u = url.parse(urlString, true, true); - return new Host({ - protocol: u.protocol ? u.protocol.substring(0, u.protocol.length - 1) : 'http', - host: u.hostname || 'localhost', - port: u.port || 9200, - auth: u.auth || '', - path: u.pathname, - query: u.query, - }); -}; - -Host.prototype = { - protocol: 'http', - host: 'localhost', - port: 9200, - auth: '', - path: '', - query: false -}; - Host.prototype.makeUrl = function (params) { params = params || {}; // build the port @@ -62,17 +96,10 @@ Host.prototype.makeUrl = function (params) { } // build the path - var path = ''; - // add the path prefix if set - if (this.path) { - path += this.path; - } - // then the path from the params - if (params.path) { - path += params.path; - } - // if we still have a path, and it doesn't start with '/' add it. - if (path && path.charAt(0) !== '/') { + var path = '' + (this.path || '') + (params.path || ''); + + // if path doesn't start with '/' add it. + if (path.charAt(0) !== '/') { path = '/' + path; } @@ -90,3 +117,7 @@ Host.prototype.makeUrl = function (params) { return this.protocol + '://' + this.host + port + path + (query ? '?' + query : ''); }; + +Host.prototype.toString = function () { + return this.makeUrl(); +}; diff --git a/src/lib/log.js b/src/lib/log.js index cafbb2d3b..040cbf525 100755 --- a/src/lib/log.js +++ b/src/lib/log.js @@ -1,20 +1,6 @@ var _ = require('./utils'); var url = require('url'); var EventEmitter = require('events').EventEmitter; -if (process.browser) { - var loggers = { - Console: require('./loggers/console') - }; -} else { - var loggers = { - File: require('./loggers/file'), - Stream: require('./loggers/file'), - Stdio: require('./loggers/stdio'), - Tracer: require('./loggers/tracer') - }; -} - - /** * Log bridge, which is an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) @@ -31,40 +17,42 @@ if (process.browser) { * @param {string} output.type - The name of the logger to use for this output */ function Log(config) { - this.config = config || {}; + config = config || {}; var i; - var output = config.loggers ? config.loggers : 'warning'; + var outputs; - if (_.isString(output) || _.isFinite(output)) { - output = [ - { - level: output - } - ]; - } else if (_.isPlainObject(output)) { - output = [output]; - } else if (_.isArray(output)) { - for (i = 0; i < output.length; i++) { - if (_.isString(output[i])) { - output[i] = { - level: output[i] - }; - } + if (config.log) { + if (_.isArrayOfStrings(config.log)) { + outputs = [{ + levels: config.log + }]; + } else { + outputs = _.createArray(config.log, function (val) { + if (_.isPlainObject(val)) { + return val; + } + if (typeof val === 'string') { + return { + level: val + }; + } + }); + } + + if (!outputs) { + throw new TypeError('Invalid logging output config. Expected either a log level, array of log levels, ' + + 'a logger config object, or an array of logger config objects.'); + } + + for (i = 0; i < outputs.length; i++) { + this.addOutput(outputs[i]); } } - - if (!_.isArrayOfPlainObjects(output)) { - throw new TypeError('Invalid Logging output config'); - } - - for (i = 0; i < output.length; i++) { - this.addOutput(output[i]); - } - } _.inherits(Log, EventEmitter); +Log.loggers = require('./loggers'); Log.prototype.close = function () { this.emit('closing'); @@ -149,11 +137,24 @@ Log.levels = [ * @return {Array} - */ Log.parseLevels = function (input) { - if (_.isString(input)) { - return Log.levels.slice(0, _.indexOf(Log.levels, input) + 1); - } - else if (_.isArray(input)) { - return _.intersection(input, Log.levels); + switch (typeof input) { + case 'string': + var i = _.indexOf(Log.levels, input); + if (i >= 0) { + return Log.levels.slice(0, i + 1); + } + /* fall through */ + case 'object': + if (_.isArray(input)) { + var valid = _.intersection(input, Log.levels); + if (valid.length === input.length) { + return valid; + } + } + /* fall through */ + default: + throw new TypeError('invalid logging level ' + input + '. Expected zero or more of these options: ' + + Log.levels.join(', ')); } }; @@ -190,22 +191,14 @@ Log.join = function (arrayish) { * @return {Logger} */ Log.prototype.addOutput = function (config) { - var levels = Log.parseLevels(config.levels || config.level || 'warning'); + config = config || {}; - _.defaults(config || {}, { - type: process.browser ? 'Console' : 'Stdio', - }); - - // force the levels config + // force "levels" key + config.levels = Log.parseLevels(config.levels || config.level || 'warning'); delete config.level; - config.levels = levels; - var Logger = loggers[_.studlyCase(config.type)]; - if (Logger) { - return new Logger(config, this); - } else { - throw new Error('Invalid logger type "' + config.type + '". Expected one of ' + _.keys(loggers).join(', ')); - } + var Logger = _.funcEnum(config, 'type', Log.loggers, process.browser ? 'console' : 'stdio'); + return new Logger(this, config); }; /** diff --git a/src/lib/logger.js b/src/lib/logger.js index 49939ae07..054fb095b 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -2,18 +2,17 @@ var _ = require('./utils'); /** * Abstract class providing common functionality to loggers + * @param {[type]} log [description] * @param {[type]} config [description] - * @param {[type]} bridge [description] */ -function LoggerAbstract(config, bridge) { - - this.bridge = bridge; +function LoggerAbstract(log, config) { + this.log = log; this.listeningLevels = []; _.makeBoundMethods(this); - // when the bridge closes, remove our event listeners - this.bridge.on('closing', this.bound.cleanUpListeners); + // when the log closes, remove our event listeners + this.log.on('closing', this.bound.cleanUpListeners); this.setupListeners(config.levels); } @@ -68,7 +67,7 @@ LoggerAbstract.prototype.setupListeners = function (levels) { _.each(this.listeningLevels, function (level) { var fnName = 'on' + _.ucfirst(level); if (this.bound[fnName]) { - this.bridge.on(level, this.bound[fnName]); + this.log.on(level, this.bound[fnName]); } else { throw new Error(fnName + ' is not a function'); } @@ -84,12 +83,12 @@ LoggerAbstract.prototype.setupListeners = function (levels) { */ LoggerAbstract.prototype.cleanUpListeners = _.handler(function () { _.each(this.listeningLevels, function (level) { - this.bridge.removeListener(level, this.bound['on' + _.ucfirst(level)]); + this.log.removeListener(level, this.bound['on' + _.ucfirst(level)]); }, this); }); /** - * Handler for the bridges "error" event + * Handler for the logs "error" event * * @method onError * @private @@ -101,7 +100,7 @@ LoggerAbstract.prototype.onError = _.handler(function (e) { }); /** - * Handler for the bridges "warning" event + * Handler for the logs "warning" event * * @method onWarning * @private @@ -113,7 +112,7 @@ LoggerAbstract.prototype.onWarning = _.handler(function (msg) { }); /** - * Handler for the bridges "info" event + * Handler for the logs "info" event * * @method onInfo * @private @@ -125,7 +124,7 @@ LoggerAbstract.prototype.onInfo = _.handler(function (msg) { }); /** - * Handler for the bridges "debug" event + * Handler for the logs "debug" event * * @method onDebug * @private @@ -137,7 +136,7 @@ LoggerAbstract.prototype.onDebug = _.handler(function (msg) { }); /** - * Handler for the bridges "trace" event + * Handler for the logs "trace" event * * @method onTrace * @private diff --git a/src/lib/loggers/browser_index.js b/src/lib/loggers/browser_index.js new file mode 100644 index 000000000..51b651356 --- /dev/null +++ b/src/lib/loggers/browser_index.js @@ -0,0 +1,3 @@ +module.exports = { + console: require('./console') +}; diff --git a/src/lib/loggers/console.js b/src/lib/loggers/console.js index 1d9a567d2..0553edad4 100644 --- a/src/lib/loggers/console.js +++ b/src/lib/loggers/console.js @@ -15,9 +15,9 @@ module.exports = Console; var LoggerAbstract = require('../logger'); var _ = require('../utils'); -function Console(config, bridge) { +function Console(log, config) { // call my super - LoggerAbstract.call(this, config, bridge); + LoggerAbstract.call(this, log, config); // config/state this.color = _.has(config, 'color') ? !!config.color : true; diff --git a/src/lib/loggers/file.js b/src/lib/loggers/file.js index ec32e2e87..d4c43fc94 100755 --- a/src/lib/loggers/file.js +++ b/src/lib/loggers/file.js @@ -15,16 +15,20 @@ var StreamLogger = require('./stream'); var _ = require('../utils'); var fs = require('fs'); -function File(config, bridge) { - // setup the stream before calling the super +function File(log, config) { + config = config || {}; + + // we should probably through an error if they don't support a path this.path = config.path || 'elasticsearch.log'; + + // yahoo! config.stream = fs.createWriteStream(this.path, { flags: 'a', encoding: 'utf8' }); // call my super - StreamLogger.call(this, config, bridge); + StreamLogger.call(this, log, config); } _.inherits(File, StreamLogger); diff --git a/src/lib/loggers/index.js b/src/lib/loggers/index.js new file mode 100644 index 000000000..29dc9042a --- /dev/null +++ b/src/lib/loggers/index.js @@ -0,0 +1,6 @@ +module.exports = { + file: require('./file'), + stream: require('./file'), + stdio: require('./stdio'), + tracer: require('./tracer') +}; diff --git a/src/lib/loggers/stdio.js b/src/lib/loggers/stdio.js index 54f00b0bc..a0aecf834 100755 --- a/src/lib/loggers/stdio.js +++ b/src/lib/loggers/stdio.js @@ -40,12 +40,12 @@ var defaultColors = { } }; -function Stdio(config, bridge) { +function Stdio(log, config) { // call my super - LoggerAbstract.call(this, config, bridge); + LoggerAbstract.call(this, log, config); // config/state - this.color = Boolean(_.has(config, 'color') ? config.color : chalk.supportsColor); + this.color = !!(_.has(config, 'color') ? config.color : chalk.supportsColor); this.colors = _.defaults(config.colors || {}, defaultColors); } diff --git a/src/lib/loggers/stream.js b/src/lib/loggers/stream.js index 8e4ec8794..f057d7adb 100755 --- a/src/lib/loggers/stream.js +++ b/src/lib/loggers/stream.js @@ -15,11 +15,11 @@ module.exports = Stream; var LoggerAbstract = require('../logger'); var _ = require('../utils'); -function Stream(config, bridge) { +function Stream(log, config) { // call my super - LoggerAbstract.call(this, config, bridge); + LoggerAbstract.call(this, log, config); - if (config.stream.write && config.stream.end) { + if (config.stream && config.stream.write && config.stream.end) { this.stream = config.stream; } else { throw new TypeError('Invalid stream, use an instance of stream.Writeable'); diff --git a/src/lib/loggers/tracer.js b/src/lib/loggers/tracer.js index 248fbede4..e33e929a7 100755 --- a/src/lib/loggers/tracer.js +++ b/src/lib/loggers/tracer.js @@ -15,9 +15,9 @@ module.exports = Tracer; var FileLogger = require('./file'); var _ = require('../utils'); -function Tracer(config, bridge) { +function Tracer(log, config) { // call my super - FileLogger.call(this, config, bridge); + FileLogger.call(this, log, config); } _.inherits(Tracer, FileLogger); diff --git a/src/lib/selectors/random.js b/src/lib/selectors/random.js index 88e565ab5..57417a6e5 100755 --- a/src/lib/selectors/random.js +++ b/src/lib/selectors/random.js @@ -1,5 +1,11 @@ -module.exports = RandomSelect; - -function RandomSelect(connections) { +/** + * Selects a connection randomly + * + * @module selectors + * @type {Function} + * @param {Array} connection - The list of connections to choose from + * @return {Connection} - The selected connection + */ +module.exports = function RandomSelector(connections) { return connections[Math.floor(Math.random() * connections.length)]; -} +}; diff --git a/src/lib/selectors/round_robin.js b/src/lib/selectors/round_robin.js index d4c035d5e..e7de6858c 100644 --- a/src/lib/selectors/round_robin.js +++ b/src/lib/selectors/round_robin.js @@ -1,13 +1,13 @@ /** * Selects a connection the simplest way possible, Round Robin * - * @class selector.roundRobin - * @constructor + * @module selectors * @type {Function} + * @param {Array} connections - The list of connections that this selector needs to choose from + * @return {Connection} - The selected connection */ -module.exports = RoundRobinSelect; - -function RoundRobinSelect(connections) { - connections.unshift(connections.pop()); - return connections[0]; -} +module.exports = function (connections) { + var connection = connections[0]; + connections.push(connections.shift()); + return connection; +}; diff --git a/src/lib/serializers/json.js b/src/lib/serializers/json.js index 12d3d1929..2ecc21bef 100755 --- a/src/lib/serializers/json.js +++ b/src/lib/serializers/json.js @@ -6,13 +6,11 @@ module.exports = Json; var _ = require('../utils'); -function Json(client) { - this.client = client; -} +function Json() {} Json.prototype.serialize = function (val, replacer, spaces) { if (val == null) { - return null; + return; } else if (typeof val === 'string') { return val; @@ -26,8 +24,7 @@ Json.prototype.unserialize = function (str) { try { return JSON.parse(str); } catch (e) { - this.client.log.error(new Error('unable to parse', str)); - return null; + return; } } else { return str; diff --git a/src/lib/transport.js b/src/lib/transport.js index f7519356b..bc7cf2fa9 100644 --- a/src/lib/transport.js +++ b/src/lib/transport.js @@ -6,12 +6,66 @@ module.exports = Transport; var _ = require('./utils'); var errors = require('./errors'); +var Host = require('./host'); +var Log = require('./log'); var when = require('when'); function Transport(config) { - this.config = config; + config = config || {}; + + var LogClass; + // setup the log + switch (typeof config.log) { + case 'function': + LogClass = config.log; + break; + case 'undefined': + config.log = 'warning'; + /* fall through */ + default: + LogClass = Log; + } + + config.log = this.log = new LogClass(config); + + // overwrite the createDefer method if a new implementation is provided + if (typeof config.createDefer === 'function') { + this.createDefer = config.createDefer; + } + + // setup the connection pool + var ConnectionPool = _.funcEnum(config, 'connectionPool', Transport.connectionPools, 'main'); + this.connectionPool = new ConnectionPool(config); + + if (config.hosts) { + var hosts = _.createArray(config.hosts, function (val) { + if (_.isPlainObject(val) || _.isString(val)) { + return val; + } + }); + if (!hosts) { + throw new Error('Invalid hosts config. Expected a URL, an array of urls, a host config object, or an array of ' + + 'host config objects.'); + } + + this.connectionPool.setHosts(_.map(hosts, function (conf) { + return new Host(conf); + })); + } + + // setup the serializer + var Serializer = _.funcEnum(config, 'serializer', Transport.serializers, 'json'); + this.serializer = new Serializer(config); } +Transport.connectionPools = { + main: require('./connection_pool') +}; + +Transport.serializers = { + json: require('./serializers/json') +}; + /** * Perform a request with the client's transport * @@ -27,15 +81,13 @@ function Transport(config) { */ Transport.prototype.request = function (params, cb) { - var log = this.config.log; - var serializer = this.config.serializer; - var connectionPool = this.config.connectionPool; - var remainingRetries = this.config.maxRetries; + var self = this; + var remainingRetries = this.maxRetries; var connection; // set in sendReqWithConnection var connectionReq; // an object with an abort method, set in sendReqWithConnection var request; // the object returned to the user, might be a deferred - log.debug('starting request', params); + self.log.debug('starting request', params); if (params.body && params.method === 'GET') { _.nextTick(respond, new TypeError('Body can not be sent with method "GET"')); @@ -44,7 +96,7 @@ Transport.prototype.request = function (params, cb) { // serialize the body if (params.body) { - params.body = serializer[params.bulkBody ? 'bulkBody' : 'serialize'](params.body); + params.body = self.serializer[params.bulkBody ? 'bulkBody' : 'serialize'](params.body); } params.req = { @@ -55,7 +107,7 @@ Transport.prototype.request = function (params, cb) { body: params.body, }; - connectionPool.select(sendReqWithConnection); + self.connectionPool.select(sendReqWithConnection); function abortRequest() { remainingRetries = 0; @@ -69,7 +121,7 @@ Transport.prototype.request = function (params, cb) { connection = _connection; connectionReq = connection.request(params.req, checkRespForFailure); } else { - log.warning('No living connections'); + self.log.warning('No living connections'); respond(new errors.NoConnections()); } } @@ -77,10 +129,10 @@ Transport.prototype.request = function (params, cb) { function checkRespForFailure(err, body, status) { if (err && remainingRetries) { remainingRetries--; - log.error(err.message, '-- retrying'); - connectionPool.select(sendReqWithConnection); + self.log.error(err.message, '-- retrying'); + self.connectionPool.select(sendReqWithConnection); } else { - log.info('Request complete'); + self.log.info('Request complete'); respond(err, body, status); } } @@ -89,21 +141,15 @@ Transport.prototype.request = function (params, cb) { var parsedBody; if (!err && body) { - parsedBody = serializer.unserialize(body); + parsedBody = self.serializer.unserialize(body); if (parsedBody == null) { err = new errors.Serialization(); } } if (!err) { - // get ignore and ensure that it's an array - var ignore = params.ignore; - if (ignore && !_.isArray(ignore)) { - ignore = [ignore]; - } - if ((status < 200 || status >= 300) - && (!ignore || !_.contains(ignore, status)) + && (!params.ignore || !_.contains(params.ignore, status)) ) { if (errors[status]) { err = new errors[status](parsedBody && parsedBody.error); @@ -140,7 +186,7 @@ Transport.prototype.request = function (params, cb) { abort: abortRequest }; } else { - var defer = when.defer(); + var defer = this.createDefer(); defer.promise.abort = abortRequest; request = defer.promise; } @@ -148,6 +194,10 @@ Transport.prototype.request = function (params, cb) { return request; }; +Transport.prototype.createDefer = function () { + return when.defer(); +}; + /** * Ask an ES node for a list of all the nodes, add/remove nodes from the connection * pool as appropriate @@ -155,7 +205,7 @@ Transport.prototype.request = function (params, cb) { * @param {Function} cb - Function to call back once complete */ Transport.prototype.sniff = function (cb) { - var config = this.config; + var self = this; // make cb a function if it isn't cb = typeof cb === 'function' ? cb : _.noop; @@ -165,9 +215,16 @@ Transport.prototype.sniff = function (cb) { method: 'GET' }, function (err, resp) { if (!err && resp && resp.nodes) { - var nodes = config.nodesToHostCallback(resp.nodes); - config.connectionPool.setNodes(nodes); + var hosts = _.map(self.nodesToHostCallback(resp.nodes), function (hostConfig) { + return new Host(hostConfig); + }); + this.connectionPool.setHosts(hosts); } cb(err, resp); }); }; + +Transport.prototype.close = function () { + this.log.close(); + this.connectionPool.close(); +}; diff --git a/src/lib/utils.js b/src/lib/utils.js index 162d006e2..1fef1521d 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -13,15 +13,6 @@ var nodeUtils = require('util'); var utils = _.extend({}, _, nodeUtils); _ = utils; -utils.inspect = function (thing, opts) { - return nodeUtils.inspect(thing, _.defaults(opts || {}, { - showHidden: true, - depth: null, - color: true - })); -}; - - /** * Link to [path.join](http://nodejs.org/api/path.html#path_path_join_path1_path2) * @@ -46,7 +37,7 @@ utils.reKey = function (obj, transform, recursive) { var out = {}; _.each(obj, function (prop, name) { - if (recursive && typeof prop === 'object') { + if (recursive && _.isPlainObject(prop)) { out[transform(name)] = utils.reKey(prop, transform, recursive); } else { out[transform(name)] = prop; @@ -104,7 +95,6 @@ utils.deepMerge = function (to, from) { /** * Capitalize the first letter of a word * - * @todo Tests * @method ucfirst * @param {string} word - The word to transform * @return {string} @@ -166,7 +156,6 @@ function adjustWordCase(firstWordCap, otherWordsCap, sep) { /** * Transform a string into StudlyCase * - * @todo Tests * @method studlyCase * @param {String} string * @return {String} @@ -176,7 +165,6 @@ utils.studlyCase = adjustWordCase(true, true, ''); /** * Transform a string into camelCase * - * @todo Tests * @method camelCase * @param {String} string * @return {String} @@ -186,7 +174,6 @@ utils.camelCase = adjustWordCase(false, true, ''); /** * Transform a string into snakeCase * - * @todo Tests * @method snakeCase * @param {String} string * @return {String} @@ -196,7 +183,6 @@ utils.snakeCase = adjustWordCase(false, false, '_'); /** * Lower-case a string, and return an empty string if any is not a string * - * @todo Tests * @param any {*} - Something or nothing * @returns {string} */ @@ -214,7 +200,6 @@ utils.toLowerString = function (any) { /** * Upper-case the string, return an empty string if any is not a string * - * @todo Tests * @param any {*} - Something or nothing * @returns {string} */ @@ -232,13 +217,12 @@ utils.toUpperString = function (any) { /** * Test if a value is "numeric" meaning that it can be transformed into something besides NaN * - * @todo Tests * @method isNumeric * @param {*} val * @return {Boolean} */ utils.isNumeric = function (val) { - return !isNaN(val === null ? NaN : val * 1); + return typeof val !== 'object' && val - parseFloat(val) >= 0; }; // regexp to test for intervals @@ -247,7 +231,6 @@ var intervalRE = /^(\d+(?:\.\d+)?)([Mwdhmsy])$/; /** * Test if a string represents an interval (eg. 1m, 2Y) * - * @todo Test * @method isInterval * @param {String} val * @return {Boolean} @@ -259,7 +242,6 @@ utils.isInterval = function (val) { /** * Repeat a string n times * - * @todo Test * @todo TestPerformance * @method repeat * @param {String} what - The string to repeat @@ -271,27 +253,18 @@ utils.repeat = function (what, times) { }; /** - * Override node's util.inherits function to also supply a callSuper function on the child class that can be called - * with the instance and the arguments passed to the child's constructor. This should only be called from within the - * constructor of the child class and should be removed from the code once the constructor is "done". + * Override node's util.inherits function, providing a browser safe version thanks to lodash * * @param constructor {Function} - the constructor that should subClass superConstructor * @param superConstructor {Function} - The parent constructor */ -utils.inherits = function (constructor, superConstructor) { - nodeUtils.inherits(constructor, superConstructor); - constructor.callSuper = function (inst, args) { - if (args) { - if (_.isArguments(args)) { - utils.applyArgs(superConstructor, inst, args); - } else { - utils.applyArgs(superConstructor, inst, arguments, 1); - } - } else { - superConstructor.call(inst); - } - }; -}; +if (process.browser) { + utils.inherits = require('inherits'); +} + +/** + * + */ /** * Remove leading/trailing spaces from a string @@ -412,15 +385,59 @@ _.makeBoundMethods = function (obj, methods) { _.noop = function () {}; -// _.getStackTrace = function (callee) { -// var e = {}; -// if (typeof Error.captureStackTrace === 'function') { -// Error.captureStackTrace(e, callee || _.getStackTrace); -// } else { -// e.stack = (new Error()).stack; -// console.log(e.stack); -// } -// return '\n' + e.stack.split('\n').slice(1).join('\n'); -// }; +/** + * Implements the standard "string or constructor" check that I was copy/pasting everywhere + * @param {String|Function} val - the value that the user passed in + * @param {Object} opts - a map of the options + * @return {Function|undefined} - If a valid option was specified, then the constructor is returned + */ +_.funcEnum = function (config, name, opts, def) { + var val = config[name]; + switch (typeof val) { + case 'undefined': + return opts[def]; + case 'function': + return val; + case 'string': + if (opts[val]) { + return opts[val]; + } + /* falls through */ + default: + throw new TypeError('Invalid ' + name + ' "' + val + '", expected a function or one of ' + + _.keys(opts).join(', ')); + } +}; + +/** + * Accepts any object and attempts to convert it into an array. If the object passed in is not + * an array it will be wrapped in one. Then the transform/map function will be called for each element + * and create a new array that is returned. If the map function fails to return something, the loop is + * halted and false is returned instead of an array. + * + * @param {*} input - The value to convert + * @param {Function} transform - A function called for each element of the resulting array + * @return {Array|false} - an array on success, or false on failure. + */ +_.createArray = function (input, transform) { + transform = typeof transform === 'function' ? transform : _.identity; + var output = []; + var item; + var i; + + if (!_.isArray(input)) { + input = [input]; + } + + for (i = 0; i < input.length; i++) { + item = transform(input[i]); + if (item === void 0) { + return false; + } else { + output.push(item); + } + } + return output; +}; module.exports = utils; diff --git a/test/browser_integration/esjs_reporter.css b/test/integration/browser_yaml_suite/esjs_reporter.css similarity index 100% rename from test/browser_integration/esjs_reporter.css rename to test/integration/browser_yaml_suite/esjs_reporter.css diff --git a/test/browser_integration/esjs_reporter.js b/test/integration/browser_yaml_suite/esjs_reporter.js similarity index 96% rename from test/browser_integration/esjs_reporter.js rename to test/integration/browser_yaml_suite/esjs_reporter.js index 0bba7d3ed..e56151ce9 100644 --- a/test/browser_integration/esjs_reporter.js +++ b/test/integration/browser_yaml_suite/esjs_reporter.js @@ -34,8 +34,12 @@ var stats = this.stats; var rootSuite = { $el: $(''), + name: 'root', + start: Date.now(), results: [], - suites: [] + suites: [], + stdout: '', + stderr: '', }; @@ -135,7 +139,7 @@ runner.on('end', function () { var testResults = { stats: stats, - suites: $.map(rootSuite.suites, function removeElements(suite) { + suites: $.map([rootSuite], function removeElements(suite) { var s = { name: suite.name, start: suite.start, diff --git a/test/browser_integration/index.html b/test/integration/browser_yaml_suite/index.html similarity index 92% rename from test/browser_integration/index.html rename to test/integration/browser_yaml_suite/index.html index 9a3bc169a..e7673afed 100644 --- a/test/browser_integration/index.html +++ b/test/integration/browser_yaml_suite/index.html @@ -12,6 +12,7 @@ + @@ -24,18 +25,21 @@ var BROWSER_NAME = <%= JSON.stringify(browser) %>; - - - + + + + diff --git a/test/browser_integration/jquery.js b/test/integration/browser_yaml_suite/jquery.js similarity index 100% rename from test/browser_integration/jquery.js rename to test/integration/browser_yaml_suite/jquery.js diff --git a/test/integration/browser_yaml_suite/yaml_tests.js b/test/integration/browser_yaml_suite/yaml_tests.js new file mode 100644 index 000000000..3a9361283 --- /dev/null +++ b/test/integration/browser_yaml_suite/yaml_tests.js @@ -0,0 +1,42354 @@ +;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= arr.length) { + callback(null); + } + } + })); + }); + }; + async.forEach = async.each; + + async.eachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + async.forEachSeries = async.eachSeries; + + async.eachLimit = function (arr, limit, iterator, callback) { + var fn = _eachLimit(limit); + fn.apply(null, [arr, iterator, callback]); + }; + async.forEachLimit = async.eachLimit; + + var _eachLimit = function (limit) { + + return function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.each].concat(args)); + }; + }; + var doParallelLimit = function(limit, fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [_eachLimit(limit)].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.eachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + async.mapLimit = function (arr, limit, iterator, callback) { + return _mapLimit(limit)(arr, iterator, callback); + }; + + var _mapLimit = function(limit) { + return doParallelLimit(limit, _asyncMap); + }; + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.eachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _each(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + callback = function () {}; + } + }); + + _each(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + if (err) { + var safeResults = {}; + _each(_keys(results), function(rkey) { + safeResults[rkey] = results[rkey]; + }); + safeResults[k] = args; + callback(err, safeResults); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + results[k] = args; + async.setImmediate(taskComplete); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor !== Array) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.setImmediate(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + var _parallel = function(eachfn, tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + eachfn.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + eachfn.each(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.parallel = function (tasks, callback) { + _parallel({ map: async.map, each: async.each }, tasks, callback); + }; + + async.parallelLimit = function(tasks, limit, callback) { + _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.eachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doWhilst = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (test()) { + async.doWhilst(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doUntil = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (!test()) { + async.doUntil(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.queue = function (worker, concurrency) { + if (concurrency === undefined) { + concurrency = 1; + } + function _insert(q, data, pos, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + var item = { + data: task, + callback: typeof callback === 'function' ? callback : null + }; + + if (pos) { + q.tasks.unshift(item); + } else { + q.tasks.push(item); + } + + if (q.saturated && q.tasks.length === concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + _insert(q, data, false, callback); + }, + unshift: function (data, callback) { + _insert(q, data, true, callback); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if (q.empty && q.tasks.length === 0) { + q.empty(); + } + workers += 1; + var next = function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if (q.drain && q.tasks.length + workers === 0) { + q.drain(); + } + q.process(); + }; + var cb = only_once(next); + worker(task.data, cb); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + async.cargo = function (worker, payload) { + var working = false, + tasks = []; + + var cargo = { + tasks: tasks, + payload: payload, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + if (cargo.saturated && tasks.length === payload) { + cargo.saturated(); + } + }); + async.setImmediate(cargo.process); + }, + process: function process() { + if (working) return; + if (tasks.length === 0) { + if(cargo.drain) cargo.drain(); + return; + } + + var ts = typeof payload === 'number' + ? tasks.splice(0, payload) + : tasks.splice(0); + + var ds = _map(ts, function (task) { + return task.data; + }); + + if(cargo.empty) cargo.empty(); + working = true; + worker(ds, function () { + working = false; + + var args = arguments; + _each(ts, function (data) { + if (data.callback) { + data.callback.apply(null, args); + } + }); + + process(); + }); + }, + length: function () { + return tasks.length; + }, + running: function () { + return working; + } + }; + return cargo; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _each(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.memo = memo; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + + async.times = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.map(counter, iterator, callback); + }; + + async.timesSeries = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.mapSeries(counter, iterator, callback); + }; + + async.compose = function (/* functions... */) { + var fns = Array.prototype.reverse.call(arguments); + return function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + async.reduce(fns, args, function (newargs, fn, cb) { + fn.apply(that, newargs.concat([function () { + var err = arguments[0]; + var nextargs = Array.prototype.slice.call(arguments, 1); + cb(err, nextargs); + }])) + }, + function (err, results) { + callback.apply(that, [err].concat(results)); + }); + }; + }; + + var _applyEach = function (eachfn, fns /*args...*/) { + var go = function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + return eachfn(fns, function (fn, cb) { + fn.apply(that, args.concat([cb])); + }, + callback); + }; + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + return go.apply(this, args); + } + else { + return go; + } + }; + async.applyEach = doParallel(_applyEach); + async.applyEachSeries = doSeries(_applyEach); + + async.forever = function (fn, callback) { + function next(err) { + if (err) { + if (callback) { + return callback(err); + } + throw err; + } + fn(next); + } + next(); + }; + + // AMD / RequireJS + if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return async; + }); + } + // Node.js + else if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + // included directly via