Skip to content

Instantly share code, notes, and snippets.

@3rd-Eden
Last active December 24, 2015 09:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 3rd-Eden/6780379 to your computer and use it in GitHub Desktop.
Save 3rd-Eden/6780379 to your computer and use it in GitHub Desktop.
Custom transports with Primus
'use strict';
var Primus = require('primus')
, http = require('http');
//
// Primus primary focus was to make it easy to build real-time applications and switch
// between real-time frameworks, encoders and what more without losing the API you're
// building upon. There are a couple of frameworks that are supported out of the box
//
// - socket.io
// - engine.io
// - sockjs
// - browserchannel
// - websockets
//
// But the same where you specify the `transformer` as string also accepts an `function`
// This ways you can use third-party provided transformers and encoders. This is
// something that has been supported since Primus 1.0 but it's not heavily documented
// this gist gives you a small overview on what it takes to build your own custom
// transport for primus.
//
var server = http.createServer()
, primus = new Primus(server, { transformer: 'websockets' });
//
// In the example above you a new Primus server is created and it will be using one
// of the default provided transformers, which is websockets in this case. The given
// transformer property checked as following:
//
// - String supplied:
// - toLowerCase() the string
// - the string is checked against our transformers.json file: https://github.com/primus/primus/blob/master/transformers.json
// - if we don't have a match, we'll send an unknown transformer error.
// - require the transformer out of our /transformers/<name> folder
// - The require failed with MODULE_NOT_FOUND, output list of dependencies
// - No MODULE_NOT_FOUND, throw the error
//
// - Anything other than a string:
// - Assume custom transport (the /primus/spec will also show tranformer: custom)
// - If it's not a function, throw an error
// - The function is initialised, as we assume that it inherits from our Transformer base class.
//
//
// So to implement a custom transport we need to inherit from the Transformer base class, luckly
// this is exposed on our Primus constructor:
//
var Transformer = require('primus').Transformer;
//
// The Transformer base class has a nice .exend({}) method to add properties to the base class
// Just like you've been doing with Backbone. I loved this pattern, so I've also added to the
// Transformer base class. There are 3 important properties that are needed for a Transformer:
//
// - .client; A function that is executed in the Primus.js client(browser) library
// - .server; A function that is executed for the server side.
// - .library; An optional library that needs to be loaded in the Primus.js client
// as on the server side you just have plain'ol node.js requires.
//
var CustomTransformer = Transformer.extend({
server: function () {
// only executed on the server
var transformer = this
, Spark = this.Spark;
},
client: function () {
// only executed on the client
},
library: 'optional string/library code that will be included'
});
//
// So you can use it as:
//
var primus = new Primus(server, { transformer: CustomTransformer });
//
// (note: as we're using the backbone .extend pattern, you can also easily inhert or extend
// our base classes. So if you want to use `websockets` instead of `ws` for as websocket
// library, you can just extend the websocket transformer and override the server method)
//
module.exports = require('primus/transformers/websockets').extend({
server: function (transformer) {
// setup a websockets server here.
}
});
//
// THE SERVER SIDE;
//
// The following piece of information is only needed to get started with building the server
// side compontent of your custom transformer.
//
// All connections on the serverside are initated by the Spark class. To create a new connection
// you only need to create a new Spark instance and it will automatically announce it self as a
// new connection to the primus server. After you've created the Spark instance you usually only
// have to hook up some EventEmitter proxy and you're done.
//
// The Spark does accept some required arguments:
//
// - arg1: headers, the request headers of the incoming connection
// - arg2: socket/ip, reference to a socket or object containing the ip and port of the user
// - arg3: querystring, object with a parsed querty string.
// - arg4: id, optional UNIQUE string that we can use as an id, most framework provide one so
// we want to re-use it instead of re-generating it.
//
Transformer.extend({
server: function () {
var Spark = this.Spark;
//
// All our Transformers register their framework on the .service property so it can be inspected
// easily when needed. So this might be a pattern you want to adopt.
//
this.service = new Framework();
this.service.on('connection', function (connection) {
var spark = new Spark(connection.headers, connection.socket, connection.query, connection.id);
//
// There are a couple events that are emitted from the created spark instance that we should
// listen to:
//
// - outgoing::end, close the connection
// - outgoing::data, send data to the connection
//
spark.on('outgoing::end', function () { connection.end() });
spark.on('outgoing::data', function (data) { connection.write(data) });
//
// In addition to listing to events, it should also emit events so we know when the socket has
// been closed or received data:
//
// - incoming::data, new data has been received
// - incoming::end, the socket has closed.
//
// Most of the frameworks are EventEmitter based, we have a really sweet Spark#emits method
// which automatically prefixes the given event with `incoming::` and returns a function which
// will emit the event and proxy the arguments.
//
connection.on('data', spark.emits('data'));
connection.on('end', spark.emits('end'));
});
}
});
//
// THE CLIENT SIDE;
// The client side function is copied to the Primus.js file by doing a fn.toString() on the given function.
// Make sure you don't have any external references as they will not be copied. Try to keep everything in
// the scope or use the `library` property.
//
Transformer.extend({
client: function () {
var primus = this
, socket;
//
// The client only needs to connect once the `outgoing::open` event is emitted. You can use this function
// to bootstrap or initialise some code.
//
this.on('incoming::open', function () {
if (socket) socket.end(); // I always close the socket before I open it, to prevent multiple connections
//
// The primus.uri method is used to properly format the connection url. The protocol should be a none
// secure version as it will automatically suffix the protocol with an `s`. There are a couple more
// options that can be configured:
//
// - query: include the given query string
// - object: return a object instead of a string
//
socket = new Connection(primus.uri({ protocol: 'ws', query: true }));
//
// Again, like the server, we need to proxy some events from the socket so we can receive data and
// know when the client has been closed. You can either manually emit the `data`, `open`, `end`
// and `error` events with an `incoming::` prefix or use our `primus#emits` method to proxy it.
//
socket.on('open', primus.emits('open'));
socket.on('error', primus.emits('error'));
socket.on('close', primus.emits('end'));
socket.on('data', primus.emits('data', function (data) {
//
// You can also provide the emits with an a "parser" function to clean up the data structure
// before you send it to Primus. This is useful for the case of WebSockets for example as it's
// in an `event.data`.
//
return data.data;
});
});
//
// The `outgoing::data` is emitted when we need to write data to the connection.
//
this.on('outgoing::data', function (data) {
if (socket) socket.write(data);
});
//
// The `outgoing::end` is called when the user wants us to close the connection.
//
this.on('outgoing::end', function () {
if (socket) {
socket.end();
socket = null;
}
});
//
// The last but not least event you need to listen for is the `outgoing::reconnect`
// event which should reconnect the current connection. If it's not possible, just
// close the "current" connection and create a new one.
//
this.on('outgoing::reconnect', function () {
this.emit('outgoing::end');
this.emit('outgoing::open');
});
}
});
//
// That should be about it.
//
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment