UNPKG

@speechly/browser-client

Version:

JavaScript client for Speechly Streaming API

2,235 lines 134 kB
/**
 * Error to be thrown when the device does not support audioContext.
 * @public
 */
const ErrDeviceNotSupported = new Error('Current device does not support microphone API');
/**
 * Error to be thrown when user tries to change appId without project login.
 * @public
 */
const ErrAppIdChangeWithoutProjectLogin = new Error('AppId changed without project login');
class WebsocketError extends Error {
    constructor(reason, code, wasClean, ...params) {
        // Pass remaining arguments (including vendor specific ones) to parent constructor
        super(...params);
        this.name = `WebsocketError code ${code}`;
        this.message = reason;
        this.code = code;
        this.wasClean = wasClean;
    }
}
/**
 * Default sample rate for microphone streams.
 * @public
 */
const DefaultSampleRate = 16000;

/**
 * A high level API for automatic speech recognition (ASR) and natural language understanding (NLU) results. Results will accumulate in Segment for the duration of the an utterance.
 * @internal
 */
class SegmentState {
    /**
     * @param contextId - Audio context id
     * @param segmentIndex - 0-based segment index within the audio context
     * @internal
     */
    constructor(contextId, segmentIndex) {
        /**
         * True when the segment will not be changed any more
         */
        this.isFinalized = false;
        /**
         * Detected words in the segment
         */
        this.words = [];
        /**
         * Detected entities in the segment
         */
        this.entities = new Map();
        /**
         * Detected intent for the segment
         */
        this.intent = { intent: '', isFinal: false };
        this.contextId = contextId;
        this.id = segmentIndex;
    }
    toSegment() {
        let i = 0;
        const entities = new Array(this.entities.size);
        this.entities.forEach(v => {
            entities[i] = v;
            i++;
        });
        return {
            id: this.id,
            contextId: this.contextId,
            isFinal: this.isFinalized,
            words: this.words,
            entities: entities,
            intent: this.intent,
        };
    }
    toString() {
        const segment = this.toSegment();
        const words = segment.words.filter((w) => w.value);
        const cleanSegment = Object.assign(Object.assign({}, segment), { words });
        return JSON.stringify(cleanSegment, null, 2);
    }
    /**
     * @param words - changed words
     * @returns updated SegmentState
     * @internal
     */
    updateTranscript(words) {
        words.forEach(w => {
            // Only accept tentative words if the segment is tentative.
            if (!this.isFinalized || w.isFinal) {
                this.words[w.index] = w;
            }
        });
        return this;
    }
    /**
     * @param entities - changed entities
     * @returns updated SegmentState
     * @internal
     */
    updateEntities(entities) {
        entities.forEach(e => {
            // Only accept tentative entities if the segment is tentative.
            if (!this.isFinalized || e.isFinal) {
                this.entities.set(this.entityMapKey(e), e);
            }
        });
        return this;
    }
    /**
     * @param intent - changed intent
     * @returns updated SegmentState
     * @internal
     */
    updateIntent(intent) {
        // Only accept tentative intent if the segment is tentative.
        if (!this.isFinalized || intent.isFinal) {
            this.intent = intent;
        }
        return this;
    }
    /**
     * @returns SegmentState with final flags set
     * @internal
     */
    finalize() {
        // Filter away any entities which were not finalized.
        this.entities.forEach((val, key) => {
            if (!val.isFinal) {
                this.entities.delete(key);
            }
        });
        // Filter away any transcripts which were not finalized.
        this.words = this.words.filter(w => w.isFinal);
        if (!this.intent.isFinal) {
            this.intent.intent = '';
            this.intent.isFinal = true;
        }
        // Mark as final.
        this.isFinalized = true;
        return this;
    }
    entityMapKey(e) {
        return `${e.startPosition.toString()}:${e.endPosition.toString()}`;
    }
}

/******************************************************************************
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */

function __awaiter(thisArg, _arguments, P, generator) {
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  return new (P || (P = Promise))(function (resolve, reject) {
      function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
      function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
      function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
      step((generator = generator.apply(thisArg, _arguments || [])).next());
  });
}

typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
  var e = new Error(message);
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};

/**
 * Error to be thrown when the microphone was accessed before it was initialized.
 * @public
 */
const ErrNotInitialized = new Error('Microphone is not initialized');
/**
 * Error to be thrown when the initialize method of a Microphone instance is called more than once.
 * @public
 */
const ErrAlreadyInitialized = new Error('Microphone is already initialized');
/**
 * Error to be thrown when user did not give consent to the application to record audio.
 * @public
 */
const ErrNoAudioConsent = new Error('Microphone consent is not given');
var AudioSourceState;
(function (AudioSourceState) {
    AudioSourceState["NoAudioConsent"] = "NoAudioConsent";
    AudioSourceState["NoBrowserSupport"] = "NoBrowserSupport";
    AudioSourceState["Stopped"] = "Stopped";
    AudioSourceState["Starting"] = "Starting";
    AudioSourceState["Started"] = "Started";
})(AudioSourceState || (AudioSourceState = {}));

/**
 * Gets browser based microphone using the window.navigator.mediaDevices interface.
 * The exposed `mediaStream` can be attached to a `BrowserClient` instance.
 * @public
 */
class BrowserMicrophone {
    constructor() {
        this.muted = false;
        this.initialized = false;
        this.state = AudioSourceState.Stopped;
        this.debug = false;
        this.stateChangeCbs = [];
        try {
            const constraints = window.navigator.mediaDevices.getSupportedConstraints();
            this.nativeResamplingSupported = constraints.sampleRate === true;
            this.autoGainControlSupported = constraints.autoGainControl === true;
        }
        catch (_a) {
            this.nativeResamplingSupported = false;
            this.autoGainControlSupported = false;
        }
    }
    /**
     * Adds a listener for the state changes of the client.
     * @param cb - the callback to invoke on a client state change.
     */
    onStateChange(cb) {
        this.stateChangeCbs.push(cb);
    }
    /**
     * Initializes the microphone. Needs to happen after a user interaction in the view.
     * The reason for that is that it's required for user to first interact with the page,
     * before it can capture or play audio and video, for privacy and user experience reasons.
     */
    initialize() {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            if (this.initialized) {
                return;
            }
            // ensure mediaDevices are available
            if (((_a = window.navigator) === null || _a === void 0 ? void 0 : _a.mediaDevices) === undefined) {
                this.setState(AudioSourceState.NoBrowserSupport);
                throw ErrDeviceNotSupported;
            }
            const mediaStreamConstraints = {
                video: false,
            };
            if (this.nativeResamplingSupported || this.autoGainControlSupported) {
                mediaStreamConstraints.audio = {
                    sampleRate: DefaultSampleRate,
                    // @ts-ignore
                    autoGainControl: this.autoGainControlSupported,
                };
            }
            else {
                mediaStreamConstraints.audio = true;
            }
            try {
                this.setState(AudioSourceState.Starting);
                this.mediaStream = yield window.navigator.mediaDevices.getUserMedia(mediaStreamConstraints);
            }
            catch (err) {
                this.setState(AudioSourceState.NoAudioConsent);
                console.error(err);
                throw ErrNoAudioConsent;
            }
            this.initialized = true;
            this.muted = true;
            this.setState(AudioSourceState.Started);
        });
    }
    setState(newState) {
        if (this.state === newState) {
            return;
        }
        if (this.debug) {
            console.log('[BrowserMicrophone]', this.state, '->', newState);
        }
        this.state = newState;
        this.stateChangeCbs.forEach(cb => cb(newState));
    }
    /**
     * Closes the microphone, releases all resources and stops the Speechly client.
     */
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.initialized)
                return;
            this.muted = true;
            // Stop all media tracks
            const stream = this.mediaStream;
            stream.getTracks().forEach(t => t.stop());
            // Unset all audio infrastructure
            this.mediaStream = undefined;
            this.initialized = false;
            this.setState(AudioSourceState.Stopped);
        });
    }
    /**
     * @returns true if microphone is open
     */
    isRecording() {
        return !this.muted;
    }
}

/**
 * Known WebSocket response types.
 * @internal
 */
var WebsocketResponseType;
(function (WebsocketResponseType) {
    WebsocketResponseType["Started"] = "started";
    WebsocketResponseType["Stopped"] = "stopped";
    WebsocketResponseType["SegmentEnd"] = "segment_end";
    WebsocketResponseType["Transcript"] = "transcript";
    WebsocketResponseType["Entity"] = "entity";
    WebsocketResponseType["Intent"] = "intent";
    WebsocketResponseType["TentativeTranscript"] = "tentative_transcript";
    WebsocketResponseType["TentativeEntities"] = "tentative_entities";
    WebsocketResponseType["TentativeIntent"] = "tentative_intent";
})(WebsocketResponseType || (WebsocketResponseType = {}));
/**
 * Messages from worker to controller
 * @internal
 */
var WorkerSignal;
(function (WorkerSignal) {
    WorkerSignal["Opened"] = "WEBSOCKET_OPEN";
    WorkerSignal["Closed"] = "WEBSOCKET_CLOSED";
    WorkerSignal["AudioProcessorReady"] = "SOURCE_SAMPLE_RATE_SET_SUCCESS";
    WorkerSignal["VadSignalHigh"] = "VadSignalHigh";
    WorkerSignal["VadSignalLow"] = "VadSignalLow";
    WorkerSignal["RequestContextStart"] = "RequestContextStart";
})(WorkerSignal || (WorkerSignal = {}));
/**
 * Messages from controller to worker
 * @internal
 */
var ControllerSignal;
(function (ControllerSignal) {
    ControllerSignal["connect"] = "connect";
    ControllerSignal["initAudioProcessor"] = "initAudioProcessor";
    ControllerSignal["adjustAudioProcessor"] = "adjustAudioProcessor";
    ControllerSignal["SET_SHARED_ARRAY_BUFFERS"] = "SET_SHARED_ARRAY_BUFFERS";
    ControllerSignal["CLOSE"] = "CLOSE";
    ControllerSignal["START_CONTEXT"] = "START_CONTEXT";
    ControllerSignal["SWITCH_CONTEXT"] = "SWITCH_CONTEXT";
    ControllerSignal["STOP_CONTEXT"] = "STOP_CONTEXT";
    ControllerSignal["AUDIO"] = "AUDIO";
    ControllerSignal["startStream"] = "startStream";
    ControllerSignal["stopStream"] = "stopStream";
    ControllerSignal["setContextOptions"] = "setContextOptions";
})(ControllerSignal || (ControllerSignal = {}));

/**
 * Error to be thrown if storage API is not supported by the device.
 * @public
 */
const ErrNoStorageSupport = new Error('Current device does not support storage API');
/**
 * Error to be thrown if requested key was not found in the storage.
 * @public
 */
const ErrKeyNotFound = new Error('Requested key was not present in storage');

const DecoderDefaultOptions = {
    connect: true,
    apiUrl: 'https://api.speechly.com',
    sampleRate: DefaultSampleRate,
    debug: false,
    logSegments: false,
    frameMillis: 30,
    historyFrames: 5,
};
/**
 * Default options for voice activity detection (VAD)
 * @public
 */
const VadDefaultOptions = {
    enabled: false,
    controlListening: true,
    signalToNoiseDb: 3.0,
    noiseGateDb: -24.0,
    noiseLearnHalftimeMillis: 400,
    signalSearchFrames: 5,
    signalActivation: 0.7,
    signalRelease: 0.2,
    signalSustainMillis: 3000,
};
const StreamDefaultOptions = {
    preserveSegments: false,
    sampleRate: DefaultSampleRate,
    immediate: false,
    autoStarted: false,
};
/**
 * All possible states of a Speechly API client. Failed state is non-recoverable.
 * It is also possible to use arithmetics for state comparison, e.g. `if (state < speechly.ClientState.Disconnected)`,
 * to react to non-recoverable states.
 * @public
 */
var DecoderState;
(function (DecoderState) {
    DecoderState[DecoderState["Failed"] = 0] = "Failed";
    DecoderState[DecoderState["Disconnected"] = 1] = "Disconnected";
    DecoderState[DecoderState["Connected"] = 2] = "Connected";
    DecoderState[DecoderState["Active"] = 3] = "Active";
})(DecoderState || (DecoderState = {}));
/**
 * Array with methods for adding and removing event listener functions
 * @internal
 */
class ListenerArray extends Array {
    addEventListener(e) {
        this.push(e);
    }
    removeEventListener(e) {
        const index = this.findIndex(cb => cb === e);
        if (index >= 0) {
            this.splice(index, 1);
        }
    }
}
/**
 * All possible callbacks for the decoder.
 * @internal
 */
class EventCallbacks {
    constructor() {
        this.stateChangeCbs = new ListenerArray();
        this.transcriptCbs = new ListenerArray();
        this.entityCbs = new ListenerArray();
        this.intentCbs = new ListenerArray();
        this.segmentChangeCbs = new ListenerArray();
        this.tentativeTranscriptCbs = new ListenerArray();
        this.tentativeEntityCbs = new ListenerArray();
        this.tentativeIntentCbs = new ListenerArray();
        this.contextStartedCbs = new ListenerArray();
        this.contextStoppedCbs = new ListenerArray();
        this.onVadStateChange = new ListenerArray();
    }
}
/**
 * Error to be thrown when BrowserClient is already started
 * @public
 */
const ErrAlreadyStarted = new Error('BrowserClient already started');
/**
 * Error to be thrown when BrowserClient is already stopped
 * @public
 */
const ErrAlreadyStopped = new Error('BrowserClient already stopped');

/**
 * Converts client state value to a string, which could be useful for debugging or metrics.
 * @param state - the state of the client
 * @public
 */
function stateToString(state) {
    var _a;
    return (_a = states.get(state)) !== null && _a !== void 0 ? _a : 'unknown';
}
// TODO: generate this from the enum.
const states = new Map([
    [DecoderState.Failed, 'Failed'],
    [DecoderState.Disconnected, 'Disconnected'],
    [DecoderState.Connected, 'Connected'],
    [DecoderState.Active, 'Active'],
]);

// Unique ID creation requires a high quality random # generator. In the browser we therefore
// require the crypto API and do not support built-in fallback to lower quality random number
// generators (like Math.random()).
var getRandomValues;
var rnds8 = new Uint8Array(16);
function rng() {
  // lazy load so that environments that need to polyfill have a chance to do so
  if (!getRandomValues) {
    // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. Also,
    // find the complete implementation of crypto (msCrypto) on IE11.
    getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto);

    if (!getRandomValues) {
      throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
    }
  }

  return getRandomValues(rnds8);
}

var REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;

function validate(uuid) {
  return typeof uuid === 'string' && REGEX.test(uuid);
}

/**
 * Convert array of 16 byte values to UUID string format of the form:
 * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
 */

var byteToHex = [];

for (var i = 0; i < 256; ++i) {
  byteToHex.push((i + 0x100).toString(16).substr(1));
}

function stringify(arr) {
  var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
  // Note: Be careful editing this code!  It's been tuned for performance
  // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
  var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID.  If this throws, it's likely due to one
  // of the following:
  // - One or more input array values don't map to a hex octet (leading to
  // "undefined" in the uuid)
  // - Invalid input values for the RFC `version` or `variant` fields

  if (!validate(uuid)) {
    throw TypeError('Stringified UUID is invalid');
  }

  return uuid;
}

