<commit_before>var assign        = require('lodash.assign'),
    bind          = require('lodash.bind'),
    isEmpty       = require('lodash.isempty'),
    jsondiffpatch = require('jsondiffpatch'),

    COMMANDS  = require('./commands'),
    utils     = require('./utils'),
    Server;

Server = function(adapter, transport, diffOptions){
  if(!(adapter && transport)){ throw new Error('Need to specify an adapter and a transport'); }
  if(!diffOptions){ diffOptions = {}; }

  this.adapter = adapter;
  this.transport = transport;
  this.data = {};
  this.requests = {};
  this.saveRequests = {};
  this.saveQueue = {};

  // bind functions
  this.trackConnection = bind(this.trackConnection, this);

  // set up the jsondiffpatch options
  // see here for options: https://github.com/benjamine/jsondiffpatch#options
  diffOptions = assign({
    objectHash: function(obj) { return obj.id || obj._id || JSON.stringify(obj); }
  }, diffOptions);

  this.jsondiffpatch = jsondiffpatch.create(diffOptions);

  this.transport.on('connection', this.trackConnection);
};

/**
 * Registers the correct event listeners
 * @param  {Connection} connection The connection that should get tracked
 */
Server.prototype.trackConnection = function(connection){
  connection.on(COMMANDS.join, this.joinConnection.bind(this, connection));
  connection.on(COMMANDS.syncWithServer, this.receiveEdit.bind(this, connection));
};

/**
 * Joins a connection to a room and send the initial data
 * @param  {Connection} connection
 * @param  {String} room             room identifier
 * @param  {Function} initializeClient Callback that is being used for initialization of the client
 */
Server.prototype.joinConnection = function(connection, room, initializeClient){
  this.getData(room, connection.userid, function(error, data){
    // connect to the room
    connection.join(room);

    // set up the client version for this socket
    // each connection has a backup and a shadow
    // and a set of edits
    data.clientVersions[connection.id] = {
      backup: {
        doc: utils.deepCopy(data.serverCopy),
        serverVersion: 0
      },
      shadow: {
        doc: utils.deepCopy(data.serverCopy),
        serverVersion: 0,
        localVersion: 0
      },
      edits: []
    };

    // send the current server version
    initializeClient(data.serverCopy);
  });
};

/**
 * Gets data for a room from the internal cache or from the adapter
 * @param  {String}   room     room identifier
 * @param  {Function} callback notifier-callback
 */
Server.prototype.getData = function(room, userid, callback){
  var cachedVersion = this.data[room],
      cache = this.data,
      requests = this.requests;

  if(cachedVersion){
    callback(null, cachedVersion);
  }else{
    // if there is no request for this room
    // ask the adapter for the data
    // do nothing in the else case because this operation
    // should only happen once
    if(!requests[room]){
      requests[room] = true;
      this.adapter.getData(room, userid, function(error, data){
        cache[room] = {
          registeredSockets: [],
          clientVersions: {},
          serverCopy: data
        };

        requests[room] = false;
        callback(null, cache[room]);
      });
    }else{
      requests[room] = true;
    }
  }
};

/**
 * Applies the sent edits to the shadow and the server copy, notifies all connected sockets and saves a snapshot
 * @param  {Object} connection   The connection that sent the edits
 * @param  {Object} editMessage  The message containing all edits
 * @param  {Function} sendToClient The callback that sends the server changes back to the client
 */
Server.prototype.receiveEdit = function(connection, editMessage, sendToClient){
  // -1) The algorithm actually says we should use a checksum here, I don't think that's necessary
  // 0) get the relevant doc
  this.getData(editMessage.room, connection.userid, function(err, doc){
    // 0.a) get the client versions
    var clientDoc = doc.clientVersions[connection.id];

    // no client doc could be found, client needs to re-auth
    if(err || !clientDoc){
      connection.emit(COMMANDS.error, 'Need to re-connect!');
      return;
    }

    // when the versions match, remove old edits stack
    if(editMessage.serverVersion === clientDoc.shadow.serverVersion){
      clientDoc.edits = [];
    }

    // 1) iterate over all edits
    editMessage.edits.forEach(function(edit){
      // 2) check the version numbers
      if(edit.serverVersion === clientDoc.shadow.serverVersion &&
        edit.localVersion === clientDoc.shadow.localVersion){
        // versions match
        // backup! TODO: is this the right place to do that?
        clientDoc.backup.doc = utils.deepCopy(clientDoc.shadow.doc);

        // 3) patch the shadow
        // var snapshot = utils.deepCopy(clientDoc.shadow.doc);
        this.jsondiffpatch.patch(clientDoc.shadow.doc, utils.deepCopy(edit.diff));
        // clientDoc.shadow.doc = snapshot;

        // apply the patch to the server's document
        // snapshot = utils.deepCopy(doc.serverCopy);
        this.jsondiffpatch.patch(doc.serverCopy, utils.deepCopy(edit.diff));
        // doc.serverCopy = snapshot;

        // 3.a) increase the version number for the shadow if diff not empty
        if(!isEmpty(edit.diff)){
          clientDoc.shadow.localVersion++;
        }
      }else{
        // TODO: implement backup workflow
        // has a low priority since `packets are not lost` - but don't quote me on that :P
        console.log('error', 'patch rejected!!', edit.serverVersion, '->', clientDoc.shadow.serverVersion, ':',
                    edit.localVersion, '->', clientDoc.shadow.localVersion);
      }
    }.bind(this));

    // 4) save a snapshot of the document
    this.saveSnapshot(editMessage.room, connection.userid);

    // notify all sockets about the update, if not empty
    if(editMessage.edits.length > 0){
      this.transport.to(editMessage.room).emit(COMMANDS.remoteUpdateIncoming, connection.id);
    }

    this.sendServerChanges(doc, clientDoc, sendToClient);
  }.bind(this));
};

Server.prototype.saveSnapshot = function(room, userid){
  var noRequestInProgress = !this.saveRequests[room],
      checkQueueAndSaveAgain = function(){
        // if another save request is in the queue, save again
        var anotherRequestScheduled = this.saveQueue[room] === true;
        this.saveRequests[room] = false;
        if(anotherRequestScheduled){
          this.saveQueue[room] = false;
          this.saveSnapshot(room, userid);
        }
      }.bind(this);

  // only save if no save going on at the moment
  if(noRequestInProgress){
    this.saveRequests[room] = true;
    // get data for saving
    this.getData(room, userid, function(err, data){
      // store data
      if(!err && data){
        this.adapter.storeData(room, userid, data.serverCopy, checkQueueAndSaveAgain);
      }else{
        checkQueueAndSaveAgain();
      }
    }.bind(this));
  }else{
    // schedule a new save request
    this.saveQueue[room] = true;
  }
};

Server.prototype.sendServerChanges = function(doc, clientDoc, send){
  // create a diff from the current server version to the client's shadow
  var diff = this.jsondiffpatch.diff(clientDoc.shadow.doc, doc.serverCopy);
  var basedOnServerVersion = clientDoc.shadow.serverVersion;

  // add the difference to the server's edit stack
  if(!isEmpty(diff)){
    clientDoc.edits.push({
      serverVersion: basedOnServerVersion,
      localVersion: clientDoc.shadow.localVersion,
      diff: diff
    });
    // update the server version
    clientDoc.shadow.serverVersion++;

    // apply the patch to the server shadow
    this.jsondiffpatch.patch(clientDoc.shadow.doc, utils.deepCopy(diff));
  }

  // we explicitly want empty diffs to get sent as well
  send({
    localVersion: clientDoc.shadow.localVersion,
    serverVersion: basedOnServerVersion,
    edits: clientDoc.edits
  });
};

module.exports = Server;
<commit_msg>Add a reset method.<commit_after>var assign = require('lodash.assign'),
  bind = require('lodash.bind'),
  isEmpty = require('lodash.isempty'),
  jsondiffpatch = require('jsondiffpatch'),

  COMMANDS = require('./commands'),
  utils = require('./utils'),
  Server;

Server = function(adapter, transport, diffOptions) {
  if (!(adapter && transport)) {
    throw new Error('Need to specify an adapter and a transport');
  }
  if (!diffOptions) {
    diffOptions = {};
  }

  this.adapter = adapter;
  this.transport = transport;
  this.data = {};
  this.requests = {};
  this.saveRequests = {};
  this.saveQueue = {};

  // bind functions
  this.trackConnection = bind(this.trackConnection, this);

  // set up the jsondiffpatch options
  // see here for options: https://github.com/benjamine/jsondiffpatch#options
  diffOptions = assign({
    objectHash: function(obj) {
      return obj.id || obj._id || JSON.stringify(obj);
    }
  }, diffOptions);

  this.jsondiffpatch = jsondiffpatch.create(diffOptions);

  this.transport.on('connection', this.trackConnection);
};

/**
 * Registers the correct event listeners
 * @param  {Connection} connection The connection that should get tracked
 */
Server.prototype.trackConnection = function(connection) {
  connection.on(COMMANDS.join, this.joinConnection.bind(this, connection));
  connection.on(COMMANDS.syncWithServer, this.receiveEdit.bind(this, connection));
};

/**
 * Joins a connection to a room and send the initial data
 * @param  {Connection} connection
 * @param  {String} room             room identifier
 * @param  {Function} initializeClient Callback that is being used for initialization of the client
 */
Server.prototype.joinConnection = function(connection, room, initializeClient) {
  this.getData(room, connection.userid, function(error, data) {
    // connect to the room
    connection.join(room);

    // set up the client version for this socket
    // each connection has a backup and a shadow
    // and a set of edits
    data.clientVersions[connection.id] = {
      backup: {
        doc: utils.deepCopy(data.serverCopy),
        serverVersion: 0
      },
      shadow: {
        doc: utils.deepCopy(data.serverCopy),
        serverVersion: 0,
        localVersion: 0
      },
      edits: []
    };

    // send the current server version
    initializeClient(data.serverCopy);
  });
};

/**
 * Reset all queues / cached data
 * @param  {Function} callback The function that should be called when complete
 */
