<commit_before>(function() {

  var supportCustomEvent = window.CustomEvent;
  if (!supportCustomEvent || typeof supportCustomEvent == 'object') {
    supportCustomEvent = function CustomEvent(event, x) {
      x = x || {};
      var ev = document.createEvent('CustomEvent');
      ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
      return ev;
    };
    supportCustomEvent.prototype = window.Event.prototype;
  }

  /**
   * Finds the nearest <dialog> from the passed element.
   *
   * @param {Element} el to search from
   * @return {HTMLDialogElement} dialog found
   */
  function findNearestDialog(el) {
    while (el) {
      if (el.nodeName.toUpperCase() == 'DIALOG') {
        return /** @type {HTMLDialogElement} */ (el);
      }
      el = el.parentElement;
    }
    return null;
  }

  /**
   * @param {!NodeList} nodeList to search
   * @param {Node} node to find
   * @return {boolean} whether node is inside nodeList
   */
  function inNodeList(nodeList, node) {
    for (var i = 0; i < nodeList.length; ++i) {
      if (nodeList[i] == node) {
        return true;
      }
    }
    return false;
  }

  /**
   * @param {!HTMLDialogElement} dialog to upgrade
   * @constructor
   */
  function dialogPolyfillInfo(dialog) {
    this.dialog_ = dialog;
    this.replacedStyleTop_ = false;
    this.openAsModal_ = false;

    dialog.show = this.show.bind(this);
    dialog.showModal = this.showModal.bind(this);
    dialog.close = this.close.bind(this);

    if (!('returnValue' in dialog)) {
      dialog.returnValue = '';
    }

    this.maybeHideModal = this.maybeHideModal.bind(this);
    if ('MutationObserver' in window) {
      // IE11+, most other browsers.
      var mo = new MutationObserver(this.maybeHideModal);
      mo.observe(dialog, { attributes: true, attributeFilter: ['open'] });
    } else {
      dialog.addEventListener('DOMAttrModified', this.maybeHideModal);
    }
    // Note that the DOM is observed inside DialogManager while any dialog
    // is being displayed as a modal, to catch modal removal from the DOM.

    Object.defineProperty(dialog, 'open', {
      set: this.setOpen.bind(this),
      get: dialog.hasAttribute.bind(dialog, 'open')
    });

    this.backdrop_ = document.createElement('div');
    this.backdrop_.className = 'backdrop';
    this.backdropClick_ = this.backdropClick_.bind(this);
  }

  dialogPolyfillInfo.prototype = {

    get dialog() {
      return this.dialog_;
    },

    /**
     * Maybe remove this dialog from the modal top layer. This is called when
     * a modal dialog may no longer be tenable, e.g., when the dialog is no
     * longer open or is no longer part of the DOM.
     */
    maybeHideModal: function() {
      if (!this.openAsModal_) { return; }
      if (this.dialog_.hasAttribute('open') &&
          document.body.contains(this.dialog_)) { return; }

      this.openAsModal_ = false;
      this.dialog_.style.zIndex = '';

      // This won't match the native <dialog> exactly because if the user set
      // top on a centered polyfill dialog, that top gets thrown away when the
      // dialog is closed. Not sure it's possible to polyfill this perfectly.
      if (this.replacedStyleTop_) {
        this.dialog_.style.top = '';
        this.replacedStyleTop_ = false;
      }

      // Optimistically clear the modal part of this <dialog>.
      this.backdrop_.removeEventListener('click', this.backdropClick_);
      if (this.backdrop_.parentElement) {
        this.backdrop_.parentElement.removeChild(this.backdrop_);
      }
      dialogPolyfill.dm.removeDialog(this);
    },

    /**
     * @param {boolean} value whether to open or close this dialog
     */
    setOpen: function(value) {
      if (value) {
        this.dialog_.hasAttribute('open') || this.dialog_.setAttribute('open', '');
      } else {
        this.dialog_.removeAttribute('open');
        this.maybeHideModal();  // nb. redundant with MutationObserver
      }
    },

    /**
     * Handles clicks on the fake .backdrop element, redirecting them as if
     * they were on the dialog itself.
     *
     * @param {!Event} e to redirect
     */
    backdropClick_: function(e) {
      var redirectedEvent = document.createEvent('MouseEvents');
      redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
          e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
          e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
      this.dialog_.dispatchEvent(redirectedEvent);
      e.stopPropagation();
    },

    /**
     * Sets the zIndex for the backdrop and dialog.
     *
     * @param {number} backdropZ
     * @param {number} dialogZ
     */
    updateZIndex: function(backdropZ, dialogZ) {
      this.backdrop_.style.zIndex = backdropZ;
      this.dialog_.style.zIndex = dialogZ;
    },

    /**
     * Shows the dialog. This is idempotent and will always succeed.
     */
    show: function() {
      this.setOpen(true);
    },

    /**
     * Show this dialog modally.
     */
    showModal: function() {
      if (this.dialog_.hasAttribute('open')) {
        throw 'Failed to execute \'showModal\' on dialog: The element is already open, and therefore cannot be opened modally.';
      }
      if (!document.body.contains(this.dialog_)) {
        throw 'Failed to execute \'showModal\' on dialog: The element is not in a Document.'
      }
      if (!dialogPolyfill.dm.pushDialog(this)) {
        throw 'Failed to execute \'showModal\' on dialog: There are too many open modal dialogs.'
      }
      this.show();
      this.openAsModal_ = true;

      // Optionally center vertically, relative to the current viewport.
      if (dialogPolyfill.needsCentering(this.dialog_)) {
        dialogPolyfill.reposition(this.dialog_);
        this.replacedStyleTop_ = true;
      } else {
        this.replacedStyleTop_ = false;
      }

      // Insert backdrop.
      this.backdrop_.addEventListener('click', this.backdropClick_);
      this.dialog_.parentNode.insertBefore(this.backdrop_,
          this.dialog_.nextSibling);

      // Find element with `autofocus` attribute or first form control.
      var target = this.dialog_.querySelector('[autofocus]:not([disabled])');
      if (!target) {
        // TODO: technically this is 'any focusable area'
        var opts = ['button', 'input', 'keygen', 'select', 'textarea'];
        var query = opts.map(function(el) {
          return el + ':not([disabled])';
        }).join(', ');
        target = this.dialog_.querySelector(query);
      }
      document.activeElement && document.activeElement.blur && document.activeElement.blur();
      target && target.focus();
    },

    /**
     * Closes this HTMLDialogElement. This is optional vs clearing the open
     * attribute, however this fires a 'close' event.
     *
     * @param {string=} opt_returnValue to use as the returnValue
     */
    close: function(opt_returnValue) {
      if (!this.dialog_.hasAttribute('open')) {
        throw 'Failed to execute \'close\' on dialog: The element does not have an \'open\' attribute, and therefore cannot be closed.'
      }
      this.setOpen(false);

      // Leave returnValue untouched in case it was set directly on the element
      if (opt_returnValue !== undefined) {
        this.dialog_.returnValue = opt_returnValue;
      }

      // Triggering "close" event for any attached listeners on the <dialog>.
      var closeEvent = new supportCustomEvent('close', {
        bubbles: false,
        cancelable: false
      });
      this.dialog_.dispatchEvent(closeEvent);
    }

  };

  var dialogPolyfill = {};

  dialogPolyfill.reposition = function(element) {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
    element.style.top = Math.max(scrollTop, topValue) + 'px';
  };

  dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
    for (var i = 0; i < document.styleSheets.length; ++i) {
      var styleSheet = document.styleSheets[i];
      var cssRules = null;
      // Some browsers throw on cssRules.
      try {
        cssRules = styleSheet.cssRules;
      } catch (e) {}
      if (!cssRules)
        continue;
      for (var j = 0; j < cssRules.length; ++j) {
        var rule = cssRules[j];
        var selectedNodes = null;
        // Ignore errors on invalid selector texts.
        try {
          selectedNodes = document.querySelectorAll(rule.selectorText);
        } catch(e) {}
        if (!selectedNodes || !inNodeList(selectedNodes, element))
          continue;
        var cssTop = rule.style.getPropertyValue('top');
        var cssBottom = rule.style.getPropertyValue('bottom');
        if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto'))
          return true;
      }
    }
    return false;
  };

  dialogPolyfill.needsCentering = function(dialog) {
    var computedStyle = window.getComputedStyle(dialog);
    if (computedStyle.position != 'absolute') {
      return false;
    }

    // We must determine whether the top/bottom specified value is non-auto.  In
    // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
    // Firefox returns the used value. So we do this crazy thing instead: check
    // the inline style and then go through CSS rules.
    if ((dialog.style.top != 'auto' && dialog.style.top != '') ||
        (dialog.style.bottom != 'auto' && dialog.style.bottom != ''))
      return false;
    return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
  };

  /**
   * @param {!Element} element to force upgrade
   */
  dialogPolyfill.forceRegisterDialog = function(element) {
    if (element.showModal) {
      console.warn('This browser already supports <dialog>, the polyfill ' +
          'may not work correctly', element);
    }
    if (element.nodeName.toUpperCase() != 'DIALOG') {
      throw 'Failed to register dialog: The element is not a dialog.';
    }
    new dialogPolyfillInfo(/** @type {!HTMLDialogElement} */ (element));
  };

  /**
   * @param {!Element} element to upgrade
   */
  dialogPolyfill.registerDialog = function(element) {
    if (element.showModal) {
      console.warn('Can\'t upgrade <dialog>: already supported', element);
    } else {
      dialogPolyfill.forceRegisterDialog(element);
    }
  };

  /**
   * @constructor
   */
  dialogPolyfill.DialogManager = function() {
    /** @type {!Array<!dialogPolyfillInfo>} */
    this.pendingDialogStack = [];

    // The overlay is used to simulate how a modal dialog blocks the document.
    // The blocking dialog is positioned on top of the overlay, and the rest of
    // the dialogs on the pending dialog stack are positioned below it. In the
    // actual implementation, the modal dialog stacking is controlled by the
    // top layer, where z-index has no effect.
    this.overlay = document.createElement('div');
    this.overlay.className = '_dialog_overlay';
    this.overlay.addEventListener('click', function(e) {
      e.stopPropagation();
    });

    this.handleKey_ = this.handleKey_.bind(this);
    this.handleFocus_ = this.handleFocus_.bind(this);
    this.handleRemove_ = this.handleRemove_.bind(this);

    this.zIndexLow_ = 100000;
    this.zIndexHigh_ = 100000 + 150;
  };

  /**
   * @return {Element} the top HTML dialog element, if any
   */
  dialogPolyfill.DialogManager.prototype.topDialogElement = function() {
    if (this.pendingDialogStack.length) {
      var t = this.pendingDialogStack[this.pendingDialogStack.length - 1];
      return t.dialog;
    }
    return null;
  };

  /**
   * Called on the first modal dialog being shown. Adds the overlay and related
   * handlers.
   */
  dialogPolyfill.DialogManager.prototype.blockDocument = function() {
    document.body.appendChild(this.overlay);
    document.body.addEventListener('focus', this.handleFocus_, true);
    document.addEventListener('keydown', this.handleKey_);
    document.addEventListener('DOMNodeRemoved', this.handleRemove_);
  };

  /**
   * Called on the first modal dialog being removed, i.e., when no more modal
   * dialogs are visible.
   */
  dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
    document.body.removeChild(this.overlay);
    document.body.removeEventListener('focus', this.handleFocus_, true);
    document.removeEventListener('keydown', this.handleKey_);
    document.removeEventListener('DOMNodeRemoved', this.handleRemove_);
  };

  dialogPolyfill.DialogManager.prototype.updateStacking = function() {
    var zIndex = this.zIndexLow_;

    for (var i = 0; i < this.pendingDialogStack.length; i++) {
      if (i == this.pendingDialogStack.length - 1) {
        this.overlay.style.zIndex = zIndex++;
      }
      this.pendingDialogStack[i].updateZIndex(zIndex++, zIndex++);
    }
  };

  dialogPolyfill.DialogManager.prototype.handleFocus_ = function(event) {
    var candidate = findNearestDialog(/** @type {Element} */ (event.target));
    if (candidate != this.topDialogElement()) {
      event.preventDefault();
      event.stopPropagation();
      event.target.blur();
      // TODO: Focus on the browser chrome (aka document) or the dialog itself
      // depending on the tab direction.
      return false;
    }
  };

  dialogPolyfill.DialogManager.prototype.handleKey_ = function(event) {
    if (event.keyCode == 27) {
      event.preventDefault();
      event.stopPropagation();
      var cancelEvent = new supportCustomEvent('cancel', {
        bubbles: false,
        cancelable: true
      });
      var dialog = this.topDialogElement();
      if (dialog.dispatchEvent(cancelEvent)) {
        dialog.close();
      }
    }
  };

  dialogPolyfill.DialogManager.prototype.handleRemove_ = function(event) {
    if (event.target.nodeName.toUpperCase() != 'DIALOG') { return; }

    var dialog = /** @type {HTMLDialogElement} */ (event.target);
    if (!dialog.open) { return; }

    // Find a dialogPolyfillInfo which matches the removed <dialog>.
    this.pendingDialogStack.some(function(dpi) {
      if (dpi.dialog == dialog) {
        // This call will clear the dialogPolyfillInfo on this DialogManager
        // as a side effect.
        dpi.maybeHideModal();
        return true;
      }
    });
  };

  /**
   * @param {!dialogPolyfillInfo} dpi
   * @return {boolean} whether the dialog was allowed
   */
  dialogPolyfill.DialogManager.prototype.pushDialog = function(dpi) {
    var allowed = (this.zIndexHigh_ - this.zIndexLow_) / 2 - 1;
    if (this.pendingDialogStack.length >= allowed) {
      return false;
    }
    this.pendingDialogStack.push(dpi);
    if (this.pendingDialogStack.length == 1) {
      this.blockDocument();
    }
    this.updateStacking();
    return true;
  };

  /**
   * @param {dialogPolyfillInfo} dpi
   */
  dialogPolyfill.DialogManager.prototype.removeDialog = function(dpi) {
    var index = this.pendingDialogStack.indexOf(dpi);
    if (index == -1) { return; }

    this.pendingDialogStack.splice(index, 1);
    this.updateStacking();
    if (this.pendingDialogStack.length == 0) {
      this.unblockDocument();
    }
  };

  dialogPolyfill.dm = new dialogPolyfill.DialogManager();

  /**
   * Global form 'dialog' method handler. Closes a dialog correctly on submit
   * and possibly sets its return value.
   */
  document.addEventListener('submit', function(ev) {
    var target = ev.target;
    if (!target || !target.hasAttribute('method')) { return; }
    if (target.getAttribute('method').toLowerCase() != 'dialog') { return; }
    ev.preventDefault();

    var dialog = findNearestDialog(/** @type {Element} */ (ev.target));
    if (!dialog) { return; }

    // FIXME: The original event doesn't contain the element used to submit the
    // form (if any). Look in some possible places.
    var returnValue;
    var cands = [document.activeElement, ev.explicitOriginalTarget];
    var els = ['BUTTON', 'INPUT'];
    cands.some(function(cand) {
      if (cand && cand.form == ev.target && els.indexOf(cand.nodeName.toUpperCase()) != -1) {
        returnValue = cand.value;
        return true;
      }
    });
    dialog.close(returnValue);
  }, true);

  window['dialogPolyfill'] = dialogPolyfill;
  dialogPolyfill['forceRegisterDialog'] = dialogPolyfill.forceRegisterDialog;
  dialogPolyfill['registerDialog'] = dialogPolyfill.registerDialog;

  if ( typeof module === 'object' && typeof module.exports === 'object' ) {
    // CommonJS support
    module.exports = dialogPolyfill;
  } else if (typeof define === 'function' && define.amd) {
    // AMD support
    define(function () { return dialogPolyfill; });
  }
})();
<commit_msg>closure fix<commit_after>(function() {

  var supportCustomEvent = window.CustomEvent;
  if (!supportCustomEvent || typeof supportCustomEvent == 'object') {
    supportCustomEvent = function CustomEvent(event, x) {
      x = x || {};
      var ev = document.createEvent('CustomEvent');
      ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
      return ev;
    };
    supportCustomEvent.prototype = window.Event.prototype;
  }

  /**
   * Finds the nearest <dialog> from the passed element.
   *
   * @param {Element} el to search from
   * @return {HTMLDialogElement} dialog found
   */
  function findNearestDialog(el) {
    while (el) {
      if (el.nodeName.toUpperCase() == 'DIALOG') {
        return /** @type {HTMLDialogElement} */ (el);
      }
      el = el.parentElement;
    }
    return null;
  }

  /**
   * @param {!NodeList} nodeList to search
   * @param {Node} node to find
   * @return {boolean} whether node is inside nodeList
   */
  function inNodeList(nodeList, node) {
    for (var i = 0; i < nodeList.length; ++i) {
      if (nodeList[i] == node) {
        return true;
      }
    }
    return false;
  }

  /**
   * @param {!HTMLDialogElement} dialog to upgrade
   * @constructor
   */
  function dialogPolyfillInfo(dialog) {
    this.dialog_ = dialog;
    this.replacedStyleTop_ = false;
    this.openAsModal_ = false;

    dialog.show = this.show.bind(this);
    dialog.showModal = this.showModal.bind(this);
    dialog.close = this.close.bind(this);

    if (!('returnValue' in dialog)) {
      dialog.returnValue = '';
    }

    this.maybeHideModal = this.maybeHideModal.bind(this);
    if ('MutationObserver' in window) {
      // IE11+, most other browsers.
      var mo = new MutationObserver(this.maybeHideModal);
      mo.observe(dialog, { attributes: true, attributeFilter: ['open'] });
    } else {
      dialog.addEventListener('DOMAttrModified', this.maybeHideModal);
    }
    // Note that the DOM is observed inside DialogManager while any dialog
    // is being displayed as a modal, to catch modal removal from the DOM.

    Object.defineProperty(dialog, 'open', {
      set: this.setOpen.bind(this),
      get: dialog.hasAttribute.bind(dialog, 'open')
    });

    this.backdrop_ = document.createElement('div');
    this.backdrop_.className = 'backdrop';
    this.backdropClick_ = this.backdropClick_.bind(this);
  }

  dialogPolyfillInfo.prototype = {

    get dialog() {
      return this.dialog_;
    },

    /**
     * Maybe remove this dialog from the modal top layer. This is called when
     * a modal dialog may no longer be tenable, e.g., when the dialog is no
     * longer open or is no longer part of the DOM.
     */
    maybeHideModal: function() {
      if (!this.openAsModal_) { return; }
      if (this.dialog_.hasAttribute('open') &&
          document.body.contains(this.dialog_)) { return; }

      this.openAsModal_ = false;
      this.dialog_.style.zIndex = '';

      // This won't match the native <dialog> exactly because if the user set
      // top on a centered polyfill dialog, that top gets thrown away when the
      // dialog is closed. Not sure it's possible to polyfill this perfectly.
      if (this.replacedStyleTop_) {
        this.dialog_.style.top = '';
        this.replacedStyleTop_ = false;
      }

      // Optimistically clear the modal part of this <dialog>.
      this.backdrop_.removeEventListener('click', this.backdropClick_);
      if (this.backdrop_.parentElement) {
        this.backdrop_.parentElement.removeChild(this.backdrop_);
      }
      dialogPolyfill.dm.removeDialog(this);
    },

    /**
     * @param {boolean} value whether to open or close this dialog
     */
    setOpen: function(value) {
      if (value) {
        this.dialog_.hasAttribute('open') || this.dialog_.setAttribute('open', '');
      } else {
        this.dialog_.removeAttribute('open');
        this.maybeHideModal();  // nb. redundant with MutationObserver
      }
    },

    /**
     * Handles clicks on the fake .backdrop element, redirecting them as if
     * they were on the dialog itself.
     *
     * @param {!Event} e to redirect
     */
    backdropClick_: function(e) {
      var redirectedEvent = document.createEvent('MouseEvents');
      redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
          e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
          e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
      this.dialog_.dispatchEvent(redirectedEvent);
      e.stopPropagation();
    },

    /**
     * Sets the zIndex for the backdrop and dialog.
     *
     * @param {number} backdropZ
     * @param {number} dialogZ
     */
    updateZIndex: function(backdropZ, dialogZ) {
      this.backdrop_.style.zIndex = backdropZ;
      this.dialog_.style.zIndex = dialogZ;
    },

    /**
     * Shows the dialog. This is idempotent and will always succeed.
     */
    show: function() {
      this.setOpen(true);
    },

    /**
     * Show this dialog modally.
     */
    showModal: function() {
      if (this.dialog_.hasAttribute('open')) {
        throw 'Failed to execute \'showModal\' on dialog: The element is already open, and therefore cannot be opened modally.';
      }
      if (!document.body.contains(this.dialog_)) {
        throw 'Failed to execute \'showModal\' on dialog: The element is not in a Document.'
      }
      if (!dialogPolyfill.dm.pushDialog(this)) {
        throw 'Failed to execute \'showModal\' on dialog: There are too many open modal dialogs.'
      }
      this.show();
      this.openAsModal_ = true;

      // Optionally center vertically, relative to the current viewport.
      if (dialogPolyfill.needsCentering(this.dialog_)) {
        dialogPolyfill.reposition(this.dialog_);
        this.replacedStyleTop_ = true;
      } else {
        this.replacedStyleTop_ = false;
      }

      // Insert backdrop.
      this.backdrop_.addEventListener('click', this.backdropClick_);
      this.dialog_.parentNode.insertBefore(this.backdrop_,
          this.dialog_.nextSibling);

      // Find element with `autofocus` attribute or first form control.
      var target = this.dialog_.querySelector('[autofocus]:not([disabled])');
      if (!target) {
        // TODO: technically this is 'any focusable area'
        var opts = ['button', 'input', 'keygen', 'select', 'textarea'];
        var query = opts.map(function(el) {
          return el + ':not([disabled])';
        }).join(', ');
        target = this.dialog_.querySelector(query);
      }
      document.activeElement && document.activeElement.blur && document.activeElement.blur();
      target && target.focus();
    },

    /**
     * Closes this HTMLDialogElement. This is optional vs clearing the open
     * attribute, however this fires a 'close' event.
     *
     * @param {string=} opt_returnValue to use as the returnValue
     */
    close: function(opt_returnValue) {
      if (!this.dialog_.hasAttribute('open')) {
        throw 'Failed to execute \'close\' on dialog: The element does not have an \'open\' attribute, and therefore cannot be closed.'
      }
      this.setOpen(false);

      // Leave returnValue untouched in case it was set directly on the element
      if (opt_returnValue !== undefined) {
        this.dialog_.returnValue = opt_returnValue;
      }

      // Triggering "close" event for any attached listeners on the <dialog>.
      var closeEvent = new supportCustomEvent('close', {
        bubbles: false,
        cancelable: false
      });
      this.dialog_.dispatchEvent(closeEvent);
    }

  };

  var dialogPolyfill = {};

  dialogPolyfill.reposition = function(element) {
    var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
    element.style.top = Math.max(scrollTop, topValue) + 'px';
  };

  dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
    for (var i = 0; i < document.styleSheets.length; ++i) {
      var styleSheet = document.styleSheets[i];
      var cssRules = null;
      // Some browsers throw on cssRules.
      try {
        cssRules = styleSheet.cssRules;
      } catch (e) {}
      if (!cssRules)
        continue;
      for (var j = 0; j < cssRules.length; ++j) {
        var rule = cssRules[j];
        var selectedNodes = null;
        // Ignore errors on invalid selector texts.
        try {
          selectedNodes = document.querySelectorAll(rule.selectorText);
        } catch(e) {}
        if (!selectedNodes || !inNodeList(selectedNodes, element))
          continue;
        var cssTop = rule.style.getPropertyValue('top');
        var cssBottom = rule.style.getPropertyValue('bottom');
        if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto'))
          return true;
      }
    }
    return false;
  };

  dialogPolyfill.needsCentering = function(dialog) {
    var computedStyle = window.getComputedStyle(dialog);
    if (computedStyle.position != 'absolute') {
      return false;
    }

    // We must determine whether the top/bottom specified value is non-auto.  In
    // WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
    // Firefox returns the used value. So we do this crazy thing instead: check
    // the inline style and then go through CSS rules.
    if ((dialog.style.top != 'auto' && dialog.style.top != '') ||
        (dialog.style.bottom != 'auto' && dialog.style.bottom != ''))
      return false;
    return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
  };

  /**
   * @param {!Element} element to force upgrade
   */
  dialogPolyfill.forceRegisterDialog = function(element) {
    if (element.showModal) {
      console.warn('This browser already supports <dialog>, the polyfill ' +
          'may not work correctly', element);
    }
    if (element.nodeName.toUpperCase() != 'DIALOG') {
      throw 'Failed to register dialog: The element is not a dialog.';
    }
    new dialogPolyfillInfo(/** @type {!HTMLDialogElement} */ (element));
  };

  /**
   * @param {!Element} element to upgrade
   */
  dialogPolyfill.registerDialog = function(element) {
    if (element.showModal) {
      console.warn('Can\'t upgrade <dialog>: already supported', element);
    } else {
      dialogPolyfill.forceRegisterDialog(element);
    }
  };

  /**
   * @constructor
   */
  dialogPolyfill.DialogManager = function() {
    /** @type {!Array<!dialogPolyfillInfo>} */
    this.pendingDialogStack = [];

    // The overlay is used to simulate how a modal dialog blocks the document.
    // The blocking dialog is positioned on top of the overlay, and the rest of
    // the dialogs on the pending dialog stack are positioned below it. In the
    // actual implementation, the modal dialog stacking is controlled by the
    // top layer, where z-index has no effect.
    this.overlay = document.createElement('div');
    this.overlay.className = '_dialog_overlay';
    this.overlay.addEventListener('click', function(e) {
      e.stopPropagation();
    });

    this.handleKey_ = this.handleKey_.bind(this);
    this.handleFocus_ = this.handleFocus_.bind(this);
    this.handleRemove_ = this.handleRemove_.bind(this);

    this.zIndexLow_ = 100000;
    this.zIndexHigh_ = 100000 + 150;
  };

  /**
   * @return {Element} the top HTML dialog element, if any
   */
  dialogPolyfill.DialogManager.prototype.topDialogElement = function() {
    if (this.pendingDialogStack.length) {
      var t = this.pendingDialogStack[this.pendingDialogStack.length - 1];
      return t.dialog;
    }
    return null;
  };

  /**
   * Called on the first modal dialog being shown. Adds the overlay and related
   * handlers.
   */
  dialogPolyfill.DialogManager.prototype.blockDocument = function() {
    document.body.appendChild(this.overlay);
    document.body.addEventListener('focus', this.handleFocus_, true);
    document.addEventListener('keydown', this.handleKey_);
    document.addEventListener('DOMNodeRemoved', this.handleRemove_);
  };

  /**
   * Called on the first modal dialog being removed, i.e., when no more modal
   * dialogs are visible.
   */
  dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
    document.body.removeChild(this.overlay);
    document.body.removeEventListener('focus', this.handleFocus_, true);
    document.removeEventListener('keydown', this.handleKey_);
    document.removeEventListener('DOMNodeRemoved', this.handleRemove_);
  };

  dialogPolyfill.DialogManager.prototype.updateStacking = function() {
    var zIndex = this.zIndexLow_;

    for (var i = 0; i < this.pendingDialogStack.length; i++) {
      if (i == this.pendingDialogStack.length - 1) {
        this.overlay.style.zIndex = zIndex++;
      }
      this.pendingDialogStack[i].updateZIndex(zIndex++, zIndex++);
    }
  };

  dialogPolyfill.DialogManager.prototype.handleFocus_ = function(event) {
    var candidate = findNearestDialog(/** @type {Element} */ (event.target));
    if (candidate != this.topDialogElement()) {
      event.preventDefault();
      event.stopPropagation();
      event.target.blur();
      // TODO: Focus on the browser chrome (aka document) or the dialog itself
      // depending on the tab direction.
      return false;
    }
  };

  dialogPolyfill.DialogManager.prototype.handleKey_ = function(event) {
    if (event.keyCode == 27) {
      event.preventDefault();
      event.stopPropagation();
      var cancelEvent = new supportCustomEvent('cancel', {
        bubbles: false,
        cancelable: true
      });
      var dialog = this.topDialogElement();
      if (dialog.dispatchEvent(cancelEvent)) {
        dialog.close();
      }
    }
  };

  dialogPolyfill.DialogManager.prototype.handleRemove_ = function(event) {
    if (event.target.nodeName.toUpperCase() != 'DIALOG') { return; }

    var dialog = /** @type {HTMLDialogElement} */ (event.target);
    if (!dialog.open) { return; }

    // Find a dialogPolyfillInfo which matches the removed <dialog>.
    this.pendingDialogStack.some(function(dpi) {
      if (dpi.dialog == dialog) {
        // This call will clear the dialogPolyfillInfo on this DialogManager
        // as a side effect.
        dpi.maybeHideModal();
        return true;
      }
    });
  };

  /**
   * @param {!dialogPolyfillInfo} dpi
   * @return {boolean} whether the dialog was allowed
   */
  dialogPolyfill.DialogManager.prototype.pushDialog = function(dpi) {
    var allowed = (this.zIndexHigh_ - this.zIndexLow_) / 2 - 1;
    if (this.pendingDialogStack.length >= allowed) {
      return false;
    }
    this.pendingDialogStack.push(dpi);
    if (this.pendingDialogStack.length == 1) {
      this.blockDocument();
    }
    this.updateStacking();
    return true;
  };

  /**
   * @param {dialogPolyfillInfo} dpi
   */
  dialogPolyfill.DialogManager.prototype.removeDialog = function(dpi) {
    var index = this.pendingDialogStack.indexOf(dpi);
    if (index == -1) { return; }

    this.pendingDialogStack.splice(index, 1);
    this.updateStacking();
    if (this.pendingDialogStack.length == 0) {
      this.unblockDocument();
    }
  };

  dialogPolyfill.dm = new dialogPolyfill.DialogManager();

  /**
   * Global form 'dialog' method handler. Closes a dialog correctly on submit
   * and possibly sets its return value.
   */
  document.addEventListener('submit', function(ev) {
    var target = ev.target;
    if (!target || !target.hasAttribute('method')) { return; }
    if (target.getAttribute('method').toLowerCase() != 'dialog') { return; }
    ev.preventDefault();

    var dialog = findNearestDialog(/** @type {Element} */ (ev.target));
    if (!dialog) { return; }

    // FIXME: The original event doesn't contain the element used to submit the
    // form (if any). Look in some possible places.
    var returnValue;
    var cands = [document.activeElement, ev.explicitOriginalTarget];
    var els = ['BUTTON', 'INPUT'];
    cands.some(function(cand) {
      if (cand && cand.form == ev.target && els.indexOf(cand.nodeName.toUpperCase()) != -1) {
        returnValue = cand.value;
        return true;
      }
    });
    dialog.close(returnValue);
  }, true);

  window['dialogPolyfill'] = dialogPolyfill;
  dialogPolyfill['forceRegisterDialog'] = dialogPolyfill.forceRegisterDialog;
  dialogPolyfill['registerDialog'] = dialogPolyfill.registerDialog;

  if (typeof module === 'object' && typeof module['exports'] === 'object') {
    // CommonJS support
    module['exports'] = dialogPolyfill;
  } else if (typeof define === 'function' && 'amd' in define) {
    // AMD support
    define(function() { return dialogPolyfill; });
  }
})();
<|endoftext|>


