<commit_before>/***************************************************************************
 *
 * Copyright (c) 2014 Baidu.com, Inc. All Rights Reserved
 * $Id$
 *
 **************************************************************************/

/**
 * lib/check-output.js ~ 2014/02/12 21:27:18
 * @author leeight(liyubei@baidu.com)
 * @version $Revision$
 * @description
 * 检查项目build完毕之后output的内容是否正确
 * 1. css引用的img是否正确
 * 2. html引用的css是否正确（TODO）
 **/

/* jshint camelcase: false */

var edp = require( 'edp-core' );

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

/**
 * 检查css引用的图片是否正确
 * @param {string} file css文件的路径.
 */
function checkResouce( file ) {
    var data = edp.fs.readFileSync( file );
    var pattern = /\burl\s*\((['']?)([^\)]+)\1\)/g;
    var match = null;

    var invalidResource = [];
    while ( !!( match = pattern.exec( data ) ) ) {
        var url = match[2];
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?#iefix&v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.woff?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.ttf?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.svg#fontawesomeregular?v=3.1.0
        if ( edp.path.isLocalPath( url ) ) {
            var resourceUrl = path.normalize( path.join(path.dirname(file), url) );
            resourceUrl = resourceUrl.replace(/[#\?].*$/g, '');
            if ( !fs.existsSync( resourceUrl ) ) {
                var relative = path.relative( process.cwd(), resourceUrl );
                if ( invalidResource.indexOf( relative )  === -1 ) {
                    invalidResource.push( relative );
                }
            }
        }
    }

    if (invalidResource.length) {
        edp.log.info('%s', path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( '→ %s', resourceUrl);
        });
    }
}

/**
 * 从output目录的html文件提取require.config的内容，生成module.conf，后续
 * 检查的时候计算模板路径依赖module.conf的内容.
 *
 * @see https://github.com/ecomfe/edp/issues/131
 * FIXME 我们从index.html里面读取了require.config的内容之后，写入到module.conf
 * 之前。需要给paths里面的配置项目添加baseUrl的前缀，否则可能导致计算路径的时候
 * 出错，但是这个逻辑以后是会修改的，因此记得参考ecomfe/edp#131这个issue的状态.
 */
function prepareModuleConfig() {
    var ok = false;
    var dir = 'output';

    if ( fs.existsSync( edp.path.join( dir, 'module.conf' ) ) ) {
        return true;
    }

    fs.readdirSync( dir ).every(
        function ( file ) {
            // 如果不是html文件，忽略之
            if ( !/.html?$/.test( file ) ) {
                // continue the loop
                return true;
            }

            var config = edp.esl.readLoaderConfig(
                fs.readFileSync( edp.path.resolve( dir, file ), 'utf-8' ) );
            if ( !config ) {
                // continue the loop
                return true;
            }

            var filename = edp.path.resolve( dir, 'module.conf' );
            var data = JSON.stringify( config.data, null, 4 );
            fs.writeFileSync( filename, data );
            ok = true;

            // break the loop
            return false;
        }
    );

    return ok;
}

/**
 * 检查js引用的模板路径是否正确
 */
function checkTemplate( file ) {
    var data = edp.fs.readFileSync( file );

    var ast = edp.esl.getAst( data, file );
    var moduleInfos = edp.esl.analyseModule( ast );
    if ( !moduleInfos ) {
        return;
    }

    if ( !(moduleInfos instanceof Array) ) {
        moduleInfos = [ moduleInfos ];
    }

    var invalidResource = [];
    for ( var i = 0; i < moduleInfos.length; i ++ ) {
        var moduleInfo = moduleInfos[ i ];
        invalidResource = invalidResource.concat(
            checkTemplateFromModuleInfo( moduleInfo, file )
        );
    }

    if (invalidResource.length) {
        edp.log.info('%s', path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( '→ %s', resourceUrl);
        });
    }
}

function checkTemplateFromModuleInfo( moduleInfo, file ) {
    var invalidResource = [];

    var actualDependencies = moduleInfo.actualDependencies;
    if ( !actualDependencies || actualDependencies.length <= 0 ) {
        return invalidResource;
    }

    var moduleConfigFile = path.join( process.cwd(), 'output', 'module.conf' );

    for ( var i = 0; i < actualDependencies.length; i ++) {
        var depId = actualDependencies[ i ];
        if ( depId.indexOf( '!' ) === -1 ) {
            continue;
        }

        var rv = edp.esl.toUrl( depId, moduleInfo.id, file, moduleConfigFile );
        var tpl = ( rv.resource || '' ).replace( /\.js$/, '' );

        // 有的项目里面会把tpl编译成js的
        if ( tpl && !( fs.existsSync( tpl ) || fs.existsSync( tpl + '.js' ) ) ) {
            if ( invalidResource.indexOf( tpl ) === -1 ) {
                invalidResource.push( tpl );
            }
        }
    }

    return invalidResource;
}

module.exports = function(args, opts) {
    edp.log.info('Checking output directory...');

    if ( !fs.existsSync( 'output' ) ) {
        return;
    }

    var FLAGS_enableCheckTemplate = true;
    if ( !prepareModuleConfig() ) {
        edp.log.warn( 'Prepare module config failed, skip `checkTemplate\'' );
        FLAGS_enableCheckTemplate = false;
    }

    edp.glob.sync( 'output/**/*.css' ).forEach( checkResouce );
    if ( FLAGS_enableCheckTemplate ) {
        edp.glob.sync( [ 'output/**/*.js', '!test/**', '!node_modules/**' ] ).forEach( checkTemplate );
    }

    // 清除output/module.conf文件
    try {
        fs.unlinkSync( 'output/module.conf' );
    } catch( ex ) {}
};

/* vim: set ts=4 sw=4 sts=4 tw=100: */
<commit_msg>Fix typos<commit_after>/***************************************************************************
 *
 * Copyright (c) 2014 Baidu.com, Inc. All Rights Reserved
 * $Id$
 *
 **************************************************************************/

/**
 * lib/check-output.js ~ 2014/02/12 21:27:18
 * @author leeight(liyubei@baidu.com)
 * @version $Revision$
 * @description
 * 检查项目build完毕之后output的内容是否正确
 * 1. css引用的img是否正确
 * 2. html引用的css是否正确（TODO）
 **/

/* jshint camelcase: false */

var edp = require( 'edp-core' );

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

/**
 * 检查css引用的图片是否正确
 * @param {string} file css文件的路径.
 */