Server.prototype.reset = function(callback) {
  var waitForDiffSyncServer = function() {
    var pending = false;
    for (room in this.saveRequests) {
      if (this.saveRequests[room] === true) {
        pending = true;
      }
    }
    if (pending) {
      setTimeout(waitForDiffSyncServer, 1000);
    } else {
      this.data = {};
      this.requests = {};
      this.saveRequests = {};
      this.saveQueue = {};
      callback();
    }
  }.bind(this);
  waitForDiffSyncServer();
};

/**
 * Gets data for a room from the internal cache or from the adapter
 * @param  {String}   room     room identifier
 * @param  {String}   userid   The id of the user who made the requests
 * @param  {Function} callback notifier-callback
 */
Server.prototype.getData = function(room, userid, callback) {
  var cachedVersion = this.data[room],
    cache = this.data,
    requests = this.requests;

  if (cachedVersion) {
    callback(null, cachedVersion);
  } else {
    // if there is no request for this room
    // ask the adapter for the data
    // do nothing in the else case because this operation
    // should only happen once
    if (!requests[room]) {
      requests[room] = true;
      this.adapter.getData(room, userid, function(error, data) {
        cache[room] = {
          registeredSockets: [],
          clientVersions: {},
          serverCopy: data
        };

        requests[room] = false;
        callback(null, cache[room]);
      });
    } else {
      requests[room] = true;
    }
  }
};

/**
 * Applies the sent edits to the shadow and the server copy, notifies all connected sockets and saves a snapshot
 * @param  {Object} connection   The connection that sent the edits
 * @param  {Object} editMessage  The message containing all edits
 * @param  {Function} sendToClient The callback that sends the server changes back to the client
 */
Server.prototype.receiveEdit = function(connection, editMessage, sendToClient) {
  // -1) The algorithm actually says we should use a checksum here, I don't think that's necessary
  // 0) get the relevant doc
  this.getData(editMessage.room, connection.userid, function(err, doc) {
    // 0.a) get the client versions
    var clientDoc = doc.clientVersions[connection.id];

    // no client doc could be found, client needs to re-auth
    if (err || !clientDoc) {
      connection.emit(COMMANDS.error, 'Need to re-connect!');
      return;
    }

    // when the versions match, remove old edits stack
    if (editMessage.serverVersion === clientDoc.shadow.serverVersion) {
      clientDoc.edits = [];
    }

    // 1) iterate over all edits
    editMessage.edits.forEach(function(edit) {
      // 2) check the version numbers
      if (edit.serverVersion === clientDoc.shadow.serverVersion &&
        edit.localVersion === clientDoc.shadow.localVersion) {
        // versions match
        // backup! TODO: is this the right place to do that?
        clientDoc.backup.doc = utils.deepCopy(clientDoc.shadow.doc);

        // 3) patch the shadow
        // var snapshot = utils.deepCopy(clientDoc.shadow.doc);
        this.jsondiffpatch.patch(clientDoc.shadow.doc, utils.deepCopy(edit.diff));
        // clientDoc.shadow.doc = snapshot;

        // apply the patch to the server's document
        // snapshot = utils.deepCopy(doc.serverCopy);
        this.jsondiffpatch.patch(doc.serverCopy, utils.deepCopy(edit.diff));
        // doc.serverCopy = snapshot;

        // 3.a) increase the version number for the shadow if diff not empty
        if (!isEmpty(edit.diff)) {
          clientDoc.shadow.localVersion++;
        }
      } else {
        // TODO: implement backup workflow
        // has a low priority since `packets are not lost` - but don't quote me on that :P
        console.log('error', 'patch rejected!!', edit.serverVersion, '->', clientDoc.shadow.serverVersion, ':',
          edit.localVersion, '->', clientDoc.shadow.localVersion);
      }
    }.bind(this));

    // 4) save a snapshot of the document
    this.saveSnapshot(editMessage.room, connection.userid);

    // notify all sockets about the update, if not empty
    if (editMessage.edits.length > 0) {
      this.transport.to(editMessage.room).emit(COMMANDS.remoteUpdateIncoming, connection.id);
    }

    this.sendServerChanges(doc, clientDoc, sendToClient);
  }.bind(this));
};

Server.prototype.saveSnapshot = function(room, userid) {
  var noRequestInProgress = !this.saveRequests[room],
    checkQueueAndSaveAgain = function() {
      // if another save request is in the queue, save again
      var anotherRequestScheduled = this.saveQueue[room] === true;
      this.saveRequests[room] = false;
      if (anotherRequestScheduled) {
        this.saveQueue[room] = false;
        this.saveSnapshot(room, userid);
      }
    }.bind(this);

  // only save if no save going on at the moment
  if (noRequestInProgress) {
    this.saveRequests[room] = true;
    // get data for saving
    this.getData(room, userid, function(err, data) {
      // store data
      if (!err && data) {
        this.adapter.storeData(room, userid, data.serverCopy, checkQueueAndSaveAgain);
      } else {
        checkQueueAndSaveAgain();
      }
    }.bind(this));
  } else {
    // schedule a new save request
    this.saveQueue[room] = true;
  }
};

Server.prototype.sendServerChanges = function(doc, clientDoc, send) {
  // create a diff from the current server version to the client's shadow
  var diff = this.jsondiffpatch.diff(clientDoc.shadow.doc, doc.serverCopy);
  var basedOnServerVersion = clientDoc.shadow.serverVersion;

  // add the difference to the server's edit stack
  if (!isEmpty(diff)) {
    clientDoc.edits.push({
      serverVersion: basedOnServerVersion,
      localVersion: clientDoc.shadow.localVersion,
      diff: diff
    });
    // update the server version
    clientDoc.shadow.serverVersion++;

    // apply the patch to the server shadow
    this.jsondiffpatch.patch(clientDoc.shadow.doc, utils.deepCopy(diff));
  }

  // we explicitly want empty diffs to get sent as well
  send({
    localVersion: clientDoc.shadow.localVersion,
    serverVersion: basedOnServerVersion,
    edits: clientDoc.edits
  });
};

module.exports = Server;<|endoftext|>


<commit_before>import bodyParser from 'body-parser';
import EventEmitter from 'events';
import mongoose from 'mongoose';
import readline from 'readline';
import Promise from 'bluebird';
import express from 'express';
import Redis from 'ioredis';
import debug from 'debug';
import http from 'http';

mongoose.Promise = Promise;

export default class UWaveServer extends EventEmitter {
  /**
  * Registers middleware on a route
  *
  * @constructor
  * @param {Object} config - for further information, see src/config/server.json.example
  */
  constructor(config = {}) {
    super();
    this.config = config;

    this.app = express();
    this.server = http.createServer(this.app);

    this.mongo = null;
    this.redis = null;

    this.log = debug('uwave:server');
    this.mongoLog = debug('uwave:mongo');
    this.redisLog = debug('uwave:redis');

    this.app.use(bodyParser.json());
    this.app.use(bodyParser.urlencoded({ extended: true }));
    this.app.use((req, res, next) => {
      /* eslint-disable no-param-reassign */
      req.uwave = {
        redis: this.redis,
        mongo: this.mongo,
        keys: this.config.keys
      };
      /* eslint-enable no-param-reassign */
      next();
    });

    /* ======== SIGINT ======== */
    // workaround to properly stop the server on termination.
    if (process.platform === 'win32') {
      readline.createInterface({
        input: process.stdin,
        output: process.stdout
      }).on('SIGINT', () => process.emit('SIGINT'));
    }

    process.on('SIGINT', () => { this.stop(); });
  }

  /**
  * Registers middleware on a route
  *
  * @param {string} path - path the middleware should be registered at
  * @param {function} middleware - the middleware that should be registered
  * @event UWaveServer:registerMiddleware
  * @private
  */
  _registerMiddleware(path, middleware) {
    if (typeof path === 'function') {
      this.app.use(path);
      this.log(`registered middleware ${path.name}`);
    } else if (typeof path === 'string') {
      this.app.use(path, middleware);
      this.log(`registered middleware ${middleware.name} on path '${path}'`);
    } else {
      return this.log('no middleware registered');
    }

    this.emit('registerMiddleware', path, middleware);
  }

  /**
  * Registers api on a route
  *
  * @param {string} path - path the api should be registered at
  * @param {function} router - the api as an express router
  * @event UWaveServer:registerAPI
  * @private
  */
  _registerAPI(path, router) {
    if (typeof path !== 'string') throw new Error('path has to be of type string');
    if (typeof router === 'undefined') throw new Error(`API router for '${path}' was not defined`);

    this.app.use(path, router);
    this.emit('registerAPI', path, router);
    this.log(`registered API ${router.name} on path '${path}'`);
  }

  _createRedisConnection() {
    return new Promise((resolve, reject) => {
      const config = this.config.redis;
      this.redis = new Redis(config.port, config.host, config.options);
      this.redis.on('ready', resolve);
      this.redis.on('error', e => {
        reject(e);
        this.emit('redisError', e);
      });
      this.redis.on('reconnecting', () => this.redisLog('trying to reconnect...'));

      this.redis.on('end', () => {
        this.redisLog('disconnected');
        this.emit('redisDisconnect');
      });

      this.redis.on('connect', () => {
        this.redisLog('connected');
        this.emit('redisConnect');
      });
    });
  }

  _createMongoConnection() {
    this.mongo = mongoose.createConnection();
    const promise = this.mongo.open(
      `mongodb://${this.config.mongo.host}:${this.config.mongo.port}/uwave`,
      this.config.mongo.options
    );

    this.mongo.on('error', e => {
      this.mongoLog(e);
      this.emit('mongoError', e);
    });

    this.mongo.on('reconnected', () => {
      this.mongoLog('reconnected');
      this.emit('mongoReconnect');
    });

    this.mongo.on('disconnected', () => {
      this.mongoLog('disconnected');
      this.emit('mongoDisconnect');
    });

    return promise;
  }

  /**
  * Registers middleware on a route
  *
  * @param {string} path - path the middleware should be registered at
  * @param {function} middleware - the middleware that should be registered
  * @event UWaveServer:registerMiddleware
  */
  registerMiddleware(path, middleware) {
    // assume that path is an array of middleware functions
    if (!Array.isArray(path)) {
      this._registerMiddleware(path, middleware);
    } else {
      path.forEach(ware => {
        this._registerMiddleware(ware.path, ware.middleware);
      });
    }
  }

