<commit_before>(function () {
    var streaming = {};

    _.extend(streaming, {
        subscriptions: {},
        callbacks: {},

        setUser: function (user_id) {
            this.user_id = user_id;
        },

        setup: function setup(socket) {
            this.socket = socket;

            this.user_id = 0;
            this.db = new PouchDB('streaming.io');
            this.db.allDocs({include_docs: true}, _.bind(function (err, response) {
                var deletes = [];

                if (!err && response) {
                    var now = Date.now();
                    _.each(response.rows, function (row) {
                        if (row.doc.expire && row.doc.expire > now) {
                            return;
                        }
                        row.doc._deleted = true;
                        deletes.push(row.doc);
                    });
                }

                this.db.bulkDocs({
                    docs: deletes
                }, _.bind(function () {
                    this.db.compact();
                }, this));
            }, this));

            socket.on('connect', _.bind(function() {
                this.connect(socket);
            }, this));

            socket.on('disconnect', _.bind(function () {
                this.disconnect();
            }, this));

            socket.on('stream', _.bind(function (message) {
                this.sync(message);
            }, this));
        },

        sync: function sync(message) {
            var item = this.subscriptions[message.url];
            var data = this.uncompress(message.data);
            item.cache = data;
            this.apply(data, item.clients);

            this.db.put({
                _id: message.url,
                _rev: item.revision,
                hash: message.hash,
                data: message.data,
                user_id: this.user_id,
                expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
            });
        },

        apply: function apply(data, items) {
            _.each(items, function (model) {
                if (!model.streaming) {
                    return;
                }

                var initial = model.streaming.options.initial;
                model.streaming.options.initial = false;

                if (model.models.length > 0) {
                    var idAttribute = model.models[0].idAttribute;
                    var equals = false;
                    do {
                        if (initial === true) {
                            break;
                        }

                        if (model.models.length != data.length) {
                            break;
                        }

                        var local = _.pluck(model.models, "id");
                        var remote = _.pluck(data, idAttribute)

                        var i = 0, n = model.models.length;
                        for (;i !=n; ++i) {
                            if (local[i] != remote[i]) {
                                break;
                            }
                        }
                        if (i != n) {
                            break;
                        }
                        equals = true;
                    } while (0);
                    if (equals) {
                        model.forEach(function (current, index) {
                            current.set(current.parse(data[index]));
                        });
                        model.trigger('sync', model, data, {});
                    } else {
                        model.reset(model.parse(data));
                    }

                } else {
                    model.reset(model.parse(data));
                }
            });
        },

        emit: function emit(url, item) {
            item.updating = true;

            var message = {
                url: url
            };

            this.db.get(url, _.bind(function (err, doc) {
                if (!err && doc) {
                    message.hash = doc.hash;
                    item.revision = doc._rev;

                    if ((doc.user_id == this.user_id) && item.options.precache) {
                        item.cache = this.uncompress(doc.data);
                        this.apply(item.cache, item.clients);
                    }
                }

                this.send("stream", message, _.bind(function (err, response) {
                    delete item.updating;

                    if (err) {
                        return;
                    }

                    var data;
                    if (response.hash == message.hash) {
                        var limit = new Date().getTime() + 1 * 60 * 60 * 1000;
                        if (doc.expire < limit || (doc.user_id != this.user_id)) {
                            this.db.put({
                                _id: url,
                                _rev: item.revision,
                                hash: message.hash,
                                data: doc.data,
                                user_id: this.user_id,
                                expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                            });
                        }

                        if (!item.cache) {
                            item.cache = data = this.uncompress(doc.data);
                        }
                    } else {
                        item.cache = data = this.uncompress(response.data);
                        this.db.put({
                            _id: url,
                            _rev: item.revision,
                            hash: response.hash,
                            data: response.data,
                            user_id: this.user_id,
                            expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                        });
                    }

                    if (data) {
                        this.apply(data, item.clients);
                    }
                }, this));

            }, this));
        },

        connect: function (socket) {
            this.socket = socket;

            _.each(this.subscriptions, function (item, url) {
                this.emit(url, item);
            }, this);
        },

        disconnect: function () {
            _.each(this.subscriptions, function (item) {
                delete item.cache;
                if (item.timeout) {
                    clearTimeout(item.timeout);
                    delete item.timeout;
                }
            });

            _.each(this.callbacks, function (callback, id) {
                callback("Disconnected");
            });
            this.callbacks = {};

            delete this.socket;
        },

        add: function (target, options) {
            var url = target.url();
            if (target.streaming) {
                return;
            }

            options = options ? _.clone(options) : {};
            _.defaults(options, {
                "precache": target.hasOwnProperty("precache") && !!target.precache,
                "initial": true
            });

            var item = this.subscriptions[url];
            if (!_.isUndefined(item)) {
                if (item.timeout) {
                    clearTimeout(item.timeout);
                    delete item.timeout;
                }

                target.streaming = { url: url, options: options };
                item.clients[target.cid] = target;

                if (item.cache) {
                    this.apply(item.cache, [target]);
                }
                return;
            } else {
                item = { clients: {}, options: options };
                item.clients[target.cid] = target;

                target.streaming = { url: url, options: options };
                this.subscriptions[url] = item;
            }

            if (_.isUndefined(this.socket)) {
                return;
            }

            this.emit(url, item);
        },

        remove: function (target) {
            if (target.streaming) {
                var url = target.streaming.url;
                var item = this.subscriptions[url];

                delete item.clients[target.cid];
                if (_.keys(item.clients).length == 0) {
                    item.timeout = setTimeout(_.bind(function () {
                        this.socket.emit("unstream", { url: url }, function (err) {});
                        delete this.subscriptions[url];
                    }, this), 5000);
                }

                delete target.streaming;
            }
        },

        send: function (type, message, callback) {
            if (!this.socket) {
                _.defer(function () {
                    callback("No connection");
                });
                return;
            }

            var id = _.uniqueId('cb');
            this.callbacks[id] = callback;

            this.socket.emit(type, message, _.bind(function (err, response) {
                delete this.callbacks[id];
                callback(err, this.uncompress(response));
            }, this));
        },

        uncompress: function (data) {
            if (!_.isObject(data)) {
                return data;
            }

            var objectKeys = _.keys(data);

            if ((objectKeys.length != 2) || (_.without(objectKeys, 'keys', 'rows').length != 0)) {
                return data;
            }

            var keys = data.keys;
            var uncompressed = _.map(data.rows, function (row) {
                return _.object(keys, row);
            });

            return uncompressed;
        }
    });

    var sync = function (method, model, options) {
        var message = {
            method: method,
            url: model.url(),
            cid: model.cid,
            data: options.arguments || model.toJSON() || {},
            emit: options.emit
        };

        streaming.send("sync", message, function (err, response) {
            if (err) {
                options.error(err);
            } else {
                options.success(response);
            }
        });
    };

    streaming.Model = Backbone.Model.extend({
        stream: function () {
            console.trace();
            alert("Streaming models is not currently supported");
        },

        unstream: function () {},

        incrementVersion: function () {
            var versionName = this.versionAttribute;
            if (!_.isUndefined(versionName)) {
                this.set(versionName, this.parseVersionNumber(this.get(versionName)) + 1, {silent: true});
            }
        },

        parseVersionNumber: function (versionNumber) {
            return _.isFinite(versionNumber) ? versionNumber : 0;
        },

        parse: function (resp) {
            var versionName = this.versionAttribute;
            if (_.isObject(resp) && !_.isUndefined(versionName)) {
                var localVersion = this.parseVersionNumber(this.get(versionName));
                var remoteVersion = this.parseVersionNumber(resp[versionName]);

                if (localVersion > remoteVersion) {
                    return {};
                }
            }
            return resp;
        },

        emit: function(method, args, options) {
            if (_.isUndefined(options)) {
                options = args ? _.clone(args) : {};
                args = undefined;
            } else {
                options = options ? _.clone(options) : {};
            }

            var model = this;

            var triggerEmit = _.bind(function() {
                model.trigger('emit emit:' + method, model, options);
            }, this);

            options.emit = method;
            options.arguments = args;

            var success = options.success;
            options.success = function (resp) {
                if (success) success(model, resp, options);
                if (options.wait) triggerEmit();
            };
            var error = options.error;
            options.error = function (resp) {
                if (error) error(model, resp, options);
                model.trigger('error', model, resp, options);
            }

            var xhr = this.sync('emit', this, options);
            if (!options.wait) triggerEmit();
            return xhr;
        },

        sync: sync
    });

    streaming.Collection = Backbone.Collection.extend({
        stream: function (options) {
            options = options || {};
            if (_.isUndefined(this.cid)) {
                this.cid = _.uniqueId('coll');
            }

            _.defer(_.bind(function () {
                streaming.add(this, options);
            }, this));
        },

        unstream: function () {
            streaming.remove(this);
        },

        emit: function (method, args, options) {
            if (_.isUndefined(options)) {
                options = args ? _.clone(args) : {};
                args = undefined;
            } else {
                options = options ? _.clone(options) : {};
            }

            var model = this;

            var triggerEmit = _.bind(function() {
                model.trigger('emit emit:' + method, model, options);
            }, this);

            options.emit = method;
            options.arguments = args;

            var success = options.success;
            options.success = function (resp) {
                if (success) success(model, resp, options);
                if (options.wait) triggerEmit();
            };
            var error = options.error;
            options.error = function (resp) {
                if (error) error(model, resp, options);
                model.trigger('error', model, resp, options);
            }

            var xhr = this.sync('emit', this, options);
            if (!options.wait) triggerEmit();
            return xhr;
        },

        sync: sync
    });

    window.Streaming = streaming;
})();