<commit_before>/*
 Description: The script is used to edit the tags of an asset
 Created Date:4/10/2013
 Filename: asset.tag.edit.js
 */
$(function () {

    /*
     Obtain the current url to get the id and asset type
     */
    var url = window.location.pathname;

    //Break the url into components
    var comps = url.split('/');

    //Given a url of the form /pub/api/asset/{asset-type}/{asset-id}
    //length=5
    //then: length-2 = {asset-type} length-1 = {asset-id}
    var assetId = comps[comps.length - 1];
    var assetType = comps[comps.length - 2];

    var TAG_CONTAINER = '#tag-test';
    var TAG_API = caramel.context +'/api/tag/';
    var TAG_THEME = 'facebook';
    var ERROR_CSS='alert alert-error';
    var MSG_CONTAINER='#msg-container-recent-activity';

    //$(TAG_CONTAINER).tokenInput([],{theme: TAG_THEME});
    //fetchTagsOfAssetType(assetType,assetId);
    fetchTagsOfType(assetType);

    /*
     The function is used to fetch the tags of the current asset type
     @assetType: The asset type for which the tags must be fetched
     */
    function fetchTagsOfType(assetType) {

        $.ajax({
            url: TAG_API + assetType + 's',
            type: 'GET',
            success: function (response) {

                var tags = response;
                console.log('obtaining tags of type.');

                fetchTagsOfAsset(assetType, assetId, tags);
            },
            error: function () {
                console.log('unable to retrieve tags for ' + assetTYpe);
            }
        });
    }

    /*
     The function is used to fetch the tags of the current asset
     @assetType: The asset type for which the tags must be fetched
     @assetId: The asset id of the asset for which the tags must be fetched
     */
    function fetchTagsOfAsset(assetType, assetId, masterTags) {

        $.ajax({
            url: TAG_API + assetType + '/' + assetId,
            type: 'GET',
            success: function (response) {
                //Initialize the tag container
                $(TAG_CONTAINER).tokenInput(masterTags, {
                    theme: TAG_THEME, prePopulate: response, preventDuplicates: false,
                    onAdd: onAdd,
                    allowFreeTagging: true,
                    onDelete: onRemove
                });

            }
        });

    }

    /*
     The function is called when a tag is added to an asset
     @item: The tag object which has been added to the asset
     */
    function onAdd(item) {

        var data = {};

        var tags = [item.name];
        data['tags'] = tags;

        //Make an api call to add the tag
        $.ajax({
            url: TAG_API + assetType + '/' + assetId,
            type: 'PUT',
            data: JSON.stringify(data),
            contentType:'application/json; charset=utf-8',
            dataType:'json',
            success:function(response){
            },
            error:function(){
                createMessage(MSG_CONTAINER,ERROR_CSS,'Unable to add the selected tag.');
            }
        });
    }

    /*
     The funciton is called when a tag is removed from an asset
     @item: The tag object which has been removed from the asset
     */
    function onRemove(item) {

        var data = {};

        var tags = [item.name];
        data['tags'] = tags;

        //Make an api call to add the tag
        $.ajax({
            url: TAG_API + assetType + '/' + assetId+'/'+item.name,
            type: 'DELETE',
            success:function(response){
            },
            error:function(){
                createMessage(MSG_CONTAINER,ERROR_CSS,'Unable to detach the selected tag.');
            }
        });
    }

    /*
     The function creates a message and displays it in the provided container element.
     @containerElement: The html element within which the message will be displayed
     @cssClass: The type of message to be displayed
     @msg: The message to be displayed
     */
    function createMessage(containerElement,cssClass,msg){
        var date=new Date();
        var time=date.getDate()+'/'+(date.getMonth()+1)+'/'+date.getFullYear()+' '+date.getHours()+':'+date.getMinutes()
            +':'+date.getSeconds();
        var infoMessage='
'
            +'x'
            +time+' '+msg+'</div';

        //Place the message
        $(containerElement).html(infoMessage);
    }

});
<commit_msg>removed redundant variable 'tags'<commit_after>/*
 Description: The script is used to edit the tags of an asset
 Created Date:4/10/2013
 Filename: asset.tag.edit.js
 */
