<commit_before>'use strict';

var bunyan = require('bunyan');
var cluster = require('cluster');
var path = require('path');
var os = require('os');

// Exports

module.exports = {
    run: run,
    reload: reload,
    setConfig: setConfig,
    shutdown: shutdown
};

// State

var config;
var generation = 0;
var isRunning = true;
var currentGenerationForked = false;
var inBeforeReload = false;
var serverStatus = {
    status: 'initializing',
    startTime: new Date(),
    connections: 0,
    requests: 0,
    bytesRead: 0,
    bytesWritten: 0,
    pid: process.pid,
    generation: null,
    config: null,
    workers: null
};
var serverStatusTimeout = null;

// Logging

var log = null;

/**
 * Run the cluster master
 *
 * Options:
 * - exec: the script to exit
 * - workers: worker count (default: host CPU count)
 * - args: arguments passed to workers (default: [])
 * - startupTimeout: how long the master waits for worker startup (default: 10000)
 * - shutdownTimeout: how long the master waits for worker shutdown (default: 30000)
 * - silent: suppress worker output
 * - log: bunyan instance for logging
 * - beforeReload: function that is executed before the master is reloaded
 * - beforeShutdown: unction that is executed before the master is shut down
 *
 * @param {Object} opts
 * @api public
 */
function run(opts) {

    // Config

    config = opts;
    if (!config.exec) throw new Error('Must define an "exec" script');
    config.workers = config.workers || os.cpus().length;
    config.startupTimeout = config.startupTimeout || 10000;
    config.shutdownTimeout = config.shutdownTimeout || 30000;

    if (opts.log) {
        log = opts.log.child({'component': 'master'});
    } else {
        log = bunyan.createLogger({name: 'flora-cluster', 'component': 'master'});
    }

    cluster.setupMaster({
        exec: path.resolve(config.exec),
        args: config.args || [],
        silent: config.silent
    });

    cluster.on('fork', onFork);

    // Setup signals

    process.on('SIGTERM', function onSigTerm() {
        log.info('received SIGTERM, exiting immediately');
        process.exit(0);
    });

    process.on('SIGINT', function onSigInt() {
        log.info('received SIGINT, exiting immediately');
        process.exit(0);
    });

    process.on('SIGQUIT', function onSigQuit() {
        log.info('received SIGQUIT, shutting down gracefully');
        shutdown();
    });

    process.on('SIGHUP', function onSigHup() {
        log.info('received SIGHUP, reloading gracefully');
        reload();
    });

    process.on('uncaughtException', function onUncaughtException(err) {
        log.error(err.stack);
        process.exit(1);
    });

    reload();

    function onFork(worker) {
        worker._floraGeneration = generation;
        worker._floraStatus = 'forked';

        worker.on('message', function onWorkerMessage(message) {
            if (!message.event) return;
            log.trace('message from worker ' + worker.process.pid, message.event);
            worker.emit('flora::' + message.event, message);
        });

        worker.on('online', function onWorkerOnline() {
            if (worker._floraStatus === 'forked') {
                worker._floraStatus = 'initializing';
            }
        });

        worker.on('listening', function onWorkerListening() {
            if (worker._floraStatus === 'initializing') {
                worker._floraStatus = 'running';

                if (worker._floraKillTimeout) {
                    clearTimeout(worker._floraKillTimeout);
                    delete worker._floraKillTimeout;
                }
            }

            readjustCluster();
        });

        worker.on('flora::shutdown', function onWorkerShutdown() {
            worker._floraStatus = 'shutdown';

            readjustCluster();
        });

        worker.on('disconnect', function onWorkerDisconnect() {
            worker._floraStatus = 'disconnected';

            readjustCluster();
        });

        worker.on('exit', function onWorkerExit(code, signal) {
            worker._floraStatus = 'exited';

            aggregateServerStatus(worker.fullStatus || {}, serverStatus);

            if (signal) {
                log.warn('worker %d killed by %s', worker.process.pid, signal);
            } else if (code !== 0) {
                log.warn('worker %d exited with error code %d', worker.process.pid, code);
            } else {
                log.info('worker %d exited successfully', worker.process.pid);
            }

            if (worker._florakillTimeout) {
                clearTimeout(worker._floraKillTimeout);
                delete worker._floraKillTimeout;
            }

            readjustCluster();
        });

        worker.on('flora::serverStatus', function onServerStatus() {
            log.trace('worker %d requested serverStatus', worker.process.pid);
            var askWorkerId, askWorker;

            worker._floraNotifyServerStatus = true;

            if (!serverStatusTimeout) {
                serverStatusTimeout = setTimeout(function onServerStatusTimeout() {
                    serverStatusTimeout = null;
                    notifyServerStatus(true);
                }, 1000);
            }

            for (askWorkerId in cluster.workers) {
                askWorker = cluster.workers[askWorkerId];

                askWorker.flora.fullStatusIsUp2date = false;
                askWorker.send({event: 'status'});
            }
        });

        worker.on('flora::status', function onWorkerStatus(message) {
            worker._floraFullStatus = message.status;
            worker._floraFullStatusIsUp2date = true;

            notifyServerStatus();
        });

        setWorkerKillTimeout(worker, 'startup');
    }
}

/**
 * Reload the workers
 *
 * @api public
 */
function reload() {
    inBeforeReload = true;

    if (config.beforeReload && generation > 0) {
        config.beforeReload(function () {
            doReload();
        });
    } else {
        doReload();
    }

    function doReload() {
        inBeforeReload = false;

        generation++;
        currentGenerationForked = false;

        if (generation === 1) {
            serverStatus.status = 'starting';
            log.info('Starting Flora master process at PID ' + process.pid);
        } else {
            serverStatus.status = 'reloading';
            log.info('Gracefully reloading Flora (generation ' + generation + ')');
        }

        readjustCluster();
    }
}

/**
 * Set the cluster config. Merges config values into the existing config
 *
 * @param {Object} opts
 * @api public
 */
function setConfig(opts) {
    for (var name in opts) {
        config[name] = opts[name];
    }

    if (!inBeforeReload) {
        readjustCluster();
    }
}

function readjustCluster() {
    var worker, workerId, i;
    var activeWorkers = 0, runningWorkers = 0;

    // count active and running workers for current generation:
    for (workerId in cluster.workers) {
        worker = cluster.workers[workerId];
        if (worker._floraGeneration !== generation) continue;

        if (worker._floraStatus === 'running' ||
            worker._floraStatus === 'initializing' ||
            worker._floraStatus === 'forked') {
            activeWorkers++;

            if (worker._floraStatus === 'running') {
                runningWorkers++;
            }
        }
    }

    if (serverStatus.status === 'reloading') {
        if (currentGenerationForked && activeWorkers < config.workers) {
            // rollback failing new generation (will be killed automatically afterwards):
            generation--;
            serverStatus.status = 'running';

            log.error('Graceful reload failed - keeping generation ' + generation + ' online');

            readjustCluster();
            return;
        }
    }

    // TODO: do throttling on permanent errors

    if (isRunning) {
        // start missing workers:
        for (i = 0; i < config.workers - activeWorkers; i++) {
            log.info('readjustCluster: forking new worker');
            cluster.fork();
        }

        currentGenerationForked = true;

        // stop workers of current generation if we have too much:
        if (activeWorkers > config.workers) {
            var tooMuchWorkers = activeWorkers - config.workers;

            for (workerId in cluster.workers) {
                if (tooMuchWorkers <= 0) break;

                worker = cluster.workers[workerId];

                if (worker._floraGeneration !== generation) continue;

                shutdownWorker(worker);
                tooMuchWorkers--;
            }
        }
    }

    // if all workers for new generation are up and running, shutdown the old ones -
    // same process if we are shutting down the complete server:
    if (runningWorkers >= config.workers || !isRunning) {
        if (isRunning) {
            serverStatus.status = 'running';
        }

        for (workerId in cluster.workers) {
            worker = cluster.workers[workerId];

            if (worker._floraGeneration === generation && isRunning) continue;

            shutdownWorker(worker);
        }
    }

    notifyServerStatus();
}

function shutdownWorker(worker) {
    if (worker._floraStatus === 'running' ||
        worker._floraStatus === 'initializing' ||
        worker._floraStatus === 'forked') {
        log.info('Shutdown worker ' + worker.process.pid + ' (generation ' + worker._floraGeneration + ')');

        worker._floraStatus = 'shutdownRequested';
        worker.send({event: 'shutdown'});

        setWorkerKillTimeout(worker, 'shutdown');
    }
}

function setWorkerKillTimeout(worker, type) {
    if (worker._floraKillTimeout) {
        clearTimeout(worker._floraKillTimeout);
    }

    // we add one second to the timeouts in master, to give the worker the chance
    // to handle the same timeouts itself before we kill him

    worker._floraKillTimeout = setTimeout(function onWorkerKillTimeout() {
        log.warn('Killing worker ' + worker.process.pid + ' after ' + type + ' timeout by master');
        worker.kill();
    }, config[type + 'Timeout'] + 1000);
}

function notifyServerStatus(timeoutReached) {
    var workerStatusIsUp2date = true;
    var workerId, worker, workerStatus;
    var serverStatusToSend = {}, key;

    if (serverStatusTimeout) {
        clearTimeout(serverStatusTimeout);
        serverStatusTimeout = null;
    }

    for (key in serverStatus) {
        serverStatusToSend[key] = serverStatus[key];
    }

    serverStatusToSend.generation = generation;
    serverStatusToSend.config = config;
    serverStatusToSend.workers = {};

    for (workerId in cluster.workers) {
        worker = cluster.workers[workerId];

        workerStatus = worker._floraFullStatus || {};
        workerStatus.status = worker._floraStatus;
        workerStatus.generation = worker._floraGeneration;

        if (timeoutReached && !worker._floraFullStatusIsUp2date) {
            workerStatus.error = 'Worker did not respond - status outdated';
        }

        aggregateServerStatus(workerStatus, serverStatusToSend);

        serverStatusToSend.workers[workerId] = workerStatus;

        if (!worker._floraFullStatusIsUp2date) {
            workerStatusIsUp2date = false;
        }
    }

    if (workerStatusIsUp2date || timeoutReached) {
        for (workerId in cluster.workers) {
            worker = cluster.workers[workerId];

            if (!worker._floraNotifyServerStatus) continue;

            worker.send({
                event: 'serverStatus',
                serverStatus: serverStatusToSend
            });

            delete worker._floraNotifyServerStatus;
        }
    }
}

function aggregateServerStatus(workerStatus, status) {
    status.connections += workerStatus.connections || 0;
    status.requests += workerStatus.requests || 0;
    status.bytesRead += workerStatus.bytesRead || 0;
    status.bytesWritten += workerStatus.bytesWritten || 0;
}

/**
 * Shutdown the cluster
 *
 * @api public
 */