<commit_msg>Now enabled auto compaction of database per default to reduce unbounded db growth<commit_after>(function () {
    var streaming = {};

    _.extend(streaming, {
        subscriptions: {},
        callbacks: {},

        setUser: function (user_id) {
            this.user_id = user_id;
        },

        setup: function setup(socket) {
            this.socket = socket;

            this.user_id = 0;
            this.db = new PouchDB('streaming.io', {auto_compaction: true});
            this.db.allDocs({include_docs: true}, _.bind(function (err, response) {
                var deletes = [];

                if (!err && response) {
                    var now = Date.now();
                    _.each(response.rows, function (row) {
                        if (row.doc.expire && row.doc.expire > now) {
                            return;
                        }
                        row.doc._deleted = true;
                        deletes.push(row.doc);
                    });
                }

                this.db.bulkDocs({
                    docs: deletes
                }, _.bind(function () {
                    this.db.compact();
                }, this));
            }, this));

            socket.on('connect', _.bind(function() {
                this.connect(socket);
            }, this));

            socket.on('disconnect', _.bind(function () {
                this.disconnect();
            }, this));

            socket.on('stream', _.bind(function (message) {
                this.sync(message);
            }, this));
        },

        sync: function sync(message) {
            var item = this.subscriptions[message.url];
            var data = this.uncompress(message.data);
            item.cache = data;
            this.apply(data, item.clients);

            this.db.put({
                _id: message.url,
                _rev: item.revision,
                hash: message.hash,
                data: message.data,
                user_id: this.user_id,
                expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
            });
        },

        apply: function apply(data, items) {
            _.each(items, function (model) {
                if (!model.streaming) {
                    return;
                }

                var initial = model.streaming.options.initial;
                model.streaming.options.initial = false;

                if (model.models.length > 0) {
                    var idAttribute = model.models[0].idAttribute;
                    var equals = false;
                    do {
                        if (initial === true) {
                            break;
                        }

                        if (model.models.length != data.length) {
                            break;
                        }

                        var local = _.pluck(model.models, "id");
                        var remote = _.pluck(data, idAttribute)

                        var i = 0, n = model.models.length;
                        for (;i !=n; ++i) {
                            if (local[i] != remote[i]) {
                                break;
                            }
                        }
                        if (i != n) {
                            break;
                        }
                        equals = true;
                    } while (0);
                    if (equals) {
                        model.forEach(function (current, index) {
                            current.set(current.parse(data[index]));
                        });
                        model.trigger('sync', model, data, {});
                    } else {
                        model.reset(model.parse(data));
                    }

                } else {
                    model.reset(model.parse(data));
                }
            });
        },

        emit: function emit(url, item) {
            item.updating = true;

            var message = {
                url: url
            };

            this.db.get(url, _.bind(function (err, doc) {
                if (!err && doc) {
                    message.hash = doc.hash;
                    item.revision = doc._rev;

                    if ((doc.user_id == this.user_id) && item.options.precache) {
                        item.cache = this.uncompress(doc.data);
                        this.apply(item.cache, item.clients);
                    }
                }

                this.send("stream", message, _.bind(function (err, response) {
                    delete item.updating;

                    if (err) {
                        return;
                    }

                    var data;
                    if (response.hash == message.hash) {
                        var limit = new Date().getTime() + 1 * 60 * 60 * 1000;
                        if (doc.expire < limit || (doc.user_id != this.user_id)) {
                            this.db.put({
                                _id: url,
                                _rev: item.revision,
                                hash: message.hash,
                                data: doc.data,
                                user_id: this.user_id,
                                expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                            });
                        }

                        if (!item.cache) {
                            item.cache = data = this.uncompress(doc.data);
                        }
                    } else {
                        item.cache = data = this.uncompress(response.data);
                        this.db.put({
                            _id: url,
                            _rev: item.revision,
                            hash: response.hash,
                            data: response.data,
                            user_id: this.user_id,
                            expire: new Date(new Date().getTime() + 2 * 60 * 60 * 1000)
                        });
                    }

                    if (data) {
                        this.apply(data, item.clients);
                    }
                }, this));

            }, this));
        },

        connect: function (socket) {
            this.socket = socket;

            _.each(this.subscriptions, function (item, url) {
                this.emit(url, item);
            }, this);
        },

        disconnect: function () {
            _.each(this.subscriptions, function (item) {
                delete item.cache;
                if (item.timeout) {
                    clearTimeout(item.timeout);
                    delete item.timeout;
                }
            });

            _.each(this.callbacks, function (callback, id) {
                callback("Disconnected");
            });
            this.callbacks = {};

            delete this.socket;
        },

        add: function (target, options) {
            var url = target.url();
            if (target.streaming) {
                return;
            }

            options = options ? _.clone(options) : {};
            _.defaults(options, {
                "precache": target.hasOwnProperty("precache") && !!target.precache,
                "initial": true
            });

            var item = this.subscriptions[url];
            if (!_.isUndefined(item)) {
                if (item.timeout) {
                    clearTimeout(item.timeout);
                    delete item.timeout;
                }

                target.streaming = { url: url, options: options };
                item.clients[target.cid] = target;

                if (item.cache) {
                    this.apply(item.cache, [target]);
                }
                return;
            } else {
                item = { clients: {}, options: options };
                item.clients[target.cid] = target;

                target.streaming = { url: url, options: options };
                this.subscriptions[url] = item;
            }

            if (_.isUndefined(this.socket)) {
                return;
            }

            this.emit(url, item);
        },

        remove: function (target) {
            if (target.streaming) {
                var url = target.streaming.url;
                var item = this.subscriptions[url];

                delete item.clients[target.cid];
                if (_.keys(item.clients).length == 0) {
                    item.timeout = setTimeout(_.bind(function () {
                        this.socket.emit("unstream", { url: url }, function (err) {});
                        delete this.subscriptions[url];
                    }, this), 5000);
                }

                delete target.streaming;
            }
        },

        send: function (type, message, callback) {
            if (!this.socket) {
                _.defer(function () {
                    callback("No connection");
                });
                return;
            }

            var id = _.uniqueId('cb');
            this.callbacks[id] = callback;

            this.socket.emit(type, message, _.bind(function (err, response) {
                delete this.callbacks[id];
                callback(err, this.uncompress(response));
            }, this));
        },

        uncompress: function (data) {
            if (!_.isObject(data)) {
                return data;
            }

            var objectKeys = _.keys(data);

            if ((objectKeys.length != 2) || (_.without(objectKeys, 'keys', 'rows').length != 0)) {
                return data;
            }

            var keys = data.keys;
            var uncompressed = _.map(data.rows, function (row) {
                return _.object(keys, row);
            });

            return uncompressed;
        }
    });

    var sync = function (method, model, options) {
        var message = {
            method: method,
            url: model.url(),
            cid: model.cid,
            data: options.arguments || model.toJSON() || {},
            emit: options.emit
        };

        streaming.send("sync", message, function (err, response) {
            if (err) {
                options.error(err);
            } else {
                options.success(response);
            }
        });
    };

    streaming.Model = Backbone.Model.extend({
        stream: function () {
            console.trace();
            alert("Streaming models is not currently supported");
        },

        unstream: function () {},

        incrementVersion: function () {
            var versionName = this.versionAttribute;
            if (!_.isUndefined(versionName)) {
                this.set(versionName, this.parseVersionNumber(this.get(versionName)) + 1, {silent: true});
            }
        },

        parseVersionNumber: function (versionNumber) {
            return _.isFinite(versionNumber) ? versionNumber : 0;
        },

        parse: function (resp) {
            var versionName = this.versionAttribute;
            if (_.isObject(resp) && !_.isUndefined(versionName)) {
                var localVersion = this.parseVersionNumber(this.get(versionName));
                var remoteVersion = this.parseVersionNumber(resp[versionName]);

                if (localVersion > remoteVersion) {
                    return {};
                }
            }
            return resp;
        },

        emit: function(method, args, options) {
            if (_.isUndefined(options)) {
                options = args ? _.clone(args) : {};
                args = undefined;
            } else {
                options = options ? _.clone(options) : {};
            }

            var model = this;

            var triggerEmit = _.bind(function() {
                model.trigger('emit emit:' + method, model, options);
            }, this);

            options.emit = method;
            options.arguments = args;

            var success = options.success;
            options.success = function (resp) {
                if (success) success(model, resp, options);
                if (options.wait) triggerEmit();
            };
            var error = options.error;
            options.error = function (resp) {
                if (error) error(model, resp, options);
                model.trigger('error', model, resp, options);
            }

            var xhr = this.sync('emit', this, options);
            if (!options.wait) triggerEmit();
            return xhr;
        },

        sync: sync
    });

    streaming.Collection = Backbone.Collection.extend({
        stream: function (options) {
            options = options || {};
            if (_.isUndefined(this.cid)) {
                this.cid = _.uniqueId('coll');
            }

            _.defer(_.bind(function () {
                streaming.add(this, options);
            }, this));
        },

        unstream: function () {
            streaming.remove(this);
        },

        emit: function (method, args, options) {
            if (_.isUndefined(options)) {
                options = args ? _.clone(args) : {};
                args = undefined;
            } else {
                options = options ? _.clone(options) : {};
            }

            var model = this;

            var triggerEmit = _.bind(function() {
                model.trigger('emit emit:' + method, model, options);
            }, this);

            options.emit = method;
            options.arguments = args;

            var success = options.success;
            options.success = function (resp) {
                if (success) success(model, resp, options);
                if (options.wait) triggerEmit();
            };
            var error = options.error;
            options.error = function (resp) {
                if (error) error(model, resp, options);
                model.trigger('error', model, resp, options);
            }

            var xhr = this.sync('emit', this, options);
            if (!options.wait) triggerEmit();
            return xhr;
        },

        sync: sync
    });

    window.Streaming = streaming;
})();

<|endoftext|>


<commit_before>const TEST_ENV = 'TEST';
const PROD_ENV = 'PROD';

class GoogleCalendarMoveToDate {
    // Maximum number of events to keep in a cell.
    #DEFAULT_MAX_EVENTS_PER_CELL = 14;

    // Oldest available cell date that has been found and should be used.
    topCellDate;

    constructor(options) {
        this.options = options;
        this.options.maxEventsPerCell = this.options.maxEventsPerCell || this.#DEFAULT_MAX_EVENTS_PER_CELL;
        this.env = options.env;
        this.debug = options.debug ? true : false;
    }

    findCellsInRow(row) {
        this.debug && console.info('findCellsInRow');
        this.debug && console.log('row:', row);
        var rowCells;
        if (this.env === PROD_ENV) {
            rowCells = row.querySelectorAll('[role="gridcell"]');
        } else if (this.env === TEST_ENV) {
            rowCells = row;
        }
        this.debug && console.log('rowCells:', rowCells);
        return rowCells;
    }

    clearTopCellDate() {
        this.debug && console.info('clearTopCellDate');
        this.topCellDate = undefined;
    }