$(function () {

    /*
     Obtain the current url to get the id and asset type
     */
    var url = window.location.pathname;

    //Break the url into components
    var comps = url.split('/');

    //Given a url of the form /pub/api/asset/{asset-type}/{asset-id}
    //length=5
    //then: length-2 = {asset-type} length-1 = {asset-id}
    var assetId = comps[comps.length - 1];
    var assetType = comps[comps.length - 2];

    var TAG_CONTAINER = '#tag-test';
    var TAG_API = caramel.context +'/api/tag/';
    var TAG_THEME = 'facebook';
    var ERROR_CSS='alert alert-error';
    var MSG_CONTAINER='#msg-container-recent-activity';

    //$(TAG_CONTAINER).tokenInput([],{theme: TAG_THEME});
    //fetchTagsOfAssetType(assetType,assetId);
    fetchTagsOfType(assetType);

    /*
     The function is used to fetch the tags of the current asset type
     @assetType: The asset type for which the tags must be fetched
     */
    function fetchTagsOfType(assetType) {

        $.ajax({
            url: TAG_API + assetType + 's',
            type: 'GET',
            success: function (response) {
                fetchTagsOfAsset(assetType, assetId, response);
            },
            error: function () {
                console.log('unable to retrieve tags for ' + assetTYpe);
            }
        });
    }

    /*
     The function is used to fetch the tags of the current asset
     @assetType: The asset type for which the tags must be fetched
     @assetId: The asset id of the asset for which the tags must be fetched
     */
    function fetchTagsOfAsset(assetType, assetId, masterTags) {

        $.ajax({
            url: TAG_API + assetType + '/' + assetId,
            type: 'GET',
            success: function (response) {
                //Initialize the tag container
                $(TAG_CONTAINER).tokenInput(masterTags, {
                    theme: TAG_THEME, prePopulate: response, preventDuplicates: false,
                    onAdd: onAdd,
                    allowFreeTagging: true,
                    onDelete: onRemove
                });

            }
        });

    }

    /*
     The function is called when a tag is added to an asset
     @item: The tag object which has been added to the asset
     */
    function onAdd(item) {

        var data = {};

        var tags = [item.name];
        data['tags'] = tags;

        //Make an api call to add the tag
        $.ajax({
            url: TAG_API + assetType + '/' + assetId,
            type: 'PUT',
            data: JSON.stringify(data),
            contentType:'application/json; charset=utf-8',
            dataType:'json',
            success:function(response){
            },
            error:function(){
                createMessage(MSG_CONTAINER,ERROR_CSS,'Unable to add the selected tag.');
            }
        });
    }

    /*
     The funciton is called when a tag is removed from an asset
     @item: The tag object which has been removed from the asset
     */
    function onRemove(item) {

        var data = {};

        var tags = [item.name];
        data['tags'] = tags;

        //Make an api call to add the tag
        $.ajax({
            url: TAG_API + assetType + '/' + assetId+'/'+item.name,
            type: 'DELETE',
            success:function(response){
            },
            error:function(){
                createMessage(MSG_CONTAINER,ERROR_CSS,'Unable to detach the selected tag.');
            }
        });
    }

    /*
     The function creates a message and displays it in the provided container element.
     @containerElement: The html element within which the message will be displayed
     @cssClass: The type of message to be displayed
     @msg: The message to be displayed
     */
    function createMessage(containerElement,cssClass,msg){
        var date=new Date();
        var time=date.getDate()+'/'+(date.getMonth()+1)+'/'+date.getFullYear()+' '+date.getHours()+':'+date.getMinutes()
            +':'+date.getSeconds();
        var infoMessage='
'
            +'x'
            +time+' '+msg+'</div';

        //Place the message
        $(containerElement).html(infoMessage);
    }

});
<|endoftext|>


