Skip to content

Instantly share code, notes, and snippets.

@bajtos
Last active January 13, 2016 00:08
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bajtos/b7e001423e17fad457ae to your computer and use it in GitHub Desktop.
Save bajtos/b7e001423e17fad457ae to your computer and use it in GitHub Desktop.
LoopBack Realtime

Imagine a chat server persisting the messages, a simplified Slack chat. The domain is designed in the object-orientated style. In order to get realtime updates, we need to transport events emitted on the model constructor ("static" events) and on a model instance ("instance" events).

While it may not be immediately clear, the example is covering few other important LoopBack concepts too:

  • authorization (loopback.token in REST)
  • current context (loopback.context in REST)

What is not covered:

  • file uploads and downloads
// will be done via common/models/chat-room.{js,json}
var ChatRoom = loopback.createModel('ChatRoom', { name: 'string' });
ChatRoom.prototype.join = function(cb) {
var user = loopback.getCurrentContext().get('currentUser');
this.users.link(user, function(err) {
if (err) return cb(err);
this.emit('joined', user); // converted to JSON by strong-remoting
cb();
});
};
ChatRoom.remoteMethod('join', { /*...*/ });
ChatRoom.prototype.post = function(message, cb) {
var user = loopback.getCurrentContext().get('currentUser');
this.posts.create(
{
author: user.id,
message: message
},
function(err, post) {
if (err) return cb(err);
this.emit('posted', post); // converted to JSON by strong-remoting
cb();
});
}
ChatRoom.remoteMethod('post', { /*...*/ });
/** server/server.js **/
var app = loopback();
app.dataSource('db', { connector: 'memory' });
app.model(ChatRoom, { dataSource: 'db' });
app.setupWebsocketTransport(); // TODO
app.listen();
// model is defined using the browserified client
var ChatRoom = app.models.ChatRoom;
var User = app.models.User;
function login(username, password, cb) {
User.login(
{ username: username, password: password},
function(err, token) {
if (err) return cb(err);
ChatRoom.find(function(err, allRooms) {
if (err) return cb(err);
// TODO: GUI - render list of rooms
ChatRoom.on('changed', function(room) {
// TODO: GUI - add a new room to the list
});
ChatRoom.on('deleted', function(roomId) {
// TODO: GUI - remove the room from a list
});
cb();
});
});
}
function join(room, cb) {
room.join(function(err) {
if (err) return cb(err);
async.parallel([
function fetchPosts(next) {
room.posts(function(err, posts) {
if (err) return next(err);
// TODO: GUI - render a list of existing posts
room.on('posted', function(post) {
// TODO: GUI - add the new post to the chat window
});
next();
});
},
function fetchUsers(next) {
room.users(function(err, users) {
if (err) return next(err);
// TODO: GUI - render a list of users
room.on('joined', function(user) {
// TODO: add user to the list of connected users
});
next();
});
},
], cb);
};
}
function post(room, message, cb) {
room.post(message, function(err, msg) {
if (err) return cb(err);
// TODO: GUI - add the new post to the chat window
cb();
});
}
@ritch
Copy link

ritch commented Nov 17, 2014

Oh cool... so this is using the original remote on() method approach. I like how easy this makes creating rooms... Under the hood I'm not sure how we can ensure only events emitted on a model instance will be sent to users who are listening. My current thinking is:

// client
room.on('posted', ...) // => invokes remote method

// server
ChatRoom.prototype.on = function(event) {
  var connection = loopback.getCurrentContext().get('currentWsConnection'); // name TBD
  this.addConnectionForEvent(event, connection);
  // ...
}

ChatRoom.prototype.emit = function(event, data) {
  this.getAliveConnectionsForEvent(event).forEach(sendData);
}

The other benefit of using on() as a remote method is that you can protect data sent over events by controlling access (ACLs) to the on() method (which could be a READ operation).

// create a role to determine if the user is allowed to view a room
Role.register('$chatRoomMember', function() {
  var room = ...;
  var userId = ...;
  return ~room.members.indexOf(userId);
});

Here's what an ACL might look like to secure the data sent over web sockets:

// allow chat room members to listen for events on a room
{
  principalType: 'ROLE',
  principalId: '$chatRoomMember',
  permission: 'ALLOW',
  property: 'on'
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment