<commit_before>(function(window) {
    "use strict";

    //
    // Constants
    //
    var REFRESH_CRUMB_INTERVAL = 5 * 1000 * 60;

    function fetchUrl(url, cb) {
        var xhr = new xhrFn();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4)
            {
                if (cb) {
                    cb(xhr);
                }
            }
        };

        xhr.open("get", url, true);
        xhr.send();
    }

    function setImm(fn) {
        if (typeof process !== "undefined" &&
            typeof process.nextTick === "function") {
            process.nextTick(fn);
        } else if (typeof window !== "undefined") {
            if (window.setImmediate) {
                window.setImmediate(fn);
            } else if (window.msSetImmediate) {
                window.msSetImmediate(fn);
            } else if (window.webkitSetImmediate) {
                window.webkitSetImmediate(fn);
            } else if (window.mozSetImmediate) {
                window.mozSetImmediate(fn);
            } else {
                setTimeout(fn, 10);
            }
        } else {
            setTimeout(fn, 10);
        }
    }

    var xhrFn;
    if (typeof XMLHttpRequest === "function") {
        xhrFn = XMLHttpRequest;
    } else if (typeof XMLHttpRequest !== "function" &&
        typeof require === "function") {
        xhrFn = require("xmlhttprequest").XMLHttpRequest;
    }

    var now = false;
    var nowOffset = +(new Date());
    if (typeof window !== "undefined") {
        if (window.performance && typeof window.performance.now === "function") {
            now = function() { return window.performance.now(); }
        } else if (window.performance) {
            methods = ["webkitNow", "msNow", "mozNow"];

            var foundNow = false;
            for (i = 0; i < methods.length; i++) {
                if (typeof window.performance[methods[i]] === "function") {
                    now = window.performance[methods[i]];
                    break;
                }
            }
        }
    }

    if (!now) {
        if (typeof window !== "undefined" &&
            window.performance &&
            window.performance.timing &&
            window.performance.timing.navigationStart) {
            nowOffset = window.performance.timing.navigationStart;
        }

        // No browser support, fall back to Date.now
        if (Date.now) {
            now = function() {
                return Date.now() - nowOffset;
            };
        } else {
            // no Date.now support, get the time from new Date()
            now = function() {
                return +(new Date()) - nowOffset;
            };
        }
    }

    function createApp(key, options) {
        options = options || {};

        //
        // Private Functions
        //
        function ensureUrlPrefix(url) {
            if (forceSSL) {
                url = "https:" + url;
            } else if (typeof window === "undefined") {
                // NodeJS
                if (url.indexOf("http:") === -1) {
                    url = "http:" + url;
                }
            } else if (typeof window !== "undefined" && window.location.protocol === "file:") {
                // NodeJS
                if (url.indexOf("http:") === -1) {
                    url = "http:" + url;
                }
            }

            return url;
        }

        function getConfigUrl() {
            var url = configUrl;
            if (url.indexOf("?") !== -1) {
                url += "&";
            } else {
                url += "?";
            }
            url += "key=" + apiKey

            return ensureUrlPrefix(url);
        }

        function getBeaconUrl() {
            return ensureUrlPrefix(configJson.beacon_url + "?");
        }

        function parseConfig(e) {
            console.log("Config.json response: " + e.responseText);

            try {
                var newConfigJson = JSON.parse(e.responseText);

                // merge in updates
                for (var key in newConfigJson) {
                    if (newConfigJson.hasOwnProperty(key)) {
                        configJson[key] = newConfigJson[key];
                    }
                }
            } catch (e) {
                return;
            }

            if (configJson.PageParams) {
                // parse custom metrics
                var cms = configJson.PageParams.customMetrics;
                var cts = configJson.PageParams.customTimers;
                var cds = configJson.PageParams.customDimensions;

                if (cms) {
                    for (var i = 0; i < cms.length; i++) {
                        var m = cms[i];
                        metricDefs[m.name] = m.label;
                    }
                }

                // timers
                if (cts) {
                    for (var i = 0; i < cts.length; i++) {
                        var t = cts[i];
                        timerDefs[t.name] = t.label;
                    }
                }

                // dimensions
                if (cds) {
                    for (var i = 0; i < cds.length; i++) {
                        var t = cds[i];
                        dimensionDefs[t.name] = t.label;
                    }
                }
            }

            // we're ready to send beacons
            initialized = true;

            // refresh the config after 5 minutes
            configJsonRefresh = true;
            setTimeout(fetchConfig.bind(this), REFRESH_CRUMB_INTERVAL);

            setImm(processQueue);
        }

        function fetchConfig() {
            var url = getConfigUrl();

            // if we've already fetched it once, add an empty refresh crumb parameter
            if (configJsonRefresh) {
                url += "&r=";
            }

            console.log("Getting config.json: " + url);
            fetchUrl(url, parseConfig.bind(this));
        }

        function currentDimensions() {
            var copy = {};

            for (var dimName in dimensions) {
                if(dimensions.hasOwnProperty(dimName)) {
                    copy[dimName] = dimensions[dimName];
                }
            }

            return copy;
        }

        function addToQueue(type, name, value) {
            beaconQueue.push({
                type: type,
                name: name,
                value: value,
                group: group,
                dimensions: currentDimensions()
            })
        }

        function processQueue() {
            if (beaconQueue.length === 0) {
                return;
            }

            if (!initialized) {
                console.log("Not yet initialized, trying again in 5 seconds...");

                // try again in 5 seconds
                setTimeout(processQueue, 5000);
                return;
            }

            var q = beaconQueue[0];

            var type = q.type;
            var name = q.name;
            var val  = q.value;

            var data = {};

            // page group
            if (typeof q.group !== "boolean") {
                data["h.pg"] = q.group;
            }

            // dimensions
            for (var dimName in q.dimensions) {
                if (q.dimensions.hasOwnProperty(dimName)) {
                    if (typeof dimensionDefs[dimName] !== "undefined") {
                        data[dimensionDefs[dimName]] = q.dimensions[dimName];
                    }
                }
            }

            if (type === "metric") {
                if (typeof metricDefs[name] !== "undefined") {
                    data[metricDefs[name]] = val;
                    sendBeacon(data);
                }
            } else if (type === "timer") {
                if (typeof timerDefs[name] !== "undefined") {
                    data["t_other"] = timerDefs[name] + "|" + val;
                    sendBeacon(data);
                }
            }

            // remove the first item from the queue
            beaconQueue.shift();

            // and run again soon until it's empty
            setImm(processQueue);
        }

        function currentSessionId() {
            return overriddenSessionId ? overriddenSessionId : configJson["session_id"];
        }

        function sendBeacon(params) {
            // TODO: do we expect consumers to send this?
            sessionLength++;

            params["d"] = configJson["site_domain"];
            params["h.key"] = configJson["h.key"];
            params["h.d"] = configJson["h.d"];
            params["h.cr"] = configJson["h.cr"];
            params["h.t"] = configJson["h.t"];
            params["rt.si"] = currentSessionId();
            params["rt.ss"] = sessionStart;
            params["rt.sl"] = sessionLength;
            params["http.initiator"] = "api";

            // TODO
            params["api"] = 1;

            // TODO remove?
            params["v"] = 1;
            params["u"] = "http://" + configJson["site_domain"];
            params["t_done"] = 0;

            var paramsArray = [];
            for (var name in params) {
                if (params.hasOwnProperty(name)) {
                    paramsArray.push(encodeURIComponent(name)
                        + "="
                        + (
                            params[name] === undefined || params[name] === null
                            ? ""
                            : encodeURIComponent(params[name])
                        )
                    );
                }
            }

            var baseUrl = getBeaconUrl();
            var url = baseUrl + ((baseUrl.indexOf("?") > -1) ? "&" : "?") + paramsArray.join("&");

            console.log("Sending beacon: " + url);

            fetchUrl(url, function(e) {
                console.log("Sent!");
            });
        }

        //
        // Private members
        //

        // API key
        var apiKey = key;

        // configuration URL
        var configUrl = "//c.go-mpulse.net/api/config.json";

        // whether or not to force SSL
        var forceSSL = false;

        // config.js data
        var configJson = {};
        var configJsonRefresh = false;
        var initialized = false;

        // beacon queue
        var beaconQueue = [];

        // dimensions
        var group = false;
        var dimensions = {};
        var dimensionDefs = {};

        var overriddenSessionId = false;

        // metrics
        var metricDefs = {};

        // timers
        var timers = {};
        var timerDefs = {};
        var currentTimerId = -1;

        // session start
        var sessionStart = now();
        var sessionLength = 0;

        //
        // Initialization
        //

        // parse input options
        if (options.configUrl) {
            configUrl = options.configUrl;
        }

        if (options.forceSSL) {
            forceSSL = true;
        }

        // fetch the config
        fetchConfig();

        //
        // Exports
        //
        var exports = {
            startTimer: function(name) {
                currentTimerId++;
                timers[currentTimerId] = {
                    time: now(),
                    name: name
                };

                return currentTimerId;
            },
            stopTimer: function(id) {
                var timer = timers[id];
                if (timer) {
                    this.sendTimer(timer.name, now() - timer.time);
                }
            },
            sendTimer: function(name, value) {
                console.log("sendTimer()");
                addToQueue('timer', name, value);
                setImm(processQueue);
            },
            sendMetric: function(name, value) {
                console.log("sendMetric()");
                addToQueue('metric', name, value);
                setImm(processQueue);
            },
            setViewGroup: function(name) {
                group = name;
            },
            clearViewGroup: function() {
                group = false;
            },
            setDimension: function(name, value) {
                dimensions[name] = value;
            },
            clearDimension: function(name) {
                if (typeof dimensions[name] !== undefined) {
                    delete dimensions[name];
                }
            },
            setSessionID: function(id) {
                overriddenSessionId = id;
            },
            getSessionID: function() {
                return currentSessionId();
            }
        }

        return exports;
    }

    //
    // Static private members
    //
    var defaultApp = false;
    var apps = {};

    //
    // Initialization
    //

    // save old ResourceTimingCompression object for noConflict()
    var root;
    var previousObj;
    if (typeof window !== "undefined") {
        root = window;
        previousObj = root.mPulse;
    }

    //
    // Exports
    //
    var mPulse = {
        /**
         * Changes the value of mPulse back to its original value, returning
         * a reference to the mPulse object.
         */
        noConflict: function() {
            root.mPulse = previousObj;
            return mPulse;
        },
        init: function(apiKey, options) {
            var app = createApp(apiKey, options);

            if (defaultApp === false) {
                defaultApp = app;
            }

            if (typeof options.name !== undefined) {
                apps[options.name] = app;
            }

            return app;
        },
        startTimer: function(name) {
            if (defaultApp !== false) {
                defaultApp.startTimer(name);
            }
        },
        stopTimer: function(id) {
            if (defaultApp !== false) {
                defaultApp.stopTimer(id);
            }
        },
        sendTimer: function(name, value) {
            if (defaultApp !== false) {
                defaultApp.sendTimer(name, value);
            }
        },
        sendMetric: function(name, value) {
            if (defaultApp !== false) {
                defaultApp.sendMetric(name, value);
            }
        },
        setViewGroup: function(name) {
            if (defaultApp !== false) {
                defaultApp.setViewGroup(name);
            }
        },
        clearViewGroup: function() {
            if (defaultApp !== false) {
                defaultApp.clearViewGroup();
            }
        },
        setDimension: function(name, value) {
            if (defaultApp !== false) {
                defaultApp.setDimension(name, value);
            }
        },
        clearDimension: function(name) {
            if (defaultApp !== false) {
                defaultApp.clearDimension(name);
            }
        },
        setSessionID: function(id) {
            if (defaultApp !== false) {
                defaultApp.setSessionID(id);
            }
        },
        getSessionID: function() {
            if (defaultApp !== false) {
                return defaultApp.getSessionID();
            }
        }
    }

    //
    // Export to the appropriate location
    //
    if (typeof define === "function" && define.amd) {
        //
        // AMD / RequireJS
        //
        define([], function () {
            return mPulse;
        });
    } else if (typeof module !== "undefined" && module.exports) {
        //
        // Node.js
        //
        module.exports = mPulse;
    } else if (typeof root !== "undefined") {
        //
        // Browser Global
        //
        root.mPulse = mPulse;
    }
}(typeof window !== "undefined" ? window : undefined));
<commit_msg>s/clear/reset/<commit_after>(function(window) {
    "use strict";

    //
    // Constants
    //
    var REFRESH_CRUMB_INTERVAL = 5 * 1000 * 60;

    function fetchUrl(url, cb) {
        var xhr = new xhrFn();
        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4)
            {
                if (cb) {
                    cb(xhr);
                }
            }
        };

        xhr.open("get", url, true);
        xhr.send();
    }

    function setImm(fn) {
        if (typeof process !== "undefined" &&
            typeof process.nextTick === "function") {
            process.nextTick(fn);
        } else if (typeof window !== "undefined") {
            if (window.setImmediate) {
                window.setImmediate(fn);
            } else if (window.msSetImmediate) {
                window.msSetImmediate(fn);
            } else if (window.webkitSetImmediate) {
                window.webkitSetImmediate(fn);
            } else if (window.mozSetImmediate) {
                window.mozSetImmediate(fn);
            } else {
                setTimeout(fn, 10);
            }
        } else {
            setTimeout(fn, 10);
        }
    }

    var xhrFn;
    if (typeof XMLHttpRequest === "function") {
        xhrFn = XMLHttpRequest;
    } else if (typeof XMLHttpRequest !== "function" &&
        typeof require === "function") {
        xhrFn = require("xmlhttprequest").XMLHttpRequest;
    }

    var now = false;
    var nowOffset = +(new Date());
    if (typeof window !== "undefined") {
        if (window.performance && typeof window.performance.now === "function") {
            now = function() { return window.performance.now(); }
        } else if (window.performance) {
            methods = ["webkitNow", "msNow", "mozNow"];

            var foundNow = false;
            for (i = 0; i < methods.length; i++) {
                if (typeof window.performance[methods[i]] === "function") {
                    now = window.performance[methods[i]];
                    break;
                }
            }
        }
    }

    if (!now) {
        if (typeof window !== "undefined" &&
            window.performance &&
            window.performance.timing &&
            window.performance.timing.navigationStart) {
            nowOffset = window.performance.timing.navigationStart;
        }

        // No browser support, fall back to Date.now
        if (Date.now) {
            now = function() {
                return Date.now() - nowOffset;
            };
        } else {
            // no Date.now support, get the time from new Date()
            now = function() {
                return +(new Date()) - nowOffset;
            };
        }
    }

    function createApp(key, options) {
        options = options || {};

        //
        // Private Functions
        //
        function ensureUrlPrefix(url) {
            if (forceSSL) {
                url = "https:" + url;
            } else if (typeof window === "undefined") {
                // NodeJS
                if (url.indexOf("http:") === -1) {
                    url = "http:" + url;
                }
            } else if (typeof window !== "undefined" && window.location.protocol === "file:") {
                // NodeJS
                if (url.indexOf("http:") === -1) {
                    url = "http:" + url;
                }
            }

            return url;
        }

        function getConfigUrl() {
            var url = configUrl;
            if (url.indexOf("?") !== -1) {
                url += "&";
            } else {
                url += "?";
            }
            url += "key=" + apiKey

            return ensureUrlPrefix(url);
        }

        function getBeaconUrl() {
            return ensureUrlPrefix(configJson.beacon_url + "?");
        }

        function parseConfig(e) {
            console.log("Config.json response: " + e.responseText);

            try {
                var newConfigJson = JSON.parse(e.responseText);

                // merge in updates
                for (var key in newConfigJson) {
                    if (newConfigJson.hasOwnProperty(key)) {
                        configJson[key] = newConfigJson[key];
                    }
                }
            } catch (e) {
                return;
            }

            if (configJson.PageParams) {
                // parse custom metrics
                var cms = configJson.PageParams.customMetrics;
                var cts = configJson.PageParams.customTimers;
                var cds = configJson.PageParams.customDimensions;

                if (cms) {
                    for (var i = 0; i < cms.length; i++) {
                        var m = cms[i];
                        metricDefs[m.name] = m.label;
                    }
                }

                // timers
                if (cts) {
                    for (var i = 0; i < cts.length; i++) {
                        var t = cts[i];
                        timerDefs[t.name] = t.label;
                    }
                }

                // dimensions
                if (cds) {
                    for (var i = 0; i < cds.length; i++) {
                        var t = cds[i];
                        dimensionDefs[t.name] = t.label;
                    }
                }
            }

            // we're ready to send beacons
            initialized = true;

            // refresh the config after 5 minutes
            configJsonRefresh = true;
            setTimeout(fetchConfig.bind(this), REFRESH_CRUMB_INTERVAL);

            setImm(processQueue);
        }

        function fetchConfig() {
            var url = getConfigUrl();

            // if we've already fetched it once, add an empty refresh crumb parameter
            if (configJsonRefresh) {
                url += "&r=";
            }

            console.log("Getting config.json: " + url);
            fetchUrl(url, parseConfig.bind(this));
        }

        function currentDimensions() {
            var copy = {};

            for (var dimName in dimensions) {
                if(dimensions.hasOwnProperty(dimName)) {
                    copy[dimName] = dimensions[dimName];
                }
            }

            return copy;
        }

        function addToQueue(type, name, value) {
            beaconQueue.push({
                type: type,
                name: name,
                value: value,
                group: group,
                dimensions: currentDimensions()
            })
        }

        function processQueue() {
            if (beaconQueue.length === 0) {
                return;
            }

            if (!initialized) {
                console.log("Not yet initialized, trying again in 5 seconds...");

                // try again in 5 seconds
                setTimeout(processQueue, 5000);
                return;
            }

            var q = beaconQueue[0];

            var type = q.type;
            var name = q.name;
            var val  = q.value;

            var data = {};

            // page group
            if (typeof q.group !== "boolean") {
                data["h.pg"] = q.group;
            }

            // dimensions
            for (var dimName in q.dimensions) {
                if (q.dimensions.hasOwnProperty(dimName)) {
                    if (typeof dimensionDefs[dimName] !== "undefined") {
                        data[dimensionDefs[dimName]] = q.dimensions[dimName];
                    }
                }
            }

            if (type === "metric") {
                if (typeof metricDefs[name] !== "undefined") {
                    data[metricDefs[name]] = val;
                    sendBeacon(data);
                }
            } else if (type === "timer") {
                if (typeof timerDefs[name] !== "undefined") {
                    data["t_other"] = timerDefs[name] + "|" + val;
                    sendBeacon(data);
                }
            }

            // remove the first item from the queue
            beaconQueue.shift();

            // and run again soon until it's empty
            setImm(processQueue);
        }

        function currentSessionId() {
            return overriddenSessionId ? overriddenSessionId : configJson["session_id"];
        }

        function sendBeacon(params) {
            // TODO: do we expect consumers to send this?
            sessionLength++;

            params["d"] = configJson["site_domain"];
            params["h.key"] = configJson["h.key"];
            params["h.d"] = configJson["h.d"];
            params["h.cr"] = configJson["h.cr"];
            params["h.t"] = configJson["h.t"];
            params["rt.si"] = currentSessionId();
            params["rt.ss"] = sessionStart;
            params["rt.sl"] = sessionLength;
            params["http.initiator"] = "api";

            // TODO
            params["api"] = 1;

            // TODO remove?
            params["v"] = 1;
            params["u"] = "http://" + configJson["site_domain"];
            params["t_done"] = 0;

            var paramsArray = [];
            for (var name in params) {
                if (params.hasOwnProperty(name)) {
                    paramsArray.push(encodeURIComponent(name)
                        + "="
                        + (
                            params[name] === undefined || params[name] === null
                            ? ""
                            : encodeURIComponent(params[name])
                        )
                    );
                }
            }

            var baseUrl = getBeaconUrl();
            var url = baseUrl + ((baseUrl.indexOf("?") > -1) ? "&" : "?") + paramsArray.join("&");

            console.log("Sending beacon: " + url);

            fetchUrl(url, function(e) {
                console.log("Sent!");
            });
        }

        //
        // Private members
        //

        // API key
        var apiKey = key;

        // configuration URL
        var configUrl = "//c.go-mpulse.net/api/config.json";

        // whether or not to force SSL
        var forceSSL = false;

        // config.js data
        var configJson = {};
        var configJsonRefresh = false;
        var initialized = false;

        // beacon queue
        var beaconQueue = [];

        // dimensions
        var group = false;
        var dimensions = {};
        var dimensionDefs = {};

        var overriddenSessionId = false;

        // metrics
        var metricDefs = {};

        // timers
        var timers = {};
        var timerDefs = {};
        var currentTimerId = -1;

        // session start
        var sessionStart = now();
        var sessionLength = 0;

        //
        // Initialization
        //

        // parse input options
        if (options.configUrl) {
            configUrl = options.configUrl;
        }

        if (options.forceSSL) {
            forceSSL = true;
        }

        // fetch the config
        fetchConfig();

        //
        // Exports
        //
        var exports = {
            startTimer: function(name) {
                currentTimerId++;
                timers[currentTimerId] = {
                    time: now(),
                    name: name
                };

                return currentTimerId;
            },
            stopTimer: function(id) {
                var timer = timers[id];
                if (timer) {
                    this.sendTimer(timer.name, now() - timer.time);
                }
            },
            sendTimer: function(name, value) {
                console.log("sendTimer()");
                addToQueue('timer', name, value);
                setImm(processQueue);
            },
            sendMetric: function(name, value) {
                console.log("sendMetric()");
                addToQueue('metric', name, value);
                setImm(processQueue);
            },
            setViewGroup: function(name) {
                group = name;
            },
            resetViewGroup: function() {
                group = false;
            },
            setDimension: function(name, value) {
                dimensions[name] = value;
            },
            resetDimension: function(name) {
                if (typeof dimensions[name] !== undefined) {
                    delete dimensions[name];
                }
            },
            setSessionID: function(id) {
                overriddenSessionId = id;
            },
            getSessionID: function() {
                return currentSessionId();
            }
        }

        return exports;
    }

    //
    // Static private members
    //
    var defaultApp = false;
    var apps = {};

    //
    // Initialization
    //

    // save old ResourceTimingCompression object for noConflict()
    var root;
    var previousObj;
    if (typeof window !== "undefined") {
        root = window;
        previousObj = root.mPulse;
    }

    //
    // Exports
    //
    var mPulse = {
        /**
         * Changes the value of mPulse back to its original value, returning
         * a reference to the mPulse object.
         */
        noConflict: function() {
            root.mPulse = previousObj;
            return mPulse;
        },
        init: function(apiKey, options) {
            var app = createApp(apiKey, options);

            if (defaultApp === false) {
                defaultApp = app;
            }

            if (typeof options.name !== undefined) {
                apps[options.name] = app;
            }

            return app;
        },
        startTimer: function(name) {
            if (defaultApp !== false) {
                defaultApp.startTimer(name);
            }
        },
        stopTimer: function(id) {
            if (defaultApp !== false) {
                defaultApp.stopTimer(id);
            }
        },
        sendTimer: function(name, value) {
            if (defaultApp !== false) {
                defaultApp.sendTimer(name, value);
            }
        },
        sendMetric: function(name, value) {
            if (defaultApp !== false) {
                defaultApp.sendMetric(name, value);
            }
        },
        setViewGroup: function(name) {
            if (defaultApp !== false) {
                defaultApp.setViewGroup(name);
            }
        },
        resetViewGroup: function() {
            if (defaultApp !== false) {
                defaultApp.resetViewGroup();
            }
        },
        setDimension: function(name, value) {
            if (defaultApp !== false) {
                defaultApp.setDimension(name, value);
            }
        },
        resetDimension: function(name) {
            if (defaultApp !== false) {
                defaultApp.resetDimension(name);
            }
        },
        setSessionID: function(id) {
            if (defaultApp !== false) {
                defaultApp.setSessionID(id);
            }
        },
        getSessionID: function() {
            if (defaultApp !== false) {
                return defaultApp.getSessionID();
            }
        }
    }

    //
    // Export to the appropriate location
    //
    if (typeof define === "function" && define.amd) {
        //
        // AMD / RequireJS
        //
        define([], function () {
            return mPulse;
        });
    } else if (typeof module !== "undefined" && module.exports) {
        //
        // Node.js
        //
        module.exports = mPulse;
    } else if (typeof root !== "undefined") {
        //
        // Browser Global
        //
        root.mPulse = mPulse;
    }
}(typeof window !== "undefined" ? window : undefined));
<|endoftext|>