<commit_before>// Copyright (c) 2015 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';

var series = require('run-series');
var allocCluster = require('./lib/alloc-cluster');
var TChannel = require('../channel');

allocCluster.test('request retries', {
    numPeers: 4
}, function t(cluster, assert) {
    var random = randSeq([
        1.0, 0.1, 0.1, 0.1, // .request, chan 1 wins
        1.0,                // chan 1 declines
             1.0, 0.1, 0.1, // .request, chan 2 wins (1 is skipped)
        0.25,               // chan 2 too busy
                  1.0, 0.1, // .request, chan 3 wins (1-2 are skipped)
        0.1,                // chan 3 unexpected error
                       0.1, // .request, chan 4 wins (only one left)
        0.0                 // success!
    ]);

    cluster.channels.forEach(function each(server, i) {
        var n = i + 1;
        var chan = server.makeSubChannel({
            serviceName: 'tristan'
        });
        chan.register('foo', function foo(req, res, arg2, arg3) {
            var rand = random();
            if (rand >= 0.5) {
                res.sendError('Declined', 'magic 8-ball says no');
            } else if (rand >= 0.25) {
                res.sendError('Busy', "can't talk");
            } else if (rand) {
                res.sendError('UnexpectedError', 'wat');
            } else {
                var str = String(arg3);
                str = str.toUpperCase();
                res.sendOk('served by ' + n, str);
            }
        });
    });

    var client = TChannel({
        timeoutFuzz: 0,
        random: random
    });
    var chan = client.makeSubChannel({
        serviceName: 'tristan',
        peers: cluster.hosts
    });

    var req = chan.request({
        timeout: 100,
        service: 'tristan'
    });
    req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
        if (err) return finish(err);

        assert.equal(req.outReqs.length, 4, 'expected 4 tries');

        assert.equal(
            req.outReqs[0].err &&
            req.outReqs[0].err.type,
            'tchannel.declined',
            'expected first request to decline');

        assert.equal(
            req.outReqs[1].err &&
            req.outReqs[1].err.type,
            'tchannel.busy',
            'expected second request to bounce b/c busy');

        assert.equal(
            req.outReqs[2].err &&
            req.outReqs[2].err.type,
            'tchannel.unexpected',
            'expected second request to bounce w/ unexpected error');

        assert.ok(req.outReqs[3].res, 'expected to have 4th response');
        assert.deepEqual(req.outReqs[3].res.arg2, arg2, 'arg2 came form 4th response');
        assert.deepEqual(req.outReqs[3].res.arg3, arg3, 'arg3 came form 4th response');
        assert.equal(String(arg2), 'served by 4', 'served by expected server');
        assert.equal(String(arg3), 'HI', 'got expected response');

        finish();
    });

    function finish(err) {
        assert.ifError(err, 'no final error');

        cluster.assertCleanState(assert, {
            channels: [{
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }]
        });
        assert.end();
    }
});

