<commit_before>var util = require('util');
var events = require('events');

var Reply = require('./reply');
var errors = require('../errors');
var factory = require('./factory');

// The method of the authentication packet to store.
var authMethod = 'auth';

/**
 * race takes an array of promises and is resolved with the first promise
 * that resolves, or rejects. After the first resolve or rejection, all
 * future resolutions and rejections will be discarded.
 * @param {Promise[]} promises
 * @return {Promise}
 */
function race(promises) {
    return new BeamSocket.Promise(function (_resolve, _reject) {
        var done = false;
        function guard(fn) {
            return function () {
                if (!done) {
                    done = true;
                    fn.apply(null, arguments);
                }
            };
        }

        var resolve = guard(_resolve);
        var reject = guard(_reject);

        for (var i = 0; i < promises.length; i++) {
            promises[i].then(resolve).catch(reject);
        }
    });
}

/**
 * Return a promise which is rejected with a TimeoutError after the
 * provided delay.
 * @param  {Number} delay
 * @return {Promise}
 */
function timeout(delay) {
    return new BeamSocket.Promise(function (resolve, reject) {
        setTimeout(function () {
            reject(new TimeoutError());
        }, delay);
    });
}

/**
 * Assigns enumerable properties of the src onto the obj, returning src.
 * Semi-polyfill for Object.assign.
 * @param  {Object} obj
 * @param  {Object} src
 * @return {Object}
 */
function assign(src, obj) {
    src = src || {};
    obj = obj || {};
    Object.keys(obj).forEach(function (key) {
        src[key] = obj[key];
    });

    return src;
}

/**
 * Manages a connect to Beam's chat servers.
 * @access public
 * @param {Array} addresses
 * @param {Object} options
 * @param {Number} [options.pingInterval=15000] How often to send ping packets
 *     to  make sure the connection is alive. Given in milliseconds.
 * @param {Number} [options.pingTimeout=5000] How long to wait after sending
 *     a ping before, if we don't get a reply, we consider the connection dead.
 * @param {Number} [options.callTimeout=20000] Number of milliseconds to wait
 *     for a reply from the chat server before giving up and rejecting with
 *     a TimeoutError. This can be overridden in the arguments to .call().
 */
function BeamSocket (addresses, options) {
    events.EventEmitter.call(this);

    options = assign({
        pingInterval: 15 * 1000,
        pingTimeout: 5 * 1000,
        callTimeout: 20 * 1000,
    }, options)

    // Which connection we use in our load balancing.
    this._addressOffset = Math.floor(Math.random() * addresses.length);

    // List of addresses we can connect to.
    this._addresses = addresses;

    // Spool to store events queued when the connection is lost.
    this._spool = [];

    // The WebSocket instance we're currently connected with.
    this.ws = null;

    // Information for server pings. We ping the server on the interval
    // (if we don't get any other packets) and consider a connection
    // dead if it doesn't respond within the timeout.
    this._pingTimeoutHandle = null;
    this._pingInterval = options.pingInterval;
    this._pingTimeout = options.pingTimeout;

    // The status of the socket connection.
    this.status = BeamSocket.IDLE;

    // Counter of the current number of reconnect retries, and the number of
    // retries before we reset our reconnect attempts.
    this._retries = 0;
    this._retryWrap = 7; // max 2 minute retry time
    // Timeout waiting to reconnect
    this._reconnectTimeout = null;

    // Number of milliseconds to wait in .call() before we give up waiting
    // for the reply. Used to prevent leakages.
    this._callTimeout = options.callTimeout;

    // Map of call IDs to promises that should be resolved on
    // method responses.
    this._replies = {};

    // Authentication packet store that we'll resend if we have to reconnect.
    this._authpacket = null;

    // Counter for method calls.
    this._callNo = 0;
}

BeamSocket.prototype = Object.create(events.EventEmitter.prototype);

/**
 * Gets the status of the socket connection.
 * @return {status} Should be compared against BeamSocket.status
 */
BeamSocket.prototype.getStatus = function () {
    return this.status;
};

/**
 * Returns whether the socket is currently connected.
 * @return {Boolean}
 */
BeamSocket.prototype.isConnected = function () {
    return this.status === BeamSocket.CONNECTED;
};

/**
 * Retrieves a chat endpoint to connect to. We use round-robin balancing.
 * @access protected
 * @return {String}
 */
BeamSocket.prototype.getAddress = function () {
    if (++this._addressOffset >= this._addresses.length) {
        this._addressOffset = 0;
    }

    return this._addresses[this._addressOffset];
};

/**
 * Returns how long to wait before attempting to reconnect. This does TCP-style
 * limited exponential backoff.
 * @return {Number}
 */
BeamSocket.prototype._getNextReconnectInterval = function () {
    var power = (this._retries++ % this._retryWrap) + Math.round(Math.random());
    return (1 << power) * 500;
};

/**
 * _handleClose is called when the websocket closes or emits an error. If
 * we weren't gracefully closed, we'll try to reconnect.
 */
BeamSocket.prototype._handleClose = function () {
    clearTimeout(this._pingTimeoutHandle);
    this._pingInterval = null;
    this.ws = null;

    if (this.status === BeamSocket.CLOSING) {
        this.status = BeamSocket.CLOSED;
        return this.emit('closed');
    }

    this.status = BeamSocket.CONNECTING;
    this._reconnectTimeout = setTimeout(
        this.boot.bind(this),
        this._getNextReconnectInterval()
    );
};

/**
 * Sets the socket to send a ping message after an interval. This is
 * called when a successful ping is received and after data is received
 * from the socket (there's no need to ping when we know the socket
 * is still alive).
 */
BeamSocket.prototype._resetPingTimeout = function () {
    clearTimeout(this._pingTimeoutHandle);

    var self = this;
    this._pingTimeoutHandle = setTimeout(function () {
        self.ping().catch(function () {});
    }, this._pingInterval);
};

/**
 * Ping runs a ping against the server and returns a promise which is
 * resolved if the server responds, or rejected on timeout.
 * @return {Promise}
 */
BeamSocket.prototype.ping = function () {
    var ws = this.ws;
    var self = this;
    clearTimeout(this._pingTimeoutHandle);

    if (!this.isConnected()) {
        return new BeamSocket.Promise(function (resolve, reject) {
            reject(new TimeoutError());
        });
    }

    var promise;

    if (typeof ws.ping === 'function') {
        // Node's ws module has a ping function we can use rather than
        // sending a message. More lightweight, less noisy.
        promise = race([
            timeout(this._pingTimeout),
            new BeamSocket.Promise(function (resolve) {
                ws.once('pong', resolve)
            }),
        ]);
        ws.ping();
    } else {
        // Otherwise we'll resort to sending a ping message over the socket.
        promise = this.call('ping', [], { timeout: this._pingTimeout });
    }

    return promise
    .then(this._resetPingTimeout.bind(this))
    .catch(TimeoutError, function (err) {
        // If we haven't noticed the socket is dead since we started trying
        // to ping, manually emit an error. This'll cause it to close.
        if (self.ws === ws) {
            self.ws.emit('error', err);
        }

        throw err;
    });
};

/**
 * Starts a socket client. Attaches events and tries to connect to a
 * chat server.
 * @access public
 * @fires BeamSocket#connected
 * @fires BeamSocket#closed
 * @fires BeamSocket#error
 */
BeamSocket.prototype.boot = function () {
    var self = this;
    var ws = this.ws = factory.create(this.getAddress());
    this.status = BeamSocket.CONNECTING;

    // Websocket connection has been established.
    ws.on('open', function () {
        self._resetPingTimeout();
        self.unspool.apply(self, arguments);
    });

    // We got an incoming data packet.
    ws.on('message', function () {
        self._resetPingTimeout();
        self.parsePacket.apply(self, arguments);
    });

    // Websocket connection closed
    ws.on('close', function () {
        self._handleClose.apply(self, arguments);
    });

    // Websocket hit an error and is about to close.
    ws.on('error', function (err) {
        self.emit('error', err);
        ws.close();
    });

    return this;
};

/**
 * Should be called on reconnection. Authenticates and sends follow-up
 * packets if we have any. After we get re-established with auth
 * we'll formally say this socket is connected. This is to prevent
 * race conditions where packets could get send before authentication
 * is reestablished.
 * @access protected
 */
BeamSocket.prototype.unspool = function () {
    var self = this;

    // If we already authed, it means we're reconnecting and should
    // establish authentication again.
    if (this._authpacket) {
        this.call(authMethod, this._authpacket, { force: true })
            .then(function (result) {
                self.emit('authresult', result);
            })
            .then(bang)
            .catch(function () {
                self.emit('error', new errors.AuthenticationFailedError());
                self.close();
            });
    }
    // Otherwise, we can reestablish immediately
    else {
        bang();
    }

    // Helper function that's called when we're fully reestablished and
    // ready to take direct calls again.
    function bang () {
        // Send any spooled events that we have.
        for (var i = 0; i < self._spool.length; i++) {
            self.send(self._spool[i], { force: true });
        }
        self._spool = [];

        // Finally, tell the world we're connected.
        self._retries = 0;
        self.status = BeamSocket.CONNECTED;
        self.emit('connected');

        // Clean up for gc
        self = null;
    }
};

/**
 * Parses an incoming packet from the websocket.
 * @access protected
 * @param  {String} data
 * @param  {Object} flags
 * @fires BeamSocket#error
 * @fires BeamSocket#packet
 */