function shutdown() {
    if (config.beforeShutdown) {
        config.beforeShutdown(function () {
            doShutdown();
        });
    } else {
        doShutdown();
    }

    function doShutdown() {
        isRunning = false;
        serverStatus.status = 'shutdown';

        readjustCluster();
    }
}
<commit_msg>Fixed Bug: flora.fullStatusIsUp2date -> _floraFullStatusIsUp2date<commit_after>'use strict';

var bunyan = require('bunyan');
var cluster = require('cluster');
var path = require('path');
var os = require('os');

// Exports

module.exports = {
    run: run,
    reload: reload,
    setConfig: setConfig,
    shutdown: shutdown
};

// State

var config;
var generation = 0;
var isRunning = true;
var currentGenerationForked = false;
var inBeforeReload = false;
var serverStatus = {
    status: 'initializing',
    startTime: new Date(),
    connections: 0,
    requests: 0,
    bytesRead: 0,
    bytesWritten: 0,
    pid: process.pid,
    generation: null,
    config: null,
    workers: null
};
var serverStatusTimeout = null;

// Logging

var log = null;

/**
 * Run the cluster master
 *
 * Options:
 * - exec: the script to exit
 * - workers: worker count (default: host CPU count)
 * - args: arguments passed to workers (default: [])
 * - startupTimeout: how long the master waits for worker startup (default: 10000)
 * - shutdownTimeout: how long the master waits for worker shutdown (default: 30000)
 * - silent: suppress worker output
 * - log: bunyan instance for logging
 * - beforeReload: function that is executed before the master is reloaded
 * - beforeShutdown: unction that is executed before the master is shut down
 *
 * @param {Object} opts
 * @api public
 */
function run(opts) {

    // Config

    config = opts;
    if (!config.exec) throw new Error('Must define an "exec" script');
    config.workers = config.workers || os.cpus().length;
    config.startupTimeout = config.startupTimeout || 10000;
    config.shutdownTimeout = config.shutdownTimeout || 30000;

    if (opts.log) {
        log = opts.log.child({'component': 'master'});
    } else {
        log = bunyan.createLogger({name: 'flora-cluster', 'component': 'master'});
    }

    cluster.setupMaster({
        exec: path.resolve(config.exec),
        args: config.args || [],
        silent: config.silent
    });

    cluster.on('fork', onFork);

    // Setup signals

    process.on('SIGTERM', function onSigTerm() {
        log.info('received SIGTERM, exiting immediately');
        process.exit(0);
    });

    process.on('SIGINT', function onSigInt() {
        log.info('received SIGINT, exiting immediately');
        process.exit(0);
    });

    process.on('SIGQUIT', function onSigQuit() {
        log.info('received SIGQUIT, shutting down gracefully');
        shutdown();
    });

    process.on('SIGHUP', function onSigHup() {
        log.info('received SIGHUP, reloading gracefully');
        reload();
    });

    process.on('uncaughtException', function onUncaughtException(err) {
        log.error(err.stack);
        process.exit(1);
    });

    reload();

    function onFork(worker) {
        worker._floraGeneration = generation;
        worker._floraStatus = 'forked';

        worker.on('message', function onWorkerMessage(message) {
            if (!message.event) return;
            log.trace('message from worker ' + worker.process.pid, message.event);
            worker.emit('flora::' + message.event, message);
        });

        worker.on('online', function onWorkerOnline() {
            if (worker._floraStatus === 'forked') {
                worker._floraStatus = 'initializing';
            }
        });

        worker.on('listening', function onWorkerListening() {
            if (worker._floraStatus === 'initializing') {
                worker._floraStatus = 'running';

                if (worker._floraKillTimeout) {
                    clearTimeout(worker._floraKillTimeout);
                    delete worker._floraKillTimeout;
                }
            }

            readjustCluster();
        });

        worker.on('flora::shutdown', function onWorkerShutdown() {
            worker._floraStatus = 'shutdown';

            readjustCluster();
        });

        worker.on('disconnect', function onWorkerDisconnect() {
            worker._floraStatus = 'disconnected';

            readjustCluster();
        });

        worker.on('exit', function onWorkerExit(code, signal) {
            worker._floraStatus = 'exited';

            aggregateServerStatus(worker.fullStatus || {}, serverStatus);

            if (signal) {
                log.warn('worker %d killed by %s', worker.process.pid, signal);
            } else if (code !== 0) {
                log.warn('worker %d exited with error code %d', worker.process.pid, code);
            } else {
                log.info('worker %d exited successfully', worker.process.pid);
            }

            if (worker._florakillTimeout) {
                clearTimeout(worker._floraKillTimeout);
                delete worker._floraKillTimeout;
            }

            readjustCluster();
        });

        worker.on('flora::serverStatus', function onServerStatus() {
            log.trace('worker %d requested serverStatus', worker.process.pid);
            var askWorkerId, askWorker;

            worker._floraNotifyServerStatus = true;

            if (!serverStatusTimeout) {
                serverStatusTimeout = setTimeout(function onServerStatusTimeout() {
                    serverStatusTimeout = null;
                    notifyServerStatus(true);
                }, 1000);
            }

            for (askWorkerId in cluster.workers) {
                askWorker = cluster.workers[askWorkerId];

                askWorker._floraFullStatusIsUp2date = false;
                askWorker.send({event: 'status'});
            }
        });

        worker.on('flora::status', function onWorkerStatus(message) {
            worker._floraFullStatus = message.status;
            worker._floraFullStatusIsUp2date = true;

            notifyServerStatus();
        });

        setWorkerKillTimeout(worker, 'startup');
    }
}

/**
 * Reload the workers
 *
 * @api public
 */
function reload() {
    inBeforeReload = true;

    if (config.beforeReload && generation > 0) {
        config.beforeReload(function () {
            doReload();
        });
    } else {
        doReload();
    }

    function doReload() {
        inBeforeReload = false;

        generation++;
        currentGenerationForked = false;

        if (generation === 1) {
            serverStatus.status = 'starting';
            log.info('Starting Flora master process at PID ' + process.pid);
        } else {
            serverStatus.status = 'reloading';
            log.info('Gracefully reloading Flora (generation ' + generation + ')');
        }

        readjustCluster();
    }
}

/**
 * Set the cluster config. Merges config values into the existing config
 *
 * @param {Object} opts
 * @api public
 */
function setConfig(opts) {
    for (var name in opts) {
        config[name] = opts[name];
    }

    if (!inBeforeReload) {
        readjustCluster();
    }
}

function readjustCluster() {
    var worker, workerId, i;
    var activeWorkers = 0, runningWorkers = 0;

    // count active and running workers for current generation:
    for (workerId in cluster.workers) {
        worker = cluster.workers[workerId];
        if (worker._floraGeneration !== generation) continue;

        if (worker._floraStatus === 'running' ||
            worker._floraStatus === 'initializing' ||
            worker._floraStatus === 'forked') {
            activeWorkers++;

            if (worker._floraStatus === 'running') {
                runningWorkers++;
            }
        }
    }

    if (serverStatus.status === 'reloading') {
        if (currentGenerationForked && activeWorkers < config.workers) {
            // rollback failing new generation (will be killed automatically afterwards):
            generation--;
            serverStatus.status = 'running';

            log.error('Graceful reload failed - keeping generation ' + generation + ' online');

            readjustCluster();
            return;
        }
    }

    // TODO: do throttling on permanent errors

    if (isRunning) {
        // start missing workers:
        for (i = 0; i < config.workers - activeWorkers; i++) {
            log.info('readjustCluster: forking new worker');
            cluster.fork();
        }

        currentGenerationForked = true;

        // stop workers of current generation if we have too much:
        if (activeWorkers > config.workers) {
            var tooMuchWorkers = activeWorkers - config.workers;

            for (workerId in cluster.workers) {
                if (tooMuchWorkers <= 0) break;

                worker = cluster.workers[workerId];

                if (worker._floraGeneration !== generation) continue;

                shutdownWorker(worker);
                tooMuchWorkers--;
            }
        }
    }

    // if all workers for new generation are up and running, shutdown the old ones -
    // same process if we are shutting down the complete server:
    if (runningWorkers >= config.workers || !isRunning) {
        if (isRunning) {
            serverStatus.status = 'running';
        }

        for (workerId in cluster.workers) {
            worker = cluster.workers[workerId];

            if (worker._floraGeneration === generation && isRunning) continue;

            shutdownWorker(worker);
        }
    }

    notifyServerStatus();
}

function shutdownWorker(worker) {
    if (worker._floraStatus === 'running' ||
        worker._floraStatus === 'initializing' ||
        worker._floraStatus === 'forked') {
        log.info('Shutdown worker ' + worker.process.pid + ' (generation ' + worker._floraGeneration + ')');

        worker._floraStatus = 'shutdownRequested';
        worker.send({event: 'shutdown'});

        setWorkerKillTimeout(worker, 'shutdown');
    }
}

function setWorkerKillTimeout(worker, type) {
    if (worker._floraKillTimeout) {
        clearTimeout(worker._floraKillTimeout);
    }

    // we add one second to the timeouts in master, to give the worker the chance
    // to handle the same timeouts itself before we kill him

    worker._floraKillTimeout = setTimeout(function onWorkerKillTimeout() {
        log.warn('Killing worker ' + worker.process.pid + ' after ' + type + ' timeout by master');
        worker.kill();
    }, config[type + 'Timeout'] + 1000);
}

function notifyServerStatus(timeoutReached) {
    var workerStatusIsUp2date = true;
    var workerId, worker, workerStatus;
    var serverStatusToSend = {}, key;

    if (serverStatusTimeout) {
        clearTimeout(serverStatusTimeout);
        serverStatusTimeout = null;
    }

    for (key in serverStatus) {
        serverStatusToSend[key] = serverStatus[key];
    }

    serverStatusToSend.generation = generation;
    serverStatusToSend.config = config;
    serverStatusToSend.workers = {};

    for (workerId in cluster.workers) {
        worker = cluster.workers[workerId];

        workerStatus = worker._floraFullStatus || {};
        workerStatus.status = worker._floraStatus;
        workerStatus.generation = worker._floraGeneration;

        if (timeoutReached && !worker._floraFullStatusIsUp2date) {
            workerStatus.error = 'Worker did not respond - status outdated';
        }

        aggregateServerStatus(workerStatus, serverStatusToSend);

        serverStatusToSend.workers[workerId] = workerStatus;

        if (!worker._floraFullStatusIsUp2date) {
            workerStatusIsUp2date = false;
        }
    }

    if (workerStatusIsUp2date || timeoutReached) {
        for (workerId in cluster.workers) {
            worker = cluster.workers[workerId];

            if (!worker._floraNotifyServerStatus) continue;

            worker.send({
                event: 'serverStatus',
                serverStatus: serverStatusToSend
            });

            delete worker._floraNotifyServerStatus;
        }
    }
}

