<commit_before>var _ = require('lodash');
var async = require('async');
var string = require('../module/util/string');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
var Issue = require('./issue');
var User = require('./user');
var stages = require('./stages');
var ew = require('../module/util/asyncWrap').errorWrap;
var hooks = {github: require('./hook/github')};
var moveToBefore = require('../module/util/array').moveToBefore;
var GitHubHook = require('./hook/github');

var ID_LENGTH = 12;

var createID = string.createRandomCode.bind(string, ID_LENGTH);

var Projects = new Schema({
    id: { type: String, default: createID, required: true },
    name: { type: String, required: true },
    members: [
        {
            user: { type: ObjectId, ref: 'User', required: true },
            created_at: { type: Date, default: Date.now, required: true },
            accessLevel: { type: String, default: 1, required: true },
            wipLimit: { type: Number, default: 4, required: true }
        }
    ],
    issues: [Issue.schema],
    github: {  // option
        userName: { type: String },
        repoName: { type: String },
        url: { type: String },
        sync: { type: Boolean, default: true, required: true },
        hook: {
            id: { type: String }
        }
    },
    create_user: { type: ObjectId, ref: 'User', required: true },
    created_at: { type: Date, default: Date.now, required: true }
});

Projects.static('findPopulated', function (where, o, callback) {
    o = o || {};

    this.find(where)
        .populate('create_user')
        .populate('members.user')
        .exec(ew(callback, function (docs) {
            if (o.one) {
                callback(null, docs.length ? docs[0] : null);
            } else {
                callback(null, docs);
            }
        }));
});

Projects.static('findPopulatedByMemberName', function (username, callback) {
    // find queryで取り出せないので、とりあえず全部取ってきちゃう
    this.findPopulated({}, {}, function (err, docs) {
        if (err) { return callback(err); }
        var containMembers = docs.filter(function (project) {
            return _.some(project.members, function (member) {
                return String(member.user.userName) === String(username);
            });
        });
        callback(null, containMembers);
    });
});

/*** member ***/
// もし存在しないユーザなら作成する
describe('addMember', function (userName, callback) {
    var project = this;

    User.findOrCreate(userName, ew(callback, function (user) {
        if (project.findMemberById(user._id)) { return callback(new Error('already added member')); }

        project.members.unshift({user: user});

        project.save(ew(callback, function (project) {
            project.populate('members.user', ew(callback, function (project) {
                callback(null, project, project.members[0]);
            }));
        }));
    }));
});

// メンバーに対して何か操作する
describe('operationMember', function (userName, predecate, callback) {
    var project = this;

    project.populate('members.user', ew(callback, function (project) {
        var member = project.findMemberByName(userName);
        if (!member) { return callback(new Error('undefined member name: ' + userName)); }

        predecate(project, member);

        project.save(ew(callback, function (project) {
            callback(null, project, member);
        }));
    }));
});

describe('removeMember', function (userName, callback) {
    this.operationMember(userName, function (project, member) {
        project.members.pull(member);
    }, callback);
});

describe('updateMember', function (userName, updateParams, callback) {
    this.operationMember(userName, function (project, member) {
        _.each(updateParams, function (value, key) {
            member[key] = value;
        });
    }, callback);
});

describe('updateMemberOrder', function (userName, insertBeforeOfUserName, callback) {
    this.populate('members.user', ew(callback, function (project) {
        var target = project.findMemberByName(userName);
        if (!target) { return callback(new Error('undefined member name: ' + userName)); }

        var insertBefore = null;
        if (insertBeforeOfUserName) {
            insertBefore = project.findMemberByName(insertBeforeOfUserName);
            if (!insertBefore) { return callback(new Error('undefined insert before of member name: ' + insertBeforeOfUserName)); }
        }

        moveToBefore(project.members, target, insertBefore);

        project.save(function (err, project) {
            if (err) { return callback(err); }
            callback(err, project, target, insertBefore);
        });
    }));
});

/*** /member ***/

/*** issue ***/
describe('addIssue', function (token, params, callback) {
    var project = this;

    Issue.create(params, ew(callback, function (issue) {
        project.issues.unshift(issue);
        project.save(ew(callback, function (project) {
            callback(null, project, issue);
            project.eachHooks('addIssue', {github: token}, params);
        }));
    }));
});

describe('removeIssue', function (token, issueId, callback) {
    // remove equals archive
    this.updateStage(token, issueId, stages.archive, null, callback);
});

// ステージとアサインを更新する
describe('updateStage', function (token, issueId, toStage, userId, callback) {
    var project = this;

    // issue取り出し
    var issue = this.findIssueById(issueId);
    if (!issue) { return callback(new Error('undefined issue id: ' + issueId)); }

    // member取り出し
    if (userId && !this.findMemberById(userId)) {
        return callback(new Error('undefined user id in this project: ' + userId));
    }

    issue.assignee = userId;
    issue.stage = toStage;
    project.save(function (err, doc) {
        if (err) { return callback(err); }
        callback(err, doc/*is project*/, issue);
        project.eachHooks('assignIssue', {github: token}, {issue: issue, userId: userId});
    });
});

describe('updateIssueDetail', function (token, issueId, title, body, callback) {
    var project = this;
    var GitHub = require('./github');

    // issue取り出し
    var issue = this.findIssueById(issueId);
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // 更新用オブジェクト
    var src = {};
    if (issue.title !== title) { src.title = title; }
    if (issue.body !== body) { src.body = body; }

    _.extend(issue, src);
    project.save(function (err, project) {
        callback(err, project, issue);
        project.eachHooks('updateDetailIssue', {github: token}, {issue: issue, detail: src});
    });
});

describe('updateIssuePriority', function (issueId, insertBeforeOfIssueId, callback) {
    var project = this;

    var targetIssue = project.findIssueById(issueId);
    if (!targetIssue) { return callback(new Error('undefined issue id: ' + issueId)); }

    var insertBeforeOfIssue = null;
    if (insertBeforeOfIssueId) {
        insertBeforeOfIssue = project.findIssueById(insertBeforeOfIssueId);
        if (!insertBeforeOfIssue) { return callback(new Error('undefined insert before of issue id: ' + insertBeforeOfIssueId)); }
    }

    moveToBefore(project.issues, targetIssue, insertBeforeOfIssue);

    project.save(function (err, project) {
        if (err) { return callback(err); }
        callback(err, project, targetIssue, insertBeforeOfIssueId);
    });
});

/*** /issue ***/

Projects.static('exists', function (where, callback) {
    this.count(where, function (err, count) {
        if (err) { callback(err); return; }
        callback(null, !!count);
    });
});

describe('findIssue', function (predicate, callback) {
    var issue = _.find(this.issues, predicate);
    if (callback) { callback(null, issue); }
    return issue;
});

describe('findIssueById', function (issueId, callback) {
    return this.findIssue(function (issue) {
        return String(issue._id) === String(issueId);
    }, callback);
});

describe('findMember', function (predicate, callback) {
    var member = _.find(this.members, predicate);
    if (callback) { callback(null, member); }
    return member;
});

describe('findMemberById', function (userId, callback) {
    return this.findMember(function (member) {
        return String(member.user._id ? member.user._id : member.user) === String(userId);
    }, callback);
});

describe('findMemberByName', function (userName, callback) {
    return this.findMember(function (member) {
        return String(member.user.userName) === String(userName);
    }, callback);
});

describe('eachHooks', function (methodName, tokens, args, anyDoneCallback, allSkipedCallback) {
    var project = this;
    anyDoneCallback = anyDoneCallback || _.noop;
    allSkipedCallback = allSkipedCallback || _.noop;

    async.series(_.map(hooks, function (Hook, hookName) {
        return function (next) {
            var token = tokens[hookName];
            if (!token || !Hook || !Hook.isHooked(project) || !Hook.prototype[methodName]) {
                return next(null);
            }

            (new Hook(token))[methodName](project, args, next);
        };
    }), function (err, results) {
        if (err) {
            anyDoneCallback(err);
        } else if(_.every(results, _.negate(Boolean))) {
            allSkipedCallback();
        } else {
            anyDoneCallback(null, results);
        }
    });
});

/*** helper ***/

function methodToStatic(methodName/* ,args..., callback */) {
    return function (projectWhere) {
        var callback = arguments[arguments.length - 1];
        var args = Array.prototype.splice.call(arguments, 1);

        this.findOne(projectWhere, function (err, project) {
            if (err) { callback(err); return; }
            if (!project) { callback(new Error('undefined project')); return; }

            project[methodName].apply(project, args);
        });
    };
}

function describe(name, func) {
    Projects.method(name, func);
    Projects.static(name, methodToStatic(name));
}

if (!mongoose.models.Project) { module.exports = mongoose.model('Project', Projects); }
else { module.exports = mongoose.model('Project'); }<commit_msg>github sync時にadd issueしたら2つissueが追加される不具合の修正<commit_after>var _ = require('lodash');
var async = require('async');
var string = require('../module/util/string');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
var Issue = require('./issue');
var User = require('./user');
var stages = require('./stages');
var ew = require('../module/util/asyncWrap').errorWrap;
var hooks = {github: require('./hook/github')};
var moveToBefore = require('../module/util/array').moveToBefore;
var GitHubHook = require('./hook/github');

var ID_LENGTH = 12;

var createID = string.createRandomCode.bind(string, ID_LENGTH);

var Projects = new Schema({
    id: { type: String, default: createID, required: true },
    name: { type: String, required: true },
    members: [
        {
            user: { type: ObjectId, ref: 'User', required: true },
            created_at: { type: Date, default: Date.now, required: true },
            accessLevel: { type: String, default: 1, required: true },
            wipLimit: { type: Number, default: 4, required: true }
        }
    ],
    issues: [Issue.schema],
    github: {  // option
        userName: { type: String },
        repoName: { type: String },
        url: { type: String },
        sync: { type: Boolean, default: true, required: true },
        hook: {
            id: { type: String }
        }
    },
    create_user: { type: ObjectId, ref: 'User', required: true },
    created_at: { type: Date, default: Date.now, required: true }
});

Projects.static('findPopulated', function (where, o, callback) {
    o = o || {};

    this.find(where)
        .populate('create_user')
        .populate('members.user')
        .exec(ew(callback, function (docs) {
            if (o.one) {
                callback(null, docs.length ? docs[0] : null);
            } else {
                callback(null, docs);
            }
        }));
});

Projects.static('findPopulatedByMemberName', function (username, callback) {
    // find queryで取り出せないので、とりあえず全部取ってきちゃう
    this.findPopulated({}, {}, function (err, docs) {
        if (err) { return callback(err); }
        var containMembers = docs.filter(function (project) {
            return _.some(project.members, function (member) {
                return String(member.user.userName) === String(username);
            });
        });
        callback(null, containMembers);
    });
});