BeamSocket.prototype.parsePacket = function (data, flags) {
    if (flags && flags.binary) {
        // We can't handle binary packets. Why the fudge are we here?
        return this.emit('error', new errors.BadMessageError('Cannot parse binary packets. Wat.'));
    }

    // Unpack the packet data.
    var packet;
    try {
        packet = JSON.parse(data);
    } catch (e) {
        return this.emit('error', new errors.BadMessageError('Unable to parse packet as json'));
    }

    this.emit('packet', packet);

    switch (packet.type) {

        case 'reply':
            // Try to look up the packet reply handler, and call it if we can.
            var reply = this._replies[packet.id];
            if (typeof reply !== 'undefined') {
                reply.handle(packet);
                delete this._replies[packet.id];
            }
            // Otherwise emit an error. This might happen occasionally,
            // but failing silently is lame.
            else {
                this.emit('error', new errors.NoMethodHandlerError('No handler for reply ID.'));
            }

        break;
        case 'event':
            // Just emit events out on this emitter.
            this.emit(packet.event, packet.data);

        break;
        default:
            this.emit('error', new errors.BadMessageError('Unknown packet type ' + packet.type));
    }
};

/**
 * Sends raw packet data to the server. It may not send immediately;
 * if we aren't connected, it'll just be spooled up.
 *
 * @access protected
 * @param  {Object} data
 * @param  {Object} options_
 * @fires BeamSocket#sent
 * @fires BeamSocket#spooled
 */
BeamSocket.prototype.send = function (data, options_) {
    var options = options_ || {};

    if (this.isConnected() || options.force) {
        this.ws.send(JSON.stringify(data));
        this.emit('sent', data);
    } else if (data.method !== authMethod) {
        this._spool.push(data);
        this.emit('spooled', data);
    }
};

/**
 * auth sends a packet over the socket to authenticate with a chat server
 * and join a specified channel. If you wish to join anonymously, user
 * and authkey can be omitted.
 * @param  {Number} id
 * @param  {Number} [user]
 * @param  {String} [authkey]
 * @return {Promise}
 */
BeamSocket.prototype.auth = function (id, user, authkey) {
    this._authpacket = [id, user, authkey];

    // Two cases here: if we're already connected, with send the auth
    // packet immediately. Otherwise we wait for a `connected` event,
    // which won't be sent until after we re-authenticate.
    if (this.isConnected()) {
        return this.call('auth', [id, user, authkey]);
    }

    var self = this;
    return new BeamSocket.Promise(function (resolve) {
        self.once('authresult', resolve);
    });
};

/**
 * Runs a method on the socket. Returns a promise that is rejected or
 * resolved upon reply.
 * @access public
 * @param  {String} method
 * @param  {Array=[]} args_ Additional arguments to pass to the method.
 * @param  {Options={}} options_
 * @return {Promise}
 */
BeamSocket.prototype.call = function (method, args_, options_) {
    // Unpack arguments, so that the client can pass both, one, or
    // neither args or options.
    var options = options_, args = args_;
    if (!Array.isArray(args)) {
        options = args;
        args = [];
    }
    options = options || {};

    // Send out the data
    var id = this._callNo++, self = this;
    this.send({ type: 'method', method: method, arguments: args, id: id }, options);

    // Then create and return a promise that's resolved when we get
    // a reply, if we expect one to be given.
    if (options.noReply) {
        return;
    }

    return race([
        timeout(options.timeout || this._callTimeout),
        new BeamSocket.Promise(function (resolve, reject) {
            self._replies[id] = new Reply(resolve, reject);
        })
    ]).catch(TimeoutError, function (err) {
        delete self._replies[id];
        throw err;
    });
};

/**
 * Closes the websocket gracefully.
 * @access public
 */
BeamSocket.prototype.close = function () {
    if (this.ws) {
        this.ws.close();
        this.status = BeamSocket.CLOSING;
    } else {
        clearTimeout(this._reconnectTimeout);
        this.status = BeamSocket.CLOSED;
    }
};

/**
 * Promise class to use in .call
 * @type {Promise}
 */
BeamSocket.Promise = require('bluebird');

/**
 * List of available statuses.
 * @readonly
 * @enum {Number}
 */
[
    /** We've not tried connecting yet */
    'IDLE',
    /** We successfully connected */
    'CONNECTED',
    /** The socket was is closing gracefully. */
    'CLOSING',
    /** The socket was closed gracefully. */
    'CLOSED',
    /** We're currently trying to connect */
    'CONNECTING'
].forEach(function (status, i) {
    BeamSocket[status] = i;
});

/**
 * A TimeoutError is thrown in call if we don't get a response from the
 * chat server within a certain interval.
 */
function TimeoutError() { Error.call(this); }
TimeoutError.prototype = Object.create(Error.prototype);

/**
 * Reference to the TimeoutError class.
 * @type {TimeoutError}
 */
BeamSocket.TimeoutError = TimeoutError;

module.exports = BeamSocket;
<commit_msg>Catch exceptions when requiring Bluebird for builds that exclude it<commit_after>var util = require('util');
var events = require('events');

var Reply = require('./reply');
var errors = require('../errors');
var factory = require('./factory');

// The method of the authentication packet to store.
var authMethod = 'auth';

/**
 * race takes an array of promises and is resolved with the first promise
 * that resolves, or rejects. After the first resolve or rejection, all
 * future resolutions and rejections will be discarded.
 * @param {Promise[]} promises
 * @return {Promise}
 */
function race(promises) {
    return new BeamSocket.Promise(function (_resolve, _reject) {
        var done = false;
        function guard(fn) {
            return function () {
                if (!done) {
                    done = true;
                    fn.apply(null, arguments);
                }
            };
        }

        var resolve = guard(_resolve);
        var reject = guard(_reject);

        for (var i = 0; i < promises.length; i++) {
            promises[i].then(resolve).catch(reject);
        }
    });
}

/**
 * Return a promise which is rejected with a TimeoutError after the
 * provided delay.
 * @param  {Number} delay
 * @return {Promise}
 */
function timeout(delay) {
    return new BeamSocket.Promise(function (resolve, reject) {
        setTimeout(function () {
            reject(new TimeoutError());
        }, delay);
    });
}

/**
 * Assigns enumerable properties of the src onto the obj, returning src.
 * Semi-polyfill for Object.assign.
 * @param  {Object} obj
 * @param  {Object} src
 * @return {Object}
 */
function assign(src, obj) {
    src = src || {};
    obj = obj || {};
    Object.keys(obj).forEach(function (key) {
        src[key] = obj[key];
    });

    return src;
}

/**
 * Manages a connect to Beam's chat servers.
 * @access public
 * @param {Array} addresses
 * @param {Object} options
 * @param {Number} [options.pingInterval=15000] How often to send ping packets
 *     to  make sure the connection is alive. Given in milliseconds.
 * @param {Number} [options.pingTimeout=5000] How long to wait after sending
 *     a ping before, if we don't get a reply, we consider the connection dead.
 * @param {Number} [options.callTimeout=20000] Number of milliseconds to wait
 *     for a reply from the chat server before giving up and rejecting with
 *     a TimeoutError. This can be overridden in the arguments to .call().
 */
function BeamSocket (addresses, options) {
    events.EventEmitter.call(this);

    options = assign({
        pingInterval: 15 * 1000,
        pingTimeout: 5 * 1000,
        callTimeout: 20 * 1000,
    }, options)

    // Which connection we use in our load balancing.
    this._addressOffset = Math.floor(Math.random() * addresses.length);

    // List of addresses we can connect to.
    this._addresses = addresses;

    // Spool to store events queued when the connection is lost.
    this._spool = [];

    // The WebSocket instance we're currently connected with.
    this.ws = null;

    // Information for server pings. We ping the server on the interval
    // (if we don't get any other packets) and consider a connection
    // dead if it doesn't respond within the timeout.
    this._pingTimeoutHandle = null;
    this._pingInterval = options.pingInterval;
    this._pingTimeout = options.pingTimeout;

    // The status of the socket connection.
    this.status = BeamSocket.IDLE;

    // Counter of the current number of reconnect retries, and the number of
    // retries before we reset our reconnect attempts.
    this._retries = 0;
    this._retryWrap = 7; // max 2 minute retry time
    // Timeout waiting to reconnect
    this._reconnectTimeout = null;

    // Number of milliseconds to wait in .call() before we give up waiting
    // for the reply. Used to prevent leakages.
    this._callTimeout = options.callTimeout;

    // Map of call IDs to promises that should be resolved on
    // method responses.
    this._replies = {};

    // Authentication packet store that we'll resend if we have to reconnect.
    this._authpacket = null;

    // Counter for method calls.
    this._callNo = 0;
}

BeamSocket.prototype = Object.create(events.EventEmitter.prototype);

/**
 * Gets the status of the socket connection.
 * @return {status} Should be compared against BeamSocket.status
 */
BeamSocket.prototype.getStatus = function () {
    return this.status;
};

/**
 * Returns whether the socket is currently connected.
 * @return {Boolean}
 */
BeamSocket.prototype.isConnected = function () {
    return this.status === BeamSocket.CONNECTED;
};

/**
 * Retrieves a chat endpoint to connect to. We use round-robin balancing.
 * @access protected
 * @return {String}
 */
BeamSocket.prototype.getAddress = function () {
    if (++this._addressOffset >= this._addresses.length) {
        this._addressOffset = 0;
    }

    return this._addresses[this._addressOffset];
};

/**
 * Returns how long to wait before attempting to reconnect. This does TCP-style
 * limited exponential backoff.
 * @return {Number}
 */
BeamSocket.prototype._getNextReconnectInterval = function () {
    var power = (this._retries++ % this._retryWrap) + Math.round(Math.random());
    return (1 << power) * 500;
};

/**
 * _handleClose is called when the websocket closes or emits an error. If
 * we weren't gracefully closed, we'll try to reconnect.
 */
