text: /**
   * Copyright (c) Facebook, Inc. and its affiliates.
   *
   * This source code is licensed under the MIT license found in the
   * LICENSE file in the root directory of this source tree.
   *
   * @flow
   * @format
   */
  
  'use strict';
  
  const ErrorUtils = require('../vendor/core/ErrorUtils');
  const Systrace = require('../Performance/Systrace');
  
  const deepFreezeAndThrowOnMutationInDev = require('../Utilities/deepFreezeAndThrowOnMutationInDev');
  const invariant = require('invariant');
  const stringifySafe = require('../Utilities/stringifySafe');
  const warnOnce = require('../Utilities/warnOnce');
  
  export type SpyData = {
    type: number,
    module: ?string,
    method: string | number,
    args: any[],
  };
  
  const TO_JS = 0;
  const TO_NATIVE = 1;
  
  const MODULE_IDS = 0;
  const METHOD_IDS = 1;
  const PARAMS = 2;
  const MIN_TIME_BETWEEN_FLUSHES_MS = 5;
  
  // eslint-disable-next-line no-bitwise
  const TRACE_TAG_REACT_APPS = 1 << 17;
  
  const DEBUG_INFO_LIMIT = 32;
  
  class MessageQueue {
    _lazyCallableModules: {[key: string]: (void) => Object};
    _queue: [number[], number[], any[], number];
    _successCallbacks: Map<number, ?Function>;
    _failureCallbacks: Map<number, ?Function>;
    _callID: number;
    _lastFlush: number;
    _eventLoopStartTime: number;
    _immediatesCallback: ?() => void;
  
    _debugInfo: {[number]: [number, number]};
    _remoteModuleTable: {[number]: string};
    _remoteMethodTable: {[number]: $ReadOnlyArray<string>};
  
    __spy: ?(data: SpyData) => void;
  
    constructor() {
      this._lazyCallableModules = {};
      this._queue = [[], [], [], 0];
      this._successCallbacks = new Map();
      this._failureCallbacks = new Map();
      this._callID = 0;
      this._lastFlush = 0;
      this._eventLoopStartTime = Date.now();
      this._immediatesCallback = null;
  
      if (__DEV__) {
        this._debugInfo = {};
        this._remoteModuleTable = {};
        this._remoteMethodTable = {};
      }
  
      (this: any).callFunctionReturnFlushedQueue = this.callFunctionReturnFlushedQueue.bind(
        this,
      );
      (this: any).callFunctionReturnResultAndFlushedQueue = this.callFunctionReturnResultAndFlushedQueue.bind(
        this,
      );
      (this: any).flushedQueue = this.flushedQueue.bind(this);
      (this: any).invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind(
        this,
      );
    }
  
    /**
     * Public APIs
     */
  
    static spy(spyOrToggle: boolean | ((data: SpyData) => void)) {
      if (spyOrToggle === true) {
        MessageQueue.prototype.__spy = info => {
          console.log(
            `${info.type === TO_JS ? 'N->JS' : 'JS->N'} : ` +
              `${info.module ? info.module + '.' : ''}${info.method}` +
              `(${JSON.stringify(info.args)})`,
          );
        };
      } else if (spyOrToggle === false) {
        MessageQueue.prototype.__spy = null;
      } else {
        MessageQueue.prototype.__spy = spyOrToggle;
      }
    }
  
    callFunctionReturnFlushedQueue(
      module: string,
      method: string,
      args: any[],
    ): null | [Array<number>, Array<number>, Array<any>, number] {
      this.__guard(() => {
        this.__callFunction(module, method, args);
      });
  
      return this.flushedQueue();
    }
  
    callFunctionReturnResultAndFlushedQueue(
      module: string,
      method: string,
      args: any[],
    ): $TEMPORARY$array<?[Array<number>, Array<number>, Array<any>, number]> {
      let result;
      this.__guard(() => {
        result = this.__callFunction(module, method, args);
      });
  
      return [result, this.flushedQueue()];
    }
  
    invokeCallbackAndReturnFlushedQueue(
      cbID: number,
      args: any[],
    ): null | [Array<number>, Array<number>, Array<any>, number] {
      this.__guard(() => {
        this.__invokeCallback(cbID, args);
      });
  
      return this.flushedQueue();
    }
  
    flushedQueue(): null | [Array<number>, Array<number>, Array<any>, number] {
      this.__guard(() => {
        this.__callImmediates();
      });
  
      const queue = this._queue;
      this._queue = [[], [], [], this._callID];
      return queue[0].length ? queue : null;
    }
  
    getEventLoopRunningTime(): number {
      return Date.now() - this._eventLoopStartTime;
    }
  
    registerCallableModule(name: string, module: Object) {
      this._lazyCallableModules[name] = () => module;
    }
  
    registerLazyCallableModule(name: string, factory: void => Object) {
      let module: Object;
      let getValue: ?(void) => Object = factory;
      this._lazyCallableModules[name] = () => {
        if (getValue) {
          module = getValue();
          getValue = null;
        }
        return module;
      };
    }
  
    getCallableModule(name: string): any | null {
      const getValue = this._lazyCallableModules[name];
      return getValue ? getValue() : null;
    }
  
    callNativeSyncHook(
      moduleID: number,
      methodID: number,
      params: any[],
      onFail: ?Function,
      onSucc: ?Function,
    ): any {
      if (__DEV__) {
        invariant(
          global.nativeCallSyncHook,
          'Calling synchronous methods on native ' +
            'modules is not supported in Chrome.\n\n Consider providing alternative ' +
            'methods to expose this method in debug mode, e.g. by exposing constants ' +
            'ahead-of-time.',
        );
      }
      this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
      try {
        return global.nativeCallSyncHook(moduleID, methodID, params);
      } catch (e) {
        if (
          typeof e === 'object' &&
          e != null &&
          typeof e.framesToPop === 'undefined' &&
          /^Exception in HostFunction: /.test(e.message)
        ) {
          e.framesToPop = 2;
        }
        throw e;
      }
    }
  
    processCallbacks(
      moduleID: number,
      methodID: number,
      params: any[],
      onFail: ?Function,
      onSucc: ?Function,
    ) {
      if (onFail || onSucc) {
        if (__DEV__) {
          this._debugInfo[this._callID] = [moduleID, methodID];
          if (this._callID > DEBUG_INFO_LIMIT) {
            delete this._debugInfo[this._callID - DEBUG_INFO_LIMIT];
          }
          if (this._successCallbacks.size > 500) {
            const info = {};
            this._successCallbacks.forEach((_, callID) => {
              const debug = this._debugInfo[callID];
              const module = debug && this._remoteModuleTable[debug[0]];
              const method = debug && this._remoteMethodTable[debug[0]][debug[1]];
              info[callID] = {module, method};
            });
            warnOnce(
              'excessive-number-of-pending-callbacks',
              `Please report: Excessive number of pending callbacks: ${
                this._successCallbacks.size
              }. Some pending callbacks that might have leaked by never being called from native code: ${stringifySafe(
                info,
              )}`,
            );
          }
        }
        // Encode callIDs into pairs of callback identifiers by shifting left and using the rightmost bit
        // to indicate fail (0) or success (1)
        // eslint-disable-next-line no-bitwise
        onFail && params.push(this._callID << 1);
        // eslint-disable-next-line no-bitwise
        onSucc && params.push((this._callID << 1) | 1);
        this._successCallbacks.set(this._callID, onSucc);
        this._failureCallbacks.set(this._callID, onFail);
      }
      if (__DEV__) {
        global.nativeTraceBeginAsyncFlow &&
          global.nativeTraceBeginAsyncFlow(
            TRACE_TAG_REACT_APPS,
            'native',
            this._callID,
          );
      }
      this._callID++;
    }
  
    enqueueNativeCall(
      moduleID: number,
      methodID: number,
      params: any[],
      onFail: ?Function,
      onSucc: ?Function,
    ) {
      this.processCallbacks(moduleID, methodID, params, onFail, onSucc);
  
      this._queue[MODULE_IDS].push(moduleID);
      this._queue[METHOD_IDS].push(methodID);
  
      if (__DEV__) {
        // Validate that parameters passed over the bridge are
        // folly-convertible.  As a special case, if a prop value is a
        // function it is permitted here, and special-cased in the
        // conversion.
        const isValidArgument = val => {
          const t = typeof val;
          if (
            t === 'undefined' ||
            t === 'null' ||
            t === 'boolean' ||
            t === 'string'
          ) {
            return true;
          }
          if (t === 'number') {
            return isFinite(val);
          }
          if (t === 'function' || t !== 'object') {
            return false;
          }
          if (Array.isArray(val)) {
            return val.every(isValidArgument);
          }
          for (const k in val) {
            if (typeof val[k] !== 'function' && !isValidArgument(val[k])) {
              return false;
            }
          }
          return true;
        };
  
        // Replacement allows normally non-JSON-convertible values to be
        // seen.  There is ambiguity with string values, but in context,
        // it should at least be a strong hint.
        const replacer = (key, val) => {
          const t = typeof val;
          if (t === 'function') {
            return '<<Function ' + val.name + '>>';
          } else if (t === 'number' && !isFinite(val)) {
            return '<<' + val.toString() + '>>';
          } else {
            return val;
          }
        };
  
        // Note that JSON.stringify
        invariant(
          isValidArgument(params),
          '%s is not usable as a native method argument',
          JSON.stringify(params, replacer),
        );
  
        // The params object should not be mutated after being queued
        deepFreezeAndThrowOnMutationInDev((params: any));
      }
      this._queue[PARAMS].push(params);
  
      const now = Date.now();
      if (
        global.nativeFlushQueueImmediate &&
        now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS
      ) {
        const queue = this._queue;
        this._queue = [[], [], [], this._callID];
        this._lastFlush = now;
        global.nativeFlushQueueImmediate(queue);
      }
      Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length);
      if (__DEV__ && this.__spy && isFinite(moduleID)) {
        this.__spy({
          type: TO_NATIVE,
          module: this._remoteModuleTable[moduleID],
          method: this._remoteMethodTable[moduleID][methodID],
          args: params,
        });
      } else if (this.__spy) {
        this.__spy({
          type: TO_NATIVE,
          module: moduleID + '',
          method: methodID,
          args: params,
        });
      }
    }
  
    createDebugLookup(
      moduleID: number,
      name: string,
      methods: ?$ReadOnlyArray<string>,
    ) {
      if (__DEV__) {
        this._remoteModuleTable[moduleID] = name;
        this._remoteMethodTable[moduleID] = methods || [];
      }
    }
  
    // For JSTimers to register its callback. Otherwise a circular dependency
    // between modules is introduced. Note that only one callback may be
    // registered at a time.
    setImmediatesCallback(fn: () => void) {
      this._immediatesCallback = fn;
    }
  
    /**
     * Private methods
     */
  
    __guard(fn: () => void) {
      if (this.__shouldPauseOnThrow()) {
        fn();
      } else {
        try {
          fn();
        } catch (error) {
          ErrorUtils.reportFatalError(error);
        }
      }
    }
  
    // MessageQueue installs a global handler to catch all exceptions where JS users can register their own behavior
    // This handler makes all exceptions to be propagated from inside MessageQueue rather than by the VM at their origin
    // This makes stacktraces to be placed at MessageQueue rather than at where they were launched
    // The parameter DebuggerInternal.shouldPauseOnThrow is used to check before catching all exceptions and
    // can be configured by the VM or any Inspector
    __shouldPauseOnThrow(): boolean {
      return (
        // $FlowFixMe
        typeof DebuggerInternal !== 'undefined' &&
        DebuggerInternal.shouldPauseOnThrow === true // eslint-disable-line no-undef
      );
    }
  
    __callImmediates() {
      Systrace.beginEvent('JSTimers.callImmediates()');
      if (this._immediatesCallback != null) {
        this._immediatesCallback();
      }
      Systrace.endEvent();
    }
  
    __callFunction(module: string, method: string, args: any[]): any {
      this._lastFlush = Date.now();
      this._eventLoopStartTime = this._lastFlush;
      if (__DEV__ || this.__spy) {
        Systrace.beginEvent(`${module}.${method}(${stringifySafe(args)})`);
      } else {
        Systrace.beginEvent(`${module}.${method}(...)`);
      }
      if (this.__spy) {
        this.__spy({type: TO_JS, module, method, args});
      }
      const moduleMethods = this.getCallableModule(module);
      invariant(
        !!moduleMethods,
        'Module %s is not a registered callable module (calling %s)',
        module,
        method,
      );
      invariant(
        !!moduleMethods[method],
        'Method %s does not exist on module %s',
        method,
        module,
      );
      const result = moduleMethods[method].apply(moduleMethods, args);
      Systrace.endEvent();
      return result;
    }
  
    __invokeCallback(cbID: number, args: any[]) {
      this._lastFlush = Date.now();
      this._eventLoopStartTime = this._lastFlush;
  
      // The rightmost bit of cbID indicates fail (0) or success (1), the other bits are the callID shifted left.
      // eslint-disable-next-line no-bitwise
      const callID = cbID >>> 1;
      // eslint-disable-next-line no-bitwise
      const isSuccess = cbID & 1;
      const callback = isSuccess
        ? this._successCallbacks.get(callID)
        : this._failureCallbacks.get(callID);
  
      if (__DEV__) {
        const debug = this._debugInfo[callID];
        const module = debug && this._remoteModuleTable[debug[0]];
        const method = debug && this._remoteMethodTable[debug[0]][debug[1]];
        invariant(
          callback,
          `No callback found with cbID ${cbID} and callID ${callID} for ` +
            (method
              ? ` ${module}.${method} - most likely the callback was already invoked`
              : `module ${module || '<unknown>'}`) +
            `. Args: '${stringifySafe(args)}'`,
        );
        const profileName = debug
          ? '<callback for ' + module + '.' + method + '>'
          : cbID;
        if (callback && this.__spy) {
          this.__spy({type: TO_JS, module: null, method: profileName, args});
        }
        Systrace.beginEvent(
          `MessageQueue.invokeCallback(${profileName}, ${stringifySafe(args)})`,
        );
      }
  
      if (!callback) {
        return;
      }
  
      this._successCallbacks.delete(callID);
      this._failureCallbacks.delete(callID);
      callback(...args);
  
      if (__DEV__) {
        Systrace.endEvent();
      }
    }
  }
  
  module.exports = MessageQueue;
  