# Source: streaming/controllers/TimeSyncController.js

    /**
     * The copyright in this software is being made available under the BSD License,
     * included below. This software may be subject to other third party and contributor
     * rights, including patent rights, and no such rights are granted under this license.
     *
     * Copyright (c) 2013, Dash Industry Forum.
     * All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without modification,
     * are permitted provided that the following conditions are met:
     *  * Redistributions of source code must retain the above copyright notice, this
     *  list of conditions and the following disclaimer.
     *  * Redistributions in binary form must reproduce the above copyright notice,
     *  this list of conditions and the following disclaimer in the documentation and/or
     *  other materials provided with the distribution.
     *  * Neither the name of Dash Industry Forum nor the names of its
     *  contributors may be used to endorse or promote products derived from this software
     *  without specific prior written permission.
     *
     *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
     *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
     *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
     *  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
     *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
     *  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     *  POSSIBILITY OF SUCH DAMAGE.
     */
    import DashJSError from './../vo/DashJSError';
    import {HTTPRequest} from '../vo/metrics/HTTPRequest';
    import EventBus from './../../core/EventBus';
    import Events from './../../core/events/Events';
    import Errors from './../../core/errors/Errors';
    import FactoryMaker from '../../core/FactoryMaker';
    import Debug from '../../core/Debug';
    import URLUtils from '../utils/URLUtils';

    const HTTP_TIMEOUT_MS = 5000;
    const DEFAULT_MAXIMUM_ALLOWED_DRIFT = 100;
    const DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS_ADJUSTMENT_FACTOR = 2;
    const DEFAULT_BACKGROUND_ATTEMPTS = 2;
    const DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS = 30;
    const DEFAULT_MINIMUM_TIME_BETWEEN_BACKGROUND_SYNC_ATTEMPTS = 30;
    const DEFAULT_MAXIMUM_TIME_BETWEEN_SYNC = 600;
    const DEFAULT_MINIMUM_TIME_BETWEEN_SYNC = 2;

    function TimeSyncController() {

        const context = this.context;
        const eventBus = EventBus(context).getInstance();
        const urlUtils = URLUtils(context).getInstance();

        let instance,
            logger,
            isSynchronizing,
            isBackgroundSynchronizing,
            settings,
            handlers,
            dashMetrics,
            backgroundSyncTimeOffsets,
            timingSources,
            timeOfLastSync,
            timeOfLastBackgroundSync,
            lastOffset,
            lastTimingSource,
            internalTimeBetweenSyncAttempts,
            errHandler,
            baseURLController;

        function setup() {
            logger = Debug(context).getInstance().getLogger(instance);

            eventBus.on(Events.ATTEMPT_BACKGROUND_SYNC, _onAttemptBackgroundSync, instance);
        }

        function setConfig(config) {
            if (!config) return;

            if (config.dashMetrics) {
                dashMetrics = config.dashMetrics;
            }

            if (config.baseURLController) {
                baseURLController = config.baseURLController;
            }

            if (config.errHandler) {
                errHandler = config.errHandler;
            }

            if (config.settings) {
                settings = config.settings;
            }
        }

        function _resetInitialSettings() {
            backgroundSyncTimeOffsets = [];
            timingSources = [];
            timeOfLastSync = null;
            timeOfLastBackgroundSync = null;
            lastTimingSource = null;
            lastOffset = NaN;
            isSynchronizing = false;
            isBackgroundSynchronizing = false;
            internalTimeBetweenSyncAttempts = settings.get().streaming.utcSynchronization.timeBetweenSyncAttempts;
        }

        /**
         * Register the timing handler depending on the schemeIdUris. This method is called once when the StreamController is initialized
         */
        function initialize() {
            _resetInitialSettings();

            // a list of known schemeIdUris and a method to call with @value
            handlers = {
                'urn:mpeg:dash:utc:http-head:2014': _httpHeadHandler,
                'urn:mpeg:dash:utc:http-xsdate:2014': _httpHandler.bind(null, _xsdatetimeDecoder),
                'urn:mpeg:dash:utc:http-iso:2014': _httpHandler.bind(null, _iso8601Decoder),
                'urn:mpeg:dash:utc:direct:2014': _directHandler,

                // some specs referencing early ISO23009-1 drafts incorrectly use
                // 2012 in the URI, rather than 2014. support these for now.
                'urn:mpeg:dash:utc:http-head:2012': _httpHeadHandler,
                'urn:mpeg:dash:utc:http-xsdate:2012': _httpHandler.bind(null, _xsdatetimeDecoder),
                'urn:mpeg:dash:utc:http-iso:2012': _httpHandler.bind(null, _iso8601Decoder),
                'urn:mpeg:dash:utc:direct:2012': _directHandler,

                // it isn't clear how the data returned would be formatted, and
                // no public examples available so http-ntp not supported for now.
                // presumably you would do an arraybuffer type xhr and decode the
                // binary data returned but I would want to see a sample first.
                'urn:mpeg:dash:utc:http-ntp:2014': _notSupportedHandler,

                // not clear how this would be supported in javascript (in browser)
                'urn:mpeg:dash:utc:ntp:2014': _notSupportedHandler,
                'urn:mpeg:dash:utc:sntp:2014': _notSupportedHandler
            };

        }

        /**
         * Sync against a timing source. T
         * @param {array} tSources
         * @param {boolean} isDynamic
         */
        function attemptSync(tSources, isDynamic) {

            timingSources = tSources;

            // Stop if we are already synchronizing
            if (isSynchronizing) {
                return;
            }

            // No synchronization required we can signal the completion immediately
            if (!_shouldPerformSynchronization(isDynamic)) {
                eventBus.trigger(Events.TIME_SYNCHRONIZATION_COMPLETED);
                return;
            }

            isSynchronizing = true;
            _attemptRecursiveSync();
        }

        /**
         * Does a synchronization in the background in case the last offset should be verified or a 404 occurs
         */
        function _onAttemptBackgroundSync() {
            if (!settings.get().streaming.utcSynchronization.enabled || isSynchronizing || isBackgroundSynchronizing || !lastTimingSource || !lastTimingSource.value || !lastTimingSource.schemeIdUri || isNaN(lastOffset) || isNaN(settings.get().streaming.utcSynchronization.backgroundAttempts)) {
                return;
            }

            if (timeOfLastBackgroundSync && ((Date.now() - timeOfLastBackgroundSync) / 1000) < DEFAULT_MINIMUM_TIME_BETWEEN_BACKGROUND_SYNC_ATTEMPTS) {
                return;
            }

            backgroundSyncTimeOffsets = [];
            isBackgroundSynchronizing = true;
            const backgroundAttempts = !isNaN(settings.get().streaming.utcSynchronization.backgroundAttempts) ? settings.get().streaming.utcSynchronization.backgroundAttempts : DEFAULT_BACKGROUND_ATTEMPTS;
            _attemptBackgroundSync(backgroundAttempts);
        }

        /**
         * Perform a defined number of background attempts
         * @param {number} attempts
         * @private
         */
        function _attemptBackgroundSync(attempts) {
            try {
                if (attempts <= 0) {
                    _completeBackgroundTimeSyncSequence();
                    return;
                }

                const deviceTimeBeforeSync = Date.now();
                handlers[lastTimingSource.schemeIdUri](
                    lastTimingSource.value,
                    function (serverTime) {
                        // the timing source returned something useful
                        const deviceTimeAfterSync = Date.now();
                        const offset = _calculateOffset(deviceTimeBeforeSync, deviceTimeAfterSync, serverTime);

                        backgroundSyncTimeOffsets.push(offset);
                        _attemptBackgroundSync(attempts - 1);
                    },
                    function () {
                        _completeBackgroundTimeSyncSequence();
                    }
                );
            } catch (e) {
                _completeBackgroundTimeSyncSequence();
            }
        }

        /**
         * Sync against a timing source. This method is called recursively if the time sync for the first entry in timingSources fails.
         * @param {number} sourceIndex
         */
        function _attemptRecursiveSync(sourceIndex = null) {
            // if called with no sourceIndex, use zero (highest priority)
            let index = sourceIndex || 0;

            // the sources should be ordered in priority from the manifest.
            // try each in turn, from the top, until either something
            // sensible happens, or we run out of sources to try.
            if (!timingSources || timingSources.length === 0 || index >= timingSources.length) {
                _onComplete();
                return;
            }
            let source = timingSources[index];

            if (source) {
                // check if there is a handler for this @schemeIdUri
                if (handlers.hasOwnProperty(source.schemeIdUri)) {
                    // if so, call it with its @value
                    const deviceTimeBeforeSync = new Date().getTime();
                    handlers[source.schemeIdUri](
                        source.value,
                        function (serverTime) {
                            // the timing source returned something useful
                            const deviceTimeAfterSync = new Date().getTime();
                            const offset = _calculateOffset(deviceTimeBeforeSync, deviceTimeAfterSync, serverTime);
                            lastTimingSource = source;

                            _onComplete(offset);
                        },
                        function () {
                            // the timing source was probably uncontactable
                            // or returned something we can't use - try again
                            // with the remaining sources
                            _attemptRecursiveSync(index + 1);
                        }
                    );
                } else {
                    // an unknown schemeIdUri must have been found
                    // try again with the remaining sources
                    _attemptRecursiveSync(index + 1);
                }
            } else {
                // no valid time source could be found, just use device time
                _onComplete();
            }

        }

        /**
         * Calculate the offset between client and server. Account for the roundtrip time
         * @param {number} deviceTimeBeforeSync
         * @param {number} deviceTimeAfterSync
         * @param {number} serverTime
         * @return {number}
         * @private
         */
        function _calculateOffset(deviceTimeBeforeSync, deviceTimeAfterSync, serverTime) {
            const deviceReferenceTime = deviceTimeAfterSync - ((deviceTimeAfterSync - deviceTimeBeforeSync) / 2);

            return serverTime - deviceReferenceTime;
        }

        /**
         * Checks if a synchronization is required
         * @param {boolean} isDynamic
         * @return {boolean}
         * @private
         */
        function _shouldPerformSynchronization(isDynamic) {
            try {
                if (!isDynamic || !settings.get().streaming.utcSynchronization.enabled) {
                    return false;
                }
                const timeBetweenSyncAttempts = !isNaN(internalTimeBetweenSyncAttempts) ? internalTimeBetweenSyncAttempts : DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS;

                if (!timeOfLastSync || !timeBetweenSyncAttempts || isNaN(timeBetweenSyncAttempts)) {
                    return true;
                }

                return ((Date.now() - timeOfLastSync) / 1000) >= timeBetweenSyncAttempts;
            } catch (e) {
                return true;
            }
        }

        /**
         * Callback after sync has been completed
         * @param {number} offset
         * @private
         */
        function _onComplete(offset = NaN) {
            let failed = isNaN(offset);
            if (failed && settings.get().streaming.utcSynchronization.useManifestDateHeaderTimeSource) {
                //Before falling back to binary search , check if date header exists on MPD. if so, use for a time source.
                _checkForDateHeader();
            } else {
                _completeTimeSyncSequence(failed, offset);
            }
        }

        /**
         * Takes xsdatetime and returns milliseconds since UNIX epoch. May not be necessary as xsdatetime is very similar to ISO 8601 which is natively understood by javascript Date parser
         * @param {string} xsdatetimeStr
         * @return {number}
         * @private
         */
        function _alternateXsdatetimeDecoder(xsdatetimeStr) {
            // taken from DashParser - should probably refactor both uses
            const SECONDS_IN_MIN = 60;
            const MINUTES_IN_HOUR = 60;
            const MILLISECONDS_IN_SECONDS = 1000;
            let datetimeRegex = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2})(?::([0-9]*)(\.[0-9]*)?)?(?:([+\-])([0-9]{2})([0-9]{2}))?/;

            let utcDate,
                timezoneOffset;

            let match = datetimeRegex.exec(xsdatetimeStr);

            // If the string does not contain a timezone offset different browsers can interpret it either
            // as UTC or as a local time so we have to parse the string manually to normalize the given date value for
            // all browsers
            utcDate = Date.UTC(
                parseInt(match[1], 10),
                parseInt(match[2], 10) - 1, // months start from zero
                parseInt(match[3], 10),
                parseInt(match[4], 10),
                parseInt(match[5], 10),
                (match[6] && (parseInt(match[6], 10) || 0)),
                (match[7] && parseFloat(match[7]) * MILLISECONDS_IN_SECONDS) || 0
            );
            // If the date has timezone offset take it into account as well
            if (match[9] && match[10]) {
                timezoneOffset = parseInt(match[9], 10) * MINUTES_IN_HOUR + parseInt(match[10], 10);
                utcDate += (match[8] === '+' ? -1 : +1) * timezoneOffset * SECONDS_IN_MIN * MILLISECONDS_IN_SECONDS;
            }

            return new Date(utcDate).getTime();
        }

        /**
         * Try to use the built in parser, since xsdate is a constrained ISO8601 which is supported natively by Date.parse. if that fails, try a regex-based version used elsewhere in this application.
         * @param {string} xsdatetimeStr
         * @return {number}
         */
        function _xsdatetimeDecoder(xsdatetimeStr) {
            let parsedDate = Date.parse(xsdatetimeStr);

            if (isNaN(parsedDate)) {
                parsedDate = _alternateXsdatetimeDecoder(xsdatetimeStr);
            }

            return parsedDate;
        }

        /**
         * Takes ISO 8601 timestamp and returns milliseconds since UNIX epoch
         * @param {string} isoStr
         * @return {number}
         */
        function _iso8601Decoder(isoStr) {
            return Date.parse(isoStr);
        }

        /**
         * Takes RFC 1123 timestamp (which is same as ISO8601) and returns milliseconds since UNIX epoch
         * @param {string} dateStr
         * @return {number}
         */
        function _rfc1123Decoder(dateStr) {
            return Date.parse(dateStr);
        }

        /**
         * Handler for unsupported scheme ids.
         * @param {string} url
         * @param {function} onSuccessCB
         * @param {function} onFailureCB
         * @private
         */
        function _notSupportedHandler(url, onSuccessCB, onFailureCB) {
            onFailureCB();
        }

        /**
         * Direct handler
         * @param {string} xsdatetimeStr
         * @param {function} onSuccessCB
         * @param {function} onFailureCB
         */
        function _directHandler(xsdatetimeStr, onSuccessCB, onFailureCB) {
            let time = _xsdatetimeDecoder(xsdatetimeStr);

            if (!isNaN(time)) {
                onSuccessCB(time);
                return;
            }

            onFailureCB();
        }

        /**
         * Generic http handler
         * @param {function} decoder
         * @param {string} url
         * @param {function} onSuccessCB
         * @param {function} onFailureCB
         * @param {boolean} isHeadRequest
         * @private
         */
        function _httpHandler(decoder, url, onSuccessCB, onFailureCB, isHeadRequest) {
            let oncomplete,
                onload;
            let complete = false;
            let req = new XMLHttpRequest();

            let verb = isHeadRequest ? HTTPRequest.HEAD : HTTPRequest.GET;
            let urls = url.match(/\S+/g);

            // according to ISO 23009-1, url could be a white-space
            // separated list of URLs. just handle one at a time.
            url = urls.shift();

            oncomplete = function () {
                if (complete) {
                    return;
                }

                // we only want to pass through here once per xhr,
                // regardless of whether the load was successful.
                complete = true;

                // if there are more urls to try, call self.
                if (urls.length) {
                    _httpHandler(decoder, urls.join(' '), onSuccessCB, onFailureCB, isHeadRequest);
                } else {
                    onFailureCB();
                }
            };

            onload = function () {
                let time,
                    result;

                if (req.status === 200) {
                    time = isHeadRequest ?
                        req.getResponseHeader('Date') :
                        req.response;

                    result = decoder(time);

                    // decoder returns NaN if non-standard input
                    if (!isNaN(result)) {
                        onSuccessCB(result);
                        complete = true;
                    }
                }
            };

            if (urlUtils.isRelative(url)) {
                // passing no path to resolve will return just MPD BaseURL/baseUri
                const baseUrl = baseURLController.resolve();
                if (baseUrl) {
                    url = urlUtils.resolve(url, baseUrl.url);
                }
            }

            req.open(verb, url);
            req.timeout = HTTP_TIMEOUT_MS || 0;
            req.onload = onload;
            req.onloadend = oncomplete;
            req.send();
        }

        /**
         * Handler for http-head schemeIdUri
         * @param {string} url
         * @param {function} onSuccessCB
         * @param {function} onFailureCB
         * @private
         */
        function _httpHeadHandler(url, onSuccessCB, onFailureCB) {
            _httpHandler(_rfc1123Decoder, url, onSuccessCB, onFailureCB, true);
        }

        /**
         * Checks if a date header is present in the MPD response and calculates the offset based on the header
         * @private
         */
        function _checkForDateHeader() {
            let dateHeaderValue = dashMetrics.getLatestMPDRequestHeaderValueByID('Date');
            let dateHeaderTime = dateHeaderValue !== null ? new Date(dateHeaderValue).getTime() : Number.NaN;

            if (!isNaN(dateHeaderTime)) {
                const offsetToDeviceTimeMs = dateHeaderTime - Date.now();
                _completeTimeSyncSequence(false, offsetToDeviceTimeMs);
            } else {
                _completeTimeSyncSequence(true);
            }
        }

        /**
         * Triggers the event to signal that the time synchronization was completed
         * @param {boolean} failed
         * @param {number} offset
         * @private
         */
        function _completeTimeSyncSequence(failed, offset) {

            // Adjust the time of the next sync based on the drift between current offset and last offset
            if (!isNaN(lastOffset) && !isNaN(offset) && !failed) {
                _adjustTimeBetweenSyncAttempts(offset);
            }

            // Update the internal data
            if (!failed && !isNaN(offset)) {
                timeOfLastSync = Date.now();
                isSynchronizing = false;

                // if this is the first sync we are doing perform background syncs as well to confirm current offset
                const shouldAttemptBackgroundSync = isNaN(lastOffset);
                lastOffset = offset;
                if (shouldAttemptBackgroundSync) {
                    _onAttemptBackgroundSync();
                }
                logger.debug(`Completed UTC sync. Setting client - server offset to ${offset}`);
            }

            if (failed) {
                lastTimingSource = null;
                isSynchronizing = false;
                errHandler.error(new DashJSError(Errors.TIME_SYNC_FAILED_ERROR_CODE, Errors.TIME_SYNC_FAILED_ERROR_MESSAGE));
            }

            // Notify other classes
            eventBus.trigger(Events.UPDATE_TIME_SYNC_OFFSET, {
                offset: offset,
            });
            eventBus.trigger(Events.TIME_SYNCHRONIZATION_COMPLETED);
        }

        function _adjustTimeBetweenSyncAttempts(offset) {
            try {
                const isOffsetDriftWithinThreshold = _isOffsetDriftWithinThreshold(offset);
                const timeBetweenSyncAttempts = !isNaN(internalTimeBetweenSyncAttempts) ? internalTimeBetweenSyncAttempts : DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS;
                const timeBetweenSyncAttemptsAdjustmentFactor = !isNaN(settings.get().streaming.utcSynchronization.timeBetweenSyncAttemptsAdjustmentFactor) ? settings.get().streaming.utcSynchronization.timeBetweenSyncAttemptsAdjustmentFactor : DEFAULT_TIME_BETWEEN_SYNC_ATTEMPTS_ADJUSTMENT_FACTOR;
                const maximumTimeBetweenSyncAttempts = !isNaN(settings.get().streaming.utcSynchronization.maximumTimeBetweenSyncAttempts) ? settings.get().streaming.utcSynchronization.maximumTimeBetweenSyncAttempts : DEFAULT_MAXIMUM_TIME_BETWEEN_SYNC;
                const minimumTimeBetweenSyncAttempts = !isNaN(settings.get().streaming.utcSynchronization.minimumTimeBetweenSyncAttempts) ? settings.get().streaming.utcSynchronization.minimumTimeBetweenSyncAttempts : DEFAULT_MINIMUM_TIME_BETWEEN_SYNC;
                let adjustedTimeBetweenSyncAttempts;

                if (isOffsetDriftWithinThreshold) {
                    // The drift between the current offset and the last offset is within the allowed threshold. Increase sync time
                    adjustedTimeBetweenSyncAttempts = Math.min(timeBetweenSyncAttempts * timeBetweenSyncAttemptsAdjustmentFactor, maximumTimeBetweenSyncAttempts);
                    logger.debug(`Increasing timeBetweenSyncAttempts to ${adjustedTimeBetweenSyncAttempts}`);
                } else {
                    // Drift between the current offset and the last offset is not within the allowed threshold. Decrease sync time
                    adjustedTimeBetweenSyncAttempts = Math.max(timeBetweenSyncAttempts / timeBetweenSyncAttemptsAdjustmentFactor, minimumTimeBetweenSyncAttempts);
                    logger.debug(`Decreasing timeBetweenSyncAttempts to ${adjustedTimeBetweenSyncAttempts}`);
                }

                internalTimeBetweenSyncAttempts = adjustedTimeBetweenSyncAttempts;
            } catch (e) {

            }
        }

        /**
         * Callback after all background syncs have been completed.
         * @private
         */
        function _completeBackgroundTimeSyncSequence() {
            if (!backgroundSyncTimeOffsets || backgroundSyncTimeOffsets.length === 0) {
                return;
            }

            const averageOffset = backgroundSyncTimeOffsets.reduce((acc, curr) => {
                return acc + curr;
            }, 0) / backgroundSyncTimeOffsets.length;

            if (!_isOffsetDriftWithinThreshold(averageOffset)) {
                logger.debug(`Completed background UTC sync. Setting client - server offset to ${averageOffset}`);
                lastOffset = averageOffset;
                eventBus.trigger(Events.UPDATE_TIME_SYNC_OFFSET, {
                    offset: lastOffset
                });
            } else {
                logger.debug(`Completed background UTC sync. Offset is within allowed threshold and is not adjusted.`);
            }

            isBackgroundSynchronizing = false;
            timeOfLastBackgroundSync = Date.now();
        }

        function _isOffsetDriftWithinThreshold(offset) {
            try {
                if (isNaN(lastOffset)) {
                    return true;
                }

                const maxAllowedDrift = settings.get().streaming.utcSynchronization.maximumAllowedDrift && !isNaN(settings.get().streaming.utcSynchronization.maximumAllowedDrift) ? settings.get().streaming.utcSynchronization.maximumAllowedDrift : DEFAULT_MAXIMUM_ALLOWED_DRIFT;
                const lowerBound = lastOffset - maxAllowedDrift;
                const upperBound = lastOffset + maxAllowedDrift;

                return offset >= lowerBound && offset <= upperBound;
            } catch (e) {
                return true;
            }
        }

        function reset() {
            _resetInitialSettings();

            eventBus.off(Events.ATTEMPT_BACKGROUND_SYNC, _onAttemptBackgroundSync, instance);
        }

        instance = {
            initialize,
            attemptSync,
            setConfig,
            reset
        };

        setup();

        return instance;
    }

    TimeSyncController.__dashjs_factory_name = 'TimeSyncController';
    const factory = FactoryMaker.getSingletonFactory(TimeSyncController);
    factory.HTTP_TIMEOUT_MS = HTTP_TIMEOUT_MS;
    FactoryMaker.updateSingletonFactory(TimeSyncController.__dashjs_factory_name, factory);
    export default factory;

#### Search results