BeamSocket.prototype._handleClose = function () {
    clearTimeout(this._pingTimeoutHandle);
    this._pingInterval = null;
    this.ws = null;

    if (this.status === BeamSocket.CLOSING) {
        this.status = BeamSocket.CLOSED;
        return this.emit('closed');
    }

    this.status = BeamSocket.CONNECTING;
    this._reconnectTimeout = setTimeout(
        this.boot.bind(this),
        this._getNextReconnectInterval()
    );
};

/**
 * Sets the socket to send a ping message after an interval. This is
 * called when a successful ping is received and after data is received
 * from the socket (there's no need to ping when we know the socket
 * is still alive).
 */
BeamSocket.prototype._resetPingTimeout = function () {
    clearTimeout(this._pingTimeoutHandle);

    var self = this;
    this._pingTimeoutHandle = setTimeout(function () {
        self.ping().catch(function () {});
    }, this._pingInterval);
};

/**
 * Ping runs a ping against the server and returns a promise which is
 * resolved if the server responds, or rejected on timeout.
 * @return {Promise}
 */
BeamSocket.prototype.ping = function () {
    var ws = this.ws;
    var self = this;
    clearTimeout(this._pingTimeoutHandle);

    if (!this.isConnected()) {
        return new BeamSocket.Promise(function (resolve, reject) {
            reject(new TimeoutError());
        });
    }

    var promise;

    if (typeof ws.ping === 'function') {
        // Node's ws module has a ping function we can use rather than
        // sending a message. More lightweight, less noisy.
        promise = race([
            timeout(this._pingTimeout),
            new BeamSocket.Promise(function (resolve) {
                ws.once('pong', resolve)
            }),
        ]);
        ws.ping();
    } else {
        // Otherwise we'll resort to sending a ping message over the socket.
        promise = this.call('ping', [], { timeout: this._pingTimeout });
    }

    return promise
    .then(this._resetPingTimeout.bind(this))
    .catch(TimeoutError, function (err) {
        // If we haven't noticed the socket is dead since we started trying
        // to ping, manually emit an error. This'll cause it to close.
        if (self.ws === ws) {
            self.ws.emit('error', err);
        }

        throw err;
    });
};

/**
 * Starts a socket client. Attaches events and tries to connect to a
 * chat server.
 * @access public
 * @fires BeamSocket#connected
 * @fires BeamSocket#closed
 * @fires BeamSocket#error
 */
BeamSocket.prototype.boot = function () {
    var self = this;
    var ws = this.ws = factory.create(this.getAddress());
    this.status = BeamSocket.CONNECTING;

    // Websocket connection has been established.
    ws.on('open', function () {
        self._resetPingTimeout();
        self.unspool.apply(self, arguments);
    });

    // We got an incoming data packet.
    ws.on('message', function () {
        self._resetPingTimeout();
        self.parsePacket.apply(self, arguments);
    });

    // Websocket connection closed
    ws.on('close', function () {
        self._handleClose.apply(self, arguments);
    });

    // Websocket hit an error and is about to close.
    ws.on('error', function (err) {
        self.emit('error', err);
        ws.close();
    });

    return this;
};

/**
 * Should be called on reconnection. Authenticates and sends follow-up
 * packets if we have any. After we get re-established with auth
 * we'll formally say this socket is connected. This is to prevent
 * race conditions where packets could get send before authentication
 * is reestablished.
 * @access protected
 */
BeamSocket.prototype.unspool = function () {
    var self = this;

    // If we already authed, it means we're reconnecting and should
    // establish authentication again.
    if (this._authpacket) {
        this.call(authMethod, this._authpacket, { force: true })
            .then(function (result) {
                self.emit('authresult', result);
            })
            .then(bang)
            .catch(function () {
                self.emit('error', new errors.AuthenticationFailedError());
                self.close();
            });
    }
    // Otherwise, we can reestablish immediately
    else {
        bang();
    }

    // Helper function that's called when we're fully reestablished and
    // ready to take direct calls again.
    function bang () {
        // Send any spooled events that we have.
        for (var i = 0; i < self._spool.length; i++) {
            self.send(self._spool[i], { force: true });
        }
        self._spool = [];

        // Finally, tell the world we're connected.
        self._retries = 0;
        self.status = BeamSocket.CONNECTED;
        self.emit('connected');

        // Clean up for gc
        self = null;
    }
};

/**
 * Parses an incoming packet from the websocket.
 * @access protected
 * @param  {String} data
 * @param  {Object} flags
 * @fires BeamSocket#error
 * @fires BeamSocket#packet
 */
BeamSocket.prototype.parsePacket = function (data, flags) {
    if (flags && flags.binary) {
        // We can't handle binary packets. Why the fudge are we here?
        return this.emit('error', new errors.BadMessageError('Cannot parse binary packets. Wat.'));
    }

    // Unpack the packet data.
    var packet;
    try {
        packet = JSON.parse(data);
    } catch (e) {
        return this.emit('error', new errors.BadMessageError('Unable to parse packet as json'));
    }

    this.emit('packet', packet);

    switch (packet.type) {

        case 'reply':
            // Try to look up the packet reply handler, and call it if we can.
            var reply = this._replies[packet.id];
            if (typeof reply !== 'undefined') {
                reply.handle(packet);
                delete this._replies[packet.id];
            }
            // Otherwise emit an error. This might happen occasionally,
            // but failing silently is lame.
            else {
                this.emit('error', new errors.NoMethodHandlerError('No handler for reply ID.'));
            }

        break;
        case 'event':
            // Just emit events out on this emitter.
            this.emit(packet.event, packet.data);

        break;
        default:
            this.emit('error', new errors.BadMessageError('Unknown packet type ' + packet.type));
    }
};

/**
 * Sends raw packet data to the server. It may not send immediately;
 * if we aren't connected, it'll just be spooled up.
 *
 * @access protected
 * @param  {Object} data
 * @param  {Object} options_
 * @fires BeamSocket#sent
 * @fires BeamSocket#spooled
 */
BeamSocket.prototype.send = function (data, options_) {
    var options = options_ || {};

    if (this.isConnected() || options.force) {
        this.ws.send(JSON.stringify(data));
        this.emit('sent', data);
    } else if (data.method !== authMethod) {
        this._spool.push(data);
        this.emit('spooled', data);
    }
};

/**
 * auth sends a packet over the socket to authenticate with a chat server
 * and join a specified channel. If you wish to join anonymously, user
 * and authkey can be omitted.
 * @param  {Number} id
 * @param  {Number} [user]
 * @param  {String} [authkey]
 * @return {Promise}
 */
BeamSocket.prototype.auth = function (id, user, authkey) {
    this._authpacket = [id, user, authkey];

    // Two cases here: if we're already connected, with send the auth
    // packet immediately. Otherwise we wait for a `connected` event,
    // which won't be sent until after we re-authenticate.
    if (this.isConnected()) {
        return this.call('auth', [id, user, authkey]);
    }

    var self = this;
    return new BeamSocket.Promise(function (resolve) {
        self.once('authresult', resolve);
    });
};

/**
 * Runs a method on the socket. Returns a promise that is rejected or
 * resolved upon reply.
 * @access public
 * @param  {String} method
 * @param  {Array=[]} args_ Additional arguments to pass to the method.
 * @param  {Options={}} options_
 * @return {Promise}
 */
BeamSocket.prototype.call = function (method, args_, options_) {
    // Unpack arguments, so that the client can pass both, one, or
    // neither args or options.
    var options = options_, args = args_;
    if (!Array.isArray(args)) {
        options = args;
        args = [];
    }
    options = options || {};

    // Send out the data
    var id = this._callNo++, self = this;
    this.send({ type: 'method', method: method, arguments: args, id: id }, options);

    // Then create and return a promise that's resolved when we get
    // a reply, if we expect one to be given.
    if (options.noReply) {
        return;
    }

    return race([
        timeout(options.timeout || this._callTimeout),
        new BeamSocket.Promise(function (resolve, reject) {
            self._replies[id] = new Reply(resolve, reject);
        })
    ]).catch(TimeoutError, function (err) {
        delete self._replies[id];
        throw err;
    });
};

/**
 * Closes the websocket gracefully.
 * @access public
 */
BeamSocket.prototype.close = function () {
    if (this.ws) {
        this.ws.close();
        this.status = BeamSocket.CLOSING;
    } else {
        clearTimeout(this._reconnectTimeout);
        this.status = BeamSocket.CLOSED;
    }
};

/**
 * Promise class to use for various operations on the socket. The only
 * requirement is that this be an A+ promise implementation. By default
 * we try to require Bluebird, but you can switch it out with your own.
 * @type {Promise}
 */
try { BeamSocket.Promise = require('bluebird'); } catch (e) {}

/**
 * List of available statuses.
 * @readonly
 * @enum {Number}
 */
[
    /** We've not tried connecting yet */
    'IDLE',
    /** We successfully connected */
    'CONNECTED',
    /** The socket was is closing gracefully. */
    'CLOSING',
    /** The socket was closed gracefully. */
    'CLOSED',
    /** We're currently trying to connect */
    'CONNECTING'
].forEach(function (status, i) {
    BeamSocket[status] = i;
});

/**
 * A TimeoutError is thrown in call if we don't get a response from the
 * chat server within a certain interval.
 */
function TimeoutError() { Error.call(this); }
TimeoutError.prototype = Object.create(Error.prototype);

/**
 * Reference to the TimeoutError class.
 * @type {TimeoutError}
 */
BeamSocket.TimeoutError = TimeoutError;

module.exports = BeamSocket;
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2011 Andr Mekkawi <contact@andremekkawi.com>
 *
 * LICENSE
 *
 * This source file is subject to the MIT license in the file LICENSE.txt.
 * The license is also available at http://diskusagereport.sf.net/license.html
 */