<commit_before>/**
 * @file TagField component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import trim from 'lodash/trim';

import EditableField from '../EditableField';
import IconButton from '../IconButton';

import Util from '../_vendors/Util';
import Dom from '../_vendors/Dom';
import CharSize from '../_vendors/CharSize';
import Event from '../_vendors/Event';
import ComponentUtil from '../_vendors/ComponentUtil';

class TagField extends Component {

    static DEFAULT_SEPARATORS = [','];

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.inputSymbol = Symbol('input');

        this.state = {
            wrapperWidth: 'auto',
            data: props.data,
            inputValue: props.inputValue,
            inputIndex: props.data.length,
            itemEditing: false,
            editingItemIndex: -1
        };

    }

    getSeparators = (separators = this.props.separators) => {

        if (separators && separators.length > 0) {
            return separators;
        }

        return DEFAULT_SEPARATORS;

    };

    generateSeparatorReg = (separators = this.props.separators) => {
        const seps = this.getSeparators(separators);
        return new RegExp(`\\s*${seps.map(sep => `\\${sep}`).join('|')}\\s*`, 'g');
    };

    removeItem = index => {

        const {data} = this.state;

        if (!data || data.length < 1 || !(index in data)) {
            return;
        }

        data.splice(index, 1);

        this.setState({
            data
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(data);
        });

    };

    calInputIndex = e => {

        const wrapperEl = this.refs.wrapper,
            offset = Dom.getOffset(wrapperEl),
            {left: minX} = offset,
            wrapperWidth = wrapperEl.getBoundingClientRect().width,
            maxX = minX + wrapperWidth;

        let x = e.clientX,
            y = e.clientY,
            inputIndex = -1;

        while (x >= minX) {

            const item = document.elementFromPoint(x, y),
                wrapperEl = Dom.findParentByClassName(item, 'tag-field-item-wrapper');

            if (wrapperEl) {
                inputIndex = +wrapperEl.dataset.index + 1;
                break;
            }

            x--;

        }

        if (inputIndex < 0) {
            while (x <= maxX) {

                const item = document.elementFromPoint(x, y),
                    wrapperEl = Dom.findParentByClassName(item, 'tag-field-item-wrapper');

                if (wrapperEl) {
                    inputIndex = +wrapperEl.dataset.index;
                    break;
                }

                x++;

            }
        }

        return inputIndex < 0 ? this.state.data.length : inputIndex;

    };

    mouseDownHandler = e => {

        if (this.props.disabled || Dom.findParentByClassName(e.target, 'tag-field-item-wrapper')
            || Dom.hasClass(e.target, 'tag-field-input')) {
            return;
        }

        const callback = (function (e) {

            return () => {

                if (Dom.findParentByClassName(e.target, 'tag-field') != this.refs.wrapper || this.state.itemEditing) {
                    this.setState({
                        inputIndex: this.state.data.length
                    });
                    return;
                }

                let x = e.clientX,
                    y = e.clientY;

                if (!x || !y) {
                    return;
                }

                const wrapperEl = this.refs.wrapper,
                    offset = Dom.getOffset(wrapperEl);

                if (!offset) {
                    return;
                }

                const inputIndex = this.calInputIndex(e);

                this.setState({
                    inputIndex
                }, () => {
                    setTimeout(() => {
                        this.refs.input.focus();
                        // wrapperEl.scrollLeft = 0;
                        // wrapperEl.scrollTop = 0;
                    }, 0);
                });

            };

        }.bind(this))(e);

        if (this.state.inputValue) {
            this.insertInputValue(callback);
        } else {
            callback();
        }

    };

    inputChangeHandler = e => {

        if (this.props.disabled) {
            return;
        }

        const value = trim(e.target.value),
            inputValue = value ? value.replace(/\r?\n/gm, this.getSeparators()[0]) : value;

        this.setState({
            inputValue
        }, () => {

            const {onInputChange} = this.props,
                width = CharSize.calculateStringWidth(inputValue, this.refs.test),
                inputWrapperEl = this.refs.inputWrapper,
                inputEl = this.refs.input;

            inputWrapperEl && (inputWrapperEl.style.width = `${width + 9}px`);
            inputEl && (inputEl.style.height = Math.max(inputEl.scrollHeight, 24) + 'px');
            onInputChange && onInputChange(inputValue);

        });

    };

    inputKeyDownHandler = e => {

        if (this.props.disabled) {
            return;
        }

        if (e.keyCode === 13) {
            this.insertInputValue();
        }

    };

    insertInputValue = callback => {

        if (this.props.disabled) {
            return;
        }

        const {data, inputValue, inputIndex} = this.state;

        if (!inputValue) {
            return;
        }

        const splitedValue = trim(inputValue).split(this.generateSeparatorReg()).filter(item => !!item);

        data.splice(inputIndex, 0, ...splitedValue);

        this.setState({
            data,
            inputValue: '',
            inputIndex: inputIndex + splitedValue.length
        }, () => {

            this.refs.inputWrapper.style.width = '9px';

            const {onChange} = this.props;
            onChange && onChange(data);

            callback && callback();

        });

    };

    itemChangeHandler = (value, index) => {

        if (this.props.disabled) {
            return;
        }

        const {data} = this.state;

        if (value) {
            const splitedValue = value.split(this.generateSeparatorReg()).filter(item => item);
            data.splice(index, 1, ...splitedValue);
        } else {
            data.splice(index, 1);
        }

        this.setState({
            data
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(data);
        });

    };

    itemEditStartHandler = editingItemIndex => {

        if (this.props.disabled) {
            return;
        }

        this.setState({
            itemEditing: true,
            editingItemIndex
        });

    };

    itemEditEndHandler = () => {

        if (this.props.disabled) {
            return;
        }

        this.setState({
            itemEditing: false
        });

    };

    componentDidMount() {

        Event.addEvent(document, 'mousedown', this.mouseDownHandler);

        this.wrapperEl = this.refs.wrapper;

        this.setState({
            wrapperWidth: this.wrapperEl ? this.wrapperEl.clientWidth : 'auto'
        });

    }

    componentWillUnmount() {
        Event.removeEvent(document, 'mousedown', this.mouseDownHandler);
    }

    static getDerivedStateFromProps(props, state) {
        return {
            prevProps: props,
            data: ComponentUtil.getDerivedState(props, state, 'data'),
            inputValue: ComponentUtil.getDerivedState(props, state, 'inputValue')
        };
    }

    render() {

        const {
                className, style, valueField, displayField, disabled,
                placeholder, isTagAutoWidth
            } = this.props,
            {wrapperWidth, data, inputValue, inputIndex, itemEditing, editingItemIndex} = this.state,

            fieldClassName = classNames('tag-field', {
                [className]: className
            }),

            indexData = Util.genIndexArray(data.length);

        indexData.splice(inputIndex, 0, this.inputSymbol);

        return (

 Event.wheelHandler(e, this.props)}>

                {
                    indexData.map(index => index === this.inputSymbol ?
                        !disabled ?

                                <textarea ref="input"
                                          className="tag-field-input"
                                          style={{width: wrapperWidth}}
                                          autoFocus="true"
                                          value={inputValue}
                                          placeholder={data.length < 1 && placeholder ? placeholder : ''}
                                          onChange={this.inputChangeHandler}
                                          onKeyDown={this.inputKeyDownHandler}/>

                            :
                            null
                        :

                            <EditableField className="tag-field-item-field"
                                           value={Util.getTextByDisplayField(data[index], displayField, valueField)}
                                           disabled={disabled || (itemEditing && index !== editingItemIndex)}
                                           autoWidth={isTagAutoWidth}
                                           onChange={value => this.itemChangeHandler(value, index)}
                                           onEditStart={() => this.itemEditStartHandler(index)}
                                           onEditEnd={this.itemEditEndHandler}>

                                <IconButton className="tag-field-item-field-delete-button"
                                            iconCls="fas fa-times"
                                            disabled={disabled || (itemEditing && index !== editingItemIndex)}
                                            onClick={() => this.removeItem(index)}/>

                            </EditableField>

                    )
                }

        );

    }
}

TagField.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    data: PropTypes.array,
    inputValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    valueField: PropTypes.string,
    displayField: PropTypes.string,
    separators: PropTypes.array,

    disabled: PropTypes.bool,
    placeholder: PropTypes.string,
    shouldPreventContainerScroll: PropTypes.bool,
    isTagAutoWidth: PropTypes.bool,

    onChange: PropTypes.func,
    onInputChange: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func

};

TagField.defaultProps = {

    data: [],
    inputValue: '',

    valueField: 'value',
    displayField: 'text',
    separators: TagField.DEFAULT_SEPARATORS,

    disabled: false,

    placeholder: '',
    shouldPreventContainerScroll: true,
    isTagAutoWidth: true

};

export default TagField;<commit_msg>optimize TagField<commit_after>/**
 * @file TagField component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import trim from 'lodash/trim';

import EditableField from '../EditableField';
import IconButton from '../IconButton';

import Util from '../_vendors/Util';
import Dom from '../_vendors/Dom';
import CharSize from '../_vendors/CharSize';
import Event from '../_vendors/Event';
import ComponentUtil from '../_vendors/ComponentUtil';

class TagField extends Component {

    static DEFAULT_SEPARATORS = [','];

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.inputSymbol = Symbol('input');

        this.state = {
            wrapperWidth: 'auto',
            data: props.data,
            inputValue: props.inputValue,
            inputIndex: props.data.length,
            itemEditing: false,
            editingItemIndex: -1
        };

    }

    getSeparators = (separators = this.props.separators) => {

        if (separators && separators.length > 0) {
            return separators;
        }

        return DEFAULT_SEPARATORS;

    };

    generateSeparatorReg = (separators = this.props.separators) => {
        const seps = this.getSeparators(separators);
        return new RegExp(`\\s*${seps ? seps.map(sep => `\\${sep}`).join('|') : ''}\\s*`, 'g');
    };

    removeItem = index => {

        const {data} = this.state;

        if (!data || data.length < 1 || !(index in data)) {
            return;
        }

        data.splice(index, 1);

        this.setState({
            data
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(data);
        });

    };

    calInputIndex = e => {

        const wrapperEl = this.refs.wrapper,
            offset = Dom.getOffset(wrapperEl),
            {left: minX} = offset,
            wrapperWidth = wrapperEl.getBoundingClientRect().width,
            maxX = minX + wrapperWidth;

        let x = e.clientX,
            y = e.clientY,
            inputIndex = -1;

        while (x >= minX) {

            const item = document.elementFromPoint(x, y),
                wrapperEl = Dom.findParentByClassName(item, 'tag-field-item-wrapper');

            if (wrapperEl) {
                inputIndex = +wrapperEl.dataset.index + 1;
                break;
            }

            x--;

        }

        if (inputIndex < 0) {
            while (x <= maxX) {

                const item = document.elementFromPoint(x, y),
                    wrapperEl = Dom.findParentByClassName(item, 'tag-field-item-wrapper');

                if (wrapperEl) {
                    inputIndex = +wrapperEl.dataset.index;
                    break;
                }

                x++;

            }
        }

        return inputIndex < 0 ? this.state.data.length : inputIndex;

    };

    mouseDownHandler = e => {

        if (this.props.disabled || Dom.findParentByClassName(e.target, 'tag-field-item-wrapper')
            || Dom.hasClass(e.target, 'tag-field-input')) {
            return;
        }

        const callback = (function (e) {

            return () => {

                if (Dom.findParentByClassName(e.target, 'tag-field') != this.refs.wrapper || this.state.itemEditing) {
                    this.setState({
                        inputIndex: this.state.data.length
                    });
                    return;
                }

                let x = e.clientX,
                    y = e.clientY;

                if (!x || !y) {
                    return;
                }

                const wrapperEl = this.refs.wrapper,
                    offset = Dom.getOffset(wrapperEl);

                if (!offset) {
                    return;
                }

                const inputIndex = this.calInputIndex(e);

                this.setState({
                    inputIndex
                }, () => {
                    setTimeout(() => {
                        this.refs.input.focus();
                        // wrapperEl.scrollLeft = 0;
                        // wrapperEl.scrollTop = 0;
                    }, 0);
                });

            };

        }.bind(this))(e);

        if (this.state.inputValue) {
            this.insertInputValue(callback);
        } else {
            callback();
        }

    };

    inputChangeHandler = e => {

        if (this.props.disabled) {
            return;
        }

        const value = trim(e.target.value),
            inputValue = value ? value.replace(/\r?\n/gm, this.getSeparators()[0]) : value;

        this.setState({
            inputValue
        }, () => {

            const {onInputChange} = this.props,
                width = CharSize.calculateStringWidth(inputValue, this.refs.test),
                inputWrapperEl = this.refs.inputWrapper,
                inputEl = this.refs.input;

            inputWrapperEl && (inputWrapperEl.style.width = `${width + 9}px`);
            inputEl && (inputEl.style.height = Math.max(inputEl.scrollHeight, 24) + 'px');
            onInputChange && onInputChange(inputValue);

        });

    };

    inputKeyDownHandler = e => {

        if (this.props.disabled) {
            return;
        }

        if (e.keyCode === 13) {
            this.insertInputValue();
        }

    };

    insertInputValue = callback => {

        if (this.props.disabled) {
            return;
        }

        const {data, inputValue, inputIndex} = this.state;

        if (!inputValue) {
            return;
        }

        const splitedValue = trim(inputValue).split(this.generateSeparatorReg()).filter(item => !!item);

        data.splice(inputIndex, 0, ...splitedValue);

        this.setState({
            data,
            inputValue: '',
            inputIndex: inputIndex + splitedValue.length
        }, () => {

            this.refs.inputWrapper.style.width = '9px';

            const {onChange} = this.props;
            onChange && onChange(data);

            callback && callback();

        });

    };

    itemChangeHandler = (value, index) => {

        if (this.props.disabled) {
            return;
        }

        const {data} = this.state;

        if (value) {
            const splitedValue = value.split(this.generateSeparatorReg()).filter(item => item);
            data.splice(index, 1, ...splitedValue);
        } else {
            data.splice(index, 1);
        }

        this.setState({
            data
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(data);
        });

    };

    itemEditStartHandler = editingItemIndex => {

        if (this.props.disabled) {
            return;
        }

        this.setState({
            itemEditing: true,
            editingItemIndex
        });

    };

    itemEditEndHandler = () => {

        if (this.props.disabled) {
            return;
        }

        this.setState({
            itemEditing: false
        });

    };

    componentDidMount() {

        Event.addEvent(document, 'mousedown', this.mouseDownHandler);

        this.wrapperEl = this.refs.wrapper;

        this.setState({
            wrapperWidth: this.wrapperEl ? this.wrapperEl.clientWidth : 'auto'
        });

    }

    componentWillUnmount() {
        Event.removeEvent(document, 'mousedown', this.mouseDownHandler);
    }

    static getDerivedStateFromProps(props, state) {
        return {
            prevProps: props,
            data: ComponentUtil.getDerivedState(props, state, 'data'),
            inputValue: ComponentUtil.getDerivedState(props, state, 'inputValue')
        };
    }

    render() {

        const {
                className, style, valueField, displayField, disabled,
                placeholder, isTagAutoWidth
            } = this.props,
            {wrapperWidth, data, inputValue, inputIndex, itemEditing, editingItemIndex} = this.state,

            fieldClassName = classNames('tag-field', {
                [className]: className
            }),

            indexData = Util.genIndexArray(data.length);

        indexData.splice(inputIndex, 0, this.inputSymbol);

        return (

 Event.wheelHandler(e, this.props)}>

                {
                    indexData && indexData.map(index => index === this.inputSymbol ?
                        !disabled ?

                                <textarea ref="input"
                                          className="tag-field-input"
                                          style={{width: wrapperWidth}}
                                          autoFocus="true"
                                          value={inputValue}
                                          placeholder={data.length < 1 && placeholder ? placeholder : ''}
                                          onChange={this.inputChangeHandler}
                                          onKeyDown={this.inputKeyDownHandler}/>

                            :
                            null
                        :

                            <EditableField className="tag-field-item-field"
                                           value={Util.getTextByDisplayField(data[index], displayField, valueField)}
                                           disabled={disabled || (itemEditing && index !== editingItemIndex)}
                                           autoWidth={isTagAutoWidth}
                                           onChange={value => this.itemChangeHandler(value, index)}
                                           onEditStart={() => this.itemEditStartHandler(index)}
                                           onEditEnd={this.itemEditEndHandler}>

                                <IconButton className="tag-field-item-field-delete-button"
                                            iconCls="fas fa-times"
                                            disabled={disabled || (itemEditing && index !== editingItemIndex)}
                                            onClick={() => this.removeItem(index)}/>

                            </EditableField>

                    )
                }

        );

    }
}

TagField.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    data: PropTypes.array,
    inputValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    valueField: PropTypes.string,
    displayField: PropTypes.string,
    separators: PropTypes.array,

    disabled: PropTypes.bool,
    placeholder: PropTypes.string,
    shouldPreventContainerScroll: PropTypes.bool,
    isTagAutoWidth: PropTypes.bool,

    onChange: PropTypes.func,
    onInputChange: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func

};

TagField.defaultProps = {

    data: [],
    inputValue: '',

    valueField: 'value',
    displayField: 'text',
    separators: TagField.DEFAULT_SEPARATORS,

    disabled: false,

    placeholder: '',
    shouldPreventContainerScroll: true,
    isTagAutoWidth: true

};

export default TagField;<|endoftext|>


<commit_before>import express from 'express';
const app = express();
import cors from 'cors';
import config from './config';
import exphbs from 'express-handlebars';
import validation from './configValidation';
import csErrorHandler from './middleware/csErrorHandler';
import instanceAuthenticator from './middleware/instanceAuthenticator';
import instanceToDigestValidator from './middleware/instanceToDigestValidator';
import instanceToInboxValidator from './middleware/instanceToInboxValidator';
import apiRoutesRequireContentTypeAppJson from './middleware/apiRoutesRequireContentTypeAppJson';
import appConfigure from './middleware/appConfigure';
import Promise from 'bluebird';
import domainMiddleware from 'express-domain-middleware';
import methodOverride from 'method-override';
import api from "./api";
import bootstrapper from './bootstrapper';
import instancesController from './api/instances/instancesController';

// DO NOT MOVE THIS. It is here to wrap routes in a domain to catch unhandled errors
app.use(domainMiddleware);

// DO NOT MOVE THIS. It is here to handle unhandled rejected Promises cleanly
Promise.onPossiblyUnhandledRejection(err => {
  throw err;
});

validation.validateConfig();
validation.validateEventStore(error => {
  if (error) {
    throw new Error(error);
  }
});

// override with the X-HTTP-Method-Override header in the request
app.use(methodOverride('X-HTTP-Method-Override'))

app.get('/version', (req, res) => {
  res.json({
    version: "0.0.0"
  });
});

// Boot the system, and create EventStore projections if needed
bootstrapper.boot(config);

// Wire up express-handlebars as the view engine for express.
app.engine('handlebars', exphbs());
app.set('view engine', 'handlebars');

// NOTE: Do not rearrange the order of these app.* statements becaused they
// are crucial for the order of operations in the pipeline of middleware
// functions!
app.use(cors());

app.use(express.static(`${__dirname}/client`));

app.get('/instances', (req, res) => {
  res.render('instances');
});

// Ensure that all routes with :instanceId parameters are properly authenticated
app.param('instanceId', instanceAuthenticator);
app.param('digestId', instanceToDigestValidator);
app.param('inboxId', instanceToInboxValidator);

// NOTE: See above warning. Why are you even considering moving these?
// Think thrice.
appConfigure(app);

// SPIKE: Provide a public entry point for the API
app.get('/api/public', (req, res) => {
  res.set('Content-Type', 'application/hal+json');
  res.status(200).send({
    "_links": {
      "self": {
        "href": req.href("/api/public")
      },
      "instances": {
        "href": req.href("/api/instances")
      },
      "instance": {
        "href": req.href("/api/instances/{instanceId}"),
        templated: true
      },
      "digest": {
        "href": req.href('/api/{instanceId}/digests/{digestId}'),
        "templated": true
      }
    }
  });
});

// Return a URL as a special header for each request
app.use((req, res, next) => {
  res.setHeader("X-CommitStream-API-Docs", "https://github.com/openAgile/CommitStream.Web");
  return next();
});

// This must happen here, before we cover additional routes with Content-Type validation
instancesController.init(app);

// Ensure all API routes have consistent Content-Type validation
app.all('/api/*', apiRoutesRequireContentTypeAppJson);

// Map API the routes
api.init(app);

// DO NOT MOVE THIS. It must be here to catch unhandled errors.
app.use(csErrorHandler);

const  getHostSettings = (req) => ({
  protocol: config.protocol || req.protocol,
  host: req.get('host'),
  key: req.query.key
});

app.get('/app', (req, res) => {
  res.setHeader('Content-Type', 'application/javascript');
  const settings = getHostSettings(req);

  res.render('app', {
    apiUrl: `${settings.protocol}://${settings.host}/api/`,
    protocol: settings.protocol,
    resourcePath: `${settings.protocol}://${settings.host}/`,
    showChildrenFeatureToggle: config.showChildrenFeatureToggle.toString()
  });
});

app.listen(config.port, () => {
  console.log(`CommitStream Web Server listening on port ${config.port}`);
});<commit_msg>S-61014 Corrected whitespace<commit_after>import express from 'express';
const app = express();
import cors from 'cors';
import config from './config';
import exphbs from 'express-handlebars';
import validation from './configValidation';
import csErrorHandler from './middleware/csErrorHandler';
import instanceAuthenticator from './middleware/instanceAuthenticator';
import instanceToDigestValidator from './middleware/instanceToDigestValidator';
import instanceToInboxValidator from './middleware/instanceToInboxValidator';
import apiRoutesRequireContentTypeAppJson from './middleware/apiRoutesRequireContentTypeAppJson';
import appConfigure from './middleware/appConfigure';
import Promise from 'bluebird';
import domainMiddleware from 'express-domain-middleware';
import methodOverride from 'method-override';
import api from "./api";
import bootstrapper from './bootstrapper';
import instancesController from './api/instances/instancesController';

// DO NOT MOVE THIS. It is here to wrap routes in a domain to catch unhandled errors
app.use(domainMiddleware);

// DO NOT MOVE THIS. It is here to handle unhandled rejected Promises cleanly
Promise.onPossiblyUnhandledRejection(err => {
  throw err;
});

validation.validateConfig();
validation.validateEventStore(error => {
  if (error) {
    throw new Error(error);
  }
});

// override with the X-HTTP-Method-Override header in the request
app.use(methodOverride('X-HTTP-Method-Override'))

app.get('/version', (req, res) => {
  res.json({
    version: "0.0.0"
  });
});

// Boot the system, and create EventStore projections if needed
bootstrapper.boot(config);

// Wire up express-handlebars as the view engine for express.
app.engine('handlebars', exphbs());
app.set('view engine', 'handlebars');

// NOTE: Do not rearrange the order of these app.* statements becaused they
// are crucial for the order of operations in the pipeline of middleware
// functions!
app.use(cors());

app.use(express.static(`${__dirname}/client`));

app.get('/instances', (req, res) => {
  res.render('instances');
});

// Ensure that all routes with :instanceId parameters are properly authenticated
app.param('instanceId', instanceAuthenticator);
app.param('digestId', instanceToDigestValidator);
app.param('inboxId', instanceToInboxValidator);

// NOTE: See above warning. Why are you even considering moving these?
// Think thrice.
appConfigure(app);

// SPIKE: Provide a public entry point for the API
app.get('/api/public', (req, res) => {
  res.set('Content-Type', 'application/hal+json');
  res.status(200).send({
    "_links": {
      "self": {
        "href": req.href("/api/public")
      },
      "instances": {
        "href": req.href("/api/instances")
      },
      "instance": {
        "href": req.href("/api/instances/{instanceId}"),
        templated: true
      },
      "digest": {
        "href": req.href('/api/{instanceId}/digests/{digestId}'),
        "templated": true
      }
    }
  });
});

// Return a URL as a special header for each request
app.use((req, res, next) => {
  res.setHeader("X-CommitStream-API-Docs", "https://github.com/openAgile/CommitStream.Web");
  return next();
});

// This must happen here, before we cover additional routes with Content-Type validation
instancesController.init(app);

// Ensure all API routes have consistent Content-Type validation
app.all('/api/*', apiRoutesRequireContentTypeAppJson);

// Map API the routes
api.init(app);

// DO NOT MOVE THIS. It must be here to catch unhandled errors.
app.use(csErrorHandler);

const getHostSettings = (req) => ({
  protocol: config.protocol || req.protocol,
  host: req.get('host'),
  key: req.query.key
});

app.get('/app', (req, res) => {
  res.setHeader('Content-Type', 'application/javascript');
  const settings = getHostSettings(req);

  res.render('app', {
    apiUrl: `${settings.protocol}://${settings.host}/api/`,
    protocol: settings.protocol,
    resourcePath: `${settings.protocol}://${settings.host}/`,
    showChildrenFeatureToggle: config.showChildrenFeatureToggle.toString()
  });
});

app.listen(config.port, () => {
  console.log(`CommitStream Web Server listening on port ${config.port}`);
});<|endoftext|>


<commit_before>/*    Copyright 2016 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const util = require('util');
const sem = require('../sensor/SensorEventManager.js').getInstance();
const Tail = require('always-tail');
const fs = require('fs');
const cp = require('child_process');
const Sensor = require('./Sensor.js').Sensor;

class OvpnConnSensor extends Sensor {
  constructor() {
    super();
  }

  initLogWatcher() {
    if (!fs.existsSync(this.config.logPath)) {
      log.debug(util.format("Log file %s does not exist, awaiting for file creation.", this.config.logPath));
      setTimeout(() => {
        this.initLogWatcher();
      }, 5000);
    } else {
      // add read permission in case it is owned by root
      const cmd = util.format("sudo chmod +r %s", this.config.logPath);
      cp.exec(cmd, (err, stdout, stderr) => {
        if (err || stderr) {
          log.error(util.format("Failed to change permission for log file: %s", err || stderr));
          setTimeout(() => {
            this.initLogWatcher();
          }, 5000);
          return;
        }
        if (this.ovpnLog == null) {
          log.debug("Initializing ovpn log watchers: ", this.config.logPath);
          this.ovpnLog = new Tail(this.config.logPath, '\n');
          if (this.ovpnLog != null) {
            this.ovpnLog.on('line', (data) => {
              log.debug("Detect:OvpnLog ", data);
              this.processOvpnLog(data);
            });
          } else {
            setTimeout(() => {
              this.initLogWatcher();
            }, 5000);
          }
        }
      });
    }
  }

  run() {
    this.initLogWatcher();
  }

  processOvpnLog(data) {
    if (data.includes(": pool returned")) {
      // vpn client connection accepted
      const words = data.split(/\s+/, 6);
      const remote = words[5];
      const peers = data.substr(data.indexOf('pool returned') + 14);
      // remote should be <name>/<ip>:<port>
      const profile = remote.split('/')[0];
      const client = remote.split('/')[1];
      const clientIP = client.split(':')[0];
      const clientPort = client.split(':')[1];
      // peerIP4 should be IPv4=<ip>,
      const peerIP4 = peers.split(', ')[0];
      let peerIPv4Address = peerIP4.split('=')[1];
      if (peerIPv4Address === "(Not enabled)") {
        peerIPv4Address = null;
      }
      // peerIP6 should be IPv6=<ip>
      const peerIP6 = peers.split(', ')[1];
      let peerIPv6Address = peerIP6.split('=')[1];
      if (peerIPv6Address === "(Not enabled)") {
        peerIPv6Address = null;
      }
      log.info(util.format("VPN client connection accepted, remote: %s, peer ipv4: %s, peer ipv6: %s, profile: %s", client, peerIPv4Address, peerIPv6Address, profile));
      sem.emitEvent({
        type: "VPNConnectionAccepted",
        message: "A new VPN connection was accepted",
        client: {
          remoteIP: clientIP,
          remotePort: clientPort,
          peerIP4: peerIPv4Address,
          peerIP6: peerIPv6Address,
          profile: profile
        }
      });
    }
  }
}

module.exports = OvpnConnSensor;<commit_msg>catch error for vpn log processing<commit_after>/*    Copyright 2016 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const util = require('util');
const sem = require('../sensor/SensorEventManager.js').getInstance();
const Tail = require('always-tail');
const fs = require('fs');
const cp = require('child_process');
const Sensor = require('./Sensor.js').Sensor;

class OvpnConnSensor extends Sensor {
  constructor() {
    super();
  }

  initLogWatcher() {
    if (!fs.existsSync(this.config.logPath)) {
      log.debug(util.format("Log file %s does not exist, awaiting for file creation.", this.config.logPath));
      setTimeout(() => {
        this.initLogWatcher();
      }, 5000);
    } else {
      // add read permission in case it is owned by root
      const cmd = util.format("sudo chmod +r %s", this.config.logPath);
      cp.exec(cmd, (err, stdout, stderr) => {
        if (err || stderr) {
          log.error(util.format("Failed to change permission for log file: %s", err || stderr));
          setTimeout(() => {
            this.initLogWatcher();
          }, 5000);
          return;
        }
        if (this.ovpnLog == null) {
          log.debug("Initializing ovpn log watchers: ", this.config.logPath);
          this.ovpnLog = new Tail(this.config.logPath, '\n');
          if (this.ovpnLog != null) {
            this.ovpnLog.on('line', (data) => {
              log.debug("Detect:OvpnLog ", data);
              this.processOvpnLog(data);
            });
          } else {
            setTimeout(() => {
              this.initLogWatcher();
            }, 5000);
          }
        }
      });
    }
  }

  run() {
    this.initLogWatcher();
  }

  processOvpnLog(data) {
    if (data.includes(": pool returned")) {
      try {
        // vpn client connection accepted
        const words = data.split(/\s+/, 6);
        const remote = words[5];
        const peers = data.substr(data.indexOf('pool returned') + 14);
        // remote should be <name>/<ip>:<port>
        const profile = remote.split('/')[0];
        const client = remote.split('/')[1];
        const clientIP = client.split(':')[0];
        const clientPort = client.split(':')[1];
        // peerIP4 should be IPv4=<ip>,
        const peerIP4 = peers.split(', ')[0];
        let peerIPv4Address = peerIP4.split('=')[1];
        if (peerIPv4Address === "(Not enabled)") {
          peerIPv4Address = null;
        }
        // peerIP6 should be IPv6=<ip>
        const peerIP6 = peers.split(', ')[1];
        let peerIPv6Address = peerIP6.split('=')[1];
        if (peerIPv6Address === "(Not enabled)") {
          peerIPv6Address = null;
        }
        log.info(util.format("VPN client connection accepted, remote: %s, peer ipv4: %s, peer ipv6: %s, profile: %s", client, peerIPv4Address, peerIPv6Address, profile));
        sem.emitEvent({
          type: "VPNConnectionAccepted",
          message: "A new VPN connection was accepted",
          client: {
            remoteIP: clientIP,
            remotePort: clientPort,
            peerIP4: peerIPv4Address,
            peerIP6: peerIPv6Address,
            profile: profile
          }
        });
      } catch(err) {
        log.error("Error processing VPN log", err)
      }
    }
  }
}

module.exports = OvpnConnSensor;
<|endoftext|>


<commit_before>/* eslint no-continue: 0 */
import events from 'events';

