marsdb-sync-client
Version:
Standalone Meteor DDP client based on MarsDB
270 lines (240 loc) • 6.81 kB
JavaScript
import _try from 'fast.js/function/try';
import _bind from 'fast.js/function/bind';
import HeartbeatManager from './HeartbeatManager';
const EventEmitter = typeof window !== 'undefined' && window.Mars
? window.Mars.EventEmitter : require('marsdb').EventEmitter;
const PromiseQueue = typeof window !== 'undefined' && window.Mars
? window.Mars.PromiseQueue : require('marsdb').PromiseQueue;
const EJSON = typeof window !== 'undefined' && window.Mars
? window.Mars.EJSON : require('marsdb').EJSON;
const Random = typeof window !== 'undefined' && window.Mars
? window.Mars.Random : require('marsdb').Random;
// Status of a DDP connection
const DDP_VERSION = '1';
const HEARTBEAT_INTERVAL = 17500;
const HEARTBEAT_TIMEOUT = 15000;
const RECONNECT_INTERVAL = 5000;
export const CONN_STATUS = {
CONNECTING: 'CONNECTING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
};
export default class DDPConnection extends EventEmitter {
constructor({ url, socket, autoReconnect = true }) {
super();
this.url = url;
this._processQueue = new PromiseQueue(1);
this._sessionId = null;
this._autoReconnect = autoReconnect;
this._socket = socket;
this._status = CONN_STATUS.DISCONNECTED;
this._fullConnectedOnce = false;
this._heartbeat = new HeartbeatManager(
HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT
);
this._heartbeat.on('timeout', _bind(this._handleHearbeatTimeout, this));
this._heartbeat.on('sendPing', _bind(this.sendPing, this));
this._heartbeat.on('sendPong', _bind(this.sendPong, this));
}
/**
* Returns true if client is fully connected to a server
* @return {Boolean}
*/
get isConnected() {
return this._status === CONN_STATUS.CONNECTED;
}
/**
* Returns true if client disconnected
* @return {Boolean}
*/
get isDisconnected() {
return this._status === CONN_STATUS.DISCONNECTED;
}
/**
* Sends a "method" message to the server with given
* parameters
* @param {String} name
* @param {String} params
* @param {String} id
* @param {String} randomSeed
*/
sendMethod(name, params = [], id, randomSeed) {
const msg = {
msg: 'method',
id: id,
method: name,
params: params,
};
if (randomSeed) {
msg.randomSeed = randomSeed;
}
this._sendMessage(msg);
}
/**
* Send "sub" message to the server with given
* publusher name and parameters
* @param {String} name
* @param {Array} params
* @param {String} id
*/
sendSub(name, params = [], id) {
this._sendMessage({
msg: 'sub',
id: id,
name: name,
params: params,
});
}
/**
* Send "unsub" message to the server for given
* subscription id
* @param {String} id
*/
sendUnsub(id) {
this._sendMessage({
msg: 'unsub',
id: id,
});
}
/**
* Send a "ping" message with randomly generated ping id
*/
sendPing() {
this._sendMessage({
msg: 'ping',
id: Random.default().id(20),
});
}
/**
* Sends a "pong" message for given id of ping message
* @param {String} id
*/
sendPong(id) {
this._sendMessage({
msg: 'pong',
id: id,
});
}
/**
* Make a new WebSocket connection to the server
* if we are not connected yet (isDicsonnected).
* Returns true if connecting, false if already connectiong
* @returns {Boolean}
*/
connect() {
if (this.isDisconnected) {
this._rawConn = new (this._socket)(this.url);
this._rawConn.onopen = _bind(this._handleOpen, this);
this._rawConn.onerror = _bind(this._handleError, this);
this._rawConn.onclose = _bind(this._handleClose, this);
this._rawConn.onmessage = _bind(this._handleRawMessage, this);
this._setStatus(CONN_STATUS.CONNECTING);
return true;
}
return false;
}
/**
* Reconnect to the server with unlimited tries. A period
* of tries is 5 seconds. It reconnects only if not
* connected. It cancels previously scheduled `connect` by `reconnect`.
* Returns a function for canceling reconnection process or undefined
* if connection is not disconnected.
* @return {Function}
*/
reconnect() {
if (this.isDisconnected) {
clearTimeout(this._reconnTimer);
this._reconnecting = true;
this._reconnTimer = setTimeout(
_bind(this.connect, this),
RECONNECT_INTERVAL
);
return () => {
clearTimeout(this._reconnTimer);
this._reconnecting = false;
this.disconnect();
};
}
}
/**
* Close WebSocket connection. If autoReconnect is enabled
* (enabled by default), then after 5 sec reconnection will
* be initiated.
*/
disconnect() {
_try(() => this._rawConn && this._rawConn.close());
}
_handleOpen() {
this._heartbeat.waitPing();
const connMsg = {
msg: 'connect',
version: DDP_VERSION,
support: [DDP_VERSION],
};
if (this._sessionId) {
connMsg.session = this._sessionId;
}
this._sendMessage(connMsg);
}
_handleConnectedMessage(msg) {
if (!this.isConnected) {
const isTrulyReconnected = this._fullConnectedOnce && this._reconnecting;
this._setStatus(CONN_STATUS.CONNECTED, isTrulyReconnected);
this._sessionId = msg.session;
this._reconnecting = false;
this._fullConnectedOnce = true;
}
}
_handleClose() {
this._heartbeat._clearTimers();
this._setStatus(CONN_STATUS.DISCONNECTED, this._fullConnectedOnce);
if (this._autoReconnect) {
this._reconnecting = false;
this.reconnect();
}
}
_handleHearbeatTimeout() {
this.disconnect();
}
_handleError(error) {
this.emit('error', error);
}
_handleRawMessage(rawMsg) {
return this._processQueue.add(() => {
const msgObj = EJSON.parse(rawMsg.data);
return this._processMessage(msgObj);
}).then(null, err => {
this._handleError(err);
});
}
_processMessage(msg) {
switch (msg.msg) {
case 'connected': return this._handleConnectedMessage(msg);
case 'ping': return this._heartbeat.handlePing(msg);
case 'pong': return this._heartbeat.handlePong(msg);
case 'removed':
case 'changed':
case 'added':
case 'updated':
case 'result':
case 'nosub':
case 'ready':
case 'error':
return this.emitAsync(`message:${msg.msg}`, msg);
default:
// just ignore unknown message
}
}
_sendMessage(msgObj) {
const result = _try(() =>
this._rawConn.send(EJSON.stringify(msgObj))
);
if (result instanceof Error) {
this._handleError(result);
}
}
_setStatus(status, a) {
this._status = status;
this.emit(`status:${status}`.toLowerCase(), a);
}
}