;(function($){

$.extend(Controller.prototype, {

	// The default language, if the requested language is not supported.
	defaultLanguage: 'en-us',

	// The current language.
	language: null,

	// The language data.
	_languages: { },

	translate: function() {
		arguments = $.makeArray(arguments);

		var key = arguments.shift(),
			str = this._languages[this.language][key],
			parts = [], isTextOnly = true,
			argIndex, lastIndex = 0, match, re = new RegExp('{([0-9])}', 'g');

		// Throw an error because the key does not exist.
		if (!$.isString(str)) {
			throw "'" + key + "' does not exist in language file.";
		}
		else {
			// TODO: Remove toUpperCase() debug code.
			//str = str.toUpperCase();

			// Find all replacements in the string.
			while (match = re.exec(str)) {

				// Add any text between the last match and this one.
				if (lastIndex < match.index) {
					parts.push(document.createTextNode(str.substring(lastIndex, match.index)));
				}

				argIndex = parseInt(match[1]) - 1;

				if ($.isUndefined(arguments[argIndex]) || arguments[argIndex] == null) {
					throw 'Replacement not passed to Controller.translate() for ' + match[0] + ' in ' + key;
				}
				else if ($.isString(arguments[argIndex]) || $.isNumber(arguments[argIndex])) {
					parts.push(document.createTextNode(arguments[argIndex]+''));
				}
				else if (arguments[argIndex].toArray) {
					parts.push.apply(parts, arguments[argIndex].toArray());
					isTextOnly = false;
				}
				else {
					throw 'Invalid argument for Controller.translate(). Must be string or jQuery object.';
				}

				lastIndex = re.lastIndex;
			}

			if (lastIndex != str.length) {
				parts.push(document.createTextNode(str.substr(lastIndex)));
			}

			if (isTextOnly) {
				return $(parts).text();
			}
			else {
				return $(parts);
			}
		}
	},

	isLanguageSupported: function(lang) {
		return $.isDefined(this._languages[lang.toLowerCase()]);
	},

	addLanguage: function(lang) {
		if ($.isUndefined(this._languages[lang.toLowerCase()]))
			this._languages[lang.toLowerCase()] = 'load';
	},

	setLanguage: function(lang) {
		var self = this, result = true;

		lang = lang.toLowerCase();

		// Mark the language as unsupported.
		if (!this.isLanguageSupported(lang)) {
			result = "Unsupported language: " + lang;
		}
		else {
			// TODO: Support 'Accept-Language' header syntax by processing lang to fall back from something like 'eng-us' to 'eng'.

			// Retrieve the language data if it has not been loaded.
			if (this._languages[lang] == 'load') {
				$.ajax({
					url: 'lang/' + lang + '.json',
					async: false,
					dataType: 'json',
					error: function(xhr, msg, ex) {
						result = "Failed to load language file (" + msg + "): " + 'lang/' + lang + '.json';
					},
					success: function(data) {
						self.language = lang;
						self._languages[lang] = data;
						self._languageChangeStatic();
					}
				});
			}
			else {
				this._languageChangeStatic();
			}
		}

		return result;
	},

	_languageChangeStatic: function(part) {
		if (!part || part == 'title') {
			if (this.settings && this.settings.name) {
				$('#Title').html(this.translate('title_with_name', $('').text(this.settings.name))).show();
				document.title = this.translate('title_with_name', this.settings.name);
			}
			else {
				$('#Title').html(this.translate('title')).show();
				document.title = this.translate('title');
			}
		}

		if (!part || part == 'errors') {
			$('#ErrorsTitle span').html(this.translate('errors_title'));
			$('#ErrorsTitle div').html(this.translate('errors_close'));
		}

		if (!part || part == 'tree') {
			$('#TreeSort .tree-sortby-label span').html(this.translate('tree_header_name'));
			$('#TreeSort .tree-sortby-byte span').html(this.translate('tree_header_size'));
			$('#TreeSort .tree-sortby-num span').html(this.translate('tree_header_count'));
			$('#DirectoryTree li.ui-tree-files span.ui-tree-label').html(this.translate('tree_files_in_this_directory'));
		}

		if (!part || part == 'tabs') {
			$('#Tab_SubDirs').html(this.translate('tab_contents'));
			$('#Tab_Files').html(this.translate('tab_file_list'));
			$('#Tab_Modified').html(this.translate('tab_last_modified'));
			$('#Tab_Sizes').html(this.translate('tab_file_sizes'));
			$('#Tab_Types').html(this.translate('tab_file_types'));
			$('#Tab_Top100').html(this.translate('tab_top_100'));
		}

		if (!part || part == 'pager') {
			$('#Sections .pager-prev').html(this.translate('prev_page_button'));
			$('#Sections .pager-next').html(this.translate('next_page_button'));
		}

		if (!part || part == 'sections') {
			$('#SubDirs > thead .totals-sortby-label span').html(this.translate('contents_header_name'));
			$('#SubDirs > thead .totals-sortby-byte span').html(this.translate('contents_header_total_size'));
			$('#SubDirs > thead .totals-sortby-num span').html(this.translate('contents_header_file_count'));
			$('#SubDirs > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Files-SortBy-name span').html(this.translate('file_list_header_name'));
			$('#Files-SortBy-type span').html(this.translate('file_list_header_type'));
			$('#Files-SortBy-size span').html(this.translate('file_list_header_size'));
			$('#Files-SortBy-modified span').html(this.translate('file_list_header_modified'));

			$('#Modified > thead .totals-sortby-label span').html(this.translate('last_modified_header_age'));
			$('#Modified > thead .totals-sortby-byte span').html(this.translate('last_modified_header_total_size'));
			$('#Modified > thead .totals-sortby-num span').html(this.translate('last_modified_header_file_count'));
			$('#Modified > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Sizes > thead .totals-sortby-label span').html(this.translate('file_sizes_header_range'));
			$('#Sizes > thead .totals-sortby-byte span').html(this.translate('file_sizes_header_total_size'));
			$('#Sizes > thead .totals-sortby-num span').html(this.translate('file_sizes_header_file_count'));
			$('#Sizes > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Types > thead .totals-sortby-label span').html(this.translate('file_types_header_extension'));
			$('#Types > thead .totals-sortby-byte span').html(this.translate('file_types_header_total_size'));
			$('#Types > thead .totals-sortby-num span').html(this.translate('file_types_header_file_count'));
			$('#Types > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Top100-SortBy-name span').html(this.translate('top_100_header_name'));
			$('#Top100-SortBy-type span').html(this.translate('top_100_header_type'));
			$('#Top100-SortBy-size span').html(this.translate('top_100_header_size'));
			$('#Top100-SortBy-modified span').html(this.translate('top_100_header_modified'));
			$('#Top100-SortBy-path span').html(this.translate('top_100_header_path'));
		}

		if (!part || part == 'footer') {
			$('#Footer').html(this.translate(this.settings ? 'footer_with_created' : 'footer',
				this.settings ? this.settings.created.htmlencode() : null,
				$('Disk Usage Reports'),
				this._languages[this.language].language_name
			));
		}

		// Resize window in case the header was previously hidden.
		this.resizeWindow();
	}
});

})(jQuery);<commit_msg>* Controller.setLanguage() now handles AJAX exceptions which may arrise if the file:/// protocol is used.<commit_after>/*
 * Copyright (c) 2011 Andr Mekkawi <contact@andremekkawi.com>
 *
 * LICENSE
 *
 * This source file is subject to the MIT license in the file LICENSE.txt.
 * The license is also available at http://diskusagereport.sf.net/license.html
 */