    countEventsInCell(cell) {
        this.debug && console.info('countEventsInCell');
        this.debug && console.log('cell:', cell);
        var cellEventsFound;
        if (this.env === PROD_ENV) {
            var cellEvents = cell.querySelectorAll('[data-eventid]');
            cellEventsFound = cellEvents.length;
            var moreCellEvents = cell.querySelector('[data-opens-day-overview]');
            if (moreCellEvents) {
                cellEventsFound += parseInt(moreCellEvents.innerText.match(/(\d+) more/)[1]);
            }
        } else if (this.env === TEST_ENV) {
            var cellEvents = cell;
            cellEventsFound = cellEvents.length;
        }
        this.debug && console.log('cellEventsFound:', cellEventsFound);
        return cellEventsFound;
    }

    findTopCell(cells) {
        // Find first top-right most cell in current view containing a non-full number of events.
        this.debug && console.group('findTopCell');
        this.debug && console.log('cells:', cells);
        var topCell;

        // Check top to bottom.
        outer_loop:
        for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
            this.debug && console.group('rowIndex:', rowIndex);

            var row = cells[rowIndex];
            this.debug && console.log('row:', row);

            var rowCells = this.findCellsInRow(row);
            // Check right to left.
            for (var cellIndex = rowCells.length - 1; cellIndex >= 0; cellIndex--) {
                this.debug && console.log('cellIndex:', cellIndex);
                var cell = rowCells[cellIndex];
                this.debug && console.log('cell:', cell);

                var cellEventsFound = this.countEventsInCell(cell);

                if (this.env === PROD_ENV) {
                    cell = cell;
                } else if (this.env === TEST_ENV) {
                    cell = [rowIndex, cellIndex];
                }

                // Determine if current top cell should be cleared now that it has the maximum number of events per
                // cell.
                if (this.topCellDate !== undefined &&
                    cellEventsFound === this.options.maxEventsPerCell) {
                    var cellDate = this.getCellDate(cell);
                    if (cellDate === this.topCellDate) {
                        this.topCellDate = undefined;
                    }
                }

                if (cellEventsFound < this.options.maxEventsPerCell) {
                    topCell = cell;
                    this.debug && console.groupEnd();
                    break outer_loop;
                } else {
                    this.debug && console.log('ELSE');
                }
            }

            this.debug && console.groupEnd();
        }

        this.debug && console.log('topCell:', topCell);
        this.debug && console.groupEnd();
        return topCell;
    }

    getCellDate(cell) {
        this.debug && console.info('getCellDate');
        this.debug && console.log('cell:', cell);
        var cellDate;
        if (this.env === PROD_ENV) {
            // "7 events, Thursday, October 17".
            var cellDescription = cell.querySelector('h2').innerText;

            // "October 17".
            var monthDate = cellDescription.match(/ events?, .*day, (.*?)$/)[1];
            this.debug && console.log('monthDate:', monthDate);

            // "Oct 17".
            monthDate = monthDate.substr(0, 3) + ' ' + monthDate.split(' ')[1];
            this.debug && console.log('monthDate:', monthDate);

            // "2019".
            var calendarYear = (new Date()).getFullYear();

            // "Oct 17, 2019".
            var monthDateYear = monthDate + ', ' + calendarYear;
            this.debug && console.log('monthDateYear:', monthDateYear);

            cellDate = monthDateYear;
        } else if (this.env === TEST_ENV) {
            var rowIndex = cell[0];
            var cellIndex = cell[1];
            cellDate = this.options.cellDates[rowIndex][cellIndex];
        }
        this.debug && console.log('cellDate:', cellDate);
        return cellDate;
    }

    getTopCellDate(calendarGridRows) {
        this.debug && console.info('getTopCellDate');

        // Calculate if cell date is older than the current top cell date only when a top cell is found.
        var topCell = this.findTopCell(calendarGridRows);
        if (topCell !== undefined) {
            var cellDate = this.getCellDate(topCell);
            if (this.topCellDate === undefined) {
                this.debug && console.log('using newly calculated cellDate:', cellDate);
                this.topCellDate = cellDate;
            } else {
                this.debug && console.log('comparing cellDate %s to current topCellDate %s', cellDate, this.topCellDate);
                if (this._getDateComparisonObject(cellDate) < this._getDateComparisonObject(this.topCellDate)) {
                    this.debug && console.log('updating current topCellDate');
                    this.topCellDate = cellDate;
                } else {
                    this.debug && console.log('keeping current topCellDate');
                }
            }
        }

        this.debug && console.log('topCellDate:', this.topCellDate);
        return this.topCellDate;
    }

    setOption(option, value) {
        this.options[option] = value;
    }

    _getDateComparisonObject(date) {
        return new Date(date).getTime();
    }
}

if (typeof(window) === 'undefined') {
    module.exports = GoogleCalendarMoveToDate
}
<commit_msg>Use function to calculate calendar year<commit_after>const TEST_ENV = 'TEST';
const PROD_ENV = 'PROD';

class GoogleCalendarMoveToDate {
    // Maximum number of events to keep in a cell.
    #DEFAULT_MAX_EVENTS_PER_CELL = 14;

    // Oldest available cell date that has been found and should be used.
    topCellDate;

    constructor(options) {
        this.options = options;
        this.options.maxEventsPerCell = this.options.maxEventsPerCell || this.#DEFAULT_MAX_EVENTS_PER_CELL;
        this.env = options.env;
        this.debug = options.debug ? true : false;
    }

    findCellsInRow(row) {
        this.debug && console.info('findCellsInRow');
        this.debug && console.log('row:', row);
        var rowCells;
        if (this.env === PROD_ENV) {
            rowCells = row.querySelectorAll('[role="gridcell"]');
        } else if (this.env === TEST_ENV) {
            rowCells = row;
        }
        this.debug && console.log('rowCells:', rowCells);
        return rowCells;
    }

    clearTopCellDate() {
        this.debug && console.info('clearTopCellDate');
        this.topCellDate = undefined;
    }

    countEventsInCell(cell) {
        this.debug && console.info('countEventsInCell');
        this.debug && console.log('cell:', cell);
        var cellEventsFound;
        if (this.env === PROD_ENV) {
            var cellEvents = cell.querySelectorAll('[data-eventid]');
            cellEventsFound = cellEvents.length;
            var moreCellEvents = cell.querySelector('[data-opens-day-overview]');
            if (moreCellEvents) {
                cellEventsFound += parseInt(moreCellEvents.innerText.match(/(\d+) more/)[1]);
            }
        } else if (this.env === TEST_ENV) {
            var cellEvents = cell;
            cellEventsFound = cellEvents.length;
        }
        this.debug && console.log('cellEventsFound:', cellEventsFound);
        return cellEventsFound;
    }

    findTopCell(cells) {
        // Find first top-right most cell in current view containing a non-full number of events.
        this.debug && console.group('findTopCell');
        this.debug && console.log('cells:', cells);
        var topCell;

        // Check top to bottom.
        outer_loop:
        for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
            this.debug && console.group('rowIndex:', rowIndex);

            var row = cells[rowIndex];
            this.debug && console.log('row:', row);

            var rowCells = this.findCellsInRow(row);
            // Check right to left.
            for (var cellIndex = rowCells.length - 1; cellIndex >= 0; cellIndex--) {
                this.debug && console.log('cellIndex:', cellIndex);
                var cell = rowCells[cellIndex];
                this.debug && console.log('cell:', cell);

                var cellEventsFound = this.countEventsInCell(cell);

                if (this.env === PROD_ENV) {
                    cell = cell;
                } else if (this.env === TEST_ENV) {
                    cell = [rowIndex, cellIndex];
                }

                // Determine if current top cell should be cleared now that it has the maximum number of events per
                // cell.
                if (this.topCellDate !== undefined &&
                    cellEventsFound === this.options.maxEventsPerCell) {
                    var cellDate = this.getCellDate(cell);
                    if (cellDate === this.topCellDate) {
                        this.topCellDate = undefined;
                    }
                }

                if (cellEventsFound < this.options.maxEventsPerCell) {
                    topCell = cell;
                    this.debug && console.groupEnd();
                    break outer_loop;
                } else {
                    this.debug && console.log('ELSE');
                }
            }

            this.debug && console.groupEnd();
        }

        this.debug && console.log('topCell:', topCell);
        this.debug && console.groupEnd();
        return topCell;
    }

    getCalendarYear() {
        this.debug && console.info('getCalendarYear');
        // "2019".
        var calendarYear = (new Date()).getFullYear();
        this.debug && console.log('calendarYear:', calendarYear);
        return calendarYear;
    }

    getCellDate(cell) {
        this.debug && console.info('getCellDate');
        this.debug && console.log('cell:', cell);
        var cellDate;
        if (this.env === PROD_ENV) {
            // "7 events, Thursday, October 17".
            var cellDescription = cell.querySelector('h2').innerText;

            // "October 17".
            var monthDate = cellDescription.match(/ events?, .*day, (.*?)$/)[1];
            this.debug && console.log('monthDate:', monthDate);

            // "Oct 17".
            monthDate = monthDate.substr(0, 3) + ' ' + monthDate.split(' ')[1];
            this.debug && console.log('monthDate:', monthDate);

            // "Oct 17, 2019".
            var monthDateYear = monthDate + ', ' + this.getCalendarYear();
            this.debug && console.log('monthDateYear:', monthDateYear);

            cellDate = monthDateYear;
        } else if (this.env === TEST_ENV) {
            var rowIndex = cell[0];
            var cellIndex = cell[1];
            cellDate = this.options.cellDates[rowIndex][cellIndex];
        }
        this.debug && console.log('cellDate:', cellDate);
        return cellDate;
    }

    getTopCellDate(calendarGridRows) {
        this.debug && console.info('getTopCellDate');

        // Calculate if cell date is older than the current top cell date only when a top cell is found.
        var topCell = this.findTopCell(calendarGridRows);
        if (topCell !== undefined) {
            var cellDate = this.getCellDate(topCell);
            if (this.topCellDate === undefined) {
                this.debug && console.log('using newly calculated cellDate:', cellDate);
                this.topCellDate = cellDate;
            } else {
                this.debug && console.log('comparing cellDate %s to current topCellDate %s', cellDate, this.topCellDate);
                if (this._getDateComparisonObject(cellDate) < this._getDateComparisonObject(this.topCellDate)) {
                    this.debug && console.log('updating current topCellDate');
                    this.topCellDate = cellDate;
                } else {
                    this.debug && console.log('keeping current topCellDate');
                }
            }
        }

        this.debug && console.log('topCellDate:', this.topCellDate);
        return this.topCellDate;
    }

    setOption(option, value) {
        this.options[option] = value;
    }

    _getDateComparisonObject(date) {
        return new Date(date).getTime();
    }
}

if (typeof(window) === 'undefined') {
    module.exports = GoogleCalendarMoveToDate
}
<|endoftext|>


