<commit_before>/* jshint node:true */
"use strict";

var events = require('events');
var util = require('util');

var _ = require('lodash');
var logging = require('omega-logger');
var pg = require('pg');
var pgTypes = require('pg').types;
var Promise = require('bluebird');

// --------------------------------------------------------------------------------------------------------------------

var logger = logging.loggerFor(module);

// Type OIDs for built-in types we need to know about.
var byteaTypeOID = 17;

// --------------------------------------------------------------------------------------------------------------------

/**
 * Connect to the given database.
 *
 * @todo Document parameters.
 */
function connect(connectionInfo)
{
    return new Promise(function(resolve, reject)
    {
        // Set application_name on the connection.
        connectionInfo.application_name = connectionInfo.application_name || 'web-pgq';

        pg.connect(connectionInfo, function(error, client, done)
        {
            if(error)
            {
                logger.error("Error connecting to %j: %s", connectionInfo, error.stack || error);

                return reject(error);
            } // end if

            logger.info("Connected to %j", connectionInfo);

            var conn = new Connection(client, done);

            resolve(
                conn.getSettings()
                    .then(conn.getTypes.bind(conn))
                    .return(conn)
            );
        }); // end pg.connect callback
    }); // end Promise callback
} // end connect

// --------------------------------------------------------------------------------------------------------------------

/**
 * Wraps a PostgreSQL connection.
 *
 * @param {pg.Client} client - the `node-postgres` client
 * @param {function} done - the function to release the client to the `node-postgres` pool
 */
function Connection(client, done)
{
    this.client = client;
    this.done = done;
} // end Connection

util.inherits(Connection, events.EventEmitter);

/**
 * The definition of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {*} queryID - the ID of the query batch (an arbitrary value provided by the client)
 * @property {Array.<Connection#QueryDef>} queries - the collection of queries to run
 * @property {boolean} [rollback] - if not undefined, run the queries in a transaction; if true, roll it back instead
 *          of committing when all finish successfully
 */

/**
 * The definition of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {string} [name] - if supplied, creates or uses a prepared statement with the given name
 * @property {string} [text] - the text of the SQL query to run or prepare (required unless using a prepared statement)
 * @property {Array} [values] - the values of the query parameters
 */

/**
 * The results of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryResults
 *
 * @property {number} rowCount - the number of rows in the result set
 * @property {number} affectedRowCount - the number of affected rows reported by the database
 * @property {number} totalTimeMS - the total time the query took to run, in milliseconds
 */

/**
 * The results of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchResults
 *
 * @property {string} status - the final status of the batch; one of: `"committed"`, `"committed (no transaction)"`, or
 *          `"rolled back"`
 * @property {number} totalTimeMS - the total time the batch took to run, in milliseconds
 * @property {Array.<Connection#QueryResults>} queryResults - the results object of each query, with some extra
 *          information attached
 */

/**
 * A row has been generated by a database query.
 *
 * @event Connection#row
 *
 * @param {*} queryID - the ID of the query that generated this row
 * @param {object} row - the row
 */

/**
 * An error occurred on this connection.
 *
 * @event Connection#error
 *
 * @param {object} error - the error that occurred
 */

Object.defineProperty(Connection.prototype, 'remoteAddress', {
    enumerable: true,
    get: function()
    {
        return this.client.connection.stream.remoteAddress;
    } // end get
}); // end '

/**
 * "Close" this connection.
 *
 * Actually, this releases the underlying client back to the to the `node-postgres` pool, unless `error` is passed.
 *
 * @param {object} error - a fatal connection error that occurred; forces the connection to be closed instead of being
 *          returned to the pool
 */
Connection.prototype.close = function(error)
{
    this.done(error);

    delete this.client;
    delete this.done;
}; // end Connection#close

Connection.prototype.getSettings = function()
{
    var self = this;
    this.settings = {};
    this.settingDescriptions = {};
    var queryDef = {text: "SHOW ALL"};

    return this._query(queryDef, function(row)
    {
        self.settings[row.name] = row.setting;
        self.settingDescriptions[row.name] = row.description;
    })
    .then(function()
    {
        logger.debug("server settings = %s", logger.dump(self.settings));
        return self.settings;
    });
}; // end Connection#getSettings

Connection.prototype.getTypes = function()
{
    var self = this;
    this.types = {};
    var queryDef = {text: "SELECT t.oid, COALESCE(an.nspname, n.nspname) AS nspname, COALESCE(at.typname, t.typname) AS typname, t.typcategory, n.nspname, t.typname FROM pg_type t LEFT JOIN pg_namespace n ON n.oid = t.typnamespace LEFT JOIN pg_type at ON at.oid = t.typelem LEFT JOIN pg_namespace an ON an.oid = at.typnamespace WHERE t.typtype = 'b' ORDER BY t.oid"};

    return this._query(queryDef, function(row)
    {
        var typeOID = row.oid;
        var typeName = (row.nspname == 'pg_catalog' ? '' : row.nspname + '.') + row.typname +
            (row.typcategory == 'A' ? '[]' : '');

        self.types[typeOID] = typeName;

        switch(typeName)
        {
            case 'public.geometry':
                logger.debug("Setting type parser for data type %s (oid=%s) to that of `bytea`.",
                    logger.dump(typeName), logger.dump(typeOID));

                pgTypes.setTypeParser(typeOID, 'text', pgTypes.getTypeParser(byteaTypeOID));
                break;
        } // end switch
    })
    .then(function()
    {
        return self.types;
    });
}; // end Connection#getTypes