;(function($){

$.extend(Controller.prototype, {

	// The default language, if the requested language is not supported.
	defaultLanguage: 'en-us',

	// The current language.
	language: null,

	// The language data.
	_languages: { },

	translate: function() {
		arguments = $.makeArray(arguments);

		var key = arguments.shift(),
			str = this._languages[this.language][key],
			parts = [], isTextOnly = true,
			argIndex, lastIndex = 0, match, re = new RegExp('{([0-9])}', 'g');

		// Throw an error because the key does not exist.
		if (!$.isString(str)) {
			throw "'" + key + "' does not exist in language file.";
		}
		else {
			// TODO: Remove toUpperCase() debug code.
			//str = str.toUpperCase();

			// Find all replacements in the string.
			while (match = re.exec(str)) {

				// Add any text between the last match and this one.
				if (lastIndex < match.index) {
					parts.push(document.createTextNode(str.substring(lastIndex, match.index)));
				}

				argIndex = parseInt(match[1]) - 1;

				if ($.isUndefined(arguments[argIndex]) || arguments[argIndex] == null) {
					throw 'Replacement not passed to Controller.translate() for ' + match[0] + ' in ' + key;
				}
				else if ($.isString(arguments[argIndex]) || $.isNumber(arguments[argIndex])) {
					parts.push(document.createTextNode(arguments[argIndex]+''));
				}
				else if (arguments[argIndex].toArray) {
					parts.push.apply(parts, arguments[argIndex].toArray());
					isTextOnly = false;
				}
				else {
					throw 'Invalid argument for Controller.translate(). Must be string or jQuery object.';
				}

				lastIndex = re.lastIndex;
			}

			if (lastIndex != str.length) {
				parts.push(document.createTextNode(str.substr(lastIndex)));
			}

			if (isTextOnly) {
				return $(parts).text();
			}
			else {
				return $(parts);
			}
		}
	},

	isLanguageSupported: function(lang) {
		return $.isDefined(this._languages[lang.toLowerCase()]);
	},

	addLanguage: function(lang) {
		if ($.isUndefined(this._languages[lang.toLowerCase()]))
			this._languages[lang.toLowerCase()] = 'load';
	},

	setLanguage: function(lang) {
		var self = this, result = true;

		lang = lang.toLowerCase();

		// Mark the language as unsupported.
		if (!this.isLanguageSupported(lang)) {
			result = "Unsupported language: " + lang;
		}
		else {
			// TODO: Support 'Accept-Language' header syntax by processing lang to fall back from something like 'eng-us' to 'eng'.

			// Retrieve the language data if it has not been loaded.
			if (this._languages[lang] == 'load') {
				try {
					$.ajax({
						url: 'lang/' + lang + '.json',
						async: false,
						dataType: 'json',
						error: function(xhr, msg, ex) {
							result = 'Failed to load language file (' + msg + '): lang/' + lang + '.json';
						},
						success: function(data) {
							if (data) {
								self.language = lang;
								self._languages[lang] = data;
								self._languageChangeStatic();
							}
						}
					});
				}
				catch (e) {
					// TODO: Handle exception when data is viewed via 'file:///' protocol.
					return 'Failed to load language file (AJAX exception): lang/' + lang + '.json';
				}
			}
			else {
				this._languageChangeStatic();
			}
		}

		return result;
	},

	_languageChangeStatic: function(part) {
		if (!part || part == 'title') {
			if (this.settings && this.settings.name) {
				$('#Title').html(this.translate('title_with_name', $('').text(this.settings.name))).show();
				document.title = this.translate('title_with_name', this.settings.name);
			}
			else {
				$('#Title').html(this.translate('title')).show();
				document.title = this.translate('title');
			}
		}

		if (!part || part == 'errors') {
			$('#ErrorsTitle span').html(this.translate('errors_title'));
			$('#ErrorsTitle div').html(this.translate('errors_close'));
		}

		if (!part || part == 'tree') {
			$('#TreeSort .tree-sortby-label span').html(this.translate('tree_header_name'));
			$('#TreeSort .tree-sortby-byte span').html(this.translate('tree_header_size'));
			$('#TreeSort .tree-sortby-num span').html(this.translate('tree_header_count'));
			$('#DirectoryTree li.ui-tree-files span.ui-tree-label').html(this.translate('tree_files_in_this_directory'));
		}

		if (!part || part == 'tabs') {
			$('#Tab_SubDirs').html(this.translate('tab_contents'));
			$('#Tab_Files').html(this.translate('tab_file_list'));
			$('#Tab_Modified').html(this.translate('tab_last_modified'));
			$('#Tab_Sizes').html(this.translate('tab_file_sizes'));
			$('#Tab_Types').html(this.translate('tab_file_types'));
			$('#Tab_Top100').html(this.translate('tab_top_100'));
		}

		if (!part || part == 'pager') {
			$('#Sections .pager-prev').html(this.translate('prev_page_button'));
			$('#Sections .pager-next').html(this.translate('next_page_button'));
		}

		if (!part || part == 'sections') {
			$('#SubDirs > thead .totals-sortby-label span').html(this.translate('contents_header_name'));
			$('#SubDirs > thead .totals-sortby-byte span').html(this.translate('contents_header_total_size'));
			$('#SubDirs > thead .totals-sortby-num span').html(this.translate('contents_header_file_count'));
			$('#SubDirs > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Files-SortBy-name span').html(this.translate('file_list_header_name'));
			$('#Files-SortBy-type span').html(this.translate('file_list_header_type'));
			$('#Files-SortBy-size span').html(this.translate('file_list_header_size'));
			$('#Files-SortBy-modified span').html(this.translate('file_list_header_modified'));

			$('#Modified > thead .totals-sortby-label span').html(this.translate('last_modified_header_age'));
			$('#Modified > thead .totals-sortby-byte span').html(this.translate('last_modified_header_total_size'));
			$('#Modified > thead .totals-sortby-num span').html(this.translate('last_modified_header_file_count'));
			$('#Modified > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Sizes > thead .totals-sortby-label span').html(this.translate('file_sizes_header_range'));
			$('#Sizes > thead .totals-sortby-byte span').html(this.translate('file_sizes_header_total_size'));
			$('#Sizes > thead .totals-sortby-num span').html(this.translate('file_sizes_header_file_count'));
			$('#Sizes > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Types > thead .totals-sortby-label span').html(this.translate('file_types_header_extension'));
			$('#Types > thead .totals-sortby-byte span').html(this.translate('file_types_header_total_size'));
			$('#Types > thead .totals-sortby-num span').html(this.translate('file_types_header_file_count'));
			$('#Types > tfoot td:eq(0)').html(this.translate('table_footer_total'));

			$('#Top100-SortBy-name span').html(this.translate('top_100_header_name'));
			$('#Top100-SortBy-type span').html(this.translate('top_100_header_type'));
			$('#Top100-SortBy-size span').html(this.translate('top_100_header_size'));
			$('#Top100-SortBy-modified span').html(this.translate('top_100_header_modified'));
			$('#Top100-SortBy-path span').html(this.translate('top_100_header_path'));
		}

		if (!part || part == 'footer') {
			$('#Footer').html(this.translate(this.settings ? 'footer_with_created' : 'footer',
				this.settings ? this.settings.created.htmlencode() : null,
				$('Disk Usage Reports'),
				this._languages[this.language].language_name
			));
		}

		// Resize window in case the header was previously hidden.
		this.resizeWindow();
	}
});

})(jQuery);<|endoftext|>


<commit_before>var Moment = require('moment-timezone');
var Async = require('async');
var Request = require('superagent');
var Pie = require('cli-pie');
var _ = require('lodash');

exports.getTimesheets = getTimesheets;

function getTimesheets(callback, auth, count, startDate, endDate, verbose, graph) {
    var startDateObj = Moment(startDate).tz('Europe/Zurich');
    var endDateObj = Moment(endDate).tz('Europe/Zurich');
    Request
        .get('https://zebra.liip.ch/api/v2/timesheets')
        .query({'token': auth })
        .query({'start_date': startDateObj.format('YYYY-MM-DD')})
        .query({'end_date': endDateObj.format('YYYY-MM-DD')})
        .end(function(err, res) {
            if (err) {
                callback('The Zebra API returned an error: ' + err);
                return;
            }
            var list = _.map(res.body.data.list, function(entry) {
                var msg = {
                    'project': entry.alias || entry.project_name,
                    'time': entry.time,
                    'text': entry.description,
                    'timestamp': Moment(entry.date).startOf('day').format('X'),
                    'type': 'zebra',
                    'comment': true,
                    'graph': {'label': entry.project_name + ' - ' + entry.time, 'value': parseFloat(entry.time) }
                };
                return msg;
            });
            if (graph) {
                var pieArgs = _.map(list, function(msg) {
                    return msg.graph;
                });
                var pie = new Pie(10, pieArgs, { legend: true, display_total: true, total_label: "Total Hours" });

                var msg = {
                    'raw': pie.toString(),
                    'timestamp': startDateObj.startOf('day').format('X'),
                    'type': 'zebra',
                };
                callback(null, [msg]);
            } else {
                callback(null, list);
            }
        });
}
<commit_msg>Change name of api field for taxi alias<commit_after>var Moment = require('moment-timezone');
var Async = require('async');
var Request = require('superagent');
var Pie = require('cli-pie');
var _ = require('lodash');

exports.getTimesheets = getTimesheets;

function getTimesheets(callback, auth, count, startDate, endDate, verbose, graph) {
    var startDateObj = Moment(startDate).tz('Europe/Zurich');
    var endDateObj = Moment(endDate).tz('Europe/Zurich');
    Request
        .get('https://zebra.liip.ch/api/v2/timesheets')
        .query({'token': auth })
        .query({'start_date': startDateObj.format('YYYY-MM-DD')})
        .query({'end_date': endDateObj.format('YYYY-MM-DD')})
        .end(function(err, res) {
            if (err) {
                callback('The Zebra API returned an error: ' + err);
                return;
            }
            var list = _.map(res.body.data.list, function(entry) {
                var msg = {
                    'project': entry.occupation_alias || entry.project_name,
                    'time': entry.time,
                    'text': entry.description,
                    'timestamp': Moment(entry.date).startOf('day').format('X'),
                    'type': 'zebra',
                    'comment': true,
                    'graph': {'label': entry.project_name + ' - ' + entry.time, 'value': parseFloat(entry.time) }
                };
                return msg;
            });
            if (graph) {
                var pieArgs = _.map(list, function(msg) {
                    return msg.graph;
                });
                var pie = new Pie(10, pieArgs, { legend: true, display_total: true, total_label: "Total Hours" });

                var msg = {
                    'raw': pie.toString(),
                    'timestamp': startDateObj.startOf('day').format('X'),
                    'type': 'zebra',
                };
                callback(null, [msg]);
            } else {
                callback(null, list);
            }
        });
}
<|endoftext|>


<commit_before>/*
 * gstorage
 * https://github.com/goliatone/gstorage
 * Created with gbase.
 * Copyright (c) 2014 goliatone
 * Licensed under the MIT license.
 */
