Rename .len() to .length, allow subclassing, export client bundle.

Instead of calling a method to find the length of a list, you can use a
property (which is a getter under the hood, doing the same thing as
`.len()`). This is cleaner and more intuitive, aligning itself more with
arrays.
Subclassing is now facilitated by a new method, `.chain`. It creates a
new list instance by calling the constructor property, instead of
statically creating a new ClientList instance. This allows you to
create subclasses that inherit from ClientList, without losing that
inheritance when calling `.filter` or `.pluck` (methods which create new
list instances).
The client bundle is now exported lazily, so when you import panic,
there's a `client` getter which memoizes a fs call for the client code.
This allows compatibility on pre-3.0 versions of npm, where other packages
might not be able to recursively find `panic-client`. Also, since it's a
getter, it doesn't do the file system call until it's needed.
This commit is contained in:
Jesse Gibson
2016-05-27 14:10:28 -06:00
parent fa2da9920c
commit 7425aaac2d
7 changed files with 116 additions and 61 deletions

View File

@@ -1,5 +1,10 @@
# Changelog
## v0.3.0
Improvements: deprecated `.len()` in favor of `.length`, allow ClientList to be subclassable, and lazily export the client code onto the `panic.client` property.
Subclassing is curtesy of a new method, `.chain`, which ensures the `this` context's constructor is called instead of statically calling `new ClientList()`.
## v0.2.4
Set the `constructor` property on the ClientList prototype.

View File

