@mistic
For starters, don’t think about using WebSockets as a different approach to data. WebSockets allow you to consume data faster and more efficiently in real time, as well as to receive data (and not just request data), but really they are just a transport, and a transport is just an implementation detail.
###Start by building the API
Use express / express + sails or some other method and build a REST API that meets your needs, as well as an adapter / serializer that allows you to consume it with Ember-Data. Once you have this API, using this same API via websockets and receiving push events is trivial. Having a solid API is the most important detail.
###How do I include socket.io?
This part depends on context. Here are a few of the lesson’s I’ve learned.
####Mobile
WebSockets don’t work well with Mobile. Specifically, reconnections following someone closing their browser or Cordova app aren’t consistent. This is because phones shut down processes when the app is closed, so while your webpage might still be right there when the app resumes, your connection is not.
If you really need to support Mobile, build a socket-controller to wrap your socket.io connection to ferry requests / rebind event listeners to whatever websocket instance it has. This makes it very easy to simply swap out websocket instances without breaking functionality/listeners defined in another part of your app.
####Use an initializer
You only want one socket.io instance. Create an initializer for your app that registers it.
e.g.
import config from "../config/environment";
export default {
name: "socket",
initialize: function (container, application) {
var socket = io(config.io.url, config.io.options);
container.register('socket:main', socket, { instantiate: false, singleton: true });
//expose it within controllers / routes / adapters as `this.get('socket')`
application.inject('controller', 'socket', 'socket:main');
application.inject('route', 'socket', 'socket:main');
application.inject('adapter', 'socket', 'socket:main');
}
};
Inject where needed
Ember now has services. This is a great candidate for use as a service, because you can inject it only where needed using the services API.
Mine do not use services, because Services weren’t available when I built the app. Instead of injecting into all routes / controllers / adapters as I do you may want to only inject it specifically when desired.
####Socket Listeners
Event though you may expose the socket throughout your app, only add listeners in a centralized place.
For instance, I have a pusher class
import Ember from "ember";
export default Ember.Object.extend({
socketEvents : {},
init : function () {
this._super();
var socket = this.get('socket'),
events = this.get('socketEvents'),
eventNames = Ember.keys(events),
cb = function (method, data) {
Ember.run.schedule(
'backgroundPush',
this,
method,
data
);
},
name,
i;
for (i = 0; i < eventNames.length; i++) {
name = eventNames[i];
socket.on(name, cb.bind(this, events[name]));
}
}
});
Which I use to add events in an organized manner.
You would use it like this
import Pusher from "../utils/pusher";
export default Pusher.extend({
socketEvents : {
'an-event-name' : function () {
/* handle the event */
}
}
});
And it would need to be initialized (I use an initializer to start all my pushers).
####Errors will close your socket
My pusher class is doing two special things you need to know about. The first, is that it wraps the method you define for the callback in a call to Ember.run.schedule
. This means that ALL the processing for your event is done asynchronously. Otherwise, any error thrown in your callback (or very long running callbacks) will cause your socket connection to close.
####Scheduling will save you from processing data at the wrong time
The second thing to notice is that I’m not using setTimeout
or Ember.run.later
or Ember.run.next
to make the method async, I’m using Ember.run.schedule
. If you don’t know anything about Ember’s run loop, I suggest reading up on it, but basically by using schedule you can give Ember information about the relative importance of the function running, and ensure that anything more critical is run first.
For instance, getting a push form your server with a lot of data right while you are in the middle of rendering a new route will likely cause your app to lock up for a moment noticeably. Its better with “push” data to process it after anything that’s affecting the view at the moment.
You could use the afterRender
queue for this processing, but personally I like to reserve afterRender
for use by tasks involved with what’s on the screen at the moment and I define a new queue for handling stuff that isn’t (such as push data).
backgroundPush
is a custom queue that I’ve defined. If you want to use the same concept, in app.js
before you call Ember.Application.extend
you will want to add the line
Ember.run._addQueue('backgroundPush', 'afterRender');
which tells Ember to add the backgroundPush
queue and to run that queue after it runs the afterRender
queue.
####Authentication
In my apps, the websocket handles everything, including authentication. With socket.io, this is tricky.
-
socket.io sends a GET request to establish the socket, so no payload can be sent
-
socket.io does not allow the use of cookies or custom headers, so auth information cannot be sent as cookies or as custom headers
In fact, the “recommended” way of sending an auth token with socket.io when creating a new connection is to send it in the url, like this
https://example.com:3030/?auth=my-auth-token
This is not secure, even on HTTPS. In my setup, I allow unauthenticated sockets to connect, and once the connection is established, I send the authorization handshake though the socket connection, which avoids exposing your auth regardless of what method you are using to authenticate.