/*** member ***/
// もし存在しないユーザなら作成する
describe('addMember', function (userName, callback) {
    var project = this;

    User.findOrCreate(userName, ew(callback, function (user) {
        if (project.findMemberById(user._id)) { return callback(new Error('already added member')); }

        project.members.unshift({user: user});

        project.save(ew(callback, function (project) {
            project.populate('members.user', ew(callback, function (project) {
                callback(null, project, project.members[0]);
            }));
        }));
    }));
});

// メンバーに対して何か操作する
describe('operationMember', function (userName, predecate, callback) {
    var project = this;

    project.populate('members.user', ew(callback, function (project) {
        var member = project.findMemberByName(userName);
        if (!member) { return callback(new Error('undefined member name: ' + userName)); }

        predecate(project, member);

        project.save(ew(callback, function (project) {
            callback(null, project, member);
        }));
    }));
});

describe('removeMember', function (userName, callback) {
    this.operationMember(userName, function (project, member) {
        project.members.pull(member);
    }, callback);
});

describe('updateMember', function (userName, updateParams, callback) {
    this.operationMember(userName, function (project, member) {
        _.each(updateParams, function (value, key) {
            member[key] = value;
        });
    }, callback);
});

describe('updateMemberOrder', function (userName, insertBeforeOfUserName, callback) {
    this.populate('members.user', ew(callback, function (project) {
        var target = project.findMemberByName(userName);
        if (!target) { return callback(new Error('undefined member name: ' + userName)); }

        var insertBefore = null;
        if (insertBeforeOfUserName) {
            insertBefore = project.findMemberByName(insertBeforeOfUserName);
            if (!insertBefore) { return callback(new Error('undefined insert before of member name: ' + insertBeforeOfUserName)); }
        }

        moveToBefore(project.members, target, insertBefore);

        project.save(function (err, project) {
            if (err) { return callback(err); }
            callback(err, project, target, insertBefore);
        });
    }));
});

/*** /member ***/

/*** issue ***/
describe('addIssue', function (token, params, callback) {
    var project = this;

    project.eachHooks('addIssue', {github: token}, params,
        // any done
        function (err, results) {
            callback(null, project, null);
        },
        // all skipped
        function () {
            Issue.create(params, ew(callback, function (issue) {
                project.issues.unshift(issue);
                project.save(ew(callback, function (project) {
                    callback(null, project, issue);
                }));
            }));
        });
});

describe('removeIssue', function (token, issueId, callback) {
    // remove equals archive
    this.updateStage(token, issueId, stages.archive, null, callback);
});

// ステージとアサインを更新する
describe('updateStage', function (token, issueId, toStage, userId, callback) {
    var project = this;

    // issue取り出し
    var issue = this.findIssueById(issueId);
    if (!issue) { return callback(new Error('undefined issue id: ' + issueId)); }

    // member取り出し
    if (userId && !this.findMemberById(userId)) {
        return callback(new Error('undefined user id in this project: ' + userId));
    }

    issue.assignee = userId;
    issue.stage = toStage;
    project.save(function (err, doc) {
        if (err) { return callback(err); }
        callback(err, doc/*is project*/, issue);
        project.eachHooks('assignIssue', {github: token}, {issue: issue, userId: userId});
    });
});

describe('updateIssueDetail', function (token, issueId, title, body, callback) {
    var project = this;
    var GitHub = require('./github');

    // issue取り出し
    var issue = this.findIssueById(issueId);
    if (!issue) { callback(new Error('undefined issue id: ' + issueId)); return; }

    // 更新用オブジェクト
    var src = {};
    if (issue.title !== title) { src.title = title; }
    if (issue.body !== body) { src.body = body; }

    _.extend(issue, src);
    project.save(function (err, project) {
        callback(err, project, issue);
        project.eachHooks('updateDetailIssue', {github: token}, {issue: issue, detail: src});
    });
});

describe('updateIssuePriority', function (issueId, insertBeforeOfIssueId, callback) {
    var project = this;

    var targetIssue = project.findIssueById(issueId);
    if (!targetIssue) { return callback(new Error('undefined issue id: ' + issueId)); }

    var insertBeforeOfIssue = null;
    if (insertBeforeOfIssueId) {
        insertBeforeOfIssue = project.findIssueById(insertBeforeOfIssueId);
        if (!insertBeforeOfIssue) { return callback(new Error('undefined insert before of issue id: ' + insertBeforeOfIssueId)); }
    }

    moveToBefore(project.issues, targetIssue, insertBeforeOfIssue);

    project.save(function (err, project) {
        if (err) { return callback(err); }
        callback(err, project, targetIssue, insertBeforeOfIssueId);
    });
});

/*** /issue ***/

Projects.static('exists', function (where, callback) {
    this.count(where, function (err, count) {
        if (err) { callback(err); return; }
        callback(null, !!count);
    });
});

describe('findIssue', function (predicate, callback) {
    var issue = _.find(this.issues, predicate);
    if (callback) { callback(null, issue); }
    return issue;
});

describe('findIssueById', function (issueId, callback) {
    return this.findIssue(function (issue) {
        return String(issue._id) === String(issueId);
    }, callback);
});

describe('findMember', function (predicate, callback) {
    var member = _.find(this.members, predicate);
    if (callback) { callback(null, member); }
    return member;
});

describe('findMemberById', function (userId, callback) {
    return this.findMember(function (member) {
        return String(member.user._id ? member.user._id : member.user) === String(userId);
    }, callback);
});

describe('findMemberByName', function (userName, callback) {
    return this.findMember(function (member) {
        return String(member.user.userName) === String(userName);
    }, callback);
});

describe('eachHooks', function (methodName, tokens, args, anyDoneCallback, allSkipedCallback) {
    var project = this;
    anyDoneCallback = anyDoneCallback || _.noop;
    allSkipedCallback = allSkipedCallback || _.noop;

    async.series(_.map(hooks, function (Hook, hookName) {
        return function (next) {
            var token = tokens[hookName];
            if (!token || !Hook || !Hook.isHooked(project) || !Hook.prototype[methodName]) {
                return next(null);
            }

            (new Hook(token))[methodName](project, args, next);
        };
    }), function (err, results) {
        if (err) {
            anyDoneCallback(err);
        } else if(_.every(results, _.negate(Boolean))) {
            allSkipedCallback();
        } else {
            anyDoneCallback(null, results);
        }
    });
});

/*** helper ***/

function methodToStatic(methodName/* ,args..., callback */) {
    return function (projectWhere) {
        var callback = arguments[arguments.length - 1];
        var args = Array.prototype.splice.call(arguments, 1);

        this.findOne(projectWhere, function (err, project) {
            if (err) { callback(err); return; }
            if (!project) { callback(new Error('undefined project')); return; }

            project[methodName].apply(project, args);
        });
    };
}

function describe(name, func) {
    Projects.method(name, func);
    Projects.static(name, methodToStatic(name));
}

if (!mongoose.models.Project) { module.exports = mongoose.model('Project', Projects); }
else { module.exports = mongoose.model('Project'); }<|endoftext|>


<commit_before>var env = process.env;
var EventEmitter = require('events').EventEmitter;

var broadcast = require(env.Z_PATH_PROJECT + 'send').broadcast;
var model = require(env.Z_PATH_MODELS + 'factory');
var view = require(env.Z_PATH_VIEWS + 'factory');
var cache = require(env.Z_PATH_CACHE + 'cache');

// init caches
var pojoInstances = cache.pojo('instances');
var compInstances = cache.comp('instances');
var pojoModules = cache.pojo('modules');
var compModules = cache.comp('modules');
var fileClient = cache.file('client');

module.exports = init;

function pathValue (path, scope) {

    if (!path) {
        return;
    }

    var o = path;
    path = path.split('.');
    scope = scope || this;

    // find keys in paths or return
    for (var i = 0; i < path.length; ++i) {
        if (!(scope = scope[path[i]])) {
            return;
        }
    }

    return scope;
}

function sendHandler (instanceName, self) {
    return function (err, data) {
        // websocket link
        if (self.link && self.link.ws) {
            self.link.send(err, data, instanceName);
        }
    };
}

function linkHandler (link) {
    var self = this;

    // listen to event
    self[link['1'] ? 'once' : 'on'](link.event, function (err, data) {
        self = this;

        // emit or call on a different instance
        // TODO check role access!
        if (link.to) {
            self = pojoInstances.get(link.to, self.link.ws.session.role);
            if (!self) {
                return;
            }
        }

        // create an empty data object
        if (!data && (link.data || link.add)) {
            data = {};
        }

        // handle static data
        if (link.data) {

            // create copy
            try {
                var copy = JSON.parse(JSON.stringify(link.data));

                // merge static data in data
                for (var key in copy) {
                    data[key] = copy[key];
                }
            } catch (error) {
                err = error;
            }
        }

        // add dynamic data to the data object
        if (link.add) {
            for (var prop in link.add) {
                data[prop] = pathValue(link.add[prop], self);
            }
        }

        // handle method call
        if (link.call) {

            if (typeof link.call === 'string') {
                link.call = pathValue(link.call, self);
            }

            link.call.call(self, err, data, sendHandler(self._name, self));
        }

        // emit event
        if (link.emit) {
            self.emit(link.emit, err, data, sendHandler(self._name, self));
        }
    });

    // ensure clinet send array
    if (!self._client.send) {
        self._client.send = [];
    }

    // add send events to client
    self._client.send.push(link.event);
    self._access[link.event] = true;
}

// set up link configuration
function linkSetup (instance, links, callback) {

    // set up links
    for (var i = 0, link; i < links.length; ++i) {
        linkHandler.call(instance, links[i]);
    }

    callback(null, instance._client);
}