export const SP_TYPE_SEND_RESPONSE = 0;
export const SP_TYPE_CHAR_COUNTING = 1;

const noop = () => {};

const stripComments = (() => {
    const re1 = new RegExp(/\s*[%#;].*/g); // Strip everything after %, #, or ; to the end of the line, including preceding spaces
    const re2 = new RegExp(/\s*\([^\)]*\)/g); // Remove anything inside the parentheses
    return line => line.replace(re1, '').replace(re2, '');
})();

class SPSendResponse {
    callback = null;

    constructor(options, callback = noop) {
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }
        if (typeof callback === 'function') {
            this.callback = callback;
        }
    }
    process() {
        this.callback && this.callback(this);
    }
    clear() {
        // Do nothing
    }
    get type() {
        return SP_TYPE_SEND_RESPONSE;
    }
}

class SPCharCounting {
    callback = null;
    state = {
        bufferSize: 128, // Defaults to 128
        dataLength: 0,
        queue: [],
        line: ''
    };

    constructor(options, callback = noop) {
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }

        // bufferSize
        const bufferSize = Number(options.bufferSize);
        if (bufferSize && bufferSize > 0) {
            this.state.bufferSize = bufferSize;
        }

        if (typeof callback === 'function') {
            this.callback = callback;
        }
    }
    process() {
        this.callback && this.callback(this);
    }
    reset() {
        this.state.bufferSize = 128; // Defaults to 128
        this.state.dataLength = 0;
        this.state.queue = [];
        this.state.line = '';
    }
    clear() {
        this.state.dataLength = 0;
        this.state.queue = [];
        this.state.line = '';
    }
    get type() {
        return SP_TYPE_CHAR_COUNTING;
    }
    get bufferSize() {
        return this.state.bufferSize;
    }
    set bufferSize(bufferSize = 0) {
        bufferSize = Number(bufferSize);
        if (!bufferSize) {
            return;
        }

        // The buffer size cannot be reduced below the size of the data within the buffer.
        this.state.bufferSize = Math.max(bufferSize, this.state.dataLength);
    }
    get dataLength() {
        return this.state.dataLength;
    }
    set dataLength(dataLength) {
        this.state.dataLength = dataLength;
    }
    get queue() {
        return this.state.queue;
    }
    set queue(queue) {
        this.state.queue = queue;
    }
    get line() {
        return this.state.line;
    }
    set line(line) {
        this.state.line = line;
    }
}

