Page MenuHomePhabricator

connection.js
No OneTemporary

connection.js

'use strict';
import debug from 'debug';
import uuid from 'node-uuid';
import { EventEmitter } from 'events';
import { setImmediate } from 'timers';
import { w3cwebsocket as W3CWebSocket } from 'websocket';
import { Account } from './account';
const SYLKRTC_PROTO = 'sylkRTC-1';
const DEBUG = debug('sylkrtc:Connection');
const INITIAL_DELAY = 0.5 * 1000;
class Connection extends EventEmitter {
constructor(options = {}) {
if (!options.server) {
throw new Error('\"server\" must be specified');
}
super();
this._wsUri = options.server;
this._sock = null;
this._state = null;
this._closed = false;
this._timer = null;
this._delay = INITIAL_DELAY;
this._accounts = new Map();
this._requests = new Map();
}
get state() {
return this._state;
}
close() {
if (this._closed) {
return;
}
this._closed = true;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
if (this._sock) {
this._sock.close();
this._sock = null;
} else {
setImmediate(() => {
this._setState('closed');
});
}
}
addAccount(options = {}, cb = null) {
if (typeof options.account !== 'string' || typeof options.password !== 'string') {
throw new Error('Invalid options, \"account\" and \"password\" must be supplied');
}
if (this._accounts.has(options.account)) {
throw new Error('Account already added');
}
let acc = new Account(options.account, options.password, this);
// add it early to the set so we don't add it more than once, ever
this._accounts.set(acc.id, acc);
let req = {
sylkrtc: 'account-add',
account: acc.id,
password: acc.password
};
this._sendRequest(req, (error) => {
if (error) {
DEBUG('add_account error: %s', error);
this._accounts.delete(acc.id);
acc = null;
}
if (cb) {
cb(error, acc);
}
});
}
removeAccount(account, cb=null) {
const acc = this._accounts.get(account.id);
if (account !== acc) {
throw new Error('Unknown account');
}
// delete the account from the mapping, regardless of the result
this._accounts.delete(account.id);
let req = {
sylkrtc: 'account-remove',
account: acc.id
};
this._sendRequest(req, (error) => {
if (error) {
DEBUG('remove_account error: %s', error);
}
if (cb) {
cb();
}
});
}
// Private API
_initialize() {
if (this._sock !== null) {
throw new Error('WebSocket already initialized');
}
if (this._timer !== null) {
throw new Error('Initialize is in progress');
}
DEBUG('Initializing');
if (process.browser) {
window.addEventListener('beforeunload', () => {
if (this._sock !== null) {
let noop = function() {};
this._sock.onerror = noop;
this._sock.onmessage = noop;
this._sock.onclose = noop;
this._sock.close();
}
});
}
this._timer = setTimeout(() => {
this._connect();
}, this._delay);
}
_connect() {
DEBUG('WebSocket connecting');
this._setState('connecting');
this._sock = new W3CWebSocket(this._wsUri, SYLKRTC_PROTO);
this._sock.onopen = () => {
DEBUG('WebSocket connection open');
this._onOpen();
};
this._sock.onerror = () => {
DEBUG('WebSocket connection got error');
};
this._sock.onclose = (event) => {
DEBUG('WebSocket connection closed: %d: (reason=\"%s\", clean=%s)', event.code, event.reason, event.wasClean);
this._onClose();
};
this._sock.onmessage = (event) => {
DEBUG('WebSocket received message: %o', event);
this._onMessage(event);
};
}
_sendRequest(req, cb) {
const transaction = uuid.v4();
req.transaction = transaction;
if (this._state !== 'ready') {
setImmediate(() => {
cb(new Error('Connection is not ready'));
});
return;
}
this._requests.set(transaction, {req: req, cb: cb});
this._sock.send(JSON.stringify(req));
}
_setState(newState) {
DEBUG('Set state: %s -> %s', this._state, newState);
const oldState = this._state;
this._state = newState;
this.emit('stateChanged', oldState, newState);
}
// WebSocket callbacks
_onOpen() {
clearTimeout(this._timer);
this._timer = null;
this._delay = INITIAL_DELAY;
this._setState('connected');
}
_onClose() {
this._sock = null;
if (this._timer) {
clearTimeout(this._timer);
this._timer = null;
}
// remove all accounts, the server no longer has them anyway
this._accounts.clear();
this._setState('disconnected');
if (!this._closed) {
this._delay = this._delay * 2;
if (this._delay > Number.MAX_VALUE) {
this._delay = INITIAL_DELAY;
}
DEBUG('Retrying connection in %s seconds', this._delay / 1000);
this._timer = setTimeout(() => {
this._connect();
}, this._delay);
} else {
this._setState('closed');
}
}
_onMessage(event) {
let message = JSON.parse(event.data);
if (typeof message.sylkrtc === 'undefined') {
DEBUG('Unrecognized message received');
return;
}
DEBUG('Received \"%s\" message: %o', message.sylkrtc, message);
if (message.sylkrtc === 'event') {
DEBUG('Received event: \"%s\"', message.event);
switch (message.event) {
case 'ready':
this._setState('ready');
break;
default:
break;
}
} else if (message.sylkrtc === 'account_event') {
let acc = this._accounts.get(message.account);
if (!acc) {
DEBUG('Account %s not found', message.account);
return;
}
acc._handleEvent(message);
} else if (message.sylkrtc === 'session_event') {
const sessionId = message.session;
for (let acc of this._accounts.values()) {
let call = acc._calls.get(sessionId);
if (call) {
call._handleEvent(message);
break;
}
}
} else if (message.sylkrtc === 'ack' || message.sylkrtc === 'error') {
const transaction = message.transaction;
const data = this._requests.get(transaction);
if (!data) {
DEBUG('Could not find transaction %s', transaction);
return;
}
this._requests.delete(transaction);
DEBUG('Received \"%s\" for request: %o', message.sylkrtc, data.req);
if (data.cb) {
if (message.sylkrtc === 'ack') {
data.cb(null);
} else {
data.cb(new Error(message.error));
}
}
}
}
}
export { Connection };

File Metadata

Mime Type
text/x-java
Expires
Sat, Nov 23, 5:12 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3408844
Default Alt Text
connection.js (7 KB)

Event Timeline