function aggregateServerStatus(workerStatus, status) {
    status.connections += workerStatus.connections || 0;
    status.requests += workerStatus.requests || 0;
    status.bytesRead += workerStatus.bytesRead || 0;
    status.bytesWritten += workerStatus.bytesWritten || 0;
}

/**
 * Shutdown the cluster
 *
 * @api public
 */
function shutdown() {
    if (config.beforeShutdown) {
        config.beforeShutdown(function () {
            doShutdown();
        });
    } else {
        doShutdown();
    }

    function doShutdown() {
        isRunning = false;
        serverStatus.status = 'shutdown';

        readjustCluster();
    }
}
<|endoftext|>


<commit_before>'use strict';

var path  = require('path');
var fs    = require('fs');

// leaves only extension from the given string
//   normalize('foo/bar.js')  // -> '.js'
//   normalize('bar.js')      // -> '.js'
//   normalize('.js')         // -> '.js'
//   normalize('js')          // -> '.js'
function normalize(extension) {
  extension = 'prefix.' + path.basename(extension);
  return path.extname(extension).toLowerCase();
}

// merges two or more objects
function merge() {
  var args = Array.prototype.slice.call(arguments), dst, src;

  // copy descriptor from src to dst
  function copyDescriptor(name) {
    var descriptor = Object.getOwnPropertyDescriptor(src, name);
    Object.defineProperty(dst, name, descriptor);
  }

  dst = args.shift() || {};

  while (args.length) {
    src = args.shift();
    if (src) {
      Object.getOwnPropertyNames(src).forEach(copyDescriptor);
    }
  }

  return dst;
}

/**
 *  new Mimoza([options])
 *
 *  Initiates new instance of Mimoza.
 *
 *  ##### Options
 *
 *  - **defaultType** _(String):_ Default mime type used as last-resort
 *    for [[Mimoza#getMimeType]]. By default: `undefined`.
 *
 **/
var Mimoza = module.exports = function Mimoza(options) {
  options = merge(
    {},
    ('object' === typeof options) ? options : {defaultType: options}
  );

  /*[nodoc]* internal
   *  Mimoza#types -> Object
   *
   *  Map of `extension -> mimeType` pairs.
   **/
  Object.defineProperty(this, 'types',        {value: Object.create(null)});

  /*[nodoc]* internal
   *  Mimoza#extensions -> Object
   *
   *  Map of `mimeType -> extensions` pairs.
   **/
  Object.defineProperty(this, 'extensions',   {value: Object.create(null)});

  /*[nodoc]* internal
   *  Mimoza#defaultType -> String
   *
   *  Used as last-resort for [[Mimoza#getMimeType]].
   **/
  Object.defineProperty(this, 'defaultType',  {value: options.defaultType});
};

/**
 *  Mimoza#define(map) -> Void
 *
 *  Batch version of [[Mimoza#register]].
 *
 *  ##### Example
 *
 *      mime.define({
 *        'audio/ogg':  ['oga', 'ogg', 'spx'],
 *        'audio/webm': ['weba']
 *      });
 *
 *      // equals to:
 *
 *      mime.register('audio/ogg', ['oga', 'ogg', 'spx']);
 *      mime.register('audio/webm', ['weba']);
 **/
Mimoza.prototype.define = function define(map) {
  Object.getOwnPropertyNames(map).forEach(function (type) {
    this.register(type, map[type]);
  }, this);
};

/**
 *  Mimoza#register(mimeType, extensions[, overrideDefault = false]) -> Void
 *  - mimeType (String):
 *  - extensions (String|Array):
 *  - overrideDefault (Boolean):
 *
 *  Register given `extensions` as representatives of `mimeType` and register
 *  first element of `extensions` as default extension for the `mimeType`.
 *
 *
 *  ##### Example
 *
 *      mime.register('audio/ogg', ['oga', 'ogg', 'spx']);
 *
 *      mime.getMimeType('.oga');       // -> 'audio/ogg'
 *      mime.getMimeType('.ogg');       // -> 'audio/ogg'
 *      mime.getExtension('audio/ogg'); // -> '.oga'
 *
 *
 *  ##### Overriding default extension
 *
 *  `mimeType -> extension` is set only once, if you wnt to override it,
 *  pass `overrideDefault` flag as true. See example below:
 *
 *      mime.register('audio/ogg', ['oga']);
 *      mime.getExtension('audio/ogg');
 *      // -> '.oga'
 *
 *      mime.register('audio/ogg', ['spx']);
 *      mime.getExtension('audio/ogg');
 *      // -> '.oga'
 *
 *      mime.register('audio/ogg', ['ogg'], true);
 *      mime.getExtension('audio/ogg');
 *      // -> '.ogg'
 **/
Mimoza.prototype.register = function register(mimeType, extensions, overrideDefault) {
  extensions = Array.isArray(extensions) ? extensions : [extensions];

  if (!mimeType || !extensions || 0 === extensions.length) {
    return;
  }

  // pollute `extension -> mimeType` map
  extensions.forEach(function (ext) {
    this.types[normalize(ext)] = mimeType;
  }, this);

  // use case insensitive mime types for extention resolve
  if (overrideDefault || undefined === this.extensions[mimeType.toLowerCase()]) {
    this.extensions[mimeType.toLowerCase()] = normalize(extensions[0]);
  }
};

/**
 *  Mimoza#loadFile(file) -> Void
 *
 *  Load an Apache2-style ".types" file
 *
 *  This may be called multiple times (it's expected).
 *  Where files declare overlapping types/extensions, the last one wins.
 **/
Mimoza.prototype.loadFile = function loadFile(file) {
  // Read file and split into lines
  var content = fs.readFileSync(file, 'ascii');

  content.split(/[\r\n]+/).forEach(function (line) {
    // TODO: fix this buggy RegExp as it produce lots of "empty" data
    // Clean up whitespace/comments, and split into fields
    var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/);
    this.register(fields.shift(), fields);
  }, this);
};

/**
 *  Mimoza#getMimeType(path[, fallback]) -> String
 *
 *  Lookup a mime type based on extension
 **/
Mimoza.prototype.getMimeType = function getMimeType(path, fallback) {
  return this.types[normalize(path)] || fallback || this.defaultType;
};

/**
 *  Mimoza#getExtension(mimeType) -> String
 *
 *  Return file extension associated with a mime type.
 **/
Mimoza.prototype.getExtension = function getExtension(mimeType) {
  // Remove charset/types/spaces, convenent for external data check
  // " tExt/htMl ; charset=UTF-8 ; type=foo " -> "text/html"
  var clearMime = mimeType.split(';')[0].trim().toLowerCase();

  return this.extensions[clearMime];
};

// builtin instance of mimoza
var builtin = new Mimoza();

// Load local copy of
// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
builtin.loadFile(path.join(__dirname, '../types/mime.types'));

// Load additional types from node.js community
builtin.loadFile(path.join(__dirname, '../types/node.types'));

/**
 *  Mimoza.getMimeType(path, fallback) -> String
 *
 *  Proxy to [[Mimoza#getMimeType]] of internal, built-in instance of [[Mimoza]]
 *  filled with some default types.
 **/
Mimoza.getMimeType = function getMimeType(path, fallback) {
  return builtin.getMimeType(path, fallback);
};

/**
 *  Mimoza.getExtension(mimeType) -> String
 *
 *  Proxy to [[Mimoza#getExtension]] of internal, built-in instance of [[Mimoza]]
 *  filled with some default types.
 **/
Mimoza.getExtension = function getExtension(mimeType) {
  return builtin.getExtension(mimeType);
};
<commit_msg>Preload mime files<commit_after>'use strict';

var path  = require('path');
var fs    = require('fs');

// cache mime files
var mimeTypes = fs.readFileSync(path.join(__dirname, '../types/mime.types'), 'ascii').split(/[\r\n]+/);
var nodeTypes = fs.readFileSync(path.join(__dirname, '../types/node.types'), 'ascii').split(/[\r\n]+/);

// leaves only extension from the given string
//   normalize('foo/bar.js')  // -> '.js'
//   normalize('bar.js')      // -> '.js'
//   normalize('.js')         // -> '.js'
//   normalize('js')          // -> '.js'
function normalize(extension) {
  extension = 'prefix.' + path.basename(extension);
  return path.extname(extension).toLowerCase();
}

// merges two or more objects
function merge() {
  var args = Array.prototype.slice.call(arguments), dst, src;

  // copy descriptor from src to dst
  function copyDescriptor(name) {
    var descriptor = Object.getOwnPropertyDescriptor(src, name);
    Object.defineProperty(dst, name, descriptor);
  }

  dst = args.shift() || {};

  while (args.length) {
    src = args.shift();
    if (src) {
      Object.getOwnPropertyNames(src).forEach(copyDescriptor);
    }
  }

  return dst;
}

/**
 *  new Mimoza([options])
 *
 *  Initiates new instance of Mimoza.
 *
 *  ##### Options
 *
 *  - **defaultType** _(String):_ Default mime type used as last-resort
 *    for [[Mimoza#getMimeType]]. By default: `undefined`.
 *
 **/
var Mimoza = module.exports = function Mimoza(options) {
  options = merge(
    {},
    ('object' === typeof options) ? options : {defaultType: options}
  );

  /*[nodoc]* internal
   *  Mimoza#types -> Object
   *
   *  Map of `extension -> mimeType` pairs.
   **/
  Object.defineProperty(this, 'types',        {value: Object.create(null)});

  /*[nodoc]* internal
   *  Mimoza#extensions -> Object
   *
   *  Map of `mimeType -> extensions` pairs.
   **/
  Object.defineProperty(this, 'extensions',   {value: Object.create(null)});

  /*[nodoc]* internal
   *  Mimoza#defaultType -> String
   *
   *  Used as last-resort for [[Mimoza#getMimeType]].
   **/
  Object.defineProperty(this, 'defaultType',  {value: options.defaultType});
};

/**
 *  Mimoza#define(map) -> Void
 *
 *  Batch version of [[Mimoza#register]].
 *
 *  ##### Example
 *
 *      mime.define({
 *        'audio/ogg':  ['oga', 'ogg', 'spx'],
 *        'audio/webm': ['weba']
 *      });
 *
 *      // equals to:
 *
 *      mime.register('audio/ogg', ['oga', 'ogg', 'spx']);
 *      mime.register('audio/webm', ['weba']);
 **/
Mimoza.prototype.define = function define(map) {
  Object.getOwnPropertyNames(map).forEach(function (type) {
    this.register(type, map[type]);
  }, this);
};