function checkResouce( file ) {
    var data = edp.fs.readFileSync( file );
    var pattern = /\burl\s*\((["']?)([^\)]+)\1\)/g;
    var match = null;

    var invalidResource = [];
    while ( !!( match = pattern.exec( data ) ) ) {
        var url = match[2];
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?#iefix&v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.woff?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.ttf?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.svg#fontawesomeregular?v=3.1.0
        if ( edp.path.isLocalPath( url ) ) {
            var resourceUrl = path.normalize( path.join(path.dirname(file), url) );
            resourceUrl = resourceUrl.replace(/[#\?].*$/g, '');
            if ( !fs.existsSync( resourceUrl ) ) {
                var relative = path.relative( process.cwd(), resourceUrl );
                if ( invalidResource.indexOf( relative )  === -1 ) {
                    invalidResource.push( relative );
                }
            }
        }
    }

    if (invalidResource.length) {
        edp.log.info('%s', path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( '→ %s', resourceUrl);
        });
    }
}

/**
 * 从output目录的html文件提取require.config的内容，生成module.conf，后续
 * 检查的时候计算模板路径依赖module.conf的内容.
 *
 * @see https://github.com/ecomfe/edp/issues/131
 * FIXME 我们从index.html里面读取了require.config的内容之后，写入到module.conf
 * 之前。需要给paths里面的配置项目添加baseUrl的前缀，否则可能导致计算路径的时候
 * 出错，但是这个逻辑以后是会修改的，因此记得参考ecomfe/edp#131这个issue的状态.
 */
function prepareModuleConfig() {
    var ok = false;
    var dir = 'output';

    if ( fs.existsSync( edp.path.join( dir, 'module.conf' ) ) ) {
        return true;
    }

    fs.readdirSync( dir ).every(
        function ( file ) {
            // 如果不是html文件，忽略之
            if ( !/.html?$/.test( file ) ) {
                // continue the loop
                return true;
            }

            var config = edp.esl.readLoaderConfig(
                fs.readFileSync( edp.path.resolve( dir, file ), 'utf-8' ) );
            if ( !config ) {
                // continue the loop
                return true;
            }

            var filename = edp.path.resolve( dir, 'module.conf' );
            var data = JSON.stringify( config.data, null, 4 );
            fs.writeFileSync( filename, data );
            ok = true;

            // break the loop
            return false;
        }
    );

    return ok;
}

/**
 * 检查js引用的模板路径是否正确
 */
function checkTemplate( file ) {
    var data = edp.fs.readFileSync( file );

    var ast = edp.esl.getAst( data, file );
    var moduleInfos = edp.esl.analyseModule( ast );
    if ( !moduleInfos ) {
        return;
    }

    if ( !(moduleInfos instanceof Array) ) {
        moduleInfos = [ moduleInfos ];
    }

    var invalidResource = [];
    for ( var i = 0; i < moduleInfos.length; i ++ ) {
        var moduleInfo = moduleInfos[ i ];
        invalidResource = invalidResource.concat(
            checkTemplateFromModuleInfo( moduleInfo, file )
        );
    }

    if (invalidResource.length) {
        edp.log.info('%s', path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( '→ %s', resourceUrl);
        });
    }
}

function checkTemplateFromModuleInfo( moduleInfo, file ) {
    var invalidResource = [];

    var actualDependencies = moduleInfo.actualDependencies;
    if ( !actualDependencies || actualDependencies.length <= 0 ) {
        return invalidResource;
    }

    var moduleConfigFile = path.join( process.cwd(), 'output', 'module.conf' );

    for ( var i = 0; i < actualDependencies.length; i ++) {
        var depId = actualDependencies[ i ];
        if ( depId.indexOf( '!' ) === -1 ) {
            continue;
        }

        var rv = edp.esl.toUrl( depId, moduleInfo.id, file, moduleConfigFile );
        var tpl = ( rv.resource || '' ).replace( /\.js$/, '' );

        // 有的项目里面会把tpl编译成js的
        if ( tpl && !( fs.existsSync( tpl ) || fs.existsSync( tpl + '.js' ) ) ) {
            if ( invalidResource.indexOf( tpl ) === -1 ) {
                invalidResource.push( tpl );
            }
        }
    }

    return invalidResource;
}

module.exports = function(args, opts) {
    edp.log.info('Checking output directory...');

    if ( !fs.existsSync( 'output' ) ) {
        return;
    }

    var FLAGS_enableCheckTemplate = true;
    if ( !prepareModuleConfig() ) {
        edp.log.warn( 'Prepare module config failed, skip `checkTemplate\'' );
        FLAGS_enableCheckTemplate = false;
    }

    edp.glob.sync( 'output/**/*.css' ).forEach( checkResouce );
    if ( FLAGS_enableCheckTemplate ) {
        edp.glob.sync( [ 'output/**/*.js', '!test/**', '!node_modules/**' ] ).forEach( checkTemplate );
    }

    // 清除output/module.conf文件
    try {
        fs.unlinkSync( 'output/module.conf' );
    } catch( ex ) {}
};

/* vim: set ts=4 sw=4 sts=4 tw=100: */
<|endoftext|>


<commit_before>var http = require('http'),
    exec = require('child_process').exec,
    util = require('util'),
    EventEmitter = require('events').EventEmitter,
    fs = require('fs'),
    path = require('path'),
    os = require('os'),
    sendIv;

var emitter = new EventEmitter;

var clientUtils = module.exports = {
    devices:{},
    config:{},
    readings:{},
    connectBuffer:[],
    instantContainer:{},
    motionContainer:{},
    watchDogIv:0,
    deviceMeta: {
        '0': {
            '2': {
                sensitivity:120
            },
            '1000': {
                instant:true
            },
            '5': {
                instant:true
            },
            '7': {
                instant:true
            },
            '11': {
                instant:true
            },
            '1002': {
                instant:true
            }
        }
    },
    configure: function(config,tty) {
        var self = this;
        this.config=config||{};
        this.tty=tty||{};

        fs.readdir(__dirname+'/../modules',function(err, files) {
            if (err) return;
            for (var i=0;i<files.length;i++) {
                if (files[i].indexOf('.js')==files[i].length-3) {
                    // File ends in .js, do it
                    try {
                        console.log(__dirname+'/../modules/'+files[i])
                        require(__dirname+'/../modules/'+files[i]).call(self);
                    } catch (err) {}
                }
            }
        });
    },
    initRemote: function(remote) {

        var self = this;
        this.remote = remote;
        this.tty.removeAllListeners('data');
        this.tty.on('data',function(data){

            self.handleRawTtyData(data);
        });
        clearInterval(sendIv);
        sendIv = setInterval(function(){

            self.remote.heartbeat(self.getHeartbeat());
        },this.config.heartbeat_interval);

        if (this.connectBuffer.length>0) {
            this.remote.data({
                'DEVICE':this.connectBuffer
            });
            this.connectBuffer=[];
        }
        // Reset arduino
        /*
        exec('/opt/utilities/bin/reset_arduino',function(code,stdout,stderr) {
            setTimeout(function() {
                self.changeLEDColor('green');
            },2000);
        });
         */
    },
    registerDevice: function(device) {
        if (!device) return;
        var self = this;

        device.guid = this.buildDeviceGuid(this.config.id,device);

        device.on('data',function(data) {

            try {
                self.sendData({
                    G:device.G,
                    V:device.V,
                    D:device.D,
                    DA:data
                } );
            } catch (err) {}
        });
        this.devices[device.guid] = device;
    },
    deregisterDevice: function(device) {

        delete this.devices[device.guid];
    },
    sendData: function(msg) {

        if (!msg) return;
        // If we're connected, send
        // Otherwise buffer until we're connected
        // But only the last 10 commands
        msg.GUID = this.buildDeviceGuid(this.config.id,msg);
        msg.TIMESTAMP = (new Date().getTime());

        if (this.remote && this.remote.data) {
            console.log({
                'DEVICE':[msg]
            })
            this.remote.data({
                'DEVICE':[msg]
            });
        } else {
            var buf = this.connectBuffer;
            buf.push(msg);
            if (buf.length>9) {
                buf.shift();
            }
            this.connectBuffer = buf;
        }
    },
    fetchBlockParams: function() {

        var params = {
            client:this.config.client,
            id:this.config.id,
            version:{
                node:this.config.nodeVersion,
                utilities:this.config.utilitiesVersion,
                system:this.config.systemVersion
            }
        };
        if (this.config.arduinoModel&&this.config.arduinoVersion) {
            params.version.arduino = {
                model:this.config.arduinoModel,
                version:this.config.arduinoVersion
            }
        };
        return params;
    },
    handleRawTtyData: function(data) {

        var jsonTtyData = this.getJSON(data) || false;
        if (!jsonTtyData || Object.keys(jsonTtyData).length!==1) return;
        switch (Object.keys(jsonTtyData)[0]) {
            case 'ACK':
                this.acknowledge(jsonTtyData.ACK)
            break;
            case 'DEVICE':
                this.handleDeviceData(jsonTtyData.DEVICE);
            break;
            case 'PLUGIN':
                console.log("Plugin")
                console.dir(jsonTtyData.PLUGIN)
            break;
            case 'UNPLUG':
                console.log("Unplug")
                console.dir(jsonTtyData.UNPLUG)
            break;
            case 'ERROR':
                // Ignore
            break;
        }
    },
    acknowledge: function(msg) {

        emitter.emit('DeviceAcknowledgement',msg);
    },
    handleDeviceData: function(deviceDataPoints) {

        if (!(deviceDataPoints instanceof Array)) return;
        for (var i=0; i<deviceDataPoints.length; i++) {
            var device = deviceDataPoints[i];
            if (device.D===2) {
                device = this.convertAccelerometerToMotion(device);
            }
            if (device) {
                this.sendData(device);
            }
        }
    },
    sendConfigData: function(deviceData) {
        var newMsg = {
            "TIMESTAMP": new Date().getTime(),
            "CONFIG":[deviceData]
        }
        this.remote.config(JSON.stringify(newMsg));
    },
    getHeartbeat: function(){
        var hb = {
            "TIMESTAMP": null,
            "DEVICE":[]
        };
        hb.TIMESTAMP = new Date().getTime();
        for (r in this.readings) {
            if (this.readings.hasOwnProperty(r)) {
                hb.DEVICE.unshift(this.readings[r]);
            }
        }
        this.readings={};
        return JSON.stringify(hb);
    },
    beatThrottle: {
        isGoodToGo : function() {
            return (!this.isEmpty(this.readings));
        }
    },
    executeCommand: function(data){
        var that = this;
        var data = this.getJSON(data);
        for (var d=0,ds=data.DEVICE;d<ds.length;d++) {

            console.log('Executing:\n\n\n\n' )
            console.log(ds[d]);

            var guid = ds[d].GUID;
            delete ds[d].GUID;
            ds[d].G = ds[d].G.toString(); //TODO get JP to fix for 0

            // If there is a handler with a .write() method, use it
            // Otherwise send to the arduino
            if (this.devices[guid] && typeof this.devices[guid].write === "function") {
                try {
                    return this.devices[guid].write(ds[d].DA);
                } catch (err) {
                    console.log(this.timestamp()+" Error Actuating "+guid+" ("+err.message+")");
                }
            }
            else {
                console.log(this.timestamp()+" Actuating "+guid+" ("+ds[d].DA+")");
                return this.writeTTY(this.tty,'{"DEVICE":['+JSON.stringify(ds[d])+']}');
            }
        }
    },
    updateCode: function(a) {
        var that = this;
        this.changeLEDColor('white');
        for (var i=0;i<a.length;i++) {
            // Nuke the lcok file that deals with the element we're updating
            try {
                fs.unlinkSync(this.config.updateLock+'_'+a[i]);
            } catch (err) {}

            switch(a[i]) {
                case 'node':
                    // Pass our existing environment into the update script
                    var options = {
                        env:process.env
                    };
                    console.log(that.timestamp()+' Beginning Client Update');
                    exec('/opt/utilities/bin/ninja_update_node',options,function (error, stdout, stderr) {
                        // Make sure that we reboot after we're done updating node code
                        // If there are other updates to do
                        console.log(that.timestamp()+' Restarting');
                        that.changeLEDColor('yellow');
                        setTimeout(function() {
                            process.exit(1);
                        },5000);
                    });
                    break;
                case 'utilities':
                case 'arduino':

                    process.exit(1);
                    break;
                case 'system':
                    // We force a reboot by clearing the watchdog timer.
                    // This is a pretty ugly hack so we don't have to give sudo access to this process
                    // We wait 45 seconds to ensure the writeback cache has been flushed to disk.
                    console.log(that.timestamp()+' Clearing WatchDog Timer');
                    setTimeout(function() {
                        clearInterval(that.watchDogIv);
                    },31000);
                    break;
            }
        }
    },
    timestamp: function() {
        return new Date().toUTCString();
    },
    getJSON: function(chunk){
        try {
            return JSON.parse(chunk);
        } catch (e) {
            console.log(this.timestamp()+' Ignored: '+e);
            return false;
        }
    },
    writeTTY: function(tty,data,errorCallback){
        try {
            tty.write(data);
            return true;
        } catch (e) {
            console.log(this.timestamp()+' writeTTY error: '+e);
            if (errorCallback) errorCallback(e);
            return false;
        }
    },
    wrapCommand: function(DA,D,V,G){
        // V defaults to Ninja Blocks, G is not used, but must be present
        var msg = {
            "DEVICE":[{
                G:G||"",
                V:V||0,
                D:D,
                DA:DA
            }]
        }
        return JSON.stringify(msg);
    },
    isEmpty : function(obj) {
        return Object.keys(obj).length === 0;
    },
    changeLEDColor: function(color) {
        var self = this;
        switch (color) {
            case 'white':
                var hex = 'FFFFFF';
            break;
            case 'yellow':
                var hex = 'FFFF00';
            break;
            case 'red':
                var hex = 'FF0000';
            break;
            case 'purple':
                var hex = 'FF00FF';
            break;
            case 'green':
                var hex = '00FF00';
            break;
            case 'cyan':
                var hex = '00FFFF';
            break;
            default:
                var hex = '000000';
            break;
        };
        // Trying this ACK thing here first
        var retryCount = 0;

        var retryIv = setInterval(function() {
            self.writeTTY(self.tty,toSend);
            if (retryCount>2) {
                emitter.removeListener('DeviceAcknowledgement',retryListener);
                clearInterval(retryIv);
            }
            else retryCount++;
        },500);

        var retryListener = function(msg) {
            if (util.isArray(msg) && msg[0].D===999 && msg[0].DA===hex) {
                emitter.removeListener('DeviceAcknowledgement',retryListener);
                clearInterval(retryIv);
            }
        };

        emitter.on('DeviceAcknowledgement',retryListener);
        var toSend = this.wrapCommand(hex,999,0,'0');
        this.writeTTY(this.tty,toSend);
    },
    convertAccelerometerToMotion: function(device) {
        if (!this.motionContainer.DA) {
            this.motionContainer = device;
            return;
        }
        // Split up data in the container which is in the form x,y,x
        var sumParts1 = this.motionContainer.DA.split(',');
        if (sumParts1.length!==3) {
            this.motionContainer={};
            return;
        }
        // Sum all the values of the coordinates
        var sum1 = parseInt(sumParts1[0])+parseInt(sumParts1[1])+parseInt(sumParts1[2]);
        var sumParts2 = device.DA.split(',');
        if (sumParts2.length!==3) {
            delete device;
            return;
        }
        // Split up data from the device which is in the form x,y,x
        var sum2 = parseInt(sumParts2[0])+parseInt(sumParts2[1])+parseInt(sumParts2[2]);
        // Set the container to the latest device value
        this.motionContainer = device;
        // Calculate the difference between the previous coordinate summate and the latest
        var diff = sum2-sum1;
        var meta = this.getDeviceMetaData(device);
        // If absolute value of the difference is greater than the sensitivity, fire off the motion device
        if (diff>meta.sensitivity || diff<-meta.sensitivity) {
            var out = {
                G:"0",
                V:0,
                D:3,
                DA:1
            };
            return out;
        }
    },
    buildDeviceGuid: function(nodeid,device) {
        return nodeid+'_'+device.G+'_'+device.V+'_'+device.D;
    },
    deviceHasMetaData: function(device) {
        return (clientUtils.deviceMeta[device.V]&&clientUtils.deviceMeta[device.V][device.D]);
    },
    getDeviceMetaData: function(device) {
        return clientUtils.deviceMeta[device.V][device.D];
    },
    setWatchDogIv: function(iv) {
        this.watchDogIv = iv;
    },
    fetchBlockToken: function() {
        return path.existsSync(this.config.tokenFile)
            && fs
                .readFileSync(this.config.tokenFile)
                .toString()
                .replace(/\n/g,'')
            || false;
    }
};<commit_msg>on ACK send data immediately, send PLUGIN and UNPLUG to pusher<commit_after>var http = require('http'),
    exec = require('child_process').exec,
    util = require('util'),
    EventEmitter = require('events').EventEmitter,
    fs = require('fs'),
    path = require('path'),
    os = require('os'),
    sendIv;

var emitter = new EventEmitter;

var clientUtils = module.exports = {
    devices:{},
    config:{},
    readings:{},
    connectBuffer:[],
    instantContainer:{},
    motionContainer:{},
    watchDogIv:0,
    deviceMeta: {
        '0': {
            '2': {
                sensitivity:120
            },
            '1000': {
                instant:true
            },
            '5': {
                instant:true
            },
            '7': {
                instant:true
            },
            '11': {
                instant:true
            },
            '1002': {
                instant:true
            }
        }
    },
    configure: function(config,tty) {
        var self = this;
        this.config=config||{};
        this.tty=tty||{};

        fs.readdir(__dirname+'/../modules',function(err, files) {
            if (err) return;
            for (var i=0;i<files.length;i++) {
                if (files[i].indexOf('.js')==files[i].length-3) {
                    // File ends in .js, do it
                    try {
                        console.log(__dirname+'/../modules/'+files[i])
                        require(__dirname+'/../modules/'+files[i]).call(self);
                    } catch (err) {}
                }
            }
        });
    },
    initRemote: function(remote) {

        var self = this;
        this.remote = remote;
        this.tty.removeAllListeners('data');
        this.tty.on('data',function(data){

            self.handleRawTtyData(data);
        });
        clearInterval(sendIv);
        sendIv = setInterval(function(){

            self.remote.heartbeat(self.getHeartbeat());
        },this.config.heartbeat_interval);

        if (this.connectBuffer.length>0) {
            this.remote.data({
                'DEVICE':this.connectBuffer
            });
            this.connectBuffer=[];
        }
        // Reset arduino
        /*
        exec('/opt/utilities/bin/reset_arduino',function(code,stdout,stderr) {
            setTimeout(function() {
                self.changeLEDColor('green');
            },2000);
        });
         */
    },
    registerDevice: function(device) {
        if (!device) return;
        var self = this;

        device.guid = this.buildDeviceGuid(this.config.id,device);

        device.on('data',function(data) {

            try {
                self.sendData({
                    G:device.G,
                    V:device.V,
                    D:device.D,
                    DA:data
                } );
            } catch (err) {}
        });
        this.devices[device.guid] = device;
    },
    deregisterDevice: function(device) {

        delete this.devices[device.guid];
    },
    sendData: function(msg) {

        if (!msg) return;
        // If we're connected, send
        // Otherwise buffer until we're connected
        // But only the last 10 commands
        msg.GUID = this.buildDeviceGuid(this.config.id,msg);
        msg.TIMESTAMP = (new Date().getTime());

        if (this.remote && this.remote.data) {
            console.log({
                'DEVICE':[msg]
            })
            this.remote.data({
                'DEVICE':[msg]
            });
        } else {
            var buf = this.connectBuffer;
            buf.push(msg);
            if (buf.length>9) {
                buf.shift();
            }
            this.connectBuffer = buf;
        }
    },
    fetchBlockParams: function() {

        var params = {
            client:this.config.client,
            id:this.config.id,
            version:{
                node:this.config.nodeVersion,
                utilities:this.config.utilitiesVersion,
                system:this.config.systemVersion
            }
        };
        if (this.config.arduinoModel&&this.config.arduinoVersion) {
            params.version.arduino = {
                model:this.config.arduinoModel,
                version:this.config.arduinoVersion
            }
        };
        return params;
    },
    handleRawTtyData: function(data) {

        var jsonTtyData = this.getJSON(data) || false;
        if (!jsonTtyData || Object.keys(jsonTtyData).length!==1) return;
        switch (Object.keys(jsonTtyData)[0]) {
            case 'ACK':
                this.acknowledge(jsonTtyData.ACK)
            break;
            case 'DEVICE':
                this.handleDeviceData(jsonTtyData.DEVICE);
            break;
            case 'PLUGIN':
                this.plugin(jsonTtyData.PLUGIN);
            break;
            case 'UNPLUG':
                this.unplug(jsonTtyData.UNPLUG);
            break;
            case 'ERROR':
                // Ignore
            break;
        }
    },
    plugin:function(msg) {

        var config = msg[0];
        config.type="PLUGIN";
        this.sendConfigData(config);
    },
    unplug: function(msg) {

        var config = msg[0];
        config.type="UNPLUG";
        this.sendConfigData(config);
    },
    acknowledge: function(msg) {

        this.sendData(msg[0]);
        emitter.emit('DeviceAcknowledgement',msg);
    },
    handleDeviceData: function(deviceDataPoints) {

        if (!(deviceDataPoints instanceof Array)) return;
        for (var i=0; i<deviceDataPoints.length; i++) {
            var device = deviceDataPoints[i];
            if (device.D===2) {
                device = this.convertAccelerometerToMotion(device);
            }
            if (device) {
                this.sendData(device);
            }
        }
    },
    sendConfigData: function(configMsg) {
        configMsg.GUID = this.buildDeviceGuid(this.config.id,configMsg);
        configMsg.TIMESTAMP = (new Date().getTime());
        if (this.remote && this.remote.config) {
            this.remote.config(JSON.stringify(configMsg));
        }
    },
    getHeartbeat: function(){
        var hb = {
            "TIMESTAMP": null,
            "DEVICE":[]
        };
        hb.TIMESTAMP = new Date().getTime();
        for (r in this.readings) {
            if (this.readings.hasOwnProperty(r)) {
                hb.DEVICE.unshift(this.readings[r]);
            }
        }
        this.readings={};
        return JSON.stringify(hb);
    },
    beatThrottle: {
        isGoodToGo : function() {
            return (!this.isEmpty(this.readings));
        }
    },
    executeCommand: function(data){
        var that = this;
        var data = this.getJSON(data);
        for (var d=0,ds=data.DEVICE;d<ds.length;d++) {

            console.log('Executing:\n\n\n\n' )
            console.log(ds[d]);

            var guid = ds[d].GUID;
            delete ds[d].GUID;
            ds[d].G = ds[d].G.toString(); //TODO get JP to fix for 0

            // If there is a handler with a .write() method, use it
            // Otherwise send to the arduino
            if (this.devices[guid] && typeof this.devices[guid].write === "function") {
                try {
                    return this.devices[guid].write(ds[d].DA);
                } catch (err) {
                    console.log(this.timestamp()+" Error Actuating "+guid+" ("+err.message+")");
                }
            }
            else {
                console.log(this.timestamp()+" Actuating "+guid+" ("+ds[d].DA+")");
                return this.writeTTY(this.tty,'{"DEVICE":['+JSON.stringify(ds[d])+']}');
            }
        }
    },
    updateCode: function(a) {
        var that = this;
        this.changeLEDColor('white');
        for (var i=0;i<a.length;i++) {
            // Nuke the lcok file that deals with the element we're updating
            try {
                fs.unlinkSync(this.config.updateLock+'_'+a[i]);
            } catch (err) {}

            switch(a[i]) {
                case 'node':
                    // Pass our existing environment into the update script
                    var options = {
                        env:process.env
                    };
                    console.log(that.timestamp()+' Beginning Client Update');
                    exec('/opt/utilities/bin/ninja_update_node',options,function (error, stdout, stderr) {
                        // Make sure that we reboot after we're done updating node code
                        // If there are other updates to do
                        console.log(that.timestamp()+' Restarting');
                        that.changeLEDColor('yellow');
                        setTimeout(function() {
                            process.exit(1);
                        },5000);
                    });
                    break;
                case 'utilities':
                case 'arduino':

                    process.exit(1);
                    break;
                case 'system':
                    // We force a reboot by clearing the watchdog timer.
                    // This is a pretty ugly hack so we don't have to give sudo access to this process
                    // We wait 45 seconds to ensure the writeback cache has been flushed to disk.
                    console.log(that.timestamp()+' Clearing WatchDog Timer');
                    setTimeout(function() {
                        clearInterval(that.watchDogIv);
                    },31000);
                    break;
            }
        }
    },
    timestamp: function() {
        return new Date().toUTCString();
    },
    getJSON: function(chunk){
        try {
            return JSON.parse(chunk);
        } catch (e) {
            console.log(this.timestamp()+' Ignored: '+e);
            return false;
        }
    },
    writeTTY: function(tty,data,errorCallback){
        try {
            tty.write(data);
            return true;
        } catch (e) {
            console.log(this.timestamp()+' writeTTY error: '+e);
            if (errorCallback) errorCallback(e);
            return false;
        }
    },
    wrapCommand: function(DA,D,V,G){
        // V defaults to Ninja Blocks, G is not used, but must be present
        var msg = {
            "DEVICE":[{
                G:G||"",
                V:V||0,
                D:D,
                DA:DA
            }]
        }
        return JSON.stringify(msg);
    },
    isEmpty : function(obj) {
        return Object.keys(obj).length === 0;
    },
    changeLEDColor: function(color) {
        var self = this;
        switch (color) {
            case 'white':
                var hex = 'FFFFFF';
            break;
            case 'yellow':
                var hex = 'FFFF00';
            break;
            case 'red':
                var hex = 'FF0000';
            break;
            case 'purple':
                var hex = 'FF00FF';
            break;
            case 'green':
                var hex = '00FF00';
            break;
            case 'cyan':
                var hex = '00FFFF';
            break;
            default:
                var hex = '000000';
            break;
        };
        // Trying this ACK thing here first
        var retryCount = 0;

        var retryIv = setInterval(function() {
            self.writeTTY(self.tty,toSend);
            if (retryCount>2) {
                emitter.removeListener('DeviceAcknowledgement',retryListener);
                clearInterval(retryIv);
            }
            else retryCount++;
        },500);

        var retryListener = function(msg) {
            if (util.isArray(msg) && msg[0].D===999 && msg[0].DA===hex) {
                emitter.removeListener('DeviceAcknowledgement',retryListener);
                clearInterval(retryIv);
            }
        };

        emitter.on('DeviceAcknowledgement',retryListener);
        var toSend = this.wrapCommand(hex,999,0,'0');
        this.writeTTY(this.tty,toSend);
    },
    convertAccelerometerToMotion: function(device) {
        if (!this.motionContainer.DA) {
            this.motionContainer = device;
            return;
        }
        // Split up data in the container which is in the form x,y,x
        var sumParts1 = this.motionContainer.DA.split(',');
        if (sumParts1.length!==3) {
            this.motionContainer={};
            return;
        }
        // Sum all the values of the coordinates
        var sum1 = parseInt(sumParts1[0])+parseInt(sumParts1[1])+parseInt(sumParts1[2]);
        var sumParts2 = device.DA.split(',');
        if (sumParts2.length!==3) {
            delete device;
            return;
        }
        // Split up data from the device which is in the form x,y,x
        var sum2 = parseInt(sumParts2[0])+parseInt(sumParts2[1])+parseInt(sumParts2[2]);
        // Set the container to the latest device value
        this.motionContainer = device;
        // Calculate the difference between the previous coordinate summate and the latest
        var diff = sum2-sum1;
        var meta = this.getDeviceMetaData(device);
        // If absolute value of the difference is greater than the sensitivity, fire off the motion device
        if (diff>meta.sensitivity || diff<-meta.sensitivity) {
            var out = {
                G:"0",
                V:0,
                D:3,
                DA:1
            };
            return out;
        }
    },
    buildDeviceGuid: function(nodeid,device) {
        return nodeid+'_'+device.G+'_'+device.V+'_'+device.D;
    },
    deviceHasMetaData: function(device) {
        return (clientUtils.deviceMeta[device.V]&&clientUtils.deviceMeta[device.V][device.D]);
    },
    getDeviceMetaData: function(device) {
        return clientUtils.deviceMeta[device.V][device.D];
    },
    setWatchDogIv: function(iv) {
        this.watchDogIv = iv;
    },
    fetchBlockToken: function() {
        return path.existsSync(this.config.tokenFile)
            && fs
                .readFileSync(this.config.tokenFile)
                .toString()
                .replace(/\n/g,'')
            || false;
    }
};<|endoftext|>


<commit_before>/*!
 *  reichat brush.js
 *
 *  Copyright (c) 2015 Yuki KAN
 *  https://github.com/kanreisa
**/
/*jslint browser:true, vars:true, plusplus:true, bitwise:true, white:true, nomen:true */
/*global Uint8Array */
(function () {
	'use strict';

	var brush = window.brush = {};

	var cur = {
		type: 'binary',
		preset: 'circle'
	};

	var createBinaryShape = function (size, data) {

		var canvas = document.createElement('canvas');
		canvas.width = canvas.height = size;
		var context = canvas.getContext('2d');

		var imageData = context.createImageData(size, size);

		var i, l, n;
		for (i = 0, l = data.length; i < l; i++) {
			n = i << 2;

			imageData.data[n + 3] = data[i];
		}

		context.putImageData(imageData, 0, 0);

		return {
			size: size,
			context: context,
			imageData: imageData
		};
	};

	var setHorizontalLineOfBinaryCircleArray = function (array, width, x1, x2, y) {

		if (x1 < x2) {
			return;
		}

		var offset = y * width,
			maxX = x1 < width ? x1 : width -1,
			minX = x2 > 0 ? x2 : 0,
			x;

		for (x = maxX; x >= minX; x--) {
			array[offset + x] = 255;
		}
	};

	var createBinaryCircleArray = function (size) {

		var x = Math.floor(size / 2),
			y = 0,
			o = Math.floor((size - 1)/2),
			radiusError = 1 - x,
			array = new Uint8Array(size * size),
			shift = size % 2 === 0 ? 1 : 0;

		while(x >= y) {
			setHorizontalLineOfBinaryCircleArray(array, size, x + o, -x + o + shift, y + o);
			setHorizontalLineOfBinaryCircleArray(array, size, x + o, -x + o + shift, -y + o + shift);

			if (radiusError < 0) {
				radiusError += 2 * y + 1;
			} else {
				setHorizontalLineOfBinaryCircleArray(array, size, y + o, -y + o + shift, x + o);
				setHorizontalLineOfBinaryCircleArray(array, size, y + o, -y + o + shift, -x + o + shift);

				x--;
				radiusError += 2 * (y - x + 1);
			}
			y++;
		}

		return array;
	};

	var binaryPreset = {
		'circle': {
			binary: true,
			shapes: (function () {

				var shapes = [];

				var i, l = 20;
				for (i = 1; i < l; i++) {
					shapes.push(
						createBinaryShape(i, createBinaryCircleArray(i))
					);
				}

				return shapes;
			}())
		}
	};

	var getBinaryShape = function (size) {

		var prst = binaryPreset[cur.preset],
			shape;

		size = Math.max(0, Math.round(size) - 1);

		if (prst.shapes[size]) {
			shape = prst.shapes[size];
		} else {
			shape = prst.shapes[prst.shapes.length - 1];
		}

		return shape;
	};

	brush.setType = function (typeName) {

		cur.type = typeName;
	};

	brush.setPreset = function (presetName) {

		cur.preset = presetName;
	};

	var dc = document.createElement('canvas');
	dc.width = dc.height = 40;
	var dctx = dc.getContext('2d');

	var drawBinaryPoint = function (context, x, y, size, alpha, color, mixing) {

		var shape = getBinaryShape(size);

		x = Math.round(x);
		y = Math.round(y);
		size = shape.size;

		var tlX = Math.ceil(x - size / 2),
			tlY = Math.ceil(y - size / 2);

		if (alpha === 1 || mixing === true) {
			dctx.globalCompositeOperation = 'copy';
			if (dctx.globalAlpha !== alpha) {
				dctx.globalAlpha = alpha;
			}
			if (dctx.fillStyle !== color) {
				dctx.fillStyle = color;
			}
			dctx.fillRect(0, 0, size, size);

			dctx.globalCompositeOperation = 'destination-in';
			if (alpha !== 1) {
				dctx.globalAlpha = 1;
			}
			dctx.drawImage(shape.context.canvas, 0, 0);

			context.drawImage(dc, 0, 0, size, size, tlX, tlY, size, size);
		} else {
			var c = parseInt(color.slice(1), 16),
				r = c >> 16,
				g = c >> 8 & 0xff,
				b = c & 0xff,
				i,
				l,
				n,
				target = context.getImageData(tlX, tlY, shape.imageData.width, shape.imageData.height);

			for (i = 0, l = shape.imageData.data.length / 4; i < l; i++) {
				n = i << 2;

				if (shape.imageData.data[n + 3] !== 0) {
					target.data[n] = r;
					target.data[n + 1] = g;
					target.data[n + 2] = b;
					target.data[n + 3] = 255 * alpha;
				}
			}

			context.putImageData(target, tlX, tlY);
		}
	};

	var pi2 = Math.PI * 2;

	var drawPoint = function (context, x, y, size, alpha, color, mixing) {

		if (alpha === 1 || mixing === true) {
			context.save();
			context.beginPath();
			context.globalAlpha = alpha;
			context.fillStyle = color;
			context.arc(x + 0.5, y + 0.5, size / 2, 0, pi2, false);
			context.fill();
			context.restore();
		} else {
			dctx.globalCompositeOperation = 'copy';
			dctx.globalAlpha = alpha;
			dctx.beginPath();
			dctx.arc(size / 2 + x % 1, size / 2 + y % 1, size / 2, 0, pi2, false);
			dctx.fill();

			var c = parseInt(color.slice(1), 16),
				r = c >> 16,
				g = c >> 8 & 0xff,
				b = c & 0xff,
				tlX = Math.floor(x - size / 2),
				tlY = Math.floor(y - size / 2),
				s = Math.ceil(size),
				i,
				l,
				n,
				source = dctx.getImageData(0, 0, s, s),
				target = context.getImageData(tlX, tlY, s, s);

			for (i = 0, l = source.data.length / 4; i < l; i++) {
				n = i << 2;

				if (source.data[n + 3] !== 0) {
					target.data[n] = r;
					target.data[n + 1] = g;
					target.data[n + 2] = b;
					target.data[n + 3] = 255 * alpha;
				}
			}

			context.putImageData(target, tlX, tlY);
		}
	};

	brush.draw = function (context, x, y, size, alpha, color, mixing) {

		if (cur.type === 'binary') {
			drawBinaryPoint(context, x, y, size, alpha, color, mixing);
		} else {
			drawPoint(context, x, y, size, alpha, color, mixing);
		}
	};
}());<commit_msg>lib/client/brush.js: fixed a bug<commit_after>/*!
 *  reichat brush.js
 *
 *  Copyright (c) 2015 Yuki KAN
 *  https://github.com/kanreisa
**/
/*jslint browser:true, vars:true, plusplus:true, bitwise:true, white:true, nomen:true */
/*global Uint8Array */
(function () {
	'use strict';

	var brush = window.brush = {};

	var cur = {
		type: 'binary',
		preset: 'circle'
	};

	var createBinaryShape = function (size, data) {

		var canvas = document.createElement('canvas');
		canvas.width = canvas.height = size;
		var context = canvas.getContext('2d');

		var imageData = context.createImageData(size, size);

		var i, l, n;
		for (i = 0, l = data.length; i < l; i++) {
			n = i << 2;

			imageData.data[n + 3] = data[i];
		}

		context.putImageData(imageData, 0, 0);

		return {
			size: size,
			context: context,
			imageData: imageData
		};
	};

	var setHorizontalLineOfBinaryCircleArray = function (array, width, x1, x2, y) {

		if (x1 < x2) {
			return;
		}

		var offset = y * width,
			maxX = x1 < width ? x1 : width -1,
			minX = x2 > 0 ? x2 : 0,
			x;

		for (x = maxX; x >= minX; x--) {
			array[offset + x] = 255;
		}
	};

	var createBinaryCircleArray = function (size) {

		var x = Math.floor(size / 2),
			y = 0,
			o = Math.floor((size - 1)/2),
			radiusError = 1 - x,
			array = new Uint8Array(size * size),
			shift = size % 2 === 0 ? 1 : 0;

		while(x >= y) {
			setHorizontalLineOfBinaryCircleArray(array, size, x + o, -x + o + shift, y + o);
			setHorizontalLineOfBinaryCircleArray(array, size, x + o, -x + o + shift, -y + o + shift);

			if (radiusError < 0) {
				radiusError += 2 * y + 1;
			} else {
				setHorizontalLineOfBinaryCircleArray(array, size, y + o, -y + o + shift, x + o);
				setHorizontalLineOfBinaryCircleArray(array, size, y + o, -y + o + shift, -x + o + shift);

				x--;
				radiusError += 2 * (y - x + 1);
			}
			y++;
		}

		return array;
	};

	var binaryPreset = {
		'circle': {
			binary: true,
			shapes: (function () {

				var shapes = [];

				var i, l = 20;
				for (i = 1; i < l; i++) {
					shapes.push(
						createBinaryShape(i, createBinaryCircleArray(i))
					);
				}

				return shapes;
			}())
		}
	};

	var getBinaryShape = function (size) {

		var prst = binaryPreset[cur.preset],
			shape;

		size = Math.max(0, Math.round(size) - 1);

		if (prst.shapes[size]) {
			shape = prst.shapes[size];
		} else {
			shape = prst.shapes[prst.shapes.length - 1];
		}

		return shape;
	};

	brush.setType = function (typeName) {

		cur.type = typeName;
	};

	brush.setPreset = function (presetName) {

		cur.preset = presetName;
	};

	var dc = document.createElement('canvas');
	dc.width = dc.height = 40;
	var dctx = dc.getContext('2d');

	var drawBinaryPoint = function (context, x, y, size, alpha, color, mixing) {

		var shape = getBinaryShape(size);

		x = Math.round(x);
		y = Math.round(y);
		size = shape.size;

		var tlX = Math.ceil(x - size / 2),
			tlY = Math.ceil(y - size / 2);

		if (alpha === 1 || mixing === true) {
			dctx.globalCompositeOperation = 'copy';
			if (dctx.globalAlpha !== alpha) {
				dctx.globalAlpha = alpha;
			}
			if (dctx.fillStyle !== color) {
				dctx.fillStyle = color;
			}
			dctx.fillRect(0, 0, size, size);

			dctx.globalCompositeOperation = 'destination-in';
			if (alpha !== 1) {
				dctx.globalAlpha = 1;
			}
			dctx.drawImage(shape.context.canvas, 0, 0);

			context.drawImage(dc, 0, 0, size, size, tlX, tlY, size, size);
		} else {
			var c = parseInt(color.slice(1), 16),
				r = c >> 16,
				g = c >> 8 & 0xff,
				b = c & 0xff,
				i,
				l,
				n,
				target = context.getImageData(tlX, tlY, shape.imageData.width, shape.imageData.height);

			for (i = 0, l = shape.imageData.data.length / 4; i < l; i++) {
				n = i << 2;

				if (shape.imageData.data[n + 3] !== 0) {
					target.data[n] = r;
					target.data[n + 1] = g;
					target.data[n + 2] = b;
					target.data[n + 3] = 255 * alpha;
				}
			}

			context.putImageData(target, tlX, tlY);
		}
	};

	var pi2 = Math.PI * 2;

	var drawPoint = function (context, x, y, size, alpha, color, mixing) {

		if (size === 0 || alpha === 0) {
			return;
		}

		if (alpha === 1 || mixing === true) {
			context.save();
			context.beginPath();
			context.globalAlpha = alpha;
			context.fillStyle = color;
			context.arc(x + 0.5, y + 0.5, size / 2, 0, pi2, false);
			context.fill();
			context.restore();
		} else {
			dctx.globalCompositeOperation = 'copy';
			dctx.globalAlpha = alpha;
			dctx.beginPath();
			dctx.arc(size / 2 + x % 1, size / 2 + y % 1, size / 2, 0, pi2, false);
			dctx.fill();

			var c = parseInt(color.slice(1), 16),
				r = c >> 16,
				g = c >> 8 & 0xff,
				b = c & 0xff,
				tlX = Math.floor(x - size / 2),
				tlY = Math.floor(y - size / 2),
				s = Math.ceil(size),
				i,
				l,
				n,
				source = dctx.getImageData(0, 0, s, s),
				target = context.getImageData(tlX, tlY, s, s);

			for (i = 0, l = source.data.length / 4; i < l; i++) {
				n = i << 2;

				if (source.data[n + 3] !== 0) {
					target.data[n] = r;
					target.data[n + 1] = g;
					target.data[n + 2] = b;
					target.data[n + 3] = 255 * alpha;
				}
			}

			context.putImageData(target, tlX, tlY);
		}
	};

	brush.draw = function (context, x, y, size, alpha, color, mixing) {

		if (cur.type === 'binary') {
			drawBinaryPoint(context, x, y, size, alpha, color, mixing);
		} else {
			drawPoint(context, x, y, size, alpha, color, mixing);
		}
	};
}());<|endoftext|>


<commit_before>const DEBUG = false;
class Debug {
    constructor() {
        this.debugPar = document.getElementById("debug");
        this.debugPar.style.display = DEBUG ? "" : "none";
        if (DEBUG) this.show();
        else this.hide();
    }
    clear() {
        if (!DEBUG) return;
        this.debugPar.innerHTML = "";
    }
    append(...html) {
        if (!DEBUG) return;
        this.debugPar.innerHTML += html.join(" ");
    }
    appendLine(...html) {
        if (!DEBUG) return;
        this.debugPar.innerHTML += "
" + html.join(" ");;
    }
    show() {
        this.debugPar.style.display = "";
    }
    hide() {
        this.debugPar.style.display = "none";
    }
}
var debug = new Debug();
class Zoom {
    constructor() {}
    reset() {
        this.zoomUpperLimit = document.getElementById("zoomUpperLimit").innerText === "true";
        this.isWheelActionZoom = document.getElementById("wheelAction").innerText === "zoom";
        this.marginPixels = 20;
        this.img = document.getElementById("image");
        this.naturalWidth = this.img.naturalWidth;
        this.naturalHeight = this.img.naturalHeight;
        this.zoom = (window.innerWidth - this.marginPixels) / this.naturalWidth * 100;
        if (this.zoom > 100) this.zoom = 100;
        this.img.style.width = "";
        this.img.style.maxWidth = "";
        document.body.style.width = "";
        if (document.body.offsetHeight < window.innerHeight) document.body.style.height = window.innerHeight - this.marginPixels + "px";
        debug.clear();
        debug.append(`
            initial zoom:${this.zoom}

            image naturalWidth:${this.naturalWidth}

            image width:${this.img.width}

            window width:${window.innerWidth}

            window height:${window.innerHeight}

            body width:${document.body.offsetWidth}

            body height:${document.body.offsetHeight}

            scroll Width:${window.innerWidth - document.body.clientWidth}

        `);
    }
    smoothZomm(to, callback, ...args) {
        let winWidth = window.innerWidth;
        let contentWidth = winWidth - this.marginPixels
        let minWidth = contentWidth < this.naturalWidth ? contentWidth : this.naturalWidth;
        let minZoom = parseInt(minWidth / this.naturalWidth * 100);
        if (to < minZoom) to = minZoom - 1;
        let from = this.zoom;
        const interval = 10;
        const level = 10;
        const delta = (to - from) / level;
        for (let i = 1; i <= level; i++) {
            setTimeout(() => {
                this.setZoom(from + delta * i);
                callback(...args);
            }, interval * i);
        }
    }
    setZoom(zoom) {
        debug.appendLine("org: ", this.zoom);
        let winWidth = window.innerWidth;
        let contentWidth = winWidth - this.marginPixels
        let minWidth = contentWidth < this.naturalWidth ? contentWidth : this.naturalWidth;
        let minZoom = parseInt(minWidth / this.naturalWidth * 100);
        const maxZoom = 100;

        if (this.zoomUpperLimit && zoom > maxZoom) zoom = maxZoom;
        if (zoom < minZoom || (minZoom == maxZoom && this.zoomUpperLimit)) {
            this.img.style.width = "";
            this.img.style.maxWidth = "";
            document.body.style.width = "";
            zoom = minZoom;
        } else {
            let imgWidth = parseInt(this.naturalWidth * zoom / 100);
            this.img.style.width = imgWidth + 'px';
            let body = document.body;
            let bodyWidth = imgWidth + this.marginPixels < winWidth ? winWidth : imgWidth + this.marginPixels;
            body.style.width = bodyWidth + 'px'
            if (body.offsetHeight < window.innerHeight) body.style.height = window.innerHeight - this.marginPixels + "px";
        }
        this.zoom = zoom;
        debug.appendLine("cur: ", this.zoom);
    }
    setScroll(left, top) {
        document.body.scrollLeft = left;
        document.body.scrollTop = top;
    }
    add() {
        let afterZoom = mouseAt => {
            this.followMousePointer(mouseAt);
            this.setToggleIcon();
            saveStatus();
        }
        this.reset();
        this.img.addEventListener("dblclick", () => {
            let mouseAt = this.getMousePointer();
            if (this.img.style.width)
                this.smoothZomm(0, afterZoom, mouseAt);
            else
                this.smoothZomm(100, afterZoom, mouseAt);
        })
        document.getElementById("btnZoomIn").addEventListener("click", () => {
            this.smoothZomm(this.zoom + 10, afterZoom, this.getImageCenterMousePointer());
        });
        document.getElementById("btnZoomOut").addEventListener("click", () => {
            this.smoothZomm(this.zoom - 10, afterZoom, this.getImageCenterMousePointer());
        });
        document.getElementById("btnZoomToggle").addEventListener("click", () => {
            if (this.img.style.width)
                this.smoothZomm(0, afterZoom, this.getImageCenterMousePointer());
            else
                this.smoothZomm(100, afterZoom, this.getImageCenterMousePointer());
        });
        document.body.addEventListener("mousewheel", () => {
            // console.log(event.ctrlKey, event.wheelDeltaX, event.wheelDeltaY);
            // scroll to zoom, or ctrl key pressed scroll
            if (this.isWheelActionZoom || event.ctrlKey) {
                // ctrlKey == true: pinch
                let delta = event.ctrlKey ? event.wheelDelta / 60 : event.wheelDelta / 12;
                let mouseAt = this.getMousePointer();
                if (this.zoomUpperLimit) {
                    this.setZoom(this.zoom + delta);
                } else {
                    // zoom level increase / decrease by 30% for each wheel scroll
                    this.setZoom(this.zoom * (delta / 50 + 1));
                }
                this.followMousePointer(mouseAt);
                this.setToggleIcon();
                saveStatus();
                if (event.preventDefault) event.preventDefault();
                return false;
            }
        });
        window.onresize = () => {
            let winWidth = window.innerWidth;
            let contentWidth = winWidth - this.marginPixels
            let minWidth = contentWidth < this.naturalWidth ? contentWidth : this.naturalWidth;
            let minZoom = parseInt(minWidth / this.naturalWidth * 100);

            if (this.img.style.width == "") {
                // console.log("update zoom value due to resize");
                this.zoom = minZoom;
            } else if (this.zoom < minZoom) {
                // console.log("change zoom to fit");
                this.zoom = minZoom;
                this.img.style.width = "";
                this.img.style.maxWidth = "";
                document.body.style.width = "";
            }
        };
    }
    followMousePointer(mouseAt) {
        let e = event || window.event;
        let imgWidth = parseInt(this.naturalWidth * this.zoom / 100);
        let imgHeight = parseInt(this.naturalHeight * this.zoom / 100);
        document.body.scrollLeft = parseInt(imgWidth * mouseAt.imageX + this.marginPixels / 2) - mouseAt.x;
        document.body.scrollTop = parseInt(imgHeight * mouseAt.imageY + this.marginPixels / 2) - mouseAt.y;
    }
    getMousePointer(x, y) {
        let imgWidth = parseInt(this.naturalWidth * this.zoom / 100);
        let imgHeight = parseInt(this.naturalHeight * this.zoom / 100);
        let e = event || window.event;
        let clientX = x || e.clientX
        let clientY = y || e.clientY
        let mouseAt = {
            x: clientX,
            y: clientY,
            imageX: (clientX + document.body.scrollLeft - this.marginPixels / 2) / imgWidth,
            imageY: (clientY + document.body.scrollTop - this.marginPixels / 2) / imgHeight,
        }
        return mouseAt;
    }
    getImageCenterMousePointer() {
        let ph = document.getElementById("placeholder");
        let x = (window.innerWidth - this.marginPixels) / 2;
        let y = (window.innerHeight - this.marginPixels - ph.clientHeight) / 2;
        return this.getMousePointer(x, y);
    }
    setToggleIcon() {
        let fit = document.getElementById("icon-fit");
        let expand = document.getElementById("icon-expand");
        if (this.img.style.width) {
            fit.style.display = "";
            expand.style.display = "none";
        } else {
            fit.style.display = "none";
            expand.style.display = "";
        }

    }
}
class Switcher {
    constructor() {
        this.current = 0;
        this.images = [];
        this.image = document.getElementById("image");
        this.pInfo = document.getElementById("pageInfo");
        this.pInfoTpl = this.pInfo.innerText;
        for (let e of document.getElementById("images").getElementsByTagName("img")) {
            this.images.push(e.src);
        }
    }
    add() {
        if (this.images.length <= 1) {
            // document.getElementById("placeholder").style.display = "none";
            document.getElementById("page-ctrls").style.display = "none";
            return;
        }
        document.getElementById("btnNext").addEventListener("click", () => {
            if (this.current == this.images.length) return;
            this.moveTo(++this.current);
            saveStatus();
        });
        document.getElementById("btnPrev").addEventListener("click", () => {
            if (this.current == 1) return;
            this.moveTo(--this.current);
            saveStatus();
        });

        this.moveTo(1);
        document.getElementById("images").remove();
        // console.log(this.images.length);
    }
    moveTo(page) {
        if (page < 1 || page > this.images.length) return;
        this.image.src = this.images[page - 1];
        this.pInfo.innerText = String.format(this.pInfoTpl, page, this.images.length);
        this.current = page;
        zoomer.reset();
    }
}
String.format = function format() {
    if (arguments.length == 0)
        return null;

    var str = arguments[0];
    for (var i = 1; i < arguments.length; i++) {
        var re = new RegExp('\\{' + (i - 1) + '\\}', 'gm');
        str = str.replace(re, arguments[i]);
    }
    return str;
}
let zoomer;
let switcher;
let sendStatus;

function saveStatus() {
    if (sendStatus) {
        let status = JSON.stringify({
            page: switcher.current,
            zoom: zoomer.zoom,
            x: document.body.scrollLeft,
            y: document.body.scrollTop
        });
        // console.log("save status: " + status);
        sendStatus.attributes["href"].value = 'command:plantuml.previewStatus?' + encodeURIComponent(status);
        sendStatus.click();
    }
}
window.addEventListener("load", () => {
    sendStatus = document.getElementById("sendStatus");
    zoomer = new Zoom();
    switcher = new Switcher();
    switcher.add();
    zoomer.add();
    let jsonStatus = document.getElementById("status").innerHTML;
    if (jsonStatus) {
        let status = {};
        try {
            status = JSON.parse(jsonStatus);
        } catch (error) {}
        status.page = status.page || 1;
        status.zoom = status.zoom || 1;
        status.x = status.x || 0;
        status.y = status.y || 0;
        switcher.moveTo(status.page);
        zoomer.setZoom(status.zoom);
        zoomer.setScroll(status.x, status.y);
    }
    if (!document.getElementById("errtxt").innerText.trim())
        document.getElementById("error-warning").style.display = "none";

});
window.addEventListener("mouseup", () => saveStatus());<commit_msg>remove preview debug<commit_after>class Zoom {
    constructor() {}
    reset() {
        this.zoomUpperLimit = document.getElementById("zoomUpperLimit").innerText === "true";
        this.isWheelActionZoom = document.getElementById("wheelAction").innerText === "zoom";
        this.marginPixels = 20;
        this.img = document.getElementById("image");
        this.naturalWidth = this.img.naturalWidth;
        this.naturalHeight = this.img.naturalHeight;
        this.zoom = (window.innerWidth - this.marginPixels) / this.naturalWidth * 100;
        if (this.zoom > 100) this.zoom = 100;
        this.img.style.width = "";
        this.img.style.maxWidth = "";
        document.body.style.width = "";
        if (document.body.offsetHeight < window.innerHeight) document.body.style.height = window.innerHeight - this.marginPixels + "px";
    }
    smoothZomm(to, callback, ...args) {
        let winWidth = window.innerWidth;
        let contentWidth = winWidth - this.marginPixels
        let minWidth = contentWidth < this.naturalWidth ? contentWidth : this.naturalWidth;
        let minZoom = parseInt(minWidth / this.naturalWidth * 100);
        if (to < minZoom) to = minZoom - 1;
        let from = this.zoom;
        const interval = 10;
        const level = 10;
        const delta = (to - from) / level;
        for (let i = 1; i <= level; i++) {
            setTimeout(() => {
                this.setZoom(from + delta * i);
                callback(...args);
            }, interval * i);
        }
    }
    setZoom(zoom) {
        let winWidth = window.innerWidth;
        let contentWidth = winWidth - this.marginPixels
        let minWidth = contentWidth < this.naturalWidth ? contentWidth : this.naturalWidth;
        let minZoom = parseInt(minWidth / this.naturalWidth * 100);
        const maxZoom = 100;

        if (this.zoomUpperLimit && zoom > maxZoom) zoom = maxZoom;
        if (zoom < minZoom || (minZoom == maxZoom && this.zoomUpperLimit)) {
            this.img.style.width = "";
            this.img.style.maxWidth = "";
            document.body.style.width = "";
            zoom = minZoom;
        } else {
            let imgWidth = parseInt(this.naturalWidth * zoom / 100);
            this.img.style.width = imgWidth + 'px';
            let body = document.body;
            let bodyWidth = imgWidth + this.marginPixels < winWidth ? winWidth : imgWidth + this.marginPixels;
            body.style.width = bodyWidth + 'px'
            if (body.offsetHeight < window.innerHeight) body.style.height = window.innerHeight - this.marginPixels + "px";
        }
        this.zoom = zoom;
    }
    setScroll(left, top) {
        document.body.scrollLeft = left;
        document.body.scrollTop = top;
    }
    add() {
        let afterZoom = mouseAt => {
            this.followMousePointer(mouseAt);
            this.setToggleIcon();
            saveStatus();
        }
        this.reset();
        this.img.addEventListener("dblclick", () => {
            let mouseAt = this.getMousePointer();
            if (this.img.style.width)
                this.smoothZomm(0, afterZoom, mouseAt);
            else
                this.smoothZomm(100, afterZoom, mouseAt);
        })
        document.getElementById("btnZoomIn").addEventListener("click", () => {
            this.smoothZomm(this.zoom + 10, afterZoom, this.getImageCenterMousePointer());
        });
        document.getElementById("btnZoomOut").addEventListener("click", () => {
            this.smoothZomm(this.zoom - 10, afterZoom, this.getImageCenterMousePointer());
        });
        document.getElementById("btnZoomToggle").addEventListener("click", () => {
            if (this.img.style.width)
                this.smoothZomm(0, afterZoom, this.getImageCenterMousePointer());
            else
                this.smoothZomm(100, afterZoom, this.getImageCenterMousePointer());
        });
        document.body.addEventListener("mousewheel", () => {
            // console.log(event.ctrlKey, event.wheelDeltaX, event.wheelDeltaY);
            // scroll to zoom, or ctrl key pressed scroll
            if (this.isWheelActionZoom || event.ctrlKey) {
                // ctrlKey == true: pinch
                let delta = event.ctrlKey ? event.wheelDelta / 60 : event.wheelDelta / 12;
                let mouseAt = this.getMousePointer();
                if (this.zoomUpperLimit) {
                    this.setZoom(this.zoom + delta);
                } else {
                    // zoom level increase / decrease by 30% for each wheel scroll
                    this.setZoom(this.zoom * (delta / 50 + 1));
                }
                this.followMousePointer(mouseAt);
                this.setToggleIcon();
                saveStatus();
                if (event.preventDefault) event.preventDefault();
                return false;
            }
        });
        window.onresize = () => {
            let winWidth = window.innerWidth;
            let contentWidth = winWidth - this.marginPixels
            let minWidth = contentWidth < this.naturalWidth ? contentWidth : this.naturalWidth;
            let minZoom = parseInt(minWidth / this.naturalWidth * 100);

            if (this.img.style.width == "") {
                // console.log("update zoom value due to resize");
                this.zoom = minZoom;
            } else if (this.zoom < minZoom) {
                // console.log("change zoom to fit");
                this.zoom = minZoom;
                this.img.style.width = "";
                this.img.style.maxWidth = "";
                document.body.style.width = "";
            }
        };
    }
    followMousePointer(mouseAt) {
        let e = event || window.event;
        let imgWidth = parseInt(this.naturalWidth * this.zoom / 100);
        let imgHeight = parseInt(this.naturalHeight * this.zoom / 100);
        document.body.scrollLeft = parseInt(imgWidth * mouseAt.imageX + this.marginPixels / 2) - mouseAt.x;
        document.body.scrollTop = parseInt(imgHeight * mouseAt.imageY + this.marginPixels / 2) - mouseAt.y;
    }
    getMousePointer(x, y) {
        let imgWidth = parseInt(this.naturalWidth * this.zoom / 100);
        let imgHeight = parseInt(this.naturalHeight * this.zoom / 100);
        let e = event || window.event;
        let clientX = x || e.clientX
        let clientY = y || e.clientY
        let mouseAt = {
            x: clientX,
            y: clientY,
            imageX: (clientX + document.body.scrollLeft - this.marginPixels / 2) / imgWidth,
            imageY: (clientY + document.body.scrollTop - this.marginPixels / 2) / imgHeight,
        }
        return mouseAt;
    }
    getImageCenterMousePointer() {
        let ph = document.getElementById("placeholder");
        let x = (window.innerWidth - this.marginPixels) / 2;
        let y = (window.innerHeight - this.marginPixels - ph.clientHeight) / 2;
        return this.getMousePointer(x, y);
    }
    setToggleIcon() {
        let fit = document.getElementById("icon-fit");
        let expand = document.getElementById("icon-expand");
        if (this.img.style.width) {
            fit.style.display = "";
            expand.style.display = "none";
        } else {
            fit.style.display = "none";
            expand.style.display = "";
        }

    }
}
class Switcher {
    constructor() {
        this.current = 0;
        this.images = [];
        this.image = document.getElementById("image");
        this.pInfo = document.getElementById("pageInfo");
        this.pInfoTpl = this.pInfo.innerText;
        for (let e of document.getElementById("images").getElementsByTagName("img")) {
            this.images.push(e.src);
        }
    }
    add() {
        if (this.images.length <= 1) {
            // document.getElementById("placeholder").style.display = "none";
            document.getElementById("page-ctrls").style.display = "none";
            return;
        }
        document.getElementById("btnNext").addEventListener("click", () => {
            if (this.current == this.images.length) return;
            this.moveTo(++this.current);
            saveStatus();
        });
        document.getElementById("btnPrev").addEventListener("click", () => {
            if (this.current == 1) return;
            this.moveTo(--this.current);
            saveStatus();
        });

        this.moveTo(1);
        document.getElementById("images").remove();
        // console.log(this.images.length);
    }
    moveTo(page) {
        if (page < 1 || page > this.images.length) return;
        this.image.src = this.images[page - 1];
        this.pInfo.innerText = String.format(this.pInfoTpl, page, this.images.length);
        this.current = page;
        zoomer.reset();
    }
}
String.format = function format() {
    if (arguments.length == 0)
        return null;

    var str = arguments[0];
    for (var i = 1; i < arguments.length; i++) {
        var re = new RegExp('\\{' + (i - 1) + '\\}', 'gm');
        str = str.replace(re, arguments[i]);
    }
    return str;
}
let zoomer;
let switcher;
let sendStatus;

function saveStatus() {
    if (sendStatus) {
        let status = JSON.stringify({
            page: switcher.current,
            zoom: zoomer.zoom,
            x: document.body.scrollLeft,
            y: document.body.scrollTop
        });
        // console.log("save status: " + status);
        sendStatus.attributes["href"].value = 'command:plantuml.previewStatus?' + encodeURIComponent(status);
        sendStatus.click();
    }
}
window.addEventListener("load", () => {
    sendStatus = document.getElementById("sendStatus");
    zoomer = new Zoom();
    switcher = new Switcher();
    switcher.add();
    zoomer.add();
    let jsonStatus = document.getElementById("status").innerHTML;
    if (jsonStatus) {
        let status = {};
        try {
            status = JSON.parse(jsonStatus);
        } catch (error) {}
        status.page = status.page || 1;
        status.zoom = status.zoom || 1;
        status.x = status.x || 0;
        status.y = status.y || 0;
        switcher.moveTo(status.page);
        zoomer.setZoom(status.zoom);
        zoomer.setScroll(status.x, status.y);
    }
    if (!document.getElementById("errtxt").innerText.trim())
        document.getElementById("error-warning").style.display = "none";

});
window.addEventListener("mouseup", () => saveStatus());<|endoftext|>


<commit_before>'use strict';

var utils = require('../shared/utils');

var errors = require('./errors');
var log = require('debug')('pouchdb:socket');
var Socket = require('engine.io-client');
var blobUtil = require('blob-util');
var isBrowser = typeof process === 'undefined' || process.browser;
var buffer = require('../shared/buffer');
var instances = {};

function preprocessAttachments(doc) {
  if (!doc._attachments || !Object.keys(doc._attachments)) {
    return utils.Promise.resolve();
  }

  return utils.Promise.all(Object.keys(doc._attachments).map(function (key) {
    var attachment = doc._attachments[key];
    if (attachment.data && typeof attachment.data !== 'string') {
      if (isBrowser) {
        return new utils.Promise(function (resolve) {
          utils.readAsBinaryString(attachment.data, function (binary) {
            attachment.data = utils.btoa(binary);
            resolve();
          });
        });
      } else {
        attachment.data = attachment.data.toString('base64');
      }
    }
  }));
}

// Implements the PouchDB API for dealing with CouchDB instances over WS
function SocketPouch(opts, callback) {
  var api = this;

  if (typeof opts === 'string') {
    var slashIdx = utils.lastIndexOf(opts, '/');
    opts = {
      url: opts.substring(0, slashIdx),
      name: opts.substring(slashIdx + 1)
    };
  } else {
    opts = utils.clone(opts);
  }

  log('constructor called', opts);

  if (!opts.url || !opts.name) {
    var optsErrMessage = 'Error: you must provide a web socket ' +
      'url and database name.';
    console.log(optsErrMessage);
    return callback(new Error(optsErrMessage));
  }

  var socket = api._socket = new Socket(opts.url);
  socket.binaryType = 'blob';
  api._callbacks = {};
  api._name = opts.originalName;
  instances['$' + opts.originalName] = api;

  function sendMessage(type, args, callback) {
    var messageId = btoa(Math.random().toString());
    api._callbacks[messageId] = callback;
    socket.send(type + ':' + messageId + ':' + JSON.stringify(args));
  }

  socket.once('open', function () {
    sendMessage('createDatabase', [api._name], function (err) {
      if (err) {
        return callback(err);
      }
      callback(null, api);
    });
    socket.once('close', function(err) {
      console.log('closed', err);
    });
  });

  socket.on('message', function (res) {
    var split = utils.parseMessage(res, 3);
    var messageId = split[0];
    var error = split[1] === '0';
    var content = split[2];
    var cb = api._callbacks[messageId];
    delete api._callbacks[messageId];
    log('message', messageId, error, content);
    if (error) {
      cb(JSON.parse(content));
    } else {
      cb(null, JSON.parse(content));
    }
  });

  socket.once('error', function (err) {
    callback(err);
  });

  api.type = function () {
    return 'http';
  };

  api._id = utils.adapterFun('id', function (callback) {
    sendMessage('id', [], callback);
  });

  api.compact = utils.adapterFun('compact', function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    sendMessage('compact', [opts], callback);
  });

  api._info = function (callback) {
    sendMessage('info', [], callback);
  };

  api.get = utils.adapterFun('get', function (id, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    sendMessage('get', [id, opts], callback);
  });

  api.remove =
    utils.adapterFun('remove', function (docOrId, optsOrRev, opts, callback) {
      var doc;
      if (typeof optsOrRev === 'string') {
        // id, rev, opts, callback style
        doc = {
          _id: docOrId,
          _rev: optsOrRev
        };
        if (typeof opts === 'function') {
          callback = opts;
          opts = {};
        }
      } else {
        // doc, opts, callback style
        doc = docOrId;
        if (typeof optsOrRev === 'function') {
          callback = optsOrRev;
          opts = {};
        } else {
          callback = opts;
          opts = optsOrRev;
        }
      }
      var rev = (doc._rev || opts.rev);

      sendMessage('remove', [doc._id, rev], callback);
  });

  api.getAttachment =
    utils.adapterFun('getAttachment', function (docId, attachmentId, opts,
                                                callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
      sendMessage('getAttachment', [docId, attachmentId, opts], callback);
  });

  api.removeAttachment =
    utils.adapterFun('removeAttachment', function (docId, attachmentId, rev,
                                                   callback) {

      sendMessage('removeAttachment', [docId, attachmentId, rev], callback);
    });

  // Add the attachment given by blob and its contentType property
  // to the document with the given id, the revision given by rev, and
  // add it to the database given by host.
  api.putAttachment =
    utils.adapterFun('putAttachment', function (docId, attachmentId, rev, blob,
                                                type, callback) {
      if (typeof type === 'function') {
        callback = type;
        type = blob;
        blob = rev;
        rev = null;
      }
      if (typeof type === 'undefined') {
        type = blob;
        blob = rev;
        rev = null;
      }

      if (typeof blob === 'string') {
        var binary;
        try {
          binary = utils.atob(blob);
        } catch (err) {
          // it's not base64-encoded, so throw error
          return callback(errors.error(errors.BAD_ARG,
            'Attachments need to be base64 encoded'));
        }
        if (isBrowser) {
          blob = utils.createBlob([utils.fixBinary(binary)], {type: type});
        } else {
          blob = binary ? new buffer(binary, 'binary') : '';
        }
      }

      // TODO: don't use base64
      if (isBrowser) {
        blobUtil.blobToBase64String(blob).then(function (b64) {
          sendMessage('putAttachment',
            [docId, attachmentId, rev, b64, type], callback);
        }).catch(callback);
      } else {
        sendMessage('putAttachment',
          [docId, attachmentId, rev, blob.toString('base64'), type], callback);
      }

    });

  api.put = utils.adapterFun('put', utils.getArguments(function (args) {
    var temp, temptype, opts;
    var doc = args.shift();
    var id = '_id' in doc;
    var callback = args.pop();
    if (typeof doc !== 'object' || Array.isArray(doc)) {
      return callback(errors.error(errors.NOT_AN_OBJECT));
    }

    doc = utils.clone(doc);

    preprocessAttachments(doc).then(function () {
      while (true) {
        temp = args.shift();
        temptype = typeof temp;
        if (temptype === "string" && !id) {
          doc._id = temp;
          id = true;
        } else if (temptype === "string" && id && !('_rev' in doc)) {
          doc._rev = temp;
        } else if (temptype === "object") {
          opts = utils.clone(temp);
        }
        if (!args.length) {
          break;
        }
      }
      opts = opts || {};

      sendMessage('put', [doc, opts], callback);
    }).catch(callback);

  }));

  api.post = utils.adapterFun('post', function (doc, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    opts = utils.clone(opts);

    sendMessage('post', [doc, opts], callback);
  });

  api._bulkDocs = function (req, opts, callback) {
    sendMessage('bulkDocs', [req, opts], callback);
  };

  api.allDocs = utils.adapterFun('allDocs', function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    sendMessage('allDocs', [opts], callback);
  });

  api._changes = function (opts) {
    opts = utils.clone(opts);

    sendMessage('changes', [opts], callback);
  };

  // Given a set of document/revision IDs (given by req), tets the subset of
  // those that do NOT correspond to revisions stored in the database.
  // See http://wiki.apache.org/couchdb/HttpPostRevsDiff
  api.revsDiff = utils.adapterFun('revsDiff', function (req, opts, callback) {
    // If no options were given, set the callback to be the second parameter
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }

    sendMessage('revsDiff', [req, opts], callback);
  });

  api._close = function (callback) {
    callback();
  };

  api.destroy = utils.adapterFun('destroy', function (callback) {
    sendMessage('destroy', [], function (err, res) {
      delete instances['$' + name];
      if (err) {
        return callback(err);
      }
      callback(null, res);
    });
  });
}

// Delete the SocketPouch specified by the given name.
SocketPouch.destroy = utils.toPromise(function (name, opts, callback) {
  opts = opts || {};
  if (typeof opts === 'function') {
    callback = opts;
  }
  var instance = instances['$' + name];
  if (instance) {
    instance.destroy(callback);
  } else {
    callback(null, {ok: true});
  }
});

// SocketPouch is a valid adapter.
SocketPouch.valid = function () {
  return true;
};

module.exports = SocketPouch;

/* istanbul ignore next */
if (typeof window !== 'undefined' && window.PouchDB) {
  window.PouchDB.adapter('socket', module.exports);
}
<commit_msg>proper close<commit_after>'use strict';

var utils = require('../shared/utils');

var errors = require('./errors');
var log = require('debug')('pouchdb:socket');
var Socket = require('engine.io-client');
var blobUtil = require('blob-util');
var isBrowser = typeof process === 'undefined' || process.browser;
var buffer = require('../shared/buffer');
var instances = {};

function preprocessAttachments(doc) {
  if (!doc._attachments || !Object.keys(doc._attachments)) {
    return utils.Promise.resolve();
  }

  return utils.Promise.all(Object.keys(doc._attachments).map(function (key) {
    var attachment = doc._attachments[key];
    if (attachment.data && typeof attachment.data !== 'string') {
      if (isBrowser) {
        return new utils.Promise(function (resolve) {
          utils.readAsBinaryString(attachment.data, function (binary) {
            attachment.data = utils.btoa(binary);
            resolve();
          });
        });
      } else {
        attachment.data = attachment.data.toString('base64');
      }
    }
  }));
}

// Implements the PouchDB API for dealing with CouchDB instances over WS
function SocketPouch(opts, callback) {
  var api = this;

  if (typeof opts === 'string') {
    var slashIdx = utils.lastIndexOf(opts, '/');
    opts = {
      url: opts.substring(0, slashIdx),
      name: opts.substring(slashIdx + 1)
    };
  } else {
    opts = utils.clone(opts);
  }

  log('constructor called', opts);

  if (!opts.url || !opts.name) {
    var optsErrMessage = 'Error: you must provide a web socket ' +
      'url and database name.';
    console.log(optsErrMessage);
    return callback(new Error(optsErrMessage));
  }

  var socket = api._socket = new Socket(opts.url);
  socket.binaryType = 'blob';
  api._callbacks = {};
  api._name = opts.originalName;
  instances['$' + opts.originalName] = api;

  function sendMessage(type, args, callback) {
    var messageId = btoa(Math.random().toString());
    api._callbacks[messageId] = callback;
    socket.send(type + ':' + messageId + ':' + JSON.stringify(args));
  }

  socket.once('open', function () {
    sendMessage('createDatabase', [api._name], function (err) {
      if (err) {
        return callback(err);
      }
      callback(null, api);
    });
  });

  socket.on('message', function (res) {
    var split = utils.parseMessage(res, 3);
    var messageId = split[0];
    var error = split[1] === '0';
    var content = split[2];
    var cb = api._callbacks[messageId];
    delete api._callbacks[messageId];
    log('message', messageId, error, content);
    if (error) {
      cb(JSON.parse(content));
    } else {
      cb(null, JSON.parse(content));
    }
  });

  socket.once('error', function (err) {
    callback(err);
  });

  api.type = function () {
    return 'http';
  };

  api._id = utils.adapterFun('id', function (callback) {
    sendMessage('id', [], callback);
  });

  api.compact = utils.adapterFun('compact', function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    sendMessage('compact', [opts], callback);
  });

  api._info = function (callback) {
    sendMessage('info', [], callback);
  };

  api.get = utils.adapterFun('get', function (id, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    sendMessage('get', [id, opts], callback);
  });

  api.remove =
    utils.adapterFun('remove', function (docOrId, optsOrRev, opts, callback) {
      var doc;
      if (typeof optsOrRev === 'string') {
        // id, rev, opts, callback style
        doc = {
          _id: docOrId,
          _rev: optsOrRev
        };
        if (typeof opts === 'function') {
          callback = opts;
          opts = {};
        }
      } else {
        // doc, opts, callback style
        doc = docOrId;
        if (typeof optsOrRev === 'function') {
          callback = optsOrRev;
          opts = {};
        } else {
          callback = opts;
          opts = optsOrRev;
        }
      }
      var rev = (doc._rev || opts.rev);

      sendMessage('remove', [doc._id, rev], callback);
  });

  api.getAttachment =
    utils.adapterFun('getAttachment', function (docId, attachmentId, opts,
                                                callback) {
      if (typeof opts === 'function') {
        callback = opts;
        opts = {};
      }
      sendMessage('getAttachment', [docId, attachmentId, opts], callback);
  });

  api.removeAttachment =
    utils.adapterFun('removeAttachment', function (docId, attachmentId, rev,
                                                   callback) {

      sendMessage('removeAttachment', [docId, attachmentId, rev], callback);
    });

  // Add the attachment given by blob and its contentType property
  // to the document with the given id, the revision given by rev, and
  // add it to the database given by host.
  api.putAttachment =
    utils.adapterFun('putAttachment', function (docId, attachmentId, rev, blob,
                                                type, callback) {
      if (typeof type === 'function') {
        callback = type;
        type = blob;
        blob = rev;
        rev = null;
      }
      if (typeof type === 'undefined') {
        type = blob;
        blob = rev;
        rev = null;
      }

      if (typeof blob === 'string') {
        var binary;
        try {
          binary = utils.atob(blob);
        } catch (err) {
          // it's not base64-encoded, so throw error
          return callback(errors.error(errors.BAD_ARG,
            'Attachments need to be base64 encoded'));
        }
        if (isBrowser) {
          blob = utils.createBlob([utils.fixBinary(binary)], {type: type});
        } else {
          blob = binary ? new buffer(binary, 'binary') : '';
        }
      }

      // TODO: don't use base64
      if (isBrowser) {
        blobUtil.blobToBase64String(blob).then(function (b64) {
          sendMessage('putAttachment',
            [docId, attachmentId, rev, b64, type], callback);
        }).catch(callback);
      } else {
        sendMessage('putAttachment',
          [docId, attachmentId, rev, blob.toString('base64'), type], callback);
      }

    });

  api.put = utils.adapterFun('put', utils.getArguments(function (args) {
    var temp, temptype, opts;
    var doc = args.shift();
    var id = '_id' in doc;
    var callback = args.pop();
    if (typeof doc !== 'object' || Array.isArray(doc)) {
      return callback(errors.error(errors.NOT_AN_OBJECT));
    }

    doc = utils.clone(doc);

    preprocessAttachments(doc).then(function () {
      while (true) {
        temp = args.shift();
        temptype = typeof temp;
        if (temptype === "string" && !id) {
          doc._id = temp;
          id = true;
        } else if (temptype === "string" && id && !('_rev' in doc)) {
          doc._rev = temp;
        } else if (temptype === "object") {
          opts = utils.clone(temp);
        }
        if (!args.length) {
          break;
        }
      }
      opts = opts || {};

      sendMessage('put', [doc, opts], callback);
    }).catch(callback);

  }));

  api.post = utils.adapterFun('post', function (doc, opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    opts = utils.clone(opts);

    sendMessage('post', [doc, opts], callback);
  });

  api._bulkDocs = function (req, opts, callback) {
    sendMessage('bulkDocs', [req, opts], callback);
  };

  api.allDocs = utils.adapterFun('allDocs', function (opts, callback) {
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    sendMessage('allDocs', [opts], callback);
  });

  api._changes = function (opts) {
    opts = utils.clone(opts);

    sendMessage('changes', [opts], callback);
  };

  // Given a set of document/revision IDs (given by req), tets the subset of
  // those that do NOT correspond to revisions stored in the database.
  // See http://wiki.apache.org/couchdb/HttpPostRevsDiff
  api.revsDiff = utils.adapterFun('revsDiff', function (req, opts, callback) {
    // If no options were given, set the callback to be the second parameter
    if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }

    sendMessage('revsDiff', [req, opts], callback);
  });

  api._close = function (callback) {
    callback();
  };

  api.destroy = utils.adapterFun('destroy', function (callback) {
    sendMessage('destroy', [], function (err, res) {
      delete instances['$' + name];
      if (err) {
        return callback(err);
      }
      socket.once('close', function(msg) {
        log('closed', msg);
        callback(null, res);
      });
      socket.close();
    });
  });
}

// Delete the SocketPouch specified by the given name.
SocketPouch.destroy = utils.toPromise(function (name, opts, callback) {
  opts = opts || {};
  if (typeof opts === 'function') {
    callback = opts;
  }
  var instance = instances['$' + name];
  if (instance) {
    instance.destroy(callback);
  } else {
    callback(null, {ok: true});
  }
});

// SocketPouch is a valid adapter.
SocketPouch.valid = function () {
  return true;
};

module.exports = SocketPouch;

/* istanbul ignore next */
if (typeof window !== 'undefined' && window.PouchDB) {
  window.PouchDB.adapter('socket', module.exports);
}
<|endoftext|>