@@ -7,9 +7,9 @@
[![Gitter](https://img.shields.io/gitter/room/amark/gun.svg?style=flat-square)](https://gitter.im/amark/gun)
> **TL;DR:**<br />
It's a glorified `eval()` with platform queries.
A lightweight tool for browser and node.js choreography.
Panic-server is designed as the underlying layer for panic-room, the distributed testing framework. It allows you to dynamically and reactively group connected clients and evaluate code on platform subsets.
Panic-server is designed for distributed testing. It allows you to dynamically group clients and control them through Javascript, and is compatible with the test frameworks you already use. Think of it as Selenium WebDriver on steroids.
For example:
```javascript
@@ -127,7 +127,10 @@ Once you have a server listening, point browsers/servers to your address ([here'
> **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.
### `panic.clients`
Every group is a ClientList instance, and inherits from EventEmitter. They update in real-time as clients are added and disconnected, and have array-like methods for manipulating and filtering. `panic.clients` is the root level list, and contains every client currently connected.
Every group is a ClientList instance, and inherits from EventEmitter. They update in real-time as clients are added and disconnected, and have [RxJS](https://github.com/Reactive-Extensions/RxJS)-style methods for manipulating and filtering. `panic.clients` is the root level list, and contains every client currently connected.
### `panic.client`
Returns the panic-client bundle code. This is useful for injection into a WebDriver instance (using `driver.executeScript`) without needing to do file system calls. The property is immutable and
#### Events
As the list changes, it will emit one of two mutation events:
@@ -188,11 +191,12 @@ var client = {
- [`.excluding()`](#excluding)
- [`.pluck()`](#pluck)
- [`.run()`](#run)
- [`.len()`](#len)
- [`.length`](#length)
- [`.get()`](#get)
- [`.add()`](#add)
- [`.remove()`](#remove)
- [`.each()`](#each)
- [`.chain()`](#chain)
##### <a name='filter'></a> `.filter(query)`
Returns a filtered list containing everything that matches a platform query.
@@ -471,8 +475,8 @@ clients.run(function () {
})
```
##### <a name='len'></a> `.len()`
Returns the number of clients in a list.
##### <a name='length'></a> `.length`
A getter property which returns the number of clients in a list.
**Low-level API**
@@ -505,6 +509,24 @@ clients.each(function (client, id, list) {
})
```
##### <a name='chain'></a> `.chain([...lists])`
This is an abstraction method that just calls `this.constructor` to create a new instance. Mainly used to allow subclassing, it makes sure the right class context is kept even when chaining off methods that create new lists, like `.filter` and `.pluck`.
```javascript
var list = new ClientList()
list.chain() instanceof ClientList // true
class SubClass extends ClientList {
coolNewMethod() { /* bacon */ }
}
var sub = new SubClass()
sub.chain() instanceof SubClass // true
sub.chain() instanceof ClientList // true
sub.chain().coolNewMethod() // properly inherits
```
If you're making an extension that creates a new list instance, use this method to play nice with other extensions.
## Support
If you have questions or ideas, we'd love to hear them! Just swing by our [gitter channel](https://gitter.im/amark/gun) and ask for @PsychoLlama or @amark. We're usually around :wink:

View File

@@ -1,6 +1,6 @@
{
"name": "panic-server",
"version": "0.2.4",
"version": "0.3.0",
"description": "Distributed Javascript runner",
"main": "src/index.js",
"scripts": {

View File

@@ -2,13 +2,13 @@
var Emitter = require('events');
var match = require('./matcher');
var Promise = require('bluebird');
var util = require('util');
function ClientList(lists) {
var list = this;
Emitter.call(this);
list.clients = {};
function add(client) {
list.add(client);
}
var add = list.add.bind(list);
if (lists instanceof Array) {
lists.forEach(function (list) {
list.each(add).on('add', add);
@@ -17,9 +17,14 @@ function ClientList(lists) {
}
var API = ClientList.prototype = new Emitter();
API.setMaxListeners(Infinity);
API.constructor = ClientList;
API.chain = function (list) {
return new this.constructor(list);
};
API.each = function (cb) {
var key;
for (key in this.clients) {
@@ -57,7 +62,7 @@ API.get = function (ID) {
};
API.filter = function (query) {
var list = new ClientList();
var list = this.chain();
function filter(client, ID) {
if (query instanceof Function && query(client, ID)) {
list.add(client);
@@ -90,19 +95,12 @@ API.excluding = function (exclude) {
return list;
};
API.len = function () {
if (Object.keys instanceof Function) {
return Object.keys(this.clients).length;
}
var num = 0;
this.each(function () {
num += 1;
});
return num;
};
API.len = util.deprecate(function () {
return this.length;
}, 'Use `.length` instead of `.len()`');
API.run = function (cb, scope) {
var key, done = 0, list = this, length = this.len();
var key, done = 0, list = this, length = this.length;
key = Math.random()
.toString(36)
.slice(2);
@@ -131,7 +129,7 @@ API.run = function (cb, scope) {
};
API.pluck = function (num) {
var self, list = new ClientList();
var self, list = this.chain();
self = this;
function measure(client) {
if (!list.atCapacity) {
@@ -139,7 +137,7 @@ API.pluck = function (num) {
}
}
list.on('add', function () {
if (list.len() === num) {
if (list.length === num) {
list.atCapacity = true;
}
});
@@ -154,4 +152,17 @@ API.pluck = function (num) {
API.atCapacity = false;
Object.defineProperty(API, 'length', {
get: function () {
if (Object.keys instanceof Function) {
return Object.keys(this.clients).length;
}
var num = 0;
this.each(function () {
num += 1;
});
return num;
}
});
module.exports = ClientList;

View File

@@ -1,16 +1,5 @@
'use strict';
var server = require('./server');
var clients = require('./clients');
var ClientList = require('./ClientList');
var msg = '\n\nAPI CHANGE: ".serve()" has been renamed to ".server()",\n' +
'and no longer works the same (see changelog#v0.2.0).\n';
module.exports = {
server: server,
serve: function () {
throw new Error(msg);
},
clients: clients,
ClientList: ClientList
};
exports.server = require('./server');
exports.clients = require('./clients');
exports.ClientList = require('./ClientList');

View File

@@ -1,13 +1,26 @@
/*eslint-disable no-sync*/
'use strict';
var io = require('socket.io');
var fs = require('fs');
var clients = require('./clients');
var file = require.resolve('panic-client/panic.js');
var Server = require('http').Server;
var panic = require('./index');
var client;
Object.defineProperty(panic, 'client', {
get: function () {
if (!client) {
client = fs.readFileSync(file, 'utf8');
}
return client;
}
});
function serve(req, res) {
if (req.url === '/panic.js') {
fs.createReadStream(file).pipe(res);
res.end(panic.client);
}
}

View File

@@ -35,22 +35,22 @@ describe('A clientList', function () {
it('should return length when "len()" is called', function () {
list.add(client);
expect(list.len()).to.eq(1);
expect(list.length).to.eq(1);
list.remove(client);
expect(list.len()).to.eq(0);
expect(list.length).to.eq(0);
});
it('should not add disconnected clients', function () {
client.socket.connected = false;
list.add(client);
expect(list.len()).to.eq(0);
expect(list.length).to.eq(0);
});
it('should remove a client on disconnect', function () {
list.add(client);
expect(list.len()).to.eq(1);
expect(list.length).to.eq(1);
client.socket.emit('disconnect');
expect(list.len()).to.eq(0);
expect(list.length).to.eq(0);
});
it('should resolve a promise when all clients finish', function (done) {
@@ -88,11 +88,11 @@ describe('A clientList', function () {
describe('filter', function () {
it('should not mutate the original list', function () {
list.add(client);
expect(list.len()).to.eq(1);
expect(list.length).to.eq(1);
list.filter(function () {
return false;
});
expect(list.len()).to.eq(1);
expect(list.length).to.eq(1);
});
it('should return a new, filtered list', function () {
@@ -101,15 +101,15 @@ describe('A clientList', function () {
var browsers = list.filter(function (client) {
return client.platform.name !== 'Node.js';
});
expect(servers.len()).to.eq(1);
expect(browsers.len()).to.eq(0);
expect(servers.length).to.eq(1);
expect(browsers.length).to.eq(0);
});
it('should be reactive to changes to the parent list', function () {
var servers = list.filter('Node.js');
expect(servers.len()).to.eq(0);
expect(servers.length).to.eq(0);
list.add(client);
expect(servers.len()).to.eq(1);
expect(servers.length).to.eq(1);
});
});
@@ -117,7 +117,7 @@ describe('A clientList', function () {
it('should not contain excluded clients', function () {
list.add(client);
var filtered = list.excluding(list);
expect(filtered.len()).to.eq(0);
expect(filtered.length).to.eq(0);
});
it('should react to removals if they are connected', function () {
@@ -127,9 +127,9 @@ describe('A clientList', function () {
.add(decoy);
var filtered = list.excluding(exclusion);
list.add(client).add(new Client());
expect(filtered.len()).to.eq(1);
expect(filtered.length).to.eq(1);
exclusion.remove(client).remove(decoy);
expect(filtered.len()).to.eq(2);
expect(filtered.length).to.eq(2);
});
});
@@ -138,26 +138,26 @@ describe('A clientList', function () {
list.add(client)
.add(new Client())
.add(new Client());
expect(list.pluck(1).len()).to.eq(1);
expect(list.pluck(1).length).to.eq(1);
});
it('should listen for additions', function () {
var subset = list.pluck(2);
expect(subset.len()).not.to.eq(2);
expect(subset.length).not.to.eq(2);
list.add(new Client()).add(new Client());
expect(subset.len()).to.eq(2);
expect(subset.length).to.eq(2);
list.add(new Client());
expect(subset.len()).to.eq(2);
expect(subset.length).to.eq(2);
});
it('should replace a client when it disconnects', function () {
var subset = list.pluck(1);
list.add(client).add(new Client());
expect(subset.len()).to.eq(1);
expect(subset.length).to.eq(1);
client.socket.emit('disconnect');
// It should be replaced with
// the second connected client.
expect(subset.len()).to.eq(1);
expect(subset.length).to.eq(1);
});
it('should set a flag whether the constraint is met', function () {
@@ -175,8 +175,8 @@ describe('A clientList', function () {
list.add(client)
.add(new Client())
.add(new Client());
expect(alice.len()).to.eq(1);
expect(bob.len()).to.eq(1);
expect(alice.length).to.eq(1);
expect(bob.length).to.eq(1);
});
});
});
@@ -208,4 +208,19 @@ describe('The ClientList constructor', function () {
list1.add(client3);
expect(list.get(client3.socket.id)).to.eq(client3);
});
it('should be subclassable', function () {
function Sub() {
ClientList.call(this);
}
Sub.prototype = new ClientList();
Sub.prototype.constructor = Sub;
var sub = new Sub();
expect(sub).to.be.an.instanceof(Sub);
// chained inheritance
expect(sub.filter('Firefox')).to.be.an.instanceof(Sub);
expect(sub.pluck(1)).to.be.an.instanceof(Sub);
});
});