/* jshint strict: false, plusplus: true */
/*global define: false, require: false, module: false, exports: false */
(function(root, name, deps, factory) {
    "use strict";
    // Node
    if (typeof deps === 'function') {
        factory = deps;
        deps = [];
    }

    if (typeof exports === 'object') {
        module.exports = factory.apply(root, deps.map(require));
    } else if (typeof define === 'function' && 'amd' in define) {
        //require js, here we assume the file is named as the lower
        //case module name.
        define(name.toLowerCase(), deps, factory);
    } else {
        // Browser
        var d, i = 0,
            global = root,
            old = global[name],
            mod;
        while ((d = deps[i]) !== undefined) deps[i++] = root[d];
        global[name] = mod = factory.apply(global, deps);
        //Export no 'conflict module', aliases the module.
        mod.noConflict = function() {
            global[name] = old;
            return mod;
        };
    }
}(this, 'gstorage', ['extend'], function(extend) {

    /**
     * Extend method.
     * @param  {Object} target Source object
     * @return {Object}        Resulting object from
     *                         meging target to params.
     */
    var _extend = extend;

    /**
     * Shim console, make sure that if no console
     * available calls do not generate errors.
     * @return {Object} Console shim.
     */
    var _shimConsole = function(con) {

        if (con) return con;

        con = {};
        var empty = {},
            noop = function() {},
            properties = 'memory'.split(','),
            methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' +
                'groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,' +
                'table,time,timeEnd,timeStamp,trace,warn').split(','),
            prop,
            method;

        while (method = methods.pop()) con[method] = noop;
        while (prop = properties.pop()) con[prop] = empty;

        return con;
    };

    ///////////////////////////////////////////////////
    // CONSTRUCTOR
    ///////////////////////////////////////////////////

    var options = {
        hashLength: 32,
        storeKey: '_GST_',
        storeID: '_gstore_default_',
        autoinitialize: true,
        buildDefaultStore: function() {
            //Use NullStore
            return new WebSQLStore();
        },
        makeStoreId: function() {
            return this.storeKey + window.location.hostname;
        }

        //store config
        // storeName: '',
        // storeTable: '',
        // storeVersion: ''

    };

    /**
     * GStorage constructor
     *
     * @param  {object} config Configuration object.
     */
    var GStorage = function(config) {
        config = config || {};

        config = _extend({}, this.constructor.DEFAULTS, config);

        if (config.autoinitialize) this.init(config);
    };

    GStorage.name = GStorage.prototype.name = 'GStorage';

    GStorage.VERSION = '0.0.0';

    /**
     * Make default options available so we
     * can override.
     */
    GStorage.DEFAULTS = options;

    GStorage.use = function(ext) {
        _extend(GStorage.prototype, ext);
    };

    ///////////////////////////////////////////////////
    // PRIVATE METHODS
    ///////////////////////////////////////////////////

    GStorage.prototype.init = function(config) {
        if (this.initialized) return this.logger.warn('Already initialized');
        this.initialized = true;

        console.log('GStorage: Init!');
        _extend(this, config);

        //TODO: Use promises!!!!!!
        this.store = this.buildDefaultStore();
        this.store.onError = this.onError.bind(this);
        this.store.onSuccess = this.onSuccess.bind(this);
        this.store.onConnected = this.onConnected.bind(this);

        return this;
    };

    GStorage.prototype.onError = function(e) {
        this.logger.error(e);
    };

    GStorage.prototype.onConnected = function(e) {
        this.logger.info('On connected', e, this);
        this.emit('connected');
    };

    GStorage.prototype.onSuccess = function(e) {
        this.logger.info('On success', e, this);
        this.emit('success', e);
    };

    ////////////////////////////////////////////
    /// DATA ACCESSSOR MEHTODS
    ///
    ////////////////////////////////////////////
    GStorage.prototype.get = function(key, def) {
        return this.store.get(key, def);
    };

    GStorage.prototype.set = function(key, value) {
        return this.store.set(key, value).then(function(){
            this.emit('change', {
                key: key,
                value: value
            });
        }.bind(this));
    };

    GStorage.prototype.del = function(key) {
        return this.store.del(key).then('success', function(result) {
            this.emit('change', {
                key: key,
                action: 'delete',
                value: result
            });
        }.bind(this));
    };

    //TODO: This might now make sense
    GStorage.prototype.has = function(key) {
        return this.store.has(key);
    };

    GStorage.prototype.key = function(key) {
        //TODO: Do we want to transform key?
        return key;
    };

    ////////////////////////////////////////////
    /// STORE DELEGATE METHODS
    ///
    ////////////////////////////////////////////
    /////TODO: This might now make sense
    GStorage.prototype.maxSize = function() {
        return this.store.maxSize();
    };

    //TODO: This might now make sense
    GStorage.prototype.size = function() {
        return this.store.size();
    };
    //TODO: This might now make sense
    GStorage.prototype.clear = function() {
        return this.store.clear();
        return this;
    };
    //TODO: This might now make sense
    GStorage.prototype.purge = function() {
        var args = Array.prototype.slice.call(arguments);
        this.store.purge(args);
        return this;
    };
    //TODO: This might now make sense
    GStorage.prototype.setTTL = function(key, time) {
        throw new Error('TODO: This needs to be implemented!!!');
    };

    ////////////////////////////////////////////
    /// UTILITY METHODS
    ///
    ////////////////////////////////////////////
    GStorage.prototype.now = function() {
        //Unix timestamp
        return new Date().getTime() / 1000 | 0;
    };

    /**
     * Get Date from timestamp
     * @param  {int} t  Unit timestamp.
     * @return {Date}   Date
     */
    GStorage.prototype.dateFromTimestamp = function(t) {
        //From unit timestamp to date.
        return new Date(parseInt(t) * 1000);
    };

    GStorage.prototype.hash = function(key) {
        var hash2 =
            hash1 = (5381 << 16) + 5381,
            pos = 0;
        while (pos < key.length) {
            hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ key.charCodeAt(pos);
            if (pos == key.length - 1) break;
            hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ key.charCodeAt(pos + 1);
            pos += 2;
        }

        return (hash1 + (hash2 * 1566083941)).toString();
    };

    /**
     * Logger method, meant to be implemented by
     * mixin. As a placeholder, we use console if available
     * or a shim if not present.
     */
    GStorage.prototype.logger = _shimConsole(console);

    /**
     * PubSub emit method stub.
     */
    GStorage.prototype.emit = function() {
        this.logger.warn(GStorage, 'emit method is not implemented', arguments);
    };

    return GStorage;
}));<commit_msg>SRC: Refactor GStorage<commit_after>/*
 * gstorage
 * https://github.com/goliatone/gstorage
 * Created with gbase.
 * Copyright (c) 2014 goliatone
 * Licensed under the MIT license.
 */