allocCluster.test('request application retries', {
    numPeers: 4
}, function t(cluster, assert) {
    var random = randSeq([
        1.0, 0.1, 0.1, 0.1, // .request, chan 1 wins
        1.0,                // chan 1 has applicatino error
             1.0, 0.1, 0.1, // .request, chan 2 wins (1 is skipped)
        0.0                 // "success"!
    ]);

    cluster.channels.forEach(function each(server, i) {
        var chan = server.makeSubChannel({
            serviceName: 'tristan'
        });
        chan.register('foo', function foo(req, res, arg2, arg3) {
            var rand = random();
            if (rand) {
                res.sendNotOk('meh', 'lol');
            } else {
                res.sendNotOk('no', 'stop');
            }
        });
    });

    var client = TChannel({
        timeoutFuzz: 0,
        random: random
    });
    var chan = client.makeSubChannel({
        serviceName: 'tristan',
        peers: cluster.hosts
    });

    var req = chan.request({
        timeout: 100,
        service: 'tristan',
        shouldApplicationRetry: function shouldApplicationRetry(req, res, arg2, arg3) {
            return String(arg2) === 'meh';
        }
    });
    req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
        if (err) return finish(err);

        assert.equal(req.outReqs.length, 2, 'expected 2 tries');

        assert.ok(
            req.outReqs[0].res &&
            !req.outReqs[0].res.ok,
            'expected first res not ok');
        assert.equal(
            req.outReqs[0].res &&
            String(req.outReqs[0].res.arg3),
            'lol',
            'expected first res arg3');

        assert.ok(req.outReqs[1].res, 'expected to have 2nd response');
        assert.ok(!res.ok, 'expected to have not been ok');
        assert.equal(String(arg3), 'stop', 'got expected response');

        finish();
    });

    function finish(err) {
        assert.ifError(err, 'no final error');

        cluster.assertCleanState(assert, {
            channels: [{
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: []
            }, {
                peers: []
            }]
        });
        assert.end();
    }
});