class Sender extends events.EventEmitter {
    sp = null; // Streaming Protocol
    state = {
        name: '',
        gcode: '',
        lines: [],
        total: 0,
        sent: 0,
        received: 0,
        startTime: 0,
        finishTime: 0,
        elapsedTime: 0,
        remainingTime: 0,
        changed: false
    };

    // @param {number} [type] Streaming protocol type. 0 for send-response, 1 for character-counting.
    // @param {object} [options] The options object.
    // @param {number} [options.bufferSize] The buffer size used in character-counting streaming protocol. Defaults to 127.
    constructor(type = SP_TYPE_SEND_RESPONSE, options = {}) {
        super();

        // character-counting
        if (type === SP_TYPE_CHAR_COUNTING) {
            this.sp = new SPCharCounting(options, (sp) => {
                if (sp.queue.length > 0) {
                    const lineLength = sp.queue.shift();
                    sp.dataLength -= lineLength;
                }

                while (this.state.sent < this.state.total) {
                    // Remove leading and trailing whitespace from both ends of a string
                    sp.line = sp.line || stripComments(this.state.lines[this.state.sent]).trim();

                    // The newline character (\n) consumed the RX buffer space
                    if ((sp.line.length > 0) && ((sp.dataLength + sp.line.length + 1) >= sp.bufferSize)) {
                        break;
                    }

                    this.state.sent++;
                    this.emit('change');

                    if (sp.line.length === 0) {
                        this.ack(); // ack empty line

                        // continue to the next line if empty
                        continue;
                    }

                    const line = sp.line + '\n';
                    sp.line = '';
                    sp.dataLength += line.length;
                    sp.queue.push(line.length);
                    this.emit('data', line);
                }
            });
        }

        // send-response
        if (type === SP_TYPE_SEND_RESPONSE) {
            this.sp = new SPSendResponse(options, (sp) => {
                while (this.state.sent < this.state.total) {
                    // Remove leading and trailing whitespace from both ends of a string
                    const line = stripComments(this.state.lines[this.state.sent]).trim();

                    this.state.sent++;
                    this.emit('change');

                    if (line.length === 0) {
                        this.ack(); // ack empty line

                        // continue to the next line if empty
                        continue;
                    }

                    this.emit('data', line + '\n');
                    break;
                }
            });
        }

        this.on('change', () => {
            this.state.changed = true;
        });
    }
    toJSON() {
        return {
            sp: this.sp.type,
            name: this.state.name,
            size: this.state.gcode.length,
            total: this.state.total,
            sent: this.state.sent,
            received: this.state.received,
            startTime: this.state.startTime,
            finishTime: this.state.finishTime,
            elapsedTime: this.state.elapsedTime,
            remainingTime: this.state.remainingTime
        };
    }
    // @return {boolean} Returns true on success, false otherwise.
    load(name, gcode = '') {
        if (typeof gcode !== 'string' || !gcode) {
            return false;
        }

        if (this.sp) {
            this.sp.clear();
        }
        this.state.name = name;
        this.state.gcode = gcode;
        this.state.lines = gcode.split('\n')
            .filter(line => (line.trim().length > 0));
        this.state.total = this.state.lines.length;
        this.state.sent = 0;
        this.state.received = 0;
        this.state.startTime = 0;
        this.state.finishTime = 0;
        this.state.elapsedTime = 0;
        this.state.remainingTime = 0;

        this.emit('load', { name: name, gcode: gcode });
        this.emit('change');

        return true;
    }
    unload() {
        if (this.sp) {
            this.sp.clear();
        }
        this.state.name = '';
        this.state.gcode = '';
        this.state.lines = [];
        this.state.total = 0;
        this.state.sent = 0;
        this.state.received = 0;
        this.state.startTime = 0;
        this.state.finishTime = 0;
        this.state.elapsedTime = 0;
        this.state.remainingTime = 0;

        this.emit('unload');
        this.emit('change');
    }
    // Tells the sender an acknowledgement has received.
    // @return {boolean} Returns true on success, false otherwise.
    ack() {
        if (!this.state.gcode) {
            return false;
        }

        this.state.received++;
        this.emit('change');

        return true;
    }
    // Tells the sender to send more data.
    // @return {boolean} Returns true on success, false otherwise.
    next() {
        if (!this.state.gcode) {
            return false;
        }

        const now = new Date().getTime();

        if (this.state.total > 0 && this.state.sent === 0) {
            this.state.startTime = now;
            this.state.finishTime = 0;
            this.state.elapsedTime = 0;
            this.state.remainingTime = 0;
            this.emit('start', { time: this.state.startTime });
            this.emit('change');
        }

        if (this.sp) {
            this.sp.process();
        }

        // Elapsed Time
        this.state.elapsedTime = now - this.state.startTime;

        // Make a 1 second delay before estimating the remaining time
        if (this.state.elapsedTime >= 1000 && this.state.received > 0) {
            const timePerCode = this.state.elapsedTime / this.state.received;
            this.state.remainingTime = (timePerCode * this.state.total - this.state.elapsedTime);
        }

        if (this.state.received >= this.state.total) {
            this.state.finishTime = now;
            this.emit('end', { time: this.state.finishTime });
            this.emit('change');
        }

        return true;
    }
    // Rewinds the internal array pointer.
    // @return {boolean} Returns true on success, false otherwise.
    rewind() {
        if (!this.state.gcode) {
            return false;
        }

        if (this.sp) {
            this.sp.clear();
        }
        this.state.sent = 0;
        this.state.received = 0;
        this.emit('change');

        return true;
    }
    // Checks if there are any state changes. It will also clear the changed flag.
    // @return {boolean} Returns true on state changes, false otherwise.
    peek() {
        if (!this.state.gcode) {
            return false;
        }

        const changed = this.state.changed;
        this.state.changed = false;
        return changed;
    }
}