// create instance
function instanceFactory (name, module, compInst, callback) {

    // create new Mono observer instance
    var instance = new EventEmitter();
    instance._module = module.name.replace(/_/g, '/') + '/';
    instance._name = compInst.name;
    instance._access = {};
    instance._roles = compInst.roles || {};
    instance._client = {};

    // save configs on instance
    if (compInst.config) {

        // save custom client config
        if (compInst.config.client) {
            instance._client = {
                config: compInst.config.client
            };
        }

        // server config
        instance._config = compInst.config.server || {};
    }

    // save observe infos in client
    if (compInst.O) {
        instance._client.O = compInst.O;
    }

    // save module name in client config
    instance._client.module = instance._module;

    // save load infos in client
    if (compInst.L) {
        instance._client.L = {};

        if (compInst.L.elms) {
            instance._client.L.elms = compInst.L.elms;
        }
    }

    // merge scripts
    // all dependencies of the instance dependencies must be in this array!
    // while installing a module, save all dependencies in the module deps
    // dependencies: ['./file.js', 's/o/modA/v']
    // => ModA deps: ['./file.js', 's/o/modB/v'] => ModB deps: []
    // => to client: ['./file.js', 's/o/n/v/file.js']
    if (module.dependencies) {

        // append custom instance scripts
        if (compInst.L && compInst.L.scripts) {
            instance._client.L.scripts = module.dependencies.concat(compInst.L.scripts);

        } else {
            instance._client.L = instance._client.L || {};
            instance._client.L.scripts = module.dependencies;
        }
    } else if (compInst.L && compInst.L.scripts) {
        instance._client.L.scripts = compInst.L.scripts;
    }

    // setup model events and add model factory to instance
    model.setup(instance);
    instance._model = model.factory;

    // setup view events and add model factory to instance
    instance._view = view(instance);

    // handle send configurations
    if (!compInst.links) {
        compInst.links = [];
    }

    // set up core send events
    compInst.links.push({event: env.Z_SEND_INST_REQ, call: load});
    compInst.links.push({event: env.Z_SEND_MODEL_REQ, call: instance._modelService});
    compInst.links.push({event: env.Z_SEND_VIEW_REQ, call: instance._viewService});

    // attach the broadcast functionality
    instance._broadcast = broadcast;

    // send original instance name to client, in case it's an alias
    if (compInst.name !== name) {
        instance._client.name = compInst.name;

        // save instance also under original name
        pojoInstances.set(compInst.name, instance);
    }

    // save instance in cache
    // TODO update this cache when a instance config changes
    pojoInstances.set(name, instance);

    // require and init mono module
    if (module.main) {

        try {
            var monoModule = require(env.Z_PATH_PROCESS_MODULES + instance._module + module.main);

            if (typeof monoModule === 'function') {
                monoModule.call(instance, instance._config || {}, function (err) {

                    if (err) {
                        return callback(err);
                    }

                    // setup links
                    if (compInst.links) {
                        return linkSetup(instance, compInst.links, callback);
                    }

                    callback(null, instance._client);
                });
            }
        } catch (err) {

            // remove instance from cache
            pojoInstances.rm(name);

            callback('Module ' + compInst.name + ' init error: ' + err.toString());
        }
    } else {

        // setup links
        if (compInst.links) {
            return linkSetup(instance, compInst.links, callback);
        }

        callback(null, instance._client);
    }
}

// init module with newly created module instance as this
function loadinstance (name, role, callback) {

    // get and set model config from composition store
    compInstances.set(name, role, function (err, instance) {

        if (err) {
            return callback(err);
        }
        // check access
        if (!instance) {
            return callback(new Error('Instance ' + name + ' not found.'));
        }

        compModules.set(instance.module, role, function (err, module) {

            if (err) {
                return callback(err);
            }

            // check access
            if (!module) {
                return callback(new Error('Module ' + instance.module + ' not found.'));
            }

            instanceFactory(name, module, instance, callback);
        });
    });
}

// load instance configuration (ws)
function load (err, instance, callback) {
    var self = this;

    // send client config from cache
    var cachedInstance = pojoInstances.get(instance, self.link.role);

    // handle no access case
    if (cachedInstance === 0) {
        return callback('Instance: ' + instance + '. Error: Instance not found.');
    }

    if (cachedInstance) {
        return callback(null, cachedInstance._client);
    }

    // load and init module
    loadinstance(instance, self.link.role, function (err, client) {

        if (err) {
            return callback('Instance: ' + instance + '. Error: ' + err);
        }

        // return client config
        callback(null, client);
    });
}

// init the core module
function init () {
    var instance = new EventEmitter();

    instance._module = 'core',
    instance._name = env.Z_CORE_INST,
    instance._access = {},

    // set public rights to core module
    instance._roles = {'*': true};

    // set core module access
    instance._access[env.Z_SEND_INST_REQ] = true;
    instance._access[env.Z_SEND_MODULE_REQ] = true;
    instance._access[env.Z_SEND_CLIENT_REQ] = true;

    // setup client event interface
    instance.on(env.Z_SEND_INST_REQ, function (err, data) {
        var self = this;
        load.call(self, err, data, sendHandler(instance._name, self));
    });
    instance.on(env.Z_SEND_MODULE_REQ, moduleFiles);
    instance.on(env.Z_SEND_CLIENT_REQ, client);

    // setup model events and add model factory to instance
    model.setup(instance);
    instance._model = model.factory;

    // attach the broadcast functionality
    instance._broadcast = broadcast;

    // init mono module and save in cache
    pojoInstances.set(instance._name, instance);
}