  /**
  * Registers api on a route
  *
  * @param {string} path - path the api should be registered at
  * @param {function} router - the api as an express router
  * @event UWaveServer:registerAPI
  */
  registerAPI(path, router) {
    // assume that path is an array of api functions
    if (!Array.isArray(path)) {
      this._registerAPI(path, router);
    } else {
      path.forEach(api => {
        this._registerAPI(api.path, api.router);
      });
    }
  }

  /**
  * get the server config
  **/
  getConfig() {
    return this.config;
  }

  /**
  * get the server instance
  **/
  getServer() {
    return this.server;
  }

  /**
  * gets the express.app instance. For information about express
  * see {@link http://expressjs.com}
  **/
  getApp() {
    return this.app;
  }

  /**
  * gets a reference to the redis instance. For information about redis
  * see {@link http://redis.io}
  **/
  getRedis() {
    return this.redis;
  }

  /**
  * gets a reference to the mongo instance. For information about mongo
  * see {@link http://mongodb.org}
  **/
  getMongo() {
    return this.mongo;
  }

  /**
  * gets a reference to the singleton that mongoose represents. For information about mongoose
  * see {@link http://mongoosejs.com/docs/api.html}
  **/
  getMongoose() {
    return mongoose;
  }

  /**
   * Set up database connections.
   */
  async connect() {
    this.log('starting server...');

    await Promise.all([
      this._createRedisConnection(),
      this._createMongoConnection()
    ]);

    this.emit('started', this);
    this.log('server started');

    return this;
  }

  /**
   * Old name for connect().
   *
   * @deprecated
   */
  start() {
    return this.connect();
  }

  /**
  * Stops the server
  */
  stop() {
    this.log('stopping server...');
    this.redis.save();
    this.redis.end();
    this.redis.removeAllListeners();
    this.redis = null;

    this.mongo.close(() => {
      this.mongo.removeAllListeners();
      this.mongo = null;
      this.mongoLog('connection closed');
      this.emit('stopped');
    });
  }
}
<commit_msg>server: pass full üWave source object to requests<commit_after>import bodyParser from 'body-parser';
import EventEmitter from 'events';
import mongoose from 'mongoose';
import readline from 'readline';
import Promise from 'bluebird';
import express from 'express';
import Redis from 'ioredis';
import debug from 'debug';
import http from 'http';

mongoose.Promise = Promise;

export default class UWaveServer extends EventEmitter {
  /**
  * Registers middleware on a route
  *
  * @constructor
  * @param {Object} config - for further information, see src/config/server.json.example
  */
  constructor(config = {}) {
    super();
    this.config = config;

    this.app = express();
    this.server = http.createServer(this.app);

    this.mongo = null;
    this.redis = null;

    this.log = debug('uwave:server');
    this.mongoLog = debug('uwave:mongo');
    this.redisLog = debug('uwave:redis');

    this.app.use(bodyParser.json());
    this.app.use(bodyParser.urlencoded({ extended: true }));
    this.app.use((req, res, next) => {
      /* eslint-disable no-param-reassign */
      req.uwave = this;
      /* eslint-enable no-param-reassign */
      next();
    });

    /* ======== SIGINT ======== */
    // workaround to properly stop the server on termination.
    if (process.platform === 'win32') {
      readline.createInterface({
        input: process.stdin,
        output: process.stdout
      }).on('SIGINT', () => process.emit('SIGINT'));
    }

    process.on('SIGINT', () => { this.stop(); });
  }

  /**
  * Registers middleware on a route
  *
  * @param {string} path - path the middleware should be registered at
  * @param {function} middleware - the middleware that should be registered
  * @event UWaveServer:registerMiddleware
  * @private
  */
  _registerMiddleware(path, middleware) {
    if (typeof path === 'function') {
      this.app.use(path);
      this.log(`registered middleware ${path.name}`);
    } else if (typeof path === 'string') {
      this.app.use(path, middleware);
      this.log(`registered middleware ${middleware.name} on path '${path}'`);
    } else {
      return this.log('no middleware registered');
    }

    this.emit('registerMiddleware', path, middleware);
  }

  /**
  * Registers api on a route
  *
  * @param {string} path - path the api should be registered at
  * @param {function} router - the api as an express router
  * @event UWaveServer:registerAPI
  * @private
  */
  _registerAPI(path, router) {
    if (typeof path !== 'string') throw new Error('path has to be of type string');
    if (typeof router === 'undefined') throw new Error(`API router for '${path}' was not defined`);

    this.app.use(path, router);
    this.emit('registerAPI', path, router);
    this.log(`registered API ${router.name} on path '${path}'`);
  }

  _createRedisConnection() {
    return new Promise((resolve, reject) => {
      const config = this.config.redis;
      this.redis = new Redis(config.port, config.host, config.options);
      this.redis.on('ready', resolve);
      this.redis.on('error', e => {
        reject(e);
        this.emit('redisError', e);
      });
      this.redis.on('reconnecting', () => this.redisLog('trying to reconnect...'));

      this.redis.on('end', () => {
        this.redisLog('disconnected');
        this.emit('redisDisconnect');
      });

      this.redis.on('connect', () => {
        this.redisLog('connected');
        this.emit('redisConnect');
      });
    });
  }

  _createMongoConnection() {
    this.mongo = mongoose.createConnection();
    const promise = this.mongo.open(
      `mongodb://${this.config.mongo.host}:${this.config.mongo.port}/uwave`,
      this.config.mongo.options
    );

    this.mongo.on('error', e => {
      this.mongoLog(e);
      this.emit('mongoError', e);
    });

    this.mongo.on('reconnected', () => {
      this.mongoLog('reconnected');
      this.emit('mongoReconnect');
    });

    this.mongo.on('disconnected', () => {
      this.mongoLog('disconnected');
      this.emit('mongoDisconnect');
    });

    return promise;
  }

  /**
  * Registers middleware on a route
  *
  * @param {string} path - path the middleware should be registered at
  * @param {function} middleware - the middleware that should be registered
  * @event UWaveServer:registerMiddleware
  */
  registerMiddleware(path, middleware) {
    // assume that path is an array of middleware functions
    if (!Array.isArray(path)) {
      this._registerMiddleware(path, middleware);
    } else {
      path.forEach(ware => {
        this._registerMiddleware(ware.path, ware.middleware);
      });
    }
  }

  /**
  * Registers api on a route
  *
  * @param {string} path - path the api should be registered at
  * @param {function} router - the api as an express router
  * @event UWaveServer:registerAPI
  */
  registerAPI(path, router) {
    // assume that path is an array of api functions
    if (!Array.isArray(path)) {
      this._registerAPI(path, router);
    } else {
      path.forEach(api => {
        this._registerAPI(api.path, api.router);
      });
    }
  }

  /**
  * get the server config
  **/
  getConfig() {
    return this.config;
  }

  /**
  * get the server instance
  **/
  getServer() {
    return this.server;
  }

  /**
  * gets the express.app instance. For information about express
  * see {@link http://expressjs.com}
  **/
  getApp() {
    return this.app;
  }

  /**
  * gets a reference to the redis instance. For information about redis
  * see {@link http://redis.io}
  **/
  getRedis() {
    return this.redis;
  }

  /**
  * gets a reference to the mongo instance. For information about mongo
  * see {@link http://mongodb.org}
  **/
  getMongo() {
    return this.mongo;
  }

  /**
  * gets a reference to the singleton that mongoose represents. For information about mongoose
  * see {@link http://mongoosejs.com/docs/api.html}
  **/
  getMongoose() {
    return mongoose;
  }

  /**
   * Set up database connections.
   */
  async connect() {
    this.log('starting server...');

    await Promise.all([
      this._createRedisConnection(),
      this._createMongoConnection()
    ]);

    this.emit('started', this);
    this.log('server started');

    return this;
  }

  /**
   * Old name for connect().
   *
   * @deprecated
   */
  start() {
    return this.connect();
  }

  /**
  * Stops the server
  */
  stop() {
    this.log('stopping server...');
    this.redis.save();
    this.redis.end();
    this.redis.removeAllListeners();
    this.redis = null;

    this.mongo.close(() => {
      this.mongo.removeAllListeners();
      this.mongo = null;
      this.mongoLog('connection closed');
      this.emit('stopped');
    });
  }
}
<|endoftext|>