<commit_before>var requireAtom = require("./helpers").requireAtom;
var _ = requireAtom("underscore-plus");
var Tabs = requireAtom("tabs");
var TabBarView = requireAtom("tabs/lib/tab-bar-view");

TabBarView.prototype.initialize = _.wrap(TabBarView.prototype.initialize, function(original, pane) {
  original.apply(this, [pane]);

  pane.command('buffy:next-tab', function(e) {
    this.activateNextTab();
  }.bind(this));

  pane.command('buffy:previous-tab', function(e) {
    this.activatePreviousTab();
  }.bind(this));

});

TabBarView.prototype.tabForItem = function(item) {
  return _.detect(this.getTabs(), function(tab) {
    return tab.hasItem(item);
  });
};

TabBarView.prototype.addTabForItem = _.wrap(TabBarView.prototype.addTabForItem, function(original, item, index) {
  var tab = this.tabForItem(item),
      activeTab = this.getActiveTab();
  if(tab) {
    tab.activateItem(item);
  } else if(activeTab) {
    activeTab.addItem(item);
  } else {
    original.apply(this, [item, index]);
  }
});

TabBarView.prototype.updateActiveTab = _.wrap(TabBarView.prototype.updateActiveTab, function(original) {
  original.apply(this);
  this.getActiveTab().activateItem(this.pane.activeItem);
});

TabBarView.prototype.getActiveTab = function() {
  return this.tabForItem(this.pane.activeItem);
};

TabBarView.prototype.activateNextTab = function() {
  var currentTab = this.getActiveTab();
  var tabIndex = (currentTab.index() + 1) % (this.getTabs().length);
  var nextTab = this.tabAtIndex(tabIndex);

  if(nextTab) {
    var itemUri = nextTab.item.uri || nextTab.item.getPath();
    this.pane.activateItemForUri(itemUri);
  }
};

TabBarView.prototype.activatePreviousTab = function() {
  var currentTab = this.getActiveTab();
  var tabIndex = currentTab.index() - 1;

  if(currentTab.index() === 0) {
    tabIndex = this.getTabs().length - 1;
  }

  var previousTab = this.tabAtIndex(tabIndex);

  if(previousTab) {
    var itemUri = previousTab.item.uri || previousTab.item.getPath();
    this.pane.activateItemForUri(itemUri);
  }
};

module.exports = TabBarView;
<commit_msg>Close all buffers associated with a tab when closing that tab<commit_after>var requireAtom = require("./helpers").requireAtom;
var _ = requireAtom("underscore-plus");
var Tabs = requireAtom("tabs");
var TabBarView = requireAtom("tabs/lib/tab-bar-view");

TabBarView.prototype.initialize = _.wrap(TabBarView.prototype.initialize, function(original, pane) {
  original.apply(this, [pane]);

  pane.command('buffy:next-tab', function(e) {
    this.activateNextTab();
  }.bind(this));

  pane.command('buffy:previous-tab', function(e) {
    this.activatePreviousTab();
  }.bind(this));

});

TabBarView.prototype.tabForItem = function(item) {
  return _.detect(this.getTabs(), function(tab) {
    return tab.hasItem(item);
  });
};

TabBarView.prototype.addTabForItem = _.wrap(TabBarView.prototype.addTabForItem, function(original, item, index) {
  var tab = this.tabForItem(item),
      activeTab = this.getActiveTab();
  if(tab) {
    tab.activateItem(item);
  } else if(activeTab) {
    activeTab.addItem(item);
  } else {
    original.apply(this, [item, index]);
  }
});

TabBarView.prototype.closeTab = function(tab) {
  tab = tab || this.children('.right-clicked').view();
  this.pane.destroyItem(tab.item);
};

TabBarView.prototype.removeTabForItem = _.wrap(TabBarView.prototype.removeTabForItem, function(original, item) {
  var tab = this.tabForItem(item);
  if(typeof tab === "undefined") {
    return; // this tab is already removed
  }
  original.apply(this, [item]);
  tab.items.forEach(function(item) {
    this.pane.destroyItem(item);
  }, this);
});

TabBarView.prototype.updateActiveTab = _.wrap(TabBarView.prototype.updateActiveTab, function(original) {
  original.apply(this);
  this.setActiveTab(this.getActiveTab());
  this.getActiveTab();
});

TabBarView.prototype.setActiveTab = _.wrap(TabBarView.prototype.setActiveTab, function(original, tabView) {
  original.apply(this, [tabView]);

  if(tabView && tabView.item !== this.pane.activeItem) {
    tabView.activateItem(this.pane.activeItem);
  }
});

TabBarView.prototype.getActiveTab = function() {
  return this.tabForItem(this.pane.activeItem);
};

TabBarView.prototype.activateNextTab = function() {
  var currentTab = this.getActiveTab();
  var tabIndex = (currentTab.index() + 1) % (this.getTabs().length);
  var nextTab = this.tabAtIndex(tabIndex);

  if(nextTab) {
    var itemUri = nextTab.item.uri || nextTab.item.getPath();
    this.pane.activateItemForUri(itemUri);
  }
};

TabBarView.prototype.activatePreviousTab = function() {
  var currentTab = this.getActiveTab();
  var tabIndex = currentTab.index() - 1;

  if(currentTab.index() === 0) {
    tabIndex = this.getTabs().length - 1;
  }

  var previousTab = this.tabAtIndex(tabIndex);

  if(previousTab) {
    var itemUri = previousTab.item.uri || previousTab.item.getPath();
    this.pane.activateItemForUri(itemUri);
  }
};

module.exports = TabBarView;
<|endoftext|>


<commit_before>var Steam = require('steam');
var SteamUser = require('../index.js');
var ByteBuffer = require('bytebuffer');

var Schema = require('./protobufs.js');

var protobufs = {};
protobufs[Steam.EMsg.ClientLogon] = Schema.CMsgClientLogon;
protobufs[Steam.EMsg.ClientLogOnResponse] = Schema.CMsgClientLogonResponse;
protobufs[Steam.EMsg.ClientLogOff] = Schema.CMsgClientLogOff;
protobufs[Steam.EMsg.ClientUpdateMachineAuth] = Schema.CMsgClientUpdateMachineAuth;
protobufs[Steam.EMsg.ClientUpdateMachineAuthResponse] = Schema.CMsgClientUpdateMachineAuthResponse;
protobufs[Steam.EMsg.ClientNewLoginKey] = Schema.CMsgClientNewLoginKey;
protobufs[Steam.EMsg.ClientNewLoginKeyAccepted] = Schema.CMsgClientNewLoginKeyAccepted;
protobufs[Steam.EMsg.ClientRequestWebAPIAuthenticateUserNonce] = Schema.CMsgClientRequestWebAPIAuthenticateUserNonce;
protobufs[Steam.EMsg.ClientRequestWebAPIAuthenticateUserNonceResponse] = Schema.CMsgClientRequestWebAPIAuthenticateUserNonceResponse;
protobufs[Steam.EMsg.ClientItemAnnouncements] = Schema.CMsgClientItemAnnouncements;
protobufs[Steam.EMsg.ClientRequestItemAnnouncements] = Schema.CMsgClientRequestItemAnnouncements;
protobufs[Steam.EMsg.ClientCommentNotifications] = Schema.CMsgClientCommentNotifications;
protobufs[Steam.EMsg.ClientRequestCommentNotifications] = Schema.CMsgClientRequestCommentNotifications;
protobufs[Steam.EMsg.ClientUserNotifications] = Schema.CMsgClientUserNotifications;
protobufs[Steam.EMsg.ClientFSOfflineMessageNotification] = Schema.CMsgClientOfflineMessageNotification;
protobufs[Steam.EMsg.ClientFSRequestOfflineMessageCount] = Schema.CMsgClientRequestOfflineMessageCount;
protobufs[Steam.EMsg.ClientGamesPlayed] = Schema.CMsgClientGamesPlayed;
protobufs[Steam.EMsg.ClientEmailAddrInfo] = Schema.CMsgClientEmailAddrInfo;
protobufs[Steam.EMsg.ClientIsLimitedAccount] = Schema.CMsgClientIsLimitedAccount;
protobufs[Steam.EMsg.ClientWalletInfoUpdate] = Schema.CMsgClientWalletInfoUpdate;
protobufs[Steam.EMsg.ClientLicenseList] = Schema.CMsgClientLicenseList;
protobufs[Steam.EMsg.ClientServiceMethod] = Schema.CMsgClientServiceMethod;
protobufs[Steam.EMsg.ClientServiceMethodResponse] = Schema.CMsgClientServiceMethodResponse;
protobufs[Steam.EMsg.ClientGMSServerQuery] = Schema.CMsgClientGMSServerQuery;
protobufs[Steam.EMsg.GMSClientServerQueryResponse] = Schema.CMsgGMSClientServerQueryResponse;
protobufs[Steam.EMsg.ClientPICSChangesSinceRequest] = Schema.CMsgClientPICSChangesSinceRequest;
protobufs[Steam.EMsg.ClientPICSChangesSinceResponse] = Schema.CMsgClientPICSChangesSinceResponse;
protobufs[Steam.EMsg.ClientPICSProductInfoRequest] = Schema.CMsgClientPICSProductInfoRequest;
protobufs[Steam.EMsg.ClientPICSProductInfoResponse] = Schema.CMsgClientPICSProductInfoResponse;
protobufs[Steam.EMsg.ClientPICSAccessTokenRequest] = Schema.CMsgClientPICSAccessTokenRequest;
protobufs[Steam.EMsg.ClientPICSAccessTokenResponse] = Schema.CMsgClientPICSAccessTokenResponse;
protobufs[Steam.EMsg.ClientCreateAccountProto] = Schema.CMsgClientCreateAccount;
protobufs[Steam.EMsg.ClientCreateAccountProtoResponse] = Schema.CMsgClientCreateAccountResponse;

// Unified protobufs
protobufs['GameServers.GetServerList#1_Request'] = Schema.CGameServers_GetServerList_Request;
protobufs['GameServers.GetServerList#1_Response'] = Schema.CGameServers_GetServerList_Response;
protobufs['GameServers.GetServerSteamIDsByIP#1_Request'] = Schema.CGameServers_GetServerSteamIDsByIP_Request;
protobufs['GameServers.GetServerSteamIDsByIP#1_Response'] = Schema.CGameServers_IPsWithSteamIDs_Response;
protobufs['GameServers.GetServerIPsBySteamID#1_Request'] = Schema.CGameServers_GetServerIPsBySteamID_Request;
protobufs['GameServers.GetServerIPsBySteamID#1_Response'] = Schema.CGameServers_IPsWithSteamIDs_Response;

