{"id":22485489,"url":"https://github.com/gundb/panic-server","last_synced_at":"2025-08-02T18:32:41.962Z","repository":{"id":57318587,"uuid":"48209991","full_name":"gundb/panic-server","owner":"gundb","description":"Testing for collaborative apps and tools","archived":false,"fork":false,"pushed_at":"2020-09-24T00:56:56.000Z","size":208,"stargazers_count":163,"open_issues_count":2,"forks_count":13,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-11-10T17:11:47.613Z","etag":null,"topics":["coordination","distributed-systems","framework","testing"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gundb.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-12-18T02:32:04.000Z","updated_at":"2024-07-03T15:40:23.000Z","dependencies_parsed_at":"2022-08-25T22:42:05.295Z","dependency_job_id":null,"html_url":"https://github.com/gundb/panic-server","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gundb%2Fpanic-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gundb%2Fpanic-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gundb%2Fpanic-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gundb%2Fpanic-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gundb","download_url":"https://codeload.github.com/gundb/panic-server/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228499977,"owners_count":17929990,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coordination","distributed-systems","framework","testing"],"created_at":"2024-12-06T17:12:43.644Z","updated_at":"2024-12-06T17:14:33.933Z","avatar_url":"https://github.com/gundb.png","language":"JavaScript","readme":"\u003cimg alt='PANIC' width='500px' src='./panic-logo.jpg'\u003e\u003c/img\u003e\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\u003e **TL;DR:**\u003cbr /\u003e\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\u003e 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\u003e 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\u003e 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\u003e 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##### \u003ca name='filter'\u003e\u003c/a\u003e `.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' \u0026\u0026 platform.version \u003e '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) \u003e 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##### \u003ca name='excluding'\u003e\u003c/a\u003e `.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##### \u003ca name='pluck'\u003e\u003c/a\u003e `.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\u003e `.pluck` is highly reactive, and will readjust itself to hold as many clients as possible.\n\n##### \u003ca name='at-least'\u003e\u003c/a\u003e `.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\u003e **Pro tip:** `.atLeast` goes great with mocha's `before` function.\n\n##### \u003ca name='run'\u003e\u003c/a\u003e `.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##### \u003ca name='length'\u003e\u003c/a\u003e `.length`\nA getter property which returns the number of clients in a list.\n\n##### \u003ca name='get'\u003e\u003c/a\u003e `.get(id)`\nReturns the client corresponding to the id. Presently, socket.io's `socket.id` is used to uniquely key clients.\n\n##### \u003ca name='add'\u003e\u003c/a\u003e `.add(client)`\nManually adds a client to the list, triggering the `\"add\"` event, but only if the client wasn't there before.\n\n##### \u003ca name='remove'\u003e\u003c/a\u003e `.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##### \u003ca name='each'\u003e\u003c/a\u003e `.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##### \u003ca name='chain'\u003e\u003c/a\u003e `.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\u003e 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\u003e **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## \u003ca name='scaffolding'\u003e\u003c/a\u003e Basic test example\nA simple \"Hello world\" panic app.\n\n**index.html**\n```html\n\u003cscript src='http://localhost:8080/panic.js'\u003e\n\u003c/script\u003e\n\n\u003cscript\u003e\n  // Connect to panic!\n  panic.server('http://localhost:8080')\n\u003c/script\u003e\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","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgundb%2Fpanic-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgundb%2Fpanic-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgundb%2Fpanic-server/lists"}