|
# Jandal |
|
|
|
An event handler for socket interfaces. It was built for use with |
|
[SockJS](https://github.com/sockjs/sockjs-node), but can be used with any |
|
socket interface, such as node streams. |
|
|
|
It has a similar feature set to [Socket.io](http://socket.io), including rooms |
|
and broadcasting. |
|
|
|
## Important |
|
|
|
Jandal has a maximum of three arguments per event. This restriction vastly |
|
improves performance in most browsers and in nodejs. |
|
|
|
This may sound harsh, but you probably don't need to use more than three args |
|
anyway. You can always store extra args in an array or object. |
|
|
|
There is also the deprecated |
|
[multi-args branch](https://github.com/stayradiated/jandal/multi-args) |
|
- but it is not kept up to date. |
|
|
|
# Example Usage |
|
|
|
## Server |
|
|
|
Add it to your project with `npm install --save jandal`. |
|
|
|
```javascript |
|
var http, Jandal, sockjs, server, conn; |
|
|
|
http = require('http'); |
|
Jandal = require('jandal'); |
|
sockjs = require('sockjs'); |
|
|
|
// standard sockjs stuff |
|
server = http.createServer(); |
|
conn = sockjs.createServer(); |
|
conn.installHandlers(server, { prefix: '/socket' }); |
|
server.listen(8080); |
|
|
|
// Listen for new connections |
|
conn.on('connection', function (socket) { |
|
var jandal; |
|
|
|
// wrap the socket in a Jandal |
|
jandal = new Jandal(socket, 'stream'); |
|
|
|
// listening for the 'log' event |
|
jandal.on('log', function (text) { |
|
console.log('log: ' + text); |
|
}); |
|
|
|
// listening for an event with a callback |
|
jandal.on('echo', function (text, callback) { |
|
callback(text); |
|
}); |
|
|
|
// send an event to the client |
|
jandal.emit('weclome', { |
|
id: socket.id, |
|
time: Date.now() |
|
}); |
|
|
|
}); |
|
``` |
|
|
|
## Client |
|
|
|
Grab a copy of `/client.js` from this repo, or use CommonJS compiler and |
|
require `jandal/client`. |
|
|
|
```javascript |
|
var conn, socket; |
|
|
|
// use browserify |
|
// or load the libraries as seperate scripts |
|
require('sockjs'); |
|
require('jandal/client'); |
|
|
|
conn = new SockJS('http://localhost:8080/socket'); |
|
socket = new Jandal(conn, 'websocket'); |
|
|
|
// Wait for socket to connect |
|
socket.on('socket.open', function () { |
|
|
|
// listen for events |
|
socket.on('welcome', function (info) { |
|
console.log(info); |
|
}); |
|
|
|
// send a message to the server |
|
socket.emit('log', 'the time is' + Date.now()); |
|
|
|
// Send a message to the server with a callback |
|
socket.emit('echo', 'hello', function (reply) { |
|
assert(reply === 'hello'); |
|
}); |
|
|
|
}); |
|
``` |
|
|
|
## Rooms |
|
|
|
```javascript |
|
|
|
conn.on('connection', function (socket) { |
|
var jandal; |
|
|
|
// wrap the socket |
|
jandal = new Jandal(socket, 'stream'); |
|
|
|
// add it to a room |
|
jandal.join('my_room'); |
|
|
|
// emit to all other sockets in a room |
|
jandal.broadcast.to('my_room').emit('a new socket has joined', jandal.id); |
|
|
|
// remove it from a room |
|
jandal.leave('my_room'); |
|
|
|
}); |
|
``` |
|
|
|
# Jandal Class |
|
|
|
## Static Properties |
|
|
|
The `Jandal` class has a couple of static properties useful for managing |
|
connected sockets. |
|
|
|
### Jandal.all |
|
|
|
This is a `Room` instance that holds all the connected sockets. See the `Room` |
|
docs for more info. |
|
|
|
**Example:** |
|
|
|
```javascript |
|
// Emitting |
|
Jandal.all.emit('hello', 1, 2,3); |
|
|
|
// Broadcasting |
|
Jandal.all.broadcast('socket-id', 'hello', 1, 2, 3); |
|
``` |
|
|
|
### Jandal.in(room) |
|
|
|
Easily access any sockets in any room. See the `Room` docs for more info. |
|
|
|
**Parameters:** |
|
|
|
- room (string) : the name of the room |
|
|
|
**Example:** |
|
|
|
```javascript |
|
Jandal.in('my-room').emit('hello'); |
|
``` |
|
|
|
## Instance Properties |
|
|
|
Every Jandal instance extends the NodeJS EventEmitter so you can also use |
|
methods like: `once`, `removeAllListeners` and `setMaxListeners`. See the |
|
[EventEmitter docs](http://nodejs.org/api/events.html) for more information. |
|
|
|
### jandal.rooms |
|
|
|
An array that holds all the rooms the socket is currently joined to. |
|
|
|
### jandal.connect |
|
|
|
**Parameters:** |
|
|
|
- socket (object) : an object that represents a socket |
|
- handle (string|object) : a handle name or an object to use as a handle |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var jandal, conn; |
|
|
|
jandal = new Jandal(); |
|
conn = new SockJS(config.url); |
|
|
|
jandal.connect(conn, 'websocket'); |
|
``` |
|
|
|
**Example with custom handles:** |
|
|
|
```javascript |
|
var jandal, handle, socket; |
|
|
|
jandal = new Jandal(); |
|
|
|
socket = new EventEmitter(); |
|
|
|
handle = { |
|
write: function (socket, message) { |
|
socket.emit('message', message); |
|
}, |
|
onread: (socket, fn) { |
|
socket.on('message', fn); |
|
}, |
|
... |
|
}; |
|
|
|
jandal.connect(socket, handle); |
|
``` |
|
|
|
|
|
### jandal.emit |
|
|
|
This is very similar to the NodeJS EventEmitter, but you are limited to three |
|
arguments. |
|
|
|
**Parameters:** |
|
|
|
- event (string) : the event to emit |
|
- arg1 (dynamic) |
|
- arg2 (dynamic) |
|
- arg3 (dynamic) |
|
|
|
Arguments can be strings, numbers, booleans, dates, objects, arrays, etc... |
|
Basically anything that `JSON.stringify` can handle. |
|
|
|
**Callbacks:** |
|
|
|
You can also send one function for use as a callback. |
|
|
|
- It must always be passed as the last argument. |
|
- Callbacks will only be run once. |
|
- They can take 0 to 3 arguments. |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var jandal; |
|
jandal = new Jandal(); |
|
|
|
// lots of different data types |
|
jandal.emit('my-event', 'arg 1', ['arg 2'], {arg: 3}) |
|
|
|
// passing functions as callbacks |
|
jandal.emit('my-callback', 'some data', function (response) { |
|
console.log('running the callback with', response); |
|
}); |
|
``` |
|
|
|
|
|
### jandal.on |
|
|
|
Works very similar to the EventEmitter. |
|
|
|
**Parameters:** |
|
|
|
- event (string) : event to listen for |
|
- listener (function) : function to run when the event is emitted |
|
|
|
**Example:** |
|
|
|
```javascript |
|
jandal.on('my-event', function (arg1, arg2, arg3) { |
|
console.log('"my-event" has been emitted with', arguments); |
|
}); |
|
|
|
// listening for a namespace + event |
|
jandal.on('task.create', listener); |
|
|
|
// this is the same as |
|
jandal.namespace('task').on('create', listener); |
|
``` |
|
|
|
### jandal.namespace |
|
|
|
Return a new Namespace instance. If the namespace already exists, it will |
|
use that instead of creating a new one. See the `Namespace` docs for more info. |
|
|
|
**Parameters:** |
|
|
|
- name (string) : namespace name |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var jandal, ns; |
|
|
|
jandal = new Jandal(); |
|
ns = jandal.namespace('app'); |
|
|
|
// sends "app.hello()" |
|
ns.emit('hello'); |
|
|
|
// listens for "app.goodbye" |
|
ns.on('goodbye', function () { |
|
console.log('bye'); |
|
}); |
|
``` |
|
|
|
### jandal.join |
|
|
|
Put the socket in a room. |
|
|
|
**Parameters:** |
|
|
|
- room (string) : name of the room |
|
|
|
**Example:** |
|
|
|
```javascript |
|
jandal.join('my-room'); |
|
``` |
|
|
|
### jandal.leave |
|
|
|
Remove the socket from a room. |
|
|
|
**Parameters:** |
|
|
|
- room (string) : name of the room |
|
|
|
**Example:** |
|
|
|
```javascript |
|
jandal.leave('my-room'); |
|
``` |
|
|
|
### jandal.room |
|
|
|
Returns a room. Same as `Jandal.in`. |
|
|
|
**Parameters:** |
|
|
|
- room (string) : name of the room |
|
|
|
**Example:** |
|
|
|
```javascript |
|
// add the socket to the room |
|
jandal.join('my-room'); |
|
|
|
// get the room |
|
var room = jandal.room('my-room'); |
|
|
|
// emit to all the sockets in the room |
|
room.emit('hello'); |
|
``` |
|
|
|
### jandal.release |
|
|
|
Remove the socket from all the rooms it is currently in. |
|
|
|
**Example:** |
|
|
|
```javascript |
|
jandal.release(); |
|
``` |
|
|
|
# Room Class |
|
|
|
## Instance Methods |
|
|
|
Rooms are just a collection of sockets. You can add or remove sockets from |
|
them, and emit events to all sockets in that room, or broadcast events from a |
|
socket to all other sockets. |
|
|
|
Every socket is added to the 'all' room, which can be acessed through |
|
`Jandal.all`. |
|
|
|
### room.length |
|
|
|
Returns the number of connected sockets in a room. |
|
|
|
**Parameters:** |
|
|
|
*No parameters* |
|
|
|
**Example:** |
|
|
|
```javascript |
|
Jandal.in('my-room').length(); |
|
``` |
|
|
|
### room.contains |
|
|
|
Check if a socket is in a room. Returns `true` or `false`. |
|
|
|
**Parameters** |
|
|
|
- jandal (Jandal) : an instance of a Jandal |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var a, b; |
|
|
|
a = new Jandal(); |
|
a.join('my-room'); |
|
|
|
b = new Jandal(); |
|
|
|
Jandal.in('my-room').contains(a); // true |
|
Jandal.in('my-room').contains(b); // false |
|
``` |
|
|
|
### room.emit |
|
|
|
Exactly the same as `jandal.emit` but will be sent to all connected sockets. |
|
|
|
**Parameters:** |
|
|
|
- event (string) : name of the event |
|
- arg1 (dynamic) |
|
- arg2 (dynamic) |
|
- arg3 (dynamic) |
|
|
|
**Example:** |
|
|
|
```javascript |
|
Jandal.in('my-room').emit('hello', 1, 2, 3); |
|
``` |
|
|
|
### room.broadcast |
|
|
|
Just like emit, but will not send to the 'sender' socket. |
|
|
|
**Parameters:** |
|
|
|
- sender (dynamic) |
|
- event (string) |
|
- arg1 (dynamic) |
|
- arg2 (dynamic) |
|
- arg3 (dynamic) |
|
|
|
**Example:** |
|
|
|
```javascript |
|
Jandal.in('my-room').broadcast('some-id', 'bye', 1, 2, 3); |
|
``` |
|
|
|
### room.namespace |
|
|
|
Get a namespace for a room. |
|
|
|
**Parameters:** |
|
|
|
- name (string) : the name of the namespace |
|
|
|
**Example:** |
|
|
|
```javascript |
|
Jandal.in('my-room').namespace('tasks').emit('create', 'something'); |
|
``` |
|
|
|
### room.destroy |
|
|
|
Destroy all sockets in a room |
|
|
|
```javascript |
|
Jandal.in('my-room').destroy() |
|
``` |
|
|
|
# Handle |
|
|
|
Handles are used as an interface between Jandal and a socket. |
|
|
|
There are two handles bundled by default: `stream` and `websocket`. |
|
|
|
## Default Handles |
|
|
|
### Stream |
|
|
|
Works with SockJS-Node |
|
|
|
**Source Code:** |
|
|
|
```javascript |
|
stream: { |
|
identify: function (socket) { |
|
return socket.id; |
|
}, |
|
write: function (socket, message) { |
|
socket.write(message); |
|
}, |
|
onread: function (socket, fn) { |
|
socket.on('data', fn); |
|
}, |
|
onclose: function (socket, fn) { |
|
socket.on('close', fn); |
|
}, |
|
onerror: function(socket, fn) { |
|
socket.on('error', fn); |
|
}, |
|
onopen: function(socket, fn) { |
|
setTimeout(fn, 0); |
|
}, |
|
release: function (socket) { |
|
socket.removeAllListeners('data'); |
|
socket.removeAllListeners('close'); |
|
socket.removeAllListeners('error'); |
|
} |
|
} |
|
``` |
|
|
|
### WebSocket |
|
|
|
Works with the WebSocket API (and also SockJS-Client). |
|
|
|
**Source Code:** |
|
|
|
```javascript |
|
websocketsId = 0; |
|
|
|
... |
|
|
|
websocket: { |
|
identify: function (socket) { |
|
if (socket.hasOwnProperty('id')) return socket.id; |
|
socket.id = ++websocketsId; |
|
return socket.id; |
|
}, |
|
write: function (socket, message) { |
|
socket.send(message); |
|
}, |
|
onread: function (socket, fn) { |
|
socket.onmessage = function (e) { fn(e.data); }; |
|
}, |
|
onclose: function (socket, fn) { |
|
socket.onclose = fn; |
|
}, |
|
onerror: function(socket, fn) { |
|
socket.onerror = fn; |
|
}, |
|
onopen: function(socket, fn) { |
|
socket.onopen = fn; |
|
}, |
|
release: function (socket) { |
|
delete socket.onmessage; |
|
delete socket.onclose; |
|
delete socket.onerror; |
|
delete socket.onopen; |
|
} |
|
} |
|
``` |
|
|
|
## Methods |
|
|
|
### identify |
|
|
|
Return something that identifies this socket, like an ID. |
|
|
|
**Parameters:** |
|
|
|
- socket (Socket) : the socket to identify |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var handler = { |
|
identify: function (socket) { |
|
|
|
// if your sockets already have an id |
|
return socket.id; |
|
|
|
// maybe assign an id? |
|
// HINT: better to use |
|
return socket.id || socket.id = ++someNumber; |
|
|
|
// if you don't care about anything |
|
return socket; |
|
|
|
} |
|
}; |
|
``` |
|
|
|
### write |
|
|
|
Write a message to the socket. Will be called whenever a message needs to be |
|
sent. |
|
|
|
**Parameters:** |
|
|
|
- socket (socket) : the socket to send the message with |
|
- message (string) : the message to send |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var handler = { |
|
write: function (socket, message) { |
|
socket.write(message); |
|
} |
|
}; |
|
``` |
|
|
|
### onread |
|
|
|
Listen for messages. Will be called once per each socket. Expects the `fn` |
|
callback to be passed a message whenever one is sent. |
|
|
|
**Parameters:** |
|
|
|
- socket (socket) : the socket to listen to |
|
- fn (function) : the callback to run |
|
|
|
**Callback Parameters:** |
|
|
|
- message (string) : the message that has been sent to the socket |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var handler = { |
|
onread: function (socket, fn) { |
|
socket.on('read', fn); |
|
} |
|
}; |
|
``` |
|
|
|
### onerror(socket, fn) |
|
|
|
Listen for errors on the socket. Will be called only once per each socket. |
|
Expects `fn` to be called whenever the socket has an error. Accepts one |
|
argument that will be be passed through to the `socket.error` event. |
|
|
|
**Parameters:** |
|
|
|
- socket (socket) : the socket to listen to |
|
- fn (function) : the callback to run |
|
|
|
**Callback Parameters:** |
|
|
|
- err (dynamic) : an error message |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var handler = { |
|
onerror: function (socket, fn) { |
|
socket.on('error', function (err) { |
|
fn(err); |
|
}); |
|
} |
|
}; |
|
``` |
|
|
|
### onopen(socket, fn) |
|
|
|
Listen for the socket connection to be opened. Will be called once per each |
|
socket. Expects the `fn` callback to called once when the socket has connected. |
|
If the socket is already open, the you can run the callback immediately. Will |
|
be passed through to the `socket.open` event. |
|
|
|
**Parameters:** |
|
|
|
- socket (socket) : the socket to listen to |
|
- fn (function) : the callback to run |
|
|
|
**Callback Parameters:** |
|
|
|
- event (dymanic) : an optional argument to pass through to `socket.open` |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var handler = { |
|
onopen: function (socket, fn) { |
|
socket.on('open', fn); |
|
} |
|
}; |
|
``` |
|
|
|
### onclose(socket, fn) |
|
|
|
Listen for the socket to be closed. Will be called once per each socket. |
|
Expects the `fn` callback to be called only once, and only when the socket has |
|
been closed. Arguments will be passed through to the `socket.close` event. |
|
|
|
**Parameters:** |
|
|
|
- socket (socket) : the socket to listen to |
|
- fn (function) : the callback to run |
|
|
|
**Callback Parameters:** |
|
|
|
- status (number) : error code |
|
- message (string) : error message |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var handler = { |
|
onclose: function (socket, fn) { |
|
socket.on('close', fn); |
|
} |
|
}; |
|
``` |
|
|
|
### release(socket) |
|
|
|
Disconnect the raw socket from the jandal instance. |
|
|
|
**Parameters:** |
|
|
|
- socket (socket) : the socket to listen to |
|
|
|
**Example:** |
|
|
|
```javascript |
|
var handler = { |
|
release: function (socket) { |
|
socket.off('data'); |
|
socket.off('open'); |
|
socket.off('close'); |
|
socket.off('error'); |
|
} |
|
}; |
|
``` |
|
|
|
# Protocol |
|
|
|
Jandal uses a simple protocol for encoding messages. It's based on the |
|
javascript syntax for objects and functions. Arguments are encoded using |
|
JSON.stringify. |
|
|
|
There are four parts to a message: |
|
|
|
- namespace |
|
- event |
|
- args |
|
- callback |
|
|
|
The namespace and callback are both optional. |
|
|
|
**Example messages:** |
|
|
|
```javascript |
|
// event + single arg |
|
fetch("info") |
|
|
|
// event + multiple args |
|
fetch("info",{"count":40}) |
|
|
|
// event + arg + callback |
|
fetch("info").fn(10) |
|
|
|
// namespace + event + arg |
|
user.load("numbers",[10,20,30]) |
|
|
|
// namespace + event + arg + callback |
|
task.create({"name":"this is a new task"}).fn(1) |
|
``` |
|
|
|
**Callbacks:** |
|
|
|
Each message can have a single callback. The callback must be the last |
|
arguments, and can only be called once. |
|
|
|
Callbacks are just like regular events, so you can also have a callback |
|
on a callback. |
|
|
|
```javascript |
|
// send a message with a callback |
|
app.login('username', 'password').fn(32) |
|
|
|
// response running the callback with args |
|
socket.fn_23({login: success}) |
|
|
|
// callback with a callback |
|
socket.fn_24({login: fail}).fn(25) |
|
``` |
|
|
|
# Browsers |
|
|
|
The same code can be run in the browser by using Browserify. |
|
|
|
This also allows you to use the library to communicate between servers, as it |
|
acts as the client and the server. |
|
|
|
To compile for the browser: |
|
|
|
npm run-script build |
|
|
|
And then either copy/paste the `client.js` file into your project, or |
|
include it via `require('jandal/client');`. |
|
|
|
# License |
|
|
|
The MIT License (MIT) |
|
|
|
Copyright (c) 2014 George Czabania |
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
of this software and associated documentation files (the "Software"), to deal |
|
in the Software without restriction, including without limitation the rights |
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
copies of the Software, and to permit persons to whom the Software is |
|
furnished to do so, subject to the following conditions: |
|
|
|
The above copyright notice and this permission notice shall be included in |
|
all copies or substantial portions of the Software. |
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
THE SOFTWARE. |
|
|
|
# Changelog |
|
|
|
## 0.0.15 |
|
|
|
- When broadcasting from a socket, check `socket.id !== sender` instead of |
|
`socket !== sender`. This requires all sockets to have an 'id' attribute. |
|
- Use the `socket` namespace instead of `Jandal` for handling callbacks. |
|
- Make `serialize` and `parse` private methods of a Jandal instance. |
|
- Make `namespaces` and `callbacks` private properties of a Jandal instance. |
|
- Fix bug where Jandal would crash if a callback is called more than once |
|
- Make `Room.prototype.join` and `Room.prototype.leave` private. |
|
- Fix bug where a socket could be added to the same room twice |
|
- Remove `Jandal.handle()`. Instead pass the handler to the `Jandal` |
|
constructor. e.g: `new Jandal(socket, 'stream');`. |
|
- Replace `room.destroy()` with `room.empty()`. No longer destroys room, just |
|
removes all the connected sockets. |
|
- Remove `Room.remove()`. |
|
- Add MIT License |
|
- Switch from `var = a, b, c;` to `var a = 1; \n var b = 2; var c = 3;` |
|
|
|
## 0.0.14 |
|
|
|
- Rebuild client.js |
|
|
|
## 0.0.13 |
|
|
|
- The `onclose` handler now accepts two arguments that will be passed through |
|
to the `socket.close` event. |
|
|
|
## 0.0.12 |
|
|
|
- Move `client.js` to the root directory. You should now use |
|
`require('jandal/client')`. |
|
- Allow users to supply a custom socket handler. |
|
|
|
## 0.0.11 |
|
|
|
- Add socket events: `socket.open`, `socket.close`, `socket.error`. |
|
- Fix an off by error with `Socket.prototype.serialize`, where callbacks could |
|
not be the last argument. |
|
|
|
## 0.0.10 |
|
|
|
- Use `.fn(20)` instead of `__fn__20` for callbacks. |
|
- Make sure that `Socket.prototype.parse` will only accept strings. |
|
|
|
## 0.0.9 |
|
|
|
- Protect `Socket.prototype.parse` against crashing on invalid messages. |
|
|
|
## 0.0.8 |
|
|
|
- Add `Socket.prototype.room` to access rooms from a jandal instance. |
|
- Limit event arguments to a maximum of three. |
|
|
|
## 0.0.7 |
|
|
|
- Clean up code. |
|
- Add examples to readme. |
|
|
|
## 0.0.6 |
|
|
|
- Use browserify to compile for browsers. |
|
- Use uglify to minify `client.js`. |
|
|
|
## 0.0.5 |
|
|
|
- Set `main` to `source/jandal.js`. |
|
|
|
## 0.0.4 |
|
|
|
- Add namespaces to broadcasting |
|
- Redo the room api |
|
|
|
## 0.0.3 |
|
|
|
- Split code into multiple files. |
|
- Add support for sorting sockets into rooms |
|
|
|
## 0.0.2 |
|
|
|
- Use handles to interface betwen jandals and sockets. |
|
- Fix bug with parsing messages. |
|
- Add `Jandal.noConflict` for browsers. |
|
|
|
## 0.0.1 |
|
|
|
- Start project |
|
- Write `jandal.js` and tests |
|
- Can serialize and parse messages |
|
- Add namespaces |
|
- Can emit messages and listen for them |
|
- Add callback functions |
|
|