export default Sender;
<commit_msg>Fixed a bug that causes problem for reporting sender status to the web interface<commit_after>/* eslint no-continue: 0 */
import events from 'events';

export const SP_TYPE_SEND_RESPONSE = 0;
export const SP_TYPE_CHAR_COUNTING = 1;

const noop = () => {};

const stripComments = (() => {
    const re1 = new RegExp(/\s*[%#;].*/g); // Strip everything after %, #, or ; to the end of the line, including preceding spaces
    const re2 = new RegExp(/\s*\([^\)]*\)/g); // Remove anything inside the parentheses
    return line => line.replace(re1, '').replace(re2, '');
})();

class SPSendResponse {
    callback = null;

    constructor(options, callback = noop) {
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }
        if (typeof callback === 'function') {
            this.callback = callback;
        }
    }
    process() {
        this.callback && this.callback(this);
    }
    clear() {
        // Do nothing
    }
    get type() {
        return SP_TYPE_SEND_RESPONSE;
    }
}

class SPCharCounting {
    callback = null;
    state = {
        bufferSize: 128, // Defaults to 128
        dataLength: 0,
        queue: [],
        line: ''
    };

    constructor(options, callback = noop) {
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }

        // bufferSize
        const bufferSize = Number(options.bufferSize);
        if (bufferSize && bufferSize > 0) {
            this.state.bufferSize = bufferSize;
        }

