<commit_before>var EventLog = require('./eventLog')
  , Heartbeat = require('./heartbeat')
  , io = require('socket.io-client')
  , Message = require('./message')
  , Principal = require('./principal')
  , request = require('request');
  
/**
 * A Session represents an authenticated session and subscription connection between a principal and a service.
 *
 * @class Session
 * @namespace nitrogen
 * @param {Object} service The service this session is associated with.
 * @param {Object} principal The principal this session is associated with.
 * @param {Object} accessToken The accessToken to use for authenticating requests with this session.
 * @param {Object} socket The subscription socket to use for realtime updates.
 */

function Session(service, principal, accessToken) {
    this.service = service;
    this.principal = principal;
    this.accessToken = accessToken;

    this.subscriptionCount = 0;
    this.sessionId = Math.floor(Math.random()*100000000);

    this.subscriptions = {};
    this.authFailureCallback = function() {};

    this.log = new EventLog(this);
    this.log.start();

    this.heartbeat = new Heartbeat(this);
    this.heartbeat.start();
}

Session.queryStringFromObject = function(obj) {
    var str = [];

    for(var p in obj)
        str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));

    return str.join("&");
};

/**
 * Stop the session with the service.
 *
 * @method stop
 * @private
 **/

Session.prototype.stop = function() {
    var self = this;
    for (var key in this.subscriptions) {
        this.disconnectSubscription(this.subscriptions[key]);        
    }

    if (this.socket)
        this.socket.disconnect();

    this.heartbeat.stop();
    this.log.stop();
};

Session.prototype.beforeRequest = function(options) {
    if (!options.headers) options.headers = {};
    options.headers.Authorization = "Bearer " + this.accessToken.token;

    var prefix = "?";
    if (options.query) {
        var queryString = JSON.stringify(options.query);
        options.url += prefix + "q=" + encodeURIComponent(queryString);
        delete options.query;

        prefix = "&";
    }

    if (options.queryOptions) {
        var optionsString = JSON.stringify(options.queryOptions);
        options.url += prefix + "options=" + encodeURIComponent(optionsString);
        delete options.queryOptions;
    }
};

Session.prototype.afterRequest = function(err, resp, body, callback) {
    if (resp && resp.statusCode !== 200) err = body && body.error ? body.error : resp.statusCode;
    if (resp && resp.statusCode === 401) this.authFailureCallback();

    // If we get an access token update, replace the current access token and continue.
    if (resp && resp.headers && resp.headers['x-n2-set-access-token']) {
        this.accessToken = JSON.parse(resp.headers['x-n2-set-access-token']);
        this.principal.accessToken = this.accessToken;

        // if we have a store, update the principal there.  (agents don't have a store)
        if (this.service.store) {
            this.service.store.set(this.principal.toStoreId(), this.principal.toStoreObject());
        }
    }

    callback(err, resp, body);
};

Session.prototype.makeRequest = function(options, requestFunc, callback) {
    var self = this;

    this.beforeRequest(options);
    return requestFunc(options, function(err, resp, body) {
        self.afterRequest(err, resp, body, callback);
    });
};

Session.prototype.get = function(options, callback) {
    return this.makeRequest(options, request.get, callback);
};

Session.prototype.post = function(options, callback) {
    return this.makeRequest(options, request.post, callback);
};

Session.prototype.put = function(options, callback) {
    return this.makeRequest(options, request.put, callback);
};

Session.prototype.remove = function(options, callback) {
    return this.makeRequest(options, request.del, callback);
};

/**
 * Connect subscription socket for principal with this accessToken to the service. 
 *
 * @method connectSocket
 * @privateses
 * @param {Object} principal The principal to open a subscription socket for.
 * @param {Object} accessToken The accessToken to open a subscription socket for.
 **/

Session.prototype.connectSocket = function() {
    if (!this.principal || !this.principal.id || !this.accessToken) throw new Error('need both principal and accessToken to connectSocket');

    // we can only share a socket on a per principal basis because otherwise principals can listen in
    // on other principals' events.
    if (!this.socket) {
        this.socket = io.connect(this.service.config.subscriptions_endpoint, {
            query: Session.queryStringFromObject({ auth: this.accessToken.token }),
            'force new connection': true
        });

        this.setupSocketEvents();
    }

    return this.socket;
};

Session.prototype.setupSocketEvents = function() {
    var self = this;

    this.socket.on('connecting', function() { self.log.info('session: socket.io connecting'); });
    this.socket.on('connect', function() { self.log.info('session: socket.io connected'); });
    this.socket.on('disconnect', function() { self.log.info('session: socket.io connection disconnected'); });
    this.socket.on('reconnecting', function() { self.log.info('session: socket.io connection reconnecting'); });

    this.socket.on('reconnect', function() {  
        self.log.warn('session: socket.io connection reconnected.  restarting subscriptions: ' + JSON.stringify(self.subscriptions));
        self.restartSubscriptions(); 
    });
};

/**
 * Disconnect the current subscription connection with the service.
 *
 * @method disconnectSubscription<commit_msg>only start heartbeat if we open a subscription socket<commit_after>var EventLog = require('./eventLog')
  , Heartbeat = require('./heartbeat')
  , io = require('socket.io-client')
  , Message = require('./message')
  , Principal = require('./principal')
  , request = require('request');
  
/**
 * A Session represents an authenticated session and subscription connection between a principal and a service.
 *
 * @class Session
 * @namespace nitrogen
 * @param {Object} service The service this session is associated with.
 * @param {Object} principal The principal this session is associated with.
 * @param {Object} accessToken The accessToken to use for authenticating requests with this session.
 * @param {Object} socket The subscription socket to use for realtime updates.
 */