// load client module files (http)
function moduleFiles () {
    var self = this;

    var source = self.link.path[0];
    var owner = self.link.path[1];
    var name = self.link.path[2];
    var version = self.link.path[3];

    // check if request format is correct
    if (!source || !owner || !name || !version) {
        return self.link.send(400, "Incorrect module request URL format");
    }

    // the module name must be almost alphanumeric
    if (self.link.pathname.replace(/[^a-z0-9\/\.\-_@]|\.\.\//gi, "") !== self.link.pathname) {
        return self.link.send(400, "Incorrect data in module request URL");
    }

    // create file path
    var path = env.Z_PATH_PROCESS_MODULES + self.link.path.join('/');

    // save compressed/compiled script in cache and send it to the client
    fileClient.set(path, function (err, script) {

        if (err) {
            self.link.res.writeHead(500, {'content-type': 'text/plain'});
            return self.link.res.end(err.toString());
        }

        self.link.res.writeHead(200, script.http);
        self.link.res.end(script.data);
        return;
    });
}

// get mono client (http)
function client (){
    var self = this;
    var path = env.Z_PATH_CLIENT + self.link.path[0];

    // save compressed/compiled script in cache and send it to the client
    fileClient.set(path, function (err, script) {

        if (err) {
            self.link.res.writeHead(500, {'content-type': 'text/plain'});
            return self.link.res.end(err.toString());
        }

        self.link.res.writeHead(200, script.http);
        self.link.res.end(script.data);
        return;
    });
}
<commit_msg>pass module file path to file cache for wrapping<commit_after>var env = process.env;
var EventEmitter = require('events').EventEmitter;

var broadcast = require(env.Z_PATH_PROJECT + 'send').broadcast;
var model = require(env.Z_PATH_MODELS + 'factory');
var view = require(env.Z_PATH_VIEWS + 'factory');
var cache = require(env.Z_PATH_CACHE + 'cache');

// init caches
var pojoInstances = cache.pojo('instances');
var compInstances = cache.comp('instances');
var pojoModules = cache.pojo('modules');
var compModules = cache.comp('modules');
var fileClient = cache.file('client');

module.exports = init;

function pathValue (path, scope) {

    if (!path) {
        return;
    }

    var o = path;
    path = path.split('.');
    scope = scope || this;

    // find keys in paths or return
    for (var i = 0; i < path.length; ++i) {
        if (!(scope = scope[path[i]])) {
            return;
        }
    }

    return scope;
}

function sendHandler (instanceName, self) {
    return function (err, data) {
        // websocket link
        if (self.link && self.link.ws) {
            self.link.send(err, data, instanceName);
        }
    };
}

function linkHandler (link) {
    var self = this;

    // listen to event
    self[link['1'] ? 'once' : 'on'](link.event, function (err, data) {
        self = this;

        // emit or call on a different instance
        // TODO check role access!
        if (link.to) {
            self = pojoInstances.get(link.to, self.link.ws.session.role);
            if (!self) {
                return;
            }
        }

        // create an empty data object
        if (!data && (link.data || link.add)) {
            data = {};
        }

        // handle static data
        if (link.data) {

            // create copy
            try {
                var copy = JSON.parse(JSON.stringify(link.data));

                // merge static data in data
                for (var key in copy) {
                    data[key] = copy[key];
                }
            } catch (error) {
                err = error;
            }
        }

        // add dynamic data to the data object
        if (link.add) {
            for (var prop in link.add) {
                data[prop] = pathValue(link.add[prop], self);
            }
        }

        // handle method call
        if (link.call) {

            if (typeof link.call === 'string') {
                link.call = pathValue(link.call, self);
            }

            link.call.call(self, err, data, sendHandler(self._name, self));
        }

        // emit event
        if (link.emit) {
            self.emit(link.emit, err, data, sendHandler(self._name, self));
        }
    });

    // ensure clinet send array
    if (!self._client.send) {
        self._client.send = [];
    }

    // add send events to client
    self._client.send.push(link.event);
    self._access[link.event] = true;
}

// set up link configuration
function linkSetup (instance, links, callback) {

    // set up links
    for (var i = 0, link; i < links.length; ++i) {
        linkHandler.call(instance, links[i]);
    }

    callback(null, instance._client);
}

// create instance
function instanceFactory (name, module, compInst, callback) {

    // create new Mono observer instance
    var instance = new EventEmitter();
    instance._module = module.name.replace(/_/g, '/') + '/';
    instance._name = compInst.name;
    instance._access = {};
    instance._roles = compInst.roles || {};
    instance._client = {};

    // save configs on instance
    if (compInst.config) {

        // save custom client config
        if (compInst.config.client) {
            instance._client = {
                config: compInst.config.client
            };
        }

        // server config
        instance._config = compInst.config.server || {};
    }

    // save observe infos in client
    if (compInst.O) {
        instance._client.O = compInst.O;
    }

    // save module name in client config
    instance._client.module = instance._module;

    // save load infos in client
    if (compInst.L) {
        instance._client.L = {};

        if (compInst.L.elms) {
            instance._client.L.elms = compInst.L.elms;
        }
    }

    // merge scripts
    // all dependencies of the instance dependencies must be in this array!
    // while installing a module, save all dependencies in the module deps
    // dependencies: ['./file.js', 's/o/modA/v']
    // => ModA deps: ['./file.js', 's/o/modB/v'] => ModB deps: []
    // => to client: ['./file.js', 's/o/n/v/file.js']
    if (module.dependencies) {

        // append custom instance scripts
        if (compInst.L && compInst.L.scripts) {
            instance._client.L.scripts = module.dependencies.concat(compInst.L.scripts);

        } else {
            instance._client.L = instance._client.L || {};
            instance._client.L.scripts = module.dependencies;
        }
    } else if (compInst.L && compInst.L.scripts) {
        instance._client.L.scripts = compInst.L.scripts;
    }

    // setup model events and add model factory to instance
    model.setup(instance);
    instance._model = model.factory;

    // setup view events and add model factory to instance
    instance._view = view(instance);

    // handle send configurations
    if (!compInst.links) {
        compInst.links = [];
    }

    // set up core send events
    compInst.links.push({event: env.Z_SEND_INST_REQ, call: load});
    compInst.links.push({event: env.Z_SEND_MODEL_REQ, call: instance._modelService});
    compInst.links.push({event: env.Z_SEND_VIEW_REQ, call: instance._viewService});

    // attach the broadcast functionality
    instance._broadcast = broadcast;

    // send original instance name to client, in case it's an alias
    if (compInst.name !== name) {
        instance._client.name = compInst.name;

        // save instance also under original name
        pojoInstances.set(compInst.name, instance);
    }

    // save instance in cache
    // TODO update this cache when a instance config changes
    pojoInstances.set(name, instance);

    // require and init mono module
    if (module.main) {

        try {
            var monoModule = require(env.Z_PATH_PROCESS_MODULES + instance._module + module.main);

            if (typeof monoModule === 'function') {
                monoModule.call(instance, instance._config || {}, function (err) {

                    if (err) {
                        return callback(err);
                    }

                    // setup links
                    if (compInst.links) {
                        return linkSetup(instance, compInst.links, callback);
                    }

                    callback(null, instance._client);
                });
            }
        } catch (err) {

            // remove instance from cache
            pojoInstances.rm(name);

            callback('Module ' + compInst.name + ' init error: ' + err.toString());
        }
    } else {

        // setup links
        if (compInst.links) {
            return linkSetup(instance, compInst.links, callback);
        }

        callback(null, instance._client);
    }
}

// init module with newly created module instance as this
function loadinstance (name, role, callback) {

    // get and set model config from composition store
    compInstances.set(name, role, function (err, instance) {

        if (err) {
            return callback(err);
        }
        // check access
        if (!instance) {
            return callback(new Error('Instance ' + name + ' not found.'));
        }

        compModules.set(instance.module, role, function (err, module) {

            if (err) {
                return callback(err);
            }

            // check access
            if (!module) {
                return callback(new Error('Module ' + instance.module + ' not found.'));
            }

            instanceFactory(name, module, instance, callback);
        });
    });
}

// load instance configuration (ws)
function load (err, instance, callback) {
    var self = this;

    // send client config from cache
    var cachedInstance = pojoInstances.get(instance, self.link.role);

    // handle no access case
    if (cachedInstance === 0) {
        return callback('Instance: ' + instance + '. Error: Instance not found.');
    }

    if (cachedInstance) {
        return callback(null, cachedInstance._client);
    }

    // load and init module
    loadinstance(instance, self.link.role, function (err, client) {

        if (err) {
            return callback('Instance: ' + instance + '. Error: ' + err);
        }

        // return client config
        callback(null, client);
    });
}

// init the core module
function init () {
    var instance = new EventEmitter();

    instance._module = 'core',
    instance._name = env.Z_CORE_INST,
    instance._access = {},

    // set public rights to core module
    instance._roles = {'*': true};

    // set core module access
    instance._access[env.Z_SEND_INST_REQ] = true;
    instance._access[env.Z_SEND_MODULE_REQ] = true;
    instance._access[env.Z_SEND_CLIENT_REQ] = true;

    // setup client event interface
    instance.on(env.Z_SEND_INST_REQ, function (err, data) {
        var self = this;
        load.call(self, err, data, sendHandler(instance._name, self));
    });
    instance.on(env.Z_SEND_MODULE_REQ, moduleFiles);
    instance.on(env.Z_SEND_CLIENT_REQ, client);

    // setup model events and add model factory to instance
    model.setup(instance);
    instance._model = model.factory;

    // attach the broadcast functionality
    instance._broadcast = broadcast;

    // init mono module and save in cache
    pojoInstances.set(instance._name, instance);
}

// load client module files (http)
function moduleFiles () {
    var self = this;

    var source = self.link.path[0];
    var owner = self.link.path[1];
    var name = self.link.path[2];
    var version = self.link.path[3];

    // check if request format is correct
    if (!source || !owner || !name || !version) {
        return self.link.send(400, "Incorrect module request URL format");
    }

    // the module name must be almost alphanumeric
    if (self.link.pathname.replace(/[^a-z0-9\/\.\-_@]|\.\.\//gi, "") !== self.link.pathname) {
        return self.link.send(400, "Incorrect data in module request URL");
    }

    // create file path
    var moduleFile = self.link.path.join('/');
    var path = env.Z_PATH_PROCESS_MODULES + moduleFile;

    // save compressed/compiled script in cache and send it to the client
    fileClient.set(path, function (err, script) {

        if (err) {
            self.link.res.writeHead(500, {'content-type': 'text/plain'});
            return self.link.res.end(err.toString());
        }

        self.link.res.writeHead(200, script.http);
        self.link.res.end(script.data);
        return;
    }, null, moduleFile);
}

// get mono client (http)
function client (){
    var self = this;
    var path = env.Z_PATH_CLIENT + self.link.path[0];

    // save compressed/compiled script in cache and send it to the client
    fileClient.set(path, function (err, script) {

        if (err) {
            self.link.res.writeHead(500, {'content-type': 'text/plain'});
            return self.link.res.end(err.toString());
        }

        self.link.res.writeHead(200, script.http);
        self.link.res.end(script.data);
        return;
    });
}
<|endoftext|>


<commit_before>'use strict';

const async = require('async');
const assert = require('assert');
const crypto = require('crypto');
const storj = require('storj-lib');
const MonitorConfig = require('./config');
const StorageModels = require('storj-service-storage-models');
const points = StorageModels.constants.POINTS;
const ComplexClient = require('storj-complex').createClient;
const MongoDBStorageAdapter = require('storj-mongodb-adapter');
const ms = require('ms');
const log = require('../logger');
const errors = require('storj-service-error-types');

/**
 * A long running daemon that will monitor farmers uptime and will replace
 * contracts associated with a farmer once the farmer is confirmed to be
 * offline for a duration of time.
 * @param {MonitorConfig} config - An instance of MonitorConfig
 */
function Monitor(config) {
  if (!(this instanceof Monitor)) {
    return new Monitor(config);
  }

  assert(config instanceof MonitorConfig, 'Invalid config supplied');

  this.storage = null;
  this.network = null;
  this.contracts = null;

  this._config = config;
  this._timeout = null;
  this._running = false;
}

Monitor.SIGINT_CHECK_INTERVAL = 1000;
Monitor.MAX_SIGINT_WAIT = 5000;

/**
 * Starts the Bridge instance
 * @param {Function} callback
 */
Monitor.prototype.start = function(callback) {
  log.info('Farmer monitor service is starting');

  this.storage = new StorageModels(
    this._config.storage.mongoUrl,
    this._config.storage.mongoOpts,
    { logger: log }
  );

  this.network = new ComplexClient(this._config.complex);

  this.contracts = new storj.StorageManager(
    new MongoDBStorageAdapter(this.storage),
    { disableReaper: true }
  );

  // setup next run event
  this.wait();

  callback();
  process.on('SIGINT', this._handleSIGINT.bind(this));
  process.on('exit', this._handleExit.bind(this));
  process.on('uncaughtException', this._handleUncaughtException.bind(this));
};

Monitor.sortByTimeoutRate = function(a, b) {
  const a1 = a.contact.timeoutRate >= 0 ? a.contact.timeoutRate : 0;
  const b1 = b.contact.timeoutRate >= 0 ? b.contact.timeoutRate : 0;
  return (a1 === b1) ? 0 : (a1 > b1) ? 1 : -1;
};

Monitor.prototype._fetchDestinations = function(shard, callback) {
  this.storage.models.Mirror
    .find({ shardHash: shard.hash })
    .populate('contact')
    .exec((err, results) => {
      if (err) {
        return callback(err);
      }
      const mirrors = results.filter((m) => {
        if (!m.contact) {
          log.warn('Mirror %s is missing contact in database', m._id);
          return false;
        } else if (shard.contracts[m.contact._id]) {
          log.warn('Shard %s already established to contact %s',
                   shard.hash, m.contact._id);
          return false;
        } else if (!m.isEstablished) {
          return true;
        }
        return false;
      });
      mirrors.sort(Monitor.sortByTimeoutRate);
      callback(null, mirrors);
    });
};

Monitor.prototype._fetchSources = function(shard, callback) {
  let farmers = Object.keys(shard.contracts);

  this.storage.models.Contact
    .find({ _id: { $in: farmers }})
    .sort({ lastSeen: -1 })
    .exec((err, results) => {
      if (err) {
        return callback(err);
      }

      let contacts = [];
      for (let i = 0; i < results.length; i++) {
        let c = results[i];
        let contact = null;
        try {
          contact = storj.Contact(c.toObject());
        } catch(e) {
          log.warn('Unable to fetch source, invalid contact: %j', c.toObject());
        }
        if (contact) {
          contacts.push(contact);
        }
      }

      callback(null, contacts);
    });
};

Monitor.prototype._saveShard = function(shard, destination, callback) {

  const contract = storj.Contract(destination.contract);
  const contact = storj.Contact(destination.contact);
  shard.addContract(contact, contract);

  this.contracts.save(shard, (err) => {
    if (err) {
      return callback(new Error('Unable to save contract to shard'));
    }

    log.info('Successfully replicated shard %s', shard.hash);
    destination.isEstablished = true;
    destination.save((err) => {
      if (err) {
        return callback(
          new Error('Unable to update mirror as established, reason: ' +
                    err.message)
        );
      }
      callback();
    });
  });
};

Monitor.prototype._createStorageEvent = function(token,
                                                 shardHash,
                                                 shardBytes,
                                                 source,
                                                 destination) {
  const StorageEvent = this.storage.models.StorageEvent;

  const storageEvent = new StorageEvent({
    token: token,
    user: null,
    client: source.nodeID,
    farmer: destination.nodeID, // farmer storing the new mirror
    timestamp: Date.now(),
    downloadBandwidth: 0,
    storage: shardBytes,
    shardHash: shardHash,
    success: false
  });

  storageEvent.save((err) => {
    if (err) {
      this._logger.warn('_createStorageEvent: Error saving event, ' +
                        'reason: %s', err.message);
    }
  });
};

Monitor.prototype._transferShard = function(shard, state, callback) {
  const source = state.sources[0];
  const destination = state.destinations[0];

  if (!source) {
    return callback(new Error('Sources exhausted'));
  }

  if (!destination) {
    return callback(new Error('Destinations exhausted'));
  }

  let contract = null;
  try {
    contract = shard.getContract(source);
  } catch(e) {
    log.warn('Unable to transfer shard, invalid contract: %j',
             destination.contract);
    state.destinations.shift();
    this._transferShard(shard, state, callback);
    return;
  }

  this.network.getRetrievalPointer(source, contract, (err, pointer) => {
    if (err || !pointer) {
      log.warn('Failed to get retrieval pointer from farmer %s, reason: %s',
               source, err ? err.message : null);

      state.sources.shift();
      this._transferShard(shard, state, callback);
      return;
    }

    const farmer = storj.Contact(destination.contact);

    const token = pointer.token;
    const shardHash = shard.hash;
    const shardBytes = contract.get('data_size');
    this._createStorageEvent(token, shardHash, shardBytes, source, destination);

    this.network.getMirrorNodes([pointer], [farmer], (err) => {
      if (err) {
        log.warn('Unable to mirror to farmer %s, reason: %s',
                 destination, err.message);
        state.destinations.shift();
        this._transferShard(shard, state, callback);
        return;
      }

      this._saveShard(shard, destination, callback);
    });
  });
};

Monitor.prototype._replicateShard = function(shard, callback) {
  async.parallel({
    destinations: (next) => {
      this._fetchDestinations(shard, next);
    },
    sources: (next) => {
      this._fetchSources(shard, next);
    }
  }, (err, state) => {
    if (err) {
      return callback(err);
    }
    this._transferShard(shard, state, callback);
  });
};

Monitor.prototype._replicateFarmer = function(contact) {
  log.info('Starting to replicate farmer %s', contact.nodeID);

  const query = {
    'contracts.nodeID': contact.nodeID
  };
  const cursor = this.storage.models.Shard.find(query).cursor();

  cursor
    .on('error', (err) => {
      log.error('Unable to replicate farmer %s, reason: %s',
                contact.nodeID, err.message);
    })
    .on('data', (data) => {
      cursor.pause();
      const shard = storj.StorageItem(data.toObject());
      log.info('Replicating shard %s for farmer', shard.hash, contact.nodeID);
      this._replicateShard(shard, (err) => {
        if (err) {
          log.error('Unable to replicate shard %s, reason: %s',
                    shard.hash, err.message);
        }
        cursor.resume();
      });
    })
    .on('close', () => {
      log.info('Ending replication of farmer %s', contact.nodeID);
    });
};

Monitor.prototype._giveOfflinePenalty = function(contact) {
  contact.recordPoints(points.OFFLINE);
  contact.save((err) => {
    if (err) {
      this._logger.error('_giveOfflinePenalty: Unable to save contact %s ' +
                         'to update reputation, reason: %s',
                         contact._id, err.message);
    }
  });
};

Monitor.prototype._markStorageEventsEnded = function(contact) {
  const StorageEvent = this.storage.models.StorageEvent;
  StorageEvent.update({
    farmer: contact._id,
    farmerEnd: { $exists: false }
  }, {
    $currentDate: {
      farmerEnd: true
    }
  }, {
    multi: true
  }, (err) => {
    if (err) {
      log.error('Unable to update farmer %s storage events with farmerEnd, ' +
                'reason: %s', contact._id, err.message);
    }
  });
};

Monitor.prototype.run = function() {
  if (this._running) {
    return this.wait();
  }

  let fail = 0;
  let success = 0;
  let total = 0;
  const limit = this._config.application.queryNumber || 10;
  const pingConcurrency = this._config.application.pingConcurrency || 10;
  const timeoutRateThreshold = this._config.application.timeoutRateThreshold;

  const finish = (err) => {
    if (err) {
      log.error(err);
    }
    log.info('Ending farmer monitor round with failure rate of %s/%s from %s',
             fail, success, total);
    this._running = false;
    this.wait();
  };

  log.info('Starting farmer monitor round for %s contacts', limit);
  this._running = true;

  // Query the least seen contacts with timeout rates below threshold
  const Contact = this.storage.models.Contact;
  const query = {
    $or: [
      { timeoutRate: { $lt: timeoutRateThreshold } },
      { timeoutRate: { $exists: false } }
    ]
  };

  const cursor = Contact.find(query).limit(limit).sort({lastSeen: 1});
  cursor.exec((err, contacts) => {
    if (err) {
      return finish(err);
    }

    if (!contacts) {
      return finish(
        new errors.InternalError('No contacts in contacts collection')
      );
    }

    // Update total length of contacts
    total = contacts.length;

    // Ping the least seen contacts
    async.eachLimit(contacts, pingConcurrency, (contactData, next) => {

      const contact = storj.Contact(contactData);

      this.network.ping(contact, (err) => {
        if (err) {
          fail += 1;
          log.error('Farmer %s failed ping, reason: %s',
                    contact.nodeID, err.message);

          contactData.recordTimeoutFailure().save((err) => {
            if (err) {
              log.error('Unable to save ping failure, farmer: %s, reason: %s',
                        contact.nodeID, err.message);
            }
          });

          if (contactData.timeoutRate >= timeoutRateThreshold) {
            log.warn('Shards need replication, farmer: %s, timeoutRate: %s',
                     contact.nodeID, contactData.timeoutRate);

            this._replicateFarmer(contact);
            this._giveOfflinePenalty(contactData);
            this._markStorageEventsEnded(contactData);
          }

        } else {
          success += 1;
        }

        next();
      });

    }, finish);

  });

};

Monitor.prototype._randomTime = function(max, min) {
  const range = max - min;

  assert(Number.isSafeInteger(range));
  assert(range > 0, 'maxInterval is expected to be greater than minInterval');

  const entropy = crypto.randomBytes(8).toString('hex');
  const offset = Math.round(parseInt('0x' + entropy) / Math.pow(2, 64) * range);

  return min + offset;
};

/**
 * Will wait and then call `run` after a random amount of time
 */
Monitor.prototype.wait = function() {
  clearTimeout(this._timeout);

  const max = ms(this._config.application.maxInterval);
  const min = ms(this._config.application.minInterval);

  const milliseconds = this._randomTime(max, min);
  const minutes = Number(milliseconds / 1000 / 60).toFixed(2);

  log.info('Scheduling next round in %s minutes', minutes);

  this._timeout = setTimeout(() => this.run(), milliseconds);
};

/**
 * Handles uncaught exceptions
 * @private
 */
/* istanbul ignore next */
Monitor.prototype._handleUncaughtException = function(err) {
  if (process.env.NODE_ENV === 'test') {
    throw err;
  }

  log.error('An unhandled exception occurred:', err);
  process.exit(1);
};

/**
 * Handles exit event from process
 * @private
 */
/* istanbul ignore next */
Monitor.prototype._handleExit = function() {
  log.info('Farmer monitor service is shutting down');
};

/**
 * Postpones process exit until requests are fullfilled
 * @private
 */
/* istanbul ignore next */
Monitor.prototype._handleSIGINT = function() {
  let waitTime = 0;

  log.info('Received shutdown signal, checking for running monitor');
  setInterval(function() {
    waitTime += Monitor.SIGINT_CHECK_INTERVAL;

    if (!this._running) {
      process.exit();
    }

    if (waitTime > Monitor.MAX_SIGINT_WAIT) {
      process.exit();
    }
  }, Monitor.SIGINT_CHECK_INTERVAL);
};

module.exports = Monitor;
<commit_msg>Fix log<commit_after>'use strict';

const async = require('async');
const assert = require('assert');
const crypto = require('crypto');
const storj = require('storj-lib');
const MonitorConfig = require('./config');
const StorageModels = require('storj-service-storage-models');
const points = StorageModels.constants.POINTS;
const ComplexClient = require('storj-complex').createClient;
const MongoDBStorageAdapter = require('storj-mongodb-adapter');
const ms = require('ms');
const log = require('../logger');
const errors = require('storj-service-error-types');

/**
 * A long running daemon that will monitor farmers uptime and will replace
 * contracts associated with a farmer once the farmer is confirmed to be
 * offline for a duration of time.
 * @param {MonitorConfig} config - An instance of MonitorConfig
 */
function Monitor(config) {
  if (!(this instanceof Monitor)) {
    return new Monitor(config);
  }

  assert(config instanceof MonitorConfig, 'Invalid config supplied');

  this.storage = null;
  this.network = null;
  this.contracts = null;

  this._config = config;
  this._timeout = null;
  this._running = false;
}

Monitor.SIGINT_CHECK_INTERVAL = 1000;
Monitor.MAX_SIGINT_WAIT = 5000;

/**
 * Starts the Bridge instance
 * @param {Function} callback
 */
Monitor.prototype.start = function(callback) {
  log.info('Farmer monitor service is starting');

  this.storage = new StorageModels(
    this._config.storage.mongoUrl,
    this._config.storage.mongoOpts,
    { logger: log }
  );

  this.network = new ComplexClient(this._config.complex);

  this.contracts = new storj.StorageManager(
    new MongoDBStorageAdapter(this.storage),
    { disableReaper: true }
  );

  // setup next run event
  this.wait();

  callback();
  process.on('SIGINT', this._handleSIGINT.bind(this));
  process.on('exit', this._handleExit.bind(this));
  process.on('uncaughtException', this._handleUncaughtException.bind(this));
};

Monitor.sortByTimeoutRate = function(a, b) {
  const a1 = a.contact.timeoutRate >= 0 ? a.contact.timeoutRate : 0;
  const b1 = b.contact.timeoutRate >= 0 ? b.contact.timeoutRate : 0;
  return (a1 === b1) ? 0 : (a1 > b1) ? 1 : -1;
};

Monitor.prototype._fetchDestinations = function(shard, callback) {
  this.storage.models.Mirror
    .find({ shardHash: shard.hash })
    .populate('contact')
    .exec((err, results) => {
      if (err) {
        return callback(err);
      }
      const mirrors = results.filter((m) => {
        if (!m.contact) {
          log.warn('Mirror %s is missing contact in database', m._id);
          return false;
        } else if (shard.contracts[m.contact._id]) {
          log.warn('Shard %s already established to contact %s',
                   shard.hash, m.contact._id);
          return false;
        } else if (!m.isEstablished) {
          return true;
        }
        return false;
      });
      mirrors.sort(Monitor.sortByTimeoutRate);
      callback(null, mirrors);
    });
};

Monitor.prototype._fetchSources = function(shard, callback) {
  let farmers = Object.keys(shard.contracts);

  this.storage.models.Contact
    .find({ _id: { $in: farmers }})
    .sort({ lastSeen: -1 })
    .exec((err, results) => {
      if (err) {
        return callback(err);
      }

      let contacts = [];
      for (let i = 0; i < results.length; i++) {
        let c = results[i];
        let contact = null;
        try {
          contact = storj.Contact(c.toObject());
        } catch(e) {
          log.warn('Unable to fetch source, invalid contact: %j', c.toObject());
        }
        if (contact) {
          contacts.push(contact);
        }
      }

      callback(null, contacts);
    });
};

Monitor.prototype._saveShard = function(shard, destination, callback) {

  const contract = storj.Contract(destination.contract);
  const contact = storj.Contact(destination.contact);
  shard.addContract(contact, contract);

  this.contracts.save(shard, (err) => {
    if (err) {
      return callback(new Error('Unable to save contract to shard'));
    }

    log.info('Successfully replicated shard %s', shard.hash);
    destination.isEstablished = true;
    destination.save((err) => {
      if (err) {
        return callback(
          new Error('Unable to update mirror as established, reason: ' +
                    err.message)
        );
      }
      callback();
    });
  });
};

Monitor.prototype._createStorageEvent = function(token,
                                                 shardHash,
                                                 shardBytes,
                                                 source,
                                                 destination) {
  const StorageEvent = this.storage.models.StorageEvent;

  const storageEvent = new StorageEvent({
    token: token,
    user: null,
    client: source.nodeID,
    farmer: destination.nodeID, // farmer storing the new mirror
    timestamp: Date.now(),
    downloadBandwidth: 0,
    storage: shardBytes,
    shardHash: shardHash,
    success: false
  });

  storageEvent.save((err) => {
    if (err) {
      log.warn('_createStorageEvent: Error saving event, ' +
               'reason: %s', err.message);
    }
  });
};

Monitor.prototype._transferShard = function(shard, state, callback) {
  const source = state.sources[0];
  const destination = state.destinations[0];

  if (!source) {
    return callback(new Error('Sources exhausted'));
  }

  if (!destination) {
    return callback(new Error('Destinations exhausted'));
  }

  let contract = null;
  try {
    contract = shard.getContract(source);
  } catch(e) {
    log.warn('Unable to transfer shard, invalid contract: %j',
             destination.contract);
    state.destinations.shift();
    this._transferShard(shard, state, callback);
    return;
  }

  this.network.getRetrievalPointer(source, contract, (err, pointer) => {
    if (err || !pointer) {
      log.warn('Failed to get retrieval pointer from farmer %s, reason: %s',
               source, err ? err.message : null);

      state.sources.shift();
      this._transferShard(shard, state, callback);
      return;
    }

    const farmer = storj.Contact(destination.contact);

    const token = pointer.token;
    const shardHash = shard.hash;
    const shardBytes = contract.get('data_size');
    this._createStorageEvent(token, shardHash, shardBytes, source, destination);

    this.network.getMirrorNodes([pointer], [farmer], (err) => {
      if (err) {
        log.warn('Unable to mirror to farmer %s, reason: %s',
                 destination, err.message);
        state.destinations.shift();
        this._transferShard(shard, state, callback);
        return;
      }

      this._saveShard(shard, destination, callback);
    });
  });
};

Monitor.prototype._replicateShard = function(shard, callback) {
  async.parallel({
    destinations: (next) => {
      this._fetchDestinations(shard, next);
    },
    sources: (next) => {
      this._fetchSources(shard, next);
    }
  }, (err, state) => {
    if (err) {
      return callback(err);
    }
    this._transferShard(shard, state, callback);
  });
};

Monitor.prototype._replicateFarmer = function(contact) {
  log.info('Starting to replicate farmer %s', contact.nodeID);

  const query = {
    'contracts.nodeID': contact.nodeID
  };
  const cursor = this.storage.models.Shard.find(query).cursor();

  cursor
    .on('error', (err) => {
      log.error('Unable to replicate farmer %s, reason: %s',
                contact.nodeID, err.message);
    })
    .on('data', (data) => {
      cursor.pause();
      const shard = storj.StorageItem(data.toObject());
      log.info('Replicating shard %s for farmer', shard.hash, contact.nodeID);
      this._replicateShard(shard, (err) => {
        if (err) {
          log.error('Unable to replicate shard %s, reason: %s',
                    shard.hash, err.message);
        }
        cursor.resume();
      });
    })
    .on('close', () => {
      log.info('Ending replication of farmer %s', contact.nodeID);
    });
};

Monitor.prototype._giveOfflinePenalty = function(contact) {
  contact.recordPoints(points.OFFLINE);
  contact.save((err) => {
    if (err) {
      this._logger.error('_giveOfflinePenalty: Unable to save contact %s ' +
                         'to update reputation, reason: %s',
                         contact._id, err.message);
    }
  });
};

Monitor.prototype._markStorageEventsEnded = function(contact) {
  const StorageEvent = this.storage.models.StorageEvent;
  StorageEvent.update({
    farmer: contact._id,
    farmerEnd: { $exists: false }
  }, {
    $currentDate: {
      farmerEnd: true
    }
  }, {
    multi: true
  }, (err) => {
    if (err) {
      log.error('Unable to update farmer %s storage events with farmerEnd, ' +
                'reason: %s', contact._id, err.message);
    }
  });
};

Monitor.prototype.run = function() {
  if (this._running) {
    return this.wait();
  }

  let fail = 0;
  let success = 0;
  let total = 0;
  const limit = this._config.application.queryNumber || 10;
  const pingConcurrency = this._config.application.pingConcurrency || 10;
  const timeoutRateThreshold = this._config.application.timeoutRateThreshold;

  const finish = (err) => {
    if (err) {
      log.error(err);
    }
    log.info('Ending farmer monitor round with failure rate of %s/%s from %s',
             fail, success, total);
    this._running = false;
    this.wait();
  };

  log.info('Starting farmer monitor round for %s contacts', limit);
  this._running = true;

  // Query the least seen contacts with timeout rates below threshold
  const Contact = this.storage.models.Contact;
  const query = {
    $or: [
      { timeoutRate: { $lt: timeoutRateThreshold } },
      { timeoutRate: { $exists: false } }
    ]
  };

  const cursor = Contact.find(query).limit(limit).sort({lastSeen: 1});
  cursor.exec((err, contacts) => {
    if (err) {
      return finish(err);
    }

    if (!contacts) {
      return finish(
        new errors.InternalError('No contacts in contacts collection')
      );
    }

    // Update total length of contacts
    total = contacts.length;

    // Ping the least seen contacts
    async.eachLimit(contacts, pingConcurrency, (contactData, next) => {

      const contact = storj.Contact(contactData);

      this.network.ping(contact, (err) => {
        if (err) {
          fail += 1;
          log.error('Farmer %s failed ping, reason: %s',
                    contact.nodeID, err.message);

          contactData.recordTimeoutFailure().save((err) => {
            if (err) {
              log.error('Unable to save ping failure, farmer: %s, reason: %s',
                        contact.nodeID, err.message);
            }
          });

          if (contactData.timeoutRate >= timeoutRateThreshold) {
            log.warn('Shards need replication, farmer: %s, timeoutRate: %s',
                     contact.nodeID, contactData.timeoutRate);

            this._replicateFarmer(contact);
            this._giveOfflinePenalty(contactData);
            this._markStorageEventsEnded(contactData);
          }

        } else {
          success += 1;
        }

        next();
      });

    }, finish);

  });

};

Monitor.prototype._randomTime = function(max, min) {
  const range = max - min;

  assert(Number.isSafeInteger(range));
  assert(range > 0, 'maxInterval is expected to be greater than minInterval');

  const entropy = crypto.randomBytes(8).toString('hex');
  const offset = Math.round(parseInt('0x' + entropy) / Math.pow(2, 64) * range);

  return min + offset;
};

/**
 * Will wait and then call `run` after a random amount of time
 */
Monitor.prototype.wait = function() {
  clearTimeout(this._timeout);

  const max = ms(this._config.application.maxInterval);
  const min = ms(this._config.application.minInterval);

  const milliseconds = this._randomTime(max, min);
  const minutes = Number(milliseconds / 1000 / 60).toFixed(2);

  log.info('Scheduling next round in %s minutes', minutes);

  this._timeout = setTimeout(() => this.run(), milliseconds);
};

/**
 * Handles uncaught exceptions
 * @private
 */
/* istanbul ignore next */
Monitor.prototype._handleUncaughtException = function(err) {
  if (process.env.NODE_ENV === 'test') {
    throw err;
  }

  log.error('An unhandled exception occurred:', err);
  process.exit(1);
};

/**
 * Handles exit event from process
 * @private
 */
/* istanbul ignore next */
Monitor.prototype._handleExit = function() {
  log.info('Farmer monitor service is shutting down');
};

/**
 * Postpones process exit until requests are fullfilled
 * @private
 */
/* istanbul ignore next */
Monitor.prototype._handleSIGINT = function() {
  let waitTime = 0;

  log.info('Received shutdown signal, checking for running monitor');
  setInterval(function() {
    waitTime += Monitor.SIGINT_CHECK_INTERVAL;

    if (!this._running) {
      process.exit();
    }

    if (waitTime > Monitor.MAX_SIGINT_WAIT) {
      process.exit();
    }
  }, Monitor.SIGINT_CHECK_INTERVAL);
};

module.exports = Monitor;
<|endoftext|>


<commit_before>/*
Copyright (c) 2011, Chris Umbel

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.
*/

exports.SoundEx = require('./phonetics/soundex');
exports.Metaphone = require('./phonetics/metaphone');
exports.DoubleMetaphone = require('./phonetics/double_metaphone');
exports.SoundExDM = require('./phonetics/dm_soundex');
exports.PorterStemmer = require('./stemmers/porter_stemmer');
exports.PorterStemmerFa = require('./stemmers/porter_stemmer_fa');
exports.PorterStemmerRu = require('./stemmers/porter_stemmer_ru');
exports.LancasterStemmer = require('./stemmers/lancaster_stemmer');
exports.StemmerJa = require('./stemmers/stemmer_ja');
exports.AggressiveTokenizerFa = require('./tokenizers/aggressive_tokenizer_fa');
exports.AggressiveTokenizerRu = require('./tokenizers/aggressive_tokenizer_ru');
exports.AggressiveTokenizer = require('./tokenizers/aggressive_tokenizer');
exports.RegexpTokenizer = require('./tokenizers/regexp_tokenizer').RegexpTokenizer;
exports.WordTokenizer = require('./tokenizers/regexp_tokenizer').WordTokenizer;
exports.WordPunctTokenizer = require('./tokenizers/regexp_tokenizer').WordPunctTokenizer;
exports.TreebankWordTokenizer = require('./tokenizers/treebank_word_tokenizer');
exports.TokenizerJa = require('./tokenizers/tokenizer_ja');
exports.BayesClassifier = require('./classifiers/bayes_classifier');
exports.LogisticRegressionClassifier = require('./classifiers/logistic_regression_classifier');
exports.NounInflector = require('./inflectors/noun_inflector');
exports.NounInflectorFr = require('./inflectors/fr/noun_inflector');
exports.NounInflectorJa = require('./inflectors/ja/noun_inflector');
exports.PresentVerbInflector = require('./inflectors/present_verb_inflector');
exports.CountInflector = require('./inflectors/count_inflector');
exports.WordNet = require('./wordnet/wordnet');
exports.TfIdf = require('./tfidf/tfidf');
exports.SentenceAnalyzer = require('./analyzers/sentence_analyzer');
exports.stopwords = require('./util/stopwords').words;
exports.NGrams = require('./ngrams/ngrams');
exports.JaroWinklerDistance = require('./distance/jaro-winkler_distance');
exports.LevenshteinDistance = require('./distance/levenshtein_distance');
exports.DiceCoefficient = require('./distance/dice_coefficient');
exports.normalize_ja = require('./normalizers/normalizer_ja').normalize_ja;
exports.removeDiacritics = require('./normalizers/remove_diacritics');
exports.transliterate_ja = require('./transliterators/ja');
<commit_msg>added exports for spanish stemmer<commit_after>/*
Copyright (c) 2011, Chris Umbel

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.
*/

exports.SoundEx = require('./phonetics/soundex');
exports.Metaphone = require('./phonetics/metaphone');
exports.DoubleMetaphone = require('./phonetics/double_metaphone');
exports.SoundExDM = require('./phonetics/dm_soundex');
exports.PorterStemmer = require('./stemmers/porter_stemmer');
exports.PorterStemmerFa = require('./stemmers/porter_stemmer_fa');
exports.PorterStemmerRu = require('./stemmers/porter_stemmer_ru');
exports.PorterStemmerEs = require('./stemmers/porter_stemmer_es');
exports.LancasterStemmer = require('./stemmers/lancaster_stemmer');
exports.StemmerJa = require('./stemmers/stemmer_ja');
exports.AggressiveTokenizerFa = require('./tokenizers/aggressive_tokenizer_fa');
exports.AggressiveTokenizerRu = require('./tokenizers/aggressive_tokenizer_ru');
exports.AggressiveTokenizerEs = require('./tokenizers/aggressive_tokenizer_es');
exports.AggressiveTokenizer = require('./tokenizers/aggressive_tokenizer');
exports.RegexpTokenizer = require('./tokenizers/regexp_tokenizer').RegexpTokenizer;
exports.WordTokenizer = require('./tokenizers/regexp_tokenizer').WordTokenizer;
exports.WordPunctTokenizer = require('./tokenizers/regexp_tokenizer').WordPunctTokenizer;
exports.TreebankWordTokenizer = require('./tokenizers/treebank_word_tokenizer');
exports.TokenizerJa = require('./tokenizers/tokenizer_ja');
exports.BayesClassifier = require('./classifiers/bayes_classifier');
exports.LogisticRegressionClassifier = require('./classifiers/logistic_regression_classifier');
exports.NounInflector = require('./inflectors/noun_inflector');
exports.NounInflectorFr = require('./inflectors/fr/noun_inflector');
exports.NounInflectorJa = require('./inflectors/ja/noun_inflector');
exports.PresentVerbInflector = require('./inflectors/present_verb_inflector');
exports.CountInflector = require('./inflectors/count_inflector');
exports.WordNet = require('./wordnet/wordnet');
exports.TfIdf = require('./tfidf/tfidf');
exports.SentenceAnalyzer = require('./analyzers/sentence_analyzer');
exports.stopwords = require('./util/stopwords').words;
exports.NGrams = require('./ngrams/ngrams');
exports.JaroWinklerDistance = require('./distance/jaro-winkler_distance');
exports.LevenshteinDistance = require('./distance/levenshtein_distance');
exports.DiceCoefficient = require('./distance/dice_coefficient');
exports.normalize_ja = require('./normalizers/normalizer_ja').normalize_ja;
exports.removeDiacritics = require('./normalizers/remove_diacritics');
exports.transliterate_ja = require('./transliterators/ja');
<|endoftext|>


<commit_before>// camelize

var RE_SEP = /\-|\./;
var RE_SEP_AZ = /(\-|\.)[a-z]/;
var RE_SEP_AZ_G = /(\-|\.)[a-z]/g;
var RE_WS = /[\s]+/g;
var BLANK = '';

module.exports = camelize;
function camelize(name) {

  var ext = BLANK;
  var id, match, ch, i;

  if (name.indexOf('.') > 0) {
    ext = name.substring(name.lastIndexOf('.'), name.length);
  }

  // replace windows backslash separators
  id = name.replace(/\\/g, '/').split('/').slice(-1)[0];
  id = id.substring(0, id.lastIndexOf(ext));

  if (match = id.match(RE_SEP_AZ_G)) {
    for (i = 0; i < match.length; i++) {
      ch = match[i].replace(RE_SEP, BLANK).toUpperCase();
      id = id.replace(RE_SEP_AZ, ch);
    }
  }

  return (id.substring(0, id.lastIndexOf('.')) || id).replace(RE_WS, BLANK);
}<commit_msg>windows separator regex<commit_after>// camelize

var RE_SEP = /\-|\./;
var RE_SEP_AZ = /(\-|\.)[a-z]/;
var RE_SEP_AZ_G = /(\-|\.)[a-z]/g;
var RE_WS = /[\s]+/g;
var BLANK = '';
var RE_WIN_SEP = /\\/g;

module.exports = camelize;
function camelize(name) {

  var ext = BLANK;
  var id, match, ch, i;

  if (name.indexOf('.') > 0) {
    ext = name.substring(name.lastIndexOf('.'), name.length);
  }

  // replace windows backslash separators
  id = name.replace(RE_WIN_SEP, '/').split('/').slice(-1)[0];
  id = id.substring(0, id.lastIndexOf(ext));

  if (match = id.match(RE_SEP_AZ_G)) {
    for (i = 0; i < match.length; i++) {
      ch = match[i].replace(RE_SEP, BLANK).toUpperCase();
      id = id.replace(RE_SEP_AZ, ch);
    }
  }

  return (id.substring(0, id.lastIndexOf('.')) || id).replace(RE_WS, BLANK);
}<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var moment = require('moment');
var times = require('../times');

var offset = times.mins(2.5).msecs;
var bucketFields = ['index', 'fromMills', 'toMills'];

function init (ctx) {

  var translate = ctx.language.translate;
  var utils = require('../utils')(ctx);

  var bgnow = {
    name: 'bgnow'
    , label: 'BG Now'
    , pluginType: 'pill-primary'
  };

  bgnow.mostRecentBucket = function mostRecentBucket (buckets) {
    return _.find(buckets, function notEmpty(bucket) {
      return bucket && !bucket.isEmpty;
    });
  };

  bgnow.previousBucket = function previousBucket(recent, buckets) {
    var previous = null;

    if (_.isObject(recent)) {
      previous = _.chain(buckets).find(function afterFirstNotEmpty(bucket) {
        return bucket.mills < recent.mills && !bucket.isEmpty;
      }).value();
    }

    return previous;
  };

  bgnow.setProperties = function setProperties (sbx) {
    var buckets = bgnow.fillBuckets(sbx);
    var recent = bgnow.mostRecentBucket(buckets);
    var previous = bgnow.previousBucket(recent, buckets);
    var delta = bgnow.calcDelta(recent, previous, sbx);

    sbx.offerProperty('bgnow', function setBGNow ( ) {
      return _.omit(recent, bucketFields);
    });

    sbx.offerProperty('delta', function setBGNow ( ) {
      return delta;
    });

    sbx.offerProperty('buckets', function setBGNow ( ) {
      return buckets;
    });
  };

  bgnow.fillBuckets = function fillBuckets (sbx, opts) {

    var bucketCount = (opts && opts.bucketCount) || 4;
    var bucketMins = (opts && opts.bucketMins) || 5;
    var bucketMsecs = times.mins(bucketMins).msecs;

    var lastSGVMills = sbx.lastSGVMills();

    var buckets = _.times(bucketCount, function createBucket (index) {
      var fromMills = lastSGVMills - offset - (index * bucketMsecs);
      return {
        index: index
        , fromMills: fromMills
        , toMills: fromMills + bucketMsecs
        , sgvs: [ ]
      };
    });

    _.takeRightWhile(sbx.data.sgvs, function addToBucket (sgv) {

      //if in the future, return true and keep taking right
      if (sgv.mills > sbx.time) {
        return true;
      }

      var bucket = _.find(buckets, function containsSGV (bucket) {
        return sgv.mills >= bucket.fromMills && sgv.mills <= bucket.toMills;
      });

      if (bucket) {
        bucket.sgvs.push(sgv);
      }

      return bucket;
    });

    return _.map(buckets, bgnow.analyzeBucket);
  };

  function notError (entry) {
    return entry && entry.mgdl > 39; //TODO maybe lower instead of expecting dexcom?
  }

  function isError (entry) {
    return !entry || !entry.mgdl || entry.mgdl < 39;
  }

  bgnow.analyzeBucket = function analyzeBucket (bucket) {

    if (_.isEmpty(bucket.sgvs)) {
      bucket.isEmpty = true;
      return bucket;
    }

    var details = { };

    var sgvs = _.filter(bucket.sgvs, notError);

    function calcMean ( ) {
      var sum = 0;
      _.forEach(sgvs, function eachSGV (sgv) {
        sum += Number(sgv.mgdl);
      });

      return sum / sgvs.length;
    }

    var mean = calcMean(sgvs);

    if (mean && _.isNumber(mean)) {
      details.mean = mean;
    }

    var mostRecent = _.maxBy(sgvs, 'mills');

    if (mostRecent) {
      details.last = mostRecent.mgdl;
      details.mills = mostRecent.mills;
    }

    var errors = _.filter(bucket.sgvs, isError);
    if (!_.isEmpty(errors)) {
      details.errors = errors;
    }

    return _.merge(details, bucket);
  };

  bgnow.calcDelta = function calcDelta (recent, previous, sbx) {

    if (_.isEmpty(recent)) {
      console.info('all buckets are empty');
      return null;
    }

    if (_.isEmpty(previous)) {
      console.info('previous bucket not found, not calculating delta');
      return null;
    }

    var delta = {
      absolute: recent.mean - previous.mean
      , elapsedMins: (recent.mills - previous.mills) / times.min().msecs
    };

    delta.interpolated = delta.elapsedMins > 9;

    delta.mean5MinsAgo = delta.interpolated ?
      recent.mean - delta.absolute / delta.elapsedMins * 5 : recent.mean - delta.absolute;

    delta.mgdl = Math.round(recent.mean - delta.mean5MinsAgo);

    delta.scaled = sbx.settings.units === 'mmol' ?
      sbx.roundBGToDisplayFormat(sbx.scaleMgdl(recent.mean) - sbx.scaleMgdl(delta.mean5MinsAgo)) : delta.mgdl;

    delta.display = (delta.scaled >= 0 ? '+' : '') + delta.scaled;

    delta.previous = _.omit(previous, bucketFields);

    return delta;

  };

  bgnow.updateVisualisation = function updateVisualisation (sbx) {
    var prop = sbx.properties.bgnow;
    var delta = sbx.properties.delta;

    var info = [];
    var display = delta && delta.display;

    if (delta && delta.interpolated) {
      display += ' *';
      info.push({label: translate('Elapsed Time'), value: Math.round(delta.elapsedMins) + ' ' + translate('mins')});
      info.push({label: translate('Absolute Delta'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.absolute)) + ' ' + sbx.unitsLabel});
      info.push({label: translate('Interpolated'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.mean5MinsAgo)) + ' ' + sbx.unitsLabel});
    }

    var deviceInfos = { };

    if (prop.sgvs) {
      _.forEach(prop.sgvs, function deviceAndValue(entry) {
        var device = utils.deviceName(entry.device);
        deviceInfos[device] = {
          time: utils.timeFormat(moment(entry.mills), sbx)
          , value: sbx.scaleEntry(entry)
          , recent: entry
        };
      });
    }

    if (delta && delta.previous && delta.previous.sgvs) {
      _.forEach(delta.previous.sgvs, function deviceAndValue(entry) {
        var device = utils.deviceName(entry.device);
        var deviceInfo = deviceInfos[device];
        if (deviceInfo) {
          var deviceDelta = bgnow.calcDelta(
            { mills: deviceInfo.recent.mills , mean: deviceInfo.recent.mgdl}
            , { mills: entry.mills, mean: entry.mgdl}
            , sbx
          );

          if (deviceDelta) {
            deviceInfo.delta = deviceDelta.display
          }
        } else {
          deviceInfos[device] = {
            time: utils.timeFormat(moment(entry.mills), sbx)
            , value: sbx.scaleEntry(entry)
          };
        }
      });

      if (_.keys(deviceInfos).length > 1) {
        _.forIn(deviceInfos, function addInfo(deviceInfo, name) {
          var display = deviceInfo.value;
          if (deviceInfo.delta) {
            display += ' ' + deviceInfo.delta;
          }

          display += ' (' + deviceInfo.time + ')';

          info.push({label: name, value: display});
        });
      }
    }

    sbx.pluginBase.updatePillText({
      name: 'delta'
      , label: translate('BG Delta')
      , pluginType: 'pill-major'
      , pillFlip: true
    }, {
      value: display
      , label: sbx.unitsLabel
      , info: _.isEmpty(info) ? null : info
    });
  };

  return bgnow;

}