/* jshint strict: false, plusplus: true */
/*global define: false, require: false, module: false, exports: false */
(function(root, name, deps, factory) {
    "use strict";
    // Node
    if (typeof deps === 'function') {
        factory = deps;
        deps = [];
    }

    if (typeof exports === 'object') {
        module.exports = factory.apply(root, deps.map(require));
    } else if (typeof define === 'function' && 'amd' in define) {
        //require js, here we assume the file is named as the lower
        //case module name.
        define(name.toLowerCase(), deps, factory);
    } else {
        // Browser
        var d, i = 0,
            global = root,
            old = global[name],
            mod;
        while ((d = deps[i]) !== undefined) deps[i++] = root[d];
        global[name] = mod = factory.apply(global, deps);
        //Export no 'conflict module', aliases the module.
        mod.noConflict = function() {
            global[name] = old;
            return mod;
        };
    }
}(this, 'gstorage', ['extend'], function(extend) {

    /**
     * Extend method.
     * @param  {Object} target Source object
     * @return {Object}        Resulting object from
     *                         meging target to params.
     */
    var _extend = extend;

    /**
     * Shim console, make sure that if no console
     * available calls do not generate errors.
     * @return {Object} Console shim.
     */
    var _shimConsole = function(con) {

        if (con) return con;

        con = {};
        var empty = {},
            noop = function() {},
            properties = 'memory'.split(','),
            methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' +
                'groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,' +
                'table,time,timeEnd,timeStamp,trace,warn').split(','),
            prop,
            method;

        while (method = methods.pop()) con[method] = noop;
        while (prop = properties.pop()) con[prop] = empty;

        return con;
    };

    ///////////////////////////////////////////////////
    // CONSTRUCTOR
    ///////////////////////////////////////////////////

    var OPTIONS = {
        hashLength: 32,
        storeKey: '_GST_',
        storeID: '_gstore_default_',
        autoinitialize: true,
        buildDefaultStore: function() {
            //Use NullStore
            return new WebSQLStore();
        },
        makeStoreId: function() {
            return this.storeKey + window.location.hostname;
        }
    };

    /**
     * GStorage constructor
     *
     * @param  {object} config Configuration object.
     */
    var GStorage = function(config) {
        config = config || {};

        config = _extend({}, this.constructor.DEFAULTS, config);

        if (config.autoinitialize) this.init(config);
    };

    GStorage.name = GStorage.prototype.name = 'GStorage';

    GStorage.VERSION = '0.0.0';

    /**
     * Make default options available so we
     * can override.
     */
    GStorage.DEFAULTS = OPTIONS;

    GStorage.use = function(ext) {
        _extend(GStorage.prototype, ext);
    };

    ///////////////////////////////////////////////////
    // PRIVATE METHODS
    ///////////////////////////////////////////////////

    GStorage.prototype.init = function(config) {
        if (this.initialized) return this.logger.warn('Already initialized');
        this.initialized = true;

        console.log('GStorage: Init!');
        _extend(this, config);

        //TODO: Use promises!!!!!!
        this.store = this.buildDefaultStore();
        this.store.onError = this.onError.bind(this);
        this.store.onSuccess = this.onSuccess.bind(this);
        this.store.onConnected = this.onConnected.bind(this);

        return this;
    };

    GStorage.prototype.onError = function(e) {
        this.logger.error(e);
    };

    GStorage.prototype.onConnected = function(e) {
        this.logger.info('On connected', e, this);
        this.emit('connected');
    };

    GStorage.prototype.onSuccess = function(e) {
        this.logger.info('On success', e, this);
        this.emit('success', e);
    };

    ////////////////////////////////////////////
    /// DATA ACCESSSOR MEHTODS
    ///
    ////////////////////////////////////////////
    GStorage.prototype.get = function(key, def) {
        return this.store.get(key, def);
    };

    GStorage.prototype.set = function(key, value) {
        return this.store.set(key, value).then(function() {
            this.emit('change', {
                key: key,
                value: value
            });
        }.bind(this));
    };

    GStorage.prototype.del = function(key) {
        return this.store.del(key).then('success', function(result) {
            this.emit('change', {
                key: key,
                action: 'delete',
                value: result
            });
        }.bind(this));
    };

    GStorage.prototype.key = function(key) {
        //TODO: Do we want to transform key?
        return key;
    };

    ////////////////////////////////////////////
    /// STORE DELEGATE METHODS
    ///
    ////////////////////////////////////////////

    //TODO: This might now make sense
    GStorage.prototype.clear = function() {
        return this.store.clear();
        return this;
    };
    //TODO: This might now make sense
    GStorage.prototype.purge = function() {
        var args = Array.prototype.slice.call(arguments);
        this.store.purge(args);
        return this;
    };
    //TODO: This might now make sense
    GStorage.prototype.setTTL = function(key, time) {
        throw new Error('TODO: This needs to be implemented!!!');
    };

    ////////////////////////////////////////////
    /// UTILITY METHODS
    ///
    ////////////////////////////////////////////
    GStorage.prototype.now = function() {
        //Unix timestamp
        return new Date().getTime() / 1000 | 0;
    };

    /**
     * Get Date from timestamp
     * @param  {int} t  Unit timestamp.
     * @return {Date}   Date
     */
    GStorage.prototype.dateFromTimestamp = function(t) {
        //From unit timestamp to date.
        return new Date(parseInt(t) * 1000);
    };

    GStorage.prototype.hash = function(key) {
        var hash2 =
            hash1 = (5381 << 16) + 5381,
            pos = 0;
        while (pos < key.length) {
            hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ key.charCodeAt(pos);
            if (pos == key.length - 1) break;
            hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ key.charCodeAt(pos + 1);
            pos += 2;
        }

        return (hash1 + (hash2 * 1566083941)).toString();
    };

    /**
     * Logger method, meant to be implemented by
     * mixin. As a placeholder, we use console if available
     * or a shim if not present.
     */
    GStorage.prototype.logger = _shimConsole(console);

    /**
     * PubSub emit method stub.
     */
    GStorage.prototype.emit = function() {
        this.logger.warn(GStorage, 'emit method is not implemented', arguments);
    };

    return GStorage;
}));<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.graphics',
  name: 'Gauge',

  extendsModel: 'foam.graphics.CView',

  properties: [
    {
      name: 'label',
      help: 'The label to appear on the meter.',
    },
    {
      model_: 'FloatProperty',
      name: 'data',
      defaultValue: 1
    },
    {
      model_: 'FloatProperty',
      name:  'maxValue',
      label: 'Max. Value',
      help: 'Maximum Value.',
      defaultValue: 10
    },
    {
      name: 'width',
      defaultValue: 280
    },
    {
      name: 'height',
      defaultValue: 180
    }
  ],

  methods: {
    /*
    toHTML: function() {
      this.canvasView = Canvas.create({background:'rgba(0,0,0,0)',width:this.width+4, height:this.height+4});
      this.canvasView.addChild(this);
      return this.canvasView.toHTML();
    },
    */

    /*
    link: function(powerInfo) {
      Events.follow(powerInfo.propertyValue('watts'), this.propertyValue('value'));
    },
    */

    rotateToValue: function(x, y, value) {
      var c = this.canvas;
      var a = value / this.maxValue * (3.0*Math.PI/5.0)-1.5*Math.PI/5.0;

      c.save();

      c.translate(x,y);
      c.rotate(a);
      c.translate(-x,-y);
    },

    paintSelf: function() {
      var c = this.canvas;

      var width  = this.width - 4;
      var height = this.height - 4;
      var r = height-30;
      var x = width/2+1;
      var y = height-8;

      c.shadowOffsetX = 0;
      c.shadowOffsetY = 0;
      c.shadowBlur = 0;

      c.fillStyle = "black";
      c.fillRect(0,0,width+10,height+10);
      c.fillStyle = "white";
      c.fillRect(2,2,width,height);

      c.strokeStyle = 'black';
      c.lineWidth = 2;
      c.beginPath();
      c.arc(x, y, r, -Math.PI/5.0, -4.0*Math.PI/5.0, true);
      c.stroke();

      c.fillStyle = 'black';
      c.textAlign = 'center';
      c.font = '16pt sans-serif';
      c.fillText(this.label, x, y-30,60);

      this.rotateToValue(x,y,this.data);

      c.lineWidth = 1;
      c.shadowOffsetX = 2;
      c.shadowOffsetY = 2;
      c.shadowBlur = 1;
      c.shadowColor = "gray";

      c.beginPath();
      c.moveTo(width/2+3, height-10);
      c.lineTo(width/2+3, height+5-r);
      c.stroke();

      c.fillStyle='white';
      c.fillRect(width/2+3, height-10, 1, -r+15);
      c.strokeStyle = 'red';
      c.beginPath();
      c.moveTo(width/2+3, height+5-r);
      c.lineTo(width/2+3, height-20-r);
      c.stroke();
      c.fillStyle='white';
      c.fillRect(width/2+3, height+5-r, 1, -30);
      c.restore();

      for ( var i = 0 ; i <= this.maxValue ; i++ ) {
        this.rotateToValue(x,y,i);

        c.fillStyle = 'black';
        c.textAlign = 'center';
        c.font = "10pt sans-serif";
        c.fillText(i, x, y-r-5);

        c.beginPath();
        c.moveTo(x, y-r);
        c.lineTo(x, y-r-4);
        c.stroke();

        c.restore();
      }

      for ( var i = 0 ; i < this.maxValue ; i++ ) {
        this.rotateToValue(x,y,i+0.5);

        c.lineWidth = 1;
        c.beginPath();
        c.moveTo(x, y-r);
        c.lineTo(x, y-r-3);
        c.stroke();

        c.restore();
      }
    }
  }
});
<commit_msg>Gauge repaints on data change.<commit_after>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.graphics',
  name: 'Gauge',

  extendsModel: 'foam.graphics.CView',

  properties: [
    {
      name: 'label',
      help: 'The label to appear on the meter.',
    },
    {
      model_: 'FloatProperty',
      name: 'data',
      postSet: function(o, n) { if ( this.view ) this.view.paint(); },
      defaultValue: 1
    },
    {
      model_: 'FloatProperty',
      name:  'maxValue',
      label: 'Max. Value',
      help: 'Maximum Value.',
      defaultValue: 10
    },
    {
      name: 'width',
      defaultValue: 280
    },
    {
      name: 'height',
      defaultValue: 180
    }
  ],

  methods: {
    /*
    toHTML: function() {
      this.canvasView = Canvas.create({background:'rgba(0,0,0,0)',width:this.width+4, height:this.height+4});
      this.canvasView.addChild(this);
      return this.canvasView.toHTML();
    },
    */

    /*
    link: function(powerInfo) {
      Events.follow(powerInfo.propertyValue('watts'), this.propertyValue('value'));
    },
    */

    rotateToValue: function(x, y, value) {
      var c = this.canvas;
      var a = value / this.maxValue * (3.0*Math.PI/5.0)-1.5*Math.PI/5.0;

      c.save();

      c.translate(x,y);
      c.rotate(a);
      c.translate(-x,-y);
    },

    paintSelf: function() {
      var c = this.canvas;

      var width  = this.width - 4;
      var height = this.height - 4;
      var r = height-30;
      var x = width/2+1;
      var y = height-8;

      c.shadowOffsetX = 0;
      c.shadowOffsetY = 0;
      c.shadowBlur = 0;

      c.fillStyle = "black";
      c.fillRect(0,0,width+10,height+10);
      c.fillStyle = "white";
      c.fillRect(2,2,width,height);

      c.strokeStyle = 'black';
      c.lineWidth = 2;
      c.beginPath();
      c.arc(x, y, r, -Math.PI/5.0, -4.0*Math.PI/5.0, true);
      c.stroke();

      c.fillStyle = 'black';
      c.textAlign = 'center';
      c.font = '16pt sans-serif';
      c.fillText(this.label, x, y-30,60);

      this.rotateToValue(x,y,this.data);

      c.lineWidth = 1;
      c.shadowOffsetX = 2;
      c.shadowOffsetY = 2;
      c.shadowBlur = 1;
      c.shadowColor = "gray";

      c.beginPath();
      c.moveTo(width/2+3, height-10);
      c.lineTo(width/2+3, height+5-r);
      c.stroke();

      c.fillStyle='white';
      c.fillRect(width/2+3, height-10, 1, -r+15);
      c.strokeStyle = 'red';
      c.beginPath();
      c.moveTo(width/2+3, height+5-r);
      c.lineTo(width/2+3, height-20-r);
      c.stroke();
      c.fillStyle='white';
      c.fillRect(width/2+3, height+5-r, 1, -30);
      c.restore();

      for ( var i = 0 ; i <= this.maxValue ; i++ ) {
        this.rotateToValue(x,y,i);

        c.fillStyle = 'black';
        c.textAlign = 'center';
        c.font = "10pt sans-serif";
        c.fillText(i, x, y-r-5);

        c.beginPath();
        c.moveTo(x, y-r);
        c.lineTo(x, y-r-4);
        c.stroke();

        c.restore();
      }

      for ( var i = 0 ; i < this.maxValue ; i++ ) {
        this.rotateToValue(x,y,i+0.5);

        c.lineWidth = 1;
        c.beginPath();
        c.moveTo(x, y-r);
        c.lineTo(x, y-r-3);
        c.stroke();

        c.restore();
      }
    }
  }
});
<|endoftext|>


<commit_before>/* jshint esversion: 6, curly: true, eqeqeq: true, forin: true */

