Source: lib/pool.js


'use strict';

var debug = require( 'debug' )( 'informix:pool' );
var Ifx = require( 'bindings' )( 'ifx' ).Ifx;

var Connection = require( './connection' );


/**
*   Class representing a connection pool
*
*   @constructor
*   @param {object} [opts] - Constructor options
*/
var Pool = function ( opts ) {
	this.$reset( opts );
};


/**
*   Internal helper function to add a new connection to the pool
*
*   @private
*   @return {Connection|undefined} - A connection object if successful or
*           undefined if unsuccessful.
*/
Pool.prototype.$add = function () {

	var conn;
	var index = ( this.$.pool.length ? this.$.pool.length : 0 );

	if ( index < this.$.max ) {
		var params = {
			database : this.$.opts.database
		};

		if ( this.$.opts.username ) { params.username = this.$.opts.username; }
		if ( this.$.opts.password ) { params.password = this.$.opts.password; }


		debug( '($add) adding new connection to the pool, index: %d', index );
		conn = new Connection( this.$.ifx, this, { index : index } );

		this.$.pool.push( conn );
		this.$.promises.push( conn.connect( params ) );
		this.$.resolvers.push( [] );
	}


	return conn;

};


/**
*   Internal helper function to get the next available connection
*
*   @private
*   @return {Connection} - Connection object
*/
Pool.prototype.$next = function () {

	var self = this;
	var conn;

	if ( this.$.pool.length ) {

		// There should be at least one connection in the pool, try to filter out any
		// idle connections.
		var filter = function ( element, index, array ) {
			// There won't be any resolvers left when all the promises for this
			// connection are resolved.
			return ( self.$.resolvers[ index ].length === 0 );
		};

		conn = this.$.pool.find( filter );

	}


	// Check whether there was an idle connection which could be used.
	if (! conn ) {

		// No idle connections found, try to add a new connection to the pool.
		conn = this.$add();

		if (! conn ) {

			// No idle connections found and couldn't add a new connection to
			// the pool, re-use existing connections in a round robin fashion.
			debug( '($next) no idle connections, reusing %d', this.$.next );
			conn = this.$.pool[ this.$.next ];
			if ( ++this.$.next >= this.$.pool.length ) {
				this.$.next = 0;
			}

		}
	}


	return conn;

};


/**
*   Internal helper function to return a promise for a connection
*
*   @private
*   @param {Connection|string} c - Connection object or context ID string
*   @return {Promise.<Connection, Error>} - A promise to a connection object or
*           an Error if rejected.
*/
Pool.prototype.$promise = function ( c ) {

	var self = this;
	var promise;

	// Take a copy of the existing promise (first ever promise will be from connect())
	// and replace it with a new promise for this connection/context which will be resolved
	// when released back to the pool.

	if ( c instanceof Connection ) {
		promise = this.$.promises[ c.index() ];
		this.$.promises[ c.index() ] = new Promise( function ( resolve, reject ) {
			self.$.resolvers[ c.index() ].push( resolve );
		} );

		debug( '($promise)[ %d ] waiters: %d', c.index(), ( this.$.resolvers[ c.index() ].length - 1 ) );
	} else {
		var ctx = this.$.contexts[ c ];
		promise = ctx.promise;
		ctx.promise = new Promise( function ( resolve, reject ) {
			ctx.resolvers.push( resolve );
		} );

		debug( '($promise)[ %s ] waiters: %d', c, ( ctx.resolvers.length - 1 ) );
	}


	// Return a promise chain which will wait for the last promise created for this
	// connection/context and then return a connection object.
	return promise
		.then( function ( conn ) {
			return conn;
		} );

};


/**
*   Internal helper function to reset the connection pool
*
*   @private
*   @param {object} [opts] - Options
*/
Pool.prototype.$reset = function ( opts ) {

	// privileged data
	this.$ = {
		ifx  : new Ifx(),
		min  : 0,
		max  : 5,
		next : 0,
		opts : {},
		pool : [],
		contexts  : {},
		promises  : [],
		resolvers : []
	};

	this.options( opts );

};


/**
*   Acquire a connection from the pool
*
*   @param {Connection|string} [c] - A connection object or a context ID string
*          to indicate which connection to be aquired from the pool.
*          If this is null, a randon connection will be aquired from the pool.
*
*   @return {Promise.<Connection, Error>} - A promise to a connection object or
*           an Error if rejected.
*/
Pool.prototype.acquire = function ( c ) {

	var self = this;

	if ( (typeof c) === 'string' ) {
		if (! this.$.contexts[ c ] ) {
			this.reserve( c );
		}

		debug( '(acquire)[ %s ]', c );
		return this.$promise( c );
	}


	var conn;
	if ( c instanceof Connection ) {
		conn = c;
	} else {
		conn = this.$next();
	}

	debug( '(acquire)[ %d ]', conn.index() );
	return this.$promise( conn );

};


/**
*   Close a context and release the reserved connection back to the pool
*
*   @param {string} context - Context ID
*   @return {Promise<string, Error>} - A promise to a context ID string which
*           resolves after the context is closed or an Error if rejected.
*/
Pool.prototype.close = function ( context ) {

	var self = this;
	var ctx  = this.$.contexts[ context ];

	debug( '(close)[ %s ] pending promises: %d', context, ctx.resolvers.length );
	return ctx.promise
		.then( function ( conn ) {
			ctx.resolve( conn );
			delete self.$.contexts[ context ];

			return context;
		} );

};


/**
*   Set options
*
*   @param {object} opts - Options
*/
Pool.prototype.options = function ( opts ) {
	this.$.opts = opts || {};

	if ( this.$.opts.min ) { this.$.min = parseInt( this.$.opts.min ); }
	if ( this.$.opts.max ) { this.$.max = parseInt( this.$.opts.max ); }
};


/**
*   Release a connection back to the pool
*
*   @param {Connection|string} c - Connection object or a context ID
*/
Pool.prototype.release = function ( c ) {

	var conn, resolve;

	if ( c instanceof Connection ) {
		debug( '(release)[ %d ]', c.index() );

		conn = c;
		resolve = this.$.resolvers[ c.index() ].shift();
	} else {
		debug( '(release)[ %s ]', c );

		var ctx = this.$.contexts[ c ];
		conn    = ctx.conn;
		resolve = ctx.resolvers.shift();
	}


	// Release the connection by resolving the first available promise. There will
	// be at least one promise to resolve since we always return a new promise to
	// a connection.
	resolve( conn );

};


/**
*   Reserve a connection to be used with a new context
*
*   @param {string} context - Context ID
*   @return {Connection} - A connection object reserved for this context.
*/
Pool.prototype.reserve = function ( context ) {

	if ( this.$.contexts[ context ] ) {
		throw new Error( 'A connection is already reserved for the given context.' );
	}

	debug( '(reserve)[ %s ]', context );

	var conn = this.$next();
	var ctx  = this.$.contexts[ context ] = {
		conn : conn,
		promise   : this.$.promises[ conn.index() ],
		resolve   : false,
		resolvers : []
	};

	this.$.promises[ conn.index() ] = new Promise( function ( resolve, reject ) {
		ctx.resolve = resolve;
	} );


	return conn;

};



module.exports = Pool;