<commit_before>(function ( $ ) {

    /**
     * Holds google map object and related utility entities.
     * @constructor
     */
    function GMapContext(domElement, options) {
        var _map = new google.maps.Map(domElement, options);
        var _marker = new google.maps.Marker({
            position: new google.maps.LatLng(54.19335, -3.92695),
            map: _map,
            title: "Drag Me",
            draggable: options.draggable
        });
        return {
            map: _map,
            marker: _marker,
            circle: null,
            location: _marker.position,
            radius: options.radius,
            locationName: options.locationName,
            addressComponents: {
                formatted_address: null,
                addressLine1: null,
                addressLine2: null,
                streetName: null,
                streetNumber: null,
                city: null,
                state: null,
                stateOrProvince: null
            },
            settings: options.settings,
            domContainer: domElement,
            geodecoder: new google.maps.Geocoder()
        }
    }

    // Utility functions for Google Map Manipulations
    var GmUtility = {
        /**
         * Draw a circle over the the map. Returns circle object.
         * Also writes new circle object in gmapContext.
         *
         * @param center - LatLng of the center of the circle
         * @param radius - radius in meters
         * @param gmapContext - context
         * @param options
         */
        drawCircle: function(gmapContext, center, radius, options) {
            if (gmapContext.circle != null) {
                gmapContext.circle.setMap(null);
            }
            if (radius > 0) {
                radius *= 1;
                options = $.extend({
                    strokeColor: "#0000FF",
                    strokeOpacity: 0.35,
                    strokeWeight: 2,
                    fillColor: "#0000FF",
                    fillOpacity: 0.20
                }, options);
                options.map = gmapContext.map;
                options.radius = radius;
                options.center = center;
                gmapContext.circle = new google.maps.Circle(options);
                return gmapContext.circle;
            }
            return null;
        },
        /**
         *
         * @param gMapContext
         * @param location
         * @param callback
         */
        setPosition: function(gMapContext, location, callback) {
            gMapContext.location = location;
            gMapContext.marker.setPosition(location);
            gMapContext.map.panTo(location);
            this.drawCircle(gMapContext, location, gMapContext.radius, {});
            if (gMapContext.settings.enableReverseGeocode) {
                gMapContext.geodecoder.geocode({latLng: gMapContext.location}, function(results, status){
                    if (status == google.maps.GeocoderStatus.OK && results.length > 0){
                        gMapContext.locationName = results[0].formatted_address;
                        gMapContext.addressComponents =
                            GmUtility.address_component_from_google_geocode(results[0].address_components);
                    }
                    if (callback) {
                        callback.call(this, gMapContext);
                    }
                });
            } else {
                if (callback) {
                    callback.call(this, gMapContext);
                }
            }

        },
        locationFromLatLng: function(lnlg) {
            return {latitude: lnlg.lat(), longitude: lnlg.lng()}
        },
        address_component_from_google_geocode: function(address_components) {
            var result = {};
            for (var i = address_components.length-1; i>=0; i--) {
                var component = address_components[i];
                // Postal code
                if (component.types.indexOf('postal_code') >= 0) {
                    result.postalCode = component.short_name;
                }
                // Street number
                else if (component.types.indexOf('street_number') >= 0) {
                    result.streetNumber = component.short_name;
                }
                // Street name
                else if (component.types.indexOf('route') >= 0) {
                    result.streetName = component.short_name;
                }
                // City
                else if (component.types.indexOf('sublocality') >= 0) {
                    result.city = component.short_name;
                }
                // State \ Province
                else if (component.types.indexOf('administrative_area_level_1') >= 0) {
                    result.stateOrProvince = component.short_name;
                }
                // State \ Province
                else if (component.types.indexOf('country') >= 0) {
                    result.country = component.short_name;
                }
            }
            result.addressLine1 = [result.streetNumber, result.streetName].join(' ').trim();
            result.addressLine2 = '';
            return result;
        }
    };

    function isPluginApplied(domObj) {
        return getContextForElement(domObj) != undefined;
    }

    function getContextForElement(domObj) {
        return $(domObj).data("locationpicker");
    }

    function updateInputValues(inputBinding, gmapContext){
        if (!inputBinding) return;
        var currentLocation = GmUtility.locationFromLatLng(gmapContext.location);
        if (inputBinding.latitudeInput) {
            inputBinding.latitudeInput.val(currentLocation.latitude);
        }
        if (inputBinding.longitudeInput) {
            inputBinding.longitudeInput.val(currentLocation.longitude);
        }
        if (inputBinding.radiusInput) {
            inputBinding.radiusInput.val(gmapContext.radius);
        }
        if (inputBinding.locationNameInput) {
            inputBinding.locationNameInput.val(gmapContext.locationName);
        }
    }

    function setupInputListenersInput(inputBinding, gmapContext) {
        if (inputBinding) {
            if (inputBinding.radiusInput){
                inputBinding.radiusInput.on("change", function() {
                    gmapContext.radius = $(this).val();
                    GmUtility.setPosition(gmapContext, gmapContext.location, function(context){
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
            if (inputBinding.locationNameInput && gmapContext.settings.enableAutocomplete) {
                gmapContext.autocomplete = new google.maps.places.Autocomplete(inputBinding.locationNameInput.get(0));
                google.maps.event.addListener(gmapContext.autocomplete, 'place_changed', function() {
                    var place = gmapContext.autocomplete.getPlace();
                    if (!place.geometry) {
                        gmapContext.settings.onlocationnotfound(place.name);
                        return;
                    }
                    GmUtility.setPosition(gmapContext, place.geometry.location, function(context) {
                        updateInputValues(inputBinding, context);
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
            if (inputBinding.latitudeInput) {
                inputBinding.latitudeInput.on("change", function() {
                    GmUtility.setPosition(gmapContext, new google.maps.LatLng($(this).val(), gmapContext.location.lng()), function(context){
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
            if (inputBinding.longitudeInput) {
                inputBinding.longitudeInput.on("change", function() {
                    GmUtility.setPosition(gmapContext, new google.maps.LatLng(gmapContext.location.lat(), $(this).val()), function(context){
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
        }
    }

    /**
     * Initialization:
     *  $("#myMap").locationpicker(options);
     * @param options
     * @param params
     * @returns {*}
     */
    $.fn.locationpicker = function( options, params ) {
        if (typeof options == 'string') { // Command provided
            var _targetDomElement = this.get(0);
            // Plug-in is not applied - nothing to do.
            if (!isPluginApplied(_targetDomElement)) return;
            var gmapContext = getContextForElement(_targetDomElement);
            switch (options) {
                case "location":
                    if (params == undefined) { // Getter
                        var location = GmUtility.locationFromLatLng(gmapContext.location);
                        location.radius = gmapContext.radius;
                        location.name = gmapContext.locationName;
                        return location;
                    } else { // Setter
                        if (params.radius) {
                            gmapContext.radius = params.radius;
                        }
                        GmUtility.setPosition(gmapContext, new google.maps.LatLng(params.latitude, params.longitude), function(gmapContext) {
                            updateInputValues(gmapContext.settings.inputBinding, gmapContext);
                        });
                    }
                    break;
                case "subscribe":
                    /**
                     * Provides interface for subscribing for GoogleMap events.
                     * See Google API documentation for details.
                     * Parameters:
                     * - event: string, name of the event
                     * - callback: function, callback function to be invoked
                     */
                    if (params == undefined) { // Getter is not available
                        return null;
                    } else {
                        var event = params.event;
                        var callback = params.callback;
                        if (!event || ! callback) {
                            console.error("LocationPicker: Invalid arguments for method \"subscribe\"")
                            return null;
                        }
                        google.maps.event.addListener(gmapContext.map, event, callback);
                    }
                    break;
                case "map":
                    /**
                     * Returns object which allows access actual google widget and marker paced on it.
                     * Structure: {
                     *  map: Instance of the google map widget
                     *  marker: marker placed on map
                     * }
                     */
                    if (params == undefined) { // Getter is not available
                        var locationObj = GmUtility.locationFromLatLng(gmapContext.location);
                        locationObj.formattedAddress = gmapContext.locationName;
                        locationObj.addressComponents = gmapContext.addressComponents;
                        return {
                            map: gmapContext.map,
                            marker: gmapContext.marker,
                            location: locationObj
                        }
                    } else {
                        return null;
                    }
            }
            return null;
        }
        return this.each(function() {
            var $target = $(this);
            // If plug-in hasn't been applied before - initialize, otherwise - skip
            if (isPluginApplied(this)) return;
            // Plug-in initialization is required
            // Defaults
            var settings = $.extend({}, $.fn.locationpicker.defaults, options );
            // Initialize
            var gmapContext = new GMapContext(this, {
                zoom: settings.zoom,
                center: new google.maps.LatLng(settings.location.latitude, settings.location.longitude),
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                mapTypeControl: false,
                disableDoubleClickZoom: false,
                scrollwheel: settings.scrollwheel,
                streetViewControl: false,
                radius: settings.radius,
                locationName: settings.locationName,
                settings: settings,
                draggable: settings.draggable
            });
            $target.data("locationpicker", gmapContext);
            // Subscribe GMap events
            google.maps.event.addListener(gmapContext.marker, "dragend", function(event) {
                GmUtility.setPosition(gmapContext, gmapContext.marker.position, function(context){
                    var currentLocation = GmUtility.locationFromLatLng(gmapContext.location);
                    context.settings.onchanged.apply(gmapContext.domContainer, [currentLocation, context.radius, true]);
                    updateInputValues(gmapContext.settings.inputBinding, gmapContext);
                });
            });
            GmUtility.setPosition(gmapContext, new google.maps.LatLng(settings.location.latitude, settings.location.longitude), function(context){
                updateInputValues(settings.inputBinding, gmapContext);
                context.settings.oninitialized($target);
                var currentLocation = GmUtility.locationFromLatLng(gmapContext.location);
                settings.onchanged.apply(gmapContext.domContainer, [currentLocation, context.radius, false]);
            });
            // Set up input bindings if needed
            setupInputListenersInput(settings.inputBinding, gmapContext);
        });
    };
    $.fn.locationpicker.defaults = {
        location: {latitude: 40.7324319, longitude: -73.82480799999996},
        locationName: "",
        radius: 500,
        zoom: 15,
        scrollwheel: true,
        inputBinding: {
            latitudeInput: null,
            longitudeInput: null,
            radiusInput: null,
            locationNameInput: null
        },
        enableAutocomplete: false,
        enableReverseGeocode: true,
        draggable: true,
        onchanged: function(currentLocation, radius, isMarkerDropped) {},
        onlocationnotfound: function(locationName) {},
        oninitialized: function (component) {}

    }

}( jQuery ));
<commit_msg>Update: fire change event on bound input changes<commit_after>(function ( $ ) {

    /**
     * Holds google map object and related utility entities.
     * @constructor
     */
    function GMapContext(domElement, options) {
        var _map = new google.maps.Map(domElement, options);
        var _marker = new google.maps.Marker({
            position: new google.maps.LatLng(54.19335, -3.92695),
            map: _map,
            title: "Drag Me",
            draggable: options.draggable
        });
        return {
            map: _map,
            marker: _marker,
            circle: null,
            location: _marker.position,
            radius: options.radius,
            locationName: options.locationName,
            addressComponents: {
                formatted_address: null,
                addressLine1: null,
                addressLine2: null,
                streetName: null,
                streetNumber: null,
                city: null,
                state: null,
                stateOrProvince: null
            },
            settings: options.settings,
            domContainer: domElement,
            geodecoder: new google.maps.Geocoder()
        }
    }

    // Utility functions for Google Map Manipulations
    var GmUtility = {
        /**
         * Draw a circle over the the map. Returns circle object.
         * Also writes new circle object in gmapContext.
         *
         * @param center - LatLng of the center of the circle
         * @param radius - radius in meters
         * @param gmapContext - context
         * @param options
         */
        drawCircle: function(gmapContext, center, radius, options) {
            if (gmapContext.circle != null) {
                gmapContext.circle.setMap(null);
            }
            if (radius > 0) {
                radius *= 1;
                options = $.extend({
                    strokeColor: "#0000FF",
                    strokeOpacity: 0.35,
                    strokeWeight: 2,
                    fillColor: "#0000FF",
                    fillOpacity: 0.20
                }, options);
                options.map = gmapContext.map;
                options.radius = radius;
                options.center = center;
                gmapContext.circle = new google.maps.Circle(options);
                return gmapContext.circle;
            }
            return null;
        },
        /**
         *
         * @param gMapContext
         * @param location
         * @param callback
         */
        setPosition: function(gMapContext, location, callback) {
            gMapContext.location = location;
            gMapContext.marker.setPosition(location);
            gMapContext.map.panTo(location);
            this.drawCircle(gMapContext, location, gMapContext.radius, {});
            if (gMapContext.settings.enableReverseGeocode) {
                gMapContext.geodecoder.geocode({latLng: gMapContext.location}, function(results, status){
                    if (status == google.maps.GeocoderStatus.OK && results.length > 0){
                        gMapContext.locationName = results[0].formatted_address;
                        gMapContext.addressComponents =
                            GmUtility.address_component_from_google_geocode(results[0].address_components);
                    }
                    if (callback) {
                        callback.call(this, gMapContext);
                    }
                });
            } else {
                if (callback) {
                    callback.call(this, gMapContext);
                }
            }

        },
        locationFromLatLng: function(lnlg) {
            return {latitude: lnlg.lat(), longitude: lnlg.lng()}
        },
        address_component_from_google_geocode: function(address_components) {
            var result = {};
            for (var i = address_components.length-1; i>=0; i--) {
                var component = address_components[i];
                // Postal code
                if (component.types.indexOf('postal_code') >= 0) {
                    result.postalCode = component.short_name;
                }
                // Street number
                else if (component.types.indexOf('street_number') >= 0) {
                    result.streetNumber = component.short_name;
                }
                // Street name
                else if (component.types.indexOf('route') >= 0) {
                    result.streetName = component.short_name;
                }
                // City
                else if (component.types.indexOf('sublocality') >= 0) {
                    result.city = component.short_name;
                }
                // State \ Province
                else if (component.types.indexOf('administrative_area_level_1') >= 0) {
                    result.stateOrProvince = component.short_name;
                }
                // State \ Province
                else if (component.types.indexOf('country') >= 0) {
                    result.country = component.short_name;
                }
            }
            result.addressLine1 = [result.streetNumber, result.streetName].join(' ').trim();
            result.addressLine2 = '';
            return result;
        }
    };

    function isPluginApplied(domObj) {
        return getContextForElement(domObj) != undefined;
    }

    function getContextForElement(domObj) {
        return $(domObj).data("locationpicker");
    }

    function updateInputValues(inputBinding, gmapContext){
        if (!inputBinding) return;
        var currentLocation = GmUtility.locationFromLatLng(gmapContext.location);
        if (inputBinding.latitudeInput) {
            inputBinding.latitudeInput.val(currentLocation.latitude).change();
        }
        if (inputBinding.longitudeInput) {
            inputBinding.longitudeInput.val(currentLocation.longitude).change();
        }
        if (inputBinding.radiusInput) {
            inputBinding.radiusInput.val(gmapContext.radius).change();
        }
        if (inputBinding.locationNameInput) {
            inputBinding.locationNameInput.val(gmapContext.locationName).change();
        }
    }

    function setupInputListenersInput(inputBinding, gmapContext) {
        if (inputBinding) {
            if (inputBinding.radiusInput){
                inputBinding.radiusInput.on("change", function() {
                    gmapContext.radius = $(this).val();
                    GmUtility.setPosition(gmapContext, gmapContext.location, function(context){
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
            if (inputBinding.locationNameInput && gmapContext.settings.enableAutocomplete) {
                gmapContext.autocomplete = new google.maps.places.Autocomplete(inputBinding.locationNameInput.get(0));
                google.maps.event.addListener(gmapContext.autocomplete, 'place_changed', function() {
                    var place = gmapContext.autocomplete.getPlace();
                    if (!place.geometry) {
                        gmapContext.settings.onlocationnotfound(place.name);
                        return;
                    }
                    GmUtility.setPosition(gmapContext, place.geometry.location, function(context) {
                        updateInputValues(inputBinding, context);
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
            if (inputBinding.latitudeInput) {
                inputBinding.latitudeInput.on("change", function() {
                    GmUtility.setPosition(gmapContext, new google.maps.LatLng($(this).val(), gmapContext.location.lng()), function(context){
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
            if (inputBinding.longitudeInput) {
                inputBinding.longitudeInput.on("change", function() {
                    GmUtility.setPosition(gmapContext, new google.maps.LatLng(gmapContext.location.lat(), $(this).val()), function(context){
                        context.settings.onchanged.apply(gmapContext.domContainer,
                            [GmUtility.locationFromLatLng(context.location), context.radius, false]);
                    });
                });
            }
        }
    }

    /**
     * Initialization:
     *  $("#myMap").locationpicker(options);
     * @param options
     * @param params
     * @returns {*}
     */
    $.fn.locationpicker = function( options, params ) {
        if (typeof options == 'string') { // Command provided
            var _targetDomElement = this.get(0);
            // Plug-in is not applied - nothing to do.
            if (!isPluginApplied(_targetDomElement)) return;
            var gmapContext = getContextForElement(_targetDomElement);
            switch (options) {
                case "location":
                    if (params == undefined) { // Getter
                        var location = GmUtility.locationFromLatLng(gmapContext.location);
                        location.radius = gmapContext.radius;
                        location.name = gmapContext.locationName;
                        return location;
                    } else { // Setter
                        if (params.radius) {
                            gmapContext.radius = params.radius;
                        }
                        GmUtility.setPosition(gmapContext, new google.maps.LatLng(params.latitude, params.longitude), function(gmapContext) {
                            updateInputValues(gmapContext.settings.inputBinding, gmapContext);
                        });
                    }
                    break;
                case "subscribe":
                    /**
                     * Provides interface for subscribing for GoogleMap events.
                     * See Google API documentation for details.
                     * Parameters:
                     * - event: string, name of the event
                     * - callback: function, callback function to be invoked
                     */
                    if (params == undefined) { // Getter is not available
                        return null;
                    } else {
                        var event = params.event;
                        var callback = params.callback;
                        if (!event || ! callback) {
                            console.error("LocationPicker: Invalid arguments for method \"subscribe\"")
                            return null;
                        }
                        google.maps.event.addListener(gmapContext.map, event, callback);
                    }
                    break;
                case "map":
                    /**
                     * Returns object which allows access actual google widget and marker paced on it.
                     * Structure: {
                     *  map: Instance of the google map widget
                     *  marker: marker placed on map
                     * }
                     */
                    if (params == undefined) { // Getter is not available
                        var locationObj = GmUtility.locationFromLatLng(gmapContext.location);
                        locationObj.formattedAddress = gmapContext.locationName;
                        locationObj.addressComponents = gmapContext.addressComponents;
                        return {
                            map: gmapContext.map,
                            marker: gmapContext.marker,
                            location: locationObj
                        }
                    } else {
                        return null;
                    }
            }
            return null;
        }
        return this.each(function() {
            var $target = $(this);
            // If plug-in hasn't been applied before - initialize, otherwise - skip
            if (isPluginApplied(this)) return;
            // Plug-in initialization is required
            // Defaults
            var settings = $.extend({}, $.fn.locationpicker.defaults, options );
            // Initialize
            var gmapContext = new GMapContext(this, {
                zoom: settings.zoom,
                center: new google.maps.LatLng(settings.location.latitude, settings.location.longitude),
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                mapTypeControl: false,
                disableDoubleClickZoom: false,
                scrollwheel: settings.scrollwheel,
                streetViewControl: false,
                radius: settings.radius,
                locationName: settings.locationName,
                settings: settings,
                draggable: settings.draggable
            });
            $target.data("locationpicker", gmapContext);
            // Subscribe GMap events
            google.maps.event.addListener(gmapContext.marker, "dragend", function(event) {
                GmUtility.setPosition(gmapContext, gmapContext.marker.position, function(context){
                    var currentLocation = GmUtility.locationFromLatLng(gmapContext.location);
                    context.settings.onchanged.apply(gmapContext.domContainer, [currentLocation, context.radius, true]);
                    updateInputValues(gmapContext.settings.inputBinding, gmapContext);
                });
            });
            GmUtility.setPosition(gmapContext, new google.maps.LatLng(settings.location.latitude, settings.location.longitude), function(context){
                updateInputValues(settings.inputBinding, gmapContext);
                context.settings.oninitialized($target);
                var currentLocation = GmUtility.locationFromLatLng(gmapContext.location);
                settings.onchanged.apply(gmapContext.domContainer, [currentLocation, context.radius, false]);
            });
            // Set up input bindings if needed
            setupInputListenersInput(settings.inputBinding, gmapContext);
        });
    };
    $.fn.locationpicker.defaults = {
        location: {latitude: 40.7324319, longitude: -73.82480799999996},
        locationName: "",
        radius: 500,
        zoom: 15,
        scrollwheel: true,
        inputBinding: {
            latitudeInput: null,
            longitudeInput: null,
            radiusInput: null,
            locationNameInput: null
        },
        enableAutocomplete: false,
        enableReverseGeocode: true,
        draggable: true,
        onchanged: function(currentLocation, radius, isMarkerDropped) {},
        onlocationnotfound: function(locationName) {},
        oninitialized: function (component) {}

    }

}( jQuery ));
<|endoftext|>


<commit_before>const fs = require('fs');
const {ipcRenderer} = require('electron');
const Vue = require('vue');
const compiler = require('vue-template-compiler');

class ElectronVue extends Vue {
    constructor(...args) {
        if(args.length)
            ElectronVue.processArguments(args[0]);
        super(...args);
    }

    /**
     * This method processes any incoming arguments and supplements them
     * for ElectronVue's needs before any Vue initialization happens.
     * @param {object} args The object defining a Vue as passed into this object's constructor
     */
    static processArguments(args) {
        // This makes sure we have a data object and if
        // it's a function then it's converted to an object.
        // Converting the function to an object might limit the
        // ability of callers to implement more dynamic data setup
        // procedures.  This should probably be addressed in a future
        // release.
        if(!args.data)
            args.data = {};
        else if(typeof args.data != 'object')
            args.data = args.data();

        // mix ev object with defaults
        args.data.electronVue = Object.assign({
            errorText: 'EV Template Error',
            errorStyles: {
                color: '#fff',
                background: '#f50000',
                textAlign: 'center',
                padding: '10px',
                borderRadius: '4px'
            }
        }, args.data.electronVue);

        // if there's not already a render method then generate one from
        // the template attribute
        if(!args.render)
            Object.assign(args, ElectronVue.createRenderer(args.template));

        // register local components which just means to convert component
        // template attributes to render functions
        if(args.components)
            ElectronVue.processComponents(args.components);

        // register a callback to use for ipc registration once the vue has
        // been instantiated
        args.mixins = [{
            created() {
                if(args.ipc)
                    ElectronVue.ipcRegistration(this, args.ipc);
            },
            destroyed() {
                if(args.ipc)
                    ElectronVue.ipcRemoveListeners(this, args.ipc);
            }
        }];
    }

    /**
     * This function recusively loops through components and creates
     * render functions for template attributes.
     * @see {@link https://vuejs.org/v2/guide/components.html#Local-Registration}
     * @param {object} components Local registration component object
     */
    static processComponents(components) {
        for(let name in components) {
            let component = components[name];

            // register callbacks for each component so that their ipc callbacks
            // can be registered once each component has been instantiated
            component.mixins = [{
                created() {
                    if(component.ipc)
                        ElectronVue.ipcRegistration(this, component.ipc);
                },
                destroyed() {
                    if(component.ipc)
                        ElectronVue.ipcRemoveListeners(this, component.ipc);
                }
            }];

            // if there's not a render method then generate one from the
            // template attribute
            if(!component.render)
                Object.assign(component, ElectronVue.createRenderer(component.template));

            // if this component has child components then recursively
            // register them too
            if(component.components)
                ElectronVue.processComponents(component.components);
        }
    }

    /**
     * This function adds the ability to reference html files
     * as templates in addition to the standard vue implementation.
     * @param {string} template Template value
     * @return {function} Vue render method
     */
    static createRenderer(template) {
        if(template && template.endsWith('.html')) {
            try {
                template = fs.readFileSync(template, 'utf-8');
            } catch(err) {
                console.error(err);
                template = '

';
            }
        }

        return compiler.compileToFunctions(template);
    }

    /**
     * This registers all functions as callbacks for the main process.
     * @param {object} thisArg A value to bind with callbacks so that they have an appropriate this.
     * @param {string} ipcCallbacks An object of callback methods to be registered.
     */
    static ipcRegistration(thisArg, ipc) {
        thisArg.$electronVue = {
            ipcCallbacks: []
        };

        for(const name in ipc) {
            const callback = ipc[name];
            let channel = ElectronVue.toSpinalCase(name);
            let boundCallback = callback.bind(thisArg);
            if(typeof callback == 'object') {
                channel = ElectronVue.toSpinalCase(callback.channel);
                boundCallback = callback.method.bind(thisArg);
            }

            thisArg.$electronVue.ipcCallbacks.push({channel: channel, method: boundCallback});
            ipcRenderer.on(channel, boundCallback);
        }
    }

    /**
     * This unregisters all ipc callback methods currently registered for this instance.  In other words,
     * this removes all channels previously registered with the ipcRenderer.
     * @param {object} thisArg The vue instance containing registered ipc callbacks.
     */
    static ipcRemoveListeners(thisArg) {
        if(thisArg.$electronVue && this.$electronVue.ipcCallbacks) {
            for(const callback of thisArg.$electronVue.ipcCallbacks) {
                ipcRenderer.removeListener(callback.channel, callback.method);
            }
        }
    }

    /**
     * This function converts camelcase strings into spinalcase
     * by attempting to break the string into words based on
     * capitalization.
     * @param {string} value The camelcase value to convert.
     * @return {string} A spinalcase version of the camelcase value.
     */
    static toSpinalCase(value) {
        let words = [];
        let lower = false;
        let wasLower = lower;

        for(const c of value) {
            if(!words.length) {
                words.push([c]);
                if(c == c.toLowerCase()) {
                    lower = true;
                    wasLower = true;
                }
            } else {
                lower = false;
                if(c == c.toLowerCase())
                    lower = true;

                const lastWordIndex = words.length - 1;
                if(lower != wasLower) {
                    const lastWordLastIndex = words[lastWordIndex].length - 1;
                    const lastChar = words[lastWordIndex][lastWordLastIndex];

                    // Okay, this gets a little dicey.  If the last character of the
                    // last word is uppercase then some things need to be shuffled
                    // around a bit.  Otherwise, just start a new word with the
                    // current character as the first letter in the word.
                    if(lastChar == lastChar.toUpperCase()) {
                        // The last character of the previous word was uppercase.
                        // Therefore, remove it from the previous word and make it
                        // the first character in the new word.
                        words[lastWordIndex].pop();

                        // If the previous word is empty after removing the last
                        // character, then remove it from the list of words so that
                        // we don't end up with a blank word.
                        if(!words[lastWordIndex].length)
                            words.splice(lastWordIndex, 1);

                        // Lastly, push the last character of the previous word and
                        // the current character into a new word as the first two
                        // characters.
                        words.push([lastChar, c]);
                    } else
                        words.push([c]);
                } else
                    words[lastWordIndex].push(c);

                wasLower = lower;
            }
        }

        // This just takes the list of words and smashes them together to
        // produce a lowercase string with hyphens between each word.
        return words.map((word) => {
            return word.join('');
        }).join('-').toLowerCase();
    }
}

module.exports = ElectronVue;
<commit_msg>refs #5 ipc binding fix for wrong this<commit_after>const fs = require('fs');
const {ipcRenderer} = require('electron');
const Vue = require('vue');
const compiler = require('vue-template-compiler');

class ElectronVue extends Vue {
    constructor(...args) {
        if(args.length)
            ElectronVue.processArguments(args[0]);
        super(...args);
    }

    /**
     * This method processes any incoming arguments and supplements them
     * for ElectronVue's needs before any Vue initialization happens.
     * @param {object} args The object defining a Vue as passed into this object's constructor
     */
    static processArguments(args) {
        // This makes sure we have a data object and if
        // it's a function then it's converted to an object.
        // Converting the function to an object might limit the
        // ability of callers to implement more dynamic data setup
        // procedures.  This should probably be addressed in a future
        // release.
        if(!args.data)
            args.data = {};
        else if(typeof args.data != 'object')
            args.data = args.data();

        // mix ev object with defaults
        args.data.electronVue = Object.assign({
            errorText: 'EV Template Error',
            errorStyles: {
                color: '#fff',
                background: '#f50000',
                textAlign: 'center',
                padding: '10px',
                borderRadius: '4px'
            }
        }, args.data.electronVue);

        // if there's not already a render method then generate one from
        // the template attribute
        if(!args.render)
            Object.assign(args, ElectronVue.createRenderer(args.template));

        // register local components which just means to convert component
        // template attributes to render functions
        if(args.components)
            ElectronVue.processComponents(args.components);

        // register a callback to use for ipc registration once the vue has
        // been instantiated
        args.mixins = [{
            created() {
                if(args.ipc)
                    ElectronVue.ipcRegistration(this, args.ipc);
            },
            destroyed() {
                if(args.ipc)
                    ElectronVue.ipcRemoveListeners(this, args.ipc);
            }
        }];
    }

    /**
     * This function recusively loops through components and creates
     * render functions for template attributes.
     * @see {@link https://vuejs.org/v2/guide/components.html#Local-Registration}
     * @param {object} components Local registration component object
     */
    static processComponents(components) {
        for(let name in components) {
            let component = components[name];

            // register callbacks for each component so that their ipc callbacks
            // can be registered once each component has been instantiated
            component.mixins = [{
                created() {
                    if(component.ipc)
                        ElectronVue.ipcRegistration(this, component.ipc);
                },
                destroyed() {
                    if(component.ipc)
                        ElectronVue.ipcRemoveListeners(this, component.ipc);
                }
            }];

            // if there's not a render method then generate one from the
            // template attribute
            if(!component.render)
                Object.assign(component, ElectronVue.createRenderer(component.template));

            // if this component has child components then recursively
            // register them too
            if(component.components)
                ElectronVue.processComponents(component.components);
        }
    }

    /**
     * This function adds the ability to reference html files
     * as templates in addition to the standard vue implementation.
     * @param {string} template Template value
     * @return {function} Vue render method
     */
    static createRenderer(template) {
        if(template && template.endsWith('.html')) {
            try {
                template = fs.readFileSync(template, 'utf-8');
            } catch(err) {
                console.error(err);
                template = '

';
            }
        }

        return compiler.compileToFunctions(template);
    }

    /**
     * This registers all functions as callbacks for the main process.
     * @param {object} thisArg A value to bind with callbacks so that they have an appropriate this.
     * @param {string} ipcCallbacks An object of callback methods to be registered.
     */
    static ipcRegistration(thisArg, ipc) {
        thisArg.$electronVue = {
            ipcCallbacks: []
        };

        for(const name in ipc) {
            const callback = ipc[name];
            let channel = ElectronVue.toSpinalCase(name);
            let boundCallback = callback.bind(thisArg);
            if(typeof callback == 'object') {
                channel = ElectronVue.toSpinalCase(callback.channel);
                boundCallback = callback.method.bind(thisArg);
            }

            thisArg.$electronVue.ipcCallbacks.push({channel: channel, method: boundCallback});
            ipcRenderer.on(channel, boundCallback);
        }
    }

    /**
     * This unregisters all ipc callback methods currently registered for this instance.  In other words,
     * this removes all channels previously registered with the ipcRenderer.
     * @param {object} thisArg The vue instance containing registered ipc callbacks.
     */
    static ipcRemoveListeners(thisArg) {
        if(thisArg.$electronVue && thisArg.$electronVue.ipcCallbacks) {
            for(const callback of thisArg.$electronVue.ipcCallbacks) {
                ipcRenderer.removeListener(callback.channel, callback.method);
            }
        }
    }

    /**
     * This function converts camelcase strings into spinalcase
     * by attempting to break the string into words based on
     * capitalization.
     * @param {string} value The camelcase value to convert.
     * @return {string} A spinalcase version of the camelcase value.
     */
    static toSpinalCase(value) {
        let words = [];
        let lower = false;
        let wasLower = lower;

        for(const c of value) {
            if(!words.length) {
                words.push([c]);
                if(c == c.toLowerCase()) {
                    lower = true;
                    wasLower = true;
                }
            } else {
                lower = false;
                if(c == c.toLowerCase())
                    lower = true;

                const lastWordIndex = words.length - 1;
                if(lower != wasLower) {
                    const lastWordLastIndex = words[lastWordIndex].length - 1;
                    const lastChar = words[lastWordIndex][lastWordLastIndex];

                    // Okay, this gets a little dicey.  If the last character of the
                    // last word is uppercase then some things need to be shuffled
                    // around a bit.  Otherwise, just start a new word with the
                    // current character as the first letter in the word.
                    if(lastChar == lastChar.toUpperCase()) {
                        // The last character of the previous word was uppercase.
                        // Therefore, remove it from the previous word and make it
                        // the first character in the new word.
                        words[lastWordIndex].pop();

                        // If the previous word is empty after removing the last
                        // character, then remove it from the list of words so that
                        // we don't end up with a blank word.
                        if(!words[lastWordIndex].length)
                            words.splice(lastWordIndex, 1);

                        // Lastly, push the last character of the previous word and
                        // the current character into a new word as the first two
                        // characters.
                        words.push([lastChar, c]);
                    } else
                        words.push([c]);
                } else
                    words[lastWordIndex].push(c);

                wasLower = lower;
            }
        }

        // This just takes the list of words and smashes them together to
        // produce a lowercase string with hyphens between each word.
        return words.map((word) => {
            return word.join('');
        }).join('-').toLowerCase();
    }
}

module.exports = ElectronVue;
<|endoftext|>


<commit_before>var ReactiveProperty = require("./reactive-property.js");
var Graph = require("./graph-data-structure.js");

var errors = {
  notASetter: Error("You cannot set the value of a reactive function directly.")
};

// The singleton data dependency graph.
// Nodes are reactive properties and reactive functions.
// Edges represent dependencies of reactive functions.
var graph = Graph();

// This object accumulates nodes that have changed.
// Keys are node ids, values are truthy (the object acts like a Set).
var changedNodes = {};

// A lookup table for properties based on their assigned id.
var propertiesById = {};

// Looks up a property by its id.
function lookup(id){ return propertiesById[id]; }

// Assigns ids to properties for use as nodes in the graph.
var assignId = (function(){
  var idCounter = 1;
  return function (property){
    if(!property.id){
      property.id = idCounter++;
      propertiesById[property.id] = property;
    }
  };
}());

// Gets a property value.
function get(property){ return property(); }

//ReactiveFunction(dependencies... , callback)
function ReactiveFunction(){
  var dependencies = Array.apply(null, arguments);
  var callback = dependencies.pop();
  var value;

  var reactiveFunction = function (){
    if(arguments.length > 0){ throw errors.notASetter; }
    return value;
  };

  reactiveFunction.evaluate = function (){
    value = callback.apply(null, dependencies.map(get));
  };

  assignId(reactiveFunction);
  dependencies.forEach(function (dependency){
    assignId(dependency);
    graph.addEdge(dependency.id, reactiveFunction.id);
  });

  var properties = dependencies
    .filter(function (dependency){
      return dependency.on;
    });

  var listeners = properties
    .map(function (property){
      return property.on(function (){
        changedNodes[property.id] = true;
        queueDigest();
      });
    });

  reactiveFunction.destroy = function (){

    // TODO test
    listeners.forEach(function (listener, i){
      properties[i].off(listener);
    });

    // TODO test
    dependencies.forEach(function (dependency){
      graph.removeEdge(dependency.id, reactiveFunction.id);
    });

  };

  return reactiveFunction;
}

ReactiveFunction.digest = function (){
  graph
    .topologicalSort(Object.keys(changedNodes))
    .map(lookup)
    .forEach(function (reactiveFunction){
      reactiveFunction.evaluate();
    });
  changedNodes = {};
};

var queueDigest = debounce(ReactiveFunction.digest);

function debounce(fn){
  var queued = false;
  return function () {
    if(!queued){
      queued = true;
      setTimeout(function () {
        queued = false;
        fn();
      }, 0);
    }
  };
};

// TESTS /////////////////////////////////////////////////////////////////////
var assert = require("assert");

// Should depend on two reactive properties.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);

var c = ReactiveFunction(a, b, function (a, b){
  return a + b;
});

ReactiveFunction.digest();

assert.equal(c(), 15);

// Should depend on any number of reactive properties.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);
var c = ReactiveProperty(15);

var d = ReactiveFunction(a, function (a){ return a * 2; });
var e = ReactiveFunction(a, b, c, function (a, b, c){ return a + b + c; });

ReactiveFunction.digest();

assert.equal(d(), 10);
assert.equal(e(), 30);

// Should depend on a reactive function.
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){ return a * 2; });
var c = ReactiveFunction(b, function (b){ return b / 2; });
ReactiveFunction.digest();
assert.equal(c(), 5);

// Should depend on a reactive property and a reactive function.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);
var c = ReactiveFunction(a, function (a){ return a * 2; });
var d = ReactiveFunction(b, c, function (b, c){ return b + c; });
ReactiveFunction.digest();
assert.equal(d(), 20);

// Should handle tricky case.
//      a
//     / \
//    b   |
//    |   d
//    c   |
//     \ /
//      e
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){ return a * 2; });
var c = ReactiveFunction(b, function (b){ return b + 5; });
var d = ReactiveFunction(a, function (a){ return a * 3; });
var e = ReactiveFunction(c, d, function (c, d){ return c + d; });
ReactiveFunction.digest();
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));

// Should throw an error if attempting to set the value directly.
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){ return a * 2; });
try{
  b(5);
} catch (err){
  console.log("Error thrown correctly.");
}

// Should clear changed nodes on digest.
var numInvocations = 0;
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){
  numInvocations++;
  return a * 2;
});
ReactiveFunction.digest();
ReactiveFunction.digest();
assert.equal(numInvocations, 1);

// Should automatically digest on next tick.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);

var c = ReactiveFunction(a, b, function (a, b){
  return a + b;
});

setTimeout(function (){
  assert.equal(c(), 15);
  //done();
}, 0);

<commit_msg>Add comments, clean up<commit_after>var ReactiveProperty = require("./reactive-property.js");
var Graph = require("./graph-data-structure.js");

var errors = {
  notASetter: Error("You cannot set the value of a reactive function directly.")
};

// The singleton data dependency graph.
// Nodes are reactive properties and reactive functions.
// Edges represent dependencies of reactive functions.
var graph = Graph();

// This object accumulates nodes that have changed since the last digest.
// Keys are node ids, values are truthy (the object acts like a Set).
var changedNodes = {};

// A lookup table for properties based on their assigned id.
var propertiesById = {};

// Assigns ids to properties for use as nodes in the graph.
var assignId = (function(){
  var idCounter = 1;
  return function (property){
    if(!property.id){
      property.id = idCounter++;
      propertiesById[property.id] = property;
    }
  };
}());

//ReactiveFunction(dependencies... , callback)
function ReactiveFunction(){

  // Parse arguments.
  var dependencies = Array.apply(null, arguments);
  var callback = dependencies.pop();

  // This stores the output value.
  var value;

  // The returned reactive function acts as a getter (not a setter).
  var reactiveFunction = function (){
    if(arguments.length > 0){ throw errors.notASetter; }
    return value;
  };

  // This gets invoked during a digest, after dependencies have been evaluated.
  reactiveFunction.evaluate = function (){
    value = callback.apply(null, dependencies.map(function (dependency){
      return dependency();
    }));
  };

  // Assign node ids to dependencies and the reactive function.
  assignId(reactiveFunction);
  dependencies.forEach(assignId);

  // Set up edges in the graph from each dependency.
  dependencies.forEach(function (dependency){
    graph.addEdge(dependency.id, reactiveFunction.id);
  });

  // Compute which of the dependencies are properties (excludes reactive functions).
  var properties = dependencies
    .filter(function (dependency){
      return dependency.on;
    });

  // Add change listeners to each dependency that is a property.
  var listeners = properties
    .map(function (property){
      return property.on(function (){
        changedNodes[property.id] = true;
        queueDigest();
      });
    });

  // This function must be called to explicitly destroy a reactive function.
  // Garbage collection is not enough, as we have added listeners and edges.
  reactiveFunction.destroy = function (){

    // TODO test
    // Remove change listeners from dependencies that are properties.
    listeners.forEach(function (listener, i){
      properties[i].off(listener);
    });

    // TODO test
    // Remove the edges that were added to the dependency graph.
    dependencies.forEach(function (dependency){
      graph.removeEdge(dependency.id, reactiveFunction.id);
    });

  };

  return reactiveFunction;
}

// Propagates changes through the dependency graph.
ReactiveFunction.digest = function (){
  graph
    .topologicalSort(Object.keys(changedNodes))
    .map(function (id){
      return propertiesById[id];
    })
    .forEach(function (reactiveFunction){
      reactiveFunction.evaluate();
    });
  changedNodes = {};
};

// This function queues a digest at the next tick of the event loop.
var queueDigest = debounce(ReactiveFunction.digest);

// Similar to http://underscorejs.org/#debounce
function debounce(fn){
  var queued = false;
  return function () {
    if(!queued){
      queued = true;
      setTimeout(function () {
        queued = false;
        fn();
      }, 0);
    }
  };
};

// TESTS /////////////////////////////////////////////////////////////////////
var assert = require("assert");

// Should depend on two reactive properties.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);

var c = ReactiveFunction(a, b, function (a, b){
  return a + b;
});

ReactiveFunction.digest();

assert.equal(c(), 15);

// Should depend on any number of reactive properties.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);
var c = ReactiveProperty(15);

var d = ReactiveFunction(a, function (a){ return a * 2; });
var e = ReactiveFunction(a, b, c, function (a, b, c){ return a + b + c; });

ReactiveFunction.digest();

assert.equal(d(), 10);
assert.equal(e(), 30);

// Should depend on a reactive function.
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){ return a * 2; });
var c = ReactiveFunction(b, function (b){ return b / 2; });
ReactiveFunction.digest();
assert.equal(c(), 5);

// Should depend on a reactive property and a reactive function.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);
var c = ReactiveFunction(a, function (a){ return a * 2; });
var d = ReactiveFunction(b, c, function (b, c){ return b + c; });
ReactiveFunction.digest();
assert.equal(d(), 20);

// Should handle tricky case.
//      a
//     / \
//    b   |
//    |   d
//    c   |
//     \ /
//      e
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){ return a * 2; });
var c = ReactiveFunction(b, function (b){ return b + 5; });
var d = ReactiveFunction(a, function (a){ return a * 3; });
var e = ReactiveFunction(c, d, function (c, d){ return c + d; });
ReactiveFunction.digest();
assert.equal(e(), ((a() * 2) + 5) + (a() * 3));

// Should throw an error if attempting to set the value directly.
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){ return a * 2; });
try{
  b(5);
} catch (err){
  console.log("Error thrown correctly.");
}

// Should clear changed nodes on digest.
var numInvocations = 0;
var a = ReactiveProperty(5);
var b = ReactiveFunction(a, function (a){
  numInvocations++;
  return a * 2;
});
ReactiveFunction.digest();
ReactiveFunction.digest();
assert.equal(numInvocations, 1);

// Should automatically digest on next tick.
var a = ReactiveProperty(5);
var b = ReactiveProperty(10);

var c = ReactiveFunction(a, b, function (a, b){
  return a + b;
});

setTimeout(function (){
  assert.equal(c(), 15);
  //done();
}, 0);

<|endoftext|>


<commit_before>var detect = require('rtc-core/detect');
var findPlugin = require('rtc-core/plugin');
var PriorityQueue = require('priorityqueuejs');
var pluck = require('whisk/pluck');
var pluckSessionDesc = pluck('sdp', 'type');

// some validation routines
var checkCandidate = require('rtc-validator/candidate');

// the sdp cleaner
var sdpclean = require('rtc-sdpclean');
var parseSdp = require('rtc-sdp');

var PRIORITY_LOW = 100;
var PRIORITY_WAIT = 1000;

// priority order (lower is better)
var DEFAULT_PRIORITIES = [
  'createOffer',
  'setLocalDescription',
  'createAnswer',
  'setRemoteDescription',
  'addIceCandidate'
];

// define event mappings
var METHOD_EVENTS = {
  setLocalDescription: 'setlocaldesc',
  setRemoteDescription: 'setremotedesc',
  createOffer: 'offer',
  createAnswer: 'answer'
};

var MEDIA_MAPPINGS = {
  data: 'application'
};

// define states in which we will attempt to finalize a connection on receiving a remote offer
var VALID_RESPONSE_STATES = ['have-remote-offer', 'have-local-pranswer'];

/**
  Allows overriding of a function
 **/
function pluggable(pluginFn, defaultFn) {
  return (pluginFn && typeof pluginFn == 'function' ? pluginFn : defaultFn);
}

/**
  # rtc-taskqueue

  This is a package that assists with applying actions to an `RTCPeerConnection`
  in as reliable order as possible. It is primarily used by the coupling logic
  of the [`rtc-tools`](https://github.com/rtc-io/rtc-tools).

  ## Example Usage

  For the moment, refer to the simple coupling test as an example of how to use
  this package (see below):

  <<< test/couple.js

**/
module.exports = function(pc, opts) {
  opts = opts || {};
  // create the task queue
  var queue = new PriorityQueue(orderTasks);
  var tq = require('mbus')('', (opts || {}).logger);

  // initialise task importance
  var priorities = (opts || {}).priorities || DEFAULT_PRIORITIES;
  var queueInterval = (opts || {}).interval || 10;

  // check for plugin usage
  var plugin = findPlugin((opts || {}).plugins);

  // initialise state tracking
  var checkQueueTimer = 0;
  var defaultFail = tq.bind(tq, 'fail');

  // look for an sdpfilter function (allow slight mis-spellings)
  var sdpFilter = (opts || {}).sdpfilter || (opts || {}).sdpFilter;
  var alwaysParse = (opts.sdpParseMode === 'always');

  // initialise session description and icecandidate objects
  var RTCSessionDescription = (opts || {}).RTCSessionDescription ||
    detect('RTCSessionDescription');

  var RTCIceCandidate = (opts || {}).RTCIceCandidate ||
    detect('RTCIceCandidate');

  // Determine plugin overridable methods
  var createIceCandidate = pluggable(plugin && plugin.createIceCandidate, function(data) {
    return new RTCIceCandidate(data);
  });

  var createSessionDescription = pluggable(plugin && plugin.createSessionDescription, function(data) {
    return new RTCSessionDescription(data);
  });

  function abortQueue(err) {
    console.error(err);
  }

  function applyCandidate(task, next) {
    var data = task.args[0];
    // Allow selective filtering of ICE candidates
    if (opts && opts.filterCandidate && !opts.filterCandidate(data)) {
      tq('ice.remote.filtered', candidate);
      return next();
    }
    var candidate = data && data.candidate && createIceCandidate(data);

    function handleOk() {
      tq('ice.remote.applied', candidate);
      next();
    }

    function handleFail(err) {
      tq('ice.remote.invalid', candidate);
      next(err);
    }

    // we have a null candidate, we have finished gathering candidates
    if (! candidate) {
      return next();
    }

    pc.addIceCandidate(candidate, handleOk, handleFail);
  }

  function checkQueue() {
    // peek at the next item on the queue
    var next = (! queue.isEmpty()) && queue.peek();
    var ready = next && testReady(next);

    // reset the queue timer
    checkQueueTimer = 0;

    // if we don't have a task ready, then abort
    if (! ready) {
      // if we have a task and it has expired then dequeue it
      if (next && (aborted(next) || expired(next))) {
        tq('task.expire', next);
        queue.deq();
      }

      return (! queue.isEmpty()) && isNotClosed(pc) && triggerQueueCheck();
    }

    // properly dequeue task
    next = queue.deq();

    // process the task
    next.fn(next, function(err) {
      var fail = next.fail || defaultFail;
      var pass = next.pass;
      var taskName = next.name;

      // if errored, fail
      if (err) {
        console.error(taskName + ' task failed: ', err);
        return fail(err);
      }

      if (typeof pass == 'function') {
        pass.apply(next, [].slice.call(arguments, 1));
      }

      // Allow tasks to indicate that processing should continue immediately to the
      // following task
      if (next.immediate) {
        if (checkQueueTimer) clearTimeout(checkQueueTimer);
        return checkQueue();
      } else {
        triggerQueueCheck();
      }
    });
  }

  function cleansdp(desc) {
    // ensure we have clean sdp
    var sdpErrors = [];
    var sdp = desc && sdpclean(desc.sdp, { collector: sdpErrors });

    // if we don't have a match, log some info
    if (desc && sdp !== desc.sdp) {
      console.info('invalid lines removed from sdp: ', sdpErrors);
      desc.sdp = sdp;
    }

    // if a filter has been specified, then apply the filter
    if (typeof sdpFilter == 'function') {
      desc.sdp = sdpFilter(desc.sdp, pc);
    }

    return desc;
  }

  function completeConnection() {
    // Clean any cached media types now that we have potentially new remote description
    if (pc.__mediaTypes) {
      delete pc.__mediaTypes;
    }

    if (VALID_RESPONSE_STATES.indexOf(pc.signalingState) >= 0) {
      return tq.createAnswer();
    }
  }

  function emitSdp() {
    tq('sdp.local', pluckSessionDesc(this.args[0]));
  }

  function enqueue(name, handler, opts) {
    return function() {
      var args = [].slice.call(arguments);

      if (opts && typeof opts.processArgs == 'function') {
        args = args.map(opts.processArgs);
      }

      var priority = priorities.indexOf(name);

      queue.enq({
        args: args,
        name: name,
        fn: handler,
        priority: priority >= 0 ? priority : PRIORITY_LOW,
        immediate: opts.immediate,
        // If aborted, the task will be removed
        aborted: false,

        // record the time at which the task was queued
        start: Date.now(),

        // initilaise any checks that need to be done prior
        // to the task executing
        checks: [ isNotClosed ].concat((opts || {}).checks || []),

        // initialise the pass and fail handlers
        pass: (opts || {}).pass,
        fail: (opts || {}).fail
      });

      triggerQueueCheck();
    };
  }

  function execMethod(task, next) {
    var fn = pc[task.name];
    var eventName = METHOD_EVENTS[task.name] || (task.name || '').toLowerCase();
    var cbArgs = [ success, fail ];
    var isOffer = task.name === 'createOffer';

    function fail(err) {
      tq.apply(tq, [ 'negotiate.error', task.name, err ].concat(task.args));
      next(err);
    }

    function success() {
      tq.apply(tq, [ ['negotiate', eventName, 'ok'], task.name ].concat(task.args));
      next.apply(null, [null].concat([].slice.call(arguments)));
    }

    if (! fn) {
      return next(new Error('cannot call "' + task.name + '" on RTCPeerConnection'));
    }

    // invoke the function
    tq.apply(tq, ['negotiate.' + eventName].concat(task.args));
    fn.apply(
      pc,
      task.args.concat(cbArgs).concat(isOffer ? generateConstraints() : [])
    );
  }

  function expired(task) {
    return (typeof task.ttl == 'number') && (task.start + task.ttl < Date.now());
  }

  function aborted(task) {
    return task && task.aborted;
  }

  function extractCandidateEventData(data) {
    // extract nested candidate data (like we will see in an event being passed to this function)
    while (data && data.candidate && data.candidate.candidate) {
      data = data.candidate;
    }

    return data;
  }

  function generateConstraints() {
    var allowedKeys = {
      offertoreceivevideo: 'OfferToReceiveVideo',
      offertoreceiveaudio: 'OfferToReceiveAudio',
      icerestart: 'IceRestart',
      voiceactivitydetection: 'VoiceActivityDetection'
    };

    var constraints = {
      OfferToReceiveVideo: true,
      OfferToReceiveAudio: true
    };

    // update known keys to match
    Object.keys(opts || {}).forEach(function(key) {
      if (allowedKeys[key.toLowerCase()]) {
        constraints[allowedKeys[key.toLowerCase()]] = opts[key];
      }
    });

    return { mandatory: constraints };
  }

  function hasLocalOrRemoteDesc(pc, task) {
    return pc.__hasDesc || (pc.__hasDesc = !!pc.remoteDescription);
  }

  function isNotNegotiating(pc) {
    return pc.signalingState !== 'have-local-offer';
  }

  function isNotClosed(pc) {
    return pc.signalingState !== 'closed';
  }

  function isStable(pc) {
    return pc.signalingState === 'stable';
  }

  function isValidCandidate(pc, data) {
    return data.__valid ||
      (data.__valid = checkCandidate(data.args[0]).length === 0);
  }

  function isConnReadyForCandidate(pc, data) {
    var sdpMid = data.args[0] && data.args[0].sdpMid;

    // remap media types as appropriate
    sdpMid = MEDIA_MAPPINGS[sdpMid] || sdpMid;

    if (sdpMid === '')
      return true;

    // Allow parsing of SDP always if required
    if (alwaysParse || !pc.__mediaTypes) {
      var sdp = parseSdp(pc.remoteDescription && pc.remoteDescription.sdp);
      // We only want to cache the SDP media types if we've received them, otherwise
      // bad things can happen
      var mediaTypes = sdp.getMediaTypes();
      if (mediaTypes && mediaTypes.length > 0) {
        pc.__mediaTypes = sdp.getMediaTypes();
      }
    }
    // the candidate is valid if we know about the media type
    var validMediaType = pc.__mediaTypes && pc.__mediaTypes.indexOf(sdpMid) >= 0;
    // Otherwise we abort the task
    if (!validMediaType) {
      data.aborted = true;
    }
    return validMediaType;
  }

  function orderTasks(a, b) {
    // apply each of the checks for each task
    var tasks = [a,b];
    var readiness = tasks.map(testReady);
    var taskPriorities = tasks.map(function(task, idx) {
      var ready = readiness[idx];
      return ready ? task.priority : PRIORITY_WAIT;
    });

    return taskPriorities[1] - taskPriorities[0];
  }

  // check whether a task is ready (does it pass all the checks)
  function testReady(task) {
    return (task.checks || []).reduce(function(memo, check) {
      return memo && check(pc, task);
    }, true);
  }

  function triggerQueueCheck() {
    if (checkQueueTimer) return;
    checkQueueTimer = setTimeout(checkQueue, queueInterval);
  }

  // patch in the queue helper methods
  tq.addIceCandidate = enqueue('addIceCandidate', applyCandidate, {
    processArgs: extractCandidateEventData,
    checks: [hasLocalOrRemoteDesc, isValidCandidate, isConnReadyForCandidate ],

    // set ttl to 5s
    ttl: 5000,
    immediate: true
  });

  tq.setLocalDescription = enqueue('setLocalDescription', execMethod, {
    processArgs: cleansdp,
    pass: emitSdp
  });

  tq.setRemoteDescription = enqueue('setRemoteDescription', execMethod, {
    processArgs: createSessionDescription,
    pass: completeConnection
  });

  tq.createOffer = enqueue('createOffer', execMethod, {
    checks: [ isNotNegotiating ],
    pass: tq.setLocalDescription
  });

  tq.createAnswer = enqueue('createAnswer', execMethod, {
    pass: tq.setLocalDescription
  });

  return tq;
};
<commit_msg>Set __mediaTypes to undefined as opposed to delete<commit_after>var detect = require('rtc-core/detect');
var findPlugin = require('rtc-core/plugin');
var PriorityQueue = require('priorityqueuejs');
var pluck = require('whisk/pluck');
var pluckSessionDesc = pluck('sdp', 'type');

// some validation routines
var checkCandidate = require('rtc-validator/candidate');

// the sdp cleaner
var sdpclean = require('rtc-sdpclean');
var parseSdp = require('rtc-sdp');

var PRIORITY_LOW = 100;
var PRIORITY_WAIT = 1000;

// priority order (lower is better)
var DEFAULT_PRIORITIES = [
  'createOffer',
  'setLocalDescription',
  'createAnswer',
  'setRemoteDescription',
  'addIceCandidate'
];

// define event mappings
var METHOD_EVENTS = {
  setLocalDescription: 'setlocaldesc',
  setRemoteDescription: 'setremotedesc',
  createOffer: 'offer',
  createAnswer: 'answer'
};

var MEDIA_MAPPINGS = {
  data: 'application'
};

// define states in which we will attempt to finalize a connection on receiving a remote offer
var VALID_RESPONSE_STATES = ['have-remote-offer', 'have-local-pranswer'];

/**
  Allows overriding of a function
 **/
function pluggable(pluginFn, defaultFn) {
  return (pluginFn && typeof pluginFn == 'function' ? pluginFn : defaultFn);
}

/**
  # rtc-taskqueue

  This is a package that assists with applying actions to an `RTCPeerConnection`
  in as reliable order as possible. It is primarily used by the coupling logic
  of the [`rtc-tools`](https://github.com/rtc-io/rtc-tools).

  ## Example Usage

  For the moment, refer to the simple coupling test as an example of how to use
  this package (see below):

  <<< test/couple.js

**/
module.exports = function(pc, opts) {
  opts = opts || {};
  // create the task queue
  var queue = new PriorityQueue(orderTasks);
  var tq = require('mbus')('', (opts || {}).logger);

  // initialise task importance
  var priorities = (opts || {}).priorities || DEFAULT_PRIORITIES;
  var queueInterval = (opts || {}).interval || 10;

  // check for plugin usage
  var plugin = findPlugin((opts || {}).plugins);

  // initialise state tracking
  var checkQueueTimer = 0;
  var defaultFail = tq.bind(tq, 'fail');

  // look for an sdpfilter function (allow slight mis-spellings)
  var sdpFilter = (opts || {}).sdpfilter || (opts || {}).sdpFilter;
  var alwaysParse = (opts.sdpParseMode === 'always');

  // initialise session description and icecandidate objects
  var RTCSessionDescription = (opts || {}).RTCSessionDescription ||
    detect('RTCSessionDescription');

  var RTCIceCandidate = (opts || {}).RTCIceCandidate ||
    detect('RTCIceCandidate');

  // Determine plugin overridable methods
  var createIceCandidate = pluggable(plugin && plugin.createIceCandidate, function(data) {
    return new RTCIceCandidate(data);
  });

  var createSessionDescription = pluggable(plugin && plugin.createSessionDescription, function(data) {
    return new RTCSessionDescription(data);
  });

  function abortQueue(err) {
    console.error(err);
  }

  function applyCandidate(task, next) {
    var data = task.args[0];
    // Allow selective filtering of ICE candidates
    if (opts && opts.filterCandidate && !opts.filterCandidate(data)) {
      tq('ice.remote.filtered', candidate);
      return next();
    }
    var candidate = data && data.candidate && createIceCandidate(data);

    function handleOk() {
      tq('ice.remote.applied', candidate);
      next();
    }

    function handleFail(err) {
      tq('ice.remote.invalid', candidate);
      next(err);
    }

    // we have a null candidate, we have finished gathering candidates
    if (! candidate) {
      return next();
    }

    pc.addIceCandidate(candidate, handleOk, handleFail);
  }

  function checkQueue() {
    // peek at the next item on the queue
    var next = (! queue.isEmpty()) && queue.peek();
    var ready = next && testReady(next);

    // reset the queue timer
    checkQueueTimer = 0;

    // if we don't have a task ready, then abort
    if (! ready) {
      // if we have a task and it has expired then dequeue it
      if (next && (aborted(next) || expired(next))) {
        tq('task.expire', next);
        queue.deq();
      }

      return (! queue.isEmpty()) && isNotClosed(pc) && triggerQueueCheck();
    }

    // properly dequeue task
    next = queue.deq();

    // process the task
    next.fn(next, function(err) {
      var fail = next.fail || defaultFail;
      var pass = next.pass;
      var taskName = next.name;

      // if errored, fail
      if (err) {
        console.error(taskName + ' task failed: ', err);
        return fail(err);
      }

      if (typeof pass == 'function') {
        pass.apply(next, [].slice.call(arguments, 1));
      }

      // Allow tasks to indicate that processing should continue immediately to the
      // following task
      if (next.immediate) {
        if (checkQueueTimer) clearTimeout(checkQueueTimer);
        return checkQueue();
      } else {
        triggerQueueCheck();
      }
    });
  }

  function cleansdp(desc) {
    // ensure we have clean sdp
    var sdpErrors = [];
    var sdp = desc && sdpclean(desc.sdp, { collector: sdpErrors });

    // if we don't have a match, log some info
    if (desc && sdp !== desc.sdp) {
      console.info('invalid lines removed from sdp: ', sdpErrors);
      desc.sdp = sdp;
    }

    // if a filter has been specified, then apply the filter
    if (typeof sdpFilter == 'function') {
      desc.sdp = sdpFilter(desc.sdp, pc);
    }

    return desc;
  }

  function completeConnection() {
    // Clean any cached media types now that we have potentially new remote description
    if (pc.__mediaTypes) {
      // Set defined as opposed to delete, for compatibility purposes
      pc.__mediaTypes = undefined;
    }

    if (VALID_RESPONSE_STATES.indexOf(pc.signalingState) >= 0) {
      return tq.createAnswer();
    }
  }

  function emitSdp() {
    tq('sdp.local', pluckSessionDesc(this.args[0]));
  }

  function enqueue(name, handler, opts) {
    return function() {
      var args = [].slice.call(arguments);

      if (opts && typeof opts.processArgs == 'function') {
        args = args.map(opts.processArgs);
      }

      var priority = priorities.indexOf(name);

      queue.enq({
        args: args,
        name: name,
        fn: handler,
        priority: priority >= 0 ? priority : PRIORITY_LOW,
        immediate: opts.immediate,
        // If aborted, the task will be removed
        aborted: false,

        // record the time at which the task was queued
        start: Date.now(),

        // initilaise any checks that need to be done prior
        // to the task executing
        checks: [ isNotClosed ].concat((opts || {}).checks || []),

        // initialise the pass and fail handlers
        pass: (opts || {}).pass,
        fail: (opts || {}).fail
      });

      triggerQueueCheck();
    };
  }

  function execMethod(task, next) {
    var fn = pc[task.name];
    var eventName = METHOD_EVENTS[task.name] || (task.name || '').toLowerCase();
    var cbArgs = [ success, fail ];
    var isOffer = task.name === 'createOffer';

    function fail(err) {
      tq.apply(tq, [ 'negotiate.error', task.name, err ].concat(task.args));
      next(err);
    }

    function success() {
      tq.apply(tq, [ ['negotiate', eventName, 'ok'], task.name ].concat(task.args));
      next.apply(null, [null].concat([].slice.call(arguments)));
    }

    if (! fn) {
      return next(new Error('cannot call "' + task.name + '" on RTCPeerConnection'));
    }

    // invoke the function
    tq.apply(tq, ['negotiate.' + eventName].concat(task.args));
    fn.apply(
      pc,
      task.args.concat(cbArgs).concat(isOffer ? generateConstraints() : [])
    );
  }

  function expired(task) {
    return (typeof task.ttl == 'number') && (task.start + task.ttl < Date.now());
  }

  function aborted(task) {
    return task && task.aborted;
  }

  function extractCandidateEventData(data) {
    // extract nested candidate data (like we will see in an event being passed to this function)
    while (data && data.candidate && data.candidate.candidate) {
      data = data.candidate;
    }

    return data;
  }

  function generateConstraints() {
    var allowedKeys = {
      offertoreceivevideo: 'OfferToReceiveVideo',
      offertoreceiveaudio: 'OfferToReceiveAudio',
      icerestart: 'IceRestart',
      voiceactivitydetection: 'VoiceActivityDetection'
    };

    var constraints = {
      OfferToReceiveVideo: true,
      OfferToReceiveAudio: true
    };

    // update known keys to match
    Object.keys(opts || {}).forEach(function(key) {
      if (allowedKeys[key.toLowerCase()]) {
        constraints[allowedKeys[key.toLowerCase()]] = opts[key];
      }
    });

    return { mandatory: constraints };
  }

  function hasLocalOrRemoteDesc(pc, task) {
    return pc.__hasDesc || (pc.__hasDesc = !!pc.remoteDescription);
  }

  function isNotNegotiating(pc) {
    return pc.signalingState !== 'have-local-offer';
  }

  function isNotClosed(pc) {
    return pc.signalingState !== 'closed';
  }

  function isStable(pc) {
    return pc.signalingState === 'stable';
  }

  function isValidCandidate(pc, data) {
    return data.__valid ||
      (data.__valid = checkCandidate(data.args[0]).length === 0);
  }

  function isConnReadyForCandidate(pc, data) {
    var sdpMid = data.args[0] && data.args[0].sdpMid;

    // remap media types as appropriate
    sdpMid = MEDIA_MAPPINGS[sdpMid] || sdpMid;

    if (sdpMid === '')
      return true;

    // Allow parsing of SDP always if required
    if (alwaysParse || !pc.__mediaTypes) {
      var sdp = parseSdp(pc.remoteDescription && pc.remoteDescription.sdp);
      // We only want to cache the SDP media types if we've received them, otherwise
      // bad things can happen
      var mediaTypes = sdp.getMediaTypes();
      if (mediaTypes && mediaTypes.length > 0) {
        pc.__mediaTypes = sdp.getMediaTypes();
      }
    }
    // the candidate is valid if we know about the media type
    var validMediaType = pc.__mediaTypes && pc.__mediaTypes.indexOf(sdpMid) >= 0;
    // Otherwise we abort the task
    if (!validMediaType) {
      data.aborted = true;
    }
    return validMediaType;
  }

  function orderTasks(a, b) {
    // apply each of the checks for each task
    var tasks = [a,b];
    var readiness = tasks.map(testReady);
    var taskPriorities = tasks.map(function(task, idx) {
      var ready = readiness[idx];
      return ready ? task.priority : PRIORITY_WAIT;
    });

    return taskPriorities[1] - taskPriorities[0];
  }

  // check whether a task is ready (does it pass all the checks)
  function testReady(task) {
    return (task.checks || []).reduce(function(memo, check) {
      return memo && check(pc, task);
    }, true);
  }

  function triggerQueueCheck() {
    if (checkQueueTimer) return;
    checkQueueTimer = setTimeout(checkQueue, queueInterval);
  }

  // patch in the queue helper methods
  tq.addIceCandidate = enqueue('addIceCandidate', applyCandidate, {
    processArgs: extractCandidateEventData,
    checks: [hasLocalOrRemoteDesc, isValidCandidate, isConnReadyForCandidate ],

    // set ttl to 5s
    ttl: 5000,
    immediate: true
  });

  tq.setLocalDescription = enqueue('setLocalDescription', execMethod, {
    processArgs: cleansdp,
    pass: emitSdp
  });

  tq.setRemoteDescription = enqueue('setRemoteDescription', execMethod, {
    processArgs: createSessionDescription,
    pass: completeConnection
  });

  tq.createOffer = enqueue('createOffer', execMethod, {
    checks: [ isNotNegotiating ],
    pass: tq.setLocalDescription
  });

  tq.createAnswer = enqueue('createAnswer', execMethod, {
    pass: tq.setLocalDescription
  });

  return tq;
};
<|endoftext|>


<commit_before>/*
 @author Zakai Hamilton
 @component AppStorage
 */

package.app.storage = function AppStorage(me) {
    me.launch = function () {
        if (me.package.core.property.get(me.singleton, "ui.node.parent")) {
            me.package.core.property.set(me.singleton, "widget.window.show", true);
            return;
        }
        me.singleton = me.package.ui.element.create(__json__, "workspace", "self");
    };
    me.refresh = {
        set: function(object) {
            me.package.core.property.set(me.singleton.var.tree, "clear");
            me.package.storage.remote.getChildren(function(root) {
                me.package.core.property.set(me.singleton.var.tree, "ui.group.data", {
                    "ui.data.keyList": ["ui.basic.text", "ui.basic.metadata", "ui.data.items"],
                    "ui.data.default": {
                        "state": false
                    },
                    "ui.data.values" : me.collectItems(root)
                });
            }, "/Kab/concepts", true);
        }
    };
    me.collectItems = function(root) {
        var items = [];
        for(var entry of root) {
            var item = [];
            item.push(entry.name);
            item.push({
                path:entry.path_lower
            });
            if(entry.entries) {
                item.push(me.collectItems(entry.entries));
            }
            items.push(item);
        }
        return items;
    };
    me.info = {
        set: function(object) {
            var selection = me.package.core.property.get(me.singleton.var.tree, "selection");
            var metadata = me.package.core.property.get(selection, "ui.basic.metadata");
            me.package.storage.remote.metadata(function(data) {
                metadata.data = data;
                me.package.core.app.launch(null, "info", [me.package.core.property.get(selection, "ui.basic.text"), metadata]);
            }, metadata.path);
        }
    };
    me.download = {
        set: function(object) {
            var selection = me.package.core.property.get(me.singleton.var.tree, "selection");
            var metadata = me.package.core.property.get(selection, "ui.basic.metadata");
            me.package.storage.remote.downloadData(function(data) {
                metadata.data = data;
                alert(JSON.stringify(metadata));
            }, metadata.path);
        }
    };
};
<commit_msg>bug fix<commit_after>/*
 @author Zakai Hamilton
 @component AppStorage
 */

package.app.storage = function AppStorage(me) {
    me.launch = function () {
        if (me.package.core.property.get(me.singleton, "ui.node.parent")) {
            me.package.core.property.set(me.singleton, "widget.window.show", true);
            return;
        }
        me.singleton = me.package.ui.element.create(__json__, "workspace", "self");
    };
    me.refresh = {
        set: function(object) {
            me.package.core.property.set(me.singleton.var.tree, "clear");
            me.package.storage.remote.getChildren(function(root) {
                me.package.core.property.set(me.singleton.var.tree, "ui.group.data", {
                    "ui.data.keyList": ["ui.basic.text", "ui.basic.metadata", "ui.data.items"],
                    "ui.data.default": {
                        "state": false
                    },
                    "ui.data.values" : me.collectItems(root)
                });
            }, "/Kab/concepts", true);
        }
    };
    me.collectItems = function(root) {
        var items = [];
        for(var entry of root) {
            var item = [];
            item.push(entry.name);
            item.push({
                path:entry.path_lower
            });
            if(entry.entries) {
                item.push(me.collectItems(entry.entries));
            }
            items.push(item);
        }
        return items;
    };
    me.info = {
        set: function(object) {
            var selection = me.package.core.property.get(me.singleton.var.tree, "selection");
            var metadata = me.package.core.property.get(selection, "ui.basic.metadata");
            me.package.storage.remote.metadata(function(data) {
                metadata.data = data;
                me.package.core.app.launch(null, "info", [me.package.core.property.get(selection, "ui.basic.text"), metadata]);
            }, metadata.path);
        }
    };
    me.download = {
        set: function(object) {
            var selection = me.package.core.property.get(me.singleton.var.tree, "selection");
            var metadata = me.package.core.property.get(selection, "ui.basic.metadata");
            me.package.storage.remote.downloadData(function(data, error) {
                if(error) {
                    metadata.data = error;
                }
                else {
                    metadata.data = data;
                }
                alert(JSON.stringify(metadata));
            }, metadata.path);
        }
    };
};
<|endoftext|>


<commit_before>var Transform = require('stream').Transform,
    util = require('util');

var StreamConcat = function(streams, options) {
  if (!options)
    options = {};

  Transform.call(this, options);

  var self = this;

  this.streams = streams;
  this.canAddStream = true;
  this.currentStream = null;
  this.streamIndex = 0;
  var nextStream = function() {
    self.currentStream = null;
    if (self.streams.constructor === Array && self.streamIndex < self.streams.length) {
      self.currentStream = self.streams[self.streamIndex++];
    } else if (typeof self.streams === 'function') {
      this.canAddStream = false;
      self.currentStream = self.streams();
    }

    if (self.currentStream === null) {
      this.canAddStream = false;
      self.push(null);
    } else {
	  let next = false;
      function goNext() {
        if (next)
          return;

        next = true;
        nextStream();
      }

      self.currentStream.pipe(self, {end: false});
      self.currentStream.on('end', goNext);
	  if (options.close)
	    self.currentStream.on('close', goNext);
    }
  };

  nextStream();
};

util.inherits(StreamConcat, Transform);

StreamConcat.prototype._transform = function(chunk, encoding, callback) {
  callback(null, chunk);
};

StreamConcat.prototype.addStream = function(newStream) {
  if (this.canAddStream)
    this.streams.push(newStream);
  else
    this.emit('error', new Error('Can\'t add stream.'));
};

module.exports = StreamConcat;<commit_msg>Improved syntax.<commit_after>var Transform = require('stream').Transform,
    util = require('util');

var StreamConcat = function(streams, options) {
  if (!options)
    options = {};

  Transform.call(this, options);

  var self = this;

  this.streams = streams;
  this.canAddStream = true;
  this.currentStream = null;
  this.streamIndex = 0;
  var nextStream = function() {
    self.currentStream = null;
    if (self.streams.constructor === Array && self.streamIndex < self.streams.length) {
      self.currentStream = self.streams[self.streamIndex++];
    } else if (typeof self.streams === 'function') {
      this.canAddStream = false;
      self.currentStream = self.streams();
    }

    if (self.currentStream === null) {
      this.canAddStream = false;
      self.push(null);
    } else {
      self.currentStream.pipe(self, {end: false});
      var next = false;
      var goNext = function() {
        if (next)
          return;

        next = true;
        nextStream();
      };

      self.currentStream.on('end', goNext);
      if (options.close)
        self.currentStream.on('close', goNext);
    }
  };

  nextStream();
};

util.inherits(StreamConcat, Transform);

StreamConcat.prototype._transform = function(chunk, encoding, callback) {
  callback(null, chunk);
};

StreamConcat.prototype.addStream = function(newStream) {
  if (this.canAddStream)
    this.streams.push(newStream);
  else
    this.emit('error', new Error('Can\'t add stream.'));
};

module.exports = StreamConcat;<|endoftext|>


<commit_before><commit_msg>Fix a bug in auto_update.js<commit_after><|endoftext|>