function v4(options, buf, offset) {
  options = options || {};
  var rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`

  rnds[6] = rnds[6] & 0x0f | 0x40;
  rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided

  if (buf) {
    offset = offset || 0;

    for (var i = 0; i < 16; ++i) {
      buf[offset + i] = rnds[i];
    }

    return buf;
  }

  return stringify(rnds);
}

var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

var base64 = {exports: {}};

/*! http://mths.be/base64 v0.1.0 by @mathias | MIT license */

(function (module, exports) {
(function(root) {

	// Detect free variables `exports`.
	var freeExports = exports;

	// Detect free variable `module`.
	var freeModule = module &&
		module.exports == freeExports && module;

	// Detect free variable `global`, from Node.js or Browserified code, and use
	// it as `root`.
	var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal;
	if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
		root = freeGlobal;
	}

	/*--------------------------------------------------------------------------*/

	var InvalidCharacterError = function(message) {
		this.message = message;
	};
	InvalidCharacterError.prototype = new Error;
	InvalidCharacterError.prototype.name = 'InvalidCharacterError';

	var error = function(message) {
		// Note: the error messages used throughout this file match those used by
		// the native `atob`/`btoa` implementation in Chromium.
		throw new InvalidCharacterError(message);
	};

	var TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
	// http://whatwg.org/html/common-microsyntaxes.html#space-character
	var REGEX_SPACE_CHARACTERS = /[\t\n\f\r ]/g;

	// `decode` is designed to be fully compatible with `atob` as described in the
	// HTML Standard. http://whatwg.org/html/webappapis.html#dom-windowbase64-atob
	// The optimized base64-decoding algorithm used is based on @atk’s excellent
	// implementation. https://gist.github.com/atk/1020396
	var decode = function(input) {
		input = String(input)
			.replace(REGEX_SPACE_CHARACTERS, '');
		var length = input.length;
		if (length % 4 == 0) {
			input = input.replace(/==?$/, '');
			length = input.length;
		}
		if (
			length % 4 == 1 ||
			// http://whatwg.org/C#alphanumeric-ascii-characters
			/[^+a-zA-Z0-9/]/.test(input)
		) {
			error(
				'Invalid character: the string to be decoded is not correctly encoded.'
			);
		}
		var bitCounter = 0;
		var bitStorage;
		var buffer;
		var output = '';
		var position = -1;
		while (++position < length) {
			buffer = TABLE.indexOf(input.charAt(position));
			bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
			// Unless this is the first of a group of 4 characters…
			if (bitCounter++ % 4) {
				// …convert the first 8 bits to a single ASCII character.
				output += String.fromCharCode(
					0xFF & bitStorage >> (-2 * bitCounter & 6)
				);
			}
		}
		return output;
	};

	// `encode` is designed to be fully compatible with `btoa` as described in the
	// HTML Standard: http://whatwg.org/html/webappapis.html#dom-windowbase64-btoa
	var encode = function(input) {
		input = String(input);
		if (/[^\0-\xFF]/.test(input)) {
			// Note: no need to special-case astral symbols here, as surrogates are
			// matched, and the input is supposed to only contain ASCII anyway.
			error(
				'The string to be encoded contains characters outside of the ' +
				'Latin1 range.'
			);
		}
		var padding = input.length % 3;
		var output = '';
		var position = -1;
		var a;
		var b;
		var c;
		var buffer;
		// Make sure any padding is handled outside of the loop.
		var length = input.length - padding;

		while (++position < length) {
			// Read three bytes, i.e. 24 bits.
			a = input.charCodeAt(position) << 16;
			b = input.charCodeAt(++position) << 8;
			c = input.charCodeAt(++position);
			buffer = a + b + c;
			// Turn the 24 bits into four chunks of 6 bits each, and append the
			// matching character for each of them to the output.
			output += (
				TABLE.charAt(buffer >> 18 & 0x3F) +
				TABLE.charAt(buffer >> 12 & 0x3F) +
				TABLE.charAt(buffer >> 6 & 0x3F) +
				TABLE.charAt(buffer & 0x3F)
			);
		}

		if (padding == 2) {
			a = input.charCodeAt(position) << 8;
			b = input.charCodeAt(++position);
			buffer = a + b;
			output += (
				TABLE.charAt(buffer >> 10) +
				TABLE.charAt((buffer >> 4) & 0x3F) +
				TABLE.charAt((buffer << 2) & 0x3F) +
				'='
			);
		} else if (padding == 1) {
			buffer = input.charCodeAt(position);
			output += (
				TABLE.charAt(buffer >> 2) +
				TABLE.charAt((buffer << 4) & 0x3F) +
				'=='
			);
		}

		return output;
	};

	var base64 = {
		'encode': encode,
		'decode': decode,
		'version': '0.1.0'
	};

	// Some AMD build optimizers, like r.js, check for specific condition patterns
	// like the following:
	if (freeExports && !freeExports.nodeType) {
		if (freeModule) { // in Node.js or RingoJS v0.8.0+
			freeModule.exports = base64;
		} else { // in Narwhal or RingoJS v0.7.0-
			for (var key in base64) {
				base64.hasOwnProperty(key) && (freeExports[key] = base64[key]);
			}
		}
	} else { // in Rhino or a web browser
		root.base64 = base64;
	}

}(commonjsGlobal));
}(base64, base64.exports));

const minTokenValidTime = 60 * 60 * 1000; // 1 hour
function fetchToken(baseUrl, projectId, appId, deviceId, fetcher = fetch, nowFn = Date.now) {
    var _a;
    return __awaiter(this, void 0, void 0, function* () {
        let body;
        if (projectId !== undefined) {
            body = { projectId, deviceId };
        }
        else {
            body = { appId, deviceId };
        }
        const response = yield fetcher(baseUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(body),
        });
        const json = yield response.json();
        if (response.status !== 200) {
            throw Error((_a = json.error) !== null && _a !== void 0 ? _a : `Speechly API login request failed with ${response.status}`);
        }
        if (json.access_token === undefined) {
            throw Error('Invalid login response from Speechly API');
        }
        if (!validateToken(json.access_token, projectId, appId, deviceId, nowFn)) {
            throw Error('Invalid token received from Speechly API');
        }
        return json.access_token;
    });
}
function validateToken(token, projectId, appId, deviceId, now = Date.now) {
    const decoded = decodeToken(token);
    if (decoded.expiresAtMs - now() < minTokenValidTime) {
        return false;
    }
    if (decoded.appId !== appId || decoded.projectId !== projectId) {
        return false;
    }
    if (decoded.deviceId !== deviceId) {
        return false;
    }
    return true;
}
function decodeToken(token) {
    const b = token.split('.')[1];
    let body;
    try {
        body = JSON.parse(base64.exports.decode(b));
    }
    catch (e) {
        throw new Error('Error decoding Speechly token!');
    }
    return {
        appId: body.appId,
        projectId: body.projectId,
        deviceId: body.deviceId,
        configId: body.configId,
        scopes: body.scope.split(' '),
        issuer: body.iss,
        audience: body.aud,
        expiresAtMs: body.exp * 1000, // JWT exp is in seconds, convert to ms, since that's what JS works with.
    };
}

function decodeBase64(base64, enableUnicode) {
    var binaryString = atob(base64);
    if (enableUnicode) {
        var binaryView = new Uint8Array(binaryString.length);
        for (var i = 0, n = binaryString.length; i < n; ++i) {
            binaryView[i] = binaryString.charCodeAt(i);
        }
        return String.fromCharCode.apply(null, new Uint16Array(binaryView.buffer));
    }
    return binaryString;
}

function createURL(base64, sourcemapArg, enableUnicodeArg) {
    var sourcemap = sourcemapArg === undefined ? null : sourcemapArg;
    var enableUnicode = enableUnicodeArg === undefined ? false : enableUnicodeArg;
    var source = decodeBase64(base64, enableUnicode);
    var start = source.indexOf('\n', 10) + 1;
    var body = source.substring(start) + (sourcemap ? '\/\/# sourceMappingURL=' + sourcemap : '');
    var blob = new Blob([body], { type: 'application/javascript' });
    return URL.createObjectURL(blob);
}

function createBase64WorkerFactory(base64, sourcemapArg, enableUnicodeArg) {
    var url;
    return function WorkerFactory(options) {
        url = url || createURL(base64, sourcemapArg, enableUnicodeArg);
        return new Worker(url, options);
    };
}

var WorkerFactory = createBase64WorkerFactory('Lyogcm9sbHVwLXBsdWdpbi13ZWItd29ya2VyLWxvYWRlciAqLwp2YXIgd29ya2VyX2NvZGUgPSAoZnVuY3Rpb24gKGV4cG9ydHMpIHsKICAgICd1c2Ugc3RyaWN0JzsKCiAgICAvKioKICAgICAqIEBpbnRlcm5hbAogICAgICovCiAgICBjbGFzcyBBdWRpb1Rvb2xzIHsKICAgICAgICBzdGF0aWMgZG93bnNhbXBsZShzcmMsIGRlc3QsIHNvdXJjZUluZGV4ID0gMCwgc291cmNlTGVuZ3RoID0gLTEsIGRlc3RJbmRleCA9IDAsIGRlc3RMZW5ndGggPSAtMSkgewogICAgICAgICAgICBpZiAoc291cmNlTGVuZ3RoIDwgMCkKICAgICAgICAgICAgICAgIHNvdXJjZUxlbmd0aCA9IHNyYy5sZW5ndGggLSBzb3VyY2VJbmRleDsKICAgICAgICAgICAgaWYgKGRlc3RMZW5ndGggPCAwKQogICAgICAgICAgICAgICAgZGVzdExlbmd0aCA9IGRlc3QubGVuZ3RoIC0gZGVzdEluZGV4OwogICAgICAgICAgICBpZiAoZGVzdExlbmd0aCA+IHNvdXJjZUxlbmd0aCkgewogICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDYW4ndCBkb3duc2FtcGxlOiBzb3VyY2UgYXJyYXkgbGVuZ3RoICgke3NvdXJjZUxlbmd0aH0pIGlzIHNob3J0ZXIgdGhhbiBkZXN0aW5hdGlvbiAoJHtkZXN0TGVuZ3RofSlgKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAoZGVzdExlbmd0aCA9PT0gMCkgewogICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBDYW4ndCBkb3duc2FtcGxlOiBzb3VyY2UgYXJyYXkgbGVuZ3RoICgke3NvdXJjZUxlbmd0aH0pIGNhbid0IGJlIGRvd25zYW1wbGVkIHRvIHplcm8tbGVuZ3RoIGRlc3RpbmF0aW9uLmApOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChzb3VyY2VMZW5ndGggPT09IDApIHsKICAgICAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigiQ2FuJ3QgZG93bnNhbXBsZTogc291cmNlIHJhbmdlIGNhbid0IGJlIHplcm8gbGVuZ3RoLiIpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChzb3VyY2VMZW5ndGggPT09IDEpIHsKICAgICAgICAgICAgICAgIGRlc3RbMF0gPSBzcmNbMF07CiAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgIH0KICAgICAgICAgICAgbGV0IGRlc3RJbmRleEZyYWN0aW9uID0gMC4wOwogICAgICAgICAgICBjb25zdCBkZXN0U3RlcCA9IChkZXN0TGVuZ3RoIC0gMSkgLyAoc291cmNlTGVuZ3RoIC0gMSk7CiAgICAgICAgICAgIGxldCBzdW0gPSAwOwogICAgICAgICAgICBsZXQgdG90YWxXZWlnaHQgPSAwOwogICAgICAgICAgICBjb25zdCBzb3VyY2VFbmRJbmRleCA9IHNvdXJjZUluZGV4ICsgc291cmNlTGVuZ3RoOwogICAgICAgICAgICBmb3IgKDsgc291cmNlSW5kZXggPCBzb3VyY2VFbmRJbmRleDsgc291cmNlSW5kZXgrKykgewogICAgICAgICAgICAgICAgY29uc3Qgd2VpZ2h0ID0gMC41IC0gTWF0aC5hYnMoZGVzdEluZGV4RnJhY3Rpb24pOwogICAgICAgICAgICAgICAgc3VtICs9IHNyY1tzb3VyY2VJbmRleF0gKiB3ZWlnaHQ7CiAgICAgICAgICAgICAgICB0b3RhbFdlaWdodCArPSB3ZWlnaHQ7CiAgICAgICAgICAgICAgICBkZXN0SW5kZXhGcmFjdGlvbiArPSBkZXN0U3RlcDsKICAgICAgICAgICAgICAgIGlmIChkZXN0SW5kZXhGcmFjdGlvbiA+PSAwLjUpIHsKICAgICAgICAgICAgICAgICAgICBkZXN0SW5kZXhGcmFjdGlvbiAtPSAxOwogICAgICAgICAgICAgICAgICAgIGRlc3RbZGVzdEluZGV4KytdID0gc3VtIC8gdG90YWxXZWlnaHQ7CiAgICAgICAgICAgICAgICAgICAgc3VtID0gMDsKICAgICAgICAgICAgICAgICAgICB0b3RhbFdlaWdodCA9IDA7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgLy8gUHV0IGxhc3QgdmFsdWUgaW4gcGxhY2UKICAgICAgICAgICAgaWYgKHRvdGFsV2VpZ2h0ID4gMCkgewogICAgICAgICAgICAgICAgZGVzdFtkZXN0SW5kZXgrK10gPSBzdW0gLyB0b3RhbFdlaWdodDsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBzdGF0aWMgZ2V0RW5lcmd5KHNhbXBsZXMsIHN0YXJ0ID0gMCwgbGVuZ3RoID0gLTEpIHsKICAgICAgICAgICAgaWYgKGxlbmd0aCA8IDApCiAgICAgICAgICAgICAgICBsZW5ndGggPSBzYW1wbGVzLmxlbmd0aCAtIHN0YXJ0OwogICAgICAgICAgICBpZiAobGVuZ3RoIDw9IDApCiAgICAgICAgICAgICAgICByZXR1cm4gMDsKICAgICAgICAgICAgY29uc3QgZW5kSW5kZXggPSBzdGFydCArIGxlbmd0aDsKICAgICAgICAgICAgbGV0IHN1bUVuZXJneVNxdWFyZWQgPSAwLjA7CiAgICAgICAgICAgIGZvciAoOyBzdGFydCA8IGVuZEluZGV4OyBzdGFydCsrKSB7CiAgICAgICAgICAgICAgICBzdW1FbmVyZ3lTcXVhcmVkICs9IHNhbXBsZXNbc3RhcnRdICogc2FtcGxlc1tzdGFydF07CiAgICAgICAgICAgIH0KICAgICAgICAgICAgcmV0dXJuIE1hdGguc3FydChzdW1FbmVyZ3lTcXVhcmVkIC8gbGVuZ3RoKTsKICAgICAgICB9CiAgICAgICAgc3RhdGljIGdldEF1ZGlvUGVhayhzYW1wbGVzLCBzdGFydCA9IDAsIGxlbmd0aCA9IC0xKSB7CiAgICAgICAgICAgIGlmIChsZW5ndGggPCAwKQogICAgICAgICAgICAgICAgbGVuZ3RoID0gc2FtcGxlcy5sZW5ndGggLSBzdGFydDsKICAgICAgICAgICAgaWYgKGxlbmd0aCA8PSAwKQogICAgICAgICAgICAgICAgcmV0dXJuIDA7CiAgICAgICAgICAgIGNvbnN0IGVuZEluZGV4ID0gc3RhcnQgKyBsZW5ndGg7CiAgICAgICAgICAgIGxldCBwZWFrID0gMDsKICAgICAgICAgICAgZm9yICg7IHN0YXJ0IDwgZW5kSW5kZXg7IHN0YXJ0KyspIHsKICAgICAgICAgICAgICAgIGlmIChzYW1wbGVzW3N0YXJ0XSA+IHBlYWspIHsKICAgICAgICAgICAgICAgICAgICBwZWFrID0gc2FtcGxlc1tzdGFydF07CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICAgICAgcmV0dXJuIHBlYWs7CiAgICAgICAgfQogICAgICAgIHN0YXRpYyBjb252ZXJ0SW50MTZUb0Zsb2F0KHNyYywgZGVzdCwgc3JjU3RhcnRTYW1wbGUgPSAwLCBsZW5ndGhTYW1wbGVzID0gLTEsIGRzdEluZGV4ID0gMCkgewogICAgICAgICAgICBpZiAobGVuZ3RoU2FtcGxlcyA8IDApCiAgICAgICAgICAgICAgICBsZW5ndGhTYW1wbGVzID0gc3JjLmxlbmd0aCAvIDIgLSBzcmNTdGFydFNhbXBsZTsKICAgICAgICAgICAgY29uc3QgbWF4TGVuID0gTWF0aC5taW4oKHNyYy5sZW5ndGggLyAyKSAtIHNyY1N0YXJ0U2FtcGxlLCBkZXN0Lmxlbmd0aCAtIGRzdEluZGV4KTsKICAgICAgICAgICAgbGVuZ3RoU2FtcGxlcyA9IE1hdGgubWluKGxlbmd0aFNhbXBsZXMsIG1heExlbik7CiAgICAgICAgICAgIGlmIChsZW5ndGhTYW1wbGVzIDw9IDApCiAgICAgICAgICAgICAgICByZXR1cm4gMDsKICAgICAgICAgICAgbGV0IGJ5dGVJbmRleCA9IHNyY1N0YXJ0U2FtcGxlICogMjsKICAgICAgICAgICAgY29uc3QgZW5kQnl0ZSA9IGJ5dGVJbmRleCArIGxlbmd0aFNhbXBsZXMgKiAyOwogICAgICAgICAgICB3aGlsZSAoYnl0ZUluZGV4IDwgZW5kQnl0ZSkgewogICAgICAgICAgICAgICAgZGVzdFtkc3RJbmRleCsrXSA9ICgoc3JjW2J5dGVJbmRleCsrXSArIChzcmNbYnl0ZUluZGV4KytdIDw8IDgpKSkgLyAweDdmZmY7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgcmV0dXJuIGxlbmd0aFNhbXBsZXM7CiAgICAgICAgfQogICAgICAgIHN0YXRpYyBjb252ZXJ0RmxvYXRUb0ludDE2KHNyYywgZGVzdCwgc291cmNlSW5kZXggPSAwLCBzb3VyY2VMZW5ndGggPSAtMSwgZHN0SW5kZXggPSAwKSB7CiAgICAgICAgICAgIGlmIChzb3VyY2VMZW5ndGggPCAwKQogICAgICAgICAgICAgICAgc291cmNlTGVuZ3RoID0gc3JjLmxlbmd0aCAtIHNvdXJjZUluZGV4OwogICAgICAgICAgICBjb25zdCBlbmRJbmRleCA9IHNvdXJjZUluZGV4ICsgc291cmNlTGVuZ3RoOwogICAgICAgICAgICB3aGlsZSAoc291cmNlSW5kZXggPCBlbmRJbmRleCkgewogICAgICAgICAgICAgICAgZGVzdFtkc3RJbmRleCsrXSA9IH5+KHNyY1tzb3VyY2VJbmRleCsrXSAqIDB4N2ZmZik7IC8vIFF1aWNrIHRydW5jYXRlLCBubyByb3VuZGluZwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIHN0YXRpYyBlbmVyZ3lUb0RiKGVuZXJneSkgewogICAgICAgICAgICByZXR1cm4gKDEwLjAgKiBNYXRoLmxvZyhlbmVyZ3kpIC8gQXVkaW9Ub29scy5MT0dfMl9QTFVTX0xPR181KTsKICAgICAgICB9CiAgICAgICAgc3RhdGljIGRiVG9FbmVyZ3koZGIpIHsKICAgICAgICAgICAgcmV0dXJuIE1hdGgucG93KDEwLjAsIGRiIC8gMTAuMCk7CiAgICAgICAgfQogICAgfQogICAgQXVkaW9Ub29scy5MT0dfMl9QTFVTX0xPR181ID0gTWF0aC5sb2coMikgKyBNYXRoLmxvZyg1KTsKCiAgICAvKioKICAgICAqIEBpbnRlcm5hbAogICAgICovCiAgICBjbGFzcyBBdWRpb1Byb2Nlc3NvciB7CiAgICAgICAgY29uc3RydWN0b3IoaW5wdXRTYW1wbGVSYXRlLCBvdXRwdXRTYW1wbGVSYXRlLCBmcmFtZU1pbGxpcywgaGlzdG9yeUZyYW1lcykgewogICAgICAgICAgICAvKioKICAgICAgICAgICAgICogU2VuZGluZyBzdGF0ZS4gSWYgdHJ1ZSwgQXVkaW9Qcm9jZXNzb3IgaXMgY3VycmVudGx5IHNlbmRpbmcgYXVkaW8gdmlhIG9uU2VuZEF1ZGlvIGNhbGxiYWNrCiAgICAgICAgICAgICAqLwogICAgICAgICAgICB0aGlzLmlzU2VuZGluZyA9IGZhbHNlOwogICAgICAgICAgICAvKioKICAgICAgICAgICAgICogQ3VycmVudCBjb3VudCBvZiBkb3duc2FtcGxlZCBhbmQgY29udGludW91c2x5IHByb2Nlc3NlZCBzYW1wbGVzICh0aHJ1IFByb2Nlc3NBdWRpbykgZnJvbSBzdGFydCBvZiBzdHJlYW0KICAgICAgICAgICAgICovCiAgICAgICAgICAgIHRoaXMuc3RyZWFtU2FtcGxlUG9zID0gMDsKICAgICAgICAgICAgdGhpcy5zYW1wbGVzU2VudCA9IDA7CiAgICAgICAgICAgIHRoaXMudXR0ZXJhbmNlU2VyaWFsID0gLTE7CiAgICAgICAgICAgIHRoaXMub25TZW5kQXVkaW8gPSAoc2FtcGxlcywgc3RhcnRJbmRleCwgbGVuZ3RoKSA9PiB7IH07CiAgICAgICAgICAgIHRoaXMub25WYWRTdGF0ZUNoYW5nZSA9IChpc1NpZ25hbERldGVjdGVkKSA9PiB7IH07CiAgICAgICAgICAgIHRoaXMuaW5wdXRTYW1wbGVSYXRlID0gMTYwMDA7CiAgICAgICAgICAgIHRoaXMuaW50ZXJuYWxTYW1wbGVSYXRlID0gMTYwMDA7CiAgICAgICAgICAgIHRoaXMuaGlzdG9yeUZyYW1lcyA9IDU7CiAgICAgICAgICAgIHRoaXMuZnJhbWVNaWxsaXMgPSAzMDsKICAgICAgICAgICAgdGhpcy5jdXJyZW50RnJhbWVOdW1iZXIgPSAwOwogICAgICAgICAgICB0aGlzLmZyYW1lU2FtcGxlUG9zID0gMDsKICAgICAgICAgICAgdGhpcy5zdHJlYW1GcmFtZVBvcyA9IDA7CiAgICAgICAgICAgIHRoaXMud2FzU2lnbmFsRGV0ZWN0ZWQgPSBmYWxzZTsKICAgICAgICAgICAgdGhpcy5pbnB1dFNhbXBsZVJhdGUgPSBpbnB1dFNhbXBsZVJhdGU7CiAgICAgICAgICAgIHRoaXMuaW50ZXJuYWxTYW1wbGVSYXRlID0gb3V0cHV0U2FtcGxlUmF0ZTsKICAgICAgICAgICAgdGhpcy5mcmFtZU1pbGxpcyA9IGZyYW1lTWlsbGlzOwogICAgICAgICAgICB0aGlzLmhpc3RvcnlGcmFtZXMgPSBoaXN0b3J5RnJhbWVzOwogICAgICAgICAgICB0aGlzLmZyYW1lU2FtcGxlcyA9IH5+KHRoaXMuaW50ZXJuYWxTYW1wbGVSYXRlICogdGhpcy5mcmFtZU1pbGxpcyAvIDEwMDApOwogICAgICAgICAgICB0aGlzLnNhbXBsZVJpbmdCdWZmZXIgPSBuZXcgRmxvYXQzMkFycmF5KHRoaXMuZnJhbWVTYW1wbGVzICogdGhpcy5oaXN0b3J5RnJhbWVzKTsKICAgICAgICB9CiAgICAgICAgLyoqCiAgICAgICAgICogUHJvY2VzcyBzcGVlY2ggYXVkaW8gc2FtcGxlcyBmcm9tIGEgbWljcm9waG9uZSBvciBvdGhlciBhdWRpbyBzb3VyY2UuCiAgICAgICAgICoKICAgICAgICAgKiBZb3UgY2FuIGNvbnRyb2wgd2hlbiB0byBzdGFydCBhbmQgc3RvcCBwcm9jZXNzIHNwZWVjaCBlaXRoZXIgbWFudWFsbHkgd2l0aCA8c2VlIGNyZWY9IlN0YXJ0Q29udGV4dCIvPiBhbmQgPHNlZSBjcmVmPSJTdG9wQ29udGV4dCIvPiBvcgogICAgICAgICAqIGF1dG9tYXRpY2FsbHkgYnkgcHJvdmlkaW5nIGEgdm9pY2UgYWN0aXZpdHkgZGV0ZWN0aW9uIChWQUQpIGZpZWxkIHRvIDxzZWUgY3JlZj0iU3BlZWNobHlDbGllbnQiLz4uCiAgICAgICAgICoKICAgICAgICAgKiBUaGUgYXVkaW8gaXMgaGFuZGxlZCBhcyBmb2xsb3dzOgogICAgICAgICAqIC0gRG93bnNhbXBsZSB0byAxNmtIeiBpZiBuZWVkZWQKICAgICAgICAgKiAtIEFkZCB0byBoaXN0b3J5IHJpbmdidWZmZXIKICAgICAgICAgKiAtIENhbGN1bGF0ZSBlbmVyZ3kgKFZBRCkKICAgICAgICAgKiAtIEF1dG9tYXRpYyBTdGFydC9TdG9wQ29udGV4dCAoVkFEKQogICAgICAgICAqIC0gU2VuZCB1dHRlcmFuY2UgYXVkaW8gdG8gU3BlZWNobHkgU0xVIGRlY29kZXIKICAgICAgICAgKgogICAgICAgICAqIEBwYXJhbSBmbG9hdHMgLSBBcnJheSBvZiBmbG9hdCBjb250YWluaW5nIHNhbXBsZXMgdG8gZmVlZCB0byB0aGUgYXVkaW8gcGlwZWxpbmUuIEVhY2ggc2FtcGxlIG5lZWRzIHRvIGJlIGluIHJhbmdlIC0xZi4uMWYuCiAgICAgICAgICogQHBhcmFtIHN0YXJ0IC0gU3RhcnQgaW5kZXggb2YgYXVkaW8gdG8gcHJvY2VzcyBpbiBzYW1wbGVzIChkZWZhdWx0OiBgMGApLgogICAgICAgICAqIEBwYXJhbSBsZW5ndGggLSBMZW5ndGggb2YgYXVkaW8gdG8gcHJvY2VzcyBpbiBzYW1wbGVzIG9yIGAtMWAgdG8gcHJvY2VzcyB0aGUgd2hvbGUgYXJyYXkgKGRlZmF1bHQ6IGAtMWApLgogICAgICAgICAqIEBwYXJhbSBlb3NfYXRfZW5kIC0gU3RvcFN0cmVhbSBpbnRlcm5hbGx5IHVzZXMgdGhpcyB0byBmb3JjZSBwcm9jZXNzaW5nIG9mIGxhc3Qgc3ViZnJhbWUgYXQgZW5kIG9mIGF1ZGlvIHN0cmVhbSAoZGVmYXVsdDogYGZhbHNlYCkuCiAgICAgICAgICogQHJldHVybnMKICAgICAgICAgKi8KICAgICAgICBwcm9jZXNzQXVkaW8oZmxvYXRzLCBzdGFydCA9IDAsIGxlbmd0aCA9IC0xLCBlb3NfYXRfZW5kID0gZmFsc2UpIHsKICAgICAgICAgICAgaWYgKGxlbmd0aCA8IDApCiAgICAgICAgICAgICAgICBsZW5ndGggPSBmbG9hdHMubGVuZ3RoOwogICAgICAgICAgICBpZiAobGVuZ3RoID09PSAwKSB7CiAgICAgICAgICAgICAgICBpZiAoZW9zX2F0X2VuZCkgewogICAgICAgICAgICAgICAgICAgIHRoaXMucHJvY2Vzc0VvcygpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGxldCBpID0gc3RhcnQ7CiAgICAgICAgICAgIGNvbnN0IGVuZEluZGV4ID0gc3RhcnQgKyBsZW5ndGg7CiAgICAgICAgICAgIHdoaWxlIChpIDwgZW5kSW5kZXgpIHsKICAgICAgICAgICAgICAgIGNvbnN0IGZyYW1lQmFzZSA9IHRoaXMuY3VycmVudEZyYW1lTnVtYmVyICogdGhpcy5mcmFtZVNhbXBsZXM7CiAgICAgICAgICAgICAgICBpZiAodGhpcy5pbnB1dFNhbXBsZVJhdGUgPT09IHRoaXMuaW50ZXJuYWxTYW1wbGVSYXRlKSB7CiAgICAgICAgICAgICAgICAgICAgLy8gQ29weSBpbnB1dCBzYW1wbGVzIHRvIGZpbGwgY3VycmVudCByaW5nYnVmZmVyIGZyYW1lCiAgICAgICAgICAgICAgICAgICAgY29uc3Qgc2FtcGxlc1RvRmlsbEZyYW1lID0gTWF0aC5taW4oZW5kSW5kZXggLSBpLCB0aGlzLmZyYW1lU2FtcGxlcyAtIHRoaXMuZnJhbWVTYW1wbGVQb3MpOwogICAgICAgICAgICAgICAgICAgIGNvbnN0IGZyYW1lRW5kSW5kZXggPSB0aGlzLmZyYW1lU2FtcGxlUG9zICsgc2FtcGxlc1RvRmlsbEZyYW1lOwogICAgICAgICAgICAgICAgICAgIHdoaWxlICh0aGlzLmZyYW1lU2FtcGxlUG9zIDwgZnJhbWVFbmRJbmRleCkgewogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnNhbXBsZVJpbmdCdWZmZXJbZnJhbWVCYXNlICsgdGhpcy5mcmFtZVNhbXBsZVBvcysrXSA9IGZsb2F0c1tpKytdOwogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGVsc2UgewogICAgICAgICAgICAgICAgICAgIC8vIERvd25zYW1wbGUgaW5wdXQgc2FtcGxlcyB0byBmaWxsIGN1cnJlbnQgcmluZ2J1ZmZlciBmcmFtZQogICAgICAgICAgICAgICAgICAgIGNvbnN0IHJhdGlvID0gMS4wICogdGhpcy5pbnB1dFNhbXBsZVJhdGUgLyB0aGlzLmludGVybmFsU2FtcGxlUmF0ZTsKICAgICAgICAgICAgICAgICAgICBjb25zdCBpbnB1dFNhbXBsZXNUb0ZpbGxGcmFtZSA9IE1hdGgubWluKGVuZEluZGV4IC0gaSwgTWF0aC5yb3VuZChyYXRpbyAqICh0aGlzLmZyYW1lU2FtcGxlcyAtIHRoaXMuZnJhbWVTYW1wbGVQb3MpKSk7CiAgICAgICAgICAgICAgICAgICAgY29uc3Qgc2FtcGxlc1RvRmlsbEZyYW1lID0gTWF0aC5taW4oTWF0aC5yb3VuZCgoZW5kSW5kZXggLSBpKSAvIHJhdGlvKSwgdGhpcy5mcmFtZVNhbXBsZXMgLSB0aGlzLmZyYW1lU2FtcGxlUG9zKTsKICAgICAgICAgICAgICAgICAgICBpZiAoc2FtcGxlc1RvRmlsbEZyYW1lID4gMCkgewogICAgICAgICAgICAgICAgICAgICAgICBBdWRpb1Rvb2xzLmRvd25zYW1wbGUoZmxvYXRzLCB0aGlzLnNhbXBsZVJpbmdCdWZmZXIsIGksIGlucHV0U2FtcGxlc1RvRmlsbEZyYW1lLCBmcmFtZUJhc2UgKyB0aGlzLmZyYW1lU2FtcGxlUG9zLCBzYW1wbGVzVG9GaWxsRnJhbWUpOwogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICBpICs9IGlucHV0U2FtcGxlc1RvRmlsbEZyYW1lOwogICAgICAgICAgICAgICAgICAgIHRoaXMuZnJhbWVTYW1wbGVQb3MgKz0gc2FtcGxlc1RvRmlsbEZyYW1lOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgY29uc3QgZW9zID0gaSA9PT0gZW5kSW5kZXggJiYgZW9zX2F0X2VuZDsKICAgICAgICAgICAgICAgIC8vIFByb2Nlc3MgZnJhbWUKICAgICAgICAgICAgICAgIGlmICh0aGlzLmZyYW1lU2FtcGxlUG9zID09PSB0aGlzLmZyYW1lU2FtcGxlcyB8fCBlb3MpIHsKICAgICAgICAgICAgICAgICAgICBjb25zdCBzdWJGcmFtZVNhbXBsZXMgPSBlb3MgPyB0aGlzLmZyYW1lU2FtcGxlUG9zIDogdGhpcy5mcmFtZVNhbXBsZXM7CiAgICAgICAgICAgICAgICAgICAgdGhpcy5wcm9jZXNzRnJhbWUodGhpcy5zYW1wbGVSaW5nQnVmZmVyLCBmcmFtZUJhc2UsIHN1YkZyYW1lU2FtcGxlcywgZW9zKTsKICAgICAgICAgICAgICAgICAgICBpZiAodGhpcy5pc1NlbmRpbmcpIHsKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKHRoaXMuc2FtcGxlc1NlbnQgPT09IDApIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIC8vIFN0YXJ0IG9mIHRoZSB1dHRlcmFuY2UgLSBzZW5kIGhpc3RvcnkgZnJhbWVzCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb25zdCBzZW5kSGlzdG9yeSA9IE1hdGgubWluKHRoaXMuc3RyZWFtRnJhbWVQb3MsIHRoaXMuaGlzdG9yeUZyYW1lcyAtIDEpOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgbGV0IGhpc3RvcnlGcmFtZUluZGV4ID0gKHRoaXMuY3VycmVudEZyYW1lTnVtYmVyICsgdGhpcy5oaXN0b3J5RnJhbWVzIC0gc2VuZEhpc3RvcnkpICUgdGhpcy5oaXN0b3J5RnJhbWVzOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgd2hpbGUgKGhpc3RvcnlGcmFtZUluZGV4ICE9PSB0aGlzLmN1cnJlbnRGcmFtZU51bWJlcikgewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMub25TZW5kQXVkaW8odGhpcy5zYW1wbGVSaW5nQnVmZmVyLCBoaXN0b3J5RnJhbWVJbmRleCAqIHRoaXMuZnJhbWVTYW1wbGVzLCB0aGlzLmZyYW1lU2FtcGxlcyk7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5zYW1wbGVzU2VudCArPSB0aGlzLmZyYW1lU2FtcGxlczsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoaXN0b3J5RnJhbWVJbmRleCA9IChoaXN0b3J5RnJhbWVJbmRleCArIDEpICUgdGhpcy5oaXN0b3J5RnJhbWVzOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMub25TZW5kQXVkaW8odGhpcy5zYW1wbGVSaW5nQnVmZmVyLCBmcmFtZUJhc2UsIHN1YkZyYW1lU2FtcGxlcyk7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc2FtcGxlc1NlbnQgKz0gc3ViRnJhbWVTYW1wbGVzOwogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICBpZiAoZW9zKSB7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc3RyZWFtU2FtcGxlUG9zICs9IHN1YkZyYW1lU2FtcGxlczsKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5wcm9jZXNzRW9zKCk7CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIGVsc2UgaWYgKHRoaXMuZnJhbWVTYW1wbGVQb3MgPT09IHRoaXMuZnJhbWVTYW1wbGVzKSB7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuZnJhbWVTYW1wbGVQb3MgPSAwOwogICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnN0cmVhbUZyYW1lUG9zICs9IDE7CiAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc3RyZWFtU2FtcGxlUG9zICs9IHN1YkZyYW1lU2FtcGxlczsKICAgICAgICAgICAgICAgICAgICAgICAgdGhpcy5jdXJyZW50RnJhbWVOdW1iZXIgPSAodGhpcy5jdXJyZW50RnJhbWVOdW1iZXIgKyAxKSAlIHRoaXMuaGlzdG9yeUZyYW1lczsKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBpZiAodGhpcy52YWQpIHsKICAgICAgICAgICAgICAgICAgICB0aGlzLndhc1NpZ25hbERldGVjdGVkID0gdGhpcy52YWQuaXNTaWduYWxEZXRlY3RlZDsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBzZXRTZW5kQXVkaW8oYWN0aXZlKSB7CiAgICAgICAgICAgIHRoaXMuaXNTZW5kaW5nID0gYWN0aXZlOwogICAgICAgICAgICBpZiAoYWN0aXZlKSB7CiAgICAgICAgICAgICAgICB0aGlzLnNhbXBsZXNTZW50ID0gMDsKICAgICAgICAgICAgICAgIHRoaXMudXR0ZXJhbmNlU2VyaWFsKys7CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICAgICAgcmVzZXQoaW5wdXRTYW1wbGVSYXRlKSB7CiAgICAgICAgICAgIHZhciBfYTsKICAgICAgICAgICAgdGhpcy5pc1NlbmRpbmcgPSBmYWxzZTsKICAgICAgICAgICAgdGhpcy5zdHJlYW1GcmFtZVBvcyA9IDA7CiAgICAgICAgICAgIHRoaXMuc3RyZWFtU2FtcGxlUG9zID0gMDsKICAgICAgICAgICAgdGhpcy5mcmFtZVNhbXBsZVBvcyA9IDA7CiAgICAgICAgICAgIHRoaXMuY3VycmVudEZyYW1lTnVtYmVyID0gMDsKICAgICAgICAgICAgdGhpcy51dHRlcmFuY2VTZXJpYWwgPSAtMTsKICAgICAgICAgICAgaWYgKGlucHV0U2FtcGxlUmF0ZSkKICAgICAgICAgICAgICAgIHRoaXMuaW5wdXRTYW1wbGVSYXRlID0gaW5wdXRTYW1wbGVSYXRlOwogICAgICAgICAgICB0aGlzLndhc1NpZ25hbERldGVjdGVkID0gZmFsc2U7CiAgICAgICAgICAgIChfYSA9IHRoaXMudmFkKSA9PT0gbnVsbCB8fCBfYSA9PT0gdm9pZCAwID8gdm9pZCAwIDogX2EucmVzZXRWQUQoKTsKICAgICAgICB9CiAgICAgICAgLyoqCiAgICAgICAgICogQHJldHVybnMgY3VycmVudCBwb3NpdGlvbiBpbiBzdHJlYW0gaW4gbWlsbGlzZWNvbmRzCiAgICAgICAgICovCiAgICAgICAgZ2V0U3RyZWFtUG9zaXRpb24oKSB7CiAgICAgICAgICAgIHJldHVybiBNYXRoLnJvdW5kKHRoaXMuc3RyZWFtU2FtcGxlUG9zIC8gdGhpcy5pbnRlcm5hbFNhbXBsZVJhdGUgKiAxMDAwKTsKICAgICAgICB9CiAgICAgICAgZW9zKCkgewogICAgICAgICAgICB0aGlzLnByb2Nlc3NBdWRpbyh0aGlzLnNhbXBsZVJpbmdCdWZmZXIsIDAsIHRoaXMuZnJhbWVTYW1wbGVQb3MsIHRydWUpOwogICAgICAgIH0KICAgICAgICBwcm9jZXNzRnJhbWUoZmxvYXRzLCBzdGFydCA9IDAsIGxlbmd0aCA9IC0xLCBlb3MgPSBmYWxzZSkgewogICAgICAgICAgICB2YXIgX2E7CiAgICAgICAgICAgIGlmICgoX2EgPSB0aGlzLnZhZCkgPT09IG51bGwgfHwgX2EgPT09IHZvaWQgMCA/IHZvaWQgMCA6IF9hLnZhZE9wdGlvbnMuZW5hYmxlZCkgewogICAgICAgICAgICAgICAgdGhpcy52YWQucHJvY2Vzc0ZyYW1lKGZsb2F0cywgc3RhcnQsIGxlbmd0aCwgZW9zKTsKICAgICAgICAgICAgICAgIGlmICh0aGlzLnZhZC5pc1NpZ25hbERldGVjdGVkICE9PSB0aGlzLndhc1NpZ25hbERldGVjdGVkKSB7CiAgICAgICAgICAgICAgICAgICAgdGhpcy5vblZhZFN0YXRlQ2hhbmdlKHRoaXMudmFkLmlzU2lnbmFsRGV0ZWN0ZWQpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgICAgIHByb2Nlc3NFb3MoKSB7CiAgICAgICAgICAgIHZhciBfYTsKICAgICAgICAgICAgaWYgKHRoaXMuaXNTZW5kaW5nICYmICgoX2EgPSB0aGlzLnZhZCkgPT09IG51bGwgfHwgX2EgPT09IHZvaWQgMCA/IHZvaWQgMCA6IF9hLnZhZE9wdGlvbnMuZW5hYmxlZCkpIHsKICAgICAgICAgICAgICAgIC8vIEVuc3VyZSBWQUQgc3RhdGUgY2hhbmdlIG9uIGVuZC1vZi1zdHJlYW0KICAgICAgICAgICAgICAgIHRoaXMudmFkLnJlc2V0VkFEKCk7CiAgICAgICAgICAgICAgICB0aGlzLm9uVmFkU3RhdGVDaGFuZ2UoZmFsc2UpOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfQoKICAgIC8qKgogICAgICogQWRhcHRpdmUgZW5lcmd5IHRocmVzaG9sZCB2b2ljZSBhY3Rpdml0eSBkZXRlY3Rpb24gKFZBRCkgaW1wbGVtZW50YXRpb24uCiAgICAgKiBJdCBjYW4gYmUgdXNlZCB0byBlbmFibGUgaGFuZHMtZnJlZSBvcGVyYXRpb24gb2YgdGhlIFNMVSBkZWNvZGVyLgogICAgICoKICAgICAqIFdoZW4gZW5vdWdoIGZyYW1lcyB3aXRoIGEgc2lnbmFsIHN0cm9uZ2VyIHRoYW4gU2lnbmFsVG9Ob2lzZURiIGhhdmUgYmVlbiBkZXRlY3RlZCwgSXNTaWduYWxEZXRlY3RlZCBnb2VzIHRydWUuIFdoZW4gZW5vdWdoIHNpbGVudCBmcmFtZXMgaGF2ZSBiZWVuIGRldGVjdGVkLCBJc1NpZ25hbERldGVjdGVkIGdvZXMgZmFsc2UgYWZ0ZXIgdGhlIHN1c3RhaW4gdGltZS4KICAgICAqIFVzZSBpdHMgcHVibGljIGZpZWxkcyB0byBjb25maWd1cmUgdGhlIHN0YXRpYyBub2lzZSBnYXRlIGxldmVsLCBzaWduYWwtdG8tbm9pc2UgbGV2ZWwsIGFjdGl2YXRpb24vZGVhY3RpdmF0aW9uIHRyZXNob2xkIChyYXRpbyBvZiBzaWduYWwgdG8gc2lsZW50IGZyYW1lcykgYW5kIHRoZSBzaWduYWwgc3VzdGFpbiB0aW1lLgogICAgICogVGhlIGJhY2tncm91bmQgbm9pc2UgbGV2ZWwgZ3JhZHVhbGx5IGFkYXB0cyB3aGVuIG5vIHNpZ25hbCBpcyBkZXRlY3RlZC4KICAgICAqCiAgICAgKiBJc1NpZ25hbERldGVjdGVkIGNhbiBiZSB1c2VkIHRvIGRyaXZlIFNwZWVjaGx5Q2xpZW50J3MgU3RhcnRDb250ZXh0IGFuZCBTdG9wQ29udGV4dCBhdXRvbWF0aWNhbGx5IGJ5IHNldHRpbmcgQ29udHJvbExpc3RlbmluZyB0cnVlLgogICAgICogQGludGVybmFsCiAgICAgKi8KICAgIGNsYXNzIEVuZXJneVRyZXNob2xkVkFEIHsKICAgICAgICBjb25zdHJ1Y3RvcihmcmFtZU1pbGxpcywgdmFkT3B0aW9ucykgewogICAgICAgICAgICB0aGlzLmlzU2lnbmFsRGV0ZWN0ZWQgPSBmYWxzZTsKICAgICAgICAgICAgdGhpcy5zaWduYWxEYiA9IC05MC4wOwogICAgICAgICAgICB0aGlzLm5vaXNlTGV2ZWxEYiA9IC05MC4wOwogICAgICAgICAgICB0aGlzLmZyYW1lTWlsbGlzID0gMzA7CiAgICAgICAgICAgIHRoaXMuZW5lcmd5ID0gMC4wOwogICAgICAgICAgICB0aGlzLmJhc2VsaW5lRW5lcmd5ID0gLTEuMDsKICAgICAgICAgICAgdGhpcy5sb3VkRnJhbWVCaXRzID0gMDsKICAgICAgICAgICAgdGhpcy52YWRTdXN0YWluTWlsbGlzTGVmdCA9IDA7CiAgICAgICAgICAgIHRoaXMuZnJhbWVNaWxsaXMgPSBmcmFtZU1pbGxpczsKICAgICAgICAgICAgdGhpcy52YWRPcHRpb25zID0gdmFkT3B0aW9uczsKICAgICAgICB9CiAgICAgICAgYWRqdXN0VmFkT3B0aW9ucyh2YWRPcHRpb25zKSB7CiAgICAgICAgICAgIHRoaXMudmFkT3B0aW9ucyA9IE9iamVjdC5hc3NpZ24oT2JqZWN0LmFzc2lnbih7fSwgdGhpcy52YWRPcHRpb25zKSwgdmFkT3B0aW9ucyk7CiAgICAgICAgfQogICAgICAgIHJlc2V0VkFEKCkgewogICAgICAgICAgICB0aGlzLmlzU2lnbmFsRGV0ZWN0ZWQgPSBmYWxzZTsKICAgICAgICAgICAgdGhpcy5sb3VkRnJhbWVCaXRzID0gMDsKICAgICAgICAgICAgdGhpcy5lbmVyZ3kgPSAwOwogICAgICAgICAgICB0aGlzLmJhc2VsaW5lRW5lcmd5ID0gLTE7CiAgICAgICAgfQogICAgICAgIHByb2Nlc3NGcmFtZShmbG9hdHMsIHN0YXJ0ID0gMCwgbGVuZ3RoID0gLTEsIGVvcyA9IGZhbHNlKSB7CiAgICAgICAgICAgIGlmICghdGhpcy52YWRPcHRpb25zLmVuYWJsZWQpIHsKICAgICAgICAgICAgICAgIHRoaXMucmVzZXRWQUQoKTsKICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAoZW9zKQogICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICB0aGlzLmVuZXJneSA9IEF1ZGlvVG9vbHMuZ2V0RW5lcmd5KGZsb2F0cywgc3RhcnQsIGxlbmd0aCk7CiAgICAgICAgICAgIGlmICh0aGlzLmJhc2VsaW5lRW5lcmd5IDwgMCkgewogICAgICAgICAgICAgICAgdGhpcy5iYXNlbGluZUVuZXJneSA9IHRoaXMuZW5lcmd5OwogICAgICAgICAgICB9CiAgICAgICAgICAgIGNvbnN0IGlzTG91ZEZyYW1lID0gdGhpcy5lbmVyZ3kgPiBNYXRoLm1heChBdWRpb1Rvb2xzLmRiVG9FbmVyZ3kodGhpcy52YWRPcHRpb25zLm5vaXNlR2F0ZURiKSwgdGhpcy5iYXNlbGluZUVuZXJneSAqIEF1ZGlvVG9vbHMuZGJUb0VuZXJneSh0aGlzLnZhZE9wdGlvbnMuc2lnbmFsVG9Ob2lzZURiKSk7CiAgICAgICAgICAgIHRoaXMucHVzaEZyYW1lSGlzdG9yeShpc0xvdWRGcmFtZSk7CiAgICAgICAgICAgIHRoaXMuaXNTaWduYWxEZXRlY3RlZCA9IHRoaXMuZGV0ZXJtaW5lTmV3U2lnbmFsU3RhdGUodGhpcy5pc1NpZ25hbERldGVjdGVkKTsKICAgICAgICAgICAgdGhpcy5hZGFwdEJhY2tncm91bmROb2lzZSgpOwogICAgICAgICAgICB0aGlzLnNpZ25hbERiID0gQXVkaW9Ub29scy5lbmVyZ3lUb0RiKHRoaXMuZW5lcmd5IC8gdGhpcy5iYXNlbGluZUVuZXJneSk7CiAgICAgICAgICAgIHRoaXMubm9pc2VMZXZlbERiID0gQXVkaW9Ub29scy5lbmVyZ3lUb0RiKHRoaXMuYmFzZWxpbmVFbmVyZ3kpOwogICAgICAgIH0KICAgICAgICBkZXRlcm1pbmVOZXdTaWduYWxTdGF0ZShjdXJyZW50U3RhdGUpIHsKICAgICAgICAgICAgdGhpcy52YWRTdXN0YWluTWlsbGlzTGVmdCA9IE1hdGgubWF4KHRoaXMudmFkU3VzdGFpbk1pbGxpc0xlZnQgLSB0aGlzLmZyYW1lTWlsbGlzLCAwKTsKICAgICAgICAgICAgY29uc3QgbG91ZEZyYW1lcyA9IHRoaXMuY291bnRMb3VkRnJhbWVzKHRoaXMudmFkT3B0aW9ucy5zaWduYWxTZWFyY2hGcmFtZXMpOwogICAgICAgICAgICBjb25zdCBhY3RpdmF0aW9uRnJhbWVzID0gTWF0aC5yb3VuZCh0aGlzLnZhZE9wdGlvbnMuc2lnbmFsQWN0aXZhdGlvbiAqIHRoaXMudmFkT3B0aW9ucy5zaWduYWxTZWFyY2hGcmFtZXMpOwogICAgICAgICAgICBjb25zdCByZWxlYXNlRnJhbWVzID0gTWF0aC5yb3VuZCh0aGlzLnZhZE9wdGlvbnMuc2lnbmFsUmVsZWFzZSAqIHRoaXMudmFkT3B0aW9ucy5zaWduYWxTZWFyY2hGcmFtZXMpOwogICAgICAgICAgICBpZiAobG91ZEZyYW1lcyA+PSBhY3RpdmF0aW9uRnJhbWVzKSB7CiAgICAgICAgICAgICAgICAvLyBSZW5ldyBzdXN0YWluIHRpbWUKICAgICAgICAgICAgICAgIHRoaXMudmFkU3VzdGFpbk1pbGxpc0xlZnQgPSB0aGlzLnZhZE9wdGlvbnMuc2lnbmFsU3VzdGFpbk1pbGxpczsKICAgICAgICAgICAgICAgIHJldHVybiB0cnVlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmIChsb3VkRnJhbWVzIDw9IHJlbGVhc2VGcmFtZXMgJiYgdGhpcy52YWRTdXN0YWluTWlsbGlzTGVmdCA9PT0gMCkgewogICAgICAgICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgICAgICB9CiAgICAgICAgICAgIHJldHVybiBjdXJyZW50U3RhdGU7CiAgICAgICAgfQogICAgICAgIGFkYXB0QmFja2dyb3VuZE5vaXNlKCkgewogICAgICAgICAgICAvLyBHcmFkdWFsbHkgbGVhcm4gYmFja2dyb3VuZCBub2lzZSBsZXZlbAogICAgICAgICAgICBpZiAoIXRoaXMuaXNTaWduYWxEZXRlY3RlZCkgewogICAgICAgICAgICAgICAgaWYgKHRoaXMudmFkT3B0aW9ucy5ub2lzZUxlYXJuSGFsZnRpbWVNaWxsaXMgPiAwKSB7CiAgICAgICAgICAgICAgICAgICAgdmFyIGRlY2F5ID0gTWF0aC5wb3coMi4wLCAtdGhpcy5mcmFtZU1pbGxpcyAvIHRoaXMudmFkT3B0aW9ucy5ub2lzZUxlYXJuSGFsZnRpbWVNaWxsaXMpOwogICAgICAgICAgICAgICAgICAgIHRoaXMuYmFzZWxpbmVFbmVyZ3kgPSAodGhpcy5iYXNlbGluZUVuZXJneSAqIGRlY2F5KSArICh0aGlzLmVuZXJneSAqICgxIC0gZGVjYXkpKTsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBwdXNoRnJhbWVIaXN0b3J5KGlzTG91ZCkgewogICAgICAgICAgICB0aGlzLmxvdWRGcmFtZUJpdHMgPSAoaXNMb3VkID8gMSA6IDApIHwgKHRoaXMubG91ZEZyYW1lQml0cyA8PCAxKTsKICAgICAgICB9CiAgICAgICAgY291bnRMb3VkRnJhbWVzKG51bUhpc3RvcnlGcmFtZXMpIHsKICAgICAgICAgICAgbGV0IG51bUFjdGl2ZUZyYW1lcyA9IDA7CiAgICAgICAgICAgIGxldCB0ID0gdGhpcy5sb3VkRnJhbWVCaXRzOwogICAgICAgICAgICB3aGlsZSAobnVtSGlzdG9yeUZyYW1lcyA+IDApIHsKICAgICAgICAgICAgICAgIGlmICgodCAmIDEpID09PSAxKQogICAgICAgICAgICAgICAgICAgIG51bUFjdGl2ZUZyYW1lcysrOwogICAgICAgICAgICAgICAgdCA9IHQgPj4gMTsKICAgICAgICAgICAgICAgIG51bUhpc3RvcnlGcmFtZXMtLTsKICAgICAgICAgICAgfQogICAgICAgICAgICByZXR1cm4gbnVtQWN0aXZlRnJhbWVzOwogICAgICAgIH0KICAgIH0KCiAgICAvKioKICAgICAqIEtub3duIFdlYlNvY2tldCByZXNwb25zZSB0eXBlcy4KICAgICAqIEBpbnRlcm5hbAogICAgICovCiAgICB2YXIgV2Vic29ja2V0UmVzcG9uc2VUeXBlOwogICAgKGZ1bmN0aW9uIChXZWJzb2NrZXRSZXNwb25zZVR5cGUpIHsKICAgICAgICBXZWJzb2NrZXRSZXNwb25zZVR5cGVbIlN0YXJ0ZWQiXSA9ICJzdGFydGVkIjsKICAgICAgICBXZWJzb2NrZXRSZXNwb25zZVR5cGVbIlN0b3BwZWQiXSA9ICJzdG9wcGVkIjsKICAgICAgICBXZWJzb2NrZXRSZXNwb25zZVR5cGVbIlNlZ21lbnRFbmQiXSA9ICJzZWdtZW50X2VuZCI7CiAgICAgICAgV2Vic29ja2V0UmVzcG9uc2VUeXBlWyJUcmFuc2NyaXB0Il0gPSAidHJhbnNjcmlwdCI7CiAgICAgICAgV2Vic29ja2V0UmVzcG9uc2VUeXBlWyJFbnRpdHkiXSA9ICJlbnRpdHkiOwogICAgICAgIFdlYnNvY2tldFJlc3BvbnNlVHlwZVsiSW50ZW50Il0gPSAiaW50ZW50IjsKICAgICAgICBXZWJzb2NrZXRSZXNwb25zZVR5cGVbIlRlbnRhdGl2ZVRyYW5zY3JpcHQiXSA9ICJ0ZW50YXRpdmVfdHJhbnNjcmlwdCI7CiAgICAgICAgV2Vic29ja2V0UmVzcG9uc2VUeXBlWyJUZW50YXRpdmVFbnRpdGllcyJdID0gInRlbnRhdGl2ZV9lbnRpdGllcyI7CiAgICAgICAgV2Vic29ja2V0UmVzcG9uc2VUeXBlWyJUZW50YXRpdmVJbnRlbnQiXSA9ICJ0ZW50YXRpdmVfaW50ZW50IjsKICAgIH0pKFdlYnNvY2tldFJlc3BvbnNlVHlwZSB8fCAoV2Vic29ja2V0UmVzcG9uc2VUeXBlID0ge30pKTsKICAgIC8qKgogICAgICogTWVzc2FnZXMgZnJvbSB3b3JrZXIgdG8gY29udHJvbGxlcgogICAgICogQGludGVybmFsCiAgICAgKi8KICAgIHZhciBXb3JrZXJTaWduYWw7CiAgICAoZnVuY3Rpb24gKFdvcmtlclNpZ25hbCkgewogICAgICAgIFdvcmtlclNpZ25hbFsiT3BlbmVkIl0gPSAiV0VCU09DS0VUX09QRU4iOwogICAgICAgIFdvcmtlclNpZ25hbFsiQ2xvc2VkIl0gPSAiV0VCU09DS0VUX0NMT1NFRCI7CiAgICAgICAgV29ya2VyU2lnbmFsWyJBdWRpb1Byb2Nlc3NvclJlYWR5Il0gPSAiU09VUkNFX1NBTVBMRV9SQVRFX1NFVF9TVUNDRVNTIjsKICAgICAgICBXb3JrZXJTaWduYWxbIlZhZFNpZ25hbEhpZ2giXSA9ICJWYWRTaWduYWxIaWdoIjsKICAgICAgICBXb3JrZXJTaWduYWxbIlZhZFNpZ25hbExvdyJdID0gIlZhZFNpZ25hbExvdyI7CiAgICAgICAgV29ya2VyU2lnbmFsWyJSZXF1ZXN0Q29udGV4dFN0YXJ0Il0gPSAiUmVxdWVzdENvbnRleHRTdGFydCI7CiAgICB9KShXb3JrZXJTaWduYWwgfHwgKFdvcmtlclNpZ25hbCA9IHt9KSk7CiAgICAvKioKICAgICAqIE1lc3NhZ2VzIGZyb20gY29udHJvbGxlciB0byB3b3JrZXIKICAgICAqIEBpbnRlcm5hbAogICAgICovCiAgICB2YXIgQ29udHJvbGxlclNpZ25hbDsKICAgIChmdW5jdGlvbiAoQ29udHJvbGxlclNpZ25hbCkgewogICAgICAgIENvbnRyb2xsZXJTaWduYWxbImNvbm5lY3QiXSA9ICJjb25uZWN0IjsKICAgICAgICBDb250cm9sbGVyU2lnbmFsWyJpbml0QXVkaW9Qcm9jZXNzb3IiXSA9ICJpbml0QXVkaW9Qcm9jZXNzb3IiOwogICAgICAgIENvbnRyb2xsZXJTaWduYWxbImFkanVzdEF1ZGlvUHJvY2Vzc29yIl0gPSAiYWRqdXN0QXVkaW9Qcm9jZXNzb3IiOwogICAgICAgIENvbnRyb2xsZXJTaWduYWxbIlNFVF9TSEFSRURfQVJSQVlfQlVGRkVSUyJdID0gIlNFVF9TSEFSRURfQVJSQVlfQlVGRkVSUyI7CiAgICAgICAgQ29udHJvbGxlclNpZ25hbFsiQ0xPU0UiXSA9ICJDTE9TRSI7CiAgICAgICAgQ29udHJvbGxlclNpZ25hbFsiU1RBUlRfQ09OVEVYVCJdID0gIlNUQVJUX0NPTlRFWFQiOwogICAgICAgIENvbnRyb2xsZXJTaWduYWxbIlNXSVRDSF9DT05URVhUIl0gPSAiU1dJVENIX0NPTlRFWFQiOwogICAgICAgIENvbnRyb2xsZXJTaWduYWxbIlNUT1BfQ09OVEVYVCJdID0gIlNUT1BfQ09OVEVYVCI7CiAgICAgICAgQ29udHJvbGxlclNpZ25hbFsiQVVESU8iXSA9ICJBVURJTyI7CiAgICAgICAgQ29udHJvbGxlclNpZ25hbFsic3RhcnRTdHJlYW0iXSA9ICJzdGFydFN0cmVhbSI7CiAgICAgICAgQ29udHJvbGxlclNpZ25hbFsic3RvcFN0cmVhbSJdID0gInN0b3BTdHJlYW0iOwogICAgICAgIENvbnRyb2xsZXJTaWduYWxbInNldENvbnRleHRPcHRpb25zIl0gPSAic2V0Q29udGV4dE9wdGlvbnMiOwogICAgfSkoQ29udHJvbGxlclNpZ25hbCB8fCAoQ29udHJvbGxlclNpZ25hbCA9IHt9KSk7CgogICAgY29uc3QgQ09OVFJPTCA9IHsKICAgICAgICBXUklURV9JTkRFWDogMCwKICAgICAgICBGUkFNRVNfQVZBSUxBQkxFOiAxLAogICAgICAgIExPQ0s6IDIsCiAgICB9OwogICAgLyoqCiAgICAgKiBXZWIgd29ya2VyIHRvIGhhbmRsZSBzdHJlYW1pbmcgYXVkaW8gdG8gY2xvdWQgYW5kIHJlY2VpdmluZyBzcGVlY2ggcHJvY2Vzc2luZyByZXN1bHRzLgogICAgICogQWxzbyBoYW5kbGVzIGF1ZGlvIHByb2Nlc3NpbmcgbGlrZSBtYWludGFpbmluZyBoaXN0b3J5IHJpbmdidWZmZXIgYW5kIHJ1bm5pbmcgdGhlIFZBRAogICAgICogQGludGVybmFsCiAgICAgKi8KICAgIGNsYXNzIFdlYnNvY2tldENsaWVudCB7CiAgICAgICAgY29uc3RydWN0b3IoY3R4KSB7CiAgICAgICAgICAgIHRoaXMudGFyZ2V0U2FtcGxlUmF0ZSA9IDE2MDAwOwogICAgICAgICAgICB0aGlzLmlzQ29udGV4dFN0YXJ0ZWQgPSBmYWxzZTsKICAgICAgICAgICAgdGhpcy5hdWRpb0NvbnRleHRTdGFydFRpbWVzID0gW107CiAgICAgICAgICAgIHRoaXMuaW1tZWRpYXRlTW9kZSA9IGZhbHNlOwogICAgICAgICAgICB0aGlzLmZyYW1lTWlsbGlzID0gMzA7CiAgICAgICAgICAgIHRoaXMub3V0cHV0QXVkaW9GcmFtZSA9IG5ldyBJbnQxNkFycmF5KHRoaXMuZnJhbWVNaWxsaXMgKiB0aGlzLnRhcmdldFNhbXBsZVJhdGUgLyAxMDAwKTsKICAgICAgICAgICAgdGhpcy5kZWJ1ZyA9IGZhbHNlOwogICAgICAgICAgICAvLyBXZWJTb2NrZXQncyBjbG9zZSBoYW5kbGVyLCBjYWxsZWQgd2hlbiBlbmNvdW50ZXJpbmcgYSBub24gdXNlci1pbml0aWF0ZWQgY2xvc2UsIGUuZy4KICAgICAgICAgICAgLy8gLSBuZXR3b3JrIHVucmVhY2hhYmxlIG9yIHVuYWJsZSB0byAocmUpY29ubmVjdCAoY29kZSAxMDA2KQogICAgICAgICAgICAvLyBMaXN0IG9mIENsb3NlRXZlbnQuY29kZSB2YWx1ZXM6IGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0FQSS9DbG9zZUV2ZW50L2NvZGUKICAgICAgICAgICAgdGhpcy5vbldlYnNvY2tldENsb3NlID0gKGV2ZW50KSA9PiB7CiAgICAgICAgICAgICAgICBpZiAodGhpcy5kZWJ1ZykgewogICAgICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKCdbV2ViU29ja2V0Q2xpZW50XScsICdvbldlYnNvY2tldENsb3NlJyk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB0aGlzLmNsb3NlV2Vic29ja2V0KGV2ZW50LmNvZGUsIGV2ZW50LnJlYXNvbiwgZXZlbnQud2FzQ2xlYW4sIGZhbHNlKTsKICAgICAgICAgICAgfTsKICAgICAgICAgICAgdGhpcy5vbldlYnNvY2tldE9wZW4gPSAoX2V2ZW50KSA9PiB7CiAgICAgICAgICAgICAgICBpZiAodGhpcy5kZWJ1ZykgewogICAgICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKCdbV2ViU29ja2V0Q2xpZW50XScsICd3ZWJzb2NrZXQgb3BlbmVkJyk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB0aGlzLndvcmtlckN0eC5wb3N0TWVzc2FnZSh7IHR5cGU6IFdvcmtlclNpZ25hbC5PcGVuZWQgfSk7CiAgICAgICAgICAgIH07CiAgICAgICAgICAgIHRoaXMub25XZWJzb2NrZXRFcnJvciA9IChfZXZlbnQpID0+IHsKICAgICAgICAgICAgICAgIGlmICh0aGlzLmRlYnVnKSB7CiAgICAgICAgICAgICAgICAgICAgY29uc29sZS5sb2coJ1tXZWJTb2NrZXRDbGllbnRdJywgJ3dlYnNvY2tldCBlcnJvcicpOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9OwogICAgICAgICAgICB0aGlzLm9uV2Vic29ja2V0TWVzc2FnZSA9IChldmVudCkgPT4gewogICAgICAgICAgICAgICAgbGV0IHJlc3BvbnNlOwogICAgICAgICAgICAgICAgdHJ5IHsKICAgICAgICAgICAgICAgICAgICByZXNwb25zZSA9IEpTT04ucGFyc2UoZXZlbnQuZGF0YSk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICBjYXRjaCAoZSkgewogICAgICAgICAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoJ1tXZWJTb2NrZXRDbGllbnRdJywgJ2Vycm9yIHBhcnNpbmcgcmVzcG9uc2UgZnJvbSB0aGUgc2VydmVyOicsIGUpOwogICAgICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIGlmIChyZXNwb25zZS50eXBlID09PSBXZWJzb2NrZXRSZXNwb25zZVR5cGUuU3RhcnRlZCkgewogICAgICAgICAgICAgICAgICAgIC8vIEFwcGVuZCBjbGllbnQtc2lkZSBtZXRhZGF0YSB0byB0aGUgYmFja2VuZCBtZXNzYWdlCiAgICAgICAgICAgICAgICAgICAgbGV0IGF1ZGlvQ29udGV4dFN0YXJ0VGltZSA9IHRoaXMuYXVkaW9Db250ZXh0U3RhcnRUaW1lcy5zaGlmdCgpOwogICAgICAgICAgICAgICAgICAgIGlmIChhdWRpb0NvbnRleHRTdGFydFRpbWUgPT09IHVuZGVmaW5lZCkgewogICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLndhcm4oJ05vIHZhbGlkIHZhbHVlIGZvciBjb250ZXh0U3RhcnRNaWxsaXMnKTsKICAgICAgICAgICAgICAgICAgICAgICAgYXVkaW9Db250ZXh0U3RhcnRUaW1lID0gMDsKICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgY29uc3Qgc3RhcnRDb250ZXh0UGFyYW1zID0gewogICAgICAgICAgICAgICAgICAgICAgICBhdWRpb1N0YXJ0VGltZU1pbGxpczogYXVkaW9Db250ZXh0U3RhcnRUaW1lLAogICAgICAgICAgICAgICAgICAgIH07CiAgICAgICAgICAgICAgICAgICAgcmVzcG9uc2UucGFyYW1zID0gc3RhcnRDb250ZXh0UGFyYW1zOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgdGhpcy53b3JrZXJDdHgucG9zdE1lc3NhZ2UocmVzcG9uc2UpOwogICAgICAgICAgICB9OwogICAgICAgICAgICB0aGlzLndvcmtlckN0eCA9IGN0eDsKICAgICAgICB9CiAgICAgICAgY29ubmVjdChhcGlVcmwsIGF1dGhUb2tlbiwgdGFyZ2V0U2FtcGxlUmF0ZSwgZGVidWcpIHsKICAgICAgICAgICAgdGhpcy5kZWJ1ZyA9IGRlYnVnOwogICAgICAgICAgICBpZiAodGhpcy5kZWJ1ZykgewogICAgICAgICAgICAgICAgY29uc29sZS5sb2coJ1tXZWJTb2NrZXRDbGllbnRdJywgJ2Nvbm5lY3RpbmcgdG8gJywgYXBpVXJsKTsKICAgICAgICAgICAgfQogICAgICAgICAgICB0aGlzLnRhcmdldFNhbXBsZVJhdGUgPSB0YXJnZXRTYW1wbGVSYXRlOwogICAgICAgICAgICB0aGlzLmlzQ29udGV4dFN0YXJ0ZWQgPSBmYWxzZTsKICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQgPSBuZXcgV2ViU29ja2V0KGFwaVVybCwgYXV0aFRva2VuKTsKICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQuYWRkRXZlbnRMaXN0ZW5lcignb3BlbicsIHRoaXMub25XZWJzb2NrZXRPcGVuKTsKICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQuYWRkRXZlbnRMaXN0ZW5lcignbWVzc2FnZScsIHRoaXMub25XZWJzb2NrZXRNZXNzYWdlKTsKICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQuYWRkRXZlbnRMaXN0ZW5lcignZXJyb3InLCB0aGlzLm9uV2Vic29ja2V0RXJyb3IpOwogICAgICAgICAgICB0aGlzLndlYnNvY2tldC5hZGRFdmVudExpc3RlbmVyKCdjbG9zZScsIHRoaXMub25XZWJzb2NrZXRDbG9zZSk7CiAgICAgICAgfQogICAgICAgIGluaXRBdWRpb1Byb2Nlc3Nvcihzb3VyY2VTYW1wbGVSYXRlLCBmcmFtZU1pbGxpcywgaGlzdG9yeUZyYW1lcywgdmFkT3B0aW9ucykgewogICAgICAgICAgICB0aGlzLmF1ZGlvUHJvY2Vzc29yID0gbmV3IEF1ZGlvUHJvY2Vzc29yKHNvdXJjZVNhbXBsZVJhdGUsIHRoaXMudGFyZ2V0U2FtcGxlUmF0ZSwgZnJhbWVNaWxsaXMsIGhpc3RvcnlGcmFtZXMpOwogICAgICAgICAgICBpZiAodmFkT3B0aW9ucykgewogICAgICAgICAgICAgICAgdGhpcy5hdWRpb1Byb2Nlc3Nvci52YWQgPSBuZXcgRW5lcmd5VHJlc2hvbGRWQUQoZnJhbWVNaWxsaXMsIHZhZE9wdGlvbnMpOwogICAgICAgICAgICAgICAgdGhpcy5hdWRpb1Byb2Nlc3Nvci5vblZhZFN0YXRlQ2hhbmdlID0gKGlzU2lnbmFsRGV0ZWN0ZWQpID0+IHsKICAgICAgICAgICAgICAgICAgICB2YXIgX2EsIF9iOwogICAgICAgICAgICAgICAgICAgIGNvbnN0IGN1cnJlbnRWYWRPcHRpb25zID0gKF9iID0gKF9hID0gdGhpcy5hdWRpb1Byb2Nlc3NvcikgPT09IG51bGwgfHwgX2EgPT09IHZvaWQgMCA/IHZvaWQgMCA6IF9hLnZhZCkgPT09IG51bGwgfHwgX2IgPT09IHZvaWQgMCA/IHZvaWQgMCA6IF9iLnZhZE9wdGlvbnM7CiAgICAgICAgICAgICAgICAgICAgaWYgKCFjdXJyZW50VmFkT3B0aW9ucykKICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICAgICAgICAgIGlmIChpc1NpZ25hbERldGVjdGVkKSB7CiAgICAgICAgICAgICAgICAgICAgICAgIGlmICghdGhpcy5pbW1lZGlhdGVNb2RlKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aGlzLndvcmtlckN0eC5wb3N0TWVzc2FnZSh7IHR5cGU6IFdvcmtlclNpZ25hbC5WYWRTaWduYWxIaWdoIH0pOwogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgIGVsc2UgaWYgKGN1cnJlbnRWYWRPcHRpb25zLmNvbnRyb2xMaXN0ZW5pbmcpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMuc3RhcnRDb250ZXh0KHRoaXMuZGVmYXVsdENvbnRleHRPcHRpb25zKTsKICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICBpZiAoIWlzU2lnbmFsRGV0ZWN0ZWQpIHsKICAgICAgICAgICAgICAgICAgICAgICAgaWYgKCF0aGlzLmltbWVkaWF0ZU1vZGUpIHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRoaXMud29ya2VyQ3R4LnBvc3RNZXNzYWdlKHsgdHlwZTogV29ya2VyU2lnbmFsLlZhZFNpZ25hbExvdyB9KTsKICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgICAgICBlbHNlIGlmIChjdXJyZW50VmFkT3B0aW9ucy5jb250cm9sTGlzdGVuaW5nKSB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0aGlzLnN0b3BDb250ZXh0KCk7CiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB9OwogICAgICAgICAgICB9CiAgICAgICAgICAgIHRoaXMuYXVkaW9Qcm9jZXNzb3Iub25TZW5kQXVkaW8gPSAoZmxvYXRzLCBzdGFydEluZGV4LCBsZW5ndGgpID0+IHsKICAgICAgICAgICAgICAgIEF1ZGlvVG9vbHMuY29udmVydEZsb2F0VG9JbnQxNihmbG9hdHMsIHRoaXMub3V0cHV0QXVkaW9GcmFtZSwgc3RhcnRJbmRleCwgbGVuZ3RoKTsKICAgICAgICAgICAgICAgIHRoaXMuc2VuZCh0aGlzLm91dHB1dEF1ZGlvRnJhbWUpOwogICAgICAgICAgICB9OwogICAgICAgICAgICBpZiAodGhpcy53b3JrZXJDdHggPT09IHVuZGVmaW5lZCkKICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgdGhpcy53b3JrZXJDdHgucG9zdE1lc3NhZ2UoeyB0eXBlOiBXb3JrZXJTaWduYWwuQXVkaW9Qcm9jZXNzb3JSZWFkeSB9KTsKICAgICAgICB9CiAgICAgICAgLyoqCiAgICAgICAgICogQ29udHJvbCBhdWRpbyBwcm9jZXNzb3IgcGFyYW1ldGVycwogICAgICAgICAqIEBwYXJhbSBhcCAtIEF1ZGlvIHByb2Nlc3NvciBwYXJhbWV0ZXJzIHRvIGFkanVzdAogICAgICAgICAqLwogICAgICAgIGFkanVzdEF1ZGlvUHJvY2Vzc29yKGFwKSB7CiAgICAgICAgICAgIGlmICh0aGlzLmF1ZGlvUHJvY2Vzc29yICYmIGFwLnZhZCkgewogICAgICAgICAgICAgICAgaWYgKCF0aGlzLmF1ZGlvUHJvY2Vzc29yLnZhZCkgewogICAgICAgICAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcignTm8gVkFEIGluIEF1ZGlvUHJvY2Vzc29yLiBEaWQgeW91IGRlZmluZSBgdmFkYCBpbiBCcm93c2VyQ2xpZW50IGNvbnN0cnVjdG9yIHBhcmFtZXRlcnM/Jyk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICB0aGlzLmF1ZGlvUHJvY2Vzc29yLnZhZC5hZGp1c3RWYWRPcHRpb25zKGFwLnZhZCk7CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICAgICAgc2V0U2hhcmVkQXJyYXlCdWZmZXJzKGNvbnRyb2xTQUIsIGRhdGFTQUIpIHsKICAgICAgICAgICAgdGhpcy5jb250cm9sU0FCID0gbmV3IEludDMyQXJyYXkoY29udHJvbFNBQik7CiAgICAgICAgICAgIHRoaXMuZGF0YVNBQiA9IG5ldyBGbG9hdDMyQXJyYXkoZGF0YVNBQik7CiAgICAgICAgICAgIGNvbnN0IGF1ZGlvSGFuZGxlSW50ZXJ2YWwgPSB0aGlzLmRhdGFTQUIubGVuZ3RoIC8gMzI7IC8vIG1zCiAgICAgICAgICAgIGlmICh0aGlzLmRlYnVnKSB7CiAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnW1dlYlNvY2tldENsaWVudF0nLCAnQXVkaW8gaGFuZGxlIGludGVydmFsJywgYXVkaW9IYW5kbGVJbnRlcnZhbCwgJ21zJyk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgc2V0SW50ZXJ2YWwodGhpcy5wcm9jZXNzQXVkaW9TQUIuYmluZCh0aGlzKSwgYXVkaW9IYW5kbGVJbnRlcnZhbCk7CiAgICAgICAgfQogICAgICAgIHN0YXJ0U3RyZWFtKHN0cmVhbU9wdGlvbnMpIHsKICAgICAgICAgICAgaWYgKCF0aGlzLmF1ZGlvUHJvY2Vzc29yKSB7CiAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ05vIEF1ZGlvUHJvY2Vzc29yJyk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgdGhpcy5pbW1lZGlhdGVNb2RlID0gc3RyZWFtT3B0aW9ucy5pbW1lZGlhdGU7CiAgICAgICAgICAgIHRoaXMuYXVkaW9Qcm9jZXNzb3IucmVzZXQoc3RyZWFtT3B0aW9ucy5zYW1wbGVSYXRlKTsKICAgICAgICAgICAgdGhpcy5hdWRpb0NvbnRleHRTdGFydFRpbWVzID0gW107CiAgICAgICAgfQogICAgICAgIHN0b3BTdHJlYW0oKSB7CiAgICAgICAgICAgIGlmICghdGhpcy5hdWRpb1Byb2Nlc3NvcikgewogICAgICAgICAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdObyBBdWRpb1Byb2Nlc3NvcicpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIC8vIFNlbmQgRU9TLiBFbnN1cmUgVkFEIHdpbGwgZ28gb2ZmIGF0IGVuZCBvZiBzdHJlYW0gYW5kIHN0b3BDb250ZXh0IGlzIGNhbGxlZCBpbiBpbW1lZGlhdGUgbW9kZQogICAgICAgICAgICB0aGlzLmF1ZGlvUHJvY2Vzc29yLmVvcygpOwogICAgICAgIH0KICAgICAgICAvKioKICAgICAgICAgKiBQcm9jZXNzZXMgYW5kIHNlbmRzIGF1ZGlvCiAgICAgICAgICogQHBhcmFtIGF1ZGlvQ2h1bmsgLSBhdWRpbyBkYXRhIHRvIHByb2Nlc3MKICAgICAgICAgKi8KICAgICAgICBwcm9jZXNzQXVkaW8oYXVkaW9DaHVuaykgewogICAgICAgICAgICBpZiAoIXRoaXMuYXVkaW9Qcm9jZXNzb3IpIHsKICAgICAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcignTm8gQXVkaW9Qcm9jZXNzb3InKTsKICAgICAgICAgICAgfQogICAgICAgICAgICB0aGlzLmF1ZGlvUHJvY2Vzc29yLnByb2Nlc3NBdWRpbyhhdWRpb0NodW5rKTsKICAgICAgICB9CiAgICAgICAgcHJvY2Vzc0F1ZGlvU0FCKCkgewogICAgICAgICAgICBpZiAoIXRoaXMuY29udHJvbFNBQiB8fCAhdGhpcy5kYXRhU0FCKSB7CiAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ05vIFNoYXJlZEFycmF5QnVmZmVycycpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGNvbnN0IGZyYW1lc0F2YWlsYWJsZSA9IHRoaXMuY29udHJvbFNBQltDT05UUk9MLkZSQU1FU19BVkFJTEFCTEVdOwogICAgICAgICAgICBjb25zdCBsb2NrID0gdGhpcy5jb250cm9sU0FCW0NPTlRST0wuTE9DS107CiAgICAgICAgICAgIGlmIChsb2NrID09PSAwICYmIGZyYW1lc0F2YWlsYWJsZSA+IDApIHsKICAgICAgICAgICAgICAgIGNvbnN0IGRhdGEgPSB0aGlzLmRhdGFTQUIuc3ViYXJyYXkoMCwgZnJhbWVzQXZhaWxhYmxlKTsKICAgICAgICAgICAgICAgIHRoaXMuY29udHJvbFNBQltDT05UUk9MLkZSQU1FU19BVkFJTEFCTEVdID0gMDsKICAgICAgICAgICAgICAgIHRoaXMuY29udHJvbFNBQltDT05UUk9MLldSSVRFX0lOREVYXSA9IDA7CiAgICAgICAgICAgICAgICBpZiAoZGF0YS5sZW5ndGggPiAwKSB7CiAgICAgICAgICAgICAgICAgICAgdGhpcy5wcm9jZXNzQXVkaW8oZGF0YSk7CiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICAgICAgc3RhcnRDb250ZXh0KGNvbnRleHRPcHRpb25zKSB7CiAgICAgICAgICAgIHZhciBfYTsKICAgICAgICAgICAgaWYgKCF0aGlzLmF1ZGlvUHJvY2Vzc29yKSB7CiAgICAgICAgICAgICAgICB0aHJvdyBFcnJvcignTm8gQXVkaW9Qcm9jZXNzb3InKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAodGhpcy5pc0NvbnRleHRTdGFydGVkKSB7CiAgICAgICAgICAgICAgICBjb25zb2xlLmVycm9yKCdbV2ViU29ja2V0Q2xpZW50XScsICJjYW4ndCBzdGFydCBjb250ZXh0OiBhY3RpdmUgY29udGV4dCBleGlzdHMiKTsKICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgfQogICAgICAgICAgICB0aGlzLmF1ZGlvUHJvY2Vzc29yLnNldFNlbmRBdWRpbyh0cnVlKTsKICAgICAgICAgICAgdGhpcy5pc0NvbnRleHRTdGFydGVkID0gdHJ1ZTsKICAgICAgICAgICAgdGhpcy5hdWRpb0NvbnRleHRTdGFydFRpbWVzLnB1c2godGhpcy5hdWRpb1Byb2Nlc3Nvci5nZXRTdHJlYW1Qb3NpdGlvbigpKTsKICAgICAgICAgICAgdGhpcy53b3JrZXJDdHgucG9zdE1lc3NhZ2UoeyB0eXBlOiBXb3JrZXJTaWduYWwuUmVxdWVzdENvbnRleHRTdGFydCB9KTsKICAgICAgICAgICAgbGV0IG9wdGlvbnMgPSAoX2EgPSB0aGlzLmRlZmF1bHRDb250ZXh0T3B0aW9ucykgIT09IG51bGwgJiYgX2EgIT09IHZvaWQgMCA/IF9hIDoge307CiAgICAgICAgICAgIGlmIChjb250ZXh0T3B0aW9ucyAhPT0gdW5kZWZpbmVkKSB7CiAgICAgICAgICAgICAgICBvcHRpb25zID0gT2JqZWN0LmFzc2lnbihPYmplY3QuYXNzaWduKHt9LCBvcHRpb25zKSwgY29udGV4dE9wdGlvbnMpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGNvbnN0IG1lc3NhZ2UgPSBjb250ZXh0T3B0aW9uc1RvTXNnKG9wdGlvbnMpOwogICAgICAgICAgICBtZXNzYWdlLmV2ZW50ID0gJ3N0YXJ0JzsKICAgICAgICAgICAgdGhpcy5zZW5kKEpTT04uc3RyaW5naWZ5KG1lc3NhZ2UpKTsKICAgICAgICB9CiAgICAgICAgc3RvcENvbnRleHQoKSB7CiAgICAgICAgICAgIGlmICghdGhpcy5hdWRpb1Byb2Nlc3NvcikgewogICAgICAgICAgICAgICAgdGhyb3cgRXJyb3IoJ05vIEF1ZGlvUHJvY2Vzc29yJyk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKCF0aGlzLmlzQ29udGV4dFN0YXJ0ZWQpIHsKICAgICAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoJ1tXZWJTb2NrZXRDbGllbnRdJywgImNhbid0IHN0b3AgY29udGV4dDogbm8gYWN0aXZlIGNvbnRleHQiKTsKICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgfQogICAgICAgICAgICB0aGlzLmF1ZGlvUHJvY2Vzc29yLnNldFNlbmRBdWRpbyhmYWxzZSk7CiAgICAgICAgICAgIHRoaXMuaXNDb250ZXh0U3RhcnRlZCA9IGZhbHNlOwogICAgICAgICAgICBjb25zdCBTdG9wRXZlbnRKU09OID0gSlNPTi5zdHJpbmdpZnkoeyBldmVudDogJ3N0b3AnIH0pOwogICAgICAgICAgICB0aGlzLnNlbmQoU3RvcEV2ZW50SlNPTik7CiAgICAgICAgfQogICAgICAgIHN3aXRjaENvbnRleHQoY29udGV4dE9wdGlvbnMpIHsKICAgICAgICAgICAgaWYgKCF0aGlzLndlYnNvY2tldCkgewogICAgICAgICAgICAgICAgdGhyb3cgRXJyb3IoJ1dlYlNvY2tldCBpcyB1bmRlZmluZWQnKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBpZiAoIXRoaXMuaXNDb250ZXh0U3RhcnRlZCkgewogICAgICAgICAgICAgICAgY29uc29sZS5lcnJvcignW1dlYlNvY2tldENsaWVudF0nLCAiY2FuJ3Qgc3dpdGNoIGNvbnRleHQ6IG5vIGFjdGl2ZSBjb250ZXh0Iik7CiAgICAgICAgICAgICAgICByZXR1cm47CiAgICAgICAgICAgIH0KICAgICAgICAgICAgaWYgKChjb250ZXh0T3B0aW9ucyA9PT0gbnVsbCB8fCBjb250ZXh0T3B0aW9ucyA9PT0gdm9pZCAwID8gdm9pZCAwIDogY29udGV4dE9wdGlvbnMuYXBwSWQpID09PSB1bmRlZmluZWQpIHsKICAgICAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoJ1tXZWJTb2NrZXRDbGllbnRdJywgImNhbid0IHN3aXRjaCBjb250ZXh0OiBuZXcgYXBwIGlkIGlzIHVuZGVmaW5lZCIpOwogICAgICAgICAgICAgICAgcmV0dXJuOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGNvbnN0IFN0b3BFdmVudEpTT04gPSBKU09OLnN0cmluZ2lmeSh7IGV2ZW50OiAnc3RvcCcgfSk7CiAgICAgICAgICAgIHRoaXMuc2VuZChTdG9wRXZlbnRKU09OKTsKICAgICAgICAgICAgY29uc3QgbWVzc2FnZSA9IGNvbnRleHRPcHRpb25zVG9Nc2coY29udGV4dE9wdGlvbnMpOwogICAgICAgICAgICBtZXNzYWdlLmV2ZW50ID0gJ3N0YXJ0JzsKICAgICAgICAgICAgdGhpcy5zZW5kKEpTT04uc3RyaW5naWZ5KG1lc3NhZ2UpKTsKICAgICAgICB9CiAgICAgICAgY2xvc2VXZWJzb2NrZXQoY29kZSA9IDEwMDUsIHJlYXNvbiA9ICdObyBTdGF0dXMgUmVjZWl2ZWQnLCB3YXNDbGVhbiA9IHRydWUsIHVzZXJJbml0aWF0ZWQgPSB0cnVlKSB7CiAgICAgICAgICAgIHZhciBfYTsKICAgICAgICAgICAgaWYgKCF0aGlzLndlYnNvY2tldCkgewogICAgICAgICAgICAgICAgY29uc29sZS53YXJuKCdXZWJTb2NrZXQgYWxyZWFkeSBjbG9zZWQnKTsKICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgfQogICAgICAgICAgICBlbHNlIGlmICh0aGlzLmRlYnVnKSB7CiAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnW1dlYlNvY2tldENsaWVudF0nLCB1c2VySW5pdGlhdGVkID8gJ1dlYnNvY2tldCBjbG9zZSByZXF1ZXN0ZWQnIDogJ1dlYnNvY2tldCBjbG9zZWQnKTsKICAgICAgICAgICAgfQogICAgICAgICAgICAvLyBSZXNldCBhdWRpb3Byb2Nlc3NvciBzbyBpdCB3b24ndCB0cnkgdG8gc2VuZCBhdWRpbyB0aGUgZmlyc3QgdGhpbmcgd2hlbiByZWNvbm5lY3QgaGFwcGVucy4gVGhpcyB3aWxsIGxlYWQgdG8gYSByZWNvbm5lY3QgbG9vcC4KICAgICAgICAgICAgKF9hID0gdGhpcy5hdWRpb1Byb2Nlc3NvcikgPT09IG51bGwgfHwgX2EgPT09IHZvaWQgMCA/IHZvaWQgMCA6IF9hLnJlc2V0KCk7CiAgICAgICAgICAgIC8vIFdlIGRvbid0IHdhbnQgYW55IG1vcmUgbWVzc2FnZXMgZnJvbSB0aGUgY2xvc2luZyB3ZWJzb2NrZXQKICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQucmVtb3ZlRXZlbnRMaXN0ZW5lcignb3BlbicsIHRoaXMub25XZWJzb2NrZXRPcGVuKTsKICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQucmVtb3ZlRXZlbnRMaXN0ZW5lcignbWVzc2FnZScsIHRoaXMub25XZWJzb2NrZXRNZXNzYWdlKTsKICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQucmVtb3ZlRXZlbnRMaXN0ZW5lcignZXJyb3InLCB0aGlzLm9uV2Vic29ja2V0RXJyb3IpOwogICAgICAgICAgICB0aGlzLndlYnNvY2tldC5yZW1vdmVFdmVudExpc3RlbmVyKCdjbG9zZScsIHRoaXMub25XZWJzb2NrZXRDbG9zZSk7CiAgICAgICAgICAgIC8vIElmIHdlJ3JlIGhlcmUgZHVlIHRvIGEgY2FsbCB0byBvbldlYlNvY2tldAogICAgICAgICAgICBpZiAodXNlckluaXRpYXRlZCkgewogICAgICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQuY2xvc2UoY29kZSwgcmVhc29uKTsKICAgICAgICAgICAgfQogICAgICAgICAgICB0aGlzLndlYnNvY2tldCA9IHVuZGVmaW5lZDsKICAgICAgICAgICAgdGhpcy53b3JrZXJDdHgucG9zdE1lc3NhZ2UoewogICAgICAgICAgICAgICAgdHlwZTogV29ya2VyU2lnbmFsLkNsb3NlZCwKICAgICAgICAgICAgICAgIGNvZGUsCiAgICAgICAgICAgICAgICByZWFzb24sCiAgICAgICAgICAgICAgICB3YXNDbGVhbiwKICAgICAgICAgICAgfSk7CiAgICAgICAgfQogICAgICAgIHNlbmQoZGF0YSkgewogICAgICAgICAgICBpZiAoIXRoaXMud2Vic29ja2V0KSB7CiAgICAgICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ05vIFdlYnNvY2tldCcpOwogICAgICAgICAgICB9CiAgICAgICAgICAgIGlmICh0aGlzLndlYnNvY2tldC5yZWFkeVN0YXRlICE9PSB0aGlzLndlYnNvY2tldC5PUEVOKSB7CiAgICAgICAgICAgICAgICBjb25zb2xlLndhcm4oYEV4cGVjdGVkIE9QRU4gV2Vic29ja2V0IHN0YXRlLCBidXQgZ290ICR7dGhpcy53ZWJzb2NrZXQucmVhZHlTdGF0ZX1gKTsKICAgICAgICAgICAgICAgIHJldHVybjsKICAgICAgICAgICAgfQogICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgdGhpcy53ZWJzb2NrZXQuc2VuZChkYXRhKTsKICAgICAgICAgICAgfQogICAgICAgICAgICBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKCdbV2ViU29ja2V0Q2xpZW50XScsICdzZXJ2ZXIgY29ubmVjdGlvbiBlcnJvcicsIGVycm9yKTsKICAgICAgICAgICAgfQogICAgICAgIH0KICAgICAgICBzZXRDb250ZXh0T3B0aW9ucyhvcHRpb25zKSB7CiAgICAgICAgICAgIHRoaXMuZGVmYXVsdENvbnRleHRPcHRpb25zID0gb3B0aW9uczsKICAgICAgICB9CiAgICB9CiAgICBjb25zdCBjdHggPSBzZWxmOwogICAgY29uc3Qgd2Vic29ja2V0Q2xpZW50ID0gbmV3IFdlYnNvY2tldENsaWVudChjdHgpOwogICAgY3R4Lm9ubWVzc2FnZSA9IGZ1bmN0aW9uIChlKSB7CiAgICAgICAgc3dpdGNoIChlLmRhdGEudHlwZSkgewogICAgICAgICAgICBjYXNlIENvbnRyb2xsZXJTaWduYWwuY29ubmVjdDoKICAgICAgICAgICAgICAgIHdlYnNvY2tldENsaWVudC5jb25uZWN0KGUuZGF0YS5hcGlVcmwsIGUuZGF0YS5hdXRoVG9rZW4sIGUuZGF0YS50YXJnZXRTYW1wbGVSYXRlLCBlLmRhdGEuZGVidWcpOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgQ29udHJvbGxlclNpZ25hbC5pbml0QXVkaW9Qcm9jZXNzb3I6CiAgICAgICAgICAgICAgICB3ZWJzb2NrZXRDbGllbnQuaW5pdEF1ZGlvUHJvY2Vzc29yKGUuZGF0YS5zb3VyY2VTYW1wbGVSYXRlLCBlLmRhdGEuZnJhbWVNaWxsaXMsIGUuZGF0YS5oaXN0b3J5RnJhbWVzLCBlLmRhdGEudmFkT3B0aW9ucyk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSBDb250cm9sbGVyU2lnbmFsLmFkanVzdEF1ZGlvUHJvY2Vzc29yOgogICAgICAgICAgICAgICAgd2Vic29ja2V0Q2xpZW50LmFkanVzdEF1ZGlvUHJvY2Vzc29yKGUuZGF0YS5wYXJhbXMpOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgQ29udHJvbGxlclNpZ25hbC5TRVRfU0hBUkVEX0FSUkFZX0JVRkZFUlM6CiAgICAgICAgICAgICAgICB3ZWJzb2NrZXRDbGllbnQuc2V0U2hhcmVkQXJyYXlCdWZmZXJzKGUuZGF0YS5jb250cm9sU0FCLCBlLmRhdGEuZGF0YVNBQik7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSBDb250cm9sbGVyU2lnbmFsLkNMT1NFOgogICAgICAgICAgICAgICAgd2Vic29ja2V0Q2xpZW50LmNsb3NlV2Vic29ja2V0KDEwMDAsICdDbG9zZSByZXF1ZXN0ZWQgYnkgY2xpZW50JywgdHJ1ZSwgdHJ1ZSk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSBDb250cm9sbGVyU2lnbmFsLnN0YXJ0U3RyZWFtOgogICAgICAgICAgICAgICAgd2Vic29ja2V0Q2xpZW50LnN0YXJ0U3RyZWFtKGUuZGF0YS5zdHJlYW1PcHRpb25zKTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlIENvbnRyb2xsZXJTaWduYWwuc3RvcFN0cmVhbToKICAgICAgICAgICAgICAgIHdlYnNvY2tldENsaWVudC5zdG9wU3RyZWFtKCk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSBDb250cm9sbGVyU2lnbmFsLlNUQVJUX0NPTlRFWFQ6CiAgICAgICAgICAgICAgICB3ZWJzb2NrZXRDbGllbnQuc3RhcnRDb250ZXh0KGUuZGF0YS5vcHRpb25zKTsKICAgICAgICAgICAgICAgIGJyZWFrOwogICAgICAgICAgICBjYXNlIENvbnRyb2xsZXJTaWduYWwuU1dJVENIX0NPTlRFWFQ6CiAgICAgICAgICAgICAgICB3ZWJzb2NrZXRDbGllbnQuc3dpdGNoQ29udGV4dChlLmRhdGEub3B0aW9ucyk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgY2FzZSBDb250cm9sbGVyU2lnbmFsLlNUT1BfQ09OVEVYVDoKICAgICAgICAgICAgICAgIHdlYnNvY2tldENsaWVudC5zdG9wQ29udGV4dCgpOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgQ29udHJvbGxlclNpZ25hbC5BVURJTzoKICAgICAgICAgICAgICAgIHdlYnNvY2tldENsaWVudC5wcm9jZXNzQXVkaW8oZS5kYXRhLnBheWxvYWQpOwogICAgICAgICAgICAgICAgYnJlYWs7CiAgICAgICAgICAgIGNhc2UgQ29udHJvbGxlclNpZ25hbC5zZXRDb250ZXh0T3B0aW9uczoKICAgICAgICAgICAgICAgIHdlYnNvY2tldENsaWVudC5zZXRDb250ZXh0T3B0aW9ucyhlLmRhdGEub3B0aW9ucyk7CiAgICAgICAgICAgICAgICBicmVhazsKICAgICAgICAgICAgZGVmYXVsdDoKICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKCdXT1JLRVInLCBlKTsKICAgICAgICB9CiAgICB9OwogICAgZnVuY3Rpb24gY29udGV4dE9wdGlvbnNUb01zZyhjb250ZXh0T3B0aW9ucykgewogICAgICAgIGNvbnN0IG1lc3NhZ2UgPSB7CiAgICAgICAgICAgIG9wdGlvbnM6IHsKICAgICAgICAgICAgICAgIHRpbWV6b25lOiBbSW50bC5EYXRlVGltZUZvcm1hdCgpLnJlc29sdmVkT3B0aW9ucygpLnRpbWVab25lXSwKICAgICAgICAgICAgfSwKICAgICAgICB9OwogICAgICAgIGlmIChjb250ZXh0T3B0aW9ucyA9PT0gdW5kZWZpbmVkKQogICAgICAgICAgICByZXR1cm4gbWVzc2FnZTsKICAgICAgICBtZXNzYWdlLm9wdGlvbnMudm9jYWJ1bGFyeSA9IGNvbnRleHRPcHRpb25zLnZvY2FidWxhcnk7CiAgICAgICAgbWVzc2FnZS5vcHRpb25zLnZvY2FidWxhcnlfYmlhcyA9IGNvbnRleHRPcHRpb25zLnZvY2FidWxhcnlCaWFzOwogICAgICAgIG1lc3NhZ2Uub3B0aW9ucy5zaWxlbmNlX3RyaWdnZXJlZF9zZWdtZW50YXRpb24gPSBjb250ZXh0T3B0aW9ucy5zaWxlbmNlVHJpZ2dlcmVkU2VnbWVudGF0aW9uOwogICAgICAgIGlmIChjb250ZXh0T3B0aW9ucy5ub25TdHJlYW1pbmdObHUpIHsKICAgICAgICAgICAgbWVzc2FnZS5vcHRpb25zLm5vbl9zdHJlYW1pbmdfbmx1ID0gWyd5ZXMnXTsKICAgICAgICB9CiAgICAgICAgZWxzZSB7CiAgICAgICAgICAgIG1lc3NhZ2Uub3B0aW9ucy5ub25fc3RyZWFtaW5nX25sdSA9IFsnbm8nXTsKICAgICAgICB9CiAgICAgICAgaWYgKChjb250ZXh0T3B0aW9ucyA9PT0gbnVsbCB8fCBjb250ZXh0T3B0aW9ucyA9PT0gdm9pZCAwID8gdm9pZCAwIDogY29udGV4dE9wdGlvbnMudGltZXpvbmUpICE9PSB1bmRlZmluZWQpIHsKICAgICAgICAgICAgbWVzc2FnZS5vcHRpb25zLnRpbWV6b25lID0gY29udGV4dE9wdGlvbnMgPT09IG51bGwgfHwgY29udGV4dE9wdGlvbnMgPT09IHZvaWQgMCA/IHZvaWQgMCA6IGNvbnRleHRPcHRpb25zLnRpbWV6b25lOyAvLyBvdmVycmlkZSBicm93c2VyIHRpbWV6b25lCiAgICAgICAgfQogICAgICAgIGlmIChjb250ZXh0T3B0aW9ucy5hcHBJZCAhPT0gdW5kZWZpbmVkKSB7CiAgICAgICAgICAgIG1lc3NhZ2UuYXBwSWQgPSBjb250ZXh0T3B0aW9ucy5hcHBJZDsKICAgICAgICB9CiAgICAgICAgcmV0dXJuIG1lc3NhZ2U7CiAgICB9CgogICAgZXhwb3J0cy5jb250ZXh0T3B0aW9uc1RvTXNnID0gY29udGV4dE9wdGlvbnNUb01zZzsKICAgIGV4cG9ydHNbImRlZmF1bHQiXSA9IFdlYnNvY2tldENsaWVudDsKCiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pOwoKICAgIHJldHVybiBleHBvcnRzOwoKfSkoe30pOwoK', null, false);
/* eslint-enable */

/**
 * Controller to communicate with the web worker
 * @internal
 */
class WebWorkerController {
    onResponse(cb) {
        this.onResponseCb = cb;
    }
    onClose(cb) {
        this.onCloseCb = cb;
    }
    constructor() {
        this.startCbs = [];
        this.stopCbs = [];
        this.onResponseCb = () => { };
        this.onCloseCb = () => { };
        this.onWebsocketMessage = (event) => {
            const response = event.data;
            switch (response.type) {
                case WorkerSignal.Opened:
                    if (this.onInitResolve) {
                        this.onInitResolve();
                    }
                    break;
                case WorkerSignal.Closed:
                    const e = new WebsocketError(event.data.reason, event.data.code, event.data.wasClean);
                    if (this.onInitReject) {
                        this.onInitReject(e);
                    }
                    else {
                        this.onCloseCb(e);
                    }
                    break;
                case WorkerSignal.AudioProcessorReady:
                    if (this.resolveSourceSampleRateSet != null) {
                        this.resolveSourceSampleRateSet();
                    }
                    break;
                case WebsocketResponseType.Started:
                    this.onResponseCb(response);
                    this.startCbs.forEach(cb => {
                        try {
                            cb(undefined, response.audio_context);
                        }
                        catch (e) {
                            console.error('[SpeechlyClient] Error while invoking "onStart" callback:', e);
                        }
                    });
                    this.startCbs.length = 0;
                    break;
                case WebsocketResponseType.Stopped:
                    this.onResponseCb(response);
                    this.stopCbs.forEach(cb => {
                        try {
                            cb(undefined, response.audio_context);
                        }
                        catch (e) {
                            console.error('[SpeechlyClient] Error while invoking "onStop" callback:', e);
                        }
                    });
                    this.stopCbs.length = 0;
                    break;
                default:
                    this.onResponseCb(response);
            }
        };
        this.worker = new WorkerFactory();
        this.worker.addEventListener('message', this.onWebsocketMessage);
    }
    initialize(apiUrl, authToken, targetSampleRate, debug) {
        return __awaiter(this, void 0, void 0, function* () {
            this.worker.postMessage({
                type: ControllerSignal.connect,
                apiUrl,
                authToken,
                targetSampleRate,
                debug,
            });
            // Reset
            this.startCbs = [];
            this.stopCbs = [];
            return new Promise((resolve, reject) => {
                this.onInitResolve = () => {
                    this.onInitResolve = undefined;
                    this.onInitReject = undefined;
                    resolve();
                };
                this.onInitReject = (err) => {
                    this.onInitResolve = undefined;
                    this.onInitReject = undefined;
                    reject(err); // Will throw WebsocketError in `await initialize()`
                };
            });
        });
    }
    initAudioProcessor(sourceSampleRate, frameMillis, historyFrames, vadOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            this.worker.postMessage({
                type: ControllerSignal.initAudioProcessor,
                sourceSampleRate: sourceSampleRate,
                frameMillis: frameMillis,
                historyFrames: historyFrames,
                vadOptions: vadOptions,
            });
            return new Promise(resolve => {
                this.resolveSourceSampleRateSet = resolve;
            });
        });
    }
    /**
     * Control audio processor parameters
     * @param ap - Audio processor parameters to adjust
     */
    adjustAudioProcessor(ap) {
        this.worker.postMessage({
            type: ControllerSignal.adjustAudioProcessor,
            params: ap,
        });
    }
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                this.worker.postMessage({
                    type: ControllerSignal.CLOSE,
                    code: 1000,
                    message: 'Client has ended the session',
                });
                resolve();
            });
        });
    }
    startStream(streamOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            this.worker.postMessage({ type: ControllerSignal.startStream, streamOptions: streamOptions });
        });
    }
    stopStream() {
        return __awaiter(this, void 0, void 0, function* () {
            this.worker.postMessage({ type: ControllerSignal.stopStream });
        });
    }
    startContext(options) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                this.startCbs.push((err, id) => {
                    if (err !== undefined) {
                        reject(err);
                    }
                    else {
                        resolve(id);
                    }
                });
                this.worker.postMessage({ type: ControllerSignal.START_CONTEXT, options });
            });
        });
    }
    stopContext() {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                this.stopCbs.push((err, id) => {
                    if (err !== undefined) {
                        reject(err);
                    }
                    else {
                        resolve(id);
                    }
                });
                this.worker.postMessage({ type: ControllerSignal.STOP_CONTEXT });
            });
        });
    }
    switchContext(options) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => {
                this.startCbs.push((err, id) => {
                    if (err !== undefined) {
                        reject(err);
                    }
                    else {
                        resolve(id);
                    }
                });
                this.worker.postMessage({ type: ControllerSignal.SWITCH_CONTEXT, options });
            });
        });
    }
    postMessage(message) {
        this.worker.postMessage(message);
    }
    sendAudio(audioChunk) {
        this.worker.postMessage({ type: ControllerSignal.AUDIO, payload: audioChunk });
    }
    setContextOptions(options) {
        return __awaiter(this, void 0, void 0, function* () {
            this.worker.postMessage({ type: ControllerSignal.setContextOptions, options });
        });
    }
}

/**
 * @internal
 */
class LocalStorage {
    constructor() {
        this.storage = window.localStorage;
    }
    get(key) {
        const val = this.storage.getItem(key);
        return val;
    }
    set(key, val) {
        this.storage.setItem(key, val);
    }
    getOrSet(key, genFn) {
        let val = this.storage.getItem(key);
        if (val === null) {
            val = genFn();
            this.storage.setItem(key, val);
        }
        return val;
    }
}

function parseTentativeTranscript(data, timeOffset) {
    return data.words.map(({ word, index, start_timestamp, end_timestamp }) => {
        return {
            value: word,
            index: index,
            startTimestamp: start_timestamp + timeOffset,
            endTimestamp: end_timestamp + timeOffset,
            isFinal: false,
        };
    });
}
function parseTranscript(data, timeOffset) {
    return {
        value: data.word,
        index: data.index,
        startTimestamp: data.start_timestamp + timeOffset,
        endTimestamp: data.end_timestamp + timeOffset,
        isFinal: true,
    };
}
function parseTentativeEntities(data) {
    return data.entities.map(({ entity, value, start_position, end_position }) => {
        return {
            type: entity,
            value: value,
            startPosition: start_position,
            endPosition: end_position,
            isFinal: false,
        };
    });
}
function parseEntity(data) {
    return {
        type: data.entity,
        value: data.value,
        startPosition: data.start_position,
        endPosition: data.end_position,
        isFinal: true,
    };
}
function parseIntent(data, isFinal) {
    return {
        intent: data.intent,
        isFinal: isFinal,
    };
}

const deviceIdStorageKey = 'speechly-device-id';
const authTokenKey = 'speechly-auth-token';
/**
 * A client for Speechly Spoken Language Understanding (SLU) API. The client handles initializing the websocket
 * connection to Speechly API, sending control events and audio stream. It reads and dispatches the responses
 * through a high-level API for interacting with so-called speech segments.
 * @internal
 */
class CloudDecoder {
    constructor(options) {
        var _a, _b;
        this.streamOptions = StreamDefaultOptions;
        this.activeContexts = 0;
        this.audioContexts = new Map();
        this.maxReconnectAttemptCount = 10;
        this.connectAttempt = 0;
        this.connectPromise = null;
        this.cbs = [];
        this.state = DecoderState.Disconnected;
        this.handleWebsocketResponse = (response) => {
            var _a;
            if (this.debug) {
                console.log('[Decoder]', 'Received response', response);
            }
            switch (response.type) {
                case WorkerSignal.VadSignalHigh:
                    this.cbs.forEach(cb => cb.onVadStateChange.forEach(f => f(true)));
                    break;
                case WorkerSignal.VadSignalLow:
                    this.cbs.forEach(cb => cb.onVadStateChange.forEach(f => f(false)));
                    break;
                case WorkerSignal.RequestContextStart:
                    this.activeContexts++;
                    break;
                case WebsocketResponseType.Started: {
                    const params = response.params;
                    this.audioContexts.set(response.audio_context, {
                        segments: new Map(),
                        audioStartTimeMillis: (_a = params === null || params === void 0 ? void 0 : params.audioStartTimeMillis) !== null && _a !== void 0 ? _a : 0,
                    });
                    this.cbs.forEach(cb => cb.contextStartedCbs.forEach(f => f(response.audio_context)));
                    break;
                }
                case WebsocketResponseType.Stopped: {
                    this.activeContexts--;
                    this.cbs.forEach(cb => cb.contextStoppedCbs.forEach(f => f(response.audio_context)));
                    if (!this.streamOptions.preserveSegments) {
                        this.audioContexts.delete(response.audio_context);
                    }
                    // Signal stopStream listeners that the final results are in, it's ok to resolve the await
                    if (this.resolveStopStream !== undefined && this.activeContexts === 0) {
                        this.resolveStopStream();
                    }
                    break;
                }
                default:
                    this.handleSegmentUpdate(response);
                    break;
            }
        };
        this.handleSegmentUpdate = (response) => {
            var _a;
            const { audio_context, segment_id, type } = response;
            let { data } = response;
            const context = this.audioContexts.get(audio_context);
            if (context === undefined) {
                console.warn('[Decoder]', 'Received response for non-existent context', audio_context);
                return;
            }
            let segmentState = (_a = context.segments.get(segment_id)) !== null && _a !== void 0 ? _a : new SegmentState(audio_context, segment_id);
            switch (type) {
                case WebsocketResponseType.TentativeTranscript:
                    data = data;
                    const words = parseTentativeTranscript(data, context.audioStartTimeMillis);
                    const transcript = data.transcript;
                    this.cbs.forEach(cb => cb.tentativeTranscriptCbs.forEach(f => f(audio_context, segment_id, words, transcript)));
                    segmentState = segmentState.updateTranscript(words);
                    break;
                case WebsocketResponseType.Transcript:
                    data = data;
                    const word = parseTranscript(data, context.audioStartTimeMillis);
                    this.cbs.forEach(cb => cb.transcriptCbs.forEach(f => f(audio_context, segment_id, word)));
                    segmentState = segmentState.updateTranscript([word]);
                    break;
                case WebsocketResponseType.TentativeEntities:
                    data = data;
                    const entities = parseTentativeEntities(data);
                    this.cbs.forEach(cb => cb.tentativeEntityCbs.forEach(f => f(audio_context, segment_id, entities)));
                    segmentState = segmentState.updateEntities(entities);
                    break;
                case WebsocketResponseType.Entity:
                    data = data;
                    const entity = parseEntity(data);
                    this.cbs.forEach(cb => cb.entityCbs.forEach(f => f(audio_context, segment_id, entity)));
                    segmentState = segmentState.updateEntities([entity]);
                    break;
                case WebsocketResponseType.TentativeIntent:
                    data = data;
                    const tentativeIntent = parseIntent(data, false);
                    this.cbs.forEach(cb => cb.tentativeIntentCbs.forEach(f => f(audio_context, segment_id, tentativeIntent)));
                    segmentState = segmentState.updateIntent(tentativeIntent);
                    break;
                case WebsocketResponseType.Intent:
                    data = data;
                    const intent = parseIntent(data, true);
                    this.cbs.forEach(cb => cb.intentCbs.forEach(f => f(audio_context, segment_id, intent)));
                    segmentState = segmentState.updateIntent(intent);
                    break;
                case WebsocketResponseType.SegmentEnd:
                    segmentState = segmentState.finalize();
                    break;
                // TODO: handle unexpected response types.
            }
            // Update the segment in current context.
            context.segments.set(segment_id, segmentState);
            // Update current contexts.
            this.audioContexts.set(audio_context, context);
            // Log segment to console
            if (this.logSegments) {
                console.info(segmentState.toString());
            }
            // Fire segment change event.
            this.cbs.forEach(cb => cb.segmentChangeCbs.forEach(f => f(segmentState.toSegment())));
        };
        // eslint-disable-next-line @typescript-eslint/member-delimiter-style
        this.handleWebsocketClosure = (err) => {
            if (err.code === 1000) {
                if (this.debug) {
                    console.log('[Decoder]', 'Websocket closed', err);
                }
            }
            else {
                console.error('[Decoder]', 'Websocket closed due to error', err);
                // If for some reason deviceId is missing, there's nothing else we can do but fail completely.
                if (this.deviceId === undefined) {
                    this.setState(DecoderState.Failed);
                    console.error('[Decoder]', 'No deviceId. Giving up reconnecting.');
                    return;
                }
                this.setState(DecoderState.Disconnected);
                this.activeContexts = 0;
                this.audioContexts.clear();
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                this.reconnect();
            }
        };
        this.logSegments = options.logSegments;
        this.appId = options.appId;
        this.projectId = options.projectId;
        this.sampleRate = options.sampleRate;
        this.debug = options.debug;
        if (this.appId !== undefined && this.projectId !== undefined) {
            throw Error('[Decoder] You cannot use both appId and projectId at the same time');
        }
        else if (this.appId === undefined && this.projectId === undefined) {
            throw Error('[Decoder] Either an appId or a projectId is required');
        }
        const apiUrl = options.apiUrl;
        this.apiUrl = generateWsUrl(apiUrl.replace('http', 'ws') + '/ws/v1', this.sampleRate);
        this.loginUrl = `${apiUrl}/login`;
        // Attempt to access local storage for caching settings. Do without if it's blocked by security settings
        try {
            this.storage = (_a = options.storage) !== null && _a !== void 0 ? _a : new LocalStorage();
            this.deviceId = this.storage.getOrSet(deviceIdStorageKey, v4);
        }
        catch (err) {
            this.deviceId = v4();
        }
        this.apiClient = new WebWorkerController();
        this.apiClient.onResponse(this.handleWebsocketResponse);
        this.apiClient.onClose(this.handleWebsocketClosure);
        if ((_b = options.connect) !== null && _b !== void 0 ? _b : true) {
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            this.connect();
        }
    }
    getReconnectDelayMs(attempt) {
        return Math.pow(2, attempt) * 100;
    }
    sleep(ms) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise(resolve => setTimeout(resolve, ms));
        });
    }
    /**
     * Connect to Speechly backend.
     * This function will be called by initialize if not manually called earlier.
     * Calling connect() immediately after constructor and setting callbacks allows
     * prewarming the connection, resulting in less noticeable waits for the user.
     */
    connect() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.connectPromise === null) {
                this.connectPromise = (() => __awaiter(this, void 0, void 0, function* () {
                    var _a;
                    this.setState(DecoderState.Disconnected);
                    // Get auth token from cache or renew it
                    const storedToken = (_a = this.storage) === null || _a === void 0 ? void 0 : _a.get(authTokenKey);
                    if (!storedToken || !validateToken(storedToken, this.projectId, this.appId, this.deviceId)) {
                        try {
                            this.authToken = yield fetchToken(this.loginUrl, this.projectId, this.appId, this.deviceId, fetch);
                            // Cache the auth token in local storage for future use.
                            if (this.storage) {
                                this.storage.set(authTokenKey, this.authToken);
                            }
                        }
                        catch (err) {
                            this.connectPromise = null;
                            this.setState(DecoderState.Failed);
                            throw err;
                        }
                    }
                    else {
                        this.authToken = storedToken;
                    }
                    // Establish websocket connection
                    try {
                        yield this.apiClient.initialize(this.apiUrl, this.authToken, this.sampleRate, this.debug);
                    }
                    catch (err) {
                        this.connectPromise = null;
                        if (!(err instanceof WebsocketError && err.code === 1000)) {
                            this.setState(DecoderState.Failed);
                        }
                        throw err;
                    }
                    this.advanceState(DecoderState.Connected);
                }))();
            }
            yield this.connectPromise;
        });
    }
    /**
     * Control audio processor parameters
     * @param ap - Audio processor parameters to adjust
     */
    adjustAudioProcessor(ap) {
        this.apiClient.adjustAudioProcessor(ap);
    }
    /**
     * Closes the client by closing the API connection.
     */
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            let error;
            try {
                yield this.apiClient.close();
            }
            catch (err) {
                // @ts-ignore
                error = err.message;
            }
            this.audioContexts.clear();
            this.activeContexts = 0;
            this.connectPromise = null;
            this.setState(DecoderState.Disconnected);
            if (error !== undefined) {
                throw Error(error);
            }
        });
    }
    startStream(streamOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.debug) {
                console.log('[Decoder]', 'startStream');
            }
            this.streamOptions = streamOptions;
            this.audioContexts.clear();
            this.activeContexts = 0;
            yield this.apiClient.startStream(streamOptions);
        });
    }
    stopStream() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.debug) {
                console.log('[Decoder]', 'stopStream');
            }
            yield this.apiClient.stopStream();
            yield this.waitResults();
        });
    }
    waitResults() {
        return __awaiter(this, void 0, void 0, function* () {
            // Wait for active contexts to finish
            if (this.activeContexts > 0) {
                const p = new Promise(resolve => {
                    this.resolveStopStream = resolve;
                });
                yield p;
            }
            this.resolveStopStream = undefined;
        });
    }
    /**
     * Starts a new SLU context by sending a start context event to the API.
     */
    startContext(options) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.state === DecoderState.Failed) {
                throw Error('[Decoder] startContext cannot be run in Failed state.');
            }
            else if (this.state < DecoderState.Connected) {
                yield this.connect();
            }
            else if (this.state > DecoderState.Connected) {
                throw Error('[Decoder] Unable to complete startContext: Expected Connected state, but was in ' +
                    stateToString(this.state) +
                    '.');
            }
            this.setState(DecoderState.Active);
            let contextId;
            if (this.projectId != null) {
                if (options === null || options === void 0 ? void 0 : options.appId) {
                    contextId = yield this.apiClient.startContext(options);
                }
                else {
                    throw new Error('options.appId is required with project login');
                }
            }
            else {
                if ((options === null || options === void 0 ? void 0 : options.appId) != null && this.appId !== (options === null || options === void 0 ? void 0 : options.appId)) {
                    this.setState(DecoderState.Failed);
                    throw ErrAppIdChangeWithoutProjectLogin;
                }
                contextId = yield this.apiClient.startContext(options);
            }
            // Ensure state has not been changed by await apiClient.startContext() due to websocket errors.
            // Due to apiClient.startContext implementation, they don't throw an error here, but call handleWebsocketClosure instead which changes to DecoderState.Disconnected
            if (this.state < DecoderState.Active) {
                throw Error('[Decoder] Unable to complete startContext: Problem acquiring contextId');
            }
            return contextId;
        });
    }
    /**
     * Send audio array.
     */
    sendAudio(audio) {
        this.apiClient.sendAudio(audio);
    }
    /**
     * Stops current SLU context by sending a stop context event to the API and muting the microphone
     * delayed by contextStopDelay = 250 ms
     */
    stopContext(stopDelayMs) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.state === DecoderState.Failed) {
                throw Error('[Decoder] stopContext cannot be run in unrecovable error state.');
            }
            else if (this.state !== DecoderState.Active) {
                throw Error('[Decoder] Unable to complete stopContext: Expected Active state, but was in ' +
                    stateToString(this.state) +
                    '.');
            }
            if (stopDelayMs > 0) {
                yield this.sleep(stopDelayMs);
            }
            const contextId = yield this.apiClient.stopContext();
            this.setState(DecoderState.Connected);
            return contextId;
        });
    }
    /**
     * Stops current context and immediately starts a new SLU context
     * by sending a start context event to the API and unmuting the microphone.
     * @param options - any custom options for the audio processing.
     */
    switchContext(options) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.state !== DecoderState.Active) {
                throw Error('[Decoder] Unable to complete switchContext: Expected Active state, but was in ' +
                    stateToString(this.state) +
                    '.');
            }
            const contextId = yield this.apiClient.switchContext(options);
            return contextId;
        });
    }
    registerListener(listener) {
        this.cbs.push(listener);
    }
    initAudioProcessor(sourceSampleRate, frameMillis, historyFrames, vadOptions) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.apiClient.initAudioProcessor(sourceSampleRate, frameMillis, historyFrames, vadOptions);
        });
    }
    useSharedArrayBuffers(controlSAB, dataSAB) {
        this.apiClient.postMessage({
            type: 'SET_SHARED_ARRAY_BUFFERS',
            controlSAB,
            dataSAB,
        });
    }
    setContextOptions(options) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.apiClient.setContextOptions(options);
        });
    }
    reconnect() {
        return __awaiter(this, void 0, void 0, function* () {
            console.log('Speechly reconnecting');
            this.connectPromise = null;
            if (this.connectAttempt < this.maxReconnectAttemptCount) {
                yield this.sleep(this.getReconnectDelayMs(this.connectAttempt++));
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                yield this.connect();
            }
            else {
                console.error('[Decoder] Maximum reconnect count reached, giving up automatic reconnect.');
            }
        });
    }
    advanceState(newState) {
        if (this.state >= newState) {
            return;
        }
        this.setState(newState);
    }
    setState(newState) {
        if (this.state === newState) {
            return;
        }
        if (this.debug) {
            console.log('[Decoder]', stateToString(this.state), '->', stateToString(newState));
        }
        this.state = newState;
        this.cbs.forEach(cb => { var _a; return (_a = cb.stateChangeCbs) === null || _a === void 0 ? void 0 : _a.forEach(f => f(newState)); });
    }
    /**
     * @returns Array of Segments since last startStream if preserveSegment options was used
     */
    getSegments() {
        const result = [];
        this.audioContexts.forEach((audioContext, _) => {
            audioContext.segments.forEach((segment, _) => {
                const deepCopy = JSON.parse(JSON.stringify(segment));
                result.push(deepCopy);
            });
        });
        return result;
    }
}
function generateWsUrl(baseUrl, sampleRate) {
    const params = new URLSearchParams();
    params.append('sampleRate', sampleRate.toString());
    return `${baseUrl}?${params.toString()}`;
}

var audioworklet = `
// Indices for the Control SAB.
const CONTROL = {
  'WRITE_INDEX': 0,
  'FRAMES_AVAILABLE': 1,
  'LOCK': 2,
};

class SpeechlyProcessor extends AudioWorkletProcessor {
  constructor() {
    super();

    this._initialized = false;
    this.debug = false;
    this.port.onmessage = this._initialize.bind(this);
  }

  _initialize(event) {
    this.controlSAB = new Int32Array(event.data.controlSAB);
    this.dataSAB = new Float32Array(event.data.dataSAB);
    this.debug = event.data.debug;
    if (this.debug) {
      console.log('[BrowserClient AudioWorkletNode]', 'initializing audioworklet');
    }
    this.sharedBufferSize = this.dataSAB.length;
    this.buffer = new Float32Array(0);
    this._initialized = true;
  }

  _transferDataToSharedBuffer(data) {
    this.controlSAB[CONTROL.LOCK] = 1;
    let inputWriteIndex = this.controlSAB[CONTROL.WRITE_INDEX];
    if (this.controlSAB[CONTROL.FRAMES_AVAILABLE] > 0) {
      if (inputWriteIndex + data.length > this.sharedBufferSize) {
        // console.log('buffer overflow')
        inputWriteIndex = 0;
      }
    }
    this.dataSAB.set(data, inputWriteIndex);
    this.controlSAB[CONTROL.WRITE_INDEX] = inputWriteIndex + data.length;
    this.controlSAB[CONTROL.FRAMES_AVAILABLE] = inputWriteIndex + data.length;
    this.controlSAB[CONTROL.LOCK] = 0;
  }

  _pushData(data) {
    if (this.debug) {
      const signalEnergy = getStandardDeviation(data)
      this.port.postMessage({
        type: 'STATS',
        signalEnergy: signalEnergy,
        samples: data.length,
      });
    }

    if (this.buffer.length > this.sharedBufferSize) {
      const dataToTransfer = this.buffer.subarray(0, this.sharedBufferSize);
      this._transferDataToSharedBuffer(dataToTransfer);
      this.buffer = this.buffer.subarray(this.sharedBufferSize);
    }
    let concat = new Float32Array(this.buffer.length + data.length);
    concat.set(this.buffer);
    concat.set(data, this.buffer.length);
    this.buffer = concat;
  }

  process(inputs, outputs, parameters) {
    const inputChannelData = inputs[0][0];
    if (inputChannelData !== undefined) {
      if (this.controlSAB && this.dataSAB) {
        this._pushData(inputChannelData);
      } else {
        this.port.postMessage({
          type: 'DATA',
          frames: inputChannelData
        });
      }
    }

    return true;
  }
}

function getStandardDeviation(array) {
  const n = array.length
  const mean = array.reduce((a, b) => a + b) / n
  return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n)
}