        if (typeof callback === 'function') {
            this.callback = callback;
        }
    }
    process() {
        this.callback && this.callback(this);
    }
    reset() {
        this.state.bufferSize = 128; // Defaults to 128
        this.state.dataLength = 0;
        this.state.queue = [];
        this.state.line = '';
    }
    clear() {
        this.state.dataLength = 0;
        this.state.queue = [];
        this.state.line = '';
    }
    get type() {
        return SP_TYPE_CHAR_COUNTING;
    }
    get bufferSize() {
        return this.state.bufferSize;
    }
    set bufferSize(bufferSize = 0) {
        bufferSize = Number(bufferSize);
        if (!bufferSize) {
            return;
        }

        // The buffer size cannot be reduced below the size of the data within the buffer.
        this.state.bufferSize = Math.max(bufferSize, this.state.dataLength);
    }
    get dataLength() {
        return this.state.dataLength;
    }
    set dataLength(dataLength) {
        this.state.dataLength = dataLength;
    }
    get queue() {
        return this.state.queue;
    }
    set queue(queue) {
        this.state.queue = queue;
    }
    get line() {
        return this.state.line;
    }
    set line(line) {
        this.state.line = line;
    }
}

class Sender extends events.EventEmitter {
    sp = null; // Streaming Protocol
    state = {
        name: '',
        gcode: '',
        lines: [],
        total: 0,
        sent: 0,
        received: 0,
        startTime: 0,
        finishTime: 0,
        elapsedTime: 0,
        remainingTime: 0,
        changed: false
    };

