Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"it": true,
"describe": true,
"before": true,
"beforeEach": true,
"after": true,
"afterEach": true,
"exports": true
},
"unused": true,
Expand Down
3 changes: 2 additions & 1 deletion lib/mixins/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ var EventEmitter = require('events').EventEmitter;
var eventMappings = {
create: 'created',
update: 'updated',
remove: 'removed'
remove: 'removed',
patch: 'patched'
};

/**
Expand Down
86 changes: 56 additions & 30 deletions lib/providers/socketio.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,55 @@ var paramsPositions = {
patch: 2
};

module.exports = function(config) {
return function() {
// Set up the service method handlers for a service and socket.
function setupMethodHandler (socket, service, path, method) {
var name = path + '::' + method;
var position = typeof paramsPositions[method] !== 'undefined' ? paramsPositions[method] : 1;

if (typeof service[method] === 'function') {
socket.on(name, function () {
var args = _.toArray(arguments);
args[position] = _.extend({}, args[position], socket.handshake.feathers);
service[method].apply(service, args);
});
}
}

// Set up event handlers for a given service and connected sockets.
// Send it through the service dispatching mechanism (`removed(data, params, callback)`,
// `updated(data, params, callback)` and `created(data, params, callback)`) if it
// exists.
function setupEventHandler (sockets, service, path, ev) {
var defaultDispatcher = function (data, params, callback) {
callback(null, data);
};

service.on(ev, function (data) {
// Check if there is a method on the service with the same name as the event
var dispatcher = typeof service[ev] === 'function' ? service[ev] : defaultDispatcher;
var eventName = path + ' ' + ev;

sockets.clients().forEach(function (socket) {
dispatcher(data, socket.handshake.feathers, function (error, dispatchData) {
if (error) {
socket.emit('error', error);
} else if (dispatchData) {
socket.emit(eventName, dispatchData);
}
});
});
});
}

module.exports = function (config) {
return function () {
var app = this;
var services = {};

app.enable('feathers socketio');

// Monkey patch app.setup(server)
Proto.mixin({
setup: function(server) {
setup: function (server) {
var self = this;
var result = this._super.apply(this, arguments);

Expand All @@ -31,32 +70,23 @@ module.exports = function(config) {

var io = this.io = socketio.listen(server);

_.each(services, function(service, path) {
// If the service emits events that we want to listen to (Event mixin)
if (typeof service.on === 'function' && service._serviceEvents) {
_.each(service._serviceEvents, function(ev) {
service.on(ev, function(data) {
io.sockets.emit(path + ' ' + ev, data);
});
// For a new connection, set up the service method handlers
io.sockets.on('connection', function (socket) {
_.each(self.services, function (service, path) {
_.each(self.methods, function (method) {
setupMethodHandler(socket, service, path, method);
});
}
});
});

io.sockets.on('connection', function(socket) {
_.each(services, function(service, path) {
_.each(self.methods, function(method) {
var name = path + '::' + method;
var position = typeof paramsPositions[method] !== 'undefined' ? paramsPositions[method] : 1;

if (typeof service[method] === 'function') {
socket.on(name, function() {
var args = _.toArray(arguments);
args[position] = _.extend({}, args[position], socket.handshake.feathers);
service[method].apply(service, args);
});
}
// Set up events and event dispatching
_.each(self.services, function (service, path) {
// If the service emits events that we want to listen to (Event mixin)
if (typeof service.on === 'function' && service._serviceEvents) {
_.each(service._serviceEvents, function (ev) {
setupEventHandler(io.sockets, service, path, ev);
});
});
}
});

if (typeof config === 'function') {
Expand All @@ -66,9 +96,5 @@ module.exports = function(config) {
return result;
}
}, app);

app.providers.push(function(path, service) {
services[path] = service;
});
};
};
92 changes: 90 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ var myService = {
get: function(id, params, callback) {},
create: function(data, params, callback) {},
update: function(id, data, params, callback) {},
patch: function(id, data, params, callback) {},
remove: function(id, params, callback) {},
setup: function(app) {}
}
Expand Down Expand Up @@ -323,6 +324,25 @@ socket.emit('todo::update', 2, {
});
```

### patch

`patch(id, data, params, callback)` patches the resource identified by `id` using `data`. The callback should be called with the updated resource data. Implement `patch` additionally to `update` if you want to separate between partial and full updates and support the `PATCH` HTTP method.

__REST__

PATCH todo/2
{ "description": "I really have to do laundry" }

__SocketIO__

```js
socket.emit('todo::patch', 2, {
description: 'I really have to do laundry'
}, {}, function(error, data) {
// data -> { id: 2, description: "I really have to do laundry" }
});
```

### remove

`remove(id, params, callback)` removes the resource with `id`. The callback should be called with the removed resource.
Expand Down Expand Up @@ -437,9 +457,9 @@ __SocketIO__
</script>
```

### updated
### updated, patched

The `updated` event will be published with the callback data when a service `update` calls back successfully.
The `updated` and `patched` events will be published with the callback data when a service `update` or `patch` method calls back successfully.

```js
app.use('/my/todos/', {
Expand Down Expand Up @@ -502,6 +522,74 @@ __SocketIO__
</script>
```

### Event filtering

By default all service events will be dispatched to all connected clients.
In many cases you probably want to be able to only dispatch events for certain clients.
This can be done by implementing the `created`, `updated`, `patched` and `removed` methods as `function(data, params, callback) {}` with `params` being the parameters set when the client connected, in SocketIO when authorizing and setting `handshake.feathers` and Primus with `req.feathers`.

```js
var myService = {
created: function(data, params, callback) {},
updated: function(data, params, callback) {},
patched: function(data, params, callback) {},
removed: function(data, params, callback) {}
}
```

The event dispatching service methods will be run for every connected client. Calling the callback with data (that you also may modify) will dispatch the according event. Callling back with a falsy value will prevent the event being dispatched to this client.

The following example only dispatches the Todo `updated` event if the authorized user belongs to the same company:

```js
app.configure(feathers.socketio(function(io) {
io.set('authorization', function (handshake, callback) {
// Authorize using the /users service
app.lookup('users').find({
username: handshake.username,
password: handshake.password
}, function(error, user) {
if(!error || !user) {
return callback(error, false);
}

handshake.feathers = {
user: user
};

callback(null, true);
});
});
}));

app.use('todos', {
update: function(id, data, params, callback) {
// Update
callback(null, data);
},

updated: function(todo, params, callback) {
// params === handshake.feathers
if(todo.companyId === params.user.companyId) {
// Dispatch the todo data to this client
return callback(null, todo);
}

// Call back with a falsy value to prevent dispatching
callback(null, false);
}
});
```

On the client:

```js
socket.on('todo updated', function(data) {
// The client will only get this event
// if authorized and in the same company
});
```

## Why?

We know! Oh God another NodeJS framework! We really didn't want to add another name to the long list of NodeJS web frameworks but also wanted to explore a different approach than any other framework we have seen. We strongly believe that data is the core of the web and should be the focus of web applications.
Expand Down
81 changes: 78 additions & 3 deletions test/providers/socketio.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ describe('SocketIO provider', function () {
name: 'created event'
};

socket.on('todo created', function (data) {
socket.once('todo created', function (data) {
verify.create(original, data);
done();
});
Expand All @@ -166,21 +166,96 @@ describe('SocketIO provider', function () {
name: 'updated event'
};

socket.on('todo updated', function (data) {
socket.once('todo updated', function (data) {
verify.update(10, original, data);
done();
});

socket.emit('todo::update', 10, original, {}, function () {});
});

it('patched', function(done) {
var original = {
name: 'patched event'
};

socket.once('todo patched', function (data) {
verify.patch(12, original, data);
done();
});

socket.emit('todo::patch', 12, original, {}, function () {});
});

it('removed', function (done) {
socket.on('todo removed', function (data) {
socket.once('todo removed', function (data) {
verify.remove(333, data);
done();
});

socket.emit('todo::remove', 333, {}, function () {});
});
});

describe('Event filtering', function() {
it('.created', function (done) {
var service = app.lookup('todo');
var original = { description: 'created event test' };
var oldCreated = service.created;

service.created = function(data, params, callback) {
assert.deepEqual(params, socketParams);
verify.create(original, data);

callback(null, _.extend({ processed: true }, data));
};

socket.emit('todo::create', original, {}, function() {});

socket.once('todo created', function (data) {
service.created = oldCreated;
// Make sure Todo got processed
verify.create(_.extend({ processed: true }, original), data);
done();
});
});

it('.updated', function (done) {
var original = {
name: 'updated event'
};

socket.once('todo updated', function (data) {
verify.update(10, original, data);
done();
});

socket.emit('todo::update', 10, original, {}, function () {});
});

it('.removed', function (done) {
var service = app.lookup('todo');
var oldRemoved = service.removed;

service.removed = function(data, params, callback) {
assert.deepEqual(params, socketParams);

if(data.id === 23) {
// Only dispatch with given id
return callback(null, data);
}

callback();
};

socket.emit('todo::remove', 1, {}, function() {});
socket.emit('todo::remove', 23, {}, function() {});

socket.on('todo removed', function (data) {
service.removed = oldRemoved;
assert.equal(data.id, 23);
done();
});
});
});
});