module.exports = init;
<commit_msg>deviceInfo.recent null check (#3048)<commit_after>'use strict';

var _ = require('lodash');
var moment = require('moment');
var times = require('../times');

var offset = times.mins(2.5).msecs;
var bucketFields = ['index', 'fromMills', 'toMills'];

function init (ctx) {

  var translate = ctx.language.translate;
  var utils = require('../utils')(ctx);

  var bgnow = {
    name: 'bgnow'
    , label: 'BG Now'
    , pluginType: 'pill-primary'
  };

  bgnow.mostRecentBucket = function mostRecentBucket (buckets) {
    return _.find(buckets, function notEmpty(bucket) {
      return bucket && !bucket.isEmpty;
    });
  };

  bgnow.previousBucket = function previousBucket(recent, buckets) {
    var previous = null;

    if (_.isObject(recent)) {
      previous = _.chain(buckets).find(function afterFirstNotEmpty(bucket) {
        return bucket.mills < recent.mills && !bucket.isEmpty;
      }).value();
    }

    return previous;
  };

  bgnow.setProperties = function setProperties (sbx) {
    var buckets = bgnow.fillBuckets(sbx);
    var recent = bgnow.mostRecentBucket(buckets);
    var previous = bgnow.previousBucket(recent, buckets);
    var delta = bgnow.calcDelta(recent, previous, sbx);

    sbx.offerProperty('bgnow', function setBGNow ( ) {
      return _.omit(recent, bucketFields);
    });

    sbx.offerProperty('delta', function setBGNow ( ) {
      return delta;
    });

    sbx.offerProperty('buckets', function setBGNow ( ) {
      return buckets;
    });
  };

  bgnow.fillBuckets = function fillBuckets (sbx, opts) {

    var bucketCount = (opts && opts.bucketCount) || 4;
    var bucketMins = (opts && opts.bucketMins) || 5;
    var bucketMsecs = times.mins(bucketMins).msecs;

    var lastSGVMills = sbx.lastSGVMills();

    var buckets = _.times(bucketCount, function createBucket (index) {
      var fromMills = lastSGVMills - offset - (index * bucketMsecs);
      return {
        index: index
        , fromMills: fromMills
        , toMills: fromMills + bucketMsecs
        , sgvs: [ ]
      };
    });

    _.takeRightWhile(sbx.data.sgvs, function addToBucket (sgv) {

      //if in the future, return true and keep taking right
      if (sgv.mills > sbx.time) {
        return true;
      }

      var bucket = _.find(buckets, function containsSGV (bucket) {
        return sgv.mills >= bucket.fromMills && sgv.mills <= bucket.toMills;
      });

      if (bucket) {
        bucket.sgvs.push(sgv);
      }

      return bucket;
    });

    return _.map(buckets, bgnow.analyzeBucket);
  };

  function notError (entry) {
    return entry && entry.mgdl > 39; //TODO maybe lower instead of expecting dexcom?
  }

  function isError (entry) {
    return !entry || !entry.mgdl || entry.mgdl < 39;
  }

  bgnow.analyzeBucket = function analyzeBucket (bucket) {

    if (_.isEmpty(bucket.sgvs)) {
      bucket.isEmpty = true;
      return bucket;
    }

    var details = { };

    var sgvs = _.filter(bucket.sgvs, notError);

    function calcMean ( ) {
      var sum = 0;
      _.forEach(sgvs, function eachSGV (sgv) {
        sum += Number(sgv.mgdl);
      });

      return sum / sgvs.length;
    }

    var mean = calcMean(sgvs);

    if (mean && _.isNumber(mean)) {
      details.mean = mean;
    }

    var mostRecent = _.maxBy(sgvs, 'mills');

    if (mostRecent) {
      details.last = mostRecent.mgdl;
      details.mills = mostRecent.mills;
    }

    var errors = _.filter(bucket.sgvs, isError);
    if (!_.isEmpty(errors)) {
      details.errors = errors;
    }

    return _.merge(details, bucket);
  };

  bgnow.calcDelta = function calcDelta (recent, previous, sbx) {

    if (_.isEmpty(recent)) {
      console.info('all buckets are empty');
      return null;
    }

    if (_.isEmpty(previous)) {
      console.info('previous bucket not found, not calculating delta');
      return null;
    }

    var delta = {
      absolute: recent.mean - previous.mean
      , elapsedMins: (recent.mills - previous.mills) / times.min().msecs
    };

    delta.interpolated = delta.elapsedMins > 9;

    delta.mean5MinsAgo = delta.interpolated ?
      recent.mean - delta.absolute / delta.elapsedMins * 5 : recent.mean - delta.absolute;

    delta.mgdl = Math.round(recent.mean - delta.mean5MinsAgo);

    delta.scaled = sbx.settings.units === 'mmol' ?
      sbx.roundBGToDisplayFormat(sbx.scaleMgdl(recent.mean) - sbx.scaleMgdl(delta.mean5MinsAgo)) : delta.mgdl;

    delta.display = (delta.scaled >= 0 ? '+' : '') + delta.scaled;

    delta.previous = _.omit(previous, bucketFields);

    return delta;

  };

  bgnow.updateVisualisation = function updateVisualisation (sbx) {
    var prop = sbx.properties.bgnow;
    var delta = sbx.properties.delta;

    var info = [];
    var display = delta && delta.display;

    if (delta && delta.interpolated) {
      display += ' *';
      info.push({label: translate('Elapsed Time'), value: Math.round(delta.elapsedMins) + ' ' + translate('mins')});
      info.push({label: translate('Absolute Delta'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.absolute)) + ' ' + sbx.unitsLabel});
      info.push({label: translate('Interpolated'), value: sbx.roundBGToDisplayFormat(sbx.scaleMgdl(delta.mean5MinsAgo)) + ' ' + sbx.unitsLabel});
    }

    var deviceInfos = { };

    if (prop.sgvs) {
      _.forEach(prop.sgvs, function deviceAndValue(entry) {
        var device = utils.deviceName(entry.device);
        deviceInfos[device] = {
          time: utils.timeFormat(moment(entry.mills), sbx)
          , value: sbx.scaleEntry(entry)
          , recent: entry
        };
      });
    }

    if (delta && delta.previous && delta.previous.sgvs) {
      _.forEach(delta.previous.sgvs, function deviceAndValue(entry) {
        var device = utils.deviceName(entry.device);
        var deviceInfo = deviceInfos[device];
        if (deviceInfo && deviceInfo.recent) {
          var deviceDelta = bgnow.calcDelta(
            { mills: deviceInfo.recent.mills , mean: deviceInfo.recent.mgdl}
            , { mills: entry.mills, mean: entry.mgdl}
            , sbx
          );

          if (deviceDelta) {
            deviceInfo.delta = deviceDelta.display
          }
        } else {
          deviceInfos[device] = {
            time: utils.timeFormat(moment(entry.mills), sbx)
            , value: sbx.scaleEntry(entry)
          };
        }
      });

      if (_.keys(deviceInfos).length > 1) {
        _.forIn(deviceInfos, function addInfo(deviceInfo, name) {
          var display = deviceInfo.value;
          if (deviceInfo.delta) {
            display += ' ' + deviceInfo.delta;
          }

          display += ' (' + deviceInfo.time + ')';

          info.push({label: name, value: display});
        });
      }
    }

    sbx.pluginBase.updatePillText({
      name: 'delta'
      , label: translate('BG Delta')
      , pluginType: 'pill-major'
      , pillFlip: true
    }, {
      value: display
      , label: sbx.unitsLabel
      , info: _.isEmpty(info) ? null : info
    });
  };

  return bgnow;

}

module.exports = init;
<|endoftext|>