Connection.prototype._query = function(queryDef, onRow)
{
    var self = this;

    onRow = onRow || function(row, result)
    {
        result.addRow(row);
    }; // end default onRow;

    if(!self.client)
    {
        return Promise.reject(new Error("Connection has been closed! Can't run query " + util.inspect(queryDef)));
    } // end if

    return new Promise(function(resolve, reject)
    {
        var query = self.client.query(queryDef)
            .on('error', onError)
            .on('row', onRow)
            .on('end', onEnd);

        function onError(error)
        {
            logger.error("Connection#_query got error: %s", error.stack || error);

            reject(error);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onError

        function onEnd(result)
        {
            resolve(result);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onEnd
    });
}; // end Connection#_query

/**
 * Execute a batch of queries on this PostgreSQL connection.
 *
 * @param {Connection#BatchDef} batchDef - the definition of the batch of queries to run
 *
 * @returns {Promise.<Connection#BatchResults>} - a promise for the results object of each query once all queries are
 *          finished, with some extra information attached
 */
Connection.prototype.query = function(batchDef)
{
    var self = this;

    var queryID = batchDef.queryID;
    var resultSetNum = -1;

    var queries = batchDef.queries.slice();
    function runNext()
    {
        return doQuery(queries.shift())
            .then(function(result)
            {
                if(queries.length > 0)
                {
                    return runNext()
                        .then(function(nextResults)
                        {
                            return [result].concat(nextResults);
                        });
                }
                else
                {
                    return [result];
                } // end if
            });
    } // end runNext

    var batchStartTime = process.hrtime();
    if(batchDef.rollback !== undefined)
    {
        // Run the queries in the batch inside a transaction.
        return self.beginTransaction()
            .then(function()
            {
                return runNext()
                    .tap(function()
                    {
                        return (batchDef.rollback ? self.rollback() : self.commit());
                    })
                    .catch(function(error)
                    {
                        return self.rollback()
                            .finally(function()
                            {
                                throw error;
                            });
                    });
            }) // end .then callback
            .then(buildBatchResults(batchDef.rollback ? "rolled back" : "committed"));
    }
    else
    {
        // Run the queries _without_ a trasaction. (on error, the results of some queries in the batch may still have
        // been committed!)
        return runNext()
            .then(buildBatchResults("committed (no transaction)"));
    } // end if

    // Build the Connection#BatchResults object for a successful response.
    function buildBatchResults(status)
    {
        return function(queryResults)
        {
            var totalTime = process.hrtime(batchStartTime);
            var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

            return {
                status: status,
                queryResults: queryResults,
                totalTimeMS: totalTimeMS
            };
        };
    } // end buildBatchResults

    // Run an individual query from this batch.
    function doQuery(queryDef)
    {
        queryDef.rowMode = "array";

        return new Promise(function(resolve, reject)
        {
            resultSetNum += 1;

            logger.info("Running query %j of batch %j...", resultSetNum, queryID);
            logger.trace("Query %j#%j: %s", queryID, resultSetNum, logger.dump(queryDef));

            var startTime = process.hrtime();
            var query = self.client.query(queryDef)
                .on('error', onError)
                .on('row', onRow)
                .on('end', onEnd);

            function onError(error)
            {
                logger.error("Query %j got error: %s", queryID, error.stack || error);

                reject(error);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onError

            var fieldsEmitted = false;
            function emitFields(fields)
            {
                if(!fieldsEmitted && fields)
                {
                    logger.info("Emitting `fields` event with queryID = %s, resultSetNum = %s",
                        logger.dump(queryID), logger.dump(resultSetNum));

                    self.emit('fields', queryID, resultSetNum, fields.map(function(field)
                    {
                        field.dataType = self.types[field.dataTypeID];
                        return field;
                    }));

                    fieldsEmitted = true;
                } // end if
            } // end emitFields

            function onRow(row, result)
            {
                emitFields(result.fields);

                result.returnedRowCount = (result.returnedRowCount || 0) + 1;

                self.emit('row', queryID, resultSetNum, row);
            } // end onRow

            function onEnd(result)
            {
                var totalTime = process.hrtime(startTime);
                var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

                result.returnedRowCount = result.returnedRowCount || 0;

                logger.info("Query %j finished. (%s row%s returned, %s row%s affected)", queryID,
                    result.returnedRowCount, result.returnedRowCount == 1 ? '' : 's',
                    result.rowCount, result.rowCount == 1 ? '' : 's'
                );

                emitFields(result.fields);

                var response = _.assign(
                    {
                        rowCount: result.returnedRowCount,
                        affectedRowCount: result.rowCount,
                        totalTimeMS: totalTimeMS
                    },
                    _.omit(result, 'returnedRowCount', 'rowCount', 'rows')
                );
                logger.trace("response: %s", logger.dump(response));

                resolve(response);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onEnd
        }); // end Promise callback
    } // end doQuery
}; // end Connection#query

/**
 * Begin a transaction.
 *
 * @returns {Promise} - a promise for when the transaction has started
 */
Connection.prototype.beginTransaction = function()
{
    var self = this;

    return self._query('BEGIN TRANSACTION', function() {})
        .catch(function(error)
        {
            return self.rollback()
                .finally(function()
                {
                    throw error;
                });
        });
}; // end Connection#beginTransaction

/**
 * Roll back the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been rolled back
 */
Connection.prototype.rollback = function()
{
    var self = this;

    return self._query('ROLLBACK', function() {})
        .catch(function(error)
        {
            // Quoting the node-postgres wiki:
            // "if there was a problem rolling back the query something is seriously messed up."

            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#rollback

/**
 * Commit the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been committed
 */
Connection.prototype.commit = function()
{
    var self = this;

    return self._query('COMMIT', function() {})
        .catch(function(error)
        {
            // Errors committing a transaction are also a sign that something is pretty messed up.
            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#commit

// --------------------------------------------------------------------------------------------------------------------

module.exports = {
    connect: connect,
    Connection: Connection
};
<commit_msg>Skip falsy queries.<commit_after>/* jshint node:true */
"use strict";

var events = require('events');
var util = require('util');

var _ = require('lodash');
var logging = require('omega-logger');
var pg = require('pg');
var pgTypes = require('pg').types;
var Promise = require('bluebird');

// --------------------------------------------------------------------------------------------------------------------

var logger = logging.loggerFor(module);

// Type OIDs for built-in types we need to know about.
var byteaTypeOID = 17;

// --------------------------------------------------------------------------------------------------------------------

/**
 * Connect to the given database.
 *
 * @todo Document parameters.
 */
function connect(connectionInfo)
{
    return new Promise(function(resolve, reject)
    {
        // Set application_name on the connection.
        connectionInfo.application_name = connectionInfo.application_name || 'web-pgq';

        pg.connect(connectionInfo, function(error, client, done)
        {
            if(error)
            {
                logger.error("Error connecting to %j: %s", connectionInfo, error.stack || error);

                return reject(error);
            } // end if

            logger.info("Connected to %j", connectionInfo);

            var conn = new Connection(client, done);

            resolve(
                conn.getSettings()
                    .then(conn.getTypes.bind(conn))
                    .return(conn)
            );
        }); // end pg.connect callback
    }); // end Promise callback
} // end connect

// --------------------------------------------------------------------------------------------------------------------

/**
 * Wraps a PostgreSQL connection.
 *
 * @param {pg.Client} client - the `node-postgres` client
 * @param {function} done - the function to release the client to the `node-postgres` pool
 */
function Connection(client, done)
{
    this.client = client;
    this.done = done;
} // end Connection

util.inherits(Connection, events.EventEmitter);

/**
 * The definition of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {*} queryID - the ID of the query batch (an arbitrary value provided by the client)
 * @property {Array.<Connection#QueryDef>} queries - the collection of queries to run
 * @property {boolean} [rollback] - if not undefined, run the queries in a transaction; if true, roll it back instead
 *          of committing when all finish successfully
 */

/**
 * The definition of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryDef
 * @see {@link https://github.com/brianc/node-postgres/wiki/Prepared-Statements}
 *
 * @property {string} [name] - if supplied, creates or uses a prepared statement with the given name
 * @property {string} [text] - the text of the SQL query to run or prepare (required unless using a prepared statement)
 * @property {Array} [values] - the values of the query parameters
 */

/**
 * The results of a PostgreSQL query.
 *
 * @typedef {object} Connection#QueryResults
 *
 * @property {number} rowCount - the number of rows in the result set
 * @property {number} affectedRowCount - the number of affected rows reported by the database
 * @property {number} totalTimeMS - the total time the query took to run, in milliseconds
 */

/**
 * The results of a batch of PostgreSQL queries.
 *
 * @typedef {object} Connection#BatchResults
 *
 * @property {string} status - the final status of the batch; one of: `"committed"`, `"committed (no transaction)"`, or
 *          `"rolled back"`
 * @property {number} totalTimeMS - the total time the batch took to run, in milliseconds
 * @property {Array.<Connection#QueryResults>} queryResults - the results object of each query, with some extra
 *          information attached
 */

/**
 * A row has been generated by a database query.
 *
 * @event Connection#row
 *
 * @param {*} queryID - the ID of the query that generated this row
 * @param {object} row - the row
 */

/**
 * An error occurred on this connection.
 *
 * @event Connection#error
 *
 * @param {object} error - the error that occurred
 */

Object.defineProperty(Connection.prototype, 'remoteAddress', {
    enumerable: true,
    get: function()
    {
        return this.client.connection.stream.remoteAddress;
    } // end get
}); // end '

/**
 * "Close" this connection.
 *
 * Actually, this releases the underlying client back to the to the `node-postgres` pool, unless `error` is passed.
 *
 * @param {object} error - a fatal connection error that occurred; forces the connection to be closed instead of being
 *          returned to the pool
 */
Connection.prototype.close = function(error)
{
    this.done(error);

    delete this.client;
    delete this.done;
}; // end Connection#close

Connection.prototype.getSettings = function()
{
    var self = this;
    this.settings = {};
    this.settingDescriptions = {};
    var queryDef = {text: "SHOW ALL"};

    return this._query(queryDef, function(row)
    {
        self.settings[row.name] = row.setting;
        self.settingDescriptions[row.name] = row.description;
    })
    .then(function()
    {
        logger.debug("server settings = %s", logger.dump(self.settings));
        return self.settings;
    });
}; // end Connection#getSettings

Connection.prototype.getTypes = function()
{
    var self = this;
    this.types = {};
    var queryDef = {text: "SELECT t.oid, COALESCE(an.nspname, n.nspname) AS nspname, COALESCE(at.typname, t.typname) AS typname, t.typcategory, n.nspname, t.typname FROM pg_type t LEFT JOIN pg_namespace n ON n.oid = t.typnamespace LEFT JOIN pg_type at ON at.oid = t.typelem LEFT JOIN pg_namespace an ON an.oid = at.typnamespace WHERE t.typtype = 'b' ORDER BY t.oid"};

    return this._query(queryDef, function(row)
    {
        var typeOID = row.oid;
        var typeName = (row.nspname == 'pg_catalog' ? '' : row.nspname + '.') + row.typname +
            (row.typcategory == 'A' ? '[]' : '');

        self.types[typeOID] = typeName;

        switch(typeName)
        {
            case 'public.geometry':
                logger.debug("Setting type parser for data type %s (oid=%s) to that of `bytea`.",
                    logger.dump(typeName), logger.dump(typeOID));

                pgTypes.setTypeParser(typeOID, 'text', pgTypes.getTypeParser(byteaTypeOID));
                break;
        } // end switch
    })
    .then(function()
    {
        return self.types;
    });
}; // end Connection#getTypes

Connection.prototype._query = function(queryDef, onRow)
{
    var self = this;

    onRow = onRow || function(row, result)
    {
        result.addRow(row);
    }; // end default onRow;

    if(!self.client)
    {
        return Promise.reject(new Error("Connection has been closed! Can't run query " + util.inspect(queryDef)));
    } // end if

    return new Promise(function(resolve, reject)
    {
        var query = self.client.query(queryDef)
            .on('error', onError)
            .on('row', onRow)
            .on('end', onEnd);

        function onError(error)
        {
            logger.error("Connection#_query got error: %s", error.stack || error);

            reject(error);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onError

        function onEnd(result)
        {
            resolve(result);

            query
                .removeListener('error', onError)
                .removeListener('row', onRow)
                .removeListener('end', onEnd);
        } // end onEnd
    });
}; // end Connection#_query

/**
 * Execute a batch of queries on this PostgreSQL connection.
 *
 * @param {Connection#BatchDef} batchDef - the definition of the batch of queries to run
 *
 * @returns {Promise.<Connection#BatchResults>} - a promise for the results object of each query once all queries are
 *          finished, with some extra information attached
 */
Connection.prototype.query = function(batchDef)
{
    var self = this;

    var queryID = batchDef.queryID;
    var resultSetNum = -1;

    var queries = batchDef.queries.slice();
    function runNext()
    {
        var query;
        while(!query && queries.length > 0)
        {
            query = queries.shift();
        } // end while

        if(!query)
        {
            return Promise.resolve([]);
        } // end if

        return doQuery(query)
            .then(function(result)
            {
                if(queries.length > 0)
                {
                    return runNext()
                        .then(function(nextResults)
                        {
                            return [result].concat(nextResults);
                        });
                }
                else
                {
                    return [result];
                } // end if
            });
    } // end runNext

    var batchStartTime = process.hrtime();
    if(batchDef.rollback !== undefined)
    {
        // Run the queries in the batch inside a transaction.
        return self.beginTransaction()
            .then(function()
            {
                return runNext()
                    .tap(function()
                    {
                        return (batchDef.rollback ? self.rollback() : self.commit());
                    })
                    .catch(function(error)
                    {
                        return self.rollback()
                            .finally(function()
                            {
                                throw error;
                            });
                    });
            }) // end .then callback
            .then(buildBatchResults(batchDef.rollback ? "rolled back" : "committed"));
    }
    else
    {
        // Run the queries _without_ a trasaction. (on error, the results of some queries in the batch may still have
        // been committed!)
        return runNext()
            .then(buildBatchResults("committed (no transaction)"));
    } // end if

    // Build the Connection#BatchResults object for a successful response.
    function buildBatchResults(status)
    {
        return function(queryResults)
        {
            var totalTime = process.hrtime(batchStartTime);
            var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

            return {
                status: status,
                queryResults: queryResults,
                totalTimeMS: totalTimeMS
            };
        };
    } // end buildBatchResults

    // Run an individual query from this batch.
    function doQuery(queryDef)
    {
        queryDef.rowMode = "array";

        return new Promise(function(resolve, reject)
        {
            resultSetNum += 1;

            logger.info("Running query %j of batch %j...", resultSetNum, queryID);
            logger.trace("Query %j#%j: %s", queryID, resultSetNum, logger.dump(queryDef));

            var startTime = process.hrtime();
            var query = self.client.query(queryDef)
                .on('error', onError)
                .on('row', onRow)
                .on('end', onEnd);

            function onError(error)
            {
                logger.error("Query %j got error: %s", queryID, error.stack || error);

                reject(error);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onError

            var fieldsEmitted = false;
            function emitFields(fields)
            {
                if(!fieldsEmitted && fields)
                {
                    logger.info("Emitting `fields` event with queryID = %s, resultSetNum = %s",
                        logger.dump(queryID), logger.dump(resultSetNum));

                    self.emit('fields', queryID, resultSetNum, fields.map(function(field)
                    {
                        field.dataType = self.types[field.dataTypeID];
                        return field;
                    }));

                    fieldsEmitted = true;
                } // end if
            } // end emitFields

            function onRow(row, result)
            {
                emitFields(result.fields);

                result.returnedRowCount = (result.returnedRowCount || 0) + 1;

                self.emit('row', queryID, resultSetNum, row);
            } // end onRow

            function onEnd(result)
            {
                var totalTime = process.hrtime(startTime);
                var totalTimeMS = totalTime[0] * 1e3 + totalTime[1] / 1e6;

                result.returnedRowCount = result.returnedRowCount || 0;

                logger.info("Query %j finished. (%s row%s returned, %s row%s affected)", queryID,
                    result.returnedRowCount, result.returnedRowCount == 1 ? '' : 's',
                    result.rowCount, result.rowCount == 1 ? '' : 's'
                );

                emitFields(result.fields);

                var response = _.assign(
                    {
                        rowCount: result.returnedRowCount,
                        affectedRowCount: result.rowCount,
                        totalTimeMS: totalTimeMS
                    },
                    _.omit(result, 'returnedRowCount', 'rowCount', 'rows')
                );
                logger.trace("response: %s", logger.dump(response));

                resolve(response);

                query
                    .removeListener('error', onError)
                    .removeListener('row', onRow)
                    .removeListener('end', onEnd);
            } // end onEnd
        }); // end Promise callback
    } // end doQuery
}; // end Connection#query

/**
 * Begin a transaction.
 *
 * @returns {Promise} - a promise for when the transaction has started
 */
Connection.prototype.beginTransaction = function()
{
    var self = this;

    return self._query('BEGIN TRANSACTION', function() {})
        .catch(function(error)
        {
            return self.rollback()
                .finally(function()
                {
                    throw error;
                });
        });
}; // end Connection#beginTransaction

/**
 * Roll back the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been rolled back
 */
Connection.prototype.rollback = function()
{
    var self = this;

    return self._query('ROLLBACK', function() {})
        .catch(function(error)
        {
            // Quoting the node-postgres wiki:
            // "if there was a problem rolling back the query something is seriously messed up."

            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#rollback

/**
 * Commit the current transaction.
 *
 * @returns {Promise} - a promise for when the transaction has been committed
 */
Connection.prototype.commit = function()
{
    var self = this;

    return self._query('COMMIT', function() {})
        .catch(function(error)
        {
            // Errors committing a transaction are also a sign that something is pretty messed up.
            // Since the connection is most likely in a bad state, close it instead of returning it to the pool.
            self.close(error);

            throw error;
        });
}; // end Connection#commit

// --------------------------------------------------------------------------------------------------------------------

module.exports = {
    connect: connect,
    Connection: Connection
};
<|endoftext|>


<commit_before>var _ = require('lodash');
var mongojs = require('mongojs');
var inflect = require('i')();
var tinySSE = require('tiny-sse');
var Promise = require('bluebird');
var hl = require('highland');
var JSONAPI_Error = require('./jsonapi-error');

/*
Usage:
======================================
When setting up Multi SSE (ie: SSE for multiple resources), you just need to pass in Harvester context as such:

this.multiSSE = new SSE();
this.multiSSE.init({
    context: harvesterApp
});

You can then point an EventReader to listen from "{base_url}/changes/stream?resouces=foo,bar,baz".

When setting up SSE for a single route, you will need to pass the resource name:

this.singleSSE = new SSE();
this.singleSSE.init({
    context: harvesterApp,
    singleResourceName: 'foo'
});

You can then point an EventReader to listen from "{base_url}/foo/changes/stream".

Verbs:
======================================
You can also pass a "verbs" option to this module. If none is passed, SSE will only listen to "insert" events from uplog.
Values you can pass are "post", "put" and "delete" which in turn currespond to oplog ops "i", "u" and "d".

this.singleSSE = new SSE();
this.singleSSE.init({
    context: harvesterApp,
    singleResourceName: 'foo',
    verbs: ['post', 'put', 'delete']
});
*/

var SSE = function() {
};

SSE.prototype.init = function(config) {
    this.config = config;
    this.options = config.context.options;
    this.harvesterApp = config.context;

    //only listen to post events if the verb is not specified
    this.verbs = config.verbs || ['post'];

    //wraps it up in an array of single item, so that it fits the current logic without too many conditions
    this.singleResourceName = config.singleResourceName && [config.singleResourceName];
    this.db = mongojs(this.options.oplogConnectionString);
    this.coll = this.db.collection('oplog.rs');

    var routePrefix =  '';

    if (config.singleResourceName) {
        var pluralName = (this.options.inflect) ? inflect.pluralize(config.singleResourceName) : config.singleResourceName;

        routePrefix = '/' + pluralName;
    }

    this.harvesterApp.router.get(this.options.baseUrl + routePrefix + '/changes/stream', this.requestValidationMiddleware.bind(this), tinySSE.head(), tinySSE.ticker({seconds: 3}), this.handler.bind(this));
};

SSE.prototype.requestValidationMiddleware = function (req, res, next) {
    routeNames = req.query.resources ? req.query.resources.split(',') : [];

    if (this.singleResourceName) {
        routeNames = this.singleResourceName;
    }

    if (routeNames.length === 0) {
        throw new JSONAPI_Error({
            status: 400,
            title: 'Requested changes on missing resource',
            detail: 'You have not specified any resources, please do so by providing "resource?foo,bar" as query'
        });
    }

    if (!this.allResourcesExist(routeNames)) {
        throw new JSONAPI_Error({
            status: 400,
            title: 'Requested changes on missing resource',
            detail: 'The follow resources don\'t exist ' +  this.getMissingResources(routeNames).join(',')
        });
    }

    if (req.headers['last-event-id']) {
        var tsSplit = _.map(req.headers['last-event-id'].split('_'), function (item) {
            return parseInt(item, 10);
        });

        isValidTS = _.all(tsSplit, function(ts) {
            return !isNaN(ts);
        });

        if(!isValidTS) {
            throw new JSONAPI_Error({
                status: 400,
                title: 'Invalid Timestamp',
                detail: 'Could not parse the time stamp provided'
            });
        }
    }

    next();
};

SSE.prototype.handler = function (req, res, next) {

    var that = this;

    routeNames = req.query.resources ? req.query.resources.split(',') : [];

    if (this.singleResourceName) {
        routeNames = this.singleResourceName;
    }

    var regex = new RegExp('.*\\.(' + routeNames.join('|') + ')', 'i');
    var docStream = hl();

    this.getQuery(req, regex)
    .then(function(query) {
        var options = {
            tailable: true,
            awaitData: true
        };

        var stream = that.coll.find(query, options);

        var docStream = hl(stream);

        docStream.resume();

        var consume = hl().consume(function(err, chunk, push, next) {
            var resourceNames = _.map(routeNames, function(routeName) {
                var pluralName = (that.options.inflect) ? inflect.pluralize(routeName) : routeName;
                return new RegExp(pluralName, 'i');
            });

            var matchesEitherResource = _.some(resourceNames, function(resourceName) {
                return resourceName.test(chunk.ns);
            });

            if (matchesEitherResource) {
                var id = chunk.ts.getHighBits() + '_' + chunk.ts.getLowBits();
                var eventName = that.getEventName(routeNames, chunk);
                var data = that.getData(routeNames[0], chunk);

                 var filters = that.getFilters(req);

                 var passedFilter = _.reduce(filters, function(obj, filter) {
                     return _.filter([data], _.matches(filter));
                 }, true);

                //if we have filters, make sure they are passed
                if (passedFilter.length > 0 || filters.length === 0) {
                    tinySSE.send({id: id, event: eventName, data: data})(req, res);
                }

                next();
            }
        });

        docStream.through(consume).errors(function(err) {
            that.handleError(err, res, docStream);
        });
    })
    .catch(function(err) {
        that.handleError(err, res, docStream);
    });
};

SSE.prototype.handleError = function(err, res, docStream) {
    res.end();
    if(docStream) {
        docStream.destroy();
    }
};

SSE.prototype.allResourcesExist = function(resourceNames) {
    return this.getMissingResources(resourceNames).length === 0;
};

SSE.prototype.getMissingResources = function(resourceNames) {

    var harvesterResourceNames = this.resourceName || _.keys(this.harvesterApp.createdResources);

    return _.difference(resourceNames, harvesterResourceNames);
}

SSE.prototype.getQuery = function(req, ns) {
    var lastEventId = req.headers['last-event-id'];
    var coll = this.db.collection('oplog.rs');

    var verbs = this.verbs.map(function(verb) {
        return {
            'post' : 'i',
            'put' : 'u',
            'delete' : 'd'
        }[verb]
    });

    var query = {
        ns : ns,
        op : new RegExp('(' + verbs.join('|') + ')', 'i')
    };

    return new Promise(function(resolve, reject) {
        if (req.headers['last-event-id']) {
            var tsSplit = _.map(lastEventId.split('_'), function (item) {
                return parseInt(item, 10);
            });

            query.ts = {
                $gt: new mongojs.Timestamp(tsSplit[1], tsSplit[0])
            };

            return resolve(query);
        }

        coll.find(query).sort({$natural : -1}).limit(1, function(err, items) {
            if (err) return reject(err);

            if(items.length === 0) {
                return coll.find().sort({$natural : -1}).limit(1, function(err, items) {
                    if (err) return reject(err);

                    query.ts = {
                        $gt: items[0].ts
                    };

                    return resolve(query);
                });
            }

            query.ts = {
                $gt: items[0].ts
            };

            return resolve(query);
        });
    });
};

SSE.prototype.getFilters = function(req) {

    var filters = _.chain(req.query)
    .map(function(item, key) {
        if (!_.contains(['limit', 'sort', 'offset', 'resources'], key)) {
            var filter = {};
            filter[key] = item;
            return filter;
        }
    })
    .filter(function(item) {
        return !!item;
    })
    //converts {'foo.bar' : 'foobar'} to {foo : { bar : 'foobar' }}
    .map(function(item) {
        var keys = _.keys(item)[0].split('.');
        return _.reduce(keys, function(obj, key, index) {

            var value = (index === keys.length - 1 || keys.length === 1) ? _.values(item)[0] : {};

            if (index === 0) {
                obj[key] = (keys.length > 1) ? {} : value;
            } else {
                obj[keys[index - 1]][key] = value;
            }
            return obj;
        }, {});
    })
    .value();

    return filters;

};

SSE.prototype.getData = function(routeName, chunk) {
    var data;
    var model = this.harvesterApp.adapter.model(routeName);

    switch (chunk.op) {
        case 'i' : data = this.harvesterApp.adapter._deserialize(model, chunk.o); break;
        case 'u' : data = chunk.o.$set; break;
        default : data = {}
    }

    return data;
};

SSE.prototype.getEventName = function(routeNames, chunk) {
    //mapping breaking MutualMobile's code
    //var nameMap = {
    //    'i': 'insert',
    //    'u': 'update',
    //    'd' : 'delete'
    //};

    return routeNames[0] + '_' + chunk.op;
};

module.exports = SSE;
<commit_msg>Pluralizes event name<commit_after>var _ = require('lodash');
var mongojs = require('mongojs');
var inflect = require('i')();
var tinySSE = require('tiny-sse');
var Promise = require('bluebird');
var hl = require('highland');
var JSONAPI_Error = require('./jsonapi-error');

/*
Usage:
======================================
When setting up Multi SSE (ie: SSE for multiple resources), you just need to pass in Harvester context as such:

this.multiSSE = new SSE();
this.multiSSE.init({
    context: harvesterApp
});

You can then point an EventReader to listen from "{base_url}/changes/stream?resouces=foo,bar,baz".

When setting up SSE for a single route, you will need to pass the resource name:

this.singleSSE = new SSE();
this.singleSSE.init({
    context: harvesterApp,
    singleResourceName: 'foo'
});

You can then point an EventReader to listen from "{base_url}/foo/changes/stream".

Verbs:
======================================
You can also pass a "verbs" option to this module. If none is passed, SSE will only listen to "insert" events from uplog.
Values you can pass are "post", "put" and "delete" which in turn currespond to oplog ops "i", "u" and "d".

this.singleSSE = new SSE();
this.singleSSE.init({
    context: harvesterApp,
    singleResourceName: 'foo',
    verbs: ['post', 'put', 'delete']
});
*/

var SSE = function() {
};

SSE.prototype.init = function(config) {
    this.config = config;
    this.options = config.context.options;
    this.harvesterApp = config.context;

    //only listen to post events if the verb is not specified
    this.verbs = config.verbs || ['post'];

    //wraps it up in an array of single item, so that it fits the current logic without too many conditions
    this.singleResourceName = config.singleResourceName && [config.singleResourceName];
    this.db = mongojs(this.options.oplogConnectionString);
    this.coll = this.db.collection('oplog.rs');

    var routePrefix =  '';

    if (config.singleResourceName) {
        var pluralName = (this.options.inflect) ? inflect.pluralize(config.singleResourceName) : config.singleResourceName;

        routePrefix = '/' + pluralName;
    }

    this.harvesterApp.router.get(this.options.baseUrl + routePrefix + '/changes/stream', this.requestValidationMiddleware.bind(this), tinySSE.head(), tinySSE.ticker({seconds: 3}), this.handler.bind(this));
};

SSE.prototype.requestValidationMiddleware = function (req, res, next) {
    routeNames = req.query.resources ? req.query.resources.split(',') : [];

    if (this.singleResourceName) {
        routeNames = this.singleResourceName;
    }

    if (routeNames.length === 0) {
        throw new JSONAPI_Error({
            status: 400,
            title: 'Requested changes on missing resource',
            detail: 'You have not specified any resources, please do so by providing "resource?foo,bar" as query'
        });
    }

    if (!this.allResourcesExist(routeNames)) {
        throw new JSONAPI_Error({
            status: 400,
            title: 'Requested changes on missing resource',
            detail: 'The follow resources don\'t exist ' +  this.getMissingResources(routeNames).join(',')
        });
    }

    if (req.headers['last-event-id']) {
        var tsSplit = _.map(req.headers['last-event-id'].split('_'), function (item) {
            return parseInt(item, 10);
        });

        isValidTS = _.all(tsSplit, function(ts) {
            return !isNaN(ts);
        });

        if(!isValidTS) {
            throw new JSONAPI_Error({
                status: 400,
                title: 'Invalid Timestamp',
                detail: 'Could not parse the time stamp provided'
            });
        }
    }

    next();
};

SSE.prototype.handler = function (req, res, next) {

    var that = this;

    routeNames = req.query.resources ? req.query.resources.split(',') : [];

    if (this.singleResourceName) {
        routeNames = this.singleResourceName;
    }

    var regex = new RegExp('.*\\.(' + routeNames.join('|') + ')', 'i');
    var docStream = hl();

    this.getQuery(req, regex)
    .then(function(query) {
        var options = {
            tailable: true,
            awaitData: true
        };

        var stream = that.coll.find(query, options);

        var docStream = hl(stream);

        docStream.resume();

        var consume = hl().consume(function(err, chunk, push, next) {
            var resourceNames = _.map(routeNames, function(routeName) {
                var pluralName = (that.options.inflect) ? inflect.pluralize(routeName) : routeName;
                return new RegExp(pluralName, 'i');
            });

            var matchesEitherResource = _.some(resourceNames, function(resourceName) {
                return resourceName.test(chunk.ns);
            });

            if (matchesEitherResource) {
                var id = chunk.ts.getHighBits() + '_' + chunk.ts.getLowBits();
                var eventName = that.getEventName(routeNames, chunk);
                var data = that.getData(routeNames[0], chunk);

                 var filters = that.getFilters(req);

                 var passedFilter = _.reduce(filters, function(obj, filter) {
                     return _.filter([data], _.matches(filter));
                 }, true);

                //if we have filters, make sure they are passed
                if (passedFilter.length > 0 || filters.length === 0) {
                    tinySSE.send({id: id, event: eventName, data: data})(req, res);
                }

                next();
            }
        });

        docStream.through(consume).errors(function(err) {
            that.handleError(err, res, docStream);
        });
    })
    .catch(function(err) {
        that.handleError(err, res, docStream);
    });
};

SSE.prototype.handleError = function(err, res, docStream) {
    res.end();
    if(docStream) {
        docStream.destroy();
    }
};

SSE.prototype.allResourcesExist = function(resourceNames) {
    return this.getMissingResources(resourceNames).length === 0;
};

SSE.prototype.getMissingResources = function(resourceNames) {

    var harvesterResourceNames = this.resourceName || _.keys(this.harvesterApp.createdResources);

    return _.difference(resourceNames, harvesterResourceNames);
}

SSE.prototype.getQuery = function(req, ns) {
    var lastEventId = req.headers['last-event-id'];
    var coll = this.db.collection('oplog.rs');

    var verbs = this.verbs.map(function(verb) {
        return {
            'post' : 'i',
            'put' : 'u',
            'delete' : 'd'
        }[verb]
    });

    var query = {
        ns : ns,
        op : new RegExp('(' + verbs.join('|') + ')', 'i')
    };

    return new Promise(function(resolve, reject) {
        if (req.headers['last-event-id']) {
            var tsSplit = _.map(lastEventId.split('_'), function (item) {
                return parseInt(item, 10);
            });

            query.ts = {
                $gt: new mongojs.Timestamp(tsSplit[1], tsSplit[0])
            };

            return resolve(query);
        }

        coll.find(query).sort({$natural : -1}).limit(1, function(err, items) {
            if (err) return reject(err);

            if(items.length === 0) {
                return coll.find().sort({$natural : -1}).limit(1, function(err, items) {
                    if (err) return reject(err);

                    query.ts = {
                        $gt: items[0].ts
                    };

                    return resolve(query);
                });
            }

            query.ts = {
                $gt: items[0].ts
            };

            return resolve(query);
        });
    });
};

SSE.prototype.getFilters = function(req) {

    var filters = _.chain(req.query)
    .map(function(item, key) {
        if (!_.contains(['limit', 'sort', 'offset', 'resources'], key)) {
            var filter = {};
            filter[key] = item;
            return filter;
        }
    })
    .filter(function(item) {
        return !!item;
    })
    //converts {'foo.bar' : 'foobar'} to {foo : { bar : 'foobar' }}
    .map(function(item) {
        var keys = _.keys(item)[0].split('.');
        return _.reduce(keys, function(obj, key, index) {

            var value = (index === keys.length - 1 || keys.length === 1) ? _.values(item)[0] : {};

            if (index === 0) {
                obj[key] = (keys.length > 1) ? {} : value;
            } else {
                obj[keys[index - 1]][key] = value;
            }
            return obj;
        }, {});
    })
    .value();

    return filters;

};

SSE.prototype.getData = function(routeName, chunk) {
    var data;
    var model = this.harvesterApp.adapter.model(routeName);

    switch (chunk.op) {
        case 'i' : data = this.harvesterApp.adapter._deserialize(model, chunk.o); break;
        case 'u' : data = chunk.o.$set; break;
        default : data = {}
    }

    return data;
};

SSE.prototype.getEventName = function(routeNames, chunk) {
    //mapping breaking MutualMobile's code
    //var nameMap = {
    //    'i': 'insert',
    //    'u': 'update',
    //    'd' : 'delete'
    //};

    return inflect.pluralize(routeNames[0]) + '_' + chunk.op;
};

module.exports = SSE;
<|endoftext|>


<commit_before>'use strict'
const util = require('util')
const EventEmitter = require('events')
const Ssh2Client = require('ssh2').Client

function MyEmitter () {
  EventEmitter.call(this)
}
util.inherits(MyEmitter, EventEmitter)

function nextConfig (dest) {
  let next = dest.shift()

  // add $USER if necessary
  if (!/@/.test(next)) {
    next = process.env.USER + '@' + next
  }

  let p = next.split(/@|:/)

  let cfg = {
    username: p[0],
    host: p[1],
    port: parseInt(p[2]) || 22,
    agent: process.env.SSH_AUTH_SOCK,
    keepaliveInterval: 60000,
    agentForward: true
  }

  if (dest[0]) {
    // there is at last one more destination
    // therefore our next must be a hop
    let p = dest[0].split(/@|:/)
    cfg.cmd = util.format('nc %s %d', p[1], p[2] || 22)
  }

  return cfg
}

/*
 * @param {Array} arg
 *  list of targets (user@host:port)
 */
module.exports = function (arg) {
  const emitter = new MyEmitter()
  let dest = arg.slice()
  let firstConnection

  process.nextTick(nextConnection)

  return emitter

  function nextConnection (stream) {
    const conn = new Ssh2Client()
    if (!firstConnection) {
      firstConnection = conn
    }

    const cfg = nextConfig(dest)
    if (!(cfg.username && cfg.host && cfg.port)) {
      return emitter.emit('error', 'invalid destination')
    }

    if (cfg.cmd) {
      //
      // hop
      //
      emitter.emit('info', 'ssh next hop:\n' + util.inspect(cfg, {colors: true}))

      conn.on('ready', function () {
        conn.exec(cfg.cmd, function (err, stream) {
          if (err) {
            emitter.emit('error', err)
            firstConnection.end()
            return
          }
          nextConnection(stream)
        })
      })

      conn.on('error', function (err) {
        emitter.emit('error', err)
        firstConnection.end()
      })

      conn.connect(cfg)
    } else {
      //
      // final destination
      //
      emitter.emit('info', 'ssh final:\n' + util.inspect(cfg, {colors: true}))

      cfg.sock = stream

      conn.on('ready', function () {
        conn.shell({term: 'xterm-color'}, function (err, stream) {
          if (err) {
            return emitter.emit('error', err)
          }
          process.stdin.setRawMode(true)
          process.stdin.pipe(stream)
          stream.pipe(process.stdout)

          stream.stderr.pipe(process.stderr)

          stream.on('data', function (data) {
            emitter.emit('data', data.toString())
          })
          stream.on('end', function () {
            process.stdin.setRawMode(false)
            firstConnection.end()
            emitter.emit('end')
          })
        })
      })

      conn.on('error', function (err) {
        emitter.emit('error', err)
        firstConnection.end()
      })

      conn.connect(cfg)
    }
  }
}

<commit_msg>code refactoring<commit_after>'use strict'
const util = require('util')
const EventEmitter = require('events')
const Ssh2Client = require('ssh2').Client

function MyEmitter () {
  EventEmitter.call(this)
}
util.inherits(MyEmitter, EventEmitter)

function nextConfig (dest) {
  let next = dest[0]

  if (!/@/.test(next)) {
    next = process.env.USER + '@' + next
  }

  let p = next.split(/@|:/)

  let cfg = {
    username: p[0],
    host: p[1],
    port: parseInt(p[2]) || 22,
    agent: process.env.SSH_AUTH_SOCK,
    keepaliveInterval: 60000,
    agentForward: true
  }

  // there is at last one more destination
  // next must be a hop
  if (dest[1]) {
    let p = dest[1].split(/@|:/)
    cfg.cmd = util.format('nc %s %d', p[1], p[2] || 22)
  }

  return cfg
}

/*
 * @param {Array} arg
 *  list of targets (user@host:port)
 */
module.exports = function (arg) {
  const emitter = new MyEmitter()
  let dest = arg.slice()
  let firstConnection

  function nextConnection (stream) {
    const cfg = nextConfig(dest)
    dest.shift(1)
    if (!(cfg.username && cfg.host && cfg.port)) {
      return emitter.emit('error', 'invalid destination')
    }
    emitter.emit('info', util.inspect(cfg, {colors: true}))

    const conn = new Ssh2Client()
    if (cfg.cmd) {
      // hop
      conn.on('ready', function () {
        conn.exec(cfg.cmd, function (err, stream) {
          if (err) {
            emitter.emit('error', err)
            firstConnection.end()
            return
          }
          nextConnection(stream)
        })
      })
    } else {
      // final destination
      cfg.sock = stream

      conn.on('ready', function () {
        conn.shell({term: 'xterm-color'}, function (err, stream) {
          if (err) {
            return emitter.emit('error', err)
          }
          process.stdin.setRawMode(true)
          process.stdin.pipe(stream)
          stream.pipe(process.stdout)

          stream.stderr.pipe(process.stderr)

          stream.on('data', function (data) {
            emitter.emit('data', data.toString())
          })
          stream.on('end', function () {
            process.stdin.setRawMode(false)
            firstConnection.end()
            emitter.emit('end')
          })
        })
      })
    }

    conn.on('error', function (err) {
      emitter.emit('error', err)
      firstConnection.end()
    })

    if (!firstConnection) {
      firstConnection = conn
    }
    conn.connect(cfg)
  }

  process.nextTick(nextConnection)
  return emitter
}

<|endoftext|>


<commit_before>/*
 *  player.js
 *  2014/09/05
 *  @auther minimo
 *  This Program is MIT license.
 */

phina.define("pbr.Player", {
    superClass: "phina.display.CanvasElement",
    _member: {
        layer: LAYER_PLAYER,

        //当り判定サイズ
        width: 2,
        height: 2,

        control: true,  //操作可能フラグ
        shotON: true,  //ショットフラグ
        mouseON: false, //マウス操作中フラグ

        isCollision: false, //当り判定有効フラグ

        timeMuteki: 0, //無敵フレーム残り時間

        speed: 4,       //移動係数
        touchSpeed: 4,  //タッチ操作時移動係数
        type: 0,        //自機タイプ(0:赤 1:緑 2:青)
        power: 0,       //パワーアップ段階
        powerMax: 5,    //パワーアップ最大

        shotPower: 10,      //ショット威力
        shotInterval: 6,    //ショット間隔

        rollcount: 50,
        pitchcount: 50,

        parentScene: null,
        indecies: [0,1,2,3,4,4,4,5,6,7,8],
    },

    init: function() {
        this.superInit();
        this.$extend(this._member);

        this.sprite = phina.display.Sprite("gunship", 48, 48)
            .addChildTo(this)
            .setFrameIndex(4);

        //ビット
        this.bits = [];
        this.bits[0] = pbr.PlayerBit(0).addChildTo(this);
        this.bits[1] = pbr.PlayerBit(1).addChildTo(this);
        this.bits[2] = pbr.PlayerBit(2).addChildTo(this);
        this.bits[3] = pbr.PlayerBit(3).addChildTo(this);

        this.openBit(0);

        //当り判定設定
        this.boundingType = "circle";
        this.radius = 2;
        this.checkHierarchy = true;

        this.time = 0;
        this.changeInterval = 0;
        return this;
    },

    update: function(app) {
        if (this.control) {
            //マウス操作
            var p = app.mouse;
            if (p.getPointing()) {
/*
                var pt = this.parentScene.pointer;
                this.x += (pt.x - this.x)/this.touchSpeed;
                this.y += (pt.y - this.y)/this.touchSpeed;
*/
                var pt = p.deltaPosition;
                this.x += pt.x;
                this.y += pt.y;

                this.mouseON = true;
                this.shotON = true;
            } else {
                this.mouseON = false;
                this.shotON = false;
            }

            //キーボード操作
            var kb = app.keyboard;
            var angle = kb.getKeyAngle();
            if (angle !== null) {
                var m = KEYBOARD_MOVE[angle];
                this.x += m.x*this.speed;
                this.y += m.y*this.speed;
            }
            if (!this.mouseON) this.shotON = app.keyboard.getKey("Z");

            //ショットタイプ変更（テスト用）
            if (app.keyboard.getKey("X") && this.time > this.changeInterval) {
                this.type = (this.type+1)%3;
                this.openBit(this.type);
                this.changeInterval = this.time+30;
            }

            //移動範囲の制限
            this.x = Math.clamp(this.x, 16, SC_W-16);
            this.y = Math.clamp(this.y, 16, SC_H-16);

            //ショット投入
            if (this.shotON && app.ticker.frame % this.shotInterval == 0) this.enterShot();
        }

        //機体ロール
        var x = ~~this.x;
        var bx = ~~this.bx;
        if (bx > x) {
            this.rollcount-=2;
            if (this.rollcount < 0) this.rollcount = 0;
        }
        if (bx < x) {
            this.rollcount+=2;
            if (this.rollcount > 100) this.rollcount = 100;
        }
        var vx = Math.abs(bx - x);
        if (vx < 2) {
            if (this.rollcount < 50) this.rollcount+=2; else this.rollcount-=2;
            if (this.rollcount < 0) this.rollcount = 0;
            if (this.rollcount > 100) this.rollcount = 100;
        }
        this.sprite.setFrameIndex(this.indecies[Math.clamp(~~(this.rollcount/10),0, 9)]);

        this.bx = this.x;
        this.by = this.y;
        this.time++;
        this.timeMuteki--;
    },

    //被弾処理
    damage: function() {
    },

    //ショット発射
    enterShot: function() {
        var shotPower = this.shotPower;
        //自機から
        pbr.Shot( 1, shotPower, 0).addChildTo(this.parentScene).setPosition(this.x+10, this.y-8);
        pbr.Shot( 0, shotPower, 0).addChildTo(this.parentScene).setPosition(this.x   , this.y-16);
        pbr.Shot(-1, shotPower, 0).addChildTo(this.parentScene).setPosition(this.x-10, this.y-8);
    },

    //ビット展開
    openBit: function(type) {
        var color = 0;
        switch (type) {
            case 0:
                //赤（前方集中型）
                this.bits[0].tweener.clear().to({ x:  5, y:-32, rotation: 2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy(-40,0,500,"easeInOutSine").moveBy( 40,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[0]));
                this.bits[1].tweener.clear().to({ x: -5, y:-32, rotation:-2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy( 40,0,500,"easeInOutSine").moveBy(-40,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[1]));
                this.bits[2].tweener.clear().to({ x: 20, y:-24, rotation: 2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy(-50,0,500,"easeInOutSine").moveBy( 50,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[2]));
                this.bits[3].tweener.clear().to({ x:-20, y:-24, rotation:-2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy( 50,0,500,"easeInOutSine").moveBy(-50,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[3]));
                color = 0;
                break;
            case 1:
                //緑（方向変更型）
                this.bits[0].tweener.clear().to({ x: 48, y:0, rotation:0, alpha:1}, 300).setLoop(false);
                this.bits[1].tweener.clear().to({ x:-48, y:0, rotation:0, alpha:1}, 300).setLoop(false);
                this.bits[2].tweener.clear().to({ x: 12, y:40, rotation:0, alpha:1}, 300).setLoop(false);
                this.bits[3].tweener.clear().to({ x:-12, y:40, rotation:0, alpha:1}, 300).setLoop(false);
                color = 80;
                break;
            case 2:
                //青（広範囲型）
                this.bits[0].tweener.clear().to({ x: 36, y:16, rotation:  5, alpha:1}, 300).setLoop(false);
                this.bits[1].tweener.clear().to({ x:-36, y:16, rotation: -5, alpha:1}, 300).setLoop(false);
                this.bits[2].tweener.clear().to({ x: 60, y:24, rotation: 10, alpha:1}, 300).setLoop(false);
                this.bits[3].tweener.clear().to({ x:-60, y:24, rotation:-10, alpha:1}, 300).setLoop(false);
                color = 200;
                break;
            default:
                //クローズ
                this.bits[0].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                this.bits[1].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                this.bits[2].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                this.bits[3].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                color = 60;
                break;
        }
    },

    //プレイヤー投入時演出
    startup: function() {
    },

    //ステージ開始時演出
    stageStartup: function() {
    },
});

//プレイヤー操作用ポインタ
phina.define("pbr.PlayerPointer", {
    superClass: "phina.display.Shape",
    layer: LAYER_OBJECT_LOWER,

    init: function() {
        this.superInit({width:32, height:32});
        this.canvas.lineWidth = 3;
        this.canvas.globalCompositeOperation = "lighter";
        this.canvas.strokeStyle = "rgb(255, 255, 255)";
        this.canvas.strokeArc(16, 16, 8, Math.PI*2, 0, true);
    },

    update: function() {
        var p = app.pointing;
        if (app.player.control && p.getPointing()) {
            if (~~(this.x) == ~~(app.player.x) && ~~(this.y) == ~~(app.player.y)) {
                this.alpha = 0;
            } else {
                this.alpha = 0.5;
            }
            this.x += (p.position.x - p.prevPosition.x);
            this.y += (p.position.y - p.prevPosition.y);
            this.x = Math.clamp(this.x, 16, SC_W-16);
            this.y = Math.clamp(this.y, 16, SC_H-16);
        } else {
            this.x = app.player.x;
            this.y = app.player.y;
            this.alpha = 0;
        }
    },
});

<commit_msg>add player startup<commit_after>/*
 *  player.js
 *  2014/09/05
 *  @auther minimo
 *  This Program is MIT license.
 */

phina.define("pbr.Player", {
    superClass: "phina.display.CanvasElement",
    _member: {
        layer: LAYER_PLAYER,

        //当り判定サイズ
        width: 2,
        height: 2,

        control: true,  //操作可能フラグ
        shotON: true,  //ショットフラグ
        mouseON: false, //マウス操作中フラグ

        isCollision: false, //当り判定有効フラグ

        timeMuteki: 0, //無敵フレーム残り時間

        speed: 4,       //移動係数
        touchSpeed: 4,  //タッチ操作時移動係数
        type: 0,        //自機タイプ(0:赤 1:緑 2:青)
        power: 0,       //パワーアップ段階
        powerMax: 5,    //パワーアップ最大

        shotPower: 10,      //ショット威力
        shotInterval: 6,    //ショット間隔

        rollcount: 50,
        pitchcount: 50,

        parentScene: null,
        indecies: [0,1,2,3,4,4,4,5,6,7,8],
    },

    init: function() {
        this.superInit();
        this.$extend(this._member);

        this.sprite = phina.display.Sprite("gunship", 48, 48)
            .addChildTo(this)
            .setFrameIndex(4);

        //ビット
        this.bits = [];
        this.bits[0] = pbr.PlayerBit(0).addChildTo(this);
        this.bits[1] = pbr.PlayerBit(1).addChildTo(this);
        this.bits[2] = pbr.PlayerBit(2).addChildTo(this);
        this.bits[3] = pbr.PlayerBit(3).addChildTo(this);

        this.openBit(0);

        //当り判定設定
        this.boundingType = "circle";
        this.radius = 2;
        this.checkHierarchy = true;

        this.time = 0;
        this.changeInterval = 0;
        return this;
    },

    update: function(app) {
        if (this.control) {
            //マウス操作
            var p = app.mouse;
            if (p.getPointing()) {
/*
                var pt = this.parentScene.pointer;
                this.x += (pt.x - this.x)/this.touchSpeed;
                this.y += (pt.y - this.y)/this.touchSpeed;
*/
                var pt = p.deltaPosition;
                this.x += pt.x;
                this.y += pt.y;

                this.mouseON = true;
                this.shotON = true;
            } else {
                this.mouseON = false;
                this.shotON = false;
            }

            //キーボード操作
            var kb = app.keyboard;
            var angle = kb.getKeyAngle();
            if (angle !== null) {
                var m = KEYBOARD_MOVE[angle];
                this.x += m.x*this.speed;
                this.y += m.y*this.speed;
            }
            if (!this.mouseON) this.shotON = app.keyboard.getKey("Z");

            //ショットタイプ変更（テスト用）
            if (app.keyboard.getKey("X") && this.time > this.changeInterval) {
                this.type = (this.type+1)%3;
                this.openBit(this.type);
                this.changeInterval = this.time+30;
            }

            //移動範囲の制限
            this.x = Math.clamp(this.x, 16, SC_W-16);
            this.y = Math.clamp(this.y, 16, SC_H-16);

            //ショット投入
            if (this.shotON && app.ticker.frame % this.shotInterval == 0) this.enterShot();
        }

        //機体ロール
        var x = ~~this.x;
        var bx = ~~this.bx;
        if (bx > x) {
            this.rollcount-=2;
            if (this.rollcount < 0) this.rollcount = 0;
        }
        if (bx < x) {
            this.rollcount+=2;
            if (this.rollcount > 100) this.rollcount = 100;
        }
        var vx = Math.abs(bx - x);
        if (vx < 2) {
            if (this.rollcount < 50) this.rollcount+=2; else this.rollcount-=2;
            if (this.rollcount < 0) this.rollcount = 0;
            if (this.rollcount > 100) this.rollcount = 100;
        }
        this.sprite.setFrameIndex(this.indecies[Math.clamp(~~(this.rollcount/10),0, 9)]);

        this.bx = this.x;
        this.by = this.y;
        this.time++;
        this.timeMuteki--;
    },

    //被弾処理
    damage: function() {
    },

    //ショット発射
    enterShot: function() {
        var shotPower = this.shotPower;
        //自機から
        pbr.Shot( 1, shotPower, 0).addChildTo(this.parentScene).setPosition(this.x+10, this.y-8);
        pbr.Shot( 0, shotPower, 0).addChildTo(this.parentScene).setPosition(this.x   , this.y-16);
        pbr.Shot(-1, shotPower, 0).addChildTo(this.parentScene).setPosition(this.x-10, this.y-8);
    },

    //ビット展開
    openBit: function(type) {
        var color = 0;
        switch (type) {
            case 0:
                //赤（前方集中型）
                this.bits[0].tweener.clear().to({ x:  5, y:-32, rotation: 2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy(-40,0,500,"easeInOutSine").moveBy( 40,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[0]));
                this.bits[1].tweener.clear().to({ x: -5, y:-32, rotation:-2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy( 40,0,500,"easeInOutSine").moveBy(-40,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[1]));
                this.bits[2].tweener.clear().to({ x: 20, y:-24, rotation: 2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy(-50,0,500,"easeInOutSine").moveBy( 50,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[2]));
                this.bits[3].tweener.clear().to({ x:-20, y:-24, rotation:-2, alpha:1}, 300).call(function(){this.tweener.clear().moveBy( 50,0,500,"easeInOutSine").moveBy(-50,0,500,"easeInOutSine").setLoop(true);}.bind(this.bits[3]));
                color = 0;
                break;
            case 1:
                //緑（方向変更型）
                this.bits[0].tweener.clear().to({ x: 48, y:0, rotation:0, alpha:1}, 300).setLoop(false);
                this.bits[1].tweener.clear().to({ x:-48, y:0, rotation:0, alpha:1}, 300).setLoop(false);
                this.bits[2].tweener.clear().to({ x: 12, y:40, rotation:0, alpha:1}, 300).setLoop(false);
                this.bits[3].tweener.clear().to({ x:-12, y:40, rotation:0, alpha:1}, 300).setLoop(false);
                color = 80;
                break;
            case 2:
                //青（広範囲型）
                this.bits[0].tweener.clear().to({ x: 36, y:16, rotation:  5, alpha:1}, 300).setLoop(false);
                this.bits[1].tweener.clear().to({ x:-36, y:16, rotation: -5, alpha:1}, 300).setLoop(false);
                this.bits[2].tweener.clear().to({ x: 60, y:24, rotation: 10, alpha:1}, 300).setLoop(false);
                this.bits[3].tweener.clear().to({ x:-60, y:24, rotation:-10, alpha:1}, 300).setLoop(false);
                color = 200;
                break;
            default:
                //クローズ
                this.bits[0].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                this.bits[1].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                this.bits[2].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                this.bits[3].tweener.clear().to({ x:0, y: 0, alpha:0}, 300);
                color = 60;
                break;
        }
    },

    //プレイヤー投入時演出
    startup: function() {
        this.x = SC_W/2;
        this.y = SC_H+128;
        this.tweener.clear()
            .wait(2000)
            .to({x: SC_W/2, y: SC_H-128}, 2000, "easeOutQuint")
            .call(function(){
                this.shotON = true;
                this.control = true;
                this.isCollision = true;
                this.timeMuteki = 180;
            }.bind(this));

        this.shotON = false;
        this.control = false;
        this.isCollision = false;

        this.parentScene.timeVanish = 300;
    },

    //ステージ開始時演出
    stageStartup: function() {
        this.x = SC_W/2;
        this.y = SC_H+128;
        this.tweener.clear()
            .to({x: SC_W/2, y: SC_H/2+32}, 1000, "easeOutCubic")
            .to({x: SC_W/2, y: SC_H-64  }, 1000)
            .call(function(){
                this.shotON = true;
                this.control = true;
                this.isCollision = true;
                this.timeMuteki = 180;
            }.bind(this));

        this.shotON = false;
        this.control = false;
        this.isCollision = false;
    },
});
<|endoftext|>


<commit_before>var fs = require('fs')

function keys (opts) {
  if (opts.key) {
    opts.key = fs.readFileSync(opts.key);
  }
  if (opts.certificate) {
    opts.certificate = fs.readFileSync(opts.certificate);
  }
  return opts;
}
module.exports = keys;

<commit_msg>fix ssl config: key "cert"<commit_after>var fs = require('fs')

function keys (opts) {
  if (opts.key) {
    opts.key = fs.readFileSync(opts.key);
  }
  if (opts.certificate) {
    opts.cert = fs.readFileSync(opts.certificate);
  }
  return opts;
}
module.exports = keys;

<|endoftext|>


<commit_before>import videojs from 'video.js';

// Default options for the plugin.
const defaults = {};

/**
 * Function to invoke when the player is ready.
 *
 * This is a great place for your plugin to initialize itself. When this
 * function is called, the player will have its DOM and child components
 * in place.
 *
 * @function onPlayerReady
 * @param    {Player} player
 * @param    {Object} [options={}]
 */
const onPlayerReady = (player, options) => {
  player.addClass('vjs-skippy');
};

/**
 * A video.js plugin.
 *
 * In the plugin function, the value of `this` is a video.js `Player`
 * instance. You cannot rely on the player being in a "ready" state here,
 * depending on how the plugin is invoked. This may or may not be important
 * to you; if not, remove the wait for "ready"!
 *
 * @function skippy
 * @param    {Object} [options={}]
 *           An object of options left to the plugin author to define.
 */
const skippy = function(options) {
  this.ready(() => {
    onPlayerReady(this, videojs.mergeOptions(defaults, options));
  });
};

// Register the plugin with video.js.
videojs.plugin('skippy', skippy);

// Include the version number.
skippy.VERSION = '__VERSION__';

export default skippy;
<commit_msg>add variables and breakage function<commit_after>import videojs from 'video.js';

// Default options for the plugin.
const defaults = {};

let recordedTime = 0;
let lastBrokeAt = false; //the last point of where the player broke, cleared at point of success
let clearLastBrokeAt = false; //timeout that clears out the lastBrokeAt variable when playback continues without interruption
let playlist; //playlist meta data for all segements within the HLS video
let brokenSentinel = 0; //maxes the amount of breaks we allow for a video (if it's too high we dont want to go into an infite loop)
let destroyEventHandler; //ensures that we don't register the destroy event twice
let stuckStuck = 0; // check if the player gets stuck
let lastCurrentTime = 0; // last time when the player got stuck
let breakages = [];

const checkForBreakage = (player) => {
  //needs url
	var breakHistory = breakages[player.src()];
	let checkThisTime = Math.floor(recordedTime);
	//if there's breakage at this time go ahead of it to the designated time
	if (breakHistory && breakHistory[checkThisTime]) {
		player.currentTime(breakHistory[checkThisTime]);
	}
	//clear out any remaining breakage queues
	if (player.currentTime() > lastBrokeAt && !clearLastBrokeAt) {
		clearLastBrokeAt = setTimeout(() => {
			lastBrokeAt = false;
		}, 500);
	}
}

/**
 * Function to invoke when the player is ready.
 *
 * This is a great place for your plugin to initialize itself. When this
 * function is called, the player will have its DOM and child components
 * in place.
 *
 * @function onPlayerReady
 * @param    {Player} player
 * @param    {Object} [options={}]
 */
const onPlayerReady = (player, options) => {
  player.addClass('vjs-skippy');
  player.on('timeupdate', (e) => {
    if(player) {
    	if (!lastBrokeAt && player.currentTime) {
    		recordedTime = player.currentTime();
    	}
    	if (player.paused() && player.currentTime() === lastCurrentTime) {
    		stuckStuck++;
    		if (stuckStuck >= 5) {
    			var skipTo = ((Math.floor(player.currentTime() / 2) + 1) * 2) + 0.1; // skip to next segment + 100ms buffer
    			player.currentTime(skipTo);
    			stuckStuck = 0;
    		}
    	} else {
    		stuckStuck = 0;
    	}
    	lastCurrentTime = player.currentTime();
    	checkForBreakage(player);
    }
  });

};

/**
 * A video.js plugin.
 *
 * In the plugin function, the value of `this` is a video.js `Player`
 * instance. You cannot rely on the player being in a "ready" state here,
 * depending on how the plugin is invoked. This may or may not be important
 * to you; if not, remove the wait for "ready"!
 *
 * @function skippy
 * @param    {Object} [options={}]
 *           An object of options left to the plugin author to define.
 */
const skippy = function(options) {
  this.ready(() => {
    onPlayerReady(this, videojs.mergeOptions(defaults, options));
  });
};

// Register the plugin with video.js.
videojs.plugin('skippy', skippy);

// Include the version number.
skippy.VERSION = '0.0.1';

export default skippy;
<|endoftext|>


<commit_before>import gaze       from 'gaze';
import readline   from 'readline';

/**
 * Returns `keepAlive` set to true so the event / plugin system stays alive while file watching is enabled for the
 * configured source globs that continues to incrementally generate documentation.
 *
 * @param {PluginEvent} ev - The plugin event.
 */
export function onComplete(ev)
{
   ev.data.keepAlive = true;

   const eventbus = ev.eventbus;

   const localEventProxy = eventbus.createEventProxy();

   const gazeOptions = Object.assign({ debounceDelay: 500 }, ev.pluginOptions.gazeOptions);

   const silent = typeof ev.pluginOptions.silent === 'boolean' ? ev.pluginOptions.silent : false;

   const terminal = typeof ev.pluginOptions.terminal === 'boolean' ? ev.pluginOptions.terminal : true;

   const verbose = typeof ev.pluginOptions.verbose === 'boolean' ? ev.pluginOptions.verbose : false;

   const log = (message) =>
   {
      if (!silent) { eventbus.trigger('log:info:time', message); }
   };

   const logVerbose = (message) =>
   {
      if (verbose && !silent) { eventbus.trigger('log:info:time', message); }
   };

   let sourceWatcher, testWatcher;
   let rl;
   let watcherCloseFunction;
   let promptVisible = false;

   const processInterruptCallback = () =>
   {
      eventbus.trigger('log:warn:time', 'Received SIGINT. Shutting down.');

      if (rl) { rl.close(); }
      if (sourceWatcher) { sourceWatcher.close(); }
      if (testWatcher) { testWatcher.close(); }

      setImmediate(() => process.exit(0));
   };

   const shutdownCallback = () =>
   {
      log('tjsdoc-plugin-watcher - shutdown requested.');
      promptVisible = false;

      if (rl) { rl.close(); }
      if (sourceWatcher) { sourceWatcher.close(); }
      if (testWatcher) { testWatcher.close(); }

      process.removeListener('SIGINT', processInterruptCallback);

      localEventProxy.off();
   };

   localEventProxy.on('tjsdoc:system:watcher:shutdown', shutdownCallback);

   if (terminal)
   {
      const rlConfig = !silent ? { input: process.stdin, output: process.stdout, prompt: 'TJSDoc> ' } :
      { input: process.stdin };

      rl = readline.createInterface(rlConfig);
   }

   const config = eventbus.triggerSync('tjsdoc:data:config:get');

   if (config._sourceGlobs)
   {
      log(`tjsdoc-plugin-watcher - watching source globs: ${JSON.stringify(config._sourceGlobs)}`);

      // Watch all source files.
      gaze(config._sourceGlobs, gazeOptions, (err, watcher) =>
      {
         sourceWatcher = watcher;

         // On source file changed.
         sourceWatcher.on('changed', (filePath) =>
         {
            if (promptVisible) { logVerbose(''); }

            logVerbose(`source changed: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:changed', { type: 'source', filePath });
         });

         // On source file added.
         sourceWatcher.on('added', (filePath) =>
         {
            if (promptVisible) { logVerbose(''); }

            logVerbose(`source addition: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:added', { type: 'source', filePath });
         });

         // On source file deleted.
         sourceWatcher.on('deleted', (filePath) =>
         {
            if (promptVisible) { logVerbose(''); }

            logVerbose(`source deletion: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:deleted', { type: 'source', filePath });
         });

         // On source watcher ending.
         sourceWatcher.on('end', () =>
         {
            eventbus.trigger('tjsdoc:system:watcher:stopped', { type: 'source' });

            if (typeof watcherCloseFunction === 'function')
            {
               const closeFunction = watcherCloseFunction;
               watcherCloseFunction = void 0;
               setImmediate(closeFunction);
            }
         });

         if (promptVisible) { logVerbose(''); }

         // Get watched files with relative paths
         const files = sourceWatcher.relative();

         log(`tjsdoc-plugin-watcher - watching source files: ${JSON.stringify(files)}`);

         eventbus.trigger('tjsdoc:system:watcher:started', { type: 'source' });
      });
   }

   if (config.test && config.test._sourceGlobs)
   {
      log(`tjsdoc-plugin-watcher - watching test globs: ${JSON.stringify(config.test._sourceGlobs)}`);

      // Watch all test files.
      gaze(config.test._sourceGlobs, gazeOptions, (err, watcher) =>
      {
         testWatcher = watcher;

         // On file changed
         testWatcher.on('changed', (filePath) =>
         {
            if (promptVisible) { logVerbose(''); }

            logVerbose(`test changed: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:changed', { type: 'test', filePath });
         });

         // On file added
         testWatcher.on('added', (filePath) =>
         {
            if (promptVisible) { logVerbose(''); }

            logVerbose(`test addition: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:added', { type: 'test', filePath });
         });

         // On file deleted
         testWatcher.on('deleted', (filePath) =>
         {
            if (promptVisible) { logVerbose(''); }

            logVerbose(`test deletion: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:deleted', { type: 'test', filePath });
         });

         testWatcher.on('end', () =>
         {
            eventbus.trigger('tjsdoc:system:watcher:stopped', { type: 'test' });

            if (typeof watcherCloseFunction === 'function' && sourceWatcher)
            {
               sourceWatcher.close();
            }
         });

         if (promptVisible) { logVerbose(''); }

         // Get watched files with relative paths
         const files = testWatcher.relative();

         log(`tjsdoc-plugin-watcher - watching test files: ${JSON.stringify(files)}`);

         eventbus.trigger('tjsdoc:system:watcher:started', { type: 'test' });
      });
   }

   if (config._sourceGlobs || (config.test && config.test._sourceGlobs))
   {
      process.on('SIGINT', processInterruptCallback);

      // Set terminal readline loop waiting for the user to type in the commands: `restart` or `exit`.

      if (rl)
      {
         rl.on('line', (line) =>
         {
            switch (line.trim())
            {
               case 'exit':
                  promptVisible = false;

                  rl.close();
                  if (sourceWatcher) { sourceWatcher.close(); }
                  if (testWatcher) { testWatcher.close(); }

                  process.removeListener('SIGINT', processInterruptCallback);

                  setImmediate(() => process.exit(0));
                  break;

               case 'globs':
                  promptVisible = false;

                  if (config._sourceGlobs)
                  {
                     log(`watching source globs: ${JSON.stringify(config._sourceGlobs)}`);
                  }

                  if (config.test && config.test._sourceGlobs)
                  {
                     log(`watching test globs: ${JSON.stringify(config.test._sourceGlobs)}`);
                  }
                  break;

               case 'regen':
               case 'regenerate':
                  promptVisible = false;

                  setImmediate(() =>
                  {
                     watcherCloseFunction = () =>
                     {
                        eventbus.trigger('tjsdoc:system:regenerate:all:docs');
                     };

                     process.removeAllListeners('SIGINT');

                     // If `testWatcher` exists it will close `sourceWatcher` otherwise
                     if (testWatcher)
                     {
                        testWatcher.close();
                     }
                     else if (sourceWatcher)
                     {
                        sourceWatcher.close();
                     }

                     rl.close();
                  });
                  break;

               case 'watching':
                  promptVisible = false;

                  if (sourceWatcher)
                  {
                     log(`watching source files: ${JSON.stringify(sourceWatcher.relative())}`);
                  }

                  if (testWatcher)
                  {
                     log(`watching test files: ${JSON.stringify(testWatcher.relative())}`);
                  }
                  break;

               default:
                  if (!silent)
                  {
                     promptVisible = true;
                     rl.prompt();
                  }
                  break;
            }
         });

         eventbus.trigger('tjsdoc:system:watcher:initialized');
      }
   }
   else
   {
      log('tjsdoc-plugin-watcher: no main source or tests to watch.');
   }
}
<commit_msg>improved control flow and callbacks<commit_after>import gaze       from 'gaze';
import readline   from 'readline';

/**
 * Returns `keepAlive` set to true so the event / plugin system stays alive while file watching is enabled for the
 * configured source globs that continues to incrementally generate documentation.
 *
 * @param {PluginEvent} ev - The plugin event.
 */
export function onComplete(ev)
{
   ev.data.keepAlive = true;

   const eventbus = ev.eventbus;

   const localEventProxy = eventbus.createEventProxy();

   const gazeOptions = Object.assign({ debounceDelay: 500 }, ev.pluginOptions.gazeOptions);

   const silent = typeof ev.pluginOptions.silent === 'boolean' ? ev.pluginOptions.silent : false;

   const terminal = typeof ev.pluginOptions.terminal === 'boolean' ? ev.pluginOptions.terminal : true;

   const verbose = typeof ev.pluginOptions.verbose === 'boolean' ? ev.pluginOptions.verbose : false;

   let watcherStartCount = 0;
   let watcherCloseCount = 0;
   const watcherStartData = {};

   const log = (message) =>
   {
      if (!silent) { eventbus.trigger('log:info:time', message); }
   };

   const logNewLine = () => { console.log(''); };

   const logVerbose = (message) =>
   {
      if (verbose && !silent) { eventbus.trigger('log:info:time', message); }
   };

   const logVerboseRaw = (message) =>
   {
      if (verbose && !silent) { eventbus.trigger('log:info:raw', message); }
   };

   let sourceWatcher, testWatcher;
   let rl;
   let watcherCloseFunction;
   let promptVisible = false;

   const processInterruptCallback = () =>
   {
      if (promptVisible) { logNewLine(); }

      eventbus.trigger('log:warn:time', 'tjsdoc-plugin-watcher - received SIGINT; shutting down.');

      setImmediate(() => eventbus.trigger('tjsdoc:system:watcher:shutdown'));
   };

   const shutdownCallback = (data) =>
   {
      const regenerate = typeof data === 'object' && typeof data.regenerate === 'boolean' ? data.regenerate : false;

      if (promptVisible) { logNewLine(); }

      logVerbose(`tjsdoc-plugin-watcher - shutdown requested${regenerate ? ' with regeneration' : ''}.`);

      promptVisible = false;

      if (rl) { rl.close(); }

      rl = void 0;

      localEventProxy.off();

      process.removeListener('SIGINT', processInterruptCallback);

      if (regenerate) { watcherCloseFunction = () => eventbus.trigger('tjsdoc:system:regenerate:all:docs'); }

      // If `testWatcher` exists it will close `sourceWatcher` otherwise potentially invoke `sourceWatcher.close()`.
      if (testWatcher)
      {
         testWatcher.close();
      }
      else if (sourceWatcher)
      {
         sourceWatcher.close();
      }

      setTimeout(() => {}, 500);
   };

   localEventProxy.on('tjsdoc:system:watcher:shutdown', shutdownCallback);

   if (terminal)
   {
      const rlConfig = !silent ? { input: process.stdin, output: process.stdout, prompt: 'TJSDoc> ' } :
      { input: process.stdin };

      rl = readline.createInterface(rlConfig);
   }

   const config = eventbus.triggerSync('tjsdoc:data:config:get');

   if (config._sourceGlobs)
   {
      log(`tjsdoc-plugin-watcher - watching source globs: ${JSON.stringify(config._sourceGlobs)}`);

      watcherStartCount++;
      watcherCloseCount++;

      // Watch all source files.
      gaze(config._sourceGlobs, gazeOptions, (err, watcher) =>
      {
         sourceWatcher = watcher;

         // On source file added.
         sourceWatcher.on('added', (filePath) =>
         {
            if (promptVisible) { logVerboseRaw(''); }

            logVerbose(`tjsdoc-plugin-watcher - source addition: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:added', { type: 'source', filePath });
         });

         // On source file changed.
         sourceWatcher.on('changed', (filePath) =>
         {
            if (promptVisible) { logVerboseRaw(''); }

            logVerbose(`tjsdoc-plugin-watcher - source changed: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:changed', { type: 'source', filePath });
         });

         // On source file deleted.
         sourceWatcher.on('deleted', (filePath) =>
         {
            if (promptVisible) { logVerboseRaw(''); }

            logVerbose(`tjsdoc-plugin-watcher - source deletion: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:deleted', { type: 'source', filePath });
         });

         // On source watcher ending.
         sourceWatcher.on('end', () =>
         {
            eventbus.trigger('tjsdoc:system:watcher:stopped', { type: 'source' });

            logVerbose('tjsdoc-plugin-watcher - source watcher stopped.');

            sourceWatcher = void 0;

            watcherCloseCount--;

            if (watcherCloseCount === 0)
            {
               eventbus.trigger('tjsdoc:system:watcher:stopped');
            }

            if (typeof watcherCloseFunction === 'function')
            {
               const closeFunction = watcherCloseFunction;

               watcherCloseFunction = void 0;

               logVerbose('tjsdoc-plugin-watcher - shutdown callback invoked.');

               setImmediate(closeFunction);
            }
         });

         watcherStartData.source = { globs: config._sourceGlobs, files: sourceWatcher.watched() };

         // Get watched files with relative paths
         const files = sourceWatcher.relative();

         if (promptVisible) { logNewLine(); }

         log(`tjsdoc-plugin-watcher - watching source files: ${JSON.stringify(files)}`);

         watcherStartCount--;

         if (watcherStartCount === 0) { eventbus.trigger('tjsdoc:system:watcher:started', watcherStartData); }
      });
   }

   if (config.test && config.test._sourceGlobs)
   {
      log(`tjsdoc-plugin-watcher - watching test globs: ${JSON.stringify(config.test._sourceGlobs)}`);

      watcherStartCount++;
      watcherCloseCount++;

      // Watch all test files.
      gaze(config.test._sourceGlobs, gazeOptions, (err, watcher) =>
      {
         testWatcher = watcher;

         // On file added
         testWatcher.on('added', (filePath) =>
         {
            if (promptVisible) { logVerboseRaw(''); }

            logVerbose(`tjsdoc-plugin-watcher - test addition: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:added', { type: 'test', filePath });
         });

         // On file changed
         testWatcher.on('changed', (filePath) =>
         {
            if (promptVisible) { logVerboseRaw(''); }

            logVerbose(`tjsdoc-plugin-watcher - test changed: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:changed', { type: 'test', filePath });
         });

         // On file deleted
         testWatcher.on('deleted', (filePath) =>
         {
            if (promptVisible) { logVerboseRaw(''); }

            logVerbose(`tjsdoc-plugin-watcher - test deletion: ${filePath}`);

            eventbus.trigger('tjsdoc:system:watcher:file:deleted', { type: 'test', filePath });
         });

         testWatcher.on('end', () =>
         {
            eventbus.trigger('tjsdoc:system:watcher:stopped', { type: 'test' });

            logVerbose('tjsdoc-plugin-watcher - test watcher stopped.');

            testWatcher = void 0;

            watcherCloseCount--;

            if (watcherCloseCount === 0)
            {
               eventbus.trigger('tjsdoc:system:watcher:stopped');
            }

            if (sourceWatcher) { sourceWatcher.close(); }
         });

         if (promptVisible) { logNewLine(); }

         watcherStartData.test = { globs: config.test._sourceGlobs, files: testWatcher.watched() };

         // Get watched files with relative paths
         const files = testWatcher.relative();

         log(`tjsdoc-plugin-watcher - watching test files: ${JSON.stringify(files)}`);

         watcherStartCount--;

         if (watcherStartCount === 0) { eventbus.trigger('tjsdoc:system:watcher:started', watcherStartData); }
      });
   }

   if (config._sourceGlobs || (config.test && config.test._sourceGlobs))
   {
      process.on('SIGINT', processInterruptCallback);

      // Set terminal readline loop waiting for the user to type in the commands: `restart` or `exit`.

      if (rl)
      {
         rl.on('line', (line) =>
         {
            switch (line.trim())
            {
               case 'exit':
                  setImmediate(() => eventbus.trigger('tjsdoc:system:watcher:shutdown'));
                  break;

               case 'globs':
                  promptVisible = false;

                  if (config._sourceGlobs)
                  {
                     log(`tjsdoc-plugin-watcher - watching source globs: ${JSON.stringify(config._sourceGlobs)}`);
                  }

                  if (config.test && config.test._sourceGlobs)
                  {
                     log(`tjsdoc-plugin-watcher - watching test globs: ${JSON.stringify(config.test._sourceGlobs)}`);
                  }
                  break;

               case 'help':
                  if (!silent)
                  {
                     eventbus.trigger('log:info:raw', `tjsdoc-plugin-watcher - options: `);
                     eventbus.trigger('log:info:raw', `  'exit', shutdown watcher `);
                     eventbus.trigger('log:info:raw', `  'globs', list globs being watched `);
                     eventbus.trigger('log:info:raw', `  'help', this listing of commands `);
                     eventbus.trigger('log:info:raw', `  'regen', regenerate all documentation `);
                     eventbus.trigger('log:info:raw', `  'watching', the files being watched `);
                     eventbus.trigger('log:info:raw', '');

                     promptVisible = true;
                     rl.prompt();
                  }
                  break;

               case 'regen':
                  setImmediate(() => eventbus.trigger('tjsdoc:system:watcher:shutdown', { regenerate: true }));
                  break;

               case 'watching':
                  promptVisible = false;

                  if (sourceWatcher)
                  {
                     log(`tjsdoc-plugin-watcher - watching source files: ${JSON.stringify(sourceWatcher.relative())}`);
                  }

                  if (testWatcher)
                  {
                     log(`tjsdoc-plugin-watcher - watching test files: ${JSON.stringify(testWatcher.relative())}`);
                  }
                  break;

               default:
                  log(`tjsdoc-plugin-watcher - unknown command (type 'help' for instructions)`);

               // eslint-disable-next-line no-fallthrough
               case '':
                  if (!silent)
                  {
                     promptVisible = true;
                     rl.prompt();
                  }

                  break;
            }
         });

         eventbus.trigger('tjsdoc:system:watcher:initialized');
      }
   }
   else
   {
      log('tjsdoc-plugin-watcher: no main source or tests to watch.');
   }
}
<|endoftext|>