ByteBuffer.DEFAULT_ENDIAN = ByteBuffer.LITTLE_ENDIAN;

SteamUser.prototype._send = function(emsg, body, callback) {
	var header = {
		"msg": emsg
	};

	var Proto = protobufs[emsg];
	if(Proto) {
		header.proto = {};
		body = new Proto(body).toBuffer();
	} else if(ByteBuffer.isByteBuffer(body)) {
		body = body.toBuffer();
	}

	var cb = null;
	if(callback) {
		cb = function(header, body) {
			if(protobufs[header.msg]) {
				body = protobufs[header.msg].decode(body);
			} else {
				body = ByteBuffer.wrap(body);
			}

			callback(body);
		};
	}

	this.emit('debug', 'Sending message: ' + emsg);
	this.client.send(header, body, cb);
};

SteamUser.prototype._handleMessage = function(header, body, callback) {
	var msgName = header.msg;

	if(this.options.debug) {
		for(var i in Steam.EMsg) {
			if(Steam.EMsg.hasOwnProperty(i) && Steam.EMsg[i] == header.msg) {
				msgName = i;
				break;
			}
		}
	}

	if(!this._handlers[header.msg]) {
		this.emit('debug', 'Unhandled message: ' + msgName);
		return;
	}

	if(protobufs[header.msg]) {
		body = protobufs[header.msg].decode(body);
	} else {
		body = ByteBuffer.wrap(body);
	}

	this.emit('debug', 'Handled message: ' + msgName);

	var cb = null;
	if(callback) {
		cb = function(emsg, body) {
			var header = {"msg": emsg};

			if(protobufs[emsg]) {
				header.proto = {};
				body = new protobufs[emsg](body).toBuffer();
			}

			callback(header, body);
		}
	}

	this._handlers[header.msg].call(this, body, cb);
};

SteamUser.prototype._handlers = {};

// Unified messages

SteamUser.prototype._sendUnified = function(methodName, methodData, notification, callback) {
	var cb;
	if(callback && protobufs[methodName + '_Response']) {
		cb = function(body) {
			var Proto = protobufs[methodName + '_Response'];
			callback(Proto.decode(body.serialized_method_response));
		};
	}

	var Proto = protobufs[methodName + '_Request'];
	this._send(Steam.EMsg.ClientServiceMethod, {
		"method_name": methodName,
		"serialized_method": new Proto(methodData).toBuffer(),
		"is_notification": notification
	}, cb);
};
<commit_msg>Don't attempt to send messages when logged off<commit_after>var Steam = require('steam');
var SteamUser = require('../index.js');
var ByteBuffer = require('bytebuffer');

var Schema = require('./protobufs.js');

var protobufs = {};
protobufs[Steam.EMsg.ClientLogon] = Schema.CMsgClientLogon;
protobufs[Steam.EMsg.ClientLogOnResponse] = Schema.CMsgClientLogonResponse;
protobufs[Steam.EMsg.ClientLogOff] = Schema.CMsgClientLogOff;
protobufs[Steam.EMsg.ClientUpdateMachineAuth] = Schema.CMsgClientUpdateMachineAuth;
protobufs[Steam.EMsg.ClientUpdateMachineAuthResponse] = Schema.CMsgClientUpdateMachineAuthResponse;
protobufs[Steam.EMsg.ClientNewLoginKey] = Schema.CMsgClientNewLoginKey;
protobufs[Steam.EMsg.ClientNewLoginKeyAccepted] = Schema.CMsgClientNewLoginKeyAccepted;
protobufs[Steam.EMsg.ClientRequestWebAPIAuthenticateUserNonce] = Schema.CMsgClientRequestWebAPIAuthenticateUserNonce;
protobufs[Steam.EMsg.ClientRequestWebAPIAuthenticateUserNonceResponse] = Schema.CMsgClientRequestWebAPIAuthenticateUserNonceResponse;
protobufs[Steam.EMsg.ClientItemAnnouncements] = Schema.CMsgClientItemAnnouncements;
protobufs[Steam.EMsg.ClientRequestItemAnnouncements] = Schema.CMsgClientRequestItemAnnouncements;
protobufs[Steam.EMsg.ClientCommentNotifications] = Schema.CMsgClientCommentNotifications;
protobufs[Steam.EMsg.ClientRequestCommentNotifications] = Schema.CMsgClientRequestCommentNotifications;
protobufs[Steam.EMsg.ClientUserNotifications] = Schema.CMsgClientUserNotifications;
protobufs[Steam.EMsg.ClientFSOfflineMessageNotification] = Schema.CMsgClientOfflineMessageNotification;
protobufs[Steam.EMsg.ClientFSRequestOfflineMessageCount] = Schema.CMsgClientRequestOfflineMessageCount;
protobufs[Steam.EMsg.ClientGamesPlayed] = Schema.CMsgClientGamesPlayed;
protobufs[Steam.EMsg.ClientEmailAddrInfo] = Schema.CMsgClientEmailAddrInfo;
protobufs[Steam.EMsg.ClientIsLimitedAccount] = Schema.CMsgClientIsLimitedAccount;
protobufs[Steam.EMsg.ClientWalletInfoUpdate] = Schema.CMsgClientWalletInfoUpdate;
protobufs[Steam.EMsg.ClientLicenseList] = Schema.CMsgClientLicenseList;
protobufs[Steam.EMsg.ClientServiceMethod] = Schema.CMsgClientServiceMethod;
protobufs[Steam.EMsg.ClientServiceMethodResponse] = Schema.CMsgClientServiceMethodResponse;
protobufs[Steam.EMsg.ClientGMSServerQuery] = Schema.CMsgClientGMSServerQuery;
protobufs[Steam.EMsg.GMSClientServerQueryResponse] = Schema.CMsgGMSClientServerQueryResponse;
protobufs[Steam.EMsg.ClientPICSChangesSinceRequest] = Schema.CMsgClientPICSChangesSinceRequest;
protobufs[Steam.EMsg.ClientPICSChangesSinceResponse] = Schema.CMsgClientPICSChangesSinceResponse;
protobufs[Steam.EMsg.ClientPICSProductInfoRequest] = Schema.CMsgClientPICSProductInfoRequest;
protobufs[Steam.EMsg.ClientPICSProductInfoResponse] = Schema.CMsgClientPICSProductInfoResponse;
protobufs[Steam.EMsg.ClientPICSAccessTokenRequest] = Schema.CMsgClientPICSAccessTokenRequest;
protobufs[Steam.EMsg.ClientPICSAccessTokenResponse] = Schema.CMsgClientPICSAccessTokenResponse;
protobufs[Steam.EMsg.ClientCreateAccountProto] = Schema.CMsgClientCreateAccount;
protobufs[Steam.EMsg.ClientCreateAccountProtoResponse] = Schema.CMsgClientCreateAccountResponse;

// Unified protobufs
protobufs['GameServers.GetServerList#1_Request'] = Schema.CGameServers_GetServerList_Request;
protobufs['GameServers.GetServerList#1_Response'] = Schema.CGameServers_GetServerList_Response;
protobufs['GameServers.GetServerSteamIDsByIP#1_Request'] = Schema.CGameServers_GetServerSteamIDsByIP_Request;
protobufs['GameServers.GetServerSteamIDsByIP#1_Response'] = Schema.CGameServers_IPsWithSteamIDs_Response;
protobufs['GameServers.GetServerIPsBySteamID#1_Request'] = Schema.CGameServers_GetServerIPsBySteamID_Request;
protobufs['GameServers.GetServerIPsBySteamID#1_Response'] = Schema.CGameServers_IPsWithSteamIDs_Response;

ByteBuffer.DEFAULT_ENDIAN = ByteBuffer.LITTLE_ENDIAN;

SteamUser.prototype._send = function(emsg, body, callback) {
	if((!this.steamID || !this.client.connected) && [Steam.EMsg.ChannelEncryptRequest, Steam.EMsg.ChannelEncryptResponse, Steam.EMsg.ChannelEncryptResult, Steam.EMsg.ClientLogon].indexOf(emsg) == -1) {
		// We're disconnected, drop it
		this.emit('debug', 'Dropping message ' + emsg + ' because we\'re not logged on.');
		return;
	}

	var header = {
		"msg": emsg
	};

	var Proto = protobufs[emsg];
	if(Proto) {
		header.proto = {};
		body = new Proto(body).toBuffer();
	} else if(ByteBuffer.isByteBuffer(body)) {
		body = body.toBuffer();
	}

	var cb = null;
	if(callback) {
		cb = function(header, body) {
			if(protobufs[header.msg]) {
				body = protobufs[header.msg].decode(body);
			} else {
				body = ByteBuffer.wrap(body);
			}

			callback(body);
		};
	}

	this.emit('debug', 'Sending message: ' + emsg);
	this.client.send(header, body, cb);
};

SteamUser.prototype._handleMessage = function(header, body, callback) {
	var msgName = header.msg;

	if(this.options.debug) {
		for(var i in Steam.EMsg) {
			if(Steam.EMsg.hasOwnProperty(i) && Steam.EMsg[i] == header.msg) {
				msgName = i;
				break;
			}
		}
	}

	if(!this._handlers[header.msg]) {
		this.emit('debug', 'Unhandled message: ' + msgName);
		return;
	}

	if(protobufs[header.msg]) {
		body = protobufs[header.msg].decode(body);
	} else {
		body = ByteBuffer.wrap(body);
	}

	this.emit('debug', 'Handled message: ' + msgName);

	var cb = null;
	if(callback) {
		cb = function(emsg, body) {
			var header = {"msg": emsg};

			if(protobufs[emsg]) {
				header.proto = {};
				body = new protobufs[emsg](body).toBuffer();
			}

			callback(header, body);
		}
	}

	this._handlers[header.msg].call(this, body, cb);
};

SteamUser.prototype._handlers = {};

// Unified messages

SteamUser.prototype._sendUnified = function(methodName, methodData, notification, callback) {
	var cb;
	if(callback && protobufs[methodName + '_Response']) {
		cb = function(body) {
			var Proto = protobufs[methodName + '_Response'];
			callback(Proto.decode(body.serialized_method_response));
		};
	}

	var Proto = protobufs[methodName + '_Request'];
	this._send(Steam.EMsg.ClientServiceMethod, {
		"method_name": methodName,
		"serialized_method": new Proto(methodData).toBuffer(),
		"is_notification": notification
	}, cb);
};
<|endoftext|>