registerProcessor('speechly-worklet', SpeechlyProcessor);
`;

/**
 * Speechly BrowserClient streams audio containing speech for cloud processing and
 * provides the results of automatic speech recogition (ASR) and natural langugage understanding (NLU) via callbacks.
 *
 * Usage:
 *
 * - Create a BrowserClient instance with {@link DecoderOptions} containing a valid appId from https://api.speechly.com/dashboard.
 * - Create and initialize a {@link BrowserMicrophone} instance and {@link attach} its mediaStream to the BrowserClient instance.
 * - Control processing manually with {@link start} and {@link stop}. Alternatively, you can enable automatic control by initializing BrowserClient with {@link VadOptions} defined in the {@link DecoderOptions}.
 * - Read the ASR/NLU result {@link Segment} by providing a custom handler for the {@link onSegmentChange} callback.
 *
 * Alternatively, you can create the BrowserClient instance and process audio files (binary data) using {@link uploadAudioData}.
 * @public
 */
class BrowserClient {
    /**
     * Create a new BrowserClient instance.
     *
     * @param customOptions - any custom options for BrowserClient and the enclosed CloudDecoder.
     */
    constructor(customOptions) {
        var _a, _b;
        this.contextStopDelay = 250;
        this.debug = false;
        this.initialized = false;
        this.audioProcessorInitialized = false;
        this.isStreaming = false;
        this.active = false;
        this.listeningPromise = null;
        this.streamOptions = Object.assign({}, StreamDefaultOptions);
        this.stats = {
            maxSignalEnergy: 0.0,
            sentSamples: 0,
        };
        this.decoderOptions = Object.assign(Object.assign(Object.assign({}, DecoderDefaultOptions), customOptions), { vad: customOptions.vad ? Object.assign(Object.assign({}, VadDefaultOptions), customOptions.vad) : undefined });
        const constraints = window.navigator.mediaDevices.getSupportedConstraints();
        this.nativeResamplingSupported = constraints.sampleRate === true;
        this.isMobileSafari = iOS();
        // @ts-ignore
        this.isSafari = this.isMobileSafari || window.safari !== undefined;
        this.useSAB = !this.isSafari;
        this.debug = (_a = this.decoderOptions.debug) !== null && _a !== void 0 ? _a : true;
        this.callbacks = new EventCallbacks();
        this.callbacks.stateChangeCbs.addEventListener(this.handleStateChange.bind(this));
        this.callbacks.onVadStateChange.addEventListener(this.autoControlListening.bind(this));
        this.decoder = (_b = this.decoderOptions.decoder) !== null && _b !== void 0 ? _b : new CloudDecoder(this.decoderOptions);
        this.decoder.registerListener(this.callbacks);
    }
    /**
     * Connect to cloud, create an AudioContext for receiving audio samples from a MediaStream
     * and initialize a worker for audio processing and bi-directional streaming to the cloud.
     */
    initialize(options) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            if (this.initialized) {
                return;
            }
            if (this.debug) {
                console.log('[BrowserClient]', 'initializing');
            }
            this.initialized = true;
            try {
                yield this.decoder.connect();
            }
            catch (err) {
                this.initialized = false;
                if (err instanceof WebsocketError) {
                    if (err.code === 1000) {
                        if (this.debug) {
                            console.log('[BrowserClient]', 'Early close of websocket.');
                        }
                        return;
                    }
                    throw Error(`Unable to connect. Most likely there is no connection to network. Websocket error code: ${err.code}`);
                }
                else {
                    throw err;
                }
            }
            try {
                const opts = {};
                if (this.nativeResamplingSupported) {
                    opts.sampleRate = DefaultSampleRate;
                }
                if (window.webkitAudioContext !== undefined) {
                    // create webkit flavor of audiocontext
                    try {
                        // eslint-disable-next-line new-cap
                        this.audioContext = new window.webkitAudioContext(opts);
                    }
                    catch (err) {
                        if (this.debug) {
                            console.log('[BrowserClient]', 'creating audioContext without samplerate conversion', err);
                        }
                        // older webkit without constructor options
                        // eslint-disable-next-line new-cap
                        this.audioContext = new window.webkitAudioContext();
                    }
                }
                else {
                    this.audioContext = new window.AudioContext(opts);
                    // Start audio context if we are dealing with a WebKit browser.
                    //
                    // WebKit browsers (e.g. Safari) require to resume the context first,
                    // before obtaining user media by calling `mediaDevices.getUserMedia`.
                    //
                    // If done in a different order, the audio context will resume successfully,
                    // but will emit empty audio buffers.
                    if (window.webkitAudioContext !== undefined) {
                        yield this.audioContext.resume();
                    }
                }
            }
            catch (_c) {
                this.initialized = false;
                throw ErrDeviceNotSupported;
            }
            if (!this.isSafari && window.AudioWorkletNode !== undefined) {
                if (this.debug) {
                    console.log('[BrowserClient]', 'using AudioWorkletNode');
                }
                const blob = new Blob([audioworklet], { type: 'text/javascript' });
                const blobURL = window.URL.createObjectURL(blob);
                yield this.audioContext.audioWorklet.addModule(blobURL);
                this.speechlyNode = new AudioWorkletNode(this.audioContext, 'speechly-worklet');
                this.speechlyNode.connect(this.audioContext.destination);
                // @ts-ignore
                if (this.useSAB && window.SharedArrayBuffer !== undefined) {
                    // Chrome, Edge, Firefox, Firefox Android
                    if (this.debug) {
                        console.log('[BrowserClient]', 'using SharedArrayBuffer');
                    }
                    // @ts-ignore
                    const controlSAB = new window.SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
                    // @ts-ignore
                    const dataSAB = new window.SharedArrayBuffer(1024 * Float32Array.BYTES_PER_ELEMENT);
                    this.decoder.useSharedArrayBuffers(controlSAB, dataSAB);
                    this.speechlyNode.port.postMessage({
                        type: 'SET_SHARED_ARRAY_BUFFERS',
                        controlSAB,
                        dataSAB,
                        debug: this.debug,
                    });
                }
                else {
                    // Safari, Opera, Chrome Android, Webview Android
                    // or if site CORS headers do not allow SharedArrayBuffer
                    if (this.debug) {
                        console.log('[BrowserClient]', 'can not use SharedArrayBuffer');
                    }
                }
                this.speechlyNode.port.onmessage = (event) => {
                    switch (event.data.type) {
                        case 'STATS':
                            if (event.data.signalEnergy > this.stats.maxSignalEnergy) {
                                this.stats.maxSignalEnergy = event.data.signalEnergy;
                            }
                            this.stats.sentSamples += parseInt(event.data.samples);
                            break;
                        case 'DATA':
                            // this is not called if SAB is used, the buffers are sent immediately
                            this.handleAudio(event.data.frames);
                            break;
                    }
                };
            }
            else {
                if (this.debug) {
                    console.log('[BrowserClient]', 'using ScriptProcessorNode');
                }
                if (window.webkitAudioContext !== undefined) {
                    // Multiply base buffer size of 4 kB by the resample ratio rounded up to the next power of 2.
                    // i.e. for 48 kHz to 16 kHz downsampling, this will be 4096 (base) * 4 = 16384.
                    const resampleRatio = this.audioContext.sampleRate / DefaultSampleRate;
                    const bufSize = 4096 * Math.pow(2, Math.ceil(Math.log(resampleRatio) / Math.log(2)));
                    this.audioProcessor = this.audioContext.createScriptProcessor(bufSize, 1, 1);
                }
                else {
                    this.audioProcessor = this.audioContext.createScriptProcessor(undefined, 1, 1);
                }
                this.audioProcessor.connect(this.audioContext.destination);
                this.audioProcessor.addEventListener('audioprocess', (event) => {
                    this.handleAudio(event.inputBuffer.getChannelData(0));
                });
            }
            if (this.debug) {
                console.log('[BrowserClient]', 'audioContext sampleRate is', (_a = this.audioContext) === null || _a === void 0 ? void 0 : _a.sampleRate);
            }
            this.streamOptions.sampleRate = (_b = this.audioContext) === null || _b === void 0 ? void 0 : _b.sampleRate;
            yield this.decoder.initAudioProcessor(this.streamOptions.sampleRate, this.decoderOptions.frameMillis, this.decoderOptions.historyFrames, this.decoderOptions.vad);
            this.audioProcessorInitialized = true;
            if (options === null || options === void 0 ? void 0 : options.mediaStream) {
                yield this.attach(options === null || options === void 0 ? void 0 : options.mediaStream);
            }
        });
    }
    /**
     * Attach a MediaStream to the client, enabling the client to send the audio to the
     * Speechly API for processing. The processing is activated by calling
     * {@link BrowserClient.start} and deactivated by calling {@link BrowserClient.stop}.
     */
    attach(mediaStream) {
        var _a, _b, _c, _d, _e, _f;
        return __awaiter(this, void 0, void 0, function* () {
            yield this.initialize();
            yield this.detach();
            this.stream = (_a = this.audioContext) === null || _a === void 0 ? void 0 : _a.createMediaStreamSource(mediaStream);
            // ensure audioContext is active
            if (((_b = this.audioContext) === null || _b === void 0 ? void 0 : _b.state) !== 'running') {
                if (this.debug) {
                    console.log('[BrowserClient]', 'audioContext resume required, state is', (_c = this.audioContext) === null || _c === void 0 ? void 0 : _c.state);
                }
                yield ((_d = this.audioContext) === null || _d === void 0 ? void 0 : _d.resume());
            }
            if (this.speechlyNode) {
                (_e = this.stream) === null || _e === void 0 ? void 0 : _e.connect(this.speechlyNode);
            }
            else if (this.audioProcessor) {
                (_f = this.stream) === null || _f === void 0 ? void 0 : _f.connect(this.audioProcessor);
            }
            else {
                throw Error('[BrowserClient] cannot attach to mediaStream, not initialized');
            }
            yield this.autoControlStream();
        });
    }
    /**
     * @returns Whether the client is processing audio at the moment.
     */
    isActive() {
        return this.active;
    }
    /**
     * Starts a new audio context, returning it's id to use for matching received responses.
     * If an active context already exists, an error is thrown.
     *
     * @param options - any custom options for the audio processing.
     * @returns The contextId of the active audio context
     */
    start(options) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.active) {
                throw ErrAlreadyStarted;
            }
            this.active = true;
            const contextId = yield this.queueTask(() => __awaiter(this, void 0, void 0, function* () {
                yield this.initialize();
                if (!this.isStreaming) {
                    // Automatically control streaming for backwards compability
                    yield this.startStream({ autoStarted: true });
                }
                const contextId = yield this.decoder.startContext(options);
                return contextId;
            }));
            return contextId;
        });
    }
    /**
     * Stops the current audio context and deactivates the audio processing pipeline.
     * If there is no active audio context, a warning is logged to console.
     */
    stop(stopDelayMs = this.contextStopDelay) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.active) {
                throw ErrAlreadyStopped;
            }
            this.active = false;
            const contextId = yield this.queueTask(() => __awaiter(this, void 0, void 0, function* () {
                var _a;
                try {
                    const contextId = yield this.decoder.stopContext(stopDelayMs);
                    // Automatically control streaming for backwards compability
                    if (!((_a = this.decoderOptions.vad) === null || _a === void 0 ? void 0 : _a.enabled) && this.isStreaming && this.streamOptions.autoStarted) {
                        yield this.stopStream();
                    }
                    if (this.stats.sentSamples === 0) {
                        console.warn('[BrowserClient]', 'audioContext contained no audio data');
                    }
                    return contextId;
                }
                catch (err) {
                    console.warn('[BrowserClient]', 'stop() failed', err);
                }
                finally {
                    this.stats.sentSamples = 0;
                }
            }));
            return contextId;
        });
    }
    /**
     * Sets the default context options (appId, inference parameters, timezone). New audio contexts
     * use these options until new options are provided. Decoder's functions startContext() can
     * also override the options per function call.
     */
    setContextOptions(options) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.decoder.setContextOptions(options);
        });
    }
    /**
     * Control audio processor parameters like VAD
     * @param ap - Audio processor parameters to adjust
     */
    adjustAudioProcessor(ap) {
        var _a;
        if (ap.vad) {
            if (this.decoderOptions.vad) {
                this.decoderOptions.vad = Object.assign(Object.assign({}, this.decoderOptions.vad), ap.vad);
            }
            else {
                throw Error('Unable to adjust VAD - it was not defined in the constructor');
            }
        }
        this.decoder.adjustAudioProcessor(ap);
        if ((_a = this.decoderOptions.vad) === null || _a === void 0 ? void 0 : _a.enabled) {
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            this.autoControlStream();
        }
        else {
            if (this.active) {
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                this.stop();
            }
        }
    }
    /**
     * Upload an audio binary (like .wav) to the cloud for automatic speech recogition (ASR) and natural langugage understanding (NLU).
     * Callbacks are fired as the processing advances.
     *
     * @param audioData - audio data in a binary format. Will be decoded.
     * @param options - any custom options for the audio processing.
     * @returns array of segments containing the final results of speech recognition (ASR and NLU).
     */
    uploadAudioData(audioData, options) {
        var _a, _b, _c;
        return __awaiter(this, void 0, void 0, function* () {
            yield this.initialize();
            const audioBuffer = yield ((_a = this.audioContext) === null || _a === void 0 ? void 0 : _a.decodeAudioData(audioData));
            if (audioBuffer === undefined) {
                throw Error('Could not decode audioData');
            }
            const samples = audioBuffer.getChannelData(0);
            // convert 2-channel audio to 1-channel if need be
            if (audioBuffer.numberOfChannels > 1) {
                const chan1samples = audioBuffer.getChannelData(1);
                for (let i = 0; i < samples.length; i++) {
                    samples[i] = (samples[i] + chan1samples[i]) / 2.0;
                }
            }
            if (this.active)
                yield this.stop(0);
            if (this.isStreaming)
                yield this.stopStream();
            yield this.startStream({
                sampleRate: audioBuffer.sampleRate,
                preserveSegments: true,
                immediate: true,
            });
            const vadActive = ((_b = this.decoderOptions.vad) === null || _b === void 0 ? void 0 : _b.enabled) && ((_c = this.decoderOptions.vad) === null || _c === void 0 ? void 0 : _c.controlListening);
            const chunkMillis = 1000;
            let throttlingWaitMillis = 0;
            if (!vadActive) {
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                yield this.start(options);
            }
            else {
                if (options)
                    yield this.setContextOptions(options);
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                if (this.decoderOptions.vad.signalSustainMillis >= chunkMillis) {
                    const allowedContexts = 10;
                    const lookbackWindowMillis = 10000;
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    const worstCaseContextsInLookback = lookbackWindowMillis / this.decoderOptions.vad.signalSustainMillis;
                    const maxSpeedUp = allowedContexts / worstCaseContextsInLookback;
                    throttlingWaitMillis = chunkMillis / maxSpeedUp;
                }
                else {
                    console.warn(`Throttling disabled due to low (<= ${chunkMillis}) VAD sustain value. Server may disconnect while processing if contexts are created at high rate.`);
                }
                throttlingWaitMillis = 0; // @DEBUG
            }
            let sendBuffer;
            const chunkSamples = Math.round(audioBuffer.sampleRate * chunkMillis / 1000);
            for (let b = 0; b < samples.length; b += chunkSamples) {
                const e = b + chunkSamples;
                if (e > samples.length) {
                    sendBuffer = samples.slice(b);
                }
                else {
                    sendBuffer = samples.slice(b, e);
                }
                this.handleAudio(sendBuffer);
                yield this.sleep(throttlingWaitMillis);
            }
            if (!vadActive) {
                yield this.stop(0);
            }
            yield this.stopStream();
            // Store result before startStream as it'll clear the results
            const result = this.decoder.getSegments();
            return result;
        });
    }
    /**
     * `startStream` is used to indicate start of continuous audio stream.
     * It resets the stream sample counters and history.
     * BrowserClient internally calls `startStream` upon `initialize` and `start` so it's not needed unless you've manually called `stopStream` and want to resume audio processing afterwards.
     * @param streamOptionOverrides - options for stream processing
     */
    startStream(streamOptionOverrides) {
        return __awaiter(this, void 0, void 0, function* () {
            this.streamOptions = Object.assign(Object.assign(Object.assign({}, this.streamOptions), { autoStarted: false }), streamOptionOverrides);
            yield this.decoder.startStream(this.streamOptions);
            this.isStreaming = true;
        });
    }
    /**
     * `stopStream` is used to indicate end of continuous audio stream.
     * It ensures that all of the internal audio buffers are flushed for processing.
     * BrowserClient internally calls `stopStream` upon `stop` so it's not needed unless then source audio stream is no longer available or you manually want to pause audio processing.
     * Use `startStream` to resume audio processing afterwards.
     */
    stopStream() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.isStreaming) {
                this.isStreaming = false;
                yield this.decoder.stopStream();
            }
        });
    }
    queueTask(task) {
        return __awaiter(this, void 0, void 0, function* () {
            const prevTask = this.listeningPromise;
            this.listeningPromise = (() => __awaiter(this, void 0, void 0, function* () {
                yield prevTask;
                return task();
            }))();
            return this.listeningPromise;
        });
    }
    autoControlListening(vadActive) {
        var _a;
        if (this.debug) {
            console.log('[BrowserClient]', 'autoControlListening', vadActive);
        }
        if ((_a = this.decoderOptions.vad) === null || _a === void 0 ? void 0 : _a.controlListening) {
            if (vadActive) {
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                if (!this.active)
                    this.start();
            }
            else {
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                if (this.active)
                    this.stop(0);
            }
        }
    }
    autoControlStream() {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.audioProcessorInitialized)
                return;
            if (!this.stream)
                return;
            // Auto-start stream if VAD is enabled
            if (((_a = this.decoderOptions.vad) === null || _a === void 0 ? void 0 : _a.enabled) && !this.isStreaming) {
                yield this.startStream({ autoStarted: true });
                return;
            }
            // Auto-stop stream if automatically started
            if (!((_b = this.decoderOptions.vad) === null || _b === void 0 ? void 0 : _b.enabled) && this.isStreaming && this.streamOptions.autoStarted) {
                yield this.stopStream();
            }
        });
    }
    handleStateChange(decoderState) {
        switch (decoderState) {
            case DecoderState.Disconnected:
            case DecoderState.Failed:
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                this.stopStream();
                this.active = false;
                this.listeningPromise = null;
                break;
        }
    }
    /**
     * Detach or disconnect the client from the audio source.
     */
    detach() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.active) {
                yield this.stop(0);
            }
            if (this.stream) {
                this.stream.disconnect();
                this.stream = undefined;
            }
        });
    }
    /**
     * Closes the client, detaching from any audio source and disconnecting any audio
     * processors.
     */
    close() {
        var _a, _b, _c;
        return __awaiter(this, void 0, void 0, function* () {
            if (this.debug) {
                console.log('[BrowserClient]', 'close');
            }
            yield this.detach();
            if (this.speechlyNode !== null) {
                (_a = this.speechlyNode) === null || _a === void 0 ? void 0 : _a.port.close();
                (_b = this.speechlyNode) === null || _b === void 0 ? void 0 : _b.disconnect();
            }
            // Disconnect and stop ScriptProcessorNode
            if (this.audioProcessor !== undefined) {
                (_c = this.audioProcessor) === null || _c === void 0 ? void 0 : _c.disconnect();
            }
            yield this.decoder.close();
            this.initialized = false;
            this.listeningPromise = null;
        });
    }
    sleep(ms) {
        return __awaiter(this, void 0, void 0, function* () {
            return new Promise(resolve => setTimeout(resolve, ms));
        });
    }
    handleAudio(array) {
        if (this.isStreaming) {
            this.stats.sentSamples += array.length;
            this.decoder.sendAudio(array);
        }
    }
    /**
     * Adds a listener for start events
     * @param cb - the callback to invoke on context start
     */
    onStart(cb) {
        this.callbacks.contextStartedCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for stop events
     * @param cb - the callback to invoke on context stop
     */
    onStop(cb) {
        this.callbacks.contextStoppedCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for current segment change events.
     * @param cb - the callback to invoke on segment change events.
     */
    onSegmentChange(cb) {
        this.callbacks.segmentChangeCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for transcript responses from the API.
     * @param cb - the callback to invoke on a transcript response.
     */
    onTranscript(cb) {
        this.callbacks.transcriptCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for entity responses from the API.
     * @param cb - the callback to invoke on an entity response.
     */
    onEntity(cb) {
        this.callbacks.entityCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for intent responses from the API.
     * @param cb - the callback to invoke on an intent response.
     */
    onIntent(cb) {
        this.callbacks.intentCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for tentative transcript responses from the API.
     * @param cb - the callback to invoke on a tentative transcript response.
     */
    onTentativeTranscript(cb) {
        this.callbacks.tentativeTranscriptCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for tentative entities responses from the API.
     * @param cb - the callback to invoke on a tentative entities response.
     */
    onTentativeEntities(cb) {
        this.callbacks.tentativeEntityCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for tentative intent responses from the API.
     * @param cb - the callback to invoke on a tentative intent response.
     */
    onTentativeIntent(cb) {
        this.callbacks.tentativeIntentCbs.addEventListener(cb);
    }
    /**
     * Adds a listener for the state changes of the client.
     * @param cb - the callback to invoke on a client state change.
     */
    onStateChange(cb) {
        this.callbacks.stateChangeCbs.addEventListener(cb);
    }
}
function iOS() {
    const iosPlatforms = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'];
    return (iosPlatforms.indexOf(navigator.platform) >= 0 ||
        // iPad on iOS 13 detection
        (navigator.userAgent.includes('Mac') && 'ontouchend' in document));
}

export { AudioSourceState, BrowserClient, BrowserMicrophone, CloudDecoder, ControllerSignal, DecoderDefaultOptions, DecoderState, DefaultSampleRate, ErrAlreadyInitialized, ErrAlreadyStarted, ErrAlreadyStopped, ErrAppIdChangeWithoutProjectLogin, ErrDeviceNotSupported, ErrKeyNotFound, ErrNoAudioConsent, ErrNoStorageSupport, ErrNotInitialized, EventCallbacks, ListenerArray, SegmentState, StreamDefaultOptions, VadDefaultOptions, WebsocketError, WebsocketResponseType, WorkerSignal, stateToString };
//# sourceMappingURL=speechly.es.js.map