    // @param {number} [type] Streaming protocol type. 0 for send-response, 1 for character-counting.
    // @param {object} [options] The options object.
    // @param {number} [options.bufferSize] The buffer size used in character-counting streaming protocol. Defaults to 127.
    constructor(type = SP_TYPE_SEND_RESPONSE, options = {}) {
        super();

        // character-counting
        if (type === SP_TYPE_CHAR_COUNTING) {
            this.sp = new SPCharCounting(options, (sp) => {
                if (sp.queue.length > 0) {
                    const lineLength = sp.queue.shift();
                    sp.dataLength -= lineLength;
                }

                while (this.state.sent < this.state.total) {
                    // Remove leading and trailing whitespace from both ends of a string
                    sp.line = sp.line || stripComments(this.state.lines[this.state.sent]).trim();

                    // The newline character (\n) consumed the RX buffer space
                    if ((sp.line.length > 0) && ((sp.dataLength + sp.line.length + 1) >= sp.bufferSize)) {
                        break;
                    }

                    this.state.sent++;
                    this.emit('change');

                    if (sp.line.length === 0) {
                        this.ack(); // ack empty line

                        // continue to the next line if empty
                        continue;
                    }

                    const line = sp.line + '\n';
                    sp.line = '';
                    sp.dataLength += line.length;
                    sp.queue.push(line.length);
                    this.emit('data', line);
                }
            });
        }

        // send-response
        if (type === SP_TYPE_SEND_RESPONSE) {
            this.sp = new SPSendResponse(options, (sp) => {
                while (this.state.sent < this.state.total) {
                    // Remove leading and trailing whitespace from both ends of a string
                    const line = stripComments(this.state.lines[this.state.sent]).trim();

                    this.state.sent++;
                    this.emit('change');

                    if (line.length === 0) {
                        this.ack(); // ack empty line

                        // continue to the next line if empty
                        continue;
                    }

                    this.emit('data', line + '\n');
                    break;
                }
            });
        }

        this.on('change', () => {
            this.state.changed = true;
        });
    }
    toJSON() {
        return {
            sp: this.sp.type,
            name: this.state.name,
            size: this.state.gcode.length,
            total: this.state.total,
            sent: this.state.sent,
            received: this.state.received,
            startTime: this.state.startTime,
            finishTime: this.state.finishTime,
            elapsedTime: this.state.elapsedTime,
            remainingTime: this.state.remainingTime
        };
    }
    // @return {boolean} Returns true on success, false otherwise.
    load(name, gcode = '') {
        if (typeof gcode !== 'string' || !gcode) {
            return false;
        }

        if (this.sp) {
            this.sp.clear();
        }
        this.state.name = name;
        this.state.gcode = gcode;
        this.state.lines = gcode.split('\n')
            .filter(line => (line.trim().length > 0));
        this.state.total = this.state.lines.length;
        this.state.sent = 0;
        this.state.received = 0;
        this.state.startTime = 0;
        this.state.finishTime = 0;
        this.state.elapsedTime = 0;
        this.state.remainingTime = 0;

        this.emit('load', { name: name, gcode: gcode });
        this.emit('change');

        return true;
    }
    unload() {
        if (this.sp) {
            this.sp.clear();
        }
        this.state.name = '';
        this.state.gcode = '';
        this.state.lines = [];
        this.state.total = 0;
        this.state.sent = 0;
        this.state.received = 0;
        this.state.startTime = 0;
        this.state.finishTime = 0;
        this.state.elapsedTime = 0;
        this.state.remainingTime = 0;

        this.emit('unload');
        this.emit('change');
    }
    // Tells the sender an acknowledgement has received.
    // @return {boolean} Returns true on success, false otherwise.
    ack() {
        if (!this.state.gcode) {
            return false;
        }

        this.state.received++;
        this.emit('change');

        return true;
    }
    // Tells the sender to send more data.
    // @return {boolean} Returns true on success, false otherwise.
    next() {
        if (!this.state.gcode) {
            return false;
        }

        const now = new Date().getTime();

        if (this.state.total > 0 && this.state.sent === 0) {
            this.state.startTime = now;
            this.state.finishTime = 0;
            this.state.elapsedTime = 0;
            this.state.remainingTime = 0;
            this.emit('start', { time: this.state.startTime });
            this.emit('change');
        }

        if (this.sp) {
            this.sp.process();
        }

        // Elapsed Time
        this.state.elapsedTime = now - this.state.startTime;

        // Make a 1 second delay before estimating the remaining time
        if (this.state.elapsedTime >= 1000 && this.state.received > 0) {
            const timePerCode = this.state.elapsedTime / this.state.received;
            this.state.remainingTime = (timePerCode * this.state.total - this.state.elapsedTime);
        }

        if (this.state.received >= this.state.total) {
            this.state.finishTime = now;
            this.emit('end', { time: this.state.finishTime });
            this.emit('change');
        }

        return true;
    }
    // Rewinds the internal array pointer.
    // @return {boolean} Returns true on success, false otherwise.
    rewind() {
        if (!this.state.gcode) {
            return false;
        }

        if (this.sp) {
            this.sp.clear();
        }
        this.state.sent = 0;
        this.state.received = 0;
        this.emit('change');

        return true;
    }
    // Checks if there are any state changes. It also clears the changed flag.
    // @return {boolean} Returns true on state changes, false otherwise.
    peek() {
        const changed = this.state.changed;
        this.state.changed = false;
        return changed;
    }
}

export default Sender;
<|endoftext|>