/**
 *  Mimoza#register(mimeType, extensions[, overrideDefault = false]) -> Void
 *  - mimeType (String):
 *  - extensions (String|Array):
 *  - overrideDefault (Boolean):
 *
 *  Register given `extensions` as representatives of `mimeType` and register
 *  first element of `extensions` as default extension for the `mimeType`.
 *
 *
 *  ##### Example
 *
 *      mime.register('audio/ogg', ['oga', 'ogg', 'spx']);
 *
 *      mime.getMimeType('.oga');       // -> 'audio/ogg'
 *      mime.getMimeType('.ogg');       // -> 'audio/ogg'
 *      mime.getExtension('audio/ogg'); // -> '.oga'
 *
 *
 *  ##### Overriding default extension
 *
 *  `mimeType -> extension` is set only once, if you wnt to override it,
 *  pass `overrideDefault` flag as true. See example below:
 *
 *      mime.register('audio/ogg', ['oga']);
 *      mime.getExtension('audio/ogg');
 *      // -> '.oga'
 *
 *      mime.register('audio/ogg', ['spx']);
 *      mime.getExtension('audio/ogg');
 *      // -> '.oga'
 *
 *      mime.register('audio/ogg', ['ogg'], true);
 *      mime.getExtension('audio/ogg');
 *      // -> '.ogg'
 **/
Mimoza.prototype.register = function register(mimeType, extensions, overrideDefault) {
  extensions = Array.isArray(extensions) ? extensions : [extensions];

  if (!mimeType || !extensions || 0 === extensions.length) {
    return;
  }

  // pollute `extension -> mimeType` map
  extensions.forEach(function (ext) {
    this.types[normalize(ext)] = mimeType;
  }, this);

  // use case insensitive mime types for extention resolve
  if (overrideDefault || undefined === this.extensions[mimeType.toLowerCase()]) {
    this.extensions[mimeType.toLowerCase()] = normalize(extensions[0]);
  }
};

/**
 *  Mimoza#loadFile(file) -> Void
 *
 *  Load an Apache2-style ".types" file, or array of strings from preloaded file
 *
 *  This may be called multiple times (it's expected).
 *  Where files declare overlapping types/extensions, the last one wins.
 **/
Mimoza.prototype.loadFile = function loadFile(file) {
  // Read file and split into lines
  var content;

  if (Array.isArray(file)) {
    content = file;
  } else {
    content = fs.readFileSync(file, 'ascii').split(/[\r\n]+/);
  }

  content.forEach(function (line) {
    // TODO: fix this buggy RegExp as it produce lots of "empty" data
    // Clean up whitespace/comments, and split into fields
    var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/);
    this.register(fields.shift(), fields);
  }, this);
};

/**
 *  Mimoza#getMimeType(path[, fallback]) -> String
 *
 *  Lookup a mime type based on extension
 **/
Mimoza.prototype.getMimeType = function getMimeType(path, fallback) {
  return this.types[normalize(path)] || fallback || this.defaultType;
};

/**
 *  Mimoza#getExtension(mimeType) -> String
 *
 *  Return file extension associated with a mime type.
 **/
Mimoza.prototype.getExtension = function getExtension(mimeType) {
  // Remove charset/types/spaces, convenent for external data check
  // " tExt/htMl ; charset=UTF-8 ; type=foo " -> "text/html"
  var clearMime = mimeType.split(';')[0].trim().toLowerCase();

  return this.extensions[clearMime];
};

// builtin instance of mimoza
var builtin = new Mimoza();

// Load local copy of
// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
builtin.loadFile(mimeTypes);

// Load additional types from node.js community
builtin.loadFile(nodeTypes);

/**
 *  Mimoza.getMimeType(path, fallback) -> String
 *
 *  Proxy to [[Mimoza#getMimeType]] of internal, built-in instance of [[Mimoza]]
 *  filled with some default types.
 **/
Mimoza.getMimeType = function getMimeType(path, fallback) {
  return builtin.getMimeType(path, fallback);
};

/**
 *  Mimoza.getExtension(mimeType) -> String
 *
 *  Proxy to [[Mimoza#getExtension]] of internal, built-in instance of [[Mimoza]]
 *  filled with some default types.
 **/
Mimoza.getExtension = function getExtension(mimeType) {
  return builtin.getExtension(mimeType);
};
<|endoftext|>


<commit_before>'use strict'

import path from 'path'
import fs from 'fs'
import {uniq} from 'lodash'
import hash from 'hash-sum'

class Module {
  constructor (nuxt) {
    this.nuxt = nuxt
    this.options = nuxt.options
  }

  addVendor (vendor) {
    if (!vendor) {
      return
    }
    this.options.build.vendor = uniq(this.options.build.vendor.concat(vendor))
  }

  addTemplate (template) {
    if (!template) {
      return
    }
    // Validate & parse source
    const src = template.src || template
    const srcPath = path.parse(src)
    if (!src || typeof src !== 'string' || !fs.existsSync(src)) {
      // eslint-disable-next-line no-console
      console.warn('[Nuxt] invalid template', template)
      return
    }
    // Generate unique and human readable dst filename
    const dst = template.dst ||
      ((template.dstName || (path.basename(srcPath.dir) + '.' + srcPath.name)) + '.' + hash(src) + (template.dstExt || srcPath.ext))

    // Add to templates list
    const templateObj = {
      src,
      dst,
      options: template.options
    }
    this.options.build.templates.push(templateObj)
    return templateObj
  }

  addPlugin (template) {
    const {dst} = this.addTemplate(template)
    // Add to nuxt plugins
    this.options.plugins.push({
      src: '~/.nuxt/' + dst,
      ssr: Boolean(template.ssr)
    })
  }

  extendBuild (extendFn) {
    if (!(extendFn instanceof Function)) {
      return
    }
    // Add extendFn to chain
    const _extend = this.options.build.extend
    this.options.build.extend = function () {
      extendFn.apply(this, arguments)
      if (_extend) {
        _extend.apply(this, arguments)
      }
    }
  }

  installModule (moduleOpts) {
    if (!moduleOpts) {
      return
    }
    // Allows passing runtime options to each module
    const options = moduleOpts.options || {}
    let src = moduleOpts.src || moduleOpts
    // Resolve module
    let module
    try {
      if (typeof src === 'string') {
        // Using ~ shorthand modules are resolved from project srcDir
        if (src.indexOf('~') === 0) {
          src = path.resolve(this.options.srcDir, src.substr(1))
        }
        // eslint-disable-next-line no-eval
        module = eval('require')(src)
      }
    } catch (e) {
      // eslint-disable-next-line no-console
      console.error('[Nuxt] Unable to resolve module', src)
      // eslint-disable-next-line no-console
      console.error(e)
      return
    }
    // Validate module
    if (!(module instanceof Function)) {
      // eslint-disable-next-line no-console
      console.error('[Nuxt] Module should be a function', module)
    }
    // Call module with `this` context and pass options
    return new Promise((resolve, reject) => {
      return module.call(this, options, err => {
        if (err) {
          return reject(err)
        }
        resolve(module)
      })
    })
  }
}

export default Module
<commit_msg>[modules] Support promise<commit_after>'use strict'

import path from 'path'
import fs from 'fs'
import {uniq} from 'lodash'
import hash from 'hash-sum'

class Module {
  constructor (nuxt) {
    this.nuxt = nuxt
    this.options = nuxt.options
  }

  addVendor (vendor) {
    if (!vendor) {
      return
    }
    this.options.build.vendor = uniq(this.options.build.vendor.concat(vendor))
  }

  addTemplate (template) {
    if (!template) {
      return
    }
    // Validate & parse source
    const src = template.src || template
    const srcPath = path.parse(src)
    if (!src || typeof src !== 'string' || !fs.existsSync(src)) {
      // eslint-disable-next-line no-console
      console.warn('[Nuxt] invalid template', template)
      return
    }
    // Generate unique and human readable dst filename
    const dst = template.dst ||
      ((template.dstName || (path.basename(srcPath.dir) + '.' + srcPath.name)) + '.' + hash(src) + (template.dstExt || srcPath.ext))

    // Add to templates list
    const templateObj = {
      src,
      dst,
      options: template.options
    }
    this.options.build.templates.push(templateObj)
    return templateObj
  }

  addPlugin (template) {
    const {dst} = this.addTemplate(template)
    // Add to nuxt plugins
    this.options.plugins.push({
      src: '~/.nuxt/' + dst,
      ssr: Boolean(template.ssr)
    })
  }

  extendBuild (extendFn) {
    if (!(extendFn instanceof Function)) {
      return
    }
    // Add extendFn to chain
    const _extend = this.options.build.extend
    this.options.build.extend = function () {
      extendFn.apply(this, arguments)
      if (_extend) {
        _extend.apply(this, arguments)
      }
    }
  }

  installModule (moduleOpts) {
    if (!moduleOpts) {
      return
    }
    // Allows passing runtime options to each module
    const options = moduleOpts.options || {}
    let src = moduleOpts.src || moduleOpts
    // Resolve module
    let module
    try {
      if (typeof src === 'string') {
        // Using ~ shorthand modules are resolved from project srcDir
        if (src.indexOf('~') === 0) {
          src = path.resolve(this.options.srcDir, src.substr(1))
        }
        // eslint-disable-next-line no-eval
        module = eval('require')(src)
      }
    } catch (e) {
      // eslint-disable-next-line no-console
      console.error('[Nuxt] Unable to resolve module', src)
      // eslint-disable-next-line no-console
      console.error(e)
      return
    }
    // Validate module
    if (!(module instanceof Function)) {
      // eslint-disable-next-line no-console
      console.error('[Nuxt] Module should be a function', module)
    }
    // Call module with `this` context and pass options
    return new Promise((resolve, reject) => {
      const result = module.call(this, options, err => {
        if (err) {
          return reject(err)
        }
        resolve(module)
      })
      if (result && result.then instanceof Function) {
        return result.then(resolve)
      }
    })
  }
}

export default Module
<|endoftext|>


