diffusion
Version:
Diffusion JavaScript client
321 lines (250 loc) • 10.7 kB
JavaScript
var Emitter = require('events/emitter');
var sessionActivityMonitorModule = require('activity/session-activity-monitor');
var ServiceAdapter = require('client/service-adapter');
var ServiceLocator = require('client/service-locator');
var StreamRegistry = require('routing/stream-registry');
var TopicRouting = require('routing/topic-routing');
var TopicCache = require('routing/topic-cache');
var InternalSessionLocks = require('../features/session-lock').InternalSessionLocks;
var DataTypes = require('data/datatypes');
var serialisers = require('services/serialisers');
var ConnectionRequest = require('protocol/connection-request');
var ResponseCode = require('protocol/response-code');
var CloseReason = require('../../client/close-reason');
var Transports = require('transports/transports');
var log = require('util/logger').create('Internal Session');
var curry = require('util/function').curry;
var FSM = require('util/fsm');
var connectionActivityMonitorFactory = require('activity/connection-activity-monitor-factory');
function InternalSession(conversationSetFactory, serviceRegistry, connectionFactory, opts) {
var emitter = Emitter.assign(this);
var principal = "";
var sessionID;
var token;
var sessionActivityMonitor;
var conversationSet = conversationSetFactory.create();
var fsm = FSM.create('initialising', {
initialising: ['connecting'],
connecting: ['connected', 'closing'],
connected: ['disconnected', 'closing', 'closed'],
disconnected: ['reconnecting', 'closing'],
reconnecting: ['connected', 'closing'],
closing: ['closed'],
closed: []
});
fsm.on('change', function (previous, current) {
log.debug('State changed: ' + previous + ' -> ' + current);
});
var self = this;
this.getOptions = function() {
return opts;
};
this.getState = function () {
return fsm.state;
};
this.getConversationSet = function () {
return conversationSet;
};
this.isConnected = function () {
return fsm.state === 'connected';
};
this.getServiceRegistry = function () {
return serviceRegistry;
};
this.getSessionId = function () {
return sessionID;
};
this.setPrincipal = function (newPrincipal) {
principal = newPrincipal;
};
this.checkConnected = function (emitter) {
if (self.isConnected()) {
return true;
} else {
emitter.error(new Error('The session is not connected. Operations are not possible at this time.'));
return false;
}
};
var transports = Transports.create();
// Forward cascading notification events
transports.on({
'transport-selected': function (name) {
emitter.emit('transport-selected', name);
},
cascade: function () {
emitter.emit('cascade');
}
});
var connection = connectionFactory.create(transports, opts.reconnect.timeout, 256);
var serviceAdapter = new ServiceAdapter(this, serialisers, connection.send);
var serviceLocator = new ServiceLocator(this, serialisers, serviceAdapter);
var topicCache = new TopicCache(DataTypes);
var topicStreamRegistry = new StreamRegistry(topicCache);
var topicRouting = new TopicRouting(this, serviceAdapter, conversationSet, topicCache, topicStreamRegistry);
serviceRegistry.addListener(serviceAdapter.addService);
// Close the session
function close(reason) {
if (fsm.change('closed')) {
topicStreamRegistry.close();
conversationSet.discardAll(reason);
emitter.close(reason);
} else {
log.debug('Unable to handle session close, session state: ', fsm.state);
}
}
// Attempt to reconnect based on existing options / sessionID
var attempts = 0;
var reconnect = function (opts) {
if (fsm.change('reconnecting')) {
log.info('Reconnect attempt (' + (++attempts) + ')');
var request = ConnectionRequest.reconnect(
token,
connection.getAvailableSequence(),
connection.lastReceivedSequence);
connection.connect(request, opts, opts.reconnect.timeout);
} else {
log.debug('Unable to attempt reconnect, session state: ', fsm.state);
}
};
// Cancel a reconnect event
var abort = function (reason) {
if (fsm.change('closing')) {
log.debug('Aborting reconnect');
close(reason || CloseReason.RECONNECT_ABORTED);
} else {
log.debug('Unable to abort reconnect, session state: ', fsm.state);
}
};
function replaceConversationSet(err) {
var oldConversationSet = conversationSet;
conversationSet = conversationSetFactory.create();
oldConversationSet.discardAll(err);
log.debug('Replaced conversation set', err);
}
this.connect = function () {
if (fsm.change('connecting')) {
// Timeout applied for reconnect attempts from initial disconnect
var reconnectTimeout;
// Construct a creation request
var request = ConnectionRequest.connect();
// Handle messages from connection
connection.on('data', topicRouting.route);
// Close events are terminal
connection.on('close', close);
if (opts.activityMonitor) {
sessionActivityMonitor = sessionActivityMonitorModule.create(connectionActivityMonitorFactory);
} else {
sessionActivityMonitor = sessionActivityMonitorModule.NOOP;
}
// Handle connection disconnections
connection.on('disconnect', function (reason) {
log.debug('Connection disconnected, reason: ', reason);
if (fsm.change('disconnected') || fsm.state === 'reconnecting') {
sessionActivityMonitor.onConnectionClosed();
// Call reconnect if applicable
if (opts.reconnect.timeout > 0 && reason.canReconnect) {
opts.token = token;
// Only emit the disconnect state once
if (fsm.state === 'disconnected') {
emitter.emit('disconnect', reason);
reconnectTimeout = setTimeout(function () {
if (fsm.state !== 'connected') {
abort(CloseReason.CONNECTION_TIMEOUT);
}
}, opts.reconnect.timeout);
}
opts.reconnect.strategy(curry(reconnect, opts), abort, reason);
} else {
abort(reason);
}
} else if (fsm.change('closing')) {
close(reason);
} else {
sessionActivityMonitor.onConnectionClosed();
log.debug('Unable to handle session disconnect, session state: ', fsm.state);
}
});
// Handle connect response
connection.on('connect', function (response) {
if (fsm.change('connected')) {
token = response.token;
if (response.response === ResponseCode.OK) {
sessionID = response.identity;
sessionActivityMonitor.onNewConnection(connection, response);
emitter.emit('connect', response.identity);
} else if (
response.response === ResponseCode.RECONNECTED ||
response.response === ResponseCode.RECONNECTED_WITH_MESSAGE_LOSS) {
attempts = 0;
clearTimeout(reconnectTimeout);
if (response.response === ResponseCode.RECONNECTED_WITH_MESSAGE_LOSS) {
connection.resetSequences();
replaceConversationSet(new Error("Peer is disconnected"));
topicCache.notifyUnsubscriptionOfAllTopics(topicStreamRegistry);
log.info("Reconnected session, but messages may have been lost");
} else {
log.info('Reconnected session');
}
sessionActivityMonitor.onNewConnection(connection, response);
emitter.emit('reconnect');
}
} else {
log.trace('Unknown connection response: ', response);
}
});
log.debug('Connecting with options:', opts);
log.trace('Connecting with request:', request);
try {
connection.connect(request, opts);
} catch (e) {
log.warn('Connection error', e);
emitter.emit('error', e);
if (fsm.change('closing')) {
close(CloseReason.CONNECTION_ERROR);
}
}
if (opts.principal) {
principal = opts.principal;
}
} else {
log.warn('Unable to connect, session state: ', fsm.state);
emitter.emit('error', new Error('Unable to connect, session state: ' + fsm.state));
}
};
this.close = function () {
if (fsm.change('closing')) {
sessionActivityMonitor.onConnectionClosed();
connection.close(CloseReason.CLOSED_BY_CLIENT);
} else {
log.debug('Unable to close, session state: ', fsm.state);
}
};
this.getErrorHandler = function () {
return function (error) {
log.error("Session error:", error.message);
};
};
this.getServiceLocator = function () {
return serviceLocator;
};
this.getServiceAdapter = function () {
return serviceAdapter;
};
this.getPrincipal = function () {
return principal;
};
this.getStreamRegistry = function () {
return topicStreamRegistry;
};
this.getRouting = function () {
return topicRouting;
};
this.onSystemPing = function () {
sessionActivityMonitor.onSystemPing();
};
var sessionLocks = new InternalSessionLocks(this, fsm);
this.lock = function(lockName, scope) {
return sessionLocks.lock(lockName, scope);
};
}
module.exports = InternalSession;