allocCluster.test('retryFlags work', {
    numPeers: 2
}, function t(cluster, assert) {
    var random = randSeq([
        1.0, 0.1, // .request, chan 1 wins
        0.5,      // chan 1 timeout

        1.0, 0.1, // .request, chan 1 wins
        0.5,      // chan 1 timeout
             1.0, // .request, chan 2 wins (1 is skipped)
        0.0,      // success!

        1.0, 0.1, // .request, chan 1 wins
        0.9       // chan 1 busy

    ]);

    cluster.channels.forEach(function each(server, i) {
        var n = i + 1;
        var chan = server.makeSubChannel({
            serviceName: 'tristan'
        });
        chan.register('foo', function foo(req, res, arg2, arg3) {
            var rand = random();
            if (rand >= 0.9) {
                res.sendError('Busy', 'nop');
            } else if (rand >= 0.5) {
                res.sendError('Timeout', 'no luck');
            } else {
                var str = String(arg3);
                str = str.toUpperCase();
                res.sendOk('served by ' + n, str);
            }
        });
    });

    var client = TChannel({
        timeoutFuzz: 0,
        random: random
    });
    var chan = client.makeSubChannel({
        serviceName: 'tristan',
        peers: cluster.hosts
    });

    series([

        function defaultToNotRetryingTimeout(next) {
            var req = chan.request({
                timeout: 100,
                service: 'tristan'
            });
            req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
                assert.equal(req.outReqs.length, 1, 'expected 1 tries');
                assert.equal(err && err.type, 'tchannel.timeout', 'expected timeout error');
                next();
            });
        },

        function canRetryTimeout(next) {
            var req = chan.request({
                service: 'tristan',
                retryFlags: {
                    never: false,
                    onConnectionError: true,
                    onTimeout: true
                }
            });
            req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
                if (err) return finish(err);

                assert.equal(req.outReqs.length, 2, 'expected 2 tries');

                assert.equal(
                    req.outReqs[0].err &&
                    req.outReqs[0].err.type,
                    'tchannel.timeout',
                    'expected first timeout error');

                assert.ok(res.ok, 'expected to have not ok');
                assert.ok(req.outReqs[1].res, 'expected to have 2nd response');
                assert.equal(String(arg3), 'HI', 'got expected response');

                next();
            });
        },

        function canOptOutFully(next) {
            var req = chan.request({
                timeout: 100,
                service: 'tristan',
                retryFlags: {
                    never: true,
                    onConnectionError: false,
                    onTimeout: false
                }
            });
            req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
                assert.equal(req.outReqs.length, 1, 'expected 1 tries');
                assert.equal(err && err.type, 'tchannel.busy', 'expected busy error');
                next();
            });
        }

    ], finish);

    function finish(err) {
        assert.ifError(err, 'no final error');

        cluster.assertCleanState(assert, {
            channels: [{
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }]
        });
        assert.end();
    }
});