<commit_before>define( [
            'dojo/_base/declare',
            'dojo/_base/array',
            'dijit/MenuItem',
            'JBrowse/View/Dialog/SetTrackHeight',
            'JBrowse/Util',
            'JBrowse/View/Track/CanvasFeatures',
            'JBrowse/View/Track/_AlignmentsMixin'
        ],
        function(
            declare,
            array,
            MenuItem,
            Dialog,
            Util,
            CanvasFeatureTrack,
            AlignmentsMixin
        ) {

return declare( [ CanvasFeatureTrack, AlignmentsMixin ], {
    _defaultConfig: function() {
        var c = Util.deepUpdate(
            dojo.clone( this.inherited(arguments) ),
            {
                glyph: 'JBrowse/View/FeatureGlyph/Alignment',
                maxFeatureGlyphExpansion: 0,
                maxFeatureScreenDensity: 15,
                orientationType: 'fr',

                hideDuplicateReads: true,
                hideQCFailingReads: true,
                hideSecondary: true,
                hideSupplementary: true,
                hideUnmapped: true,
                hideUnsplicedReads: false,
                hideMissingMatepairs: false,
                hideIncorrectAlignments: false,
                hideForwardStrand: false,
                hideReverseStrand: false,
                useXS: false,
                useTS: false,
                useReverseTemplate: false,
                useReverseTemplateOption: true,
                viewAsPairs: false,
                viewAsSpans: false,
                maxInsertSize: 50000,
                readCloudLogScale: true,
                showInterchromosomalArcs: true,
                showLargeArcs: true,

                histograms: {
                    description: 'coverage depth',
                    binsPerBlock: 200
                },

                style: {
                    showLabels: false
                }
            }
        );
        return c;
    },

    _trackType: function() {
    },

    _trackMenuOptions() {
        var thisB = this;
        var displayOptions = [];

        var m = {
            type: 'dijit/Menu',
            label: 'Track visualization types',
            children: []
        }
        var c = {
            type: 'dijit/Menu',
            label: 'Coloring options',
            children: []
        }

        displayOptions.push(m)
        displayOptions.push(c)

        m.children.push({
            label: 'View as unpaired',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/Alignment',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/Alignment'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        m.children.push({
            label: 'View as pairs',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedAlignment',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/PairedAlignment'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        m.children.push({
            label: 'View as arcs',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/PairedArc'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        m.children.push({
            label: 'View as read cloud',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/PairedReadCloud'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        m.children.push({
            label: 'View coverage',
            type: 'dijit/RadioMenuItem',
            checked: false,
            onClick: function(event) {
                thisB.config.type = 'JBrowse/View/Track/SNPCoverage'
                thisB.config._oldAlignmentsHeight = thisB.config.style.height
                thisB.config.style.height = thisB.config._oldSnpCoverageHeight
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        c.children.push({
            label: 'Color by default',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.defaultColor,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.defaultColor = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by XS tag (RNA-seq orientation)',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.useXS,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.useXS = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        c.children.push({
            label: 'Color by TS tag (RNA-seq orientation)',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.useTS,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.useTS = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color mate pair as flipped (RNA-seq orientation)',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.useReverseTemplate,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.useReverseTemplate = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        c.children.push({
            label: 'Color by mapping quality',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorByMAPQ,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorByMAPQ = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by orientation',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorByOrientation,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorByOrientation = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by insert size',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorBySize,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorBySize = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by orientation and insert size',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorByOrientationAndSize,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorByOrientationAndSize = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        if(this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud') {
            displayOptions.push({
                type: 'dijit/Menu',
                label: 'Read cloud options',
                children: [{
                    label: 'View log scale',
                    type: 'dijit/CheckedMenuItem',
                    checked: !!this.config.readCloudLogScale,
                    onClick: function(event) {
                        thisB.config.readCloudLogScale = this.get('checked');
                        thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                    }
                }, {
                    label: 'Set Y-scale size',
                    onClick: function(event) {
                        new Dialog({
                            title: 'Set read cloud Y-scale in terms of the maximum expected insert size',
                            msg: ' insert size (note: default estimated from sampling track data)',
                            maxHeight: Infinity,
                            height: thisB.config.readCloudYScaleMax || 50000,
                            setCallback: ret => {
                                thisB.config.readCloudYScaleMax = ret
                                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                            }
                        }).show()
                    }
                }]
            });
        }
        if(this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc') {
            displayOptions.push({
                type: 'dijit/Menu',
                label: 'Paired arc options',
                children: [{
                    label: 'Show interchromosomal',
                    type: 'dijit/CheckedMenuItem',
                    checked: !!this.config.showInterchromosomalArcs,
                    onClick: function(event) {
                        thisB.config.showInterchromosomalArcs = this.get('checked');
                        thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                    }
                }, {
                    label: 'Show large arcs',
                    type: 'dijit/CheckedMenuItem',
                    checked: !!this.config.showLargeArcs,
                    onClick: function(event) {
                        thisB.config.showLargeArcs = this.get('checked');
                        thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                    }
                }]
            });
        }

        return Promise.all([ this.inherited(arguments), this._alignmentsFilterTrackMenuOptions(), displayOptions ])
            .then( function( options ) {
                       var o = options.shift();
                       options.unshift({ type: 'dijit/MenuSeparator' } );
                       return o.concat.apply( o, options );
                   });
    },
    clearColorConfig() {
        Object.assign(this.config, {
            defaultColor: false,
            useTS: false,
            useXS: false,
            useReverseTemplate: false,
            colorByMAPQ: false,
            colorByOrientation: false,
            colorBySize: false,
            colorByOrientationAndSize: false
        })
    },

    // override getLayout to access addRect method
    _getLayout() {
        var layout = this.inherited(arguments);
        if(this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud' ||
            this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc') {
            layout = declare.safeMixin(layout, {
                addRect: function() {
                    this.pTotalHeight = this.maxHeight;
                    return 0;
                }
            });
        }
        return layout;
    },

    fillFeatures( args ) {
        const finishCallback = args.finishCallback
        const errorCallback = e => {
            console.error(e)
            this._handleError(e, args)
            finishCallback(e)
        }

        if(this.config.viewAsPairs || this.config.viewAsSpans
            || (this.config.colorByOrientationAndSize || this.config.colorBySize && !this.insertSizeStats)) {
            let supermethod = this.getInherited(arguments)
            const len = args.rightBase - args.leftBase
            const region = {
                ref: this.refSeq.name,
                start: Math.max(0, args.leftBase),
                end: args.rightBase,
                viewAsPairs: true
            }
            const numNeighboringBlockFetches = 6
            const min = Math.max(0, region.start - Math.min(Math.max(len*numNeighboringBlockFetches, this.config.maxInsertSize), 100000))
            const max = region.end + Math.min(Math.max(len*numNeighboringBlockFetches, this.config.maxInsertSize), 100000)

            var cachePromise = new Promise((resolve, reject) => {
                this.store.getFeatures({
                    ref: this.refSeq.name,
                    start: min,
                    end: max,
                    viewAsPairs: this.config.viewAsPairs,
                    viewAsSpans: this.config.viewAsSpans,
                    maxInsertSize: this.config.maxInsertSize
                }, () => { /* do nothing */}, () => {
                    this.insertSizeStats = this.insertSizeStats || this.store.getInsertSizeStats()

                    resolve()
                }, reject)
            })
            cachePromise.then(() => {
                args.finishCallback = () => {
                    finishCallback()
                    setTimeout(() => {
                        this.store.cleanFeatureCache({
                            ref: this.refSeq.name,
                            start: min,
                            end: max
                        })
                    }, 10000)
                }
                supermethod.call(this, args)
            }, errorCallback)
        } else {
            this.inherited(arguments);
        }
    },

    constructor() {
        if (this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc') {
            this.config.viewAsSpans = true
            this.config.viewAsPairs = false
        } else if (this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedAlignment'
            || this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud') {
            this.config.viewAsPairs = true
            this.config.viewAsSpans = false
        } else {
            this.config.viewAsPairs = false
            this.config.viewAsSpans = false
        }

        // determine if alternate color scheme in use, otherwise make default
        var elts = ['defaultColor', 'useXS', 'useTS', 'useReverseTemplate', 'colorByOrientation', 'colorBySize', 'colorByOrientationAndSize', 'colorByMAPQ']
        var none = true
        for(var i = 0; i < elts.length; i++) {
            if(this.config[elts[i]]) {
                none = false
            }
        }
        if(none) {
            this.config.defaultColor = true
        }

    },

    renderClickMap() {
        if (this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc'
        || this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud') {
            return
        } else {
            this.inherited(arguments)
        }
    }
});
});
<commit_msg>Remove the timeout on clearing feature cache<commit_after>define( [
            'dojo/_base/declare',
            'dojo/_base/array',
            'dijit/MenuItem',
            'JBrowse/View/Dialog/SetTrackHeight',
            'JBrowse/Util',
            'JBrowse/View/Track/CanvasFeatures',
            'JBrowse/View/Track/_AlignmentsMixin'
        ],
        function(
            declare,
            array,
            MenuItem,
            Dialog,
            Util,
            CanvasFeatureTrack,
            AlignmentsMixin
        ) {

return declare( [ CanvasFeatureTrack, AlignmentsMixin ], {
    _defaultConfig: function() {
        var c = Util.deepUpdate(
            dojo.clone( this.inherited(arguments) ),
            {
                glyph: 'JBrowse/View/FeatureGlyph/Alignment',
                maxFeatureGlyphExpansion: 0,
                maxFeatureScreenDensity: 15,
                orientationType: 'fr',

                hideDuplicateReads: true,
                hideQCFailingReads: true,
                hideSecondary: true,
                hideSupplementary: true,
                hideUnmapped: true,
                hideUnsplicedReads: false,
                hideMissingMatepairs: false,
                hideIncorrectAlignments: false,
                hideForwardStrand: false,
                hideReverseStrand: false,
                useXS: false,
                useTS: false,
                useReverseTemplate: false,
                useReverseTemplateOption: true,
                viewAsPairs: false,
                viewAsSpans: false,
                maxInsertSize: 50000,
                readCloudLogScale: true,
                showInterchromosomalArcs: true,
                showLargeArcs: true,

                histograms: {
                    description: 'coverage depth',
                    binsPerBlock: 200
                },

                style: {
                    showLabels: false
                }
            }
        );
        return c;
    },

    _trackType: function() {
    },

    _trackMenuOptions() {
        var thisB = this;
        var displayOptions = [];

        var m = {
            type: 'dijit/Menu',
            label: 'Track visualization types',
            children: []
        }
        var c = {
            type: 'dijit/Menu',
            label: 'Coloring options',
            children: []
        }

        displayOptions.push(m)
        displayOptions.push(c)

        m.children.push({
            label: 'View as unpaired',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/Alignment',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/Alignment'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        m.children.push({
            label: 'View as pairs',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedAlignment',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/PairedAlignment'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        m.children.push({
            label: 'View as arcs',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/PairedArc'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        m.children.push({
            label: 'View as read cloud',
            type: 'dijit/RadioMenuItem',
            checked: this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud',
            onClick: function(event) {
                thisB.config.glyph = 'JBrowse/View/FeatureGlyph/PairedReadCloud'
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        m.children.push({
            label: 'View coverage',
            type: 'dijit/RadioMenuItem',
            checked: false,
            onClick: function(event) {
                thisB.config.type = 'JBrowse/View/Track/SNPCoverage'
                thisB.config._oldAlignmentsHeight = thisB.config.style.height
                thisB.config.style.height = thisB.config._oldSnpCoverageHeight
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        c.children.push({
            label: 'Color by default',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.defaultColor,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.defaultColor = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by XS tag (RNA-seq orientation)',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.useXS,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.useXS = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        c.children.push({
            label: 'Color by TS tag (RNA-seq orientation)',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.useTS,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.useTS = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color mate pair as flipped (RNA-seq orientation)',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.useReverseTemplate,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.useReverseTemplate = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });

        c.children.push({
            label: 'Color by mapping quality',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorByMAPQ,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorByMAPQ = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by orientation',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorByOrientation,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorByOrientation = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by insert size',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorBySize,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorBySize = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        c.children.push({
            label: 'Color by orientation and insert size',
            type: 'dijit/RadioMenuItem',
            checked: !!this.config.colorByOrientationAndSize,
            onClick: function(event) {
                thisB.clearColorConfig()
                thisB.config.colorByOrientationAndSize = this.get('checked');
                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
            }
        });
        if(this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud') {
            displayOptions.push({
                type: 'dijit/Menu',
                label: 'Read cloud options',
                children: [{
                    label: 'View log scale',
                    type: 'dijit/CheckedMenuItem',
                    checked: !!this.config.readCloudLogScale,
                    onClick: function(event) {
                        thisB.config.readCloudLogScale = this.get('checked');
                        thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                    }
                }, {
                    label: 'Set Y-scale size',
                    onClick: function(event) {
                        new Dialog({
                            title: 'Set read cloud Y-scale in terms of the maximum expected insert size',
                            msg: ' insert size (note: default estimated from sampling track data)',
                            maxHeight: Infinity,
                            height: thisB.config.readCloudYScaleMax || 50000,
                            setCallback: ret => {
                                thisB.config.readCloudYScaleMax = ret
                                thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                            }
                        }).show()
                    }
                }]
            });
        }
        if(this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc') {
            displayOptions.push({
                type: 'dijit/Menu',
                label: 'Paired arc options',
                children: [{
                    label: 'Show interchromosomal',
                    type: 'dijit/CheckedMenuItem',
                    checked: !!this.config.showInterchromosomalArcs,
                    onClick: function(event) {
                        thisB.config.showInterchromosomalArcs = this.get('checked');
                        thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                    }
                }, {
                    label: 'Show large arcs',
                    type: 'dijit/CheckedMenuItem',
                    checked: !!this.config.showLargeArcs,
                    onClick: function(event) {
                        thisB.config.showLargeArcs = this.get('checked');
                        thisB.browser.publish('/jbrowse/v1/v/tracks/replace', [thisB.config]);
                    }
                }]
            });
        }

        return Promise.all([ this.inherited(arguments), this._alignmentsFilterTrackMenuOptions(), displayOptions ])
            .then( function( options ) {
                       var o = options.shift();
                       options.unshift({ type: 'dijit/MenuSeparator' } );
                       return o.concat.apply( o, options );
                   });
    },
    clearColorConfig() {
        Object.assign(this.config, {
            defaultColor: false,
            useTS: false,
            useXS: false,
            useReverseTemplate: false,
            colorByMAPQ: false,
            colorByOrientation: false,
            colorBySize: false,
            colorByOrientationAndSize: false
        })
    },

    // override getLayout to access addRect method
    _getLayout() {
        var layout = this.inherited(arguments);
        if(this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud' ||
            this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc') {
            layout = declare.safeMixin(layout, {
                addRect: function() {
                    this.pTotalHeight = this.maxHeight;
                    return 0;
                }
            });
        }
        return layout;
    },

    fillFeatures( args ) {
        const finishCallback = args.finishCallback
        const errorCallback = e => {
            console.error(e)
            this._handleError(e, args)
            finishCallback(e)
        }

        if(this.config.viewAsPairs || this.config.viewAsSpans
            || (this.config.colorByOrientationAndSize || this.config.colorBySize && !this.insertSizeStats)) {
            let supermethod = this.getInherited(arguments)
            const len = args.rightBase - args.leftBase
            const region = {
                ref: this.refSeq.name,
                start: Math.max(0, args.leftBase),
                end: args.rightBase,
                viewAsPairs: true
            }
            const numNeighboringBlockFetches = 6
            const min = Math.max(0, region.start - Math.min(Math.max(len*numNeighboringBlockFetches, this.config.maxInsertSize), 100000))
            const max = region.end + Math.min(Math.max(len*numNeighboringBlockFetches, this.config.maxInsertSize), 100000)

            var cachePromise = new Promise((resolve, reject) => {
                this.store.getFeatures({
                    ref: this.refSeq.name,
                    start: min,
                    end: max,
                    viewAsPairs: this.config.viewAsPairs,
                    viewAsSpans: this.config.viewAsSpans,
                    maxInsertSize: this.config.maxInsertSize
                }, () => { /* do nothing */}, () => {
                    this.insertSizeStats = this.insertSizeStats || this.store.getInsertSizeStats()

                    resolve()
                }, reject)
            })
            cachePromise.then(() => {
                args.finishCallback = () => {
                    finishCallback()
                    this.store.cleanFeatureCache({
                        ref: this.refSeq.name,
                        start: min,
                        end: max
                    })
                }
                supermethod.call(this, args)
            }, errorCallback)
        } else {
            this.inherited(arguments);
        }
    },

    constructor() {
        if (this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc') {
            this.config.viewAsSpans = true
            this.config.viewAsPairs = false
        } else if (this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedAlignment'
            || this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud') {
            this.config.viewAsPairs = true
            this.config.viewAsSpans = false
        } else {
            this.config.viewAsPairs = false
            this.config.viewAsSpans = false
        }

        // determine if alternate color scheme in use, otherwise make default
        var elts = ['defaultColor', 'useXS', 'useTS', 'useReverseTemplate', 'colorByOrientation', 'colorBySize', 'colorByOrientationAndSize', 'colorByMAPQ']
        var none = true
        for(var i = 0; i < elts.length; i++) {
            if(this.config[elts[i]]) {
                none = false
            }
        }
        if(none) {
            this.config.defaultColor = true
        }

    },

    renderClickMap() {
        if (this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedArc'
        || this.config.glyph == 'JBrowse/View/FeatureGlyph/PairedReadCloud') {
            return
        } else {
            this.inherited(arguments)
        }
    }
});
});
<|endoftext|>


<commit_before>;window.Caster = (function (window, document, undefined)
{ 'use strict';

    // the private property/method store
    var _p = {};

    // the main application constructor
    var Caster = function ()
    {
        // private local properties (constructor access only)
        var THAT = this;

        // private properties
        _p.cast   = {
            message_bus:      null,
            receiver_manager: null,
            sender_list:      null,
            session:          null
        };
        _p.peerjs = {
            api_key:          'w19j6rp47wx9a4i',
            connection:       null,
            id:               null,
            peer:             null,
            sender_id:        null
        };

        // private methods
        _p.initializePeerJS = function ()
        {
            _p.peerjs.peer = new Peer({ key: _p.peerjs.api_key });

            _p.peerjs.peer.on('open', function (id)
            {
                _p.peerjs.id = id;
            });
        };

        _p.initializeCastAPI = function ()
        {
            var launchMessageListener = function ()
            {
                document.getElementById('message-container').innerHTML = 'Now Listening for Messages...';

                _p.cast.message_bus.onMessage = function (messageEvent)
                {
                    var sender_id = messageEvent.senderId;
                    var message   = JSON.parse(messageEvent.data);

                    switch (message.type)
                    {
                        case 'message':
                            document.getElementById('message-container').innerHTML = message.payload;

                            break;
                        case 'peerjs-id':
                            _p.peerjs.sender_id = message.payload;
                            _p.peerjs.connection = _p.peerjs.peer.connect(_p.peerjs.sender_id);

                            _p.peerjs.connection.on('open', function ()
                            {
                                var message = {
                                    type:    'peerjs-id',
                                    payload: _p.peerjs.id
                                };

                                _p.peerjs.connection.send(JSON.stringify(message));
                            });

                            _p.peerjs.peer.on('connection', function (conn)
                            {
                                var video = document.getElementById('video');
                                var count = 0;

                                video.src = video.webkitMediaSourceURL;

                                conn.on('data', function (data)
                                {
                                    video.webkitSourceAppend(new Uint8Array(data.payload));

                                    console.log('adding video chunk (' + count + ')...');
                                    ++count;
                                });
                            });

                            break;
                        default:

                            break;
                    }
                };
            };

            _p.cast.receiver_manager = cast.receiver.CastReceiverManager.getInstance();
            _p.cast.message_bus      = _p.cast.receiver_manager.getCastMessageBus('urn:x-cast:io.renobit.apps.just-cast-it');

            // start listening for messages from senders
            launchMessageListener();

            // initiate Cast application
            _p.cast.receiver_manager.start();
        };
    };

    Caster.prototype.Start = function ()
    {
        _p.initializePeerJS();
        _p.initializeCastAPI();
    };

    return new Caster();

})(window, document);

window.Caster.Start();
<commit_msg>Updating to newer usage of MediaSource.<commit_after>;window.Caster = (function (window, document, undefined)
{ 'use strict';

    // the private property/method store
    var _p = {};

    // the main application constructor
    var Caster = function ()
    {
        // private local properties (constructor access only)
        var THAT = this;

        // private properties
        _p.cast   = {
            message_bus:      null,
            receiver_manager: null,
            sender_list:      null,
            session:          null
        };
        _p.peerjs = {
            api_key:          'w19j6rp47wx9a4i',
            connection:       null,
            id:               null,
            peer:             null,
            sender_id:        null
        };

        // private methods
        _p.initializePeerJS = function ()
        {
            _p.peerjs.peer = new Peer({ key: _p.peerjs.api_key });

            _p.peerjs.peer.on('open', function (id)
            {
                _p.peerjs.id = id;
            });
        };

        _p.initializeCastAPI = function ()
        {
            var launchMessageListener = function ()
            {
                document.getElementById('message-container').innerHTML = 'Now Listening for Messages...';

                _p.cast.message_bus.onMessage = function (messageEvent)
                {
                    var sender_id = messageEvent.senderId;
                    var message   = JSON.parse(messageEvent.data);

                    switch (message.type)
                    {
                        case 'message':
                            document.getElementById('message-container').innerHTML = message.payload;

                            break;
                        case 'peerjs-id':
                            _p.peerjs.sender_id = message.payload;
                            _p.peerjs.connection = _p.peerjs.peer.connect(_p.peerjs.sender_id);

                            _p.peerjs.connection.on('open', function ()
                            {
                                var message = {
                                    type:    'peerjs-id',
                                    payload: _p.peerjs.id
                                };

                                _p.peerjs.connection.send(JSON.stringify(message));
                            });

                            _p.peerjs.peer.on('connection', function (conn)
                            {
                                var video = document.getElementById('video');
                                var count = 0;
                                var mediaSource = new MediaSource();
                                var sourceBuffer = mediaSource.addSourceBuffer('video/mp4');

                                video.src = window.URL.createObjectURL(mediaSource);;

                                conn.on('data', function (data)
                                {
                                    sourceBuffer.appendBuffer(new Uint8Array(data.payload));

                                    console.log('adding video chunk (' + count + ')...');
                                    ++count;
                                });
                            });

                            break;
                        default:

                            break;
                    }
                };
            };

            _p.cast.receiver_manager = cast.receiver.CastReceiverManager.getInstance();
            _p.cast.message_bus      = _p.cast.receiver_manager.getCastMessageBus('urn:x-cast:io.renobit.apps.just-cast-it');

            // start listening for messages from senders
            launchMessageListener();

            // initiate Cast application
            _p.cast.receiver_manager.start();
        };
    };

    Caster.prototype.Start = function ()
    {
        _p.initializePeerJS();
        _p.initializeCastAPI();
    };

    return new Caster();

})(window, document);

window.Caster.Start();
<|endoftext|>


<commit_before>run.$inject = ['$rootScope', '$location', '$cookieStore', '$http', '$state',
    '$timeout'
];

function run($rootScope, $location, $cookieStore, $http, $state, $timeout) {
    $rootScope.links = [];
    $rootScope.stateLoading = false;
    $rootScope.hideLoading = true;
    $rootScope.generalLoading = true;
    $rootScope.Session = window.localStorage['Session'];
    $rootScope.Username = window.localStorage['Username'];
    $rootScope.Role = window.localStorage['Role'];
    $rootScope.StudentId = window.localStorage['StudentId'];
    $rootScope.ProfessorDBId = window.localStorage['ProfessorDBId'];
    $rootScope.guest = true;
    $rootScope.loggedIn = $rootScope.Session.length > 0;

    //console.log($location.$$path);/registro-maestro/MNzKO9OcC%2fxsr3gN41FTDw%3d%3d
    let path = $location.$$path.substring(0, 18);
    let view = $location.$$path.substring(18);
    if(path === '/registro-maestro/') {
        view = encodeURIComponent(view);
        $location.path(path + view);
    }

    let stateUrl = "";
    let redirect = require('./redirect');

    getBasicAuthentication();

    $rootScope.$on('$stateChangeStart', stateChangeStart);

    $rootScope.$on('$stateChangeSuccess', stateChangeSuccess);

    function getBasicAuthentication() {
        $rootScope.globals = $cookieStore.get('globals') || {};

        if ($rootScope.globals.token) {
            $http.defaults.headers.common['Authorization'] =
                $rootScope.globals.token;
        }
    }

    function stateChangeStart (event, toState) {
        $rootScope.stateLoading = true;
        stateUrl = toState.url;
        redirect($state, toState.name,
            $rootScope.Role.toLowerCase(), $rootScope.Session.length > 0, event);
    }

    function stateChangeSuccess (event) {
        $timeout(function(){
            $rootScope.stateLoading = false;
            $rootScope.generalLoading = true;
        }, $rootScope.generalLoading ? 500 : 500);
    }
}

module.exports = run;
<commit_msg>Change variable name from view to token<commit_after>run.$inject = ['$rootScope', '$location', '$cookieStore', '$http', '$state',
    '$timeout'
];

function run($rootScope, $location, $cookieStore, $http, $state, $timeout) {
    $rootScope.links = [];
    $rootScope.stateLoading = false;
    $rootScope.hideLoading = true;
    $rootScope.generalLoading = true;
    $rootScope.Session = window.localStorage['Session'];
    $rootScope.Username = window.localStorage['Username'];
    $rootScope.Role = window.localStorage['Role'];
    $rootScope.StudentId = window.localStorage['StudentId'];
    $rootScope.ProfessorDBId = window.localStorage['ProfessorDBId'];
    $rootScope.guest = true;
    $rootScope.loggedIn = $rootScope.Session.length > 0;

    //console.log($location.$$path);/registro-maestro/MNzKO9OcC%2fxsr3gN41FTDw%3d%3d
    let path = $location.$$path.substring(0, 18);
    let token = $location.$$path.substring(18);
    if(path === '/registro-maestro/') {
        token = encodeURIComponent(token);
        $location.path(path + token);
    }

    let stateUrl = "";
    let redirect = require('./redirect');

    getBasicAuthentication();

    $rootScope.$on('$stateChangeStart', stateChangeStart);

    $rootScope.$on('$stateChangeSuccess', stateChangeSuccess);

    function getBasicAuthentication() {
        $rootScope.globals = $cookieStore.get('globals') || {};

        if ($rootScope.globals.token) {
            $http.defaults.headers.common['Authorization'] =
                $rootScope.globals.token;
        }
    }

    function stateChangeStart (event, toState) {
        $rootScope.stateLoading = true;
        stateUrl = toState.url;
        redirect($state, toState.name,
            $rootScope.Role.toLowerCase(), $rootScope.Session.length > 0, event);
    }

    function stateChangeSuccess (event) {
        $timeout(function(){
            $rootScope.stateLoading = false;
            $rootScope.generalLoading = true;
        }, $rootScope.generalLoading ? 500 : 500);
    }
}

module.exports = run;
<|endoftext|>


<commit_before>/**
 * Dependencies
 */

var fs = require('fs');
var path = require('path');
var spawnSync = require('child_process').spawnSync;

/**
 * Options
 */

startDir = process.argv[2];

/**
 * Define functions
 */

// recursive search a la *nix find

function findGitDirs(dirFile) {
  fs.stat(dirFile, function(err, stats) {
    if (err) throw err;
    // if directory, call findGitDirs again on all files within the directory
    if (stats.isDirectory()) {
      var absPath = path.resolve(dirFile);
      if (path.basename(absPath) === '.git') {
        gitPull(absPath);
      }
      fs.readdir(dirFile, function(err, files) {
        if (err) throw err;
        for(var i = 0; i < files.length; i++) {
          findGitDirs(path.join(dirFile, files[i])); // recurse
        }
      });
    }
  });
}

function gitPull(gitPath) {
  var parentDir = path.dirname(gitPath);
  var gitRemote = spawnSync('git', ['--git-dir=' + gitPath, 'config', '--get', 'remote.origin.url']);
  if (gitRemote.status === 0) {
    var gitPull = spawnSync('git', ['--git-dir=' + gitPath, 'pull']);
    if (gitPull.error) throw gitPull.error;
    // handle errors
    if (gitPull.status !== 0) {
      console.log('Cannot update ' + parentDir);
      console.log('non-zero status: ' + gitPull.status);
    } else {
      console.log('Updating ' + parentDir);
      console.log(gitPull.stdout.toString());
    }
  } else {
    console.log('Cannot update ' + parentDir);
    console.log('Nothing to update as there is not a remote origin');
    console.log();
  }
};

/**
 * Call function
 */

findGitDirs(startDir) ;
<commit_msg>added #!/usr/bin/env node<commit_after>#!/usr/bin/env node

/**
 * Dependencies
 */

var fs = require('fs');
var path = require('path');
var spawnSync = require('child_process').spawnSync;

/**
 * Options
 */

startDir = process.argv[2];

/**
 * Define functions
 */

// recursive search a la *nix find

function findGitDirs(dirFile) {
  fs.stat(dirFile, function(err, stats) {
    if (err) throw err;
    // if directory, call findGitDirs again on all files within the directory
    if (stats.isDirectory()) {
      var absPath = path.resolve(dirFile);
      if (path.basename(absPath) === '.git') {
        gitPull(absPath);
      }
      fs.readdir(dirFile, function(err, files) {
        if (err) throw err;
        for(var i = 0; i < files.length; i++) {
          findGitDirs(path.join(dirFile, files[i])); // recurse
        }
      });
    }
  });
}

function gitPull(gitPath) {
  var parentDir = path.dirname(gitPath);
  var gitRemote = spawnSync('git', ['--git-dir=' + gitPath, 'config', '--get', 'remote.origin.url']);
  if (gitRemote.status === 0) {
    var gitPull = spawnSync('git', ['--git-dir=' + gitPath, 'pull']);
    if (gitPull.error) throw gitPull.error;
    // handle errors
    if (gitPull.status !== 0) {
      console.log('Cannot update ' + parentDir);
      console.log('non-zero status: ' + gitPull.status);
    } else {
      console.log('Updating ' + parentDir);
      console.log(gitPull.stdout.toString());
    }
  } else {
    console.log('Cannot update ' + parentDir);
    console.log('Nothing to update as there is not a remote origin');
    console.log();
  }
};

/**
 * Call function
 */

findGitDirs(startDir) ;
<|endoftext|>


<commit_before>/* eslint-disable no-await-in-loop */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2020
 */

import { ToastAndroid } from 'react-native';
import { generateUUID } from 'react-native-database';

import { UIDatabase } from '../database';
import { Sensor } from '../database/DataTypes';
import { createRecord } from '../database/utilities';
import { MILLISECONDS } from '../utilities';
import { chunk } from '../utilities/chunk';

import { vaccineStrings } from '../localization';
import { PageActions } from '../pages/dataTableUtilities/actions';
import { ROUTES } from '../navigation';

export const TEMPERATURE_SYNC_ACTIONS = {
  OPEN_MODAL: 'TemperatureSync/openModal',
  CLOSE_MODAL: 'TemperatureSync/closeModal',
  ERROR_NO_SENSORS: 'TemperatureSync/errorNoSensors',

  START_SYNC: 'TemperatureSync/startSync',
  COMPLETE_SYNC: 'TemperatureSync/completeSync',

  SCAN_START: 'TemperatureSync/scanStart',
  SCAN_COMPLETE: 'TemperatureSync/scanComplete',
  SCAN_ERROR: 'TemperatureSync/scanError',

  DOWNLOAD_LOGS_START: 'TemperatureSync/downloadLogsStart',
  DOWNLOAD_LOGS_COMPLETE: 'TemperatureSync/downloadLogsComplete',
  DOWNLOAD_LOGS_ERROR: 'TemperatureSync/downloadLogsError',

  START_RESETTING_ADVERTISEMENT_FREQUENCY: 'TemperatureSync/startResettingAdvertisementFrequency',
  ERROR_RESETTING_ADVERTISEMENT_FREQUENCY: 'TemperatureSync/errorResettingAdvertisementFrequency',
  COMPLETE_RESETTING_ADVERTISEMENT_FREQUENCY:
    'TemperatureSync/completeResettingAdvertisementFrequency',
  START_RESETTING_LOG_FREQUENCY: 'TemperatureSync/startResettingLogFrequency',
  COMPLETE_RESETTING_LOG_FREQUENCY: 'TemperatureSync/completeResettingLogFrequency',
  ERROR_RESETTING_LOG_FREQUENCY: 'TemperatureSync/errorResettingLogFrequency',

  START_SAVING_TEMPERATURE_LOGS: 'TemperatureSync/startSavingTemperatureLogs',
  COMPLETE_SAVING_TEMPERATURE_LOGS: 'TemperatureSync/completeSavingTemperatureLogs',

  UPDATE_SENSOR_PROGRESS: 'TemperatureSync/updateSensorProgress',
};

const openModal = () => ({ type: TEMPERATURE_SYNC_ACTIONS.OPEN_MODAL });
const closeModal = () => ({ type: TEMPERATURE_SYNC_ACTIONS.CLOSE_MODAL });

const scanStart = () => ({ type: TEMPERATURE_SYNC_ACTIONS.SCAN_START });
const scanComplete = () => ({ type: TEMPERATURE_SYNC_ACTIONS.SCAN_COMPLETE });
const scanError = () => ({ type: TEMPERATURE_SYNC_ACTIONS.SCAN_ERROR });

const updateSensors = sensorAdvertisements => dispatch => {
  const isArray = Array.isArray(sensorAdvertisements);

  if (!isArray) dispatch(scanError());
  if (isArray && !sensorAdvertisements.length) dispatch(scanError());

  if (isArray && sensorAdvertisements.length) {
    sensorAdvertisements.forEach(({ macAddress, batteryLevel }) => {
      UIDatabase.write(() => {
        UIDatabase.update('Sensor', {
          id: generateUUID(),
          ...UIDatabase.get('Sensor', macAddress, 'macAddress'),
          macAddress,
          batteryLevel,
        });
      });
    });
  }
};

const startSensorScan = () => async dispatch => {
  await dispatch(scanForSensors());
  dispatch(PageActions.refreshData(ROUTES.VACCINE_ADMIN_PAGE));
};

const scanForSensors = () => async dispatch => {
  dispatch(scanStart());

  const sensorResult = await Sensor.startScan();
  const { success, data } = sensorResult;

  dispatch(updateSensors(data));

  if (success) {
    ToastAndroid.show(
      vaccineStrings.formatString(vaccineStrings.found_sensors, data.length),
      ToastAndroid.LONG
    );
    dispatch(scanComplete());
  } else {
    ToastAndroid.show(vaccineStrings.could_not_find_sensors, ToastAndroid.LONG);
  }
};

const downloadLogsError = () => ({ type: TEMPERATURE_SYNC_ACTIONS.DOWNLOAD_LOGS_ERROR });
const downloadLogsStart = () => ({ type: TEMPERATURE_SYNC_ACTIONS.DOWNLOAD_LOGS_START });
const downloadLogsComplete = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.DOWNLOAD_LOGS_COMPLETE,
});

const saveLogs = (logData, sensor) => async dispatch => {
  dispatch(startSavingTemperatureLogs(logData?.length));
  const timeNow = new Date().getTime();

  UIDatabase.write(() =>
    logData.forEach(({ temperature }, i) => {
      const timestamp = timeNow - (i + 1) * MILLISECONDS.FIVE_MINUTES;
      createRecord(UIDatabase, 'SensorLog', temperature, new Date(timestamp), sensor);
    })
  );
};

const downloadLogs = sensor => async dispatch => {
  dispatch(downloadLogsStart());

  const downloadedLogsResult = (await sensor?.downloadLogs()) ?? {};
  const { success = false, data = [] } = downloadedLogsResult;

  if (success && data?.[0]?.logs) {
    dispatch(downloadLogsComplete());
    return data?.[0]?.logs;
  }
  dispatch(downloadLogsError());
  return null;
};

const startResettingAdvertisementFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.START_RESETTING_ADVERTISEMENT_FREQUENCY,
});
const completeResettingAdvertisementFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_RESETTING_ADVERTISEMENT_FREQUENCY,
});
const errorResettingAdvertisementFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.ERROR_RESETTING_ADVERTISEMENT_FREQUENCY,
});

const resetAdvertisementFrequency = sensor => async dispatch => {
  dispatch(startResettingAdvertisementFrequency());

  const resettingAdvertisementFrequencyResult = await sensor.resetAdvertisementFrequency();

  const { success } = resettingAdvertisementFrequencyResult;

  if (success) dispatch(completeResettingAdvertisementFrequency());
  else dispatch(errorResettingAdvertisementFrequency());

  return success;
};

const startResettingLogFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.START_RESETTING_LOG_FREQUENCY,
});
const completeResettingLogFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_RESETTING_LOG_FREQUENCY,
});
const errorResettingLogFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.ERROR_RESETTING_LOG_FREQUENCY,
});

const resetLogFrequency = sensor => async dispatch => {
  dispatch(startResettingLogFrequency());

  const resettingLogFrequencyResult = await sensor.resetLogFrequency();

  const { success } = resettingLogFrequencyResult;

  if (success) dispatch(completeResettingLogFrequency());
  else dispatch(errorResettingLogFrequency());

  return success;
};

const startSavingTemperatureLogs = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.START_SAVING_TEMPERATURE_LOGS,
});
const completeSavingTemperatureLogs = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_SAVING_TEMPERATURE_LOGS,
});

const createTemperatureLogs = sensor => async dispatch => {
  const { sensorLogs, location } = sensor;

  // Only create TemperatureLogs for the greatest multiple of 6 SensorLogs,
  // as each SensorLog is a 5 minute log, and each Temperature log a 30 minute log.
  const iterateTo = sensorLogs.length - (sensorLogs.length % 6);
  const sensorLogsToGroup = sensorLogs.sorted('timestamp').slice(0, iterateTo);
  const groupedSensorLogs = chunk(sensorLogsToGroup, 6);

  UIDatabase.write(() => {
    groupedSensorLogs.forEach(sensorLogGroup => {
      const { hasBreached, mostRecentTemperatureBreach } = sensor;
      const newLogTemperature = Math.min(...sensorLogGroup.map(({ temperature }) => temperature));
      const newLogTimestamp = new Date(
        Math.min(...sensorLogGroup.map(({ timestamp }) => timestamp))
      );

      const newLog = createRecord(
        UIDatabase,
        'TemperatureLog',
        newLogTemperature,
        newLogTimestamp,
        location
      );

      if (hasBreached) {
        if (mostRecentTemperatureBreach?.willContinueBreach(newLog)) {
          UIDatabase.update('TemperatureLog', { ...newLog, breach: mostRecentTemperatureBreach });
        } else {
          UIDatabase.update('TemperatureBreach', {
            ...mostRecentTemperatureBreach,
            endTimestamp: newLogTimestamp,
          });
        }
      } else {
        const breachConfigs = UIDatabase.objects('TemperatureBreachConfiguration');
        breachConfigs.some(breachConfig => breachConfig.createBreach(UIDatabase, location, newLog));
      }
    });

    UIDatabase.delete('SensorLog', sensorLogsToGroup);
  });

  dispatch(completeSavingTemperatureLogs());
};

const errorNoSensors = () => ({ type: TEMPERATURE_SYNC_ACTIONS.ERROR_NO_SENSORS });
const startSync = () => ({ type: TEMPERATURE_SYNC_ACTIONS.START_SYNC });
const completeSync = () => ({ type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_SYNC });
const updateSensorProgress = sensor => ({
  type: TEMPERATURE_SYNC_ACTIONS.UPDATE_SENSOR_PROGRESS,
  payload: { sensor },
});

const syncTemperatures = () => async (dispatch, getState) => {
  const { temperatureSync } = getState();
  const { isSyncing } = temperatureSync;

  const sensors = UIDatabase.objects('Sensor').filtered('location != null && isActive == true');
  const { length: numberOfSensors } = sensors;

  if (isSyncing) return null;
  if (!numberOfSensors) return dispatch(errorNoSensors());

  dispatch(startSync());

  for (let i = 0; i < numberOfSensors; i++) {
    const sensor = sensors[i];

    dispatch(updateSensorProgress(sensor));
    const downloadedLogsResult = await dispatch(downloadLogs(sensor));

    if (downloadedLogsResult) {
      const resetLogFrequencyResult = await dispatch(resetLogFrequency(sensor));

      if (resetLogFrequencyResult) {
        const resetAdvertisementFrequencyResult = await dispatch(
          resetAdvertisementFrequency(sensor)
        );

        if (resetAdvertisementFrequencyResult) {
          await dispatch(saveLogs(downloadedLogsResult, sensor));
          await dispatch(createTemperatureLogs(sensor));
        }
      }
    }
  }
  return dispatch(completeSync());
};

export const TemperatureSyncActions = {
  syncTemperatures,
  openModal,
  closeModal,
  startSensorScan,
};
<commit_msg>Add use of constants in TemperatureSyncActions<commit_after>/* eslint-disable no-await-in-loop */
/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2020
 */

import { ToastAndroid } from 'react-native';
import { generateUUID } from 'react-native-database';

import { UIDatabase } from '../database';
import { Sensor } from '../database/DataTypes';
import { createRecord } from '../database/utilities';
import { MILLISECONDS } from '../utilities';
import { chunk } from '../utilities/chunk';

import { vaccineStrings } from '../localization';
import { PageActions } from '../pages/dataTableUtilities/actions';
import { ROUTES } from '../navigation';
import { VACCINE_CONSTANTS } from '../utilities/modules/vaccines';

export const TEMPERATURE_SYNC_ACTIONS = {
  OPEN_MODAL: 'TemperatureSync/openModal',
  CLOSE_MODAL: 'TemperatureSync/closeModal',
  ERROR_NO_SENSORS: 'TemperatureSync/errorNoSensors',

  START_SYNC: 'TemperatureSync/startSync',
  COMPLETE_SYNC: 'TemperatureSync/completeSync',

  SCAN_START: 'TemperatureSync/scanStart',
  SCAN_COMPLETE: 'TemperatureSync/scanComplete',
  SCAN_ERROR: 'TemperatureSync/scanError',

  DOWNLOAD_LOGS_START: 'TemperatureSync/downloadLogsStart',
  DOWNLOAD_LOGS_COMPLETE: 'TemperatureSync/downloadLogsComplete',
  DOWNLOAD_LOGS_ERROR: 'TemperatureSync/downloadLogsError',

  START_RESETTING_ADVERTISEMENT_FREQUENCY: 'TemperatureSync/startResettingAdvertisementFrequency',
  ERROR_RESETTING_ADVERTISEMENT_FREQUENCY: 'TemperatureSync/errorResettingAdvertisementFrequency',
  COMPLETE_RESETTING_ADVERTISEMENT_FREQUENCY:
    'TemperatureSync/completeResettingAdvertisementFrequency',
  START_RESETTING_LOG_FREQUENCY: 'TemperatureSync/startResettingLogFrequency',
  COMPLETE_RESETTING_LOG_FREQUENCY: 'TemperatureSync/completeResettingLogFrequency',
  ERROR_RESETTING_LOG_FREQUENCY: 'TemperatureSync/errorResettingLogFrequency',

  START_SAVING_TEMPERATURE_LOGS: 'TemperatureSync/startSavingTemperatureLogs',
  COMPLETE_SAVING_TEMPERATURE_LOGS: 'TemperatureSync/completeSavingTemperatureLogs',

  UPDATE_SENSOR_PROGRESS: 'TemperatureSync/updateSensorProgress',
};

const openModal = () => ({ type: TEMPERATURE_SYNC_ACTIONS.OPEN_MODAL });
const closeModal = () => ({ type: TEMPERATURE_SYNC_ACTIONS.CLOSE_MODAL });

const scanStart = () => ({ type: TEMPERATURE_SYNC_ACTIONS.SCAN_START });
const scanComplete = () => ({ type: TEMPERATURE_SYNC_ACTIONS.SCAN_COMPLETE });
const scanError = () => ({ type: TEMPERATURE_SYNC_ACTIONS.SCAN_ERROR });

const updateSensors = sensorAdvertisements => dispatch => {
  const isArray = Array.isArray(sensorAdvertisements);

  if (!isArray) dispatch(scanError());
  if (isArray && !sensorAdvertisements.length) dispatch(scanError());

  if (isArray && sensorAdvertisements.length) {
    sensorAdvertisements.forEach(({ macAddress, batteryLevel }) => {
      UIDatabase.write(() => {
        UIDatabase.update('Sensor', {
          id: generateUUID(),
          ...UIDatabase.get('Sensor', macAddress, 'macAddress'),
          macAddress,
          batteryLevel,
        });
      });
    });
  }
};

const startSensorScan = () => async dispatch => {
  await dispatch(scanForSensors());
  dispatch(PageActions.refreshData(ROUTES.VACCINE_ADMIN_PAGE));
};

const scanForSensors = () => async dispatch => {
  dispatch(scanStart());

  const sensorResult = await Sensor.startScan();
  const { success, data } = sensorResult;

  dispatch(updateSensors(data));

  if (success) {
    ToastAndroid.show(
      vaccineStrings.formatString(vaccineStrings.found_sensors, data.length),
      ToastAndroid.LONG
    );
    dispatch(scanComplete());
  } else {
    ToastAndroid.show(vaccineStrings.could_not_find_sensors, ToastAndroid.LONG);
  }
};

const downloadLogsError = () => ({ type: TEMPERATURE_SYNC_ACTIONS.DOWNLOAD_LOGS_ERROR });
const downloadLogsStart = () => ({ type: TEMPERATURE_SYNC_ACTIONS.DOWNLOAD_LOGS_START });
const downloadLogsComplete = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.DOWNLOAD_LOGS_COMPLETE,
});

const saveLogs = (logData, sensor) => async dispatch => {
  dispatch(startSavingTemperatureLogs(logData?.length));
  const timeNow = new Date().getTime();

  UIDatabase.write(() =>
    logData.forEach(({ temperature }, i) => {
      const timestamp = timeNow - (i + 1) * MILLISECONDS.FIVE_MINUTES;
      createRecord(UIDatabase, 'SensorLog', temperature, new Date(timestamp), sensor);
    })
  );
};

const downloadLogs = sensor => async dispatch => {
  dispatch(downloadLogsStart());

  const downloadedLogsResult = (await sensor?.downloadLogs()) ?? {};
  const { success = false, data = [] } = downloadedLogsResult;

  if (success && data?.[0]?.logs) {
    dispatch(downloadLogsComplete());
    return data?.[0]?.logs;
  }
  dispatch(downloadLogsError());
  return null;
};

const startResettingAdvertisementFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.START_RESETTING_ADVERTISEMENT_FREQUENCY,
});
const completeResettingAdvertisementFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_RESETTING_ADVERTISEMENT_FREQUENCY,
});
const errorResettingAdvertisementFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.ERROR_RESETTING_ADVERTISEMENT_FREQUENCY,
});