<commit_before>const path = require('path');

const debug = require('debug')('ghost-cli:backup');
const execa = require('execa');
const fs = require('fs-extra');

const {ProcessError} = require('../errors');
const {exportTask} = require('./import');

module.exports = async function (ui, instance) {
    const datetime = require('moment')().format('YYYY-MM-DD-HH-mm-ss');
    const backupSuffix = `from-v${instance.version}-on-${datetime}`;

    // First we need to export the content into a JSON file & members into a CSV file
    const contentExportFile = `content-${backupSuffix}.json`;
    const membersExportFile = `members-${backupSuffix}.csv`;

    // Ensure the backup directory exists and has write permissions
    const {USER} = process.env;
    await ui.sudo(`mkdir -p ${path.join(instance.dir, 'backup')}`);
    await ui.sudo(`chown -R ${USER}:${USER} ${path.join(instance.dir, 'backup')}`);

    // Generate backup files
    await exportTask(ui, instance, path.join(instance.dir, 'backup/', contentExportFile), path.join(instance.dir, 'backup/', membersExportFile));

    // Next we need to copy `redirects.*` files from `data/` to `settings/` because
    // we're not going to backup `data/
    const backupFiles = {
        'content/data/redirects.json': 'content/settings/redirects.json',
        'content/data/redirects.yaml': 'content/settings/redirects.yaml',
        [`backup/${contentExportFile}`]: `content/data/${contentExportFile}`,
        [`backup/${membersExportFile}`]: `content/data/${membersExportFile}`
    };

    for (const backupFilesKey in backupFiles) {
        const filePath = path.join(instance.dir, backupFilesKey);
        const fileExists = fs.existsSync(filePath);

        if (fileExists) {
            debug(`copying ${backupFilesKey} to ${backupFiles[backupFilesKey]}`);

            const destinationFilePath = path.join(instance.dir, backupFiles[backupFilesKey]);
            await ui.sudo(`cp ${filePath} ${destinationFilePath}`);
        }
    }

    await ui.sudo(`chown -R ghost:ghost ${path.join(instance.dir, 'content')}`);

    // Finally we zip everything up into a nice little package
    const zipPath = path.join(process.cwd(), `backup-${backupSuffix}.zip`);

    try {
        execa.shellSync(`zip -r ${zipPath} data/${membersExportFile} data/${membersExportFile} files/ images/ media/ settings/ themes/`, {
            cwd: path.join(instance.dir, 'content/')
        });
    } catch (err) {
        throw new ProcessError(err);
    }

    return zipPath;
};
<commit_msg>fix(backup): clear error if zip is not installed<commit_after>const path = require('path');

const debug = require('debug')('ghost-cli:backup');
const execa = require('execa');
const fs = require('fs-extra');

const {ProcessError} = require('../errors');
const {exportTask} = require('./import');

module.exports = async function (ui, instance) {
    const datetime = require('moment')().format('YYYY-MM-DD-HH-mm-ss');
    const backupSuffix = `from-v${instance.version}-on-${datetime}`;

    // First we need to export the content into a JSON file & members into a CSV file
    const contentExportFile = `content-${backupSuffix}.json`;
    const membersExportFile = `members-${backupSuffix}.csv`;

    // Ensure the backup directory exists and has write permissions
    const {USER} = process.env;
    await ui.sudo(`mkdir -p ${path.join(instance.dir, 'backup')}`);
    await ui.sudo(`chown -R ${USER}:${USER} ${path.join(instance.dir, 'backup')}`);

    // Generate backup files
    await exportTask(ui, instance, path.join(instance.dir, 'backup/', contentExportFile), path.join(instance.dir, 'backup/', membersExportFile));

    // Next we need to copy `redirects.*` files from `data/` to `settings/` because
    // we're not going to backup `data/
    const backupFiles = {
        'content/data/redirects.json': 'content/settings/redirects.json',
        'content/data/redirects.yaml': 'content/settings/redirects.yaml',
        [`backup/${contentExportFile}`]: `content/data/${contentExportFile}`,
        [`backup/${membersExportFile}`]: `content/data/${membersExportFile}`
    };

    for (const backupFilesKey in backupFiles) {
        const filePath = path.join(instance.dir, backupFilesKey);
        const fileExists = fs.existsSync(filePath);

        if (fileExists) {
            debug(`copying ${backupFilesKey} to ${backupFiles[backupFilesKey]}`);

            const destinationFilePath = path.join(instance.dir, backupFiles[backupFilesKey]);
            await ui.sudo(`cp ${filePath} ${destinationFilePath}`);
        }
    }

    await ui.sudo(`chown -R ghost:ghost ${path.join(instance.dir, 'content')}`);

    // Finally we zip everything up into a nice little package
    const zipPath = path.join(process.cwd(), `backup-${backupSuffix}.zip`);

    try {
        execa.shellSync(`zip -r ${zipPath} data/${membersExportFile} data/${membersExportFile} files/ images/ media/ settings/ themes/`, {
            cwd: path.join(instance.dir, 'content/')
        });
    } catch (err) {
        if (err.stderr.match(/zip.*?not found/)) {
            err.message = `Package zip was not found. Please install by running \`sudo apt install zip\`. \n${err.message}`;
        }
        throw new ProcessError(err);
    }

    return zipPath;
};
<|endoftext|>


<commit_before>var _onclick = on("click");
var preventClick = false;
onmousedown(window, function (event) {
    preventClick = false;
});
onmousemove(window, function (event) {
    preventClick = true;
});
var onclick = function (target, handler) {
    return _onclick(target, function (event) {
        if (preventClick) return;
        return handler.call(this, event);
    });
}<commit_msg>增加onclick的响应范围<commit_after>var _onclick = on("click");
var preventClick = false, saved_x, saved_y;
onmousedown(window, function (event) {
    saved_x = event.clientX, saved_y = event.clientY;
    preventClick = false;
});
onmousemove(window, function (event) {
    var abs = Math.abs;
    if (abs(event.clientX - saved_x) > 2 || abs(event.clientY - saved_y) > 2)
        preventClick = true;
});
var onclick = function (target, handler) {
    return _onclick(target, function (event) {
        if (preventClick) return;
        return handler.call(this, event);
    });
}<|endoftext|>


<commit_before>(function() {
  // handler for any page javascript errors
  var errorHandler = function(msg, trace) {
    var msgStack = ['ERROR: ' + msg];
    if (trace) {
      msgStack.push('TRACE:');
      trace.forEach(function(t) {
        msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function + '")' : ''));
      });
    }
    console.error(msgStack.join('\n'));
    phantom.exit(1);
  };
  phantom.onError = errorHandler;

  var system = require('system');
  var args = system.args;

  if (args.length !== 2) {
    console.log("Need a url as the argument");
    phantom.exit(1);
  }

  var page = new WebPage();

  // log messages to stdout
  page.onConsoleMessage = function(msg) {
    console.log(msg);
  };

  // listen for event from parent page
  page.onCallback = function(data) {
    if (data.event === 'exit') {
      phantom.exit(data.exitCode);
    } else if (data.event === 'writeFile') {
      var fs = require("fs");
      fs.write(data.filename, data.text, 'w');
    } else {
      console.log('unkown event callback: ' + data);
    }
  };

  // log javascript errors
  page.onError = errorHandler;

  // setup listeners for jasmine events
  page.onInitialized = function() {
    return page.evaluate(function() {
      return window.onload = function() {
        jsApiReporter.exitCode = 0;
        jsApiReporter.reportSpecResults = function(spec) {
          if (spec.results().failedCount > 0) {
            jsApiReporter.exitCode = 1;
          }
        };
        jsApiReporter.reportRunnerResults = function() {
          setTimeout(function() {
            window.callPhantom({
              event: 'exit',
              exitCode: jsApiReporter.exitCode
            });
          }, 1);
        };
      };
    });
  };

  var address = args[1];
  console.log('Running: ' + address);
  page.open(address, function(status) {
    if (status !== "success") {
      console.log("can't load the address!");
      return phantom.exit(1);
    }
  });
})();
<commit_msg>fix typo<commit_after>(function() {
  // handler for any page javascript errors
  var errorHandler = function(msg, trace) {
    var msgStack = ['ERROR: ' + msg];
    if (trace) {
      msgStack.push('TRACE:');
      trace.forEach(function(t) {
        msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function + '")' : ''));
      });
    }
    console.error(msgStack.join('\n'));
    phantom.exit(1);
  };
  phantom.onError = errorHandler;

  var system = require('system');
  var args = system.args;

  if (args.length !== 2) {
    console.log("Need a url as the argument");
    phantom.exit(1);
  }

  var page = new WebPage();

  // log messages to stdout
  page.onConsoleMessage = function(msg) {
    console.log(msg);
  };

  // listen for event from parent page
  page.onCallback = function(data) {
    if (data.event === 'exit') {
      phantom.exit(data.exitCode);
    } else if (data.event === 'writeFile') {
      var fs = require("fs");
      fs.write(data.filename, data.text, 'w');
    } else {
      console.log('unknown event callback: ' + data);
    }
  };

  // log javascript errors
  page.onError = errorHandler;

  // setup listeners for jasmine events
  page.onInitialized = function() {
    return page.evaluate(function() {
      return window.onload = function() {
        jsApiReporter.exitCode = 0;
        jsApiReporter.reportSpecResults = function(spec) {
          if (spec.results().failedCount > 0) {
            jsApiReporter.exitCode = 1;
          }
        };
        jsApiReporter.reportRunnerResults = function() {
          setTimeout(function() {
            window.callPhantom({
              event: 'exit',
              exitCode: jsApiReporter.exitCode
            });
          }, 1);
        };
      };
    });
  };

  var address = args[1];
  console.log('Running: ' + address);
  page.open(address, function(status) {
    if (status !== "success") {
      console.log("can't load the address!");
      return phantom.exit(1);
    }
  });
})();
<|endoftext|>


<commit_before>
import { combine, just, merge, zip }   from 'most'
import proxy                    from 'cycle-proxy/most'

import { simpleClock }          from '../clocks/simple/simpleClock.js'
import rungeKutta               from '../solvers/rungeKuttaMidpoint.js'

function initValue(opt, def) {
  if (opt != undefined && opt != null) return opt
  return def
}

const START = "Start"
const STOP  = "Stop"
const END   = "End"