function randSeq(seq) {
    var i = 0;
    return function random() {
        var r = seq[i];
        i = (i + 1) % seq.length;
        return r;
    };
}
<commit_msg>test/retry: use requestDefaults serviceName to demonstrate the intended path<commit_after>// Copyright (c) 2015 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';

var series = require('run-series');
var allocCluster = require('./lib/alloc-cluster');
var TChannel = require('../channel');

allocCluster.test('request retries', {
    numPeers: 4
}, function t(cluster, assert) {
    var random = randSeq([
        1.0, 0.1, 0.1, 0.1, // .request, chan 1 wins
        1.0,                // chan 1 declines
             1.0, 0.1, 0.1, // .request, chan 2 wins (1 is skipped)
        0.25,               // chan 2 too busy
                  1.0, 0.1, // .request, chan 3 wins (1-2 are skipped)
        0.1,                // chan 3 unexpected error
                       0.1, // .request, chan 4 wins (only one left)
        0.0                 // success!
    ]);

    cluster.channels.forEach(function each(server, i) {
        var n = i + 1;
        var chan = server.makeSubChannel({
            serviceName: 'tristan'
        });
        chan.register('foo', function foo(req, res, arg2, arg3) {
            var rand = random();
            if (rand >= 0.5) {
                res.sendError('Declined', 'magic 8-ball says no');
            } else if (rand >= 0.25) {
                res.sendError('Busy', "can't talk");
            } else if (rand) {
                res.sendError('UnexpectedError', 'wat');
            } else {
                var str = String(arg3);
                str = str.toUpperCase();
                res.sendOk('served by ' + n, str);
            }
        });
    });

    var client = TChannel({
        timeoutFuzz: 0,
        random: random
    });
    var chan = client.makeSubChannel({
        serviceName: 'tristan',
        peers: cluster.hosts,
        requestDefaults: {
            serviceName: 'tristan'
        }
    });

    var req = chan.request({
        timeout: 100
    });
    req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
        if (err) return finish(err);

        assert.equal(req.outReqs.length, 4, 'expected 4 tries');

        assert.equal(
            req.outReqs[0].err &&
            req.outReqs[0].err.type,
            'tchannel.declined',
            'expected first request to decline');

        assert.equal(
            req.outReqs[1].err &&
            req.outReqs[1].err.type,
            'tchannel.busy',
            'expected second request to bounce b/c busy');

        assert.equal(
            req.outReqs[2].err &&
            req.outReqs[2].err.type,
            'tchannel.unexpected',
            'expected second request to bounce w/ unexpected error');

        assert.ok(req.outReqs[3].res, 'expected to have 4th response');
        assert.deepEqual(req.outReqs[3].res.arg2, arg2, 'arg2 came form 4th response');
        assert.deepEqual(req.outReqs[3].res.arg3, arg3, 'arg3 came form 4th response');
        assert.equal(String(arg2), 'served by 4', 'served by expected server');
        assert.equal(String(arg3), 'HI', 'got expected response');

        finish();
    });

    function finish(err) {
        assert.ifError(err, 'no final error');

        cluster.assertCleanState(assert, {
            channels: [{
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }]
        });
        assert.end();
    }
});