const resetAdvertisementFrequency = sensor => async dispatch => {
  dispatch(startResettingAdvertisementFrequency());

  const resettingAdvertisementFrequencyResult = await sensor.resetAdvertisementFrequency();

  const { success } = resettingAdvertisementFrequencyResult;

  if (success) dispatch(completeResettingAdvertisementFrequency());
  else dispatch(errorResettingAdvertisementFrequency());

  return success;
};

const startResettingLogFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.START_RESETTING_LOG_FREQUENCY,
});
const completeResettingLogFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_RESETTING_LOG_FREQUENCY,
});
const errorResettingLogFrequency = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.ERROR_RESETTING_LOG_FREQUENCY,
});

const resetLogFrequency = sensor => async dispatch => {
  dispatch(startResettingLogFrequency());

  const resettingLogFrequencyResult = await sensor.resetLogFrequency();

  const { success } = resettingLogFrequencyResult;

  if (success) dispatch(completeResettingLogFrequency());
  else dispatch(errorResettingLogFrequency());

  return success;
};

const startSavingTemperatureLogs = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.START_SAVING_TEMPERATURE_LOGS,
});
const completeSavingTemperatureLogs = () => ({
  type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_SAVING_TEMPERATURE_LOGS,
});

const createTemperatureLogs = sensor => async dispatch => {
  const { sensorLogs, location } = sensor;

  // Only create TemperatureLogs for the greatest multiple of 6 SensorLogs,
  // as each SensorLog is a 5 minute log, and each Temperature log a 30 minute log.
  const iterateTo =
    sensorLogs.length - (sensorLogs.length % VACCINE_CONSTANTS.SENSOR_LOGS_PER_TEMPERATURE_LOG);
  const sensorLogsToGroup = sensorLogs.sorted('timestamp').slice(0, iterateTo);
  const groupedSensorLogs = chunk(
    sensorLogsToGroup,
    VACCINE_CONSTANTS.SENSOR_LOGS_PER_TEMPERATURE_LOG
  );

  UIDatabase.write(() => {
    groupedSensorLogs.forEach(sensorLogGroup => {
      const { hasBreached, mostRecentTemperatureBreach } = sensor;
      const newLogTemperature = Math.min(...sensorLogGroup.map(({ temperature }) => temperature));
      const newLogTimestamp = new Date(
        Math.min(...sensorLogGroup.map(({ timestamp }) => timestamp))
      );

      const newLog = createRecord(
        UIDatabase,
        'TemperatureLog',
        newLogTemperature,
        newLogTimestamp,
        location
      );

      if (hasBreached) {
        if (mostRecentTemperatureBreach?.willContinueBreach(newLog)) {
          UIDatabase.update('TemperatureLog', { ...newLog, breach: mostRecentTemperatureBreach });
        } else {
          UIDatabase.update('TemperatureBreach', {
            ...mostRecentTemperatureBreach,
            endTimestamp: newLogTimestamp,
          });
        }
      } else {
        const breachConfigs = UIDatabase.objects('TemperatureBreachConfiguration');
        breachConfigs.some(breachConfig => breachConfig.createBreach(UIDatabase, location, newLog));
      }
    });

    UIDatabase.delete('SensorLog', sensorLogsToGroup);
  });

  dispatch(completeSavingTemperatureLogs());
};