/***********************************************************************************
* Title: Hamsters.js                                                               *
* Description: 100% Vanilla Javascript Multithreading & Parallel Execution Library *
* Author: Austin K. Smith                                                          *
* Contact: austin@asmithdev.com                                                    *
* Copyright: 2015 Austin K. Smith - austin@asmithdev.com                           *
* License: Artistic License 2.0                                                    *
***********************************************************************************/

import hamstersVersion from './core/version';
import hamstersHabitat from './core/habitat';
import hamstersPool from './core/pool';
import hamstersData from './core/data';
import hamstersWheel from './core/wheel';
import hamstersLogger from './core/logger';
import hamstersMemoizer from './core/memoizer';

'use strict';

class hamstersjs {

  /**
  * @constructor
  * @function constructor - Sets properties for this class
  */
  constructor() {
    this.persistence = true;
    this.memoize = false;
    this.atomics = false;
    this.debug = false;
    this.version = hamstersVersion;
    this.maxThreads = hamstersHabitat.logicalThreads;
    this.habitat = hamstersHabitat;
    this.data = hamstersData;
    this.pool = hamstersPool;
    this.logger = hamstersLogger;
    this.memoizer = hamstersMemoizer;
    this.run = this.hamstersRun;
    this.promise = this.hamstersPromise;
    this.init = this.initializeLibrary;
  }

  /**
  * @function initializeLibrary - Prepares & initializes Hamsters.js library
  * @param {object} startOptions - Provided library functionality options
  */
  initializeLibrary(startOptions) {
    this.logger.info(`Preparing the hamster wheels & readying hamsters`);
    if (typeof startOptions !== 'undefined') {
      this.processStartOptions(startOptions);
    }
    hamstersPool.spawnHamsters(this.persistence, hamstersPool.selectHamsterWheel(), this.maxThreads);
    delete this.init;
  }

  /**
  * @function processStartOptions - Adjusts library functionality based on provided options
  * @param {object} startOptions - Provided library functionality options
  */
  processStartOptions(startOptions) {
    let habitatKeys = ['worker', 'sharedWorker', 'legacy'];
    for (var key in startOptions) {
      if (startOptions.hasOwnProperty(key)) {
        if (habitatKeys.indexOf(key.toLowerCase()) !== -1) {
          this.habitat[key] = startOptions[key];
        } else {
          this[key] = startOptions[key];
        }
      }
    }
    // Ensure legacy mode is disabled when we pass a third party worker library
    if(typeof this.habitat.Worker === 'function') {
      this.habitat.legacy = false;
    }
  }

  /**
  * @constructor
  * @function hamstersTask - Constructs a new task object from provided arguments
  * @param {object} params - Provided library execution options
  * @param {function} functionToRun - Function to execute
  * @param {object} scope - Reference to main library context
  * @return {object} new Hamsters.js task
  */
  hamstersTask(params, functionToRun, scope) {
    this.id = scope.pool.tasks.length;
    this.count = 0;
    this.aggregate = (params.aggregate || false);
    this.output = [];
    this.workers = [];
    this.memoize = (params.memoize || false);
    this.dataType = (params.dataType ? params.dataType.toLowerCase() : null);
    this.input = params;
    // Do not modify function if we're running on the main thread for legacy fallback
    if(hamstersHabitat.legacy) {
      this.threads = 1;
      this.input.hamstersJob = functionToRun;
    } else {
      this.threads = (params.threads || 1);
      this.input.hamstersJob = scope.data.prepareJob(functionToRun);
    }
  }

  /**
  * @async
  * @function hamstersPromise - Calls library functionality using async promises
  * @param {object} params - Provided library execution options
  * @param {function} functionToRun - Function to execute
  * @return {array} Results from functionToRun.
  */
  hamstersPromise(params, functionToRun) {
    return new Promise((resolve, reject) => {
      let task = new this.hamstersTask(params, functionToRun, this);
      this.pool.scheduleTask(task, this.persistence, scaffold, this.maxThreads).then((results) => {
        resolve(results);
      }).catch((error) => {
        hamstersLogger.error(error.messsage, reject);
      });
    });
  }

  /**
  * @async
  * @function hamstersRun - Calls library functionality using async callbacks
  * @param {object} params - Provided library execution options
  * @param {function} functionToRun - Function to execute
  * @param {function} onSuccess - Function to call upon successful execution
  * @param {function} onError - Function to call upon execution failure
  * @return {array} Results from functionToRun.
  */
  hamstersRun(params, functionToRun, onSuccess, onError) {
    let task = new this.hamstersTask(params, functionToRun, this);
    let scaffold = hamstersPool.selectHamsterWheel();
    this.pool.scheduleTask(task, this.persistence, scaffold, this.maxThreads).then((results) => {
      onSuccess(results);
    }).catch((error) => {
      hamstersLogger.error(error.messsage, onError);
    });
  }
}

var hamsters = new hamstersjs();

if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  module.exports = hamsters;
}<commit_msg>Add habitat overrides<commit_after>/* jshint esversion: 6, curly: true, eqeqeq: true, forin: true */

/***********************************************************************************
* Title: Hamsters.js                                                               *
* Description: 100% Vanilla Javascript Multithreading & Parallel Execution Library *
* Author: Austin K. Smith                                                          *
* Contact: austin@asmithdev.com                                                    *
* Copyright: 2015 Austin K. Smith - austin@asmithdev.com                           *
* License: Artistic License 2.0                                                    *
***********************************************************************************/

import hamstersVersion from './core/version';
import hamstersHabitat from './core/habitat';
import hamstersPool from './core/pool';
import hamstersData from './core/data';
import hamstersWheel from './core/wheel';
import hamstersLogger from './core/logger';
import hamstersMemoizer from './core/memoizer';

'use strict';

class hamstersjs {

  /**
  * @constructor
  * @function constructor - Sets properties for this class
  */
  constructor() {
    this.persistence = true;
    this.memoize = false;
    this.atomics = false;
    this.debug = false;
    this.version = hamstersVersion;
    this.maxThreads = hamstersHabitat.logicalThreads;
    this.habitat = hamstersHabitat;
    this.data = hamstersData;
    this.pool = hamstersPool;
    this.logger = hamstersLogger;
    this.memoizer = hamstersMemoizer;
    this.run = this.hamstersRun;
    this.promise = this.hamstersPromise;
    this.init = this.initializeLibrary;
  }

  /**
  * @function initializeLibrary - Prepares & initializes Hamsters.js library
  * @param {object} startOptions - Provided library functionality options
  */
  initializeLibrary(startOptions) {
    this.logger.info(`Preparing the hamster wheels & readying hamsters`);
    if (typeof startOptions !== 'undefined') {
      this.processStartOptions(startOptions);
    }
    hamstersPool.spawnHamsters(this.persistence, hamstersPool.selectHamsterWheel(), this.maxThreads);
    delete this.init;
  }

  /**
  * @function processStartOptions - Adjusts library functionality based on provided options
  * @param {object} startOptions - Provided library functionality options
  */
  processStartOptions(startOptions) {
    let habitatKeys = [
      'worker', 'sharedworker',
      'legacy', 'webworker',
      'shell'
    ];
    for (var key in startOptions) {
      if (startOptions.hasOwnProperty(key)) {
        if (habitatKeys.indexOf(key.toLowerCase()) !== -1) {
          this.habitat[key] = startOptions[key];
        } else {
          this[key] = startOptions[key];
        }
      }
    }
    // Ensure legacy mode is disabled when we pass a third party worker library
    if(typeof this.habitat.Worker === 'function') {
      this.habitat.legacy = false;
    }
  }

  /**
  * @constructor
  * @function hamstersTask - Constructs a new task object from provided arguments
  * @param {object} params - Provided library execution options
  * @param {function} functionToRun - Function to execute
  * @param {object} scope - Reference to main library context
  * @return {object} new Hamsters.js task
  */
  hamstersTask(params, functionToRun, scope) {
    this.id = scope.pool.tasks.length;
    this.count = 0;
    this.aggregate = (params.aggregate || false);
    this.output = [];
    this.workers = [];
    this.memoize = (params.memoize || false);
    this.dataType = (params.dataType ? params.dataType.toLowerCase() : null);
    this.input = params;
    // Do not modify function if we're running on the main thread for legacy fallback
    if(hamstersHabitat.legacy) {
      this.threads = 1;
      this.input.hamstersJob = functionToRun;
    } else {
      this.threads = (params.threads || 1);
      this.input.hamstersJob = scope.data.prepareJob(functionToRun);
    }
  }

  /**
  * @async
  * @function hamstersPromise - Calls library functionality using async promises
  * @param {object} params - Provided library execution options
  * @param {function} functionToRun - Function to execute
  * @return {array} Results from functionToRun.
  */
  hamstersPromise(params, functionToRun) {
    return new Promise((resolve, reject) => {
      let task = new this.hamstersTask(params, functionToRun, this);
      this.pool.scheduleTask(task, this.persistence, scaffold, this.maxThreads).then((results) => {
        resolve(results);
      }).catch((error) => {
        hamstersLogger.error(error.messsage, reject);
      });
    });
  }

  /**
  * @async
  * @function hamstersRun - Calls library functionality using async callbacks
  * @param {object} params - Provided library execution options
  * @param {function} functionToRun - Function to execute
  * @param {function} onSuccess - Function to call upon successful execution
  * @param {function} onError - Function to call upon execution failure
  * @return {array} Results from functionToRun.
  */
  hamstersRun(params, functionToRun, onSuccess, onError) {
    let task = new this.hamstersTask(params, functionToRun, this);
    let scaffold = hamstersPool.selectHamsterWheel();
    this.pool.scheduleTask(task, this.persistence, scaffold, this.maxThreads).then((results) => {
      onSuccess(results);
    }).catch((error) => {
      hamstersLogger.error(error.messsage, onError);
    });
  }
}

var hamsters = new hamstersjs();

if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  module.exports = hamsters;
}<|endoftext|>