allocCluster.test('request application retries', {
    numPeers: 4
}, function t(cluster, assert) {
    var random = randSeq([
        1.0, 0.1, 0.1, 0.1, // .request, chan 1 wins
        1.0,                // chan 1 has applicatino error
             1.0, 0.1, 0.1, // .request, chan 2 wins (1 is skipped)
        0.0                 // "success"!
    ]);

    cluster.channels.forEach(function each(server, i) {
        var chan = server.makeSubChannel({
            serviceName: 'tristan'
        });
        chan.register('foo', function foo(req, res, arg2, arg3) {
            var rand = random();
            if (rand) {
                res.sendNotOk('meh', 'lol');
            } else {
                res.sendNotOk('no', 'stop');
            }
        });
    });

    var client = TChannel({
        timeoutFuzz: 0,
        random: random
    });
    var chan = client.makeSubChannel({
        serviceName: 'tristan',
        peers: cluster.hosts,
        requestDefaults: {
            serviceName: 'tristan'
        }
    });

    var req = chan.request({
        timeout: 100,
        shouldApplicationRetry: function shouldApplicationRetry(req, res, arg2, arg3) {
            return String(arg2) === 'meh';
        }
    });
    req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
        if (err) return finish(err);

        assert.equal(req.outReqs.length, 2, 'expected 2 tries');

        assert.ok(
            req.outReqs[0].res &&
            !req.outReqs[0].res.ok,
            'expected first res not ok');
        assert.equal(
            req.outReqs[0].res &&
            String(req.outReqs[0].res.arg3),
            'lol',
            'expected first res arg3');

        assert.ok(req.outReqs[1].res, 'expected to have 2nd response');
        assert.ok(!res.ok, 'expected to have not been ok');
        assert.equal(String(arg3), 'stop', 'got expected response');

        finish();
    });

    function finish(err) {
        assert.ifError(err, 'no final error');

        cluster.assertCleanState(assert, {
            channels: [{
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: []
            }, {
                peers: []
            }]
        });
        assert.end();
    }
});

allocCluster.test('retryFlags work', {
    numPeers: 2
}, function t(cluster, assert) {
    var random = randSeq([
        1.0, 0.1, // .request, chan 1 wins
        0.5,      // chan 1 timeout

        1.0, 0.1, // .request, chan 1 wins
        0.5,      // chan 1 timeout
             1.0, // .request, chan 2 wins (1 is skipped)
        0.0,      // success!

        1.0, 0.1, // .request, chan 1 wins
        0.9       // chan 1 busy

    ]);

    cluster.channels.forEach(function each(server, i) {
        var n = i + 1;
        var chan = server.makeSubChannel({
            serviceName: 'tristan'
        });
        chan.register('foo', function foo(req, res, arg2, arg3) {
            var rand = random();
            if (rand >= 0.9) {
                res.sendError('Busy', 'nop');
            } else if (rand >= 0.5) {
                res.sendError('Timeout', 'no luck');
            } else {
                var str = String(arg3);
                str = str.toUpperCase();
                res.sendOk('served by ' + n, str);
            }
        });
    });

    var client = TChannel({
        timeoutFuzz: 0,
        random: random
    });
    var chan = client.makeSubChannel({
        serviceName: 'tristan',
        peers: cluster.hosts,
        requestDefaults: {
            serviceName: 'tristan'
        }
    });

    series([

        function defaultToNotRetryingTimeout(next) {
            var req = chan.request({
                timeout: 100
            });
            req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
                assert.equal(req.outReqs.length, 1, 'expected 1 tries');
                assert.equal(err && err.type, 'tchannel.timeout', 'expected timeout error');
                next();
            });
        },

        function canRetryTimeout(next) {
            var req = chan.request({
                retryFlags: {
                    never: false,
                    onConnectionError: true,
                    onTimeout: true
                }
            });
            req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
                if (err) return finish(err);

                assert.equal(req.outReqs.length, 2, 'expected 2 tries');

                assert.equal(
                    req.outReqs[0].err &&
                    req.outReqs[0].err.type,
                    'tchannel.timeout',
                    'expected first timeout error');

                assert.ok(res.ok, 'expected to have not ok');
                assert.ok(req.outReqs[1].res, 'expected to have 2nd response');
                assert.equal(String(arg3), 'HI', 'got expected response');

                next();
            });
        },

        function canOptOutFully(next) {
            var req = chan.request({
                timeout: 100,
                retryFlags: {
                    never: true,
                    onConnectionError: false,
                    onTimeout: false
                }
            });
            req.send('foo', '', 'hi', function done(err, res, arg2, arg3) {
                assert.equal(req.outReqs.length, 1, 'expected 1 tries');
                assert.equal(err && err.type, 'tchannel.busy', 'expected busy error');
                next();
            });
        }

    ], finish);

    function finish(err) {
        assert.ifError(err, 'no final error');

        cluster.assertCleanState(assert, {
            channels: [{
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }, {
                peers: [{
                    connections: [
                        {direction: 'in', inReqs: 0, outReqs: 0}
                    ]
                }]
            }]
        });
        assert.end();
    }
});

function randSeq(seq) {
    var i = 0;
    return function random() {
        var r = seq[i];
        i = (i + 1) % seq.length;
        return r;
    };
}
<|endoftext|>