Last active
December 24, 2015 09:58
-
-
Save 3rd-Eden/6780379 to your computer and use it in GitHub Desktop.
Custom transports with Primus
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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