[
  {
    "path": ".gitignore",
    "content": "lib/\nnode_modules/\n.DS_Store\ntemp/\n.nyc_output\ncoverage\n"
  },
  {
    "path": ".npmignore",
    "content": "node_modules/\nnpm-debug.log\n.DS_Store\n.gitignore\n.travis.yml\ntemp/\n.nyc_output\ncoverage\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nsudo: false\nnode_js:\n  - \"10\"\n  - \"9\"\n  - \"8\"\n  - \"7\"\n  - \"6\"\nenv:\n  - CXX=\"g++-4.8\" CC=\"gcc-4.8\"\naddons:\n  apt:\n    sources:\n    - ubuntu-toolchain-r-test\n    packages:\n    - gcc-4.8\n    - g++-4.8\n    - clang\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2013, node-etcd authors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of the authors nor the\n      names of its contributors may be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# node-etcd\n\nA nodejs library for [ETCD v2](https://coreos.com/etcd/docs/latest/v2/api.html), written in coffee-script.\n\n[![Travis](https://img.shields.io/travis/stianeikeland/node-etcd.svg)](https://travis-ci.org/stianeikeland/node-etcd)\n\n[![npm](https://img.shields.io/npm/v/node-etcd.svg)](https://www.npmjs.com/package/node-etcd)\n\n## Install\n\nFor nodes >= 6.x:\n\n```\n$ npm install node-etcd\n```\n\nFor nodes 4.x <= node < 6.x:\n\n```\n$ npm install node-etcd@6\n```\n\nFor 0.10.x <= nodejs <= 4.x and iojs:\n\n```\n$ npm install node-etcd@5.1.0\n```\n\nFor nodejs == 0.8:\n\n```\n$ npm install node-etcd@3.0.2\n```\n\n## Changes\n- 7.0.0\n  - Fixing vulnerabilities\n  - Drop support for nodejs version 4 &4\n- 6.0.0\n  - Migrate from underscore to lodash for performance / @derektbrown\n  - Updated package versions / @derektbrown\n  - Update deasync call for newer node versions / @derektbrown\n  - Add istanbul coverage reporting / @derektbrown\n\n- 5.1.0\n  - Upgrade deasync dep (caused build problems on newer node) #67 / @jefflembeck\n  - Upgrade request dep (security vulnerability) #71 / @esatterwhite\n  - Sync functions no longer mutate input opts.\n- 5.0.3\n  - Fix bug #56 (exception when calling mkdir with no options or callback)\n- 5.0.2\n  - Update deasync dependency, possible fix for #47.\n- 5.0.1\n  - Was forced to publish 5.0.0 as 5.0.1 because of previous beta tag.\n- 5.0.0\n  - Supports basic auth, timeout, etc. See options.\n  - **Breaking**: Constructor changes (see below)\n  - **Breaking**: Must provide https url to use https\n  - **Breaking**: Uses new default port 2379.\n- 4.2.1\n  - Newer deasync fixes issues with iojs 3.3.0 and nodejs 4.0.0.\n- 4.1.0\n  - Bumps [request](https://github.com/request/request) library version to\n  v2.60.0, this solves an issue with HTTP proxies. `HTTP_PROXY` and `NO_PROXY`\n  env variables should now work as expected for all requests. See issue #40\n- 4.0.2\n  - 307 redirects from etcd 0.4.x clusters when using SSL didn't work properly\n  because of a change in the underlying request library. See issue #39\n- 4.0.1\n  - Minor fixes for syncronous operations, better handling of server failure.\n- 4.0.0\n  - Adds support for synchronous operations (@brockwood) - See\n    [Synchronous Operations](#synchronous-operations).\n  - Adds support for iojs.\n  - Drops support for nodejs 0.8 (use v3.x.x).\n  - Upgrade dependencies.\n- 3.0.2 - Handle cluster leader election periods better (etcd will disconnect us\n  and reject new connections until a new leader is chosen). The client will now\n  retry 3 times with exp backoff if it believes a cluster election is in\n  progress. Retry count is controllable via the `{ maxRetries: x }` option for a\n  request. (npm failed on me and I had to publish as 3.0.2)\n- 3.0.0 - Added cluster support, library now accepts a list of servers to\n  connect to, see constructor changes below. All requests now return a\n  cancellation token, meaning you can cancel requests by calling .cancel() or\n  .abort(). This release might break something if you've previously depended on\n  the leaky abstraction to the request library (request object from request\n  library was returned on all api calls - this has been replaced by the\n  cancellation token - the current request is still available under .req on the\n  token if you really need it.).\n- 2.1.5 - Watcher: try to resync if etcd reports cleared index\n- 2.1.4 - Don't wait before reconnecting if Etcd server times out our watcher.\n- 2.1.3 - Etcd sends an empty response on timeout in recent versions. Parsing\n  the empty message caused watcher to emit error. Now it reconnects instead.\n- 2.1.2 - Exponential backoff (retry), fix spinning reconnect on error. (@ptte)\n- 2.1.1 - Increase request pool.maxSockets to 100\n- 2.1.0 - Use proper error objects instead of strings for errors.\n- 2.0.10 - Fix error in documentation\n- 2.0.9 - Added .post() alias of .create(). Added .compareAndDelete() (for etcd v0.3.0)\n- 2.0.8 - Watchers can be canceled. In-order keys using #create(). Raw requests using #raw().\n- 2.0.7 - Avoid calling callback if callback not given.\n- 2.0.6 - Refactoring, fix responsehandler error.\n- 2.0.5 - Undo use of 'x-etcd-index', this refers to global state.\n- 2.0.4 - Use 'x-etcd-index' for index when watching a key.\n- 2.0.3 - Watcher supports options. Watcher emits etcd action type.\n- 2.0.2 - Mkdir and rmdir. Fix watcher for v2 api.\n- 2.0.1 - Watch, delete and stats now use new v2 api. Added testAndSet convenience method.\n- 2.0.0 - Basic support for etcd protocol v2. set, get, del now supports options.\n- 0.6.1 - Fixes issue #10, missing response caused error when server connection failed / server responded incorrectly.\n- 0.6.0 - Watcher now emits 'error' on invalid responses.\n\n## Basic usage\n\n```javascript\nvar Etcd = require('node-etcd');\nvar etcd = new Etcd();\netcd.set(\"key\", \"value\");\netcd.get(\"key\", console.log);\n```\n\nCallbacks follows the default (error, result) nodejs convention:\n\n```javascript\nfunction callback(err, res) {\n    console.log(\"Error: \", err);\n    console.log(\"Return: \", res);\n}\netcd.get(\"key\", callback);\n// Error: null\n// Return: { action: 'get', node: { key: '/key', value: 'value', modifiedIndex: 4, createdIndex: 4 } }\n```\n\n## Methods\n\n### Etcd(hosts = ['127.0.0.1:2379'], [options])\n\nCreate a new etcd client for a single host etcd setup\n\n```javascript\netcd = new Etcd();\netcd = new Etcd(\"127.0.0.1:2379\");\netcd = new Etcd(\"http://127.0.0.1:2379\");\netcd = new Etcd(\"https://127.0.0.1:2379\");\netcd = new Etcd([\"http://127.0.0.1:2379\"]);\n```\n\n### Etcd(hosts, [options])\n\nCreate a new etcd client for a clustered etcd setup. Client will connect to\nservers in random order. On failure it will try the next server. When all\nservers have failed it will callback with error. If it suspects the cluster is\nin leader election mode it will retry up to 3 times with exp backoff. Number of\nretries can be controlled by adding `{ maxRetries: x }` as an option to requests.\n\n```javascript\netcd = new Etcd(['127.0.0.1:2379','192.168.1.1:2379']);\netcd = new Etcd(['http://127.0.0.1:2379','http://192.168.1.1:2379']);\n```\n\n### .set(key, value = null, [options], [callback])\n\nSet key to value, or create key/directory.\n\n```javascript\netcd.set(\"key\");\netcd.set(\"key\", \"value\");\netcd.set(\"key\", \"value\", console.log);\netcd.set(\"key\", \"value\", { ttl: 60 }, console.log);\netcd.set(\"key\", \"value\", { maxRetries: 3 }, console.log);\n```\n\nAvailable options include:\n\n- `ttl` (time to live in seconds)\n- `prevValue` (previous value, for compare and swap)\n- `prevExist` (existance test, for compare and swap)\n- `prevIndex` (previous index, for compare and swap)\n\nWill create a directory when used without value (value=null): `etcd.set(\"directory/\");`\n\n### .compareAndSwap(key, value, oldvalue, [options], [callback])\n\nConvenience method for test and set (set with {prevValue: oldvalue})\n\n```javascript\netcd.compareAndSwap(\"key\", \"newvalue\", \"oldvalue\");\netcd.compareAndSwap(\"key\", \"newValue\", \"oldValue\", options, console.log);\n```\n\nAlias: `.testAndSet()`\n\n### .get(key, [options], [callback])\n\nGet a key or path.\n\n```javascript\netcd.get(\"key\", console.log);\netcd.get(\"key\", { recursive: true }, console.log);\n```\n\nAvailable options include:\n\n- `recursive` (bool, list all values in directory recursively)\n- `wait` (bool, wait for changes to key)\n- `waitIndex` (wait for changes after given index)\n\n### .del(key, [options], [callback])\n\nDelete a key or path\n\n```javascript\netcd.del(\"key\");\netcd.del(\"key\", console.log);\netcd.del(\"key/\", { recursive: true }, console.log);\n```\n\nAvailable options include:\n\n- `recursive` (bool, delete recursively)\n\nAlias: `.delete()`\n\n### .compareAndDelete(key, oldvalue, [options], [callback])\n\nConvenience method for test and delete (delete with {prevValue: oldvalue})\n\n```javascript\netcd.compareAndDelete(\"key\", \"oldvalue\");\netcd.compareAndDelete(\"key\", \"oldValue\", options, console.log);\n```\n\nAlias: `.testAndDelete()`\n\n### .mkdir(dir, [options], [callback])\n\nCreate a directory\n\n```javascript\netcd.mkdir(\"dir\");\netcd.mkdir(\"dir\", console.log);\netcd.mkdir(\"dir/\", options, console.log);\n```\n\n### .rmdir(dir, [options], [callback])\n\nRemove a directory\n\n```javascript\netcd.rmdir(\"dir\");\netcd.rmdir(\"dir\", console.log);\netcd.rmdir(\"dir/\", { recursive: true }, console.log);\n```\n\nAvailable options include:\n\n- `recursive` (bool, delete recursively)\n\n### .create(path, value, [options], [callback])\n\nAtomically create in-order keys.\n\n```javascript\netcd.create(\"queue\", \"first\")\netcd.create(\"queue\", \"next\", console.log)\n```\n\nAlias: `.post()`\n\n### .watch(key, [options], [callback])\n\nThis is a convenience method for get with `{wait: true}`.\n\n```javascript\netcd.watch(\"key\");\netcd.watch(\"key\", console.log);\n```\n\nThe watch command is pretty low level, it does not handle reconnects or\ntimeouts (Etcd will disconnect you after 5 minutes). Use the `.watcher()` below\nif you do not wish to handle this yourself.\n\n### .watchIndex(key, index, [options], callback)\n\nThis is a convenience method for get with `{wait: true, waitIndex: index}`.\n\n```javascript\netcd.watchIndex(\"key\", 7, console.log);\n```\n\nSee `.watch()` above.\n\n### .watcher(key, [index], [options])\n\nReturns an eventemitter for watching for changes on a key\n\n```javascript\nwatcher = etcd.watcher(\"key\");\nwatcher.on(\"change\", console.log); // Triggers on all changes\nwatcher.on(\"set\", console.log);    // Triggers on specific changes (set ops)\nwatcher.on(\"delete\", console.log); // Triggers on delete.\nwatcher2 = etcd.watcher(\"key\", null, {recursive: true});\nwatcher2.on(\"error\", console.log);\n```\n\nYou can cancel a watcher by calling `.stop()`.\n\nSignals:\n- `change` - emitted on value change\n- `reconnect` - emitted on reconnect\n- `error` - emitted on invalid content\n- `<etcd action>` - the etcd action that triggered the watcher (ex: set, delete).\n- `stop` - watcher was canceled.\n- `resync` - watcher lost sync (server cleared and outdated the index).\n\nIt will handle reconnects and timeouts for you, it will also resync (best\neffort) if it loses sync with Etcd (Etcd only keeps 1000 items in its event\nhistory - for high frequency setups it's possible to fall behind).\n\nUse the `.watch()` command in you need more direct control.\n\n### .raw(method, key, value, options, callback)\n\nBypass the API and do raw queries.\nMethod must be one of: PUT, GET, POST, PATCH, DELETE\n\n```javascript\netcd.raw(\"GET\", \"v2/stats/leader\", null, {}, callback)\netcd.raw(\"PUT\", \"v2/keys/key\", \"value\", {}, callback)\n```\n\nRemember to provide the full path, without any leading '/'\n\n### .machines(callback)\n\nReturns information about etcd nodes in the cluster\n\n```javascript\netcd.machines(console.log);\n```\n\n### .leader(callback)\n\nReturn the leader in the cluster\n\n```javascript\netcd.leader(console.log);\n```\n\n### .leaderStats(callback)\n\nReturn statistics about cluster leader\n\n```javascript\netcd.leaderStats(console.log);\n```\n\n### .selfStats(callback)\n\nReturn statistics about connected etcd node\n\n```javascript\netcd.selfStats(console.log);\n```\n\n## Synchronous operations\n\nThe library supports a set of basic synchronous/blocking operations that can be useful during\nprogram startup (used like [fs.readFileSync](http://nodejs.org/api/fs.html#fs_fs_readfilesync_filename_options)).\n\nSynchronous functions perform the etcd request immediately (blocking) and return the following:\n\n```javascript\n{\n  err // Error message or null if request completed successfully\n  body // Body of the message or null if error\n  headers // Headers from the response\n}\n```\n\n### .setSync(key, value = null, [options])\n\nSynchronously set key to value, or create key/directory.\n\n```javascript\netcd.setSync(\"key\");\netcd.setSync(\"key\", \"value\");\netcd.setSync(\"key\", \"value\", { ttl: 60 });\netcd.setSync(\"key\", \"value\", { maxRetries: 3 });\n```\n\nSame options and function as .set().\n\n### .getSync(key, [options])\n\nGet a key or path.\n\n```javascript\netcd.getSync(\"key\");\netcd.getSync(\"key\", { recursive: true });\n```\n\n### .delSync(key, [options])\n\nSynchronously delete a key or path\n\n```javascript\netcd.delSync(\"key\");\netcd.delSync(\"key/\", { recursive: true });\n```\n\nThe available options are the same as .del() above.\n\n### .mkdirSync(dir, [options])\n\nSynchronously create a directory\n\n```javascript\netcd.mkdirSync(\"dir\");\netcd.mkdirSync(\"dir/\", options);\n```\n\n### .rmdirSync(dir, [options])\n\nSynchronously remove a directory\n\n```javascript\netcd.rmdirSync(\"dir\");\netcd.rmdirSync(\"dir/\", { recursive: true });\n```\n\nThe available options are the same as .rmdir() above.\n\n## Aborting a request\n\nAll async requests will return a cancellation token, to abort a request, do\nthe following:\n\n```javascript\nvar token = etcd.get(\"key\", console.log);\ntoken.abort() // also aliased as token.cancel()\n\nconsole.log(\"Request is cancelled: \", token.isAborted());\n```\n\nNote that there are no guarantees that aborted write operations won't have\naffected server state before they were aborted. They only guarantee here is that\nyou won't get any callbacks from the request after calling `.abort()`.\n\n## SSL support\n\nProvide `ca`, `cert`, `key` as options. Remember to use `https`-url.\n\n```javascript\nvar fs = require('fs');\n\nvar options = {\n    ca:   fs.readFileSync('ca.pem'),\n    cert: fs.readFileSync('cert.pem'),\n    key:  fs.readFileSync('key.pem')\n};\n\nvar etcdssl = new Etcd(\"https://localhost:2379\", options);\n```\n\n## Basic auth\n\nPass a hash containing username and password as auth option to use basic auth.\n\n```javascript\nvar auth = {\n    user: \"username\",\n    pass: \"password\"\n};\n\nvar etcd = new Etcd(\"localhost:2379\", { auth: auth });\n```\n\n## Constructor options\n\nPass in a hash after server in the constructor to set options. Some useful constructor options include:\n\n- `timeout` - Integer request timeout in milliseconds to wait for server response.\n- `ca` - Ca certificate\n- `cert` - Client certificate\n- `key` - Client key\n- `passphrase` - Client key passphrase\n- `auth` - A hash containing `{user: \"username\", pass: \"password\"}` for basic auth.\n\n```javascript\nvar etcd = new Etcd(\"127.0.0.1:2379\", { timeout: 1000, ... });'\n```\n\n## Debugging\n\nNodejs `console.log`/`console.dir` truncates output to a couple of levels -\noften hiding nested errors. Use `util.inspect` to show inner errors.\n\n```javascript\netcd.get('key', function(err, val) {\n    console.log(require('util').inspect(err, true, 10));\n});\n\n//{ [Error: All servers returned error]\n//  [stack]: [Getter/Setter],\n//  [message]: 'All servers returned error',\n//  errors:\n//   [ { server: 'https://localhost:2379',\n//       httperror:\n//        { [Error: Hostname/IP doesn't match certificate's altnames: \"Host: localhost. is not cert's CN: undefined\"]\n//          [stack]: [Getter/Setter],\n//          [message]: 'Hostname/IP doesn\\'t match certificate\\'s altnames: \"Host: localhost. is not cert\\'s CN: undefined\"',\n//          reason: 'Host: localhost. is not cert\\'s CN: undefined',\n//          host: 'localhost.',\n//          cert:\n//           { subject: { C: 'USA', O: 'etcd-ca', OU: 'CA' },\n//             issuer: { C: 'USA', O: 'etcd-ca', OU: 'CA' } } },\n//       httpstatus: undefined,\n//       httpbody: undefined,\n//       response: undefined,\n//       timestamp: Sun Dec 27 2015 23:05:15 GMT+0100 (CET) },\n//     [length]: 1 ],\n//  retries: 0 }\n```\n\n## FAQ:\n\n- Are there any order of execution guarantees when doing multiple requests without using callbacks?\n    - No, order of execution is up to NodeJS and the network. Requests run from a connection pool, meaning that if one request is delayed for some reason they'll arrive at the server out of order. Use callbacks (and maybe even a nice [async](https://github.com/caolan/async) callback handling library for convenient syntax) if ordering is important to prevent race conditions.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"node-etcd\",\n  \"version\": \"7.0.0\",\n  \"description\": \"etcd library for node.js (etcd v2 api)\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"http://github.com/stianeikeland/node-etcd.git\"\n  },\n  \"licenses\": [\n    {\n      \"type\": \"BSD 3-Clause\",\n      \"url\": \"http://opensource.org/licenses/BSD-3-Clause\"\n    }\n  ],\n  \"maintainers\": [\n    {\n      \"name\": \"Stian Eikeland\",\n      \"email\": \"stian@eikeland.se\",\n      \"web\": \"http://eikeland.se/\"\n    }\n  ],\n  \"contribuitors\": [\n    {\n      \"name\": \"Stian Eikeland\",\n      \"url\": \"https://github.com/stianeikeland\"\n    },\n    {\n      \"name\": \"Derek Brown\",\n      \"email\": \"mail@derektbrown.com\",\n      \"url\": \"derektbrown.com\"\n    },\n    {\n      \"name\": \"Jon Morehouse\",\n      \"email\": \"morehousej09@gmail.com\",\n      \"url\": \"https://github.com/jonmorehouse\"\n    },\n    {\n      \"name\": \"Bryan Rockwood\",\n      \"email\": \"bryan.rockwood@c2fo.com\",\n      \"url\": \"https://github.com/brockwood\"\n    }\n  ],\n  \"engines\": {\n    \"node\": \">= 6.0.0\"\n  },\n  \"main\": \"lib/index.js\",\n  \"scripts\": {\n    \"build\": \"node_modules/.bin/coffee --bare --compile --output lib/ src/*.coffee\",\n    \"prepublish\": \"node_modules/.bin/coffee --bare --compile --output lib/ src/*.coffee\",\n    \"test\": \"node_modules/.bin/mocha --require coffee-coverage/register-istanbul --compilers coffee:coffee-script/register test/\",\n    \"coverage\": \"node_modules/.bin/nyc npm test\",\n    \"watch\": \"node_modules/.bin/mocha --compilers coffee:coffee-script/register --watch\"\n  },\n  \"nyc\": {\n    \"check-coverage\": true,\n    \"include\": [\n      \"src/**/*.js\"\n    ]\n  },\n  \"dependencies\": {\n    \"deasync\": \"^0.1.13\",\n    \"lodash\": \"^4.17.10\",\n    \"request\": \"^2.87.0\",\n    \"url-parse\": \"^1.4.3\"\n  },\n  \"devDependencies\": {\n    \"coffee-coverage\": \"2.0.1\",\n    \"coffee-script\": \"1.12.7\",\n    \"mocha\": \"^4.0.0\",\n    \"nock\": \"^9.4.4\",\n    \"nyc\": \"^12.0.0\",\n    \"should\": \"11.2.1\",\n    \"simple-mock\": \"0.8.0\"\n  },\n  \"keywords\": [\n    \"etcd\",\n    \"raft\"\n  ]\n}\n"
  },
  {
    "path": "src/client.coffee",
    "content": "request     = require 'request'\ndeasync     = require 'deasync'\n_          = require 'lodash'\n\n\n# Default options for request library\ndefaultRequestOptions =\n  pool:\n    maxSockets: 100\n  followAllRedirects: true\n\ndefaultClientOptions =\n  maxRetries: 3\n\n\n# CancellationToken to abort a request\nclass CancellationToken\n  constructor: (@servers, @maxRetries, @retries = 0, @errors = []) ->\n    @aborted = false\n\n  setRequest: (req) =>\n    @req = req\n\n  isAborted: () =>\n    @aborted\n\n  abort: () =>\n    @aborted = true\n    @req.abort() if @req?\n\n  cancel: @::abort\n  wasAborted: @::isAborted\n\n# HTTP Client for connecting to etcd servers\nclass Client\n\n  constructor: (@hosts, @options, @sslopts) ->\n    @syncmsg = {}\n\n  execute: (method, options, callback) =>\n    opt = _.defaults (_.clone options), @options, defaultRequestOptions, { method: method }\n    opt.clientOptions = _.defaults opt.clientOptions, defaultClientOptions\n\n    servers = _.shuffle @hosts\n    token = new CancellationToken servers, opt.clientOptions.maxRetries\n    syncResp = @_multiserverHelper servers, opt, token, callback\n    if options.synchronous is true\n      return syncResp\n    else\n      return token\n\n\n  put: (options, callback) => @execute \"PUT\", options, callback\n  get: (options, callback) => @execute \"GET\", options, callback\n  post: (options, callback) => @execute \"POST\", options, callback\n  patch: (options, callback) => @execute \"PATCH\", options, callback\n  delete: (options, callback) => @execute \"DELETE\", options, callback\n\n  # Multiserver (cluster) executer\n  _multiserverHelper: (servers, options, token, callback) =>\n    host = _.first(servers)\n    options.url = \"#{host}#{options.path}\"\n\n    return if token.isAborted() # Aborted by user?\n\n    if not host? # No servers left?\n      return @_retry token, options, callback if @_shouldRetry token\n      return @_error token, callback\n\n    reqRespHandler = (err, resp, body) =>\n      return if token.isAborted()\n\n      if @_isHttpError err, resp\n        token.errors.push\n          server: host\n          httperror: err\n          httpstatus: resp?.statusCode\n          httpbody: resp?.body\n          response: resp\n          timestamp: new Date()\n\n        # Recurse:\n        return @_multiserverHelper _.drop(servers), options, token, callback\n\n      # Deliver response\n      @_handleResponse err, resp, body, callback\n\n    syncRespHandler = (err, body, headers) =>\n      options.syncdone = true\n      @syncmsg =\n        err: err\n        body: body\n        headers: headers\n    callback = syncRespHandler if options.synchronous is true\n\n    req = @_doRequest options, reqRespHandler\n    token.setRequest req\n\n    if options.synchronous is true and options.syncdone is undefined\n      options.syncdone = false\n      deasync.loopWhile(() => return !options.syncdone);\n      delete options.syncdone\n      return @syncmsg\n    else\n      return req\n\n\n  _doRequest: (options, reqRespHandler) ->\n    request options, reqRespHandler\n\n\n  _retry: (token, options, callback) =>\n    doRetry = () =>\n      @_multiserverHelper token.servers, options, token, callback\n    waitTime =  @_waitTime token.retries\n    token.retries += 1\n    setTimeout doRetry, waitTime\n\n\n  _waitTime: (retries) ->\n    return 1 if process.env.RUNNING_UNIT_TESTS is 'true'\n    ### !pragma no-coverage-next ###\n    return 100 * Math.pow 16, retries\n\n\n  _shouldRetry: (token) =>\n    token.retries < token.maxRetries and @_isPossibleLeaderElection token.errors\n\n\n  # All tries (all servers, all retries) failed\n  _error: (token, callback) ->\n    error = new Error 'All servers returned error'\n    error.errors = token.errors\n    error.retries = token.retries\n    callback error if callback\n\n\n  # If all servers reject connect or return raft error it's possible the\n  # cluster is in leader election mode.\n  _isPossibleLeaderElection: (errors) ->\n    checkError = (e) ->\n      e?.httperror?.code in ['ECONNREFUSED', 'ECONNRESET'] or\n        e?.httpbody?.errorCode in [300, 301] or\n        /Not current leader/.test e?.httpbody\n    errors? and _.every errors, checkError\n\n\n  _isHttpError: (err, resp) ->\n    err or (resp?.statusCode? and resp.statusCode >= 500)\n\n\n  _handleResponse: (err, resp, body, callback) ->\n    return if not callback?\n    if body?.errorCode? # http ok, but etcd gave us an error\n      error = new Error body?.message || 'Etcd error'\n      error.errorCode = body.errorCode\n      error.error = body\n      callback error, \"\", (resp?.headers or {})\n    else\n      callback null, body, (resp?.headers or {})\n\n\nexports = module.exports = Client\n"
  },
  {
    "path": "src/index.coffee",
    "content": "_          = require 'lodash'\nWatcher    = require './watcher'\nClient     = require './client'\nHttpsAgent = (require 'https').Agent\nURL        = require 'url-parse'\n\n# Etcd client for etcd protocol version 2\nclass Etcd\n\n  # Constructor, set etcd host and port.\n  # For https: provide {ca, crt, key} as sslopts.\n  # constructor: (hosts = [\"http://127.0.0.1:2379\"], options = {}) ->\n  constructor: (hosts = \"127.0.0.1:2379\", options = {}) ->\n    @hosts = @_cleanHostList hosts\n    @client = new Client(@hosts, options, null)\n\n  # Set key to value\n  # Usage:\n  #   .set(\"key\", \"value\", callback)\n  #   .set(\"key\", \"value\", {prevValue: \"oldvalue\"}, callback)\n  set: (key, value, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    opt = @_prepareOpts (\"keys/\" + @_stripSlashPrefix key), \"/v2\", value, options\n    @client.put opt, callback\n\n  # Set key to value synchronously\n  # Usage:\n  #   .set(\"key\", \"value\")\n  #   .set(\"key\", \"value\", {prevValue: \"oldvalue\"})\n  setSync: (key, value, options = {}) ->\n    this.set key, value, @_synchronousOpts(options)\n\n  # Get value of key\n  # Usage:\n  #   .get(\"key\", callback)\n  #   .get(\"key\", {recursive: true}, callback)\n  get: (key, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    opt = @_prepareOpts (\"keys/\" + @_stripSlashPrefix key), \"/v2\", null, options\n    @client.get opt, callback\n\n  # Synchronously get value of key\n  # Usage:\n  #   .get(\"key\")\n  #   .get(\"key\", {recursive: true})\n  getSync: (key, options = {}) ->\n    this.get key, @_synchronousOpts(options)\n\n  # Create a key (atomic in order)\n  # Usage:\n  #   .create(\"path\", \"value\", callback)\n  #   .create(\"path\", \"value\", options, callback)\n  create: (dir, value, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    opt = @_prepareOpts (\"keys/\" + @_stripSlashPrefix dir), \"/v2\", value, options\n    @client.post opt, callback\n\n  post: @::create\n\n\n  # Delete a key\n  # Usage:\n  #   .del(\"key\", callback)\n  #   .del(\"key\", {recursive: true}, callback)\n  #   .delete(\"key\", callback)\n  del: (key, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    opt = @_prepareOpts (\"keys/\" + @_stripSlashPrefix key), \"/v2\", null, options\n    @client.delete opt, callback\n\n  delete: @::del\n\n  # Synchronous delete a key\n  # Usage:\n  #   .del(\"key\")\n  #   .del(\"key\", {recursive: true}))\n  delSync: (key, options = {}) ->\n    this.del key, @_synchronousOpts(options)\n\n  # Make a directory\n  # Usage:\n  #   .mkdir(\"dir\", callback)\n  #   .mkdir(\"dir\", options, callback)\n  mkdir: (dir, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    options.dir = true\n    @set dir, null, options, callback\n\n  # Synchronously make a directory\n  # Usage:\n  #   .mkdir(\"dir\")\n  #   .mkdir(\"dir\", options)\n  mkdirSync: (dir, options = {}) ->\n    this.mkdir dir, @_synchronousOpts(options)\n\n  # Remove a directory\n  # Usage:\n  #   .rmdir(\"dir\", callback)\n  #   .rmdir(\"dir\", {recursive: true}, callback)\n  rmdir: (dir, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    options.dir = true\n    @del dir, options, callback\n\n  # Synchronously remove a directory\n  # Usage:\n  #   .rmdir(\"dir\")\n  #   .rmdir(\"dir\", {recursive: true})\n  rmdirSync: (dir, options = {}) ->\n    this.rmdir dir, @_synchronousOpts(options)\n\n  # Compare and swap value if unchanged\n  # Usage:\n  #   .compareAndSwap(\"key\", \"newValue\", \"oldValue\", callback)\n  #   .compareAndSwap(\"key\", \"newValue\", \"oldValue\", options, callback)\n  #   .testAndSet(\"key\", \"newValue\", \"oldValue\", options, callback)\n  compareAndSwap: (key, value, oldvalue, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    options ?= {}\n    options.prevValue = oldvalue\n\n    @set key, value, options, callback\n\n  testAndSet: @::compareAndSwap\n\n\n  # Compare and delete if value is unchanged\n  # Usage:\n  #   .compareAndDelete(\"key\", \"oldValue\", options, callback)\n  compareAndDelete: (key, oldvalue, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    options ?= {}\n    options.prevValue = oldvalue\n\n    @del key, options, callback\n\n  testAndDelete: @::compareAndDelete\n\n\n  # Execute a raw etcd query\n  # Where method is one of: PUT, GET, POST, PATCH, DELETE\n  #\n  # Usage:\n  #   .raw(\"METHOD\", \"path\", \"value\", options, callback)\n  #   .raw(\"GET\", \"v2/stats/leader\", null, {}, callback)\n  #   .raw(\"PUT\", \"v2/keys/key\", \"value\", {}, callback)\n  raw: (method, key, value, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    opt = @_prepareOpts key, \"\", value, options\n    @client.execute method, opt, callback\n\n\n  # Watch for value changes on a key\n  watch: (key, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    options ?= {}\n    options.wait = true\n\n    @get key, options, callback\n\n\n  # Watch for value changes on a key since a specific index\n  watchIndex: (key, index, options, callback) ->\n    [options, callback] = @_argParser options, callback\n    options ?= {}\n    options.waitIndex = index\n\n    @watch key, options, callback\n\n\n  # Returns an eventemitter that watches a key, emits 'change' on value change\n  # or 'reconnect' when trying to recover from errors.\n  watcher: (key, index = null, options = {}) =>\n    return new Watcher this, key, index, options\n\n\n  # Get the etcd cluster machines (server)\n  machines: (callback) ->\n    opt = @_prepareOpts \"keys/_etcd/machines\"\n    @client.get opt, callback\n\n\n  # List servers this etcd client will try to connect to\n  getHosts: () ->\n    _.clone(@hosts)\n\n\n  # Get the current cluster leader\n  leader: (callback) ->\n    opt = @_prepareOpts \"leader\"\n    @client.get opt, callback\n\n\n  # Get statistics about the leader\n  leaderStats: (callback) ->\n    opt = @_prepareOpts \"stats/leader\"\n    @client.get opt, callback\n\n\n  # Get statistics about the currently connected entity\n  selfStats: (callback) ->\n    opt = @_prepareOpts \"stats/self\"\n    @client.get opt, callback\n\n\n  # Get version of etcd\n  version: (callback) ->\n    opt = @_prepareOpts \"version\", \"\"\n    @client.get opt, callback\n\n\n  # Strip the prefix slash if set\n  _stripSlashPrefix: (key) ->\n    key.replace /^\\//, ''\n\n  _synchronousOpts: (options) ->\n    _.extend {}, options, { synchronous: true }\n\n  # Prepare request options\n  _prepareOpts: (path, apiVersion = \"/v2\", value = null, allOpts = {}) ->\n    # serverprotocol = if @sslopts? then \"https\" else \"http\"\n\n    queryString = _.omit allOpts, 'maxRetries', 'synchronous'\n\n    clientOptions = _.pick allOpts, 'maxRetries'\n\n    opt = {\n      path: \"#{apiVersion}/#{path}\"\n      # serverprotocol: serverprotocol\n      json: true\n      qs: queryString\n      clientOptions: clientOptions\n      synchronous: allOpts.synchronous\n      form: { value: value } if value?\n      agentOptions: @sslopts if @sslopts?\n    }\n\n\n  # Swap callback and options if no options was given.\n  _argParser: (options = {}, callback) ->\n    if typeof options is 'function'\n      [{}, options]\n    else\n      [options, callback]\n\n  # Make sure hosts is a list, make sure all have protocol added\n  # defaults to http and remove trailing slash\n  _cleanHostList: (hosts) ->\n    hostlist = if _.isArray(hosts) then hosts else [hosts]\n    hostlist.map (host) ->\n      host = 'http://' + host if host.indexOf('http') is -1\n      url = new URL(host)\n      url.href.replace /\\/$/, \"\" # Trailing slash\n\n\nexports = module.exports = Etcd\n"
  },
  {
    "path": "src/watcher.coffee",
    "content": "{EventEmitter} = require 'events'\n\n# A eventemitter for watching changes on a given key for etcd.\n# Emits:\n#   'change' - on value change\n#   'reconnect' - on errors/timeouts\n#   '<etcd action>' - the etcd action that triggered the watcher (set, delete, etc)\n#\n#   Automatically reconnects and backs off on errors.\n#\nclass Watcher extends EventEmitter\n\n  constructor: (@etcd, @key, @index = null, @options = {}) ->\n    @stopped = false\n    @retryAttempts = 0\n    @_watch()\n\n\n  stop: () =>\n    @stopped = true\n    @request.abort()\n    @emit 'stop', \"Watcher for '#{@key}' aborted.\"\n\n\n  _watch: () =>\n    if @index is null\n      @request = @etcd.watch @key, @options, @_respHandler\n    else\n      @request = @etcd.watchIndex @key, @index, @options, @_respHandler\n\n\n  _error: (err) =>\n    # Something went wrong, most likely on the network,\n    # maybe disconnected, or similar.\n    error = new Error 'Connection error, reconnecting.'\n    error.error = err\n    error.reconnectCount = @retryAttempts\n    @emit 'reconnect', error\n    @_retry()\n\n\n  _missingValue: (headers) =>\n    # Etcd sent us an empty response, it seems to do this when\n    # it times out a watching client.\n    error = new Error 'Etcd timed out watcher, reconnecting.'\n    error.headers = headers\n    @retryAttempts = 0\n    @emit 'reconnect', error\n    @_watch()\n\n\n  _valueChanged: (val, headers) =>\n    # Valid data received, value was changed.\n    @retryAttempts = 0\n    @index = val.node.modifiedIndex + 1\n    @emit 'change', val, headers\n    @emit val.action, val, headers if val.action?\n    @_watch()\n\n\n  _unexpectedData: (val, headers) =>\n    # Unexpected data received\n    error = new Error 'Received unexpected response'\n    error.response = val;\n    @emit 'error', error\n    @_retry()\n\n\n  _resync: (err) =>\n    @index = err.error.index\n    @retryAttempts = 0\n    @emit 'resync', err\n    @_watch()\n\n\n  _respHandler: (err, val, headers) =>\n    return if @stopped\n\n    if err?.errorCode is 401 and err.error?.index?\n      @_resync err\n    else if err\n      @_error err\n    else if headers?['x-etcd-index']? and not val?\n      @_missingValue headers\n    else if val?.node?.modifiedIndex?\n      @_valueChanged val, headers\n    else\n      @_unexpectedData val, headers\n\n\n  _retry: () =>\n    timeout = (Math.pow(2,@retryAttempts)*300) + (Math.round(Math.random() * 1000))\n    setTimeout @_watch, timeout\n    @retryAttempts++\n\n\nexports = module.exports = Watcher\n"
  },
  {
    "path": "test/client.coffee",
    "content": "should = require 'should'\nnock = require 'nock'\nClient = require '../src/client.coffee'\n\ndescribe 'Client', ->\n\n  client = new Client\n\n  # TODO: Fix these tests.. I've been stupid and tested implementation details, not api..\n  describe '#_handleResponse()', ->\n\n    # it 'fails on http error', ->\n    #   client._handleResponse 'error', '', '', (err) ->\n    #     err.error.should.equal 'error'\n\n    it 'should use error objects for errors', ->\n      client._handleResponse null, 'resp', {errorCode: 100}, (err) ->\n        err.should.be.an.instanceOf Error\n\n    it 'fails on etcd error', ->\n      client._handleResponse null, \"resp\", {errorCode: 100}, (err) ->\n        err.error.errorCode.should.equal 100\n\n    it 'succeeds on no errors', ->\n      client._handleResponse null, \"resp\", \"data\", (_, val) ->\n        val.should.equal \"data\"\n\n    it 'passthrough any headers set in response', ->\n      client._handleResponse null, {headers: {a: \"b\"}}, \"data\", (e, v, headers) ->\n        headers.a.should.equal \"b\"\n\n    it 'sets empty object as header if none received', ->\n      client._handleResponse null, null, \"data\", (e, v, headers) ->\n        headers.should.be.an.Object\n        Object.keys(headers).should.be.empty\n\n    it 'should not fail if callback is not given', ->\n      client._handleResponse null, 'resp', 'body'\n"
  },
  {
    "path": "test/index.coffee",
    "content": "should = require 'should'\nnock = require 'nock'\nsimple = require 'simple-mock'\nEtcd = require '../src/index.coffee'\n\n\n\n# Set env var to skip timeouts\nprocess.env.RUNNING_UNIT_TESTS = true\n\n# Helpers\n\ngetNock = (host = 'http://127.0.0.1:2379') ->\n  nock host\n\n\nbeforeEach () ->\n  nock.cleanAll()\n\n# Tests for utility functions\n\ndescribe 'Utility', ->\n\n  etcd = new Etcd\n\n  describe '#_stripSlashPrefix()', ->\n    it 'should strip prefix-/ from key', ->\n      etcd._stripSlashPrefix(\"/key/\").should.equal(\"key/\")\n      etcd._stripSlashPrefix(\"key/\").should.equal(\"key/\")\n\n  describe '#_prepareOpts()', ->\n    it 'should return default request options', ->\n      etcd._prepareOpts('keypath/key').should.containEql {\n        json: true\n        path: '/v2/keypath/key'\n      }\n\n\ndescribe 'Connecting', ->\n\n  mock = (host = 'http://127.0.0.1:2379/') ->\n    nock(host)\n      .get('/v2/keys/key')\n      .reply(200, '{\"action\":\"GET\",\"key\":\"/key\",\"value\":\"value\",\"index\":1}')\n\n  it 'should support empty constructor (localhost:2379)', (done) ->\n    etcd = new Etcd\n    m = mock()\n    etcd.get 'key', (err, val) ->\n      m.isDone().should.be.true\n      done err, val\n\n  it 'should support string as connect host', (done) ->\n    etcd = new Etcd \"testhost.com:4009\"\n    m = mock(\"http://testhost.com:4009\")\n    etcd.get 'key', (err, val) ->\n      m.isDone().should.be.true\n      done err, val\n\n  it 'should support string prefixed by http:// as host', (done) ->\n    etcd = new Etcd \"http://testhost.com:4009\"\n    m = mock(\"http://testhost.com:4009\")\n    etcd.get 'key', (err, val) ->\n      m.isDone().should.be.true\n      done err, val\n\n  it 'should support string postfixed by / as host', (done) ->\n    etcd = new Etcd \"http://testhost.com:4009/\"\n    m = mock(\"http://testhost.com:4009\")\n    etcd.get 'key', (err, val) ->\n      m.isDone().should.be.true\n      done err, val\n\n  it 'should support array of strings as host', (done) ->\n    etcd = new Etcd [\"http://testhost.com:4009\"]\n    m = mock(\"http://testhost.com:4009\")\n    etcd.get 'key', (err, val) ->\n      m.isDone().should.be.true\n      done err, val\n\n  it 'should support https strings', (done) ->\n    etcd = new Etcd [\"https://testhost.com:1000\"]\n    m = mock(\"https://testhost.com:1000\")\n    etcd.get 'key', (err, val) ->\n      m.isDone().should.be.true\n      done err, val\n\n\ndescribe 'Basic auth', ->\n\n  it 'should support basic auth', (done) ->\n    auth =\n      user: \"username\"\n      pass: \"password\"\n    etcd = new Etcd \"localhost:2379\", { auth: auth }\n\n    m = nock(\"http://localhost:2379\")\n      .get(\"/v2/keys/key\")\n      .basicAuth(\n        user: \"username\",\n        pass: \"password\"\n      )\n      .reply(200)\n\n    etcd.get 'key', (err, val) ->\n      m.isDone().should.be.true\n      done err, val\n\n\ndescribe 'Basic functions', ->\n\n  etcd = new Etcd\n\n  checkVal = (done) ->\n    (err, val) ->\n      val.should.containEql { value: \"value\" }\n      done err, val\n\n  describe '#get()', ->\n    it 'should return entry from etcd', (done) ->\n      getNock()\n        .get('/v2/keys/key')\n        .reply(200, '{\"action\":\"GET\",\"key\":\"/key\",\"value\":\"value\",\"index\":1}')\n      etcd.get 'key', checkVal done\n\n    it 'should send options to etcd as request url', (done) ->\n      getNock()\n        .get('/v2/keys/key?recursive=true')\n        .reply(200, '{\"action\":\"GET\",\"key\":\"/key\",\"value\":\"value\",\"index\":1}')\n      etcd.get 'key', { recursive: true }, checkVal done\n\n    it 'should callback with error on error', (done) ->\n      getNock()\n        .get('/v2/keys/key')\n        .reply(404, {\"errorCode\": 100, \"message\": \"Key not found\"})\n      etcd.get 'key', (err, val) ->\n        err.should.be.instanceOf Error\n        err.error.errorCode.should.equal 100\n        err.message.should.equal \"Key not found\"\n        done()\n\n  describe '#getSync()', ->\n    it 'should synchronously return entry from etcd', (done) ->\n      getNock()\n        .get('/v2/keys/key')\n        .reply(200, '{\"action\":\"GET\",\"key\":\"/key\",\"value\":\"value\",\"index\":1}')\n      val = etcd.getSync 'key'\n      doneCheck = checkVal done\n      doneCheck val.err, val.body\n\n    it 'should synchronously return with error on error', (done) ->\n      getNock()\n        .get('/v2/keys/key')\n        .reply(404, {\"errorCode\": 100, \"message\": \"Key not found\"})\n      val = etcd.getSync 'key'\n      val.err.should.be.instanceOf Error\n      val.err.error.errorCode.should.equal 100\n      val.err.message.should.equal \"Key not found\"\n      done()\n\n\n  describe '#set()', ->\n    it 'should put to etcd', (done) ->\n      getNock()\n        .put('/v2/keys/key', { value: \"value\" })\n        .reply(200, '{\"action\":\"SET\",\"key\":\"/key\",\"prevValue\":\"value\",\"value\":\"value\",\"index\":1}')\n      etcd.set 'key', 'value', checkVal done\n\n    it 'should send options to etcd as request url', (done) ->\n      getNock()\n        .put('/v2/keys/key?prevValue=oldvalue', { value: \"value\"})\n        .reply(200, '{\"action\":\"SET\",\"key\":\"/key\",\"prevValue\":\"oldvalue\",\"value\":\"value\",\"index\":1}')\n      etcd.set 'key', 'value', { prevValue: \"oldvalue\" }, checkVal done\n\n    it 'should follow 307 redirects', (done) ->\n      (nock 'http://127.0.0.1:4002')\n        .put('/v2/keys/key', { value: \"value\" })\n        .reply(200, '{\"action\":\"SET\",\"key\":\"/key\",\"prevValue\":\"value\",\"value\":\"value\",\"index\":1}')\n\n      (nock 'http://127.0.0.1:2379')\n        .put('/v2/keys/key', { value: \"value\" })\n        .reply(307, \"\", { location: \"http://127.0.0.1:4002/v2/keys/key\" })\n\n      etcd.set 'key', 'value', checkVal done\n\n  describe '#setSync()', ->\n    it 'should synchronously put to etcd', (done) ->\n      getNock()\n        .put('/v2/keys/key', { value: \"value\" })\n        .reply(200, '{\"action\":\"SET\",\"key\":\"/key\",\"prevValue\":\"value\",\"value\":\"value\",\"index\":1}')\n      val = etcd.setSync 'key', 'value'\n      doneCheck = checkVal done\n      doneCheck val.err, val.body\n\n  describe '#create()', ->\n    it 'should post value to dir', (done) ->\n      getNock()\n        .post('/v2/keys/dir', { value: \"value\" })\n        .reply(200, '{\"action\":\"create\", \"node\":{\"key\":\"/dir/2\"}}')\n\n      etcd.create 'dir', 'value', (err, val) ->\n        val.should.containEql { action: \"create\" }\n        done err, val\n\n  describe '#post()', ->\n    it 'should post value to key', (done) ->\n      getNock().post('/v2/keys/key', { value: \"value\" }).reply(200)\n      etcd.post 'key', 'value', done\n\n\n  describe '#compareAndSwap()', ->\n    it 'should set using prevValue', (done) ->\n      getNock()\n        .put('/v2/keys/key?prevValue=oldvalue', { value: \"value\"})\n        .reply(200, '{\"action\":\"SET\",\"key\":\"/key\",\"prevValue\":\"oldvalue\",\"value\":\"value\",\"index\":1}')\n      etcd.compareAndSwap 'key', 'value', 'oldvalue', checkVal done\n\n    it 'has alias testAndSet', ->\n      etcd.testAndSet.should.equal etcd.testAndSet\n\n  describe '#compareAndDelete', ->\n    it 'should delete using prevValue', (done) ->\n      getNock().delete('/v2/keys/key?prevValue=oldvalue').reply(200)\n      etcd.compareAndDelete 'key', 'oldvalue', done\n\n    it 'has alias testAndDelete', ->\n      etcd.compareAndDelete.should.equal etcd.testAndDelete\n\n  describe '#mkdir()', ->\n    it 'should create directory', (done) ->\n      getNock()\n        .put('/v2/keys/key?dir=true')\n        .reply(200, '{\"action\":\"create\",\"node\":{\"key\":\"/key\",\"dir\":true,\"modifiedIndex\":1,\"createdIndex\":1}}')\n      etcd.mkdir 'key', (err, val) ->\n        val.should.containEql {action: \"create\"}\n        val.node.should.containEql {key: \"/key\"}\n        val.node.should.containEql {dir: true}\n        done()\n\n    it 'should work when no options or callback given - bug #56', (done) ->\n      replybody = '{\"action\":\"create\", \"node\":{\"key\":\"/key\",\"dir\":true,\"modifiedIndex\":1,\"createdIndex\":1}}'\n      getNock()\n        .put('/v2/keys/key?dir=true')\n        .reply(200, (uri, req, cb) ->\n          cb(replybody)\n          done()\n        )\n      etcd.mkdir 'key'\n\n\n  describe '#mkdirSync()', ->\n    it 'should synchronously create directory', (done) ->\n      getNock()\n        .put('/v2/keys/key?dir=true')\n        .reply(200, '{\"action\":\"create\",\"node\":{\"key\":\"/key\",\"dir\":true,\"modifiedIndex\":1,\"createdIndex\":1}}')\n      val = etcd.mkdirSync 'key'\n      val.body.should.containEql {action: \"create\"}\n      val.body.node.should.containEql {key: \"/key\"}\n      val.body.node.should.containEql {dir: true}\n      done()\n\n  describe '#rmdir()', ->\n    it 'should remove directory', (done) ->\n      getNock().delete('/v2/keys/key?dir=true').reply(200)\n      etcd.rmdir 'key', done\n\n  describe '#rmdirSync()', ->\n    it 'should synchronously remove directory', (done) ->\n      getNock().delete('/v2/keys/key?dir=true')\n        .reply(200, '{\"action\":\"delete\",\"node\":{\"key\":\"/key\",\"dir\":true,\"modifiedIndex\":1,\"createdIndex\":3}}')\n      val = etcd.rmdirSync 'key'\n      val.body.should.containEql {action: \"delete\"}\n      val.body.node.should.containEql {dir: true}\n      done()\n\n  describe '#del()', ->\n    it 'should delete a given key in etcd', (done) ->\n      getNock().delete('/v2/keys/key').reply(200)\n      etcd.del 'key', done\n\n  describe '#delSync()', ->\n    it 'should synchronously delete a given key in etcd', (done) ->\n      getNock().delete('/v2/keys/key2').reply(200, '{\"action\":\"delete\"}')\n      val = etcd.delSync 'key2'\n      val.body.should.containEql {action: \"delete\"}\n      done()\n\n  describe '#watch()', ->\n    it 'should do a get with wait=true', (done) ->\n      getNock()\n        .get('/v2/keys/key?wait=true')\n        .reply(200, '{\"action\":\"set\",\"key\":\"/key\",\"value\":\"value\",\"modifiedIndex\":7}')\n      etcd.watch 'key', checkVal done\n\n  describe '#watchIndex()', ->\n    it 'should do a get with wait=true and waitIndex=x', (done) ->\n      getNock()\n        .get('/v2/keys/key?waitIndex=1&wait=true')\n        .reply(200, '{\"action\":\"set\",\"key\":\"/key\",\"value\":\"value\",\"modifiedIndex\":7}')\n      etcd.watchIndex 'key', 1, checkVal done\n\n  describe '#raw()', ->\n    it 'should use provided method', (done) ->\n      getNock().patch('/key').reply(200, 'ok')\n      etcd.raw 'PATCH', 'key', null, {}, done\n\n    it 'should send provided value', (done) ->\n      getNock().post('/key', { value: \"value\" }).reply(200, 'ok')\n      etcd.raw 'POST', 'key', \"value\", {}, done\n\n    it 'should call cb on value from etcd', (done) ->\n      getNock().get('/key').reply(200, 'value')\n      etcd.raw 'GET', 'key', null, {}, (err, val) ->\n        val.should.equal 'value'\n        done err, val\n\n  describe '#machines()', ->\n    it 'should ask etcd for connected machines', (done) ->\n      getNock().get('/v2/keys/_etcd/machines').reply(200, '{\"value\":\"value\"}')\n      etcd.machines checkVal done\n\n  describe '#leader()', ->\n    it 'should ask etcd for leader', (done) ->\n      getNock().get('/v2/leader').reply(200, '{\"value\":\"value\"}')\n      etcd.leader checkVal done\n\n  describe '#leaderStats()', ->\n    it 'should ask etcd for statistics for leader', (done) ->\n      getNock().get('/v2/stats/leader').reply(200, '{\"value\":\"value\"}')\n      etcd.leaderStats checkVal done\n\n  describe '#selfStats()', ->\n    it 'should ask etcd for statistics for connected server', (done) ->\n      getNock().get('/v2/stats/self').reply(200, '{\"value\":\"value\"}')\n      etcd.selfStats checkVal done\n\n  describe '#version()', ->\n    it 'should ask etcd for version', (done) ->\n      getNock().get('/version').reply(200, 'etcd v0.1.0-8-gaad1626')\n      etcd.version (err, val) ->\n        val.should.equal 'etcd v0.1.0-8-gaad1626'\n        done err, val\n\n\ndescribe 'SSL support', ->\n\n  beforeEach () ->\n    nock.cleanAll()\n\n  it 'passes ssl options to request lib', (done) ->\n    etcdssl = new Etcd 'https://localhost:4009', {ca: 'myca', cert: 'mycert', key: 'mykey'}\n    simple.mock(etcdssl.client, \"_doRequest\").callFn (options) ->\n      options.should.containEql\n        ca: 'myca'\n        cert: 'mycert'\n        key: 'mykey'\n      done()\n\n    etcdssl.get 'key'\n\n\ndescribe 'Cancellation Token', ->\n\n  beforeEach () ->\n    nock.cleanAll()\n\n  it 'should return token on request', ->\n    getNock().get('/version').reply(200, 'etcd v0.1.0-8-gaad1626')\n    etcd = new Etcd\n    token = etcd.version()\n    token.abort.should.be.a.function\n    token.isAborted().should.be.false\n\n  it 'should stop execution on abort', (done) ->\n    http = getNock()\n      .log(console.log)\n      .get('/v2/keys/key')\n      .reply(200, '{\"action\":\"GET\",\"key\":\"/key\",\"value\":\"value\",\"index\":1}')\n    etcd = new Etcd '127.0.0.1', 2379\n\n    # This sucks a bit.. are there any better way of checking that a callback\n    # does not happen?\n    token = etcd.get \"key\", () -> throw new Error \"Call should have been aborted\"\n    token.abort()\n    setTimeout done, 50\n\n\ndescribe 'Multiserver/Cluster support', ->\n\n  beforeEach () ->\n    nock.cleanAll()\n\n  it 'should accept list of servers in constructor', ->\n    etcd = new Etcd ['localhost:2379', 'localhost:4002']\n    etcd.getHosts().should.eql ['http://localhost:2379', 'http://localhost:4002']\n\n  it 'should try next server in list on http error', (done) ->\n    path = '/v2/keys/foo'\n    response = '{\"action\":\"GET\",\"key\":\"/key\",\"value\":\"value\",\"index\":1}'\n\n    handler = (uri) ->\n      nock.cleanAll()\n      getNock('http://s1').get(path).reply(200, response)\n      getNock('http://s2').get(path).reply(200, response)\n      return {}\n\n    getNock('http://s1').get(path).reply(500, handler)\n    getNock('http://s2').get(path).reply(500, handler)\n\n    etcd = new Etcd ['s1', 's2']\n    etcd.get 'foo', (err, res) ->\n      res.value.should.eql 'value'\n      done()\n\n\n  it 'should callback error if all servers failed', (done) ->\n    path = '/v2/keys/foo'\n    getNock('http://s1').get(path).reply(500, {})\n    getNock('http://s2').get(path).reply(500, {})\n\n    etcd = new Etcd ['s1', 's2']\n    etcd.get 'foo', (err, res) ->\n      err.should.be.an.instanceOf Error\n      err.errors.should.have.lengthOf 2\n      done()\n\n\n  describe 'when cluster is doing leader elect', () ->\n\n    it 'should retry on connection refused', (done) ->\n      etcd = new Etcd (\"localhost:#{p}\" for p in [47187, 47188, 47189])\n      token = etcd.set 'a', 'b', (err) ->\n        err.errors.length.should.be.exactly 12\n        token.errors.length.should.be.exactly 12\n        token.retries.should.be.exactly 3\n        done()\n\n    it 'should allow maxRetries to control number of retries', (done) ->\n      etcd = new Etcd (\"localhost:#{p}\" for p in [47187, 47188, 47189])\n      token = etcd.set 'a', 'b', { maxRetries: 1 }, (err) ->\n        err.errors.length.should.be.exactly 6\n        token.retries.should.be.exactly 1\n        done()\n"
  },
  {
    "path": "test/watcher.coffee",
    "content": "should = require 'should'\nnock = require 'nock'\n\nEtcd = require '../src/index'\nWatcher = require '../src/watcher.coffee'\n\nclass FakeEtcd\n  constructor: ->\n    @stopped = false\n    @cb = ->\n\n  abort: -> {abort: => @stopped = true}\n\n  watch: (key, options, cb) ->\n    key.should.equal 'key'\n    @cb = cb\n    return @abort()\n\n  watchIndex: (key, index, options, cb) ->\n    key.should.equal 'key'\n    @cb = cb\n    return @abort()\n\n  change: (err, val, header = {}) ->\n    @cb err, val, header\n\n\ndescribe 'Watcher', ->\n  it 'should emit change on watch change', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n\n    w.on 'change', (val) ->\n      val.should.containEql { node: { modifiedIndex: 0 } }\n      done()\n\n    etcd.change null, { node: { modifiedIndex: 0 } }\n\n  it 'should emit reconnect event on error', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n\n    w.on 'reconnect', (err) ->\n      err.should.containEql { error: \"error\" }\n      done()\n\n    etcd.change \"error\", null\n\n  it 'should emit error if received content is invalid', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n    w.on 'error', -> done()\n\n    etcd.change null, 'invalid content', {}\n\n  it 'should emit error object on error', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n    w.on 'error', (err) ->\n      err.should.be.an.instanceOf Error\n      done()\n\n    etcd.change null, 'invalid content', {}\n\n  it 'should use provided options', (done) ->\n    etcd = new FakeEtcd\n\n    etcd.watch = (key, opt, cb) ->\n      opt.should.containEql { recursive: true }\n      done()\n\n    w = new Watcher etcd, 'key', null, { recursive: true }\n\n  it 'should emit action on event', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n    w.on 'set', (res) -> done()\n\n    etcd.change null, { action: 'set', node: { key: '/key', value: 'value', modifiedIndex: 1, createdIndex: 1 } }\n\n  it 'should reconnect (call watch again) on error', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n\n    etcd.watch = (key, cb) ->\n      w.retryAttempts.should.equal 1\n      done()\n\n    etcd.change \"error\", null\n\n  it 'should reconnect (watch again) on empty body (etcd timeout)', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n\n    w.on 'reconnect', () ->\n      done()\n\n    etcd.change null, null, {'x-etcd-index': 123}\n\n  it 'should call watch on next index after getting change', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n\n    i = 5\n\n    etcd.watchIndex = (key, index, cb) ->\n      index.should.equal i + 1\n      done()\n\n    etcd.change null, { node: { modifiedIndex: i } }\n\n  it 'should abort request when stop is called', ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n\n    w.stop()\n    etcd.stopped.should.be.true\n\n  it 'should emit stop when stopped', (done) ->\n    etcd = new FakeEtcd\n    w = new Watcher etcd, 'key'\n\n    w.on 'stop', -> done()\n    w.stop()\n\n\ndescribe 'Watcher resync', ->\n\n  getNock = ->\n    nock 'http://127.0.0.1:2379'\n\n  it 'should resync if index is outdated and cleared', (done) ->\n    getNock()\n      .get('/v2/keys/key?waitIndex=0&wait=true')\n      .reply(401, {\n        errorCode: 401\n        message: \"The event in requested index is outdated and cleared\"\n        cause: \"the requested history has been cleared [1007/4]\"\n        index: 2006\n        })\n      .get('/v2/keys/key?waitIndex=2006&wait=true')\n      .reply(200, {\n          action:\"set\"\n          node:\n            key: \"/key\"\n            value: \"banan\"\n            modifiedIndex: 2013\n            createdIndex: 2013\n          prevNode:\n            key: \"/key\"\n            value: \"2\"\n            modifiedIndex: 5\n            createdIndex: 5\n          })\n      .get('/v2/keys/key?waitIndex=2014&wait=true').reply(200,{})\n\n    w = new Watcher (new Etcd), 'key', 0\n    w.on 'change', (res) ->\n      res.node.value.should.equal 'banan'\n      w.stop()\n      done()\n"
  }
]