diffusion
Version:
Diffusion JavaScript client
291 lines (231 loc) • 9.49 kB
JavaScript
/*eslint valid-jsdoc: "off"*/
var RecoveryBuffer = require('message-queue/recovery-buffer');
var BufferOutputStream = require('io/buffer-output-stream');
var ResponseCode = require('protocol/response-code');
var CloseReason = require('../../client/close-reason');
var Message = require('v4-stack/message');
var Emitter = require('events/emitter');
var logger = require('util/logger').create('Connection');
var FSM = require('util/fsm');
// The amount of time to wait for the server to close the connection after a req is sent.
var CLOSE_TIMEOUT = global.DIFFUSION_CLOSE_TIMEOUT || 1000;
// The amount of time waiting for a connection to occur.
var CONNECT_TIMEOUT = global.DIFFUSION_CONNECT_TIMEOUT || 10000;
// The default recovery buffer index size.
var RECOVERY_BUFFER_INDEX_SIZE = global.DIFFUSION_RECOVERY_BUFFER_INDEX_SIZE || 8;
/**
* The layer that abstracts across transports and handles the connection-level
* protocol. The externally exposed events from a connection are "connect",
* "disconnect" and "error". A connection only cares about whether it is connected or not.
*
* Higher level semantics such as reconnect or session close, are determined by
* the consumer of this connection's events, i.e the internal session.
*/
function Connection(transports, reconnectTimeout, recoveryBufferSize) {
var emitter = Emitter.assign(this);
var fsm = FSM.create('disconnected', {
connecting : ['connected', 'disconnected', 'closed'],
connected : ['disconnecting', 'disconnected', 'closed'],
disconnecting : ['disconnected'],
disconnected : ['connecting', 'closed'],
closed : []
});
var lastSentSequence = 0;
this.lastReceivedSequence = 0;
var recoveryBuffer = new RecoveryBuffer(recoveryBufferSize, RECOVERY_BUFFER_INDEX_SIZE);
var scheduledRecoveryBufferTrim;
var scheduledClose;
var closeReason;
var transport = null;
var self = this;
fsm.on('change', function(previous, current) {
logger.debug('State changed: ' + previous + ' -> ' + current);
});
// Parse messages and propagate
function onData(data) {
logger.debug('Received message from transport', data);
Message.parse(data, function(err, message) {
if (!err) {
if (message.type === Message.types.ABORT_NOTIFICATION) {
transport.close();
return;
}
self.lastReceivedSequence++;
emitter.emit('data', message);
}
else {
logger.warn('Unable to parse message', err);
if (fsm.change('closed')) {
// Should this be TRANSPORT_ERROR?
closeReason = CloseReason.CONNECTION_ERROR;
transport.close();
}
}
});
}
// Handle close notifications
function onClose() {
if (fsm.change('disconnected') || fsm.change('closed')) {
clearInterval(scheduledRecoveryBufferTrim);
clearTimeout(scheduledClose);
logger.trace('Transport closed: ', closeReason);
if (fsm.state === 'disconnected') {
emitter.emit('disconnect', closeReason);
} else {
emitter.close(closeReason);
}
} else {
logger.debug('Unable to disconnect, current state: ', fsm.state);
}
}
function onError(err) {
logger.error("Error from transport", err);
if (fsm.change('closed')) {
closeReason = CloseReason.TRANSPORT_ERROR;
transport.close();
}
}
/**
* Handle connection responses.
* @param response {ConnectionResponse} Parsed connection response
*/
function onHandshakeSuccess(response) {
if (response.response === ResponseCode.RECONNECTED) {
var messageDelta = lastSentSequence - response.sequence + 1;
if (recoveryBuffer.recover(messageDelta, dispatch)) {
recoveryBuffer.clear();
lastSentSequence = response.sequence - 1;
} else {
var outboundLoss = lastSentSequence - recoveryBuffer.size() - response.sequence + 1;
logger.warn("Unable to reconnect due to lost messages (" + outboundLoss + ")");
if (fsm.change('disconnected')) {
closeReason = CloseReason.LOST_MESSAGES;
transport.close();
}
return response;
}
}
if (response.success && fsm.change('connected')) {
logger.trace('Connection response: ', response.response);
closeReason = CloseReason.TRANSPORT_ERROR;
emitter.emit('connect', response);
} else {
logger.error('Connection failed: ', response.response);
if (response.response === ResponseCode.AUTHENTICATION_FAILED) {
closeReason = CloseReason.ACCESS_DENIED;
} else {
closeReason = CloseReason.HANDSHAKE_REJECTED;
}
transport.close();
}
clearTimeout(scheduledClose);
return response;
}
/**
* Handle connection response parsing errors.
* @param response {Error} Error thrown parsing the response
*/
function onHandshakeError(error) {
closeReason = CloseReason.HANDSHAKE_ERROR;
// Transport closed and close event emitted
clearTimeout(scheduledClose);
logger.trace('Unable to deserialise handshake response', error);
return;
}
/**
* Establish a connection with a provided connection request and connection options.
*/
this.connect = function connect(request, opts, timeout) {
if (fsm.state !== 'disconnected') {
logger.warn('Cannot connect, current state: ', fsm.state);
return;
}
if (fsm.change('connecting')) {
timeout = timeout === undefined ? CONNECT_TIMEOUT : timeout;
closeReason = CloseReason.CONNECTION_ERROR;
// Begin a connection cycle with a transport
transport = transports.get(opts);
if (transport === null) {
throw new Error('No valid transports requested');
}
transport.on('data', onData);
transport.on('close', onClose);
transport.on('error', onError);
// Attempt to connect the transport
transport.connect(request, onHandshakeSuccess, onHandshakeError);
// Ensure we will emit a close reason if we're unable to connect within a timeout
scheduledClose = setTimeout(function() {
logger.debug('Timed out connection attempt after ' + timeout);
closeReason = CloseReason.CONNECTION_TIMEOUT;
transport.close();
}, timeout);
scheduledRecoveryBufferTrim = setInterval(function() {
if (fsm.state === 'connected') {
recoveryBuffer.flush(Date.now() - reconnectTimeout);
}
}, reconnectTimeout);
}
};
this.resetSequences = function() {
recoveryBuffer.clear();
lastSentSequence = 0;
this.lastReceivedSequence = 0;
};
this.getAvailableSequence = function() {
return lastSentSequence + 1 - recoveryBuffer.size();
};
// Internal dispatch method for converting a message into a buffer
function dispatch(message) {
var bos = new BufferOutputStream();
Message.writeToBuffer(message, bos);
lastSentSequence += 1;
recoveryBuffer.put(message);
recoveryBuffer.markTime(Date.now());
transport.dispatch(bos.getBuffer());
}
/**
* Send a message. Will return true if the message was succesfully enqueued
* on the transport.
* <P>
* Sending messages is only valid in a 'connected' state. This method will
* return false otherwise.
*/
this.send = function(message) {
if (fsm.state === 'connected') {
return dispatch(message);
}
return false;
};
/**
* Close the connection. When the connection has been closed, it will emit a
* 'disconnected' event.
*
* @param {CloseReason} reason - The close reason
*/
this.close = function(reason) {
closeReason = reason;
if (fsm.state === 'disconnected') {
// transport is already closed and the event isn't emitted again so the callback is called directly
onClose();
} else if (fsm.change('disconnecting')) {
// We allow the server a grace period to close the transport connection
dispatch(Message.create(Message.types.CLOSE_REQUEST));
// Explicitly close the transport if a given timeout elapses.
scheduledClose = setTimeout(transport.close, CLOSE_TIMEOUT);
}
};
this.closeIdleConnection = function() {
if (fsm.change('disconnecting')) {
logger.debug('Connection detected as idle');
closeReason = CloseReason.IDLE_CONNECTION;
transport.close();
}
};
/**
* Get the current state of the connection.
*/
this.getState = function() {
return fsm.state;
};
}
module.exports = Connection;