function rollingSum (state, x) {
  // do the adds
  var newSum = state[0] + x
  var newArr = state[1].concat(x)
  // if rolling, do the removes
  if (newArr.length > 10) {
    newSum -= newArr[0]
    newArr = newArr.slice(1,100)
  }
  // pack and ship
  return [ newSum, newArr ]
}

export function harmonicOscillator (trigger$, options = {} ) {

  // Parameter sources; implies piecewise continuous ODE
  const restPos$  = options.restPos$ ? options.restPos$  : just(0)

  // Parameter constants (this would be so much easier in CoffeeScript)
  const initPos   = initValue(options.initPos, 0)    // pixels... Could be a problem
  const initVel   = initValue(options.initVel, 0)    // pixels/sec... same problem
  const damping   = initValue(options.damping, 1)    // Damping ratio; 1=critical, 0=none
  const frequency = initValue(options.frequency, 1)  // Undamped frequency, Hz

  const omega0    = 2 * Math.PI * frequency  // Undamped angular speed

  const beta      = 2 * damping * omega0
  const omega0_2  = omega0 * omega0

  // The governing function defines the ordinary differential equaiton F=MA for a
  // damped, driven harmonic oscillator. When it changes the ODE problem changes.
  //
  // (Note - this function is somewhat analogous to an Action generator in a React/Flux
  //         or React/Redux app. In both cases the function is called to create a concise
  //         description of a desired change. The big difference is that Action generators //         produce Action objects and the governing function generator produces functions)
  //
  function governingFunction ( y_star ) {
    return function f(y, yd) {
      return [
        yd,                                     // Integrate velocity to get position
        - beta * yd - omega0_2 * (y - y_star)   // integrate acceleration to get velocity
      ]                                         //   where Accel = F/M = (-beta*rd ...)
    }
  }

  // The function stream computes the integration function.  This only needs to change when
  // the parameters for the function change, so it is cached in its own stream.
  const func$ = restPos$.map(governingFunction)

  // Build the control stream.
  const start$   = func$.constant(START)  // START whenever the governing equation changes
    .startWith(START)                     //   ... and when oscillator is instantiated
  const stopP$   = proxy()                // Proxy for "no movement" STOP stream
  const control$ = merge(start$, stopP$)

  // simple clock ticks every animation frame after it has been "Start"-ed
  // and stops ticking when "Stop"-ed.  Emit only the delta time values.
  const delT$ = simpleClock(trigger$, control$)
    .map( t => t[0]/1000)  // delta time, seconds.

  // Pulse produces a [ delta time, integration function ] pair on every clock tick
  const pulse$  = zip(
    ( h, f) => {
      return [ h, f ]  // zip time interval and integration function together
    },
    delT$,
    delT$.sample( ( f ) => f, func$ )
  ).until(control$.filter( x => x == END))      // until done

  // State steps the ODE once with every pulse.  This computes F=MA for the oscillator.
  const state$ = rungeKutta( pulse$, [ initPos, initVel ] )

  // stop$ emits "Stop" when the solution converges, defined as when the
  // rolling sum of |speed| values is very small.
  const stop$ = state$
    .scan(
      (rs, s) => rollingSum(rs, Math.abs(s[1])),  // Adds last n speed values together
      [ 0, [] ]                                   // Initial state
    )
    .map( rs => rs[0] < 0.01) // termination criteria; true if rolling speed sum is small

    //.tap( x => console.log (`Oscillator should stop = ${x}`))

    .skipRepeats()             // Only need one true, so ignore the rest. Resets with a false.
    .filter( x => x )          // Only let through the true values. False means it's moving.
    .map( x => STOP)           // Convert true events to stop messages
    .thru(stopP$.proxy)        // The output.  Passes events into stopP$
    .subscribe({               // Required to activate stop$
      start: (s) => null,
      next: (v) => null,
      complete: (cv) => null,
      error: (ev) => null
    })

  return { state$, control$ }
}
<commit_msg>Updated oscillator<commit_after>
import { combine, just, merge, zip }   from 'most'
import proxy                    from 'cycle-proxy/most'

import { simpleClock }          from '../clocks/simple/simpleClock.js'
import rungeKutta               from '../solvers/rungeKuttaMidpoint.js'

function initValue(opt, def) {
  if (opt != undefined && opt != null) return opt
  return def
}

const START = "Start"
const STOP  = "Stop"
const END   = "End"

function rollingSum (state, x) {
  // do the adds
  var newSum = state[0] + x
  var newArr = state[1].concat(x)
  // if rolling, do the removes
  if (newArr.length > 10) {
    newSum -= newArr[0]
    newArr = newArr.slice(1,100)
  }
  // pack and ship
  return [ newSum, newArr ]
}

export function harmonicOscillator (trigger$, options = {} ) {

  // Parameter sources; implies piecewise continuous ODE
  const restPos$  = options.restPos$ ? options.restPos$  : just(0)

  // Parameter constants (this would be so much easier in CoffeeScript)
  const initPos   = initValue(options.initPos, 0)    // pixels... Could be a problem
  const initVel   = initValue(options.initVel, 0)    // pixels/sec... same problem
  const damping   = initValue(options.damping, 1)    // Damping ratio; 1=critical, 0=none
  const frequency = initValue(options.frequency, 1)  // Undamped frequency, Hz

  const omega0    = 2 * Math.PI * frequency  // Undamped angular speed

  const beta      = 2 * damping * omega0
  const omega0_2  = omega0 * omega0

  // The governing function defines the ordinary differential equaiton F=MA for a
  // damped, driven harmonic oscillator. When it changes the ODE problem changes.
  //
  // (Note - this function is somewhat analogous to an Action generator in a React/Flux
  //         or React/Redux app. In both cases the function is called to create a concise
  //         description of a desired change. The big difference is that Action generators //         produce Action objects and the governing function generator produces functions)
  //
  function governingFunction ( y_star ) {
    return function f(y, yd) {
      return [
        yd,                                     // Integrate velocity to get position
        - beta * yd - omega0_2 * (y - y_star)   // integrate acceleration to get velocity
      ]                                         //   where Accel = F/M = (-beta*rd ...)
    }
  }

  // The function stream computes the integration function.  This only needs to change when
  // the parameters for the function change, so it is cached in its own stream.
  const func$ = restPos$.map(governingFunction)

  // Build the control stream.
  const start$   = func$.constant(START)  // START whenever the governing equation changes
    .startWith(START)                     //   ... and when oscillator is instantiated
  const stopP$   = proxy()                // Proxy for "no movement" STOP stream
  const control$ = merge(start$, stopP$)

  // simple clock ticks every animation frame after it has been "Start"-ed
  // and stops ticking when "Stop"-ed.  Emit only the delta time values.
  const delT$ = simpleClock(trigger$, control$)
    .map( t => t[0]/1000)  // delta time, seconds.

  // Pulse produces a [ delta time, integration function ] pair on every clock tick
  const pulse$  = zip(
    ( h, f) => {
      return [ h, f ]  // zip time interval and integration function together
    },
    delT$,
    delT$.sample( ( f ) => f, func$ )
  ).until(control$.filter( x => x == END))      // until done

  // State steps the ODE once with every pulse.  This computes F=MA for the oscillator.
  const state$ = rungeKutta( pulse$, [ initPos, initVel ] )

  // stop$ emits "Stop" when the solution converges, defined as when the
  // rolling sum of |speed| values is very small.
  const stop$ = state$
    .scan(
      (rs, s) => rollingSum(rs, Math.abs(s[1])),  // Adds last n speed values together
      [ 0, [] ]                                   // Initial state
    )
    .map( rs => rs[0] < 0.01) // termination criteria; true if rolling speed sum is small

    //.tap( x => console.log (`Oscillator should stop = ${x}`))

    .skipRepeats()             // Only need one true, so ignore the rest. Resets with a false.
    .filter( x => x )          // Only let through the true values. False means it's moving.
    .map( x => STOP)           // Convert true events to stop messages
    .thru(stopP$.proxy)        // The output.  Passes events into stopP$
    .subscribe({               // Required to activate stop$
      start: (s) => null,
      next: (v) => null,
      complete: (cv) => null,
      error: (ev) => null
    })

  return { state$, control$ }  // Both streams are output only; state is data, control is meta
}
<|endoftext|>


<commit_before>/*
 * MIT License
 *
 * Copyright (c) 2017-2017 Jan Dockx
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/* eslint-env mocha */

const selectInstance = require('../../lib/selectInstance')
const selectionInstanceContract = selectInstance.contract
const notOneOf = selectInstance.notOneOf

const serviceTypePostfix = '._tcp.dns-sd-lookup.toryt.org'
const serviceType1InstanceNoSubtype = '_t1i-no-sub' + serviceTypePostfix
const serviceType1InstanceSubtype = '_subtype._sub._t7i-sub' + serviceTypePostfix
const serviceType5InstancesWithWeight = '_t8i-5inst' + serviceTypePostfix
const must = require('must')

