UNPKG

diffusion

Version:

Diffusion JavaScript client

291 lines (231 loc) 9.49 kB
/*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;