<commit_before>var request = require('request')
var cheerio = require('cheerio')
module.exports = function (id, cb) {
  request('http://www.imdb.com/title/' + id + '/', function (error, response, body) {
    if (!error && response.statusCode == 200) {
      body = body.replace(/(\r\n|\n|\r)/gm, '').replace(/ +(?= )/g, '')
      $ = cheerio.load(body)

      var title = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > h1').text().replace(/\(\d+\)/g, '').trim()
      var year = $('#titleYear > a').text()
      var contentRating = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div > meta').attr('content')
      var runtime = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div > time').text().trim()
      var description = $('#title-overview-widget > div.plot_summary_wrapper > div.plot_summary > div.summary_text').text().trim()
      var rating = $('#title-overview-widget > div.vital > div.title_block > div > div.ratings_wrapper > div.imdbRating > div.ratingValue > strong > span').text()
      var poster = $('#title-overview-widget > div.vital > div.slate_wrapper > div.poster > a > img').attr('src')
      var director = $('#title-overview-widget > div.plot_summary_wrapper > div.plot_summary > div:nth-child(2) > span > a > span').text()
      var metascore = $('#title-overview-widget > div.plot_summary_wrapper > div.titleReviewBar > div:nth-child(1) > a > div > span').text()
      var writer = $('#title-overview-widget > div.plot_summary_wrapper > div.plot_summary > div:nth-child(3) > span:nth-child(2) > a > span').text()
      var languageArray = $('#titleDetails > div:nth-child(5) > a')
      var language = ''
      var cast = [];
      $('div#main_bottom > div#titleCast > table.cast_list  tr > td.itemprop > a > span').each(function () {
        cast.push($(this).text());
      });

      if (languageArray.length === 1) {
        language = $(languageArray[0]).text()
      } else {
        for (var i = 0; i < languageArray.length; i++) {
          if (i < languageArray.length - 1) {
            language += $(languageArray[i]).text() + ', '
          } else {
            language += $(languageArray[i]).text()
          }
        }
      }

      if ($('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div').text().split('|')[2] != null) {
        var genre = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div').text().split('|')[2].split(',')
      } else {
        var genre = null // I need fix this! If you need this feature for unreleased movies, please create an issue and I'll make it my priority
      }

      if (description === '') {
        description = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.plot_summary_wrapper > div.plot_summary.minPlotHeightWithPoster > div.summary_text').text().trim()
      }

      if (poster === null) {
        poster = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.poster > a > img').attr('src')
      }

      if (director === '') {
        director = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.plot_summary_wrapper > div.plot_summary.minPlotHeightWithPoster > div:nth-child(2) > span > a > span').text()
      }

      if (writer === '') {
        writer = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.plot_summary_wrapper > div.plot_summary.minPlotHeightWithPoster > div:nth-child(3) > span:nth-child(2) > a > span').text()
      }

      cb(null, {
        title: title || 'N/A',
        year: year || 'N/A',
        contentRating: contentRating || 'N/A',
        runtime: runtime || 'N/A',
        description: description,
        rating: rating || 'N/A',
        poster: poster || 'N/A',
        genre: genre || ['N/A'],
        director: director || 'N/A',
        metascore: metascore || 'N/A',
        writer: writer || 'N/A',
        language: language || 'N/A',
        actors: cast
      })
    } else {
      cb(new Error('IMDB Failed to respond, or responded with error code'), null)
    }
  })
}
<commit_msg>Add original title to movie object (#10)<commit_after>var request = require('request')
var cheerio = require('cheerio')
module.exports = function (id, cb) {
  request('http://www.imdb.com/title/' + id + '/', function (error, response, body) {
    if (!error && response.statusCode == 200) {
      body = body.replace(/(\r\n|\n|\r)/gm, '').replace(/ +(?= )/g, '')
      $ = cheerio.load(body)

      var title = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > h1').text().replace(/\(\d+\)/g, '').trim()
      var originalTitle = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div.originalTitle').text().replace(/\(\d+\)/g, '').trim()
      originalTitle = originalTitle.replace(/\(original title\)/g, '').trim();
      var year = $('#titleYear > a').text()
      var contentRating = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div > meta').attr('content')
      var runtime = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div > time').text().trim()
      var description = $('#title-overview-widget > div.plot_summary_wrapper > div.plot_summary > div.summary_text').text().trim()
      var rating = $('#title-overview-widget > div.vital > div.title_block > div > div.ratings_wrapper > div.imdbRating > div.ratingValue > strong > span').text()
      var poster = $('#title-overview-widget > div.vital > div.slate_wrapper > div.poster > a > img').attr('src')
      var director = $('#title-overview-widget > div.plot_summary_wrapper > div.plot_summary > div:nth-child(2) > span > a > span').text()
      var metascore = $('#title-overview-widget > div.plot_summary_wrapper > div.titleReviewBar > div:nth-child(1) > a > div > span').text()
      var writer = $('#title-overview-widget > div.plot_summary_wrapper > div.plot_summary > div:nth-child(3) > span:nth-child(2) > a > span').text()
      var languageArray = $('#titleDetails > div:nth-child(5) > a')
      var language = ''
      var cast = [];
      $('div#main_bottom > div#titleCast > table.cast_list  tr > td.itemprop > a > span').each(function () {
        cast.push($(this).text());
      });

      if (languageArray.length === 1) {
        language = $(languageArray[0]).text()
      } else {
        for (var i = 0; i < languageArray.length; i++) {
          if (i < languageArray.length - 1) {
            language += $(languageArray[i]).text() + ', '
          } else {
            language += $(languageArray[i]).text()
          }
        }
      }

      if ($('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div').text().split('|')[2] != null) {
        var genre = $('#title-overview-widget > div.vital > div.title_block > div > div.titleBar > div.title_wrapper > div').text().split('|')[2].split(',')
      } else {
        var genre = null // I need fix this! If you need this feature for unreleased movies, please create an issue and I'll make it my priority
      }

      if (description === '') {
        description = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.plot_summary_wrapper > div.plot_summary.minPlotHeightWithPoster > div.summary_text').text().trim()
      }

      if (poster === null) {
        poster = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.poster > a > img').attr('src')
      }

      if (director === '') {
        director = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.plot_summary_wrapper > div.plot_summary.minPlotHeightWithPoster > div:nth-child(2) > span > a > span').text()
      }

      if (writer === '') {
        writer = $('#title-overview-widget > div.minPosterWithPlotSummaryHeight > div.plot_summary_wrapper > div.plot_summary.minPlotHeightWithPoster > div:nth-child(3) > span:nth-child(2) > a > span').text()
      }

      cb(null, {
        title: title || 'N/A',
        originalTitle: originalTitle || 'N/A',
        year: year || 'N/A',
        contentRating: contentRating || 'N/A',
        runtime: runtime || 'N/A',
        description: description,
        rating: rating || 'N/A',
        poster: poster || 'N/A',
        genre: genre || ['N/A'],
        director: director || 'N/A',
        metascore: metascore || 'N/A',
        writer: writer || 'N/A',
        language: language || 'N/A',
        actors: cast
      })
    } else {
      cb(new Error('IMDB Failed to respond, or responded with error code'), null)
    }
  })
}
<|endoftext|>


<commit_before>var config   = require(__dirname + '/../config.js');
var http     = require('http');

var db       = require(__dirname + '/../database/database.js');
var Movie    = require(__dirname + '/../database/movieDb.js');
var utils    = require(__dirname + '/utils.js');

var tmdb     = require('moviedb')(config.theMovieDbApiKey);

/*
 * Cron Function to update RSS-Feed-Entries into our Database
 */
var updateFeeds = function(callback) {

    utils.log('updating Movie-Feeds...');
    rssGet(function(rssItems) {
        utils.log('retrieved ' + rssItems.length + ' items from feeds');
        var movies = createMovieSetFromRss(rssItems);
        for(var i in movies) {
            db.movie.exists(movies[i], function(exists, movieEntry, movieInput) {
                // merge movies if already in database
                if(exists) {
                    var merged = db.movie.mergeMovies(movieEntry, movieInput);
                    movieEntry = merged.movie;
                    if(merged.newlinks) {
                        utils.log('Updating: ' + movieEntry.title);
                        movieEntry.lastUpdate = new Date(movieEntry.sites[movieEntry.sites.length - 1].pubdate);
                        movieEntry.save(function(e){callback(e)});
                    } else utils.log('No new links found for ' + movieEntry.title);
                }
                // add new movie
                else {
                    utils.log('Adding: ' + movieEntry.title);
                    movieEntry.lastUpdate = new Date(movieEntry.sites[movieEntry.sites.length - 1].pubdate);
                    movieEntry.save(function(e){callback(e)});
                }
            });
        }
    });
};
exports.updateFeeds = updateFeeds;

/*
 * picks one movie from db which has no Info
 * and adds gets info which is later added back to the db
 */
var cronUpdateInfo = function() {
    db.movie.findNew(function(err, movie) {
        if (movie !== null) {
            utils.log('Movie to udpate: ' + movie);
            catchAllInfos(movie, function() {});
        }
    });
};
exports.cronUpdateInfo = cronUpdateInfo;

var updateInfoById = function(_id, callback) {
    Movie.model.findById(_id, function(err, movie){
        movie.info.year = Math.max.apply(Math, movie.search.years);
        catchAllInfos(movie, function(){
            callback(true);
        });
    });
};
exports.updateInfoById = updateInfoById;

/**
 * Takes the data from the RSS Feeds and creates
 * database-models out of it
 */
var createMovieSetFromRss = function(items) {
    var movies = {};
    for (var i in items) {
        var title = getFilmTitleFromString(items[i].title);
        title = title.toLowerCase();
        var year  = yearFromString(items[i].title);
        // TODO: If there are two movies with same titles
        // but different year, this puts them into same
        // movie frame; this can be fixed
        if (movies[title] === undefined) {
            movies[title] = new Movie.model({
                title: title,
                search: {
                    years: [year],
                    titles: [title]
                },
                info: {
                    year: year,
                    lastUpdated: undefined
                }
            });
        }
        movies[title].sites.push(items[i]);
        // get resolution of download-page
        var res = getResFromString(items[i].title);
        // add quality to list, if not existent
        if (res && movies[title].resolutions.indexOf(res) === -1)
            movies[title].resolutions.push(res);
    }
    return movies;
};
exports.createMovieSetFromRss = createMovieSetFromRss;

/*
 * Reads Info for one movie from tomatoes or TheMovieDb and saves it to the database
 *
 * TODO: get additional info from omdbapi via http://www.omdbapi.com/?i=9874923432
 */
var catchAllInfos = function(movie, callback) {
    utils.log('fetching info for: ' + movie.title);
    // Retrieve Data from TheMovieDb
    tmdb.searchMovie({
        query: movie.title
    }, function(err, res) {
        var correctMovie = null;
        movie.info.lastUpdated = new Date();
        utils.log(res);
        if(res.results.length === 0) {
            movie.save(function(err, product, numberAffected) {
                if(err) callback(err);
                else callback(true);
            });
        } else {
            var success = false;
            for(var i in res.results) {
                if(parseInt(String(res.results[i].release_date).substring(0, 4), 10) === movie.info.year) {
                    success = true;
                    correctMovie = res.results[i];
                    movie.info.ids.tmdb = correctMovie.id;
                    tmdb.movieInfo({
                        id: correctMovie.id
                    }, function(err, res) {
                        utils.log('-'); utils.log(res);
                        movie.info.runtime = (res.runtime!==null?res.runtime:0);
                        movie.info.synopsis = res.overview;
                        movie.info.ids.imdb = res.imdb_id;
                        movie.title = res.original_title;
                        movie.info.posters.thumbnail = 'http://image.tmdb.org/t/p/w500' + res.poster_path;
                        utils.log('going to save the following entry...');
                        utils.log(movie);
                        movie.save(function(err, product, numberAffected) {
                            if(err) callback(err);
                            else callback(true);
                        });
                    });
                    break;
                }
            }
            if(!success) { // if no date matches, then take first result
                correctMovie = res.results[0];
                movie.info.ids.tmdb = correctMovie.id;
                tmdb.movieInfo({
                    id: correctMovie.id
                }, function(err, res) {
                    movie.info.runtime = res.runtime;
                    movie.info.synopsis = res.overview;
                    movie.info.ids.imdb = res.imdb_id;
                    movie.title = res.original_title;
                    movie.info.posters.thumbnail = 'http://image.tmdb.org/t/p/w500' + res.poster_path;
                    utils.log('going to save the following entry...');
                    utils.log(movie);
                    movie.save(function(err, product, numberAffected) {
                        if(err) callback(err);
                        else callback(true);
                    });
                });
            }
        }
    });
};
exports.catchAllInfos = catchAllInfos;

/*
 * Finds a Year in a String
 *
 * Year must be preceded by space or period
 * Year must be followed by space or period
 * Finds years from 1900-2099
 * exp: "Fathers.Day.2011.DC.BDRiP"
 * exp: "Fathers Day 2011 DC BDRiP"
 */
var yearFromString = function(title) {
    title = title.replace(/\./g, " "); // for "Fathers.Day.2011.DC.BDRiP" strings
    var year = title.match(/(.+)\s((19|20)\d{2})\s?/i);
    if(year !== null && year[2] !== undefined) {
        return parseInt(year[2], 10);
    }
    return 1900;
};
exports.yearFromString = yearFromString;

/**
 * Extracts Film Title from Movie-Filename-String
 *
 * Takes everything from the Start of the string to the first year-string
 * If there's no year, then the whole string is returned with stripped periods
 *
 * @param string string A string containing a Movie-Title. Must have a year
 * @returns {String} The Title of the Movie; If no year, then returns string
 */
var getFilmTitleFromString = function(string) {
    string = string.replace(/\./g, " "); // for "Fathers.Day.2011.DC.BDRiP" strings
    string = string.replace(/\’/g, "");
    var title = string.match(/(.+)\s(19|20)\d{2}\s?/i); // everything til the first 4-digit year followed by space or nothing
    if(title !== null) {
        return title[1];
    }
    return string;
};
exports.getFilmTitleFromString = getFilmTitleFromString;

/*
 * Extracts Resolution/Format from given String
 *
 * only returns first occurence
 *
 * TODO: Cam, TS with .CAM. and .TS.
 */
var getResFromString = function(string) {
    if(typeof string !== 'string') return false;
    var formats = utils.resolutions;
    var regex = new RegExp(formats.join('|'), 'i');
    var results = regex.exec(string);
    return(results === null) ? false : results[0];
};
exports.getResFromString = getResFromString;

/*
 * retrieves RSS content from all RSS-Movie-Feeds
 *
 */
var rssGet = function(callback) {
    utils.log('start reading feeds...');
    var Scrapper = require(__dirname + '/scrapper.js');
    var items = [];
    var feedCount = config.feeds.Movies.feeds.length;
    var feedcounter = 0;
    var processItems = function(err, rss) {
        if (err === null) {
            var i, title, regex;
            for (i in rss.items) {
                for (title in config.feeds.Movies.queries) {
                    regex = new RegExp(config.feeds.Movies.queries[title].title, 'g');
                    if (regex.exec(rss.items[i].title)) {
                        items.push(rss.items[i]);
                    }
                }
            }
        }
        feedcounter++;
        if (feedcounter === feedCount) {
            callback(items);
        }
    };
    utils.log('reading from total of: ' + feedCount + ' feeds');
    for (var i = 0; i < feedCount; i++) {
        utils.log(config.feeds.Movies.feeds[i]);
        Scrapper.scrap(config.feeds.Movies.feeds[i], function(err, rss) {
            utils.log(rss);
            utils.log(err);
            processItems(err, rss);
        });
    }
};

/*
 * Clean-up/remove old entries after n-days
 */
exports.cleanUp = function(n, callback) {
    utils.log('-');
    var expire = new Date(new Date().setDate(new Date().getDate()-n)).toISOString();
    utils.log('removing all entries that were last updated before ' + n + ' days...');
   utils.log(expire);
    Movie.model.remove({lastUpdate : {$lt : expire}}, function(){
        utils.log('removed!');
        utils.log('-');
    });
    callback();
};

// moviedb return for movieInfo
/*

{ adult: false,
  backdrop_path: '/4IamAvByBWnclmbwlnZB8otMyB7.jpg',
  belongs_to_collection: null,
  budget: 0,
  genres: [ { id: 35, name: 'Comedy' }, { id: 18, name: 'Drama' } ],
  homepage: '',
  id: 243684,
  imdb_id: 'tt3179568',
  original_language: 'en',
  original_title: 'Men, Women & Children',
  overview: 'Follows the story of a group of high school teenagers and their parents as they attempt to navigate the many ways the internet has changed their relationships, their communication, their self-image, and their love lives.',
  popularity: 4.07571172700321,
  poster_path: '/5odPwDRx5rYaA4GX85w7cUgYDrX.jpg',
  production_companies:
   [ { name: 'Right of Way Films', id: 32157 },
     { name: 'Paramount Pictures', id: 4 } ],
  production_countries: [ { iso_3166_1: 'US', name: 'United States of America' } ],
  release_date: '2014-10-17',
  revenue: 0,
  runtime: 116,
  spoken_languages: [ { iso_639_1: 'en', name: 'English' } ],
  status: 'Released',
  tagline: 'Discover how little you know about the people you know.',
  title: 'Men, Women & Children',
  video: false,
  vote_average: 6.4,
  vote_count: 30 }

*/

/**
 * deprecated IMDB-Functions
 * IMDB has no API to officialy retrieve Movie-information
 *
 */

/*
exports.getImdbFromTitle = function(title, callback) {
    var imdb = require('imdb-api');
    imdb.getReq({
        name: title
    }, function(err, info) {
        callback(err, info);
    });
};

exports.getImdbFromString = function(string, callback) {
    var imdb = require('imdb-api');
    var title = getFilmTitleFromString(string);
    if (title) {
        imdb.getReq({
            name: title
        }, function(err, info) {
            callback(err, info);
        });
    }
    else {
        callback(false);
    }
};
*/<commit_msg>runtime fix<commit_after>var config   = require(__dirname + '/../config.js');
var http     = require('http');

var db       = require(__dirname + '/../database/database.js');
var Movie    = require(__dirname + '/../database/movieDb.js');
var utils    = require(__dirname + '/utils.js');

var tmdb     = require('moviedb')(config.theMovieDbApiKey);

/*
 * Cron Function to update RSS-Feed-Entries into our Database
 */
var updateFeeds = function(callback) {

    utils.log('updating Movie-Feeds...');
    rssGet(function(rssItems) {
        utils.log('retrieved ' + rssItems.length + ' items from feeds');
        var movies = createMovieSetFromRss(rssItems);
        for(var i in movies) {
            db.movie.exists(movies[i], function(exists, movieEntry, movieInput) {
                // merge movies if already in database
                if(exists) {
                    var merged = db.movie.mergeMovies(movieEntry, movieInput);
                    movieEntry = merged.movie;
                    if(merged.newlinks) {
                        utils.log('Updating: ' + movieEntry.title);
                        movieEntry.lastUpdate = new Date(movieEntry.sites[movieEntry.sites.length - 1].pubdate);
                        movieEntry.save(function(e){callback(e)});
                    } else utils.log('No new links found for ' + movieEntry.title);
                }
                // add new movie
                else {
                    utils.log('Adding: ' + movieEntry.title);
                    movieEntry.lastUpdate = new Date(movieEntry.sites[movieEntry.sites.length - 1].pubdate);
                    movieEntry.save(function(e){callback(e)});
                }
            });
        }
    });
};
exports.updateFeeds = updateFeeds;

/*
 * picks one movie from db which has no Info
 * and adds gets info which is later added back to the db
 */
var cronUpdateInfo = function() {
    db.movie.findNew(function(err, movie) {
        if (movie !== null) {
            utils.log('Movie to udpate: ' + movie);
            catchAllInfos(movie, function() {});
        }
    });
};
exports.cronUpdateInfo = cronUpdateInfo;

var updateInfoById = function(_id, callback) {
    Movie.model.findById(_id, function(err, movie){
        movie.info.year = Math.max.apply(Math, movie.search.years);
        catchAllInfos(movie, function(){
            callback(true);
        });
    });
};
exports.updateInfoById = updateInfoById;

/**
 * Takes the data from the RSS Feeds and creates
 * database-models out of it
 */
var createMovieSetFromRss = function(items) {
    var movies = {};
    for (var i in items) {
        var title = getFilmTitleFromString(items[i].title);
        title = title.toLowerCase();
        var year  = yearFromString(items[i].title);
        // TODO: If there are two movies with same titles
        // but different year, this puts them into same
        // movie frame; this can be fixed
        if (movies[title] === undefined) {
            movies[title] = new Movie.model({
                title: title,
                search: {
                    years: [year],
                    titles: [title]
                },
                info: {
                    year: year,
                    lastUpdated: undefined
                }
            });
        }
        movies[title].sites.push(items[i]);
        // get resolution of download-page
        var res = getResFromString(items[i].title);
        // add quality to list, if not existent
        if (res && movies[title].resolutions.indexOf(res) === -1)
            movies[title].resolutions.push(res);
    }
    return movies;
};
exports.createMovieSetFromRss = createMovieSetFromRss;

/*
 * Reads Info for one movie from tomatoes or TheMovieDb and saves it to the database
 *
 * TODO: get additional info from omdbapi via http://www.omdbapi.com/?i=9874923432
 */
var catchAllInfos = function(movie, callback) {
    utils.log('fetching info for: ' + movie.title);
    // Retrieve Data from TheMovieDb
    tmdb.searchMovie({
        query: movie.title
    }, function(err, res) {
        var correctMovie = null;
        movie.info.lastUpdated = new Date();
        utils.log(res);
        if(res.results.length === 0) {
            movie.save(function(err, product, numberAffected) {
                if(err) callback(err);
                else callback(true);
            });
        } else {
            var success = false;
            for(var i in res.results) {
                if(parseInt(String(res.results[i].release_date).substring(0, 4), 10) === movie.info.year) {
                    success = true;
                    correctMovie = res.results[i];
                    movie.info.ids.tmdb = correctMovie.id;
                    tmdb.movieInfo({
                        id: correctMovie.id
                    }, function(err, res) {
                        utils.log('-'); utils.log(res);
                        movie.info.runtime = (res.runtime!==null?res.runtime:0);
                        movie.info.synopsis = res.overview;
                        movie.info.ids.imdb = res.imdb_id;
                        movie.title = res.original_title;
                        movie.info.posters.thumbnail = 'http://image.tmdb.org/t/p/w500' + res.poster_path;
                        utils.log('going to save the following entry...');
                        utils.log(movie);
                        movie.save(function(err, product, numberAffected) {
                            if(err) callback(err);
                            else callback(true);
                        });
                    });
                    break;
                }
            }
            if(!success) { // if no date matches, then take first result
                correctMovie = res.results[0];
                movie.info.ids.tmdb = correctMovie.id;
                tmdb.movieInfo({
                    id: correctMovie.id
                }, function(err, res) {
                    movie.info.runtime = res.runtime;
                    movie.info.synopsis = res.overview;
                    movie.info.ids.imdb = res.imdb_id;
                    movie.title = res.original_title;
                    movie.info.posters.thumbnail = 'http://image.tmdb.org/t/p/w500' + res.poster_path;
                    utils.log('going to save the following entry...');
                    utils.log(movie);
                    movie.save(function(err, product, numberAffected) {
                        if(err) callback(err);
                        else callback(true);
                    });
                });
            }
        }
    });
};
exports.catchAllInfos = catchAllInfos;

/*
 * Finds a Year in a String
 *
 * Year must be preceded by space or period
 * Year must be followed by space or period
 * Finds years from 1900-2099
 * exp: "Fathers.Day.2011.DC.BDRiP"
 * exp: "Fathers Day 2011 DC BDRiP"
 */
var yearFromString = function(title) {
    title = title.replace(/\./g, " "); // for "Fathers.Day.2011.DC.BDRiP" strings
    var year = title.match(/(.+)\s((19|20)\d{2})\s?/i);
    if(year !== null && year[2] !== undefined) {
        return parseInt(year[2], 10);
    }
    return 1900;
};
exports.yearFromString = yearFromString;

/**
 * Extracts Film Title from Movie-Filename-String
 *
 * Takes everything from the Start of the string to the first year-string
 * If there's no year, then the whole string is returned with stripped periods
 *
 * @param string string A string containing a Movie-Title. Must have a year
 * @returns {String} The Title of the Movie; If no year, then returns string
 */
var getFilmTitleFromString = function(string) {
    string = string.replace(/\./g, " "); // for "Fathers.Day.2011.DC.BDRiP" strings
    string = string.replace(/\’/g, "");
    var title = string.match(/(.+)\s(19|20)\d{2}\s?/i); // everything til the first 4-digit year followed by space or nothing
    if(title !== null) {
        return title[1];
    }
    return string;
};
exports.getFilmTitleFromString = getFilmTitleFromString;

/*
 * Extracts Resolution/Format from given String
 *
 * only returns first occurence
 *
 * TODO: Cam, TS with .CAM. and .TS.
 */
var getResFromString = function(string) {
    if(typeof string !== 'string') return false;
    var formats = utils.resolutions;
    var regex = new RegExp(formats.join('|'), 'i');
    var results = regex.exec(string);
    return(results === null) ? false : results[0];
};
exports.getResFromString = getResFromString;

/*
 * retrieves RSS content from all RSS-Movie-Feeds
 *
 */
var rssGet = function(callback) {
    utils.log('start reading feeds...');
    var Scrapper = require(__dirname + '/scrapper.js');
    var items = [];
    var feedCount = config.feeds.Movies.feeds.length;
    var feedcounter = 0;
    var processItems = function(err, rss) {
        if (err === null) {
            var i, title, regex;
            for (i in rss.items) {
                for (title in config.feeds.Movies.queries) {
                    regex = new RegExp(config.feeds.Movies.queries[title].title, 'g');
                    if (regex.exec(rss.items[i].title)) {
                        items.push(rss.items[i]);
                    }
                }
            }
        }
        feedcounter++;
        if (feedcounter === feedCount) {
            callback(items);
        }
    };
    utils.log('reading from total of: ' + feedCount + ' feeds');
    for (var i = 0; i < feedCount; i++) {
        utils.log(config.feeds.Movies.feeds[i]);
        Scrapper.scrap(config.feeds.Movies.feeds[i], function(err, rss) {
            utils.log(rss);
            utils.log(err);
            processItems(err, rss);
        });
    }
};

/*
 * Clean-up/remove old entries after n-days
 */
exports.cleanUp = function(n, callback) {
   utils.log('-');
   var expire = new Date(new Date().setDate(new Date().getDate()-n)).toISOString();
   utils.log('removing all entries that were last updated before ' + n + ' days...');
   utils.log('> removing everything older than ' + expire);
   Movie.model.find({lastUpdate : {$lt : expire}}).remove(function(){
      utils.log('removed!');
      utils.log('-');
   });
   callback();
};

// moviedb return for movieInfo
/*

{ adult: false,
  backdrop_path: '/4IamAvByBWnclmbwlnZB8otMyB7.jpg',
  belongs_to_collection: null,
  budget: 0,
  genres: [ { id: 35, name: 'Comedy' }, { id: 18, name: 'Drama' } ],
  homepage: '',
  id: 243684,
  imdb_id: 'tt3179568',
  original_language: 'en',
  original_title: 'Men, Women & Children',
  overview: 'Follows the story of a group of high school teenagers and their parents as they attempt to navigate the many ways the internet has changed their relationships, their communication, their self-image, and their love lives.',
  popularity: 4.07571172700321,
  poster_path: '/5odPwDRx5rYaA4GX85w7cUgYDrX.jpg',
  production_companies:
   [ { name: 'Right of Way Films', id: 32157 },
     { name: 'Paramount Pictures', id: 4 } ],
  production_countries: [ { iso_3166_1: 'US', name: 'United States of America' } ],
  release_date: '2014-10-17',
  revenue: 0,
  runtime: 116,
  spoken_languages: [ { iso_639_1: 'en', name: 'English' } ],
  status: 'Released',
  tagline: 'Discover how little you know about the people you know.',
  title: 'Men, Women & Children',
  video: false,
  vote_average: 6.4,
  vote_count: 30 }

*/

/**
 * deprecated IMDB-Functions
 * IMDB has no API to officialy retrieve Movie-information
 *
 */

/*
exports.getImdbFromTitle = function(title, callback) {
    var imdb = require('imdb-api');
    imdb.getReq({
        name: title
    }, function(err, info) {
        callback(err, info);
    });
};

exports.getImdbFromString = function(string, callback) {
    var imdb = require('imdb-api');
    var title = getFilmTitleFromString(string);
    if (title) {
        imdb.getReq({
            name: title
        }, function(err, info) {
            callback(err, info);
        });
    }
    else {
        callback(false);
    }
};
*/<|endoftext|>


<commit_before>// NML VM
// Runs NML AST, stored in a MObject
// A VM would exist for each script/verb, and would be reset/replaced with a
// new VM when those scripts/verbs are updated/edited.

var async = require('async');

var OPERATORS = {'+': function(left, right) { return left + right; },
                 '-': function(left, right) { return left - right; },
                 '/': function(left, right) { return left / right; },
                 '*': function(left, right) { return left * right; },
                 '==': function(left, right) { return left === right; },
                 '!=': function(left, right) { return left !== right; },
                 '>=': function(left, right) { return left >= right; },
                 '<=': function(left, right) { return left <= right; },
                 '>': function(left, right) { return left > right; },
                 '<': function(left, right) { return left < right; }};

var TYPE_COMPATIBILITY = {
  '+': {
    'number': ['string', 'number'],
    'string': ['string'],
    'array': ['string'],
    'null': ['string']
  },
  '-': {
    'number': ['number'],
    'string': [],
    'array': [],
    'null': []
  },
  '*': {
    'number': ['number'],
    'string': [],
    'array': [],
    'null': []
  },
  '/': {
    'number': ['number'],
    'string': [],
    'array': [],
    'null': []
  }
}

var VM = function(app, mobj, ast) {
  this._app = app;
  this.mobj = mobj; // MOO Object

  // Initialize the state
  this.state = {
    ast: ast,
    localVars: {},
    ip: [0] // Instruction pointer/path; this is an array as the AST is a tree with many levels
            // Example: [2, 4, 10] for the second object, then fourth, then tenth object
  }
}

VM.findIns = function(ip, ast) {
  // Find the instruction specified by instruction path IP
  if(ip.length <= 1) {
    return ast[ip[0]];
  } else {
    if(typeof(ast[ip[0]]) === 'object') {
      return VM.findIns(ip.slice(1, ip.length), ast[ip[0]].block);
    } else {
      return ast; // Path not valid, just return the ast at it's current state
    }
  }
}

var err = function(str) {
  throw 'runtime error: ' + str;
}

// Resolve possible indirect values, like variables, properties, etc
// Async because it _might_ hit the DB
VM.prototype.resolveValue = function(value, callback) {
  if(typeof(value) !== 'object') return callback(null, value); // This is a direct value, just return it.
  switch(value.type) {
    case 'var': // Local variable, return it from our state.
      return callback(null, this.state.localVars[value.name]);
    case 'prop': // This is a property, fetch it from the object
      return this.mobj.getProp(value.name, callback);
    case 'objectIdAlias': // This is an object id alias, ask the app for what it means
      return callback(null, this._app.resolveObjIdAlias(value.value));
    default:
      return callback(null, null);
  }
}

// Evaluate simple expresisons, like 4 * 5
// Async because resolveValue _might_ hit the DB
VM.prototype.evalExpr = function(expr, callback) {
  // First, if the second component is not an operator and the whole expression
  // has no JS objects/hashes, then just return the expression joined by space
  // UNLESS, the first component is a number
  if(OPERATORS[expr[1]] === undefined && typeof(expr[0]) !== 'number') {
    var noObjects = true;
    for(var obj of expr) { if(typeof(obj) === 'object') { noObjects = false; break; }}
    if(noObjects) return callback(null, expr.join(' '));
  }

  // Loop over every component in the expression
  // Every second component is an operator (+ - / *)
  var output = null;
  var parseNextComponent = function(i) {
    if(i >= expr.length) {
      callback(null, output);
    } else if(i > 0) {
      var op = expr[i - 1];
      // Resolve the value so the correct value is used for the type checking
      this.resolveValue(expr[i], function(err, value) {
        if(err) return callback(err);
        if(OPERATORS[op] === undefined) return callback(op + ' is not a valid operator (' + Object.keys(OPERATORS).join(' ') + ')');
        if(TYPE_COMPATIBILITY[op][typeof(value)].indexOf(typeof(output)) < 0) return callback('type ' + typeof(value) + ' cannot be opped ' + op + ' with ' + typeof(output));
        output = OPERATORS[op](output, value);
        return parseNextComponent(i + 2);
      }.bind(this));
    } else {
      this.resolveValue(expr[i], function(err, value) {
        output = value;
        parseNextComponent(i + 2);
      });
    }
  }.bind(this);

  parseNextComponent(0);
}

VM.prototype.stepOnce = function(callback) {
  // First find the Instruction
  var ins = VM.findIns(this.state.ip, this.state.ast); // _that was easy_

  // Finishes job by incrementing the IP accordingly
  var jobDone = function(err, results) {
    if(err) return callback(err);
    // TODO increment IP
    // If this is a branching instruction and we need to branch (results[0]),
    // branch instead of incrementing the IP
    if((ins.type === 'if' || ins.type === 'while') && results[0] && ins.block.length > 0) {
      this.state.ip.push(0); // This simply bumps us into the next instruction to the right on the AST
    } else {
      // Otherwise, we need to decide what to do

      // While we are at the end of the current block (if we are)
      while(this.state.ip.length > 1 &&
            this.state.ip.slice(-1)[0] ===
            VM.findIns(this.state.ip.slice(0, -1)).block.length - 1) {
        if(VM.findIns(this.state.ip.slice(0, -1)).type === 'while') {
          // TODO Consider looping around
        } else { // Probably an if statement, just pop off the stack
          this.state.ip.pop(); // Pop off the current state
        }
      }

      // Are we at the end of the script
      if(this.state.ip.length === 1 && this.state.ip[0] === this.state.ast.length - 1)
        return callback('endOfScript'); // Error with endOfScript

      // Carry on, increment by one
      this.state.ip[this.state.ip.length - 1]++;
    }
    callback();
  }.bind(this);

  switch(ins.type) {
    case 'while':
      break;
    case 'if':
      break;
    case 'assign':
      async.waterfall([
      function(c) { this.evalExpr(ins.src, c); }.bind(this),
      function(value, c) { // We have evaluated the right side of the assignment, set the value.
        if(ins.op === '=') {
          // Simple assignment
          if(ins.dst.type === 'var') {
            // Local variable, just set it here
            this.state.localVars[ins.dst.name] = value;
            c();
          } else if(ins.dst.type === 'prop') {
            // Object prop, set it and pass it the stepOnce callback.
            this.mobj.setProp(ins.dst.name, value, c);
          } else {
            c('unspecifiable error (cac96aa5-fade-4263-a56c-e4cd883447cc)');
          }
        } else {
          // Complex assignment, runs an expression.
          // We need to resolve the dst so we can type check
          this.resolveValue(ins.dst, function(err, currentValue) {
            if(TYPE_COMPATIBILITY[ins.op][typeof(value)].indexOf(typeof(currentValue)) < 0) return c('type ' + typeof(value) + ' cannot be opped ' + ins.op + ' with ' + typeof(currentValue));
            // It's all valid, do the assignment
            currentValue = OPERATORS[ins.op](currentValue, value);
            // Set currentValue back to where it came from
            if(ins.dst.type === 'prop') {
              this.mobj.setProp(ins.dst.name, currentValue, c);
            } else if(ins.dst.type === 'var') {
              this.state.localVars[ins.dst.name] = currentValue;
              return c();
            } else {
              return c('unspecifiable error (13041b5b-7392-400d-9623-44347db5f81e)');
            }
          }.bind(this));
        }
      }.bind(this)], jobDone);
      break;
    case 'verbcall':
      break;
  }
}

VM.prototype.step = function(times, callback) {
  async.times(times, function(i, next) {
    this.stepOnce(next);
  }.bind(this), callback);
}

module.exports = VM;
<commit_msg>skip type compatibility checks for ops not in type compat table (eg. comparisons)<commit_after>// NML VM
// Runs NML AST, stored in a MObject
// A VM would exist for each script/verb, and would be reset/replaced with a
// new VM when those scripts/verbs are updated/edited.

var async = require('async');

var OPERATORS = {'+': function(left, right) { return left + right; },
                 '-': function(left, right) { return left - right; },
                 '/': function(left, right) { return left / right; },
                 '*': function(left, right) { return left * right; },
                 '==': function(left, right) { return left === right; },
                 '!=': function(left, right) { return left !== right; },
                 '>=': function(left, right) { return left >= right; },
                 '<=': function(left, right) { return left <= right; },
                 '>': function(left, right) { return left > right; },
                 '<': function(left, right) { return left < right; }};

var TYPE_COMPATIBILITY = {
  '+': {'number': ['string', 'number'], 'string': ['string'], 'array': ['string'], 'null': ['string']},
  '-': {'number': ['number'], 'string': [], 'array': [], 'null': []},
  '*': {'number': ['number'], 'string': [], 'array': [], 'null': []},
  '/': {'number': ['number'], 'string': [], 'array': [], 'null': []}
}

var VM = function(app, mobj, ast) {
  this._app = app;
  this.mobj = mobj; // MOO Object

  // Initialize the state
  this.state = {
    ast: ast,
    localVars: {},
    ip: [0] // Instruction pointer/path; this is an array as the AST is a tree with many levels
            // Example: [2, 4, 10] for the second object, then fourth, then tenth object
  }
}

VM.findIns = function(ip, ast) {
  // Find the instruction specified by instruction path IP
  if(ip.length <= 1) {
    return ast[ip[0]];
  } else {
    if(typeof(ast[ip[0]]) === 'object') {
      return VM.findIns(ip.slice(1, ip.length), ast[ip[0]].block);
    } else {
      return ast; // Path not valid, just return the ast at it's current state
    }
  }
}

var err = function(str) {
  throw 'runtime error: ' + str;
}

// Resolve possible indirect values, like variables, properties, etc
// Async because it _might_ hit the DB
VM.prototype.resolveValue = function(value, callback) {
  if(typeof(value) !== 'object') return callback(null, value); // This is a direct value, just return it.
  switch(value.type) {
    case 'var': // Local variable, return it from our state.
      return callback(null, this.state.localVars[value.name]);
    case 'prop': // This is a property, fetch it from the object
      return this.mobj.getProp(value.name, callback);
    case 'objectIdAlias': // This is an object id alias, ask the app for what it means
      return callback(null, this._app.resolveObjIdAlias(value.value));
    default:
      return callback(null, null);
  }
}

// Evaluate simple expresisons, like 4 * 5
// Async because resolveValue _might_ hit the DB
VM.prototype.evalExpr = function(expr, callback) {
  // First, if the second component is not an operator and the whole expression
  // has no JS objects/hashes, then just return the expression joined by space
  // UNLESS, the first component is a number
  if(OPERATORS[expr[1]] === undefined && typeof(expr[0]) !== 'number') {
    var noObjects = true;
    for(var obj of expr) { if(typeof(obj) === 'object') { noObjects = false; break; }}
    if(noObjects) return callback(null, expr.join(' '));
  }

  // Loop over every component in the expression
  // Every second component is an operator (+ - / *)
  var output = null;
  var parseNextComponent = function(i) {
    if(i >= expr.length) {
      callback(null, output);
    } else if(i > 0) {
      var op = expr[i - 1];
      // Resolve the value so the correct value is used for the type checking
      this.resolveValue(expr[i], function(err, value) {
        if(err) return callback(err);
        if(OPERATORS[op] === undefined) return callback(op + ' is not a valid operator (' + Object.keys(OPERATORS).join(' ') + ')');
        if(TYPE_COMPATIBILITY[op] !== undefined && [op][typeof(value)].indexOf(typeof(output)) < 0) return callback('type ' + typeof(value) + ' cannot be opped ' + op + ' with ' + typeof(output));
        output = OPERATORS[op](output, value);
        return parseNextComponent(i + 2);
      }.bind(this));
    } else {
      this.resolveValue(expr[i], function(err, value) {
        output = value;
        parseNextComponent(i + 2);
      });
    }
  }.bind(this);

  parseNextComponent(0);
}

VM.prototype.stepOnce = function(callback) {
  // First find the Instruction
  var ins = VM.findIns(this.state.ip, this.state.ast); // _that was easy_

  // Finishes job by incrementing the IP accordingly
  var jobDone = function(err, results) {
    if(err) return callback(err);
    // TODO increment IP
    // If this is a branching instruction and we need to branch (results[0]),
    // branch instead of incrementing the IP
    if((ins.type === 'if' || ins.type === 'while') && results[0] && ins.block.length > 0) {
      this.state.ip.push(0); // This simply bumps us into the next instruction to the right on the AST
    } else {
      // Otherwise, we need to decide what to do

      // While we are at the end of the current block (if we are)
      while(this.state.ip.length > 1 &&
            this.state.ip.slice(-1)[0] ===
            VM.findIns(this.state.ip.slice(0, -1)).block.length - 1) {
        if(VM.findIns(this.state.ip.slice(0, -1)).type === 'while') {
          // TODO Consider looping around
        } else { // Probably an if statement, just pop off the stack
          this.state.ip.pop(); // Pop off the current state
        }
      }

      // Are we at the end of the script
      if(this.state.ip.length === 1 && this.state.ip[0] === this.state.ast.length - 1)
        return callback('endOfScript'); // Error with endOfScript

      // Carry on, increment by one
      this.state.ip[this.state.ip.length - 1]++;
    }
    callback();
  }.bind(this);

  switch(ins.type) {
    case 'while':
      break;
    case 'if':
      break;
    case 'assign':
      async.waterfall([
      function(c) { this.evalExpr(ins.src, c); }.bind(this),
      function(value, c) { // We have evaluated the right side of the assignment, set the value.
        if(ins.op === '=') {
          // Simple assignment
          if(ins.dst.type === 'var') {
            // Local variable, just set it here
            this.state.localVars[ins.dst.name] = value;
            c();
          } else if(ins.dst.type === 'prop') {
            // Object prop, set it and pass it the stepOnce callback.
            this.mobj.setProp(ins.dst.name, value, c);
          } else {
            c('unspecifiable error (cac96aa5-fade-4263-a56c-e4cd883447cc)');
          }
        } else {
          // Complex assignment, runs an expression.
          // We need to resolve the dst so we can type check
          this.resolveValue(ins.dst, function(err, currentValue) {
            if(TYPE_COMPATIBILITY[ins.op][typeof(value)].indexOf(typeof(currentValue)) < 0) return c('type ' + typeof(value) + ' cannot be opped ' + ins.op + ' with ' + typeof(currentValue));
            // It's all valid, do the assignment
            currentValue = OPERATORS[ins.op](currentValue, value);
            // Set currentValue back to where it came from
            if(ins.dst.type === 'prop') {
              this.mobj.setProp(ins.dst.name, currentValue, c);
            } else if(ins.dst.type === 'var') {
              this.state.localVars[ins.dst.name] = currentValue;
              return c();
            } else {
              return c('unspecifiable error (13041b5b-7392-400d-9623-44347db5f81e)');
            }
          }.bind(this));
        }
      }.bind(this)], jobDone);
      break;
    case 'verbcall':
      break;
  }
}

VM.prototype.step = function(times, callback) {
  async.times(times, function(i, next) {
    this.stepOnce(next);
  }.bind(this), callback);
}

module.exports = VM;
<|endoftext|>