Source: src/available_users.js

/**
 * @file Manage a list of available users
 * @author Andrew Sayers
 * @description Keep track of which users are currently "available", and share jobs fairly between them
 */

/**
 * @summary Manage a list of available users
 * @param {Object} args available users arguments
 * @constructor
 * @abstract
 *
 * @example
 * var available_users = new AvailableUsers({
 *     bb: bb, // BulletinBoard object
 *     ss: ss, // SharedStore object
 *     language: 'en' // user will only be marked available for items in this language
 * });
 *
 *
 * @description All registered users can be "available" or "unavailable".
 * Jobs can only be assigned to available users.  Availability is calculated
 * from two metrics: has the user set their state to "active", and have
 * they moved the mouse in the past minute or so.
 */
function AvailableUsers(args) {

    var username = args.bb.user_current().username;

    this.current_user = { username: username, active_time: new Date().getTime() };
    this.users = [this.current_user];

    var available = true;
    this._available = function() { available = true };

    var au = this;
    this.promise = args.bb.ping().then(function(data) {
        au.time_offset = data.offset;
        au.current_user.active_time = new Date().getTime() - au.time_offset;
        args.ss.interval_transaction(function(data) {

            var need_update = false;

            if ( data.hasOwnProperty('available_users') ) {
                au.users = data.available_users;
                au.current_user = au.users.reduce(function(prev,user) { return user.username == username ? user : prev }, null );
                if ( !au.current_user ) {
                    au.current_user = { username: username, active_time: 0 };
                    au.users.push(au.current_user);
                    need_update = true;
                }
            } else {
                data.available_users = au.users;
                need_update = true;
            }

            if ( au.active && available ) {
                au.current_user.active_time = new Date().getTime() - au.time_offset;
                au.current_user.language    = args.language;
                need_update = true;
            }

            available = false;

            return need_update;

        });
        return args.ss.transaction(function(){}); // trigger an initial update
    });

}

AvailableUsers.prototype.constructor = AvailableUsers;
AvailableUsers.prototype = Object.create(Object, {
    current_user: { writable: true, configurable: false },
    users       : { writable: true, configurable: false },
    active      : { writable: true, configurable: false },
    time_offset : { writable: true, configurable: false },
    _available  : { writable: true, configurable: false },
});

/**
 * @summary getset a user's activity state
 * @param {Boolean=} val new activity state
 * @return {Boolean} activity state
 *
 * @description a user is deemed to be available if their state
 * is set to "active" and they have moved the mouse in the past
 * minute or so.
 */
AvailableUsers.prototype.active = function(val) {
    if ( typeof(val) != 'undefined' && val != this.active ) {
        this.active = val;
        if ( this.active ) {
            $(window       ).on ( 'focus scroll'      , this._available );
            $(document.body).on ( 'mousemove keypress', this._available );
            this._available();
        } else {
            $(window       ).off( 'focus scroll'      , this._available );
            $(document.body).off( 'mousemove keypress', this._available );
        }
    }
    return this.active;
}

/**
 * @summary decide whether an element belongs to us based on an ID
 * @param {Number} id Item ID
 * @return {Boolean} true if the item belongs to us
 *
 * @description
 * Based on the list of users and their availability, decides whether
 * this item should be assigned to us.  This should assign items
 * as stably as possible, even when users' availability changes.
 */
AvailableUsers.prototype.is_ours = function( id, language ) {

    // anyone that has not updated their activity in the last five minutes is assumed to be unavailable:
    var activity_cutoff = new Date().getTime() - this.time_offset - 5*60*1000;

    if ( this.current_user.active_time < activity_cutoff ) return false;

    // leave users with other languages in, so the list isn't reshuffled when someone changes language:
    var users = this.users.slice(0);

    if ( language ) language = language.substr(0,2);

    for (;;) { // no need for an end condition - we know at least one user (us) is available
        var user = users.splice(id % users.length, 1)[0];
        if (
            user.active_time >= activity_cutoff &&
            ( !language || language == user.language.substr(0,2) ) // users with another language are treated as offline
        )
            return user === this.current_user;
    }

}