UNPKG

happn-primus

Version:

Primus is a simple abstraction around real-time frameworks. It allows you to easily switch between different frameworks without any code changes.

1,265 lines (990 loc) 85.3 kB
moved package "lws": "0.6.x", to 0.8.0 removed uws support # Primus [![Version npm](https://img.shields.io/npm/v/happn-primus.svg?style=flat-square)](https://www.npmjs.com/package/happn-primus)[![Build Status](https://img.shields.io/travis/happner/primus/master.svg?style=flat-square)](https://travis-ci.org/happner/primus)[![Dependencies](https://img.shields.io/david/happner/primus.svg?style=flat-square)](https://david-dm.org/happner/primus) Primus, the creator god of transformers but now also known as universal wrapper for real-time frameworks. There are a lot of real-time frameworks available for Node.js and they all have different opinions on how real-time should be done. Primus provides a common low level interface to communicate in real-time using various real-time frameworks. ### Advantages 1. Effortless switching between real-time frameworks by changing one single line of code. No more API rewrites needed when your project requirements change, the framework gets abandoned or simply breaks down. 2. Built-in reconnect, it just works. The reconnect is controlled by a randomised exponential back-off algorithm to reduce server stress. 3. Offline detection, Primus is smart enough to detect when users drop their internet connection (switching WIFI points/cell towers for example) and reconnects when they are back online. 4. Automatically encodes and decodes messages using custom parsers. Can be easily switched for binary encoding for example. 5. A clean, stream-compatible interface for the client and server. You can just `stream#pipe` data around. In addition to that, the client works on Node.js as well, write once, run it everywhere. 6. Fixes various of bugs in the supported frameworks and additional stability patches to improve real-time communication. 7. Comes with an amazing plugin interface to keep the core library as fast and lean as possible while still allowing the server and the client to be extended. 8. Last but not least, Primus is built with love, passion and dedication to the real-time web. ``` If you have questions or need help with primus, come chat in our IRC room: server: irc.freenode.net room: #primus ``` ### Installation Primus is released on `npm` and can be installed using: ``` npm install happn-primus --save ``` ### Before Starting If you deploy your application behind a reverse proxy (Nginx, HAProxy, etc.) you might need to add WebSocket specific settings to its configuration files. If you intend to use WebSockets, please ensure that these settings have been added. There are some example configuration files available in the [observing/balancerbattle](https://github.com/observing/balancerbattle) repository. ### Table of Contents - [Introduction](#primus) - [Advantages](#advantages) - [Installation](#installation) - [Getting started](#getting-started) - [Client library](#client-library) - [Connecting from the browser](#connecting-from-the-browser) - [Connecting from the server](#connecting-from-the-server) - [Authorization](#authorization) - [Broadcasting](#broadcasting) - [Destruction](#destruction) - [Events](#events) - [Heartbeats and latency](#heartbeats-and-latency) - [Supported real-time frameworks](#supported-real-time-frameworks) - [BrowserChannel](#browserchannel) - [Engine.IO](#engineio) - [Faye](#faye) - [lws](#lws) - [Socket.IO](#socketio) - [SockJS](#sockjs) - [WebSockets](#websockets) - [Transformer inconsistencies](#transformer-inconsistencies) - [Parsers](#parsers) - [Middleware](#middleware) - [Plugins](#plugins) - [Extending the Spark / Socket](#extending-the-spark--socket) - [Transforming and intercepting messages](#transforming-and-intercepting-messages) - [Primus project plugins](#primus-project-plugins) - [Community plugins](#community-plugins) - [Community](#community) - [FAQ](#FAQ) - [Scaling](#what-is-the-best-way-to-scale-primus) - [Cluster](#can-i-use-cluster) - [Express](#how-do-i-use-primus-with-express-3) - [RequireJS](#is-requirejs-supported) - [Custom headers](#can-i-send-custom-headers-to-the-server) - [Versioning](#versioning) - [History](#history) - [Convention](#convention) - [Release cycle](#release-cycle) - [Other languages](#other-languages) - [Protocol](#protocol) - [License](#license) ### Getting started Primus doesn't ship with real-time frameworks as dependencies, it assumes that you as user add them yourself as a dependency. This is done to keep the module as lightweight as possible. This works because `require` in will walk through your directories searching for `node_module` folders that have these matching dependencies. Primus needs to be "attached" to a HTTP compatible server. These includes the built-in `http` and `https` servers but also the `spdy` module as it has the same API as node servers. Creating a new Primus instance is relatively straightforward: ```js 'use strict'; var Primus = require('primus') , http = require('http'); var server = http.createServer(/* request handler */) , primus = new Primus(server, {/* options */}); ``` The following options can be provided: | Name | Description | Default | | ---------------------- | ---------------------------------------- | ---------------------------------- | | authorization | Authorization handler | `null` | | pathname | The URL namespace that Primus can own | `/primus` | | parser | Message encoder for all communication | `JSON` | | transformer | The transformer we should use internally | `websockets` | | plugin | The plugins that should be applied | `{}` | | timeout | The heartbeat timeout (__ignored, server learns timeout from client on connect__) | `35000` | | allowSkippedHeartBeats | To allow waylaid pings | `2` | | global | Set a custom client class / global name | `Primus` | | compression | Use permessage-deflate / HTTP compression | `false` | | origins | **cors** List of origins | `*` | | methods | **cors** List of accepted HTTP methods | `GET,HEAD,PUT,POST,DELETE,OPTIONS` | | credentials | **cors** Allow sending of credentials | `true` | | maxAge | **cors** Cache duration of CORS preflight | `30 days` | | headers | **cors** Allowed headers | `false` | | exposed | **cors** Headers exposed to the client | `false` | | pongSkipTime | deduplicate songs | `1000ms` | The options that are prefixed with **cors** are supplied to our [access-control](https://github.com/primus/access-control) module which handles HTTP Access Control (CORS), so for a more detailed explanation of these options check it out. The `heartbeat timeout` is used to disconnect the client if no ping arrives within that time. The timeout is not configurable, instead it is learned from the client's connect url. This allows the server to handle new client deployments with longer timeouts as well as older deployments with shorter/default timeouts. The connecting url includes the client's ping and pong values. It calculates an appropriate `heartbeat timeout` as `ping + (pong / 2)`. The division by 2 allows for the server to emit pongs to the client sufficiently timeously to avert the waylaid ping conundrum as described below. The waylaid ping conundrum occurs when the client sends a payload whose transmission time exceeds the heartbeat timeout at the server. Although the client sent the ping in time, its arrival at the server is delayed behind the large payload. So either the server closes the socket because no ping arrived in time or the client closes the socket because the server never replied with a pong in time. To avert this problem the server can be configured to allow for the missing heartbeats/pings by setting the `allowSkippedHeartBeats` option sufficiently large to encompass the duration of the large payload's transmission, specifically, `allowSkippedHeartBeats` x `heartbeat timeout` should be long enough to transmit the payload. That solves the problem of the server closing the socket for a limited number of missed pings. To solve the problem of the client closing the socket the server sends a unsolicited pong to the client when the heartbeat is skipped. For this to work the pong needs to be sent before the client times out waiting for it. To achieve this the heatbeat timeout at the server (as calculated from the connect url with the formula above) exceeds the client's ping interval to prevent the sending of unnecessary pongs, but does not exceed the client's ping interval plus pong timeout so that the unsolicited pong does arrive at the client in time. For already deplyed clients the pong timeout is 10000ms. This means that the defaulted `heartbeat timeout` at the server will cause the server to emit the unsolicited pong with only 5000ms client-bound transmission latency leeway before the client-side timeout closes the socket. For new deployments the client's deafult pong timeout is increased to 20000ms. Allowing for a 10000ms latency. If you don't have a pre-existing server where you want or can attach your Primus server to you can also use the `Primus.createServer` convenience method. The `createServer method will automatically: - Setup a HTTP, HTTPS or SPDY server for you on the given port number. - Setup your Primus server with the given configuration. - Listen on the HTTP, HTTPS, SPDY server. - Attach a `primus.on('connection')` listener. - Return the created Primus instance. ```js Primus.createServer(function connection(spark) { }, { port: 8080, transformer: 'websockets' }); ``` In the above example we automatically create a HTTP server which will listen on port 8080, a primus instance with the `websockets` transformer and start listening for incoming connections. The supplied function in the `Primus.createServer` method is optional. You can just listen for incoming connections your self using the returned Primus instance. If you want to listen to a HTTPS or SPDY server, which is recommended, you can directly pass the SPDY and HTTPS certs/keys/pfx files in the options object: ```js var primus = Primus.createServer({ port: 443, root: '/folder/with/https/cert/files', cert: 'myfilename.cert', key: 'myfilename.cert', ca: 'myfilename.ca', pfx: 'filename.pfx', passphrase: 'my super sweet password' }); primus.on('connection', function (spark) { spark.write('hello connnection'); }); ``` `Primus.createServer` returns a warning when it starts a HTTP server. The warning advises you to use a HTTPS server and can be disabled setting the option `iknowhttpsisbetter` to `true`. #### Client library As most libraries come with their own client-side framework for making the connection we've also created a small wrapper for this. The library can be retrieved using: ```js primus.library(); ``` Which returns the client-side library as a string (which can then be minified or even have more code added to it). It does not come pre-minified as that is out of the scope of this project. You can store this on a CDN or on your static server. Do whatever you want with it, but remember to regenerate it every time you change Primus server options. This is important because some properties of the client are set using the server configuration. For example if you change the `pathname`, the client should be regenerated to reflect that change and work correctly. We advise you to regenerate the library every time you redeploy so you always have a client compatible with your back-end. To save the file you can use: ```js primus.save(__dirname +'/primus.js'); ``` This will store the compiled library in your current directory. If you want to save it asynchronously, you can supply the method with a callback method: ```js primus.save(__dirname +'/primus.js', function save(err) { }); ``` But to make it easier for you during development we've automatically added an extra route to the supplied HTTP server, this will serve the library for you so you don't have to save it. Please note, that this route isn't optimised for serving static assets and should only be used during development. In your HTML page add: ```html <script src="/primus/primus.js"></script> ``` As you can see, it will use the `/primus` pathname by default. Primus needs to own the whole path/namespace in order to function properly as it will forward all other requests directly in to the transformers so they can work their magic. If you already have a static folder with the name `primus` you can change the pathname to something different and still make this work. But you would of course need to update the `src` attribute of the script tag to set the correct location. It's always available at: ``` <protocol>://<server location>/<pathname>/primus.js ``` Here `<pathname>` is the `pathname` set in server options above. The client is cross domain compatible so you don't have to serve it from the same domain you're running Primus on. But please note, that the real-time framework you're using might be tied to same domain restrictions. Once you're all set up you can start listening for connections. These connections are announced through the `connection` event. ```js primus.on('connection', function (spark) { // spark is the new connection. }); ``` Disconnects are announced using a `disconnection` event: ```js primus.on('disconnection', function (spark) { // the spark that disconnected }); ``` The `spark` argument is the actual real-time socket/connection. Sparks have a really low level interface and only expose a couple properties that are cross engine supported. The interface is modeled towards a Node.js stream compatible interface. So this will include all methods that are available on the [stream interface](https://nodejs.org/api/stream.html) including `Spark#pipe`. #### spark.headers The `spark.headers` property contains the headers of either the request that started a handshake with the server or the headers of the actual real-time connection. This depends on the module you are using. *Please note that sending custom headers from the client to the server is impossible as not all transports that these transformers support can add custom headers to a request (JSONP for example). If you need to send custom data, use a query string when connecting* #### spark.address The `spark.address` property contains the `ip` and `port` of the connection. If you're running your server behind a reverse proxy it will automatically use the `x-forwarded-for` header. This way you will always have the address of the connecting client and not the IP address of your proxy. *Please note that the `port` is probably out of date by the time you're going to read it as it's retrieved from an old request, not the request that is active at the time you access this property.* #### spark.query The `spark.query` contains the query string you used to connect to the server. It's parsed as an object. Please note that this may not be available for all supported transformers. #### spark.id This is a unique id that we use to identify this single connection with. Normally the frameworks refer to this as a `sessionid`, which is confusing as it's only used for the duration of one single connection. You should not see this as a "session id", and rather expect it to change between disconnects and reconnects. #### spark.request The `spark.request` gives you access to the HTTP request that was used to initiate the real-time connection with the server. Please note that this request is already answered and closed (in most cases) so do not attempt to write or answer it anyway. But it might be useful to access methods that get added by middleware layers, etc. #### spark.write(data) You can use the `spark.write` method to send data over the socket. The data is automatically encoded for you using the `parser` that you've set while creating the Primus server instance. This method always returns `true` on success and `false` on failure so back pressure isn't handled. ```js spark.write({ foo: 'bar' }); ``` #### spark.end(data, options) You can use `spark.end` to close the connection. This method takes two optional arguments. The first, if provided, is the `data` to send to the client before closing the connection. The second is an options object used to customize the behavior of the method. By default the `spark.end` method closes the connection in a such way that the client knows it was intentional and it doesn't attempt a reconnection. ```js spark.end(); // the client doesn't reconnect automatically ``` You can change this behavior and trigger a client-side reconnection using the `reconnect` option. ```js spark.end(undefined, { reconnect: true }); // trigger a client-side reconnection ``` #### spark.emits(event, parser) This method is mostly used internally. It works similarly to the native `bind` function, returning a function that emits the assigned `event` every time it's called. If the last argument is a function, it will be used to parse the arguments of the returned function. The `parser` is optional and always async, its **first** argument is a callback that follows the usual error first pattern, all successive arguments are the ones to parse. Using the `parser` you can reduce the arguments down to a single value, remove them completely or prevent the event from being emitted. See [emits](https://github.com/primus/emits) for detailed usage instructions. ```js spark.emits('event', function parser(next, structure) { next(undefined, structure.data); }); ``` Please note that the data that is received here isn't decoded yet. #### spark.on('data') The `data` event is emitted when a message is received from the client. It's automatically decoded by the specified decoder. ```js spark.on('data', function message(data) { // the message we've received. }); ``` #### spark.on('end') The `end` event is emitted when the client has disconnected. ```js primus.on('connection', function (spark) { console.log('connection has the following headers', spark.headers); console.log('connection was made from', spark.address); console.log('connection id', spark.id); spark.on('data', function (data) { console.log('received data from the client', data); // // Always close the connection if we didn't receive our secret imaginary // handshake. // if ('foo' !== data.secrethandshake) spark.end(); spark.write({ foo: 'bar' }); spark.write('banana'); }); spark.write('Hello world'); }) ``` ### Connecting from the Browser Primus comes with its client framework which can be compiled using `primus.library()` as mentioned above. To create a connection you can simply create a new Primus instance: ```js var primus = new Primus(url, { options }); // // But it can be easier, with some syntax sugar. // var primus = Primus.connect(url, { options }); ``` The URL should confirm the following conditions: - It should include the protocol it needs to connect with. This can either be `http` or `https`. We recommend that you're using HTTPS for all your connections as this prevents connection blocking by firewalls and anti-virus programs. - The URL should not include a pathname. The pathname is configured by the server (See: [getting-started](#getting-started)) and needs to be configured there as it will be compiled in to the `primus.js` client file. If no `url` argument is passed, it will default to the current URL. The following options can be provided: | Name | Description | Default | | ----------- | --------------------------------------- | ----------------------------- | | [reconnect] | Configures the exponential back off | `{}` | | timeout | Connect time out | `10000` ms | | ping | Ping interval to test connection | `25000` ms (false to disable) | | pong | Time the server has to respond to ping | `10000` ms | | [strategy] | Our reconnect strategies | `"disconnect,online,timeout"` | | manual | Manually open the connection | `false` | | websockets | Should we use WebSockets | Boolean, is detected | | network | Use native `online`/`offline` detection | Boolean, is feature detected | | transport | Transport specific configuration | `{}` | | queueSize | Number of messages that can be queued | `Infinity` | There are 2 important options that we're going to look a bit closer at. ##### Reconnect When the connection goes down unexpectedly an automatic reconnect process is started. It uses a randomised exponential back-off algorithm to prevent clients from DDoSing your server when you reboot as they will all be re-connecting at different times. The reconnection can be configured using the `options` argument in `Primus` and you should add these options to the `reconnect` property: | Name | Description | Default | | ----------------- | ---------------------------------------- | ---------- | | max | Maximum delay for a reconnection attempt | `Infinity` | | min | Minimum delay for a reconnection attempt | `500` ms | | retries | Maximum amount of attempts | `10` | | reconnect timeout | Maximum time for an attempt to complete | `30000` ms | | factor | Exponential back off factor | `2` | ```js primus = Primus.connect(url, { reconnect: { max: Infinity // Number: The max delay before we try to reconnect. , min: 500 // Number: The minimum delay before we try reconnect. , retries: 10 // Number: How many times we should try to reconnect. } }); ``` When you're going to customize `min` please note that it will grow exponentially e.g. `500 -> 1000 -> 2000 -> 4000 -> 8000` and is randomized so expect to have slightly higher or lower values. Please note that when we reconnect, we will receive a new `connection` event on the server and a new `open` event on the client, as the previous connection was completely dead and should therefore be considered a new connection. If you are interested in learning more about the backoff algorithm you might want to read http://dthain.blogspot.nl/2009/02/exponential-backoff-in-distributed.html ##### Strategy The strategy allows you to configure when you want a `reconnect` operation to kick in. We're providing some **sane** defaults for this but we still want to provide users with highest level of customization: <dl> <dt>disconnect</dt> <dd> Reconnect when we detect an unintentional disconnect in the connection. </dd> <dt>online</dt> <dd> Reconnect when the browser went from an offline event to an online event. </dd> <dt>timeout</dt> <dd> Reconnect when we failed to establish our initial connection. This can happen because we took too long to connect or because there was an error while we tried to connect (which happens when you connect to a dead server) </dd> </dl> You can supply these options as a comma-separated `String`: ```js var primus = new Primus(url, { strategy: 'online, timeout, disconnect' }) ``` Or as an `Array`: ```js var primus = new Primus(url, { strategy: [ 'online', 'timeout', 'disconnect' ]}); ``` We'll try to normalize everything as much as possible, we `toLowerCase` everything and join it back to a readable string so if you wrote `dIsconNect` it will get normalized to `disconnect`. **If you are using authentication you should disable the `timeout` strategy as there is no way of detecting the difference between a failed authorization and a failed connect. If you leave this enabled with authorization every unauthorized access will try to reconnect again**. We automatically disable this for you when you've set the authorization before you save the library. But there are always use cases where reconnection is not advised for your application. In these cases we've provided a way to completely disable the reconnection, this is done by setting the `strategy` to `false`: ```js var primus = new Primus(url, { strategy: false }); ``` If you want to manually control the reconnection you can call `primus.end()` to close the connection and `primus.open()` to establish a new one. **Be sure to use `primus.open()` correctly, see below for details.** [reconnect]: #reconnect [strategy]: #strategy ##### transport The transport object allows you to add a transport specific configuration. We only recommend using this if you understand and accept the following consequences: - Primus will try to override configuration properties that are needed to ensure a correct functioning. - We might start using options without any announcement or major version bump. - Expect your client and its connection to malfunction once you switch between different transports, as these configurations are specific to the bundled transformer library/client. - Bugs and bug reports caused by using this functionality are closed immediately. Having that said, this gives you total freedom while still getting the benefits of Primus. #### primus.open() This method opens a connection with the server. By default it is called automatically when the Primus instance is created, but there are cases where it's desirable to open the connection manually. To do this set the `manual` option to `true` and when you have the Primus instance call the method: ```js primus.open(); ``` **When you call `primus.open()` you should make sure that the connection is totally dead (e.g. after an `end` event) and primus isn't already trying or planning to reconnect**. #### primus.write(message) Once you've created your Primus instance you're ready to go. When you want to write data to your server you can just call the `.write` method: ```js primus.write('message'); ``` It automatically encodes your messages using the parser that you've specified on the server. So sending objects back and forth between the server is nothing different then just writing: ```js primus.write({ foo: 'bar' }); ``` When you are sending messages to the server, you don't have to wait for the `open` event to happen, the client will automatically buffer all the data you've send and automatically write it to the server once it's connected. The client supports a couple of different events. #### primus.on('data') The `data` event is the most important event of the whole library. It's emitted when we receive data from the server. The data that is received is already decoded by the specified parser. ```js primus.on('data', function message(data) { console.log('Received a new message from the server', data); }); ``` #### primus.on('open') The `open` event is emitted when we've successfully created a connection with the server. It will also be emitted when we've successfully reconnected after the connection goes down unintentionally. ```js primus.on('open', function open() { console.log('Connection is alive and kicking'); }); ``` #### primus.on('error') The `error` event is emitted when something breaks that is out of our control. Unlike Node.js, we do not throw an error if no `error` event listener is specified. In general, when there is an active connection, it is not directly closed when an `error` event is emitted. The cause of an error, in fact, could be that the parser failed to encode or decode a message. In this case we only emit the error, discard the message and keep the connection alive. An `error` event can also be emitted when a connection fails to establish. When this happens the client automatically tries to reconnect, unless the connection gets closed for some other reason. The only exception is when there is an authorization hook. If we get an error when connecting to a server where authorization is required, we simply close the connection, as we can't determinate if the error is the result of an unauthorized access or not. ```js primus.on('error', function error(err) { console.error('Something horrible has happened', err.stack); }); ``` #### primus.on('reconnect') The `reconnect` event is emitted when we're attempting to reconnect to the server. This all happens transparently and it's just a way for you to know when these reconnects are actually happening. ```js primus.on('reconnect', function (opts) { console.log('Reconnection attempt started'); }); ``` #### primus.on('reconnect scheduled') Looks a lot like the `reconnect` event mentioned above, but it's emitted when we've detected that connection went/is down and we're going to start a reconnect operation. This event would be ideal to update your application's UI when the connection is down and you are trying to reconnect in x seconds. ```js primus.on('reconnect scheduled', function (opts) { console.log('Reconnecting in %d ms', opts.scheduled); console.log('This is attempt %d out of %d', opts.attempt, opts.retries); }); ``` #### primus.on('reconnected') The client successfully reconnected with the server. ```js primus.on('reconnected', function (opts) { console.log('It took %d ms to reconnect', opts.duration); }); ``` #### primus.on('reconnect timeout') The `reconnect timeout` event is emitted when a reconnection attempt takes too much time. This can happen for example when the server does not answer a request in a timely manner. ```js primus.on('reconnect timeout', function (err, opts) { console.log('Timeout expired: %s', err.message); }); ``` After this event a whole new reconnection procedure is automatically started, so you don't have to worry about it. #### primus.on('reconnect failed') This event is emitted when the reconnection failed, for example when all attempts to reconnect have been unsuccessful. ```js primus.on('reconnect failed', function (err, opts) { console.log('The reconnection failed: %s', err.message); }); ``` #### primus.on('end') The `end` event is emitted when we've closed the connection. When this event is emitted you should consider your connection to be fully dead with no way of reconnecting. But it's also emitted when the server closes the connection. ```js primus.on('end', function () { console.log('Connection closed'); }); ``` #### primus.end() When you want to close the connection you can call the `primus.end()` method. After this the connection should be considered dead and a new connection needs to be made using `Primus.connect(url)` or `primus = new Primus(url)` if you want to talk with the server again. ```js primus.end(); ``` #### primus.destroy() This method literally destroys the `primus` instance. Internally it calls the `primus.end()` method but it also frees some potentially heavy objects like the underlying socket, the timers, the message transformers, etc. It also removes all the event listeners but before doing that it emits a final `destroy` event. Keep in mind that once this method is executed, you can no longer use `primus.open()` on the same `primus` instance. ```js primus.on('destroy', function () { console.log('Feel the power of my lasers!'); }); primus.destroy(); ``` #### primus.emits(event, parser) This method is analogous to the [`spark.emits`](#sparkemitsevent-parser) method. It returns a function that emits the given event every time it's called. See [emits](https://github.com/primus/emits) for detailed usage instructions. ```js primus.emits('event', function parser(next, structure) { next(undefined, structure.data); }); ``` #### primus.id(callback) There are cases where it is necessary to retrieve the [`spark.id`](#sparkid) from the client. To make this easier, we added a `primus.id()` method that takes a callback function to which the id will be passed. ```js primus.id(function (id) { console.log(id); }); ``` ### Connecting from the server The client-side library has been made compatible with Node.js so the same code base can be re-used for server side connections. There are two ways of creating a server side client. 1. When you've created your `primus` instance you can access the `Socket` property on it. This `Socket` is automatically configured to connect to the correct pathname, using the same `transformer` and `parser` that you've specified when you created your `primus` instance. ```js var primus = new Primus(server, { transformer: transformer, parser: parser }) , Socket = primus.Socket; var client = new Socket('http://localhost:8080'); // // It has the same interface as the client, so you can just socket.write or // listen for the `open` events etc. // ``` 2. You might need to connect from a different node process where you don't have access to your `primus` instance and the compatible `Socket` instance. For these cases there a special `createSocket` method where you can specify the `transformer`, `parser`, `plugin` that you are using on your server to create another compatible socket. ```js var Socket = Primus.createSocket({ transformer: transformer, parser: parser }) , client = new Socket('http://localhost:8080'); ``` ``` When you are using plugins with Primus make sure you add them **before** you reference the `primus.Socket` or it will compile a client without your plugins. If you're using the `Primus.createSocket` api you can directly supply the plugins as part of the options as it supports `plugin` object: ​```js var Socket = Primus.createSocket({ transformer: transformer, parser: parser, plugin: { 'my-emitter': require('my-emitter'), 'substream': require('substream') } }); ``` The constructor returned by `primus.Socket` or `Primus.createSocket` has the same signature of the constructor used to connect from the browser. This means that you can use all the options mentioned in the previous [section](#connecting-from-the-browser): ```js var Socket = Primus.createSocket() , client = new Socket('http://localhost:8080', { options }); ``` If you do not know which transformer and parser are used on the server, we expose a small JSON "spec" file that exposes this information. The specification can be reached on the `/<pathname>/spec` and will output the following JSON document: ```json { "version":"2.4.0", "pathname":"/primus", "parser":"json", "transformer":"websockets" } ``` ### Authorization #### Server Primus has a built-in auth hook that allows you to leverage the basic auth header to validate the connection. To setup the optional auth hook, use the `Primus#authorize` method: ```js var authParser = require('basic-auth-parser'); // // Add hook on server // primus.authorize(function (req, done) { var auth; try { auth = authParser(req.headers['authorization']) } catch (ex) { return done(ex) } // // Do some async auth check // authCheck(auth, done); }); primus.on('connection', function (spark) { // // You only get here if you make it through the auth hook! // }); ``` In this particular case, if an error is passed to `done` by `authCheck` or the exception handler then the connection attempt will never make it to the `primus.on('connection')` handler. The error you pass can either be a string or an object. If an object, it can have the following properties which affect the response sent to the client: - `statusCode`: The HTTP status code returned to the client. Defaults to 401. - `authenticate`: If set and `statusCode` is 401 then a `WWW-Authenticate` header is added to the response, with a value equal to the `authenticate` property's value. - `message`: The error message returned to the client. The response body will be `{error: message}`, JSON-encoded. If the error you pass is a string then a 401 response is sent to the client with no `WWW-Authenticate` header and the string as the error message. For example to send 500 when an exception is caught, 403 for forbidden users and details of the basic auth scheme being used when authentication fails: ```js primus.authorize(function (req, done) { var auth; if (req.headers.authorization) { try { auth = authParser(req.headers.authorization) } catch (ex) { ex.statusCode = 500; return done(ex); } if ((auth.scheme === 'myscheme') && checkCredentials(auth.username, auth.password)) { if (userAllowed(auth.username)) { return done(); } else { return done({ statusCode: 403, message: 'Go away!' }); } } } done({ message: 'Authentication required', authenticate: 'Basic realm="myscheme"' }); }); ``` #### Client Unfortunately, the amount of detail you get in your client when authorization fails depends on the transformer in use. Most real-time frameworks supported by Primus don't expose the status code, headers or response body. The WebSocket transformer's underlying transport socket will fire an `unexpected-response` event with the HTTP request and response: ```js primus.on('outgoing::open', function () { primus.socket.on('unexpected-response', function (req, res) { console.error(res.statusCode); console.error(res.headers['www-authenticate']); // // It's up to us to close the request (although it will time out). // req.abort(); // // It's also up to us to emit an error so primus can clean up. // primus.socket.emit('error', 'authorization failed: ' + res.statusCode); }); }); ``` If you want to read the response body then you can do something like this: ```js primus.on('outgoing::open', function () { primus.socket.on('unexpected-response', function (req, res) { console.error(res.statusCode); console.error(res.headers['www-authenticate']); var data = ''; res.on('data', function (v) { data += v; }); res.on('end', function () { // // Remember error message is in the 'error' property. // primus.socket.emit('error', new Error(JSON.parse(data).error)); }); }); }); ``` If `unexpected-response` isn't caught (because the WebSocket transformer isn't being used or you don't listen for it) then you'll get an `error` event: ```js primus.on('error', function error(err) { console.error('Something horrible has happened', err.stack); }); ``` As noted above, `err` won't contain any details about the authorization failure so you won't be able to distinguish it from other errors. ### Broadcasting Broadcasting allows you to write a message to every connected `Spark` on your server. There are 2 different ways of doing broadcasting in Primus. The easiest way is to use the `Primus#write` method which will write a message to every connected user: ```js primus.write('message'); ``` There are cases where you only want to broadcast a message to a smaller group of users. To make it easier to do this, we've added a `Primus#forEach` method which allows you to iterate over all active connections. ```js primus.forEach(function (spark, id, connections) { if (spark.query.foo !== 'bar') return; spark.write('message'); }); ``` The method can be also used asynchronously. To enable the asynchronous iteration you have to call `Primus#forEach` with two arguments. The first is the iterator function that is called on every step. The iterator is called with a connection from the list and a callback for when it has finished. The second argument is the main callback and is called when the iteration has finished. ```js primus.forEach(function (spark, next) { // // Do something and call next when done // next(); }, function (err) { console.log('We are done'); }); ``` There are also cases where you want to select a single `Spark`. To do this you can use the `Primus#spark` method. ```js // Get a spark by its id var spark = primus.spark(id); spark.write('message'); ``` This method returns a `Spark` or `undefined` if the given id doesn't match any of the active `Spark` ids on the server. ### Destruction In rare cases you might need to destroy the Primus instance you've created. You can use the `primus.destroy()` or `primus.end()` method for this. This method accepts an Object which allows you to configure the destruction process: - `close` Close the HTTP server that Primus received. Defaults to `true`. - `reconnect` Automatically reconnect the clients. Defaults to `false`. - `timeout` Close all active connections and clean up the Primus instance after the specified amount of timeout. Defaults to `0`. The timeout is especially useful if you want gracefully shutdown your server but really don't want to wait an infinite amount of time. ```js primus.destroy({ timeout: 10000 }); ``` ### Events Primus is built upon the Stream and EventEmitter interfaces. This is a summary of the events emitted by Primus. | Event | Usage | Location | Description | | --------------------- | ---------- | ------------- | ---------------------------------------- | | `outgoing::reconnect` | private | client | Transformer should reconnect. | | `reconnect scheduled` | **public** | client | We're scheduling a reconnect. | | `reconnect` | **public** | client | Reconnect attempt is about to be made. | | `reconnected` | **public** | client | Successfully reconnected. | | `reconnect timeout` | **public** | client | Reconnect attempt took too much time. | | `reconnect failed` | **public** | client | Failed to reconnect. | | `timeout` | **public** | client | Failed to connect to server. | | `outgoing::open` | private | client/spark | Transformer should connect. | | `incoming::open` | private | client/spark | Transformer has connected. | | `open` | **public** | client | Connection is open. | | `destroy` | **public** | client | The instance has been destroyed. | | `incoming::error` | private | client | Transformer received an error. | | `error` | **public** | client/spark | An error happened. | | `incoming::data` | private | client/server | Transformer received data. | | `outgoing::data` | private | client/spark | Transformer should write data. | | `data` | **public** | client/spark | We received data. | | `incoming::end` | private | client/spark | Transformer closed the connection. | | `outgoing::end` | private | client/spark | Transformer should close connection. | | `end` | **public** | client/spark | The connection has ended. | | `close` | **public** | client/server | The connection has closed, we might reconnect. / The server has been destroyed. | | `connection` | **public** | server | We received a new connection. | | `disconnection` | **public** | server | We received a disconnection. | | `initialised` | **public** | server | The server is initialised. | | `plugin` | **public** | server | A new plugin has been added. | | `plugout` | **public** | server | A plugin has been removed. | | `incoming::ping` | private | spark | We received a ping message. | | `outgoing::ping` | private | client | We're sending a ping message. | | `incoming::pong` | private | client | We received a pong message. | | `outgoing::pong` | private | spark | We're sending a pong message. | | `heartbeat` | **public** | spark | We've received a heartbeat and have reset the timer. | | `online` | **public** | client | We've regained a network connection. | | `offline` | **public** | client | We've lost our internet connection. | | `log` | **public** | server | Log messages. | | `readyStateChange` | **public** | client/spark | The readyState has changed. | | `outgoing::url` | private | client | The options used to construct the URL. | As a rule of thumb assume that every event that is prefixed with `incoming::` or `outgoing::` is reserved for internal use only and that emitting such events your self will most likely result in c̮̫̞͚͉̮̙͕̳̲͉̤̗̹̮̦̪̖̱h̛͍͙̖̟͕̹͕̙̦̣̲̠̪̯̳͖̝̩a̴̝̦͇̥̠̟͚̳̤̹̗̻̭͍͖͕͓̻o̥̹̮̙͔̗͍͚͓̗̦̹͈͙͕̘̮͖̝ș̗̲̤̗̮͈̙͈̹̼̣̹̖̱̤̼̺̤ ̻͙̗̥̠̱͇̱̝̟̺͍̺̼͆̅̓̓̇a̜̖͈͇͎͙̲̙̗͇̫̘̖̹͖͓͔̺̱n̹͓̮͇̯̜̤̗͍̯̰̫̫̖̰ͬ͌ͬͫd͚̪͚̭͚̥̰̤̟͎̝̲̯̭̹̭̙̼̤ ͖̞̙̹͈͚̥̦͚͉͖̼̬͓͚̳͉͙͎d̴͚̱̮̗͍̩̻̰̣̫͉͈̞̲͉̫̞͔ẻͩͦ̃͌̿̐ͪͩ̌̇͂̆̑͐ͣ ҉̲͉͔͎̤̼̘͇̮̥̻̜̹̥͚̲̻̖s̶̗̻̫̼̠̳̗̺̤̗̳͈̪̮̗̝͇͈t̙͇͕̺̱̼̤̗̰̬̣͌ͬͧ͊́ͧͩ͌r͌̐̓̃ͥ̄ͤ͑̈ͬ͆ͬ͂̇̿̅ ҉̙̼̳̭̙͍̻̱̠͈̮̺̣̝̱̙̺͉ư̳͎̻͔̯̪̝͕͚̣̜̼̞͇̠̘̠̪c̨̫͙͙̬̰̰̫̐͋͊͑̌̾̉͆t͚̗͕̝̤̗͕̲̮̝̼̺͙͚̟͓̣̥͍ĭ͙̘̩̖͇͎̆̍̿̾ͤ̔̉̈̂̾̈ͭo̬̠̝͈̺̙̮̬̗̪̤͕͇͕̰̮͖͉̬n̙̪̤̝̹͖͖̻̬̹͙̞̗͓̞̭̜̠̟. To make it easier for developers to emit events on Primus itself, we've added a small helper function that checks if the event you want to emit is reserved for Primus only. This would be all `incoming::` and `outgoing::` prefixed events and the events listed above. This method is called `<class>.reserved()` and it's implemented on the `Spark`: ```js primus.on('connection', function connection(spark) { spark.on('data', function (data) { // // Just imagine that we receive an array of arguments from the client which // first argument is the name of the event that we need to emit and the // second argument are the arguments for function. // if (spark.reserved(data.args[0])) return; spark.emit.apply(spark, data.args[0]); }); }); ``` But also the client: ```js var primus = new Primus('http://example.bar'); primus.on('data', function (data) { if (primus.reserved(data.args[0])) return; primus.emit.apply(primus, data.args); }); ``` And of course the `Primus` instance as well. ### Heartbeats and latency Heartbeats are used in Primus to figure out if we still have an active, working and reliable connection with the server. These heartbeats are sent from the **client** to the server. The heartbeats will only be sent when there is an idle connection, so there is very little to no overhead at all. The main reason for this is that we already know that the connection is alive when we receive data from the server. The heartbeat package that we send over the connection is `primus::ping::<timestamp>`. The server will echo back the exact same package. This allows Primus to also calculate the latency between messages by simply getting the `<timestamp>` from echo and comparing it with the local time. This heartbeat is then stored in a `primus.latency` property. The initial value of the `primus.latency` is to the time it took to send an `open` package and to actually receive a confirmation that the connection has been opened. ### Supported Real-time Frameworks The following transformers/transports are supported in Primus: #### BrowserChannel BrowserChannel was the original technology that GMail used for their real-time communication. It's designed for same domain communication and does not use WebSockets. To use BrowserChannel you need to install the `browserchannel` module: ``` npm install browserchannel --save ``` And tell `Primus` that you want to use `browserchannel` as transformer: ```js var primus = new Primus(server, { transformer: 'browserchannel' }); ``` The `browserchannel` transformer comes with built-in node client support and can be accessed using: ```js var Socket = primus.Socket , socket = new Socket('url'); ``` Please note that you should use at least version `1.0.6` which contains support for query strings. #### Engine.IO Engine.IO is the low level transport functionality of Socket.IO 1.0. It supports multiple transports for creating a real-time connection. It uses transport upgrading instead of downgrading which makes it more resilient to blocking proxies and firewalls. To enable `engine.io` you need to install the `engine.io` module: ``` npm install engine.io --save ``` And tell `Primus` that you want to use `engine.io` as transformer: ```js var primus = new Primus(server, { transformer: 'engine.io' }); ``` If you want to use the client interface inside of Node.js you also need to install the `engine.io-client`: ``` npm install engine.io-client --save ``` And then you can access it from your server instance: ```js var Socket = primus.Socket , socket = new Socket('url'); ``` #### Faye Faye is a WebSocket only transformer. It uses the `faye-websocket` module which is part of the [Faye](http://faye.jcoglan.com/) project and supports all protocol specifications. To use this you need to install the `faye-websocket` module: ``` npm install faye-websocket --save ``` And tell `Primus` that you want to use `faye` as transformer: ```js var primus = new Primus(server, { transformer: 'faye' }); ``` The `faye` transformer comes with built-in no