function Session(service, principal, accessToken) {
    this.service = service;
    this.principal = principal;
    this.accessToken = accessToken;

    this.subscriptionCount = 0;
    this.sessionId = Math.floor(Math.random()*100000000);

    this.subscriptions = {};
    this.authFailureCallback = function() {};

    this.log = new EventLog(this);
    this.log.start();
}

Session.queryStringFromObject = function(obj) {
    var str = [];

    for(var p in obj)
        str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));

    return str.join("&");
};

/**
 * Stop the session with the service.
 *
 * @method stop
 * @private
 **/

Session.prototype.stop = function() {
    var self = this;
    for (var key in this.subscriptions) {
        this.disconnectSubscription(this.subscriptions[key]);        
    }

    if (this.socket)
        this.socket.disconnect();

    if (this.heartbeat) 
        this.heartbeat.stop();
    
    this.log.stop();
};

Session.prototype.beforeRequest = function(options) {
    if (!options.headers) options.headers = {};
    options.headers.Authorization = "Bearer " + this.accessToken.token;

    var prefix = "?";
    if (options.query) {
        var queryString = JSON.stringify(options.query);
        options.url += prefix + "q=" + encodeURIComponent(queryString);
        delete options.query;

        prefix = "&";
    }

    if (options.queryOptions) {
        var optionsString = JSON.stringify(options.queryOptions);
        options.url += prefix + "options=" + encodeURIComponent(optionsString);
        delete options.queryOptions;
    }
};

Session.prototype.afterRequest = function(err, resp, body, callback) {
    if (resp && resp.statusCode !== 200) err = body && body.error ? body.error : resp.statusCode;
    if (resp && resp.statusCode === 401) this.authFailureCallback();

    // If we get an access token update, replace the current access token and continue.
    if (resp && resp.headers && resp.headers['x-n2-set-access-token']) {
        this.accessToken = JSON.parse(resp.headers['x-n2-set-access-token']);
        this.principal.accessToken = this.accessToken;

        // if we have a store, update the principal there.  (agents don't have a store)
        if (this.service.store) {
            this.service.store.set(this.principal.toStoreId(), this.principal.toStoreObject());
        }
    }

    callback(err, resp, body);
};

Session.prototype.makeRequest = function(options, requestFunc, callback) {
    var self = this;

    this.beforeRequest(options);
    return requestFunc(options, function(err, resp, body) {
        self.afterRequest(err, resp, body, callback);
    });
};

Session.prototype.get = function(options, callback) {
    return this.makeRequest(options, request.get, callback);
};

Session.prototype.post = function(options, callback) {
    return this.makeRequest(options, request.post, callback);
};

Session.prototype.put = function(options, callback) {
    return this.makeRequest(options, request.put, callback);
};

Session.prototype.remove = function(options, callback) {
    return this.makeRequest(options, request.del, callback);
};

/**
 * Clear all of the credentials for a particular principal.
 *
 * @method clearCredentials
 * @private
 * @param {Object} principal The principal to clear credentials for.
 **/

Session.prototype.clearCredentials = function() {
    this.service.clearCredentials(this.principal);
};

/**
 * Connect subscription socket for principal with this accessToken to the service. 
 *
 * @method connectSocket
 * @privateses
 * @param {Object} principal The principal to open a subscription socket for.
 * @param {Object} accessToken The accessToken to open a subscription socket for.
 **/

Session.prototype.connectSocket = function() {
    var self = this;
    if (!this.principal || !this.principal.id || !this.accessToken) throw new Error('need both principal and accessToken to connectSocket');

    // we can only share a socket on a per principal basis because otherwise principals can listen in
    // on other principals' events.
    if (!this.socket) {
        this.socket = io.connect(this.service.config.subscriptions_endpoint, {
            query: Session.queryStringFromObject({ auth: this.accessToken.token }),
            'force new connection': true
        });

        this.setupSocketEvents();

        self.heartbeat = new Heartbeat(self);
        self.heartbeat.start();
    }

    return this.socket;
};

Session.prototype.setupSocketEvents = function() {
    var self = this;

    this.socket.on('connecting', function() { self.log.info('session: socket.io connecting'); });
    this.socket.on('connect', function() { self.log.info('session: socket.io connected'); });
    this.socket.on('disconnect', function() { self.log.info('session: socket.io connection disconnected'); });
    this.socket.on('reconnecting', function() { self.log.info('session: socket.io connection reconnecting'); });

    this.socket.on('reconnect', function() {  
        self.log.warn('session: socket.io connection reconnected.  restarting subscriptions: ' + JSON.stringify(self.subscriptions));
        self.restartSubscriptions(); 
    });
};

/**
 * Disconnect the current subscription connection with the service.
 *
 * @method disconnectSubscription
{'fraction_non_alphanumeric': 0.13071173772719552, 'fraction_numerical': 0.0030575845082384916, 'mean_word_length': 3.492560091568104, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1249492', 'n_tokens_mistral': 3290, 'n_tokens_neox': 3230, 'n_words': 860}