[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n# global file settings\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "'use strict';\nvar eslint = exports;\n\neslint.env = {\n  node: true,\n  commonjs: true,\n};\n\neslint.extends = [\n  'eslint:recommended',\n  'llama',\n];\n\neslint.rules = {\n  'no-var': 'off',\n  'prefer-template': 'off',\n};\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\n.DS_Store\n*.min.js*\nnotes.js\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n - node\n\nscript: npm run all-tests\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v1.1.0\n### Added\n- New `Client#matches` method accepts platform queries like `ClientList#filter`, returning a boolean of whether it matches.\n\n## v1.0.0\n### Changed\n- Several breaking changes in `panic-client` v1.0, reference [their changelog](https://github.com/gundb/panic-client/blob/master/CHANGELOG.md#v100) for differences.\n\n### Removed\n- `.len()` has been permanently removed (was previously under deprecation notice).\n\n### Added\n- Each client object held within a `ClientList` is capable of dispatching it's own jobs, independently of a list.\n\n## v0.4.1\n### Added\n- Method `.atLeast`, useful for pausing until a minimum number of clients join.\n\n## v0.4.0\n### Added\n- Upgraded to panic-client `v0.3` (brings `.get` and `.set` client methods).\n\n## v0.3.0\n### Changed\n- position of the `done` changed to the first parameter. `this` context is no longer passed.\n- `export vars` is no longer enabled by default, but opt-in using the `{ '@scope': true }` property.\n\n### Removed\n- `.len()` has been deprecated in favor of `.length`.\n\n### Added\n- Lazily loads the panic-client bundle through `panic.client` property.\n- Subclassing support by chains instantiating `this.constructor`.\n- Added `.chain` method which ensures the proper class is called.\n\n## v0.2.4\n### Fixed\n- Set the `constructor` property on the ClientList prototype.\n\n## v0.2.3\n### Fixed\n- Removed the `Function.prototype.toJSON` extension.\n\n## v0.2.2\n### Fixed\n- `.excluding()` did not listen for remove events on exclusion lists. If a client was removed yet still connected, it wouldn't make it into the exclusion set.\n- The \"add\" event would fire each time you add the same client, even though it was already contained.\n\n### Added\n- New `clients.pluck(Number)` method will create a new list constrained to a maximum number of clients.\n\n## v0.2.1\n### Added\n- The `ClientList` constructor to the `panic` object.\n- The `ClientList` constructor now accepts an array of smaller lists to pull from.\n\n## v0.2.0\n### Changed\n- `panic.serve` has been renamed to `panic.server`.\n- `panic.server` accepts an `http.Server` instance instead of an options object.\n- The server no longer automatically listens on a port.\n- `panic.server` returns the server, not the options object.\n- `panic.js` is no longer served on the root route, only from `/panic.js`.\n\n## v0.1.0\nFirst minor release.\n"
  },
  {
    "path": "README.md",
    "content": "<img alt='PANIC' width='500px' src='./panic-logo.jpg'></img>\n\n[![Travis branch](https://img.shields.io/travis/PsychoLlama/panic-server/master.svg?style=flat-square)](https://travis-ci.org/PsychoLlama/panic-server)\n[![npm](https://img.shields.io/npm/dt/panic-server.svg?style=flat-square)](https://www.npmjs.com/package/panic-server)\n[![npm](https://img.shields.io/npm/v/panic-server.svg?style=flat-square)](https://www.npmjs.com/package/panic-server)\n[![Gitter](https://img.shields.io/gitter/room/amark/gun.svg?style=flat-square)](https://gitter.im/amark/gun)\n\n> **TL;DR:**<br />\nA remote control for browsers and servers.\n\nPanic is an end-to-end testing framework, designed specifically for distributed systems and collaborative apps.\n\n## Why\nAt [gunDB](http://gun.js.org/), we're building a real-time, distributed JS database.\n\nWe needed a testing tool that could simulate complex scenarios, and programmatically report success or failure. For instance, how would you write this test?\n\n1. Start a server.\n2. Spin up two browsers, each syncing with the server.\n3. Save data on just one browser.\n4. Assert that it replicated to the other.\n\nAnd that's just browser to browser replication. What about simulating app failures?\n\n1. Start a server.\n2. Start two browsers, each synced with the server.\n3. Save some initial data.\n4. Kill the server, and erase all it's data.\n5. Make conflicting edits on the browsers.\n6. Start the server again.\n7. Assert both conflicting browsers converge on a value.\n\nThat's why we built panic.\n\n## How it works\nWell, there are two repos: `panic-server`, and [`panic-client`](https://github.com/gundb/panic-client/).\n\nYou'll start a panic server (sometimes called a coordinator), then you'll connect to it from panic clients.\n\nLoading the client software into a browser or Node.js process exposes the mother of all XSS vulnerabilities. Connect it to the coordinator, then it'll have full control over your process.\n\nThat's where panic gets its power. It remotely controls every client and server in your app.\n\nNow obviously, due to those vulnerabilities, you wouldn't want panic in user-facing code. Hang on, lemme make this bigger...\n\n### DO NOT USE PANIC IN USER-FACING CODE.\n\nWell, unless running `eval` on arbitrary code is an app feature.\n\nCool, so we've covered the \"why\" and the \"how it works\". Now onto the API!\n\n## API\n> If you're massively bored by documentation and just wanna copy/paste working code, well, [happy birthday](#scaffolding).\n\n### Clients\nA client is an instance of `panic.Client`, and represents another computer or process connected through websockets.\n\n#### Properties\nEvery client has some properties you can use, although you probably won't need to.\n\n##### `.socket`\nReferences the [`socket.io`](http://socket.io/) interface connecting you to the other process. Unless you're developing a plugin, you'll probably never need to use this.\n\n##### `.platform`\nThis references the [`platform.js`](https://github.com/bestiejs/platform.js/) object. It's sent as part of the handshake by [`panic-client`](https://github.com/gundb/panic-client/).\n\n#### Methods\nRight now there's only one, but it's where the whole party's at!\n\n##### `.run()`\nSends code to execute on the client.\n\nIt takes two parameters:\n\n1. The function to execute remotely.\n2. Optionally, some variables to send with it.\n\nThis is by far the weirdest part of panic. Your function is run, but not in the same context, not even in the same process, maybe a different JS environment and OS entirely.\n\nIt's stringified, sent to the client, then evaluated in a special job context.\n\n```js\nconsole.log('This runs in your process.')\n\nclient.run(function () {\n  console.log(\"This doesn't.\")\n})\n```\n\nSome of the common confusion points:\n\n- You can't use any variables outside your function.\n- That includes other functions.\n- If the client is a browser, obviously you won't have `require` available.\n- The client might have different packages or package versions installed.\n\nBottom line, your code is run on the client, not where you wrote it.\n\nInside the function, you've got access to the whole [`panic-client` API](https://github.com/gundb/panic-client/#api).\n\nBecause your function can't see any local scope variables, anything the function depends on needs to be sent with it. That's our second parameter, `props`.\n\n**Example**\n```js\nvar clientPort = 8085\n\nclient.run(function () {\n  var http = require('http')\n  var server = new http.Server()\n\n  // The variable you sent.\n  var port = this.props.port\n\n  server.listen(port)\n}, {\n\n  // Sends the local variable\n  // as `props.port`.\n  port: clientPort\n})\n```\n\nCareful though, any props you send have to be JSON compatible. It'll crash if you try to send a circular reference.\n\n###### Return values\nSo, we've showed how values can be sent to the client, but what about getting values back?\n\nPrepare yourself, this is pretty awesome.\n\n`.run` returns a promise. Any return value from the client will be the resolve value. For instance:\n\n```js\nclient.run(function () {\n  var ip = require('ip')\n  return ip.address()\n}).then(function (ip) {\n\n  // The address of the other machine\n  console.log(ip)\n})\n```\n\n> For more details on return values and edge cases, read the panic client [API](https://github.com/gundb/panic-client/#api).\n\nSo, if one of your clients is a node process...\n\n```js\nfunction sh () {\n  var child = require('child_process')\n  var spawn = child.spawnSync\n\n  var cmd = this.props.cmd\n  var args = this.props.args\n\n  var result = spawn(cmd, args || [])\n\n  return result.stdout\n}\n\nclient.run(sh, {\n  cmd: 'ls',\n  args: ['-lah']\n}).then(function (dir) {\n  var output = dir.toString('utf8')\n  console.log(output)\n})\n```\n\nTada, now you have SSH over node.\n\n> If you're into node stuff, you probably noticed `result.stdout` is a Buffer. That's allowed, since socket.io has first-class support for binary streams. Magical.\n\n###### Errors\nWhat's a test suite without error reporting? I dunno. I've never seen one.\n\nIf your job throws an error, you'll get the message back on the server:\n\n```js\nclient.run(function () {\n  throw new Error(\n   'Hrmm, servers are on fire.'\n  )\n}).catch(function (error) {\n  console.log(error)\n  /*\n  {\n   message: 'Hrmm, servers...',\n   source: `function () {\n     throw new Error(\n      'Hrmm, servers are on fire.'\n     )\n   }`,\n   platform: {} // platform.js\n  }\n  */\n})\n```\n\nAs you can see, some extra debugging information is attached to each error.\n\n- `.message`: the error message thrown.\n- `.source`: the job that failed.\n- `.platform`: the platform it failed on, courtesy of platform.js.\n\nHowever, due to complexity, stack traces aren't included. `eval` and socket.io make it hard to parse. Maybe in the future.\n\n#### `.matches()`\nEvery client has a [`platform`](https://github.com/gundb/panic-server#platform) property. The `matches` method allows you to query it.\n\nThis is useful when filtering a group of clients, or ensuring you're sending code to the platform you expect.\n\n> You probably won't use this method directly. However, it's used heavily by the `ClientList#filter` method to select platform groups, which passes through to the `.matches()` API.\n\nWhen passed a platform expression (more on this in a second), `.matches` returns a boolean of whether the client's platform satisfies the expression.\n\nFor example, this code is asking if the platform name matches the given regex:\n\n```js\n// Is this client a Chrome or Firefox browser?\nclient.matches(/(Chrome|Firefox)/)\n```\n\nTo be more specific, you can pass the exact string you're looking for:\n\n```js\n// Is this a Node.js process?\nclient.matches('Node.js')\n```\n\nThough as you can imagine, there's more to a platform than it's name. You can see the full list [here](https://github.com/bestiejs/platform.js/tree/master/doc).\n\nMore complex queries can be written by passing an object with more fields to match.\n\n```javascript\n// Is the client a Node.js process running\n// on 64-bit Fedora?\nclients.matches({\n  name: 'Node.js',\n  os: {\n    architecture: 64,\n    family: 'Fedora',\n  },\n})\n```\n\nIf you crave more power, you can use regular expressions as the field names.\n\n```js\n// Is this an ether Opera Mini or an\n// IE browser running on either Mac\n// or Android?\nclient.matches({\n  name: /(Opera Mini|Internet Explorer)/,\n\n  os: {\n    family: /(OS X|Android)/,\n  },\n})\n```\n\nOnly the fields given are matched, so you can be as specific or as loose as you want to be.\n\n### Lists of clients\nOften, you're working with groups of clients. Like, only run this code on IE, or only on Node.js processes.\n\nThat's where dynamic lists come in. Declaratively, you describe what the list should contain, and panic keeps them up to date.\n\n#### `panic.clients`\nThis is the top-level reactive list, containing every client currently connected. As new clients join, they're added to this list. When disconnected, they're removed.\n\n\n##### Events\nEvery list of clients will emit these events.\n\n###### `\"add\"`\nFires when a new client is added to the list.\n\nIt'll pass both the `Client` and the socket ID.\n\n```js\nclients.on('add', function (client, id) {\n  console.log('New client:', id)\n})\n```\n\n###### `\"remove\"`\nBasically the same as `\"add\"`, just backwards.\n\n```js\nclients.on('remove', function (client, id) {\n  console.log('Client', id, 'left.')\n})\n```\n\n#### `panic.ClientList`\nEvery list is an instance of `ClientList`. You can manually create a new lists, but generally you won't need to.\n\nIt's most useful for creating a new reactive list as the union of others. For example:\n\n```js\nvar explorer = clients.filter('Internet Explorer')\nvar opera = clients.filter('Opera Mini')\n\nvar despicable = new ClientList([\n  explorer,\n  opera,\n])\n```\n\nIn the example above, any new clients added to either `explorer` or `opera` will make it into the `despicable` list.\n\nAll clients are deduplicated automatically.\n\nIf you don't pass an array, you're left with a sad, empty client list.\n\n#### `ClientList` API\n\n**Table of Contents**\n - [`.filter()`](#filter)\n - [`.excluding()`](#excluding)\n - [`.pluck()`](#pluck)\n - [`.atLeast`](#at-least)\n - [`.run()`](#run)\n - [`.length`](#length)\n - [`.get()`](#get)\n - [`.add()`](#add)\n - [`.remove()`](#remove)\n - [`.each()`](#each)\n - [`.chain()`](#chain)\n\n##### <a name='filter'></a> `.filter(query)`\nCreates a new list of clients filtered by their platform.\n\nFor simpler queries, you can select via string or regular expression, which is matched against the `platform.name`:\n\n```js\n// Selects all the chrome browsers.\nvar chrome = clients.filter('Chrome')\n\n// Selects all firefox and chrome browsers.\nvar awesome = clients.filter(/(Firefox|Chrome)/)\n```\n\nYou can also do more complex queries by passing an object. Refer to the `Client#matches` API to see more examples.\n\nIf you're looking for something really specific, you can filter by passing a callback, which functions almost exactly like [`Array.prototype.filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).\n\n```javascript\nvar firefox = clients.filter(function (client, id, list) {\n  // `id`: The unique client id\n  // `list`: The parent list object, in this case `clients`\n\n  var platform = client.platform;\n\n  /*\n   This query only adds versions of\n   Firefox later than version 36.\n  */\n  if (platform.name === 'Firefox' && platform.version > '36') {\n   // add this client to the new list\n   return true;\n  } else {\n   // leave the client out of the new list\n   return false;\n  }\n});\n```\n\nTo make things cooler, you can chain filters off one another. For example, the above query only allows versions of firefox after 36. You could write that as two queries composed together...\n\n```javascript\n// The list of all firefox clients.\nvar firefox = clients.filter('Firefox')\n\n// The list of firefox newer than version 36.\nvar firefoxAfter36 = firefox.filter(function (client) {\n  var version = client.platform.version\n  var major = version.split('.')[0]\n\n  return Number(major) > 36;\n});\n```\n\nAs new clients are added, they'll be run through the firefox filters, and if added, will be run through the version filter. The dynamic filtering process allows for some cool RxJS style code.\n\n##### <a name='excluding'></a> `.excluding(ClientList)`\nYou can also create lists that exclude other lists, like a list of browsers might be anything that isn't a server, or perhaps you want to exclude all Chrome browsers from a list. You can do that with `.excluding`.\n\n```javascript\n// create a dynamic list of all node.js clients\nvar servers = clients.filter('Node.js')\n\n// the list of all clients,\n// except anything that belongs to `servers`.\nvar browsers = clients.excluding(servers)\n```\n\nLike filter, you can chain queries off each other to create really powerful queries.\n\n```javascript\n// using `browsers` from above\nvar chrome = browsers.filter('Chrome')\nvar notChrome = browsers.excluding(chrome)\n```\n\n##### <a name='pluck'></a> `.pluck(Number)`\n`.pluck` restricts the list length to a number, reactively listening for changes to ensure it's as close to the maximum as it can be. An excellent use case for `.pluck` is singling out clients of the same platform. This becomes especially powerful when paired with [`.excluding`](#excluding) and the `ClientList` constructor. For example, if you want to control 3 clients individually, it might look like this:\n\n```javascript\nvar clients = panic.clients\nvar List = panic.ClientList\n\n// grab one client from the list\nvar alice = clients.pluck(1)\n\n// grab another, so long as it isn't alice\nvar bob = clients\n.excluding(alice)\n.pluck(1)\n\n// and another, so long as it isn't alice or bob\nvar carl = clients\n.excluding(\n  new List([ alice, bob ])\n)\n.pluck(1)\n```\n\n> `.pluck` is highly reactive, and will readjust itself to hold as many clients as possible.\n\n##### <a name='at-least'></a> `.atLeast(Number)`\nOftentimes, you need a certain number of clients before running any tests. `.atLeast` takes that minimum number, and returns a promise.\n\nThat promise resolves when the minimum has been reached.\n\nHere's an example:\n```js\nvar clients = panic.clients\n\n// Waits for 2 clients before resolving.\nvar minimum = clients.atLeast(2)\n\nminimum.then(function () {\n\n  // 2 clients are connected now.\n  return clients.run(/* ... */)\n})\n```\n\nIt can also be used on derived lists, like so:\n\n```js\nvar node = clients.filter('Node.js')\nnode.atLeast(3).then(/* ... */)\n```\n\n> **Pro tip:** `.atLeast` goes great with mocha's `before` function.\n\n##### <a name='run'></a> `.run(Function)`\nIt just calls the `client.run` function for every item in the list, wrapping them in `Promise.all`.\n\nWhen every client reports success, it resolves to a list of return values.\n\nHowever, if any client fails, the promise rejects.\n\n```js\npanic.clients.run(function () {\n  var ip = require('ip')\n  return ip.address()\n}).then(function (ips) {\n  console.log(ips) // Array of IPs.\n})\n```\n\n##### <a name='length'></a> `.length`\nA getter property which returns the number of clients in a list.\n\n##### <a name='get'></a> `.get(id)`\nReturns the client corresponding to the id. Presently, socket.io's `socket.id` is used to uniquely key clients.\n\n##### <a name='add'></a> `.add(client)`\nManually adds a client to the list, triggering the `\"add\"` event, but only if the client wasn't there before.\n\n##### <a name='remove'></a> `.remove(client)`\nRemoves a client from the list, emitting a `remove` event. Again, if the client wasn't in the list, the event doesn't fire.\n\n##### <a name='each'></a> `.each(Function)`\nIt's basically a `.forEach` on the list. The function you pass will get the client, the client's ID, and the list it was called on.\n\n**Example**\n```javascript\nclients.each(function (client, id, list) {\n  client.run(function () {\n   // Fun stuff\n  })\n})\n```\n\n##### <a name='chain'></a> `.chain([...lists])`\nThis is a low-level API for subclasses. It makes sure the right class context is kept even when chaining off methods that create new lists, like `.filter` and `.pluck`.\n\n```javascript\nvar list = new ClientList()\nlist.chain() instanceof ClientList // true\n\nclass SubClass extends ClientList {\n  coolNewMethod() { /* bacon */ }\n}\n\nvar sub = new SubClass()\nsub.chain() instanceof SubClass // true\nsub.chain() instanceof ClientList // true\nsub.chain().coolNewMethod() // properly inherits\n```\n\nIf you're making an extension that creates a new list instance, use this method to play nice with other extensions.\n\n### `panic.server(Server)`\nIf an [`http.Server`](https://nodejs.org/api/http.html#http_class_http_server) is passed, panic will use it to configure [socket.io](http://socket.io/) and the `/panic.js` route will be added that servers up the [`panic-client`](https://github.com/gundb/panic-client) browser code.\n\nIf no server is passed, a new one will be created.\n\nIf you're not familiar with Node.js' http module, that's okay. The quickest way to get up and running is to call `.listen(8080)` which listens for requests on port 8080. In a browser, the url will look something like this: `http://localhost:8080/panic.js`.\n\n**Create a new server**\n```javascript\nvar panic = require('panic-server')\n\n// create a new http server instance\nvar server = panic.server()\n\n// listen for requests on port 8080\nserver.listen(8080)\n```\n\n**Reuse an existing one**\n```javascript\nvar panic = require('panic-server')\n\n// create a new http server\nvar server = require('http').Server()\n\n// pass it to panic\npanic.server(server)\n\n// start listening on a port\nserver.listen(8080)\n```\n\n> If you want to listen on port 80 (the default for browsers), you may need to run node as `sudo`.\n\nOnce you have a server listening, point browsers/servers to your address. More API details on the [panic-client readme](https://github.com/gundb/panic-client/#loading-panic-client).\n\n> **Note:** if you're using [PhantomJS](https://github.com/ariya/phantomjs), you'll need to serve the html page over http/s for socket.io to work.\n\n### `panic.client`\nReturns the panic-client webpack bundle. This is useful for injection into a WebDriver instance (using `driver.executeScript`) without needing to do file system calls.\n\n## <a name='scaffolding'></a> Basic test example\nA simple \"Hello world\" panic app.\n\n**index.html**\n```html\n<script src='http://localhost:8080/panic.js'>\n</script>\n\n<script>\n  // Connect to panic!\n  panic.server('http://localhost:8080')\n</script>\n```\n\n**demo.js**\n```js\nvar panic = require('panic-server')\n\n// Start the server on port 8080.\npanic.server().listen(8080)\n\n// Get the dynamic list of clients.\nvar clients = panic.clients\n\n// Create dynamic lists of\n// browsers and servers.\nvar servers = clients.filter('Node.js')\nvar browsers = clients.excluding(servers)\n\n// Wait for the browser to connect.\nbrowsers.on('add', function (browser) {\n\n  browser.run(function () {\n\n   // This is run in the browser!\n   var header = document.createElement('h1')\n   header.innerHTML = 'OHAI BROWSR!'\n   document.body.appendChild(header)\n  })\n})\n```\n\nRun `demo.js`, then open `index.html` in a browser. Enjoy!\n\n## Support\n- Oh, why thank you! Just star this repo, that's all the support we need :heart:\n\nOh.\n\nJust drop by [our gitter channel](https://gitter.im/amark/gun/) and ping @PsychoLlama, or submit an issue on the repo. We're there for ya.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"panic-server\",\n  \"version\": \"1.1.1\",\n  \"description\": \"Distributed Javascript runner\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src/**\",\n    \"test\": \"mocha\",\n    \"all-tests\": \"npm run test && npm run lint\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/gundb/panic-server.git\"\n  },\n  \"keywords\": [\n    \"gun\",\n    \"gundb\",\n    \"test\",\n    \"eval\",\n    \"testing\",\n    \"distributed\",\n    \"cross-platform\"\n  ],\n  \"author\": \"Jesse Gibson <jesse@gundb.io> (http://techllama.com)\",\n  \"license\": \"(Zlib OR MIT OR Apache-2.0)\",\n  \"bugs\": {\n    \"url\": \"https://github.com/gundb/panic-server/issues\"\n  },\n  \"homepage\": \"https://github.com/gundb/panic-server#readme\",\n  \"dependencies\": {\n    \"panic-client\": \"^1.0.0\",\n    \"socket.io\": \"^1.4.5\"\n  },\n  \"devDependencies\": {\n    \"chai\": \"^3.5.0\",\n    \"eslint\": \"^3.0.1\",\n    \"eslint-config-llama\": \"^3.0.0\",\n    \"expect\": \"^1.20.2\",\n    \"mocha\": \"^3.0.1\"\n  }\n}\n"
  },
  {
    "path": "src/Client.js",
    "content": "'use strict';\n\nvar matches = require('./matcher');\n\n/**\n * A wrapper around a websocket, providing\n * methods for interacting with clients.\n * @throws {Error} - The socket and platform must be valid.\n * @param {Object} client - A panic-client handshake.\n * @param {Socket} client.socket - The socket.io connection.\n * @param {Object} client.platform - The client platform.js object.\n * @class Client\n */\nfunction Client (client) {\n\n  /** Basic input validation. */\n  if (!client.socket) {\n    throw new Error('Invalid \"client.socket\" property.');\n  }\n  if (!client.platform) {\n    throw new Error('Invalid \"client.platform\" property.');\n  }\n\n  this.socket = client.socket;\n  this.platform = client.platform;\n}\n\nClient.prototype = {\n  constructor: Client,\n\n  /**\n   * Sends a function to be run on the client.\n   * @param  {Function} job - A function to be run remotely.\n   * The function will be stringified, so it cannot depend on\n   * external \"local\" variables, including other functions.\n   * @param  {Object} [props] - Any variables used in the job.\n   * @return {Promise} - Resolves if the job finishes,\n   * rejects if it throws an error.\n   */\n  run: function (job, props) {\n    if (typeof job !== 'function') {\n      throw new TypeError(\n      'Expected job \"' + job + '\" to be a function.'\n     );\n    }\n\n    var source = String(job);\n    var jobID = Math.random()\n     .toString(36)\n     .slice(2);\n\n    var socket = this.socket;\n\n   /** Report the success or failure of the job. */\n    var promise = new Promise(function (resolve, reject) {\n      socket.once('disconnect', resolve);\n\n      socket.once(jobID, function (report) {\n        socket.removeListener('disconnect', resolve);\n\n        if (report.hasOwnProperty('error')) {\n          reject(report.error);\n        } else {\n          resolve(report.value);\n        }\n      });\n    });\n\n    socket.emit('run', source, jobID, props);\n\n    return promise;\n  },\n\n  /**\n   * Test whether the client matches a platform query.\n   * @param  {Object|RegExp|String} query - A platform description.\n   * @return {Boolean} - Whether the platform satisfies the query.\n   */\n  matches: function (query) {\n\n    /** Assume non-objects are matching against the platform name. */\n    if (typeof query === 'string' || query instanceof RegExp) {\n      query = { name: query };\n    }\n\n    return matches(query, this.platform);\n  },\n};\n\nmodule.exports = Client;\n"
  },
  {
    "path": "src/ClientList.js",
    "content": "'use strict';\nvar Emitter = require('events');\n\n/**\n * Creates reactive lists of clients.\n * @param {Array} [lists] - A list of other lists\n * to join into a larger list.\n * @class ClientList\n * @augments EventEmitter\n */\nfunction ClientList (lists) {\n  var list = this;\n  Emitter.call(this);\n  list.clients = {};\n\n  var add = list.add.bind(list);\n\n  /** See if the user passed an array. */\n  if (lists instanceof Array) {\n    lists.forEach(function (list) {\n\n     /** Add each client, listening for additions. */\n      list.each(add).on('add', add);\n    });\n  }\n}\n\nvar API = ClientList.prototype = new Emitter();\nAPI.setMaxListeners(Infinity);\n\nAPI.constructor = ClientList;\n\n/**\n * Call the correct subclass when creating\n * new chains.\n * @param  {Array} [list] - A list of client lists to add.\n * @return {ClientList} - Either a ClientList instance\n * or a subclass.\n */\nAPI.chain = function (list) {\n  return new this.constructor(list);\n};\n\n/**\n * Iterate over the collection of low-level clients.\n * @param  {Function} cb - Callback, invoked for each client.\n * @return {this} - The current context.\n */\nAPI.each = function (cb) {\n  var key;\n  for (key in this.clients) {\n    if (this.clients.hasOwnProperty(key)) {\n      cb(this.clients[key], key, this);\n    }\n  }\n  return this;\n};\n\n/**\n * Add a low-level client object to the list.\n *\n * @param {Object} client - A strict client interface.\n * @param {Socket} client.socket - A socket.io interface.\n * @param {Object} client.platform - The `platform.js` object.\n * @returns {this} - The current context.\n */\nAPI.add = function (client) {\n  var socket, list = this;\n  socket = client.socket;\n\n  /**\n   * Ignore disconnected clients,\n   * or those already in the list.\n   */\n  if (!socket.connected || this.get(socket.id)) {\n    return this;\n  }\n\n  /** Add the client. */\n  this.clients[socket.id] = client;\n\n  /** Remove on disconnect. */\n  socket.on('disconnect', function () {\n    list.remove(client);\n  });\n\n  /** Fire the 'add' event. */\n  this.emit('add', client, socket.id);\n\n  return this;\n};\n\n/**\n * Remove a client from the list.\n * @param  {Object} client - A client object.\n * @return {this} - The current context.\n */\nAPI.remove = function (client) {\n\n  /** Make sure we really have that client. */\n  if (client.socket.id in this.clients) {\n\n   /** Remove the client. */\n    delete this.clients[client.socket.id];\n\n   /** Fire the 'remove' event. */\n    this.emit('remove', client, client.socket.id);\n  }\n\n  return this;\n};\n\n/**\n * Get the client corresponding to an ID.\n * @param  {String} ID - The socket.id of the client.\n * @return {Object|null} - The client object, if found.\n */\nAPI.get = function (ID) {\n  return this.clients[ID] || null;\n};\n\n/**\n * Create a new reactive list as the result of a\n * platform query.\n * @param  {Object|String|RegExp|Function} filter - Platform query.\n * @return {ClientList} - A new list of clients.\n */\nAPI.filter = function (filter) {\n\n  /** Create a new target list. */\n  var list = this.chain();\n\n  if (typeof filter !== 'function') {\n    var query = filter;\n    filter = function (client) {\n      return client.matches(query);\n    };\n  }\n\n  /**\n   * Adds a client to the new list if it satisfies the query.\n   * @param  {Client} client - A client instance.\n   * @param  {String} ID - The unique client ID.\n   * @return {undefined}\n   */\n  function add (client, ID) {\n    var matches = filter(client, ID);\n\n    if (matches) {\n      list.add(client);\n    }\n  }\n\n  /**\n   * Filter everything in the list, then listen\n   * for future clients.\n   */\n  this.each(add).on('add', add);\n\n  return list;\n};\n\n/**\n * Create a new reactive list containing the original\n * items, minus anything in a provided exclusion list.\n * @param  {ClientList} exclude - A list of clients.\n * @return {ClientList} - A new client list.\n */\nAPI.excluding = function (exclude) {\n\n  /**\n   * Add anything not in the exclusion list.\n   * Remember .filter is reactive.\n   */\n  if(exclude instanceof Array){\n    exclude = new ClientList(exclude);\n  }\n\n  var list = this.filter(function (client) {\n    var excluded = exclude.get(client.socket.id);\n\n    return !excluded;\n  });\n\n  var self = this;\n\n  /**\n   * Add clients removed from the exclusion list,\n   * and contained in the original list.\n   */\n  exclude.on('remove', function (client) {\n    var socket = client.socket;\n    var connected = socket.connected;\n    var relevant = self.get(socket.id);\n\n    if (connected && relevant) {\n      list.add(client);\n    }\n  });\n\n  return list;\n};\n\n/**\n * Run a function remotely on a group of clients.\n * @param  {Function} job - The function eval on clients.\n * @param  {Object} [props] - Any variables the job needs.\n * @return {Promise} - Resolves when the jobs finish,\n * rejects if any of them fail.\n */\nAPI.run = function (job, props) {\n  var jobs = [];\n\n  /** Run the job on each client. */\n  this.each(function (client) {\n    var promise = client.run(job, props);\n    jobs.push(promise);\n  });\n\n  /** Wait for all jobs to finish. */\n  return Promise.all(jobs);\n};\n\n/**\n * Wait until a number of clients have joined the list.\n * @param  {Number} min - The minimum number of clients needed.\n * @return {Promise} - Resolves when the minimum is reached.\n */\nAPI.atLeast = function (min) {\n  var list = this;\n\n  /** Check to see if we already have enough. */\n  if (list.length >= min) {\n    return Promise.resolve();\n  }\n\n  return new Promise(function (resolve) {\n\n   /** Wait for new clients. */\n    list.on('add', function cb () {\n\n     /** If we have enough... */\n      if (list.length >= min) {\n\n      /** Unsubscribe and resolve. */\n        list.removeListener('add', cb);\n        resolve();\n      }\n    });\n  });\n};\n\n/**\n * Create a new list with a maximum number of clients.\n *\n * @param  {Number} num - The maximum number of items.\n * @return {ClientList} - A new constrained list.\n */\nAPI.pluck = function (num) {\n\n  /** Create a new target list. */\n  var list = this.chain();\n  var self = this;\n\n  /**\n   * Add a client if there's still room.\n   * @param  {Object} client - A client object.\n   * @return {undefined}\n   */\n  function measure (client) {\n    if (!list.atCapacity) {\n      list.add(client);\n    }\n  }\n\n  /** Check to see if it's already full. */\n  list.on('add', function () {\n    if (list.length === num) {\n      list.atCapacity = true;\n    }\n  });\n\n  /** See if we can replace the lost client. */\n  list.on('remove', function () {\n    list.atCapacity = false;\n    self.each(measure);\n  });\n\n  /** Add as many clients as we can. */\n  this.each(measure).on('add', measure);\n\n  return list;\n};\n\nAPI.atCapacity = false;\n\n/**\n * A getter, providing the number of clients in a list.\n * @returns {Number} - The length of the list.\n */\nObject.defineProperty(API, 'length', {\n  get: function () {\n\n   /** Feature detect Object.keys. */\n    if (Object.keys instanceof Function) {\n      return Object.keys(this.clients).length;\n    }\n\n   /** Fall back to iterating. */\n    var length = 0;\n    this.each(function () {\n      length += 1;\n    });\n\n    return length;\n  },\n});\n\nmodule.exports = ClientList;\n"
  },
  {
    "path": "src/clients.js",
    "content": "'use strict';\nvar List = require('./ClientList');\nmodule.exports = new List();\n"
  },
  {
    "path": "src/index.js",
    "content": "'use strict';\n\nvar ClientList = require('./ClientList');\nvar Client = require('./Client');\nvar server = require('./server');\nvar clients = require('./clients');\n\nexports.server = server;\nexports.clients = clients;\nexports.Client = Client;\nexports.ClientList = ClientList;\n"
  },
  {
    "path": "src/matcher.js",
    "content": "'use strict';\n\nvar match;\n\n/**\n * Whether two values look the same.\n * @param  {Mixed} expected - An expression to match the value against.\n * Can be a regular expression, an object containing more comparisons,\n * or just a primitive.\n * @param  {Mixed} value - A value to compare against the expression.\n * @return {Boolean} - Whether the values look similar.\n */\nfunction matches (expected, value) {\n\n  /** Matchers can be regular expressions. */\n  if (expected instanceof RegExp) {\n    return expected.test(value);\n  }\n\n  /** Matchers can be nested objects. */\n  if (expected instanceof Object) {\n    return match(expected, value || {});\n  }\n\n  /** Or, just a primitive value. */\n  return value === expected;\n}\n\n/**\n * Runs a platform query against a platform.\n * @param  {Object} query - Contains properties to match against the\n * platform. If it contains nested objects, they will match against the\n * platform object of the same name, regardless of depth. If a property\n * is a regular expression, it will run against the platform property of\n * the same name.\n * @param  {Object} platform - A platform.js object.\n * @return {Boolean} - Whether the platform matches the query.\n */\nmatch = function (query, platform) {\n  var fields = Object.keys(query);\n\n  var invalid = fields.some(function isInvalid (field) {\n    var expected = query[field];\n    var value = platform[field];\n\n    /** Look for values that don't match the query. */\n    return matches(expected, value) === false;\n  });\n\n  return !invalid;\n};\n\nmodule.exports = match;\n"
  },
  {
    "path": "src/server.js",
    "content": "/* eslint-disable no-sync*/\n'use strict';\n\nvar io = require('socket.io');\nvar fs = require('fs');\nvar clients = require('./clients');\nvar file = require.resolve('panic-client/panic.js');\nvar Server = require('http').Server;\nvar panic = require('./index');\nvar Client = require('./Client');\nvar client;\n\n/**\n * Lazy getter for the panic-client bundle.\n * @returns {String} - The whole webpacked client bundle.\n */\nObject.defineProperty(panic, 'client', {\n  get: function () {\n    if (!client) {\n      client = fs.readFileSync(file, 'utf8');\n    }\n\n    return client;\n  },\n});\n\n/**\n * Filter requests for /panic.js and send\n * the bundle.js file.\n * @param  {Object} req - http request object.\n * @param  {Object} res - http response object.\n * @return {undefined}\n */\nfunction serve (req, res) {\n  if (req.url === '/panic.js') {\n    res.end(panic.client);\n  }\n}\n\n/**\n * Listen for a panic handshake,\n * only then adding it to the panic.clients list.\n * @param  {Socket} socket - A socket.io websocket.\n * @return {undefined}\n */\nfunction upgrade (socket) {\n  socket.on('handshake', function (platform) {\n\n   /** Create a new panic client. */\n    var client = new Client({\n      socket: socket,\n      platform: platform,\n    });\n\n   /** Add the new client. */\n    clients.add(client);\n\n  });\n}\n\n/**\n * Attach to a server, handling incoming panic traffic.\n * @param  {Server} [server]\n * An http server instance.\n * If none is provided, a server will be created.\n * @return {Server} - Either the server passed, or a new server.\n */\nfunction open (server) {\n\n  if (!server) { // https is not instance of http, so be more relaxed in our check.\n    server = new Server();\n  }\n\n  /** Handle /panic.js route. */\n  server.on('request', serve);\n\n  /** Upgrade with socket.io */\n  io(server).on('connection', upgrade);\n\n  return server;\n}\n\nmodule.exports = open;\n"
  },
  {
    "path": "test/Client.js",
    "content": "/* eslint-disable require-jsdoc */\n/* eslint-env mocha */\n'use strict';\nvar Client = require('../src/Client');\nvar Emitter = require('events');\nvar expect = require('expect');\n\ndescribe('A client', function () {\n  var client, socket, platform;\n\n  beforeEach(function () {\n    socket = new Emitter();\n\n    platform = {\n      name: 'Node.js',\n      version: '6.6.0',\n    };\n\n    client = new Client({\n      socket: socket,\n      platform: platform,\n    });\n  });\n\n  it('should expose the socket', function () {\n    expect(client.socket).toBe(socket);\n  });\n\n  it('should expose the platform', function () {\n    expect(client.platform).toBe(platform);\n  });\n\n  it('should validate the socket', function () {\n    function fail () {\n      return new Client({\n        // Missing \"socket\".\n        platform: platform,\n      });\n    }\n    expect(fail).toThrow();\n  });\n\n  it('should validate the platform', function () {\n    function fail () {\n      return new Client({\n        socket: new Emitter(),\n        // Missing \"platform\".\n      });\n    }\n    expect(fail).toThrow();\n  });\n\n  describe('\"run\" call', function () {\n    var spy;\n\n    beforeEach(function () {\n      spy = expect.createSpy();\n    });\n\n    it('should send jobs to the client', function () {\n      client.socket.on('run', spy);\n      client.run(function () {});\n      expect(spy).toHaveBeenCalled();\n    });\n\n    it('should make sure the job is a function', function () {\n      function fail () {\n        client.run(9000);\n      }\n      expect(fail).toThrow(TypeError);\n    });\n\n    it('should send the stringified job', function () {\n      client.socket.on('run', spy);\n      client.run(function () {\n        // I haz a comment.\n      });\n      var str = spy.calls[0].arguments[0];\n      expect(str).toContain('I haz a comment');\n    });\n\n    it('should pass a job ID', function () {\n      client.socket.on('run', spy);\n      client.run(function () {});\n      client.run(function () {});\n      var id1 = spy.calls[0].arguments[1];\n      var id2 = spy.calls[1].arguments[1];\n      expect(id1).toBeA('string');\n      expect(id2).toBeA('string');\n      expect(id1).toNotBe(id2);\n    });\n\n    it('should send the props to the client', function () {\n      client.socket.on('run', spy);\n      var props = {};\n      client.run(function () {}, props);\n      var args = spy.calls[0].arguments;\n      expect(args[2]).toBe(props);\n    });\n\n    it('should return a promise', function () {\n      var job = client.run(function () {});\n      expect(job).toBeA(Promise);\n    });\n\n    it('should resolve when the job does', function (done) {\n      client.socket.on('run', spy);\n      var job = client.run(function () {});\n      var jobID = spy.calls[0].arguments[1];\n\n      job.then(done);\n      client.socket.emit(jobID, {});\n    });\n\n    it('should resolve to the job value', function () {\n      client.socket.on('run', spy);\n      var job = client.run(function () {});\n\n      var jobID = spy.calls[0].arguments[1];\n\n      client.socket.emit(jobID, {\n        value: 'Hello world!',\n      });\n\n      return job.then(function (value) {\n        expect(value).toBe('Hello world!');\n      });\n    });\n\n    it('should reject if the job fails', function (done) {\n      client.socket.on('run', spy);\n      var job = client.run(function () {});\n      var jobID = spy.calls[0].arguments[1];\n      var error = new Error('\".run\" rejection test.');\n\n      job.catch(function (err) {\n        expect(err).toBe(error);\n        done();\n      });\n\n      client.socket.emit(jobID, {\n        error: error,\n      });\n    });\n\n    it('should unsubscribe once finished', function () {\n      var socket = client.socket;\n      socket.on('run', spy);\n      client.run(function () {});\n      var jobID = spy.calls[0].arguments[1];\n      expect(socket.listenerCount(jobID)).toBe(1);\n      socket.emit(jobID, {});\n      expect(socket.listenerCount(jobID)).toBe(0);\n    });\n\n    it('should resolve if disconnected', function (done) {\n      var job = client.run(function () {});\n      job.then(done);\n      client.socket.emit('disconnect');\n    });\n\n  });\n\n  describe('platform query', function () {\n\n    beforeEach(function () {\n      client.platform = {\n        name: 'Node.js',\n        version: '7.1.0',\n        os: { family: 'Darwin' },\n      };\n    });\n\n    it('should return a boolean', function () {\n      var matches = client.matches({ name: 'Node.js' });\n      expect(matches).toBeA('boolean');\n    });\n\n    it('should pass if the given fields match', function () {\n      var matches = client.matches({\n        name: 'Node.js',\n      });\n      expect(matches).toBe(true);\n    });\n\n    it('should only pass if all the fields match', function () {\n      var matches = client.matches({\n        name: 'Node.js',\n        version: 'nah',\n      });\n\n      expect(matches).toBe(false);\n    });\n\n    it('should accept regular expressions', function () {\n      var matches = client.matches({\n        name: /node/i,\n      });\n      expect(matches).toBe(true);\n    });\n\n    it('should assume regex input matches the name', function () {\n      expect(client.matches(/Node/)).toBe(true);\n      expect(client.matches(/Firefox/)).toBe(false);\n    });\n\n    it('should assume string input matches the name', function () {\n      expect(client.matches('Node.js')).toBe(true);\n      expect(client.matches('Firefox')).toBe(false);\n    });\n\n    it('should match against nested fields', function () {\n      var matches;\n\n      matches = client.matches({\n        os: { family: 'Darwin' },\n      });\n      expect(matches).toBe(true);\n\n      matches = client.matches({\n        os: { family: 'Honestly it\\'s just a box of potatoes' },\n      });\n      expect(matches).toBe(false);\n    });\n\n    it('should fail if the nested query is not in the platform', function () {\n      var matches = client.matches({\n        'super-weird~field': {\n          burger: true,\n          fries: 'yes please',\n        },\n      });\n      expect(matches).toBe(false);\n    });\n\n    it('should allow nested regex matching', function () {\n      var matches;\n\n      matches = client.matches({\n        os: { family: /Darwin/ },\n      });\n      expect(matches).toBe(true);\n\n      matches = client.matches({\n        os: { family: /why do they call these regular?/ },\n      });\n      expect(matches).toBe(false);\n    });\n\n    it('should fail if the properties given do not match', function () {\n      var matches = client.matches({\n        name: 'Some Non-Existent Platform™',\n      });\n      expect(matches).toBe(false);\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "test/index.js",
    "content": "/* eslint-disable require-jsdoc */\n/* eslint-env mocha */\n'use strict';\nvar mock = require('./mock');\nvar Client = mock.Client;\nvar ClientList = require('../src/ClientList');\n\nvar expect = require('chai').expect;\n\ndescribe('A clientList', function () {\n  var list, client;\n\n  beforeEach(function () {\n    list = new ClientList();\n    client = new Client({\n      name: 'Node.js',\n    });\n  });\n\n  it('should emit when a client is added', function () {\n    var fired = false;\n    list.on('add', function () {\n      fired = true;\n    }).add(client);\n    expect(fired).to.eq(true);\n  });\n\n  it('should emit when a client is removed', function () {\n    var fired = false;\n    list.on('remove', function () {\n      fired = true;\n    })\n   .add(client)\n   .remove(client);\n    expect(fired).to.eq(true);\n  });\n\n  it('should return length when \"len()\" is called', function () {\n    list.add(client);\n    expect(list.length).to.eq(1);\n    list.remove(client);\n    expect(list.length).to.eq(0);\n  });\n\n  it('should not add disconnected clients', function () {\n    client.socket.connected = false;\n    list.add(client);\n    expect(list.length).to.eq(0);\n  });\n\n  it('should remove a client on disconnect', function () {\n    list.add(client);\n    expect(list.length).to.eq(1);\n    client.socket.emit('disconnect');\n    expect(list.length).to.eq(0);\n  });\n\n  it('should resolve when all clients finish', function (done) {\n    var socket = client.socket;\n    socket.on('run', function (cb, jobID) {\n      socket.emit(jobID, {});\n    });\n    list.add(client).run(function () {})\n   .then(function () {\n     done();\n   })\n   .catch(done);\n  });\n\n  it('should reject a promise if an error is sent', function (done) {\n    client.socket.on('run', function (cb, job) {\n      client.socket.emit(job, {\n        error: 'fake error',\n      });\n    });\n    list.add(client).run(function () {})\n   .catch(function (err) {\n     expect(err).to.eq('fake error');\n     done();\n   });\n  });\n\n  it('should not emit \"add\" if it contains the client', function () {\n    var called = 0;\n    list.on('add', function () {\n      called += 1;\n    });\n    list.add(client);\n    list.add(client);\n    expect(called).to.eq(1);\n  });\n\n  describe('filter', function () {\n    it('should not mutate the original list', function () {\n      list.add(client);\n      expect(list.length).to.eq(1);\n      list.filter(function () {\n        return false;\n      });\n      expect(list.length).to.eq(1);\n    });\n\n    it('should return a new, filtered list', function () {\n      list.add(client);\n      var servers = list.filter('Node.js');\n      var browsers = list.filter(function (client) {\n        return client.platform.name !== 'Node.js';\n      });\n      expect(servers.length).to.eq(1);\n      expect(browsers.length).to.eq(0);\n    });\n\n    it('should be reactive to changes to the parent list', function () {\n      var servers = list.filter('Node.js');\n      expect(servers.length).to.eq(0);\n      list.add(client);\n      expect(servers.length).to.eq(1);\n    });\n  });\n\n  describe('exclusion', function () {\n    it('should not contain excluded clients', function () {\n      list.add(client);\n      var filtered = list.excluding(list);\n      expect(filtered.length).to.eq(0);\n    });\n\n    it('should react to removals if they are connected', function () {\n      var decoy = new Client();\n      var exclusion = new ClientList()\n     .add(client)\n     .add(decoy);\n      var filtered = list.excluding(exclusion);\n      list.add(client).add(new Client());\n      expect(filtered.length).to.eq(1);\n      exclusion.remove(client).remove(decoy);\n      expect(filtered.length).to.eq(2);\n    });\n  });\n\n  describe('number constraint', function () {\n    it('should return no more than the number requested', function () {\n      list.add(client)\n     .add(new Client())\n     .add(new Client());\n      expect(list.pluck(1).length).to.eq(1);\n    });\n\n    it('should listen for additions', function () {\n      var subset = list.pluck(2);\n      expect(subset.length).not.to.eq(2);\n      list.add(new Client()).add(new Client());\n      expect(subset.length).to.eq(2);\n      list.add(new Client());\n      expect(subset.length).to.eq(2);\n    });\n\n    it('should replace a client when it disconnects', function () {\n      var subset = list.pluck(1);\n      list.add(client).add(new Client());\n      expect(subset.length).to.eq(1);\n      client.socket.emit('disconnect');\n     // It should be replaced with\n     // the second connected client.\n      expect(subset.length).to.eq(1);\n    });\n\n    it('should set a flag whether the constraint is met', function () {\n      var subset = list.pluck(1);\n      expect(subset.atCapacity).to.eq(false);\n      list.add(client);\n      expect(subset.atCapacity).to.eq(true);\n      client.socket.emit('disconnect');\n      expect(subset.atCapacity).to.eq(false);\n    });\n\n    it('should play well with exclusions', function () {\n      var bob, alice = list.pluck(1);\n      bob = list.excluding(alice).pluck(1);\n      list.add(client)\n     .add(new Client())\n     .add(new Client());\n      expect(alice.length).to.eq(1);\n      expect(bob.length).to.eq(1);\n    });\n  });\n\n  describe('minimum qualifier', function () {\n\n    it('should resolve when the minimum is reached', function () {\n      var promise = list.atLeast(1);\n      var called = false;\n      promise.then(function () {\n        called = true;\n      });\n      expect(called).to.eq(false);\n      list.add(new Client());\n\n     // Mocha will wait for this to resolve.\n      return promise;\n    });\n\n    it('should resolve if the min is already reached', function () {\n      var promise = list.atLeast(0);\n\n     // This will time out if unresolved.\n      return promise;\n    });\n\n    it('should resolve to undefined', function () {\n      function validate (arg) {\n        expect(arg).to.eq(undefined);\n      }\n      var immediate = list.atLeast(0).then(validate);\n      var later = list.atLeast(1).then(validate);\n\n      list.add(new Client());\n\n      return Promise.all([immediate, later]);\n    });\n\n    it('should resolve if it has more than enough', function () {\n      list.add(new Client()).add(new Client());\n\n      return list.atLeast(1);\n    });\n\n    it('should unsubscribe after resolving', function () {\n      list.atLeast(1);\n      expect(list.listenerCount('add')).to.eq(1);\n      list.add(new Client());\n      expect(list.listenerCount('add')).to.eq(0);\n    });\n\n  });\n});\n\ndescribe('The ClientList constructor', function () {\n  var list1, list2, client1, client2;\n\n  beforeEach(function () {\n    list1 = new ClientList();\n    list2 = new ClientList();\n    client1 = new Client();\n    client2 = new Client();\n    list1.add(client1);\n    list2.add(client2);\n  });\n\n  it('should accept an array of clientLists', function () {\n    var list = new ClientList([list1, list2]);\n\n   // it should contain both clients\n    expect(list.get(client1.socket.id)).to.eq(client1);\n    expect(list.get(client2.socket.id)).to.eq(client2);\n  });\n\n  it('should reactively add new clients from source lists', function () {\n    var list = new ClientList([list1, list2]);\n    var client3 = new Client();\n    expect(list.get(client3.socket.id)).to.eq(null);\n    list1.add(client3);\n    expect(list.get(client3.socket.id)).to.eq(client3);\n  });\n\n  it('should be subclassable', function () {\n    function Sub () {\n      ClientList.call(this);\n    }\n    Sub.prototype = new ClientList();\n    Sub.prototype.constructor = Sub;\n\n    var sub = new Sub();\n    expect(sub).to.be.an.instanceof(Sub);\n\n   // chained inheritance\n    expect(sub.filter('Firefox')).to.be.an.instanceof(Sub);\n    expect(sub.pluck(1)).to.be.an.instanceof(Sub);\n  });\n});\n"
  },
  {
    "path": "test/mock.js",
    "content": "'use strict';\nvar Emitter = require('events');\nvar Client = require('../src/Client');\n\n/**\n * Creates a client wrapping a new fake websocket.\n * @param  {Object} [platform] - A platform.js-style object.\n * @return {Client} - Draws from a fake socket instance.\n */\nfunction mock (platform) {\n  var rand = Math.random();\n\n  // Fake a socket.io socket.\n  var socket = new Emitter();\n  socket.connected = true;\n  socket.id = rand.toString(36).slice(2);\n\n  return new Client({\n    socket: socket,\n    platform: platform || {},\n  });\n}\n\nmodule.exports = {\n  Client: mock,\n};\n"
  }
]