describe('selectInstance', function () {
  it('works in the nominal case with 1 instance, without a subtype', function () {
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceType1InstanceNoSubtype)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.equal('instance 1.' + serviceType1InstanceNoSubtype)
        console.log(selection)
      }))
  })
  it('works in the nominal case with 1 instance, with a subtype', function () {
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceType1InstanceSubtype)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.equal('instance 7._t7i-sub' + serviceTypePostfix)
        console.log(selection)
      }))
  })
  it('works in the nominal case, with 5 instances', function () {
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceType5InstancesWithWeight)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.equal('instance 8a.' + serviceType5InstancesWithWeight)
        console.log(selection)
      }))
  })
  it('resolves to null with a non-existent service type', function () {
    // noinspection JSUnresolvedVariable
    return selectInstance('_not-exist' + serviceTypePostfix)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        must(selection).be.null()
        console.log(selection)
      }))
  })
  it('selects according to weight with a filter', function () {
    const deaths = [
      `Instance 8a.${serviceType5InstancesWithWeight}`,
      `Instance 8d.${serviceType5InstancesWithWeight}`
    ]
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceType5InstancesWithWeight, notOneOf(deaths))
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.match(`instance 8b.${serviceType5InstancesWithWeight}`)
        console.log(selection)
      }))
  })
  it('selects according to weight with a filter that excludes every instance', function () {
    const deaths = [
      `Instance 8a.${serviceType5InstancesWithWeight}`,
      `Instance 8d.${serviceType5InstancesWithWeight}`,
      `Instance 8c.${serviceType5InstancesWithWeight}`,
      `Instance 8b.${serviceType5InstancesWithWeight}`,
      `Instance 8e.${serviceType5InstancesWithWeight}`
    ]
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceType5InstancesWithWeight, notOneOf(deaths))
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        must(selection).be.null()
        console.log(selection)
      }))
  })
  it('selects according to weight with a filter evenly', function () {
    const batch = 16
    this.timeout(60000)
    const timerLabel = 'selects according to weight evenly'

    const deaths = [
      `Instance 8a.${serviceType5InstancesWithWeight}`,
      `Instance 8b.${serviceType5InstancesWithWeight}`
    ]

    const matchExpr = new RegExp(`(instance 8c|instance 8d)\\.${serviceType5InstancesWithWeight.replace(/\./g, '\\.')}`)

    const expected = {}
    expected[`instance 8c.${serviceType5InstancesWithWeight}`] = 0.3
    expected[`instance 8d.${serviceType5InstancesWithWeight}`] = 0.7

    function totalCount (selections) {
      return Object.keys(selections).reduce(
        (acc, key) => {
          acc += selections[key]
          return acc
        },
        0
      )
    }

    function report (selections) {
      const total = totalCount(selections)
      console.log(`${total} - expected c = 30%, d = 70%`)
      Object.keys(selections).forEach(k => {
        console.log('  %s: %d/%d (%f%%)', k, selections[k], total, Math.round(selections[k] * 1000 / total) / 10)
      })
      console.log()
    }
    console.time(timerLabel)

    function chain (todo, previous) {
      if (todo <= 0) {
        return previous
      }
      return chain(
        todo - batch,
        previous
          .then(selections => {
            const next = []
            for (let i = 0; i < batch; i++) {
              // noinspection JSUnresolvedFunction
              next.push(
                selectInstance(serviceType5InstancesWithWeight, notOneOf(deaths))
                  .then(selectionInstanceContract.resolved.implementation(selection => {
                    selection.must.be.an.object()
                    selection.instance.must.match(matchExpr)
                    selections[selection.instance]
                      ? selections[selection.instance]++
                      : selections[selection.instance] = 1
                  }))
              )
            }
            return Promise.all(next).then(() => {
              report(selections)
              return selections
            })
          })
      )
    }

    return chain(512, Promise.resolve({}))
      .then(selections => {
        console.timeEnd(timerLabel)
        const total = totalCount(selections)
        Object.keys(expected).forEach(e => {
          Math.abs(expected[e] - (selections[e] / total)).must.be.below(0.05)
          /* NOTE: I would like for the deviation from the expected weight distribution to be less than 2.5%.
                   When testing with 2 instances, in 1024 tries, if often happens that the deviation that the deviation
                   is larger. That is surprising. This would mean that a random choice is not good enough.
                   That would imply that we rather need some sort of memory, which would be bad.
                   After this observation, the limit was lowered to 5%, and the tries are lowered to 512, for
                   test speed reasons. */
        })
      })
  })

  let failures = [
    't2i-2-txt',
    't3i-2-srv',
    't4i-2-txt-srv',
    't5i-no-txt',
    't6i-no-srv'
  ]
  failures = failures.map(f => `_${f}${serviceTypePostfix}`)
  failures.forEach(serviceType => {
    it(`fails for instance type ${serviceType}`, function () {
      // noinspection JSUnresolvedVariable
      return selectInstance(serviceType).must.betray(selectionInstanceContract.rejected.implementation(err => {
        console.log(err)
      }))
    })
  })
})
<commit_msg>refactor test to reuse distribution test code<commit_after>/*
 * MIT License
 *
 * Copyright (c) 2017-2017 Jan Dockx
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/* eslint-env mocha */

const selectInstance = require('../../lib/selectInstance')
const selectionInstanceContract = selectInstance.contract
const notOneOf = selectInstance.notOneOf

const serviceTypePostfix = '._tcp.dns-sd-lookup.toryt.org'
const serviceType1InstanceNoSubtype = '_t1i-no-sub' + serviceTypePostfix
const serviceType1InstanceSubtype = '_subtype._sub._t7i-sub' + serviceTypePostfix
const serviceTypeNInstancesWithWeight = '_t8i-n-inst' + serviceTypePostfix
const manyInstanceCount = 12
const must = require('must')

const batch = 16

function testDistribution (timerLabel, deaths, expected) {
  const expectedPattern = Object.keys(expected).join('|')
  const matchExpr = new RegExp(`(${expectedPattern})\\.${serviceTypeNInstancesWithWeight.replace(/\./g, '\\.')}`)

  function totalCount (selections) {
    return Object.keys(selections).reduce(
      (acc, key) => {
        acc += selections[key]
        return acc
      },
      0
    )
  }

  const expectedStr = Object.keys(expected).map(k => `${k} = ${expected[k] * 100}%`).join(', ')

  function report (selections) {
    const total = totalCount(selections)
    console.log(`${total} - expected ${expectedStr}`)
    Object.keys(selections).forEach(k => {
      console.log('  %s: %d/%d (%f%%)', k, selections[k], total, Math.round(selections[k] * 1000 / total) / 10)
    })
    console.log()
  }

  function chain (todo, previous) {
    if (todo <= 0) {
      return previous
    }
    return chain(
      todo - batch,
      previous.then(selections => {
        const next = []
        for (let i = 0; i < batch; i++) {
          // noinspection JSUnresolvedFunction
          next.push(
            selectInstance(serviceTypeNInstancesWithWeight, notOneOf(deaths))
              .then(selectionInstanceContract.resolved.implementation(selection => {
                selection.must.be.an.object()
                selection.instance.must.match(matchExpr)
                selections[selection.instance]
                  ? selections[selection.instance]++
                  : selections[selection.instance] = 1
              }))
          )
        }
        return Promise.all(next).then(() => {
          report(selections)
          return selections
        })
      })
    )
  }

  console.time(timerLabel)

  return chain(512, Promise.resolve({}))
    .then(selections => {
      console.timeEnd(timerLabel)
      const total = totalCount(selections)
      Object.keys(expected).forEach(e => {
        Math.abs(expected[e] - (selections[e + '.' + serviceTypeNInstancesWithWeight] / total)).must.be.below(0.05)
        /* NOTE: I would like for the deviation from the expected weight distribution to be less than 2.5%.
                 When testing with 2 instances, in 1024 tries, if often happens that the deviation that the deviation
                 is larger. That is surprising. This would mean that a random choice is not good enough.
                 That would imply that we rather need some sort of memory, which would be bad.
                 After this observation, the limit was lowered to 5%, and the tries are lowered to 512, for
                 test speed reasons. */
      })
    })
}

describe('selectInstance', function () {
  it('works in the nominal case with 1 instance, without a subtype', function () {
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceType1InstanceNoSubtype)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.equal('instance 1.' + serviceType1InstanceNoSubtype)
        console.log(selection)
      }))
  })
  it('works in the nominal case with 1 instance, with a subtype', function () {
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceType1InstanceSubtype)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.equal('instance 7._t7i-sub' + serviceTypePostfix)
        console.log(selection)
      }))
  })
  it(`works in the nominal case, with ${manyInstanceCount} instances`, function () {
    this.timeout(10000)

    // noinspection JSUnresolvedVariable
    return selectInstance(serviceTypeNInstancesWithWeight)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.equal('instance 8a.' + serviceTypeNInstancesWithWeight)
        console.log(selection)
      }))
  })
  it('resolves to null with a non-existent service type', function () {
    // noinspection JSUnresolvedVariable
    return selectInstance('_not-exist' + serviceTypePostfix)
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        must(selection).be.null()
        console.log(selection)
      }))
  })
  it('selects according to weight with a filter', function () {
    this.timeout(10000)

    const deaths = [
      `Instance 8a.${serviceTypeNInstancesWithWeight}`,
      `Instance 8d.${serviceTypeNInstancesWithWeight}`
    ]
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceTypeNInstancesWithWeight, notOneOf(deaths))
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        selection.must.be.an.object()
        selection.instance.must.match(`instance 8b.${serviceTypeNInstancesWithWeight}`)
        console.log(selection)
      }))
  })
  it('selects according to weight with a filter that excludes every instance', function () {
    const deaths = [
      `Instance 8c.${serviceTypeNInstancesWithWeight}`,
      `Instance 8e.${serviceTypeNInstancesWithWeight}`,
      `Instance 8c.${serviceTypeNInstancesWithWeight}`,
      `Instance 8a.${serviceTypeNInstancesWithWeight}`,
      `Instance 8f.${serviceTypeNInstancesWithWeight}`,
      `Instance 8e.${serviceTypeNInstancesWithWeight}`,
      `Instance 8j.${serviceTypeNInstancesWithWeight}`,
      `Instance 8i.${serviceTypeNInstancesWithWeight}`,
      `Instance 8e.${serviceTypeNInstancesWithWeight}`,
      `Instance 8g.${serviceTypeNInstancesWithWeight}`,
      `Instance 8k.${serviceTypeNInstancesWithWeight}`,
      `Instance 8h.${serviceTypeNInstancesWithWeight}`,
      `Instance 8d.${serviceTypeNInstancesWithWeight}`,
      `Instance 8l.${serviceTypeNInstancesWithWeight}`,
      `Instance 8b.${serviceTypeNInstancesWithWeight}`
    ]
    // noinspection JSUnresolvedVariable
    return selectInstance(serviceTypeNInstancesWithWeight, notOneOf(deaths))
      .must.fulfill(selectionInstanceContract.resolved.implementation(selection => {
        must(selection).be.null()
        console.log(selection)
      }))
  })

  const labelA = 'selects according to weight with a filter evenly'
  it(labelA, function () {
    this.timeout(60000)

    const deaths = [
      `Instance 8a.${serviceTypeNInstancesWithWeight}`,
      `Instance 8b.${serviceTypeNInstancesWithWeight}`
    ]

    const expected = {}
    expected[`instance 8c`] = 0.3
    expected[`instance 8d`] = 0.7

    return testDistribution(labelA, deaths, expected)
  })

  let failures = [
    't2i-2-txt',
    't3i-2-srv',
    't4i-2-txt-srv',
    't5i-no-txt',
    't6i-no-srv'
  ]
  failures = failures.map(f => `_${f}${serviceTypePostfix}`)
  failures.forEach(serviceType => {
    it(`fails for instance type ${serviceType}`, function () {
      // noinspection JSUnresolvedVariable
      return selectInstance(serviceType).must.betray(selectionInstanceContract.rejected.implementation(err => {
        console.log(err)
      }))
    })
  })
})
<|endoftext|>