const errorNoSensors = () => ({ type: TEMPERATURE_SYNC_ACTIONS.ERROR_NO_SENSORS });
const startSync = () => ({ type: TEMPERATURE_SYNC_ACTIONS.START_SYNC });
const completeSync = () => ({ type: TEMPERATURE_SYNC_ACTIONS.COMPLETE_SYNC });
const updateSensorProgress = sensor => ({
  type: TEMPERATURE_SYNC_ACTIONS.UPDATE_SENSOR_PROGRESS,
  payload: { sensor },
});

const syncTemperatures = () => async (dispatch, getState) => {
  const { temperatureSync } = getState();
  const { isSyncing } = temperatureSync;

  const sensors = UIDatabase.objects('Sensor').filtered('location != null && isActive == true');
  const { length: numberOfSensors } = sensors;

  if (isSyncing) return null;
  if (!numberOfSensors) return dispatch(errorNoSensors());

  dispatch(startSync());

  for (let i = 0; i < numberOfSensors; i++) {
    const sensor = sensors[i];

    dispatch(updateSensorProgress(sensor));
    const downloadedLogsResult = await dispatch(downloadLogs(sensor));

    if (downloadedLogsResult) {
      const resetLogFrequencyResult = await dispatch(resetLogFrequency(sensor));

      if (resetLogFrequencyResult) {
        const resetAdvertisementFrequencyResult = await dispatch(
          resetAdvertisementFrequency(sensor)
        );

        if (resetAdvertisementFrequencyResult) {
          await dispatch(saveLogs(downloadedLogsResult, sensor));
          await dispatch(createTemperatureLogs(sensor));
        }
      }
    }
  }
  return dispatch(completeSync());
};

export const TemperatureSyncActions = {
  syncTemperatures,
  openModal,
  closeModal,
  startSensorScan,
};
<|endoftext|>