'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var _throttle = _interopDefault(require('lodash/throttle'));
var ms = _interopDefault(require('ms'));
var Courier = _interopDefault(require('@workgrid/courier'));
var jwtDecode = _interopDefault(require('jwt-decode'));
var ResizeObserver = _interopDefault(require('resize-observer-polyfill'));
var pAny = _interopDefault(require('p-any'));

var queue = (() => {
  const queue = [];
  return {
    queue,

    push(fn) {
      if (queue.flushed) fn();else queue.push(fn);
    },

    flush() {
      queue.flushed = true;
      queue.map(fn => fn());
    }

  };
});

const EVENTS = {
  READY: 'ready',
  ERROR: 'error',
  SET_SIZE: 'setSize',
  SHOW_DETAIL: 'showDetail',
  HIDE_DETAIL: 'hideDetail',
  UPDATE_TITLE: 'updateTitle',
  REFRESH_TOKEN: 'refreshToken'
};
const READY_TIMEOUT = ms('10s');
const READY_INTERVAL = ms('100ms');

const isExpired = token => {
  try {
    const decoded = jwtDecode(token);
    return decoded.exp < Date.now() / 1000;
  } catch (e) {
    return true;
  }
};
/**
 * Create a new instance of the Micro App library.
 *
 * @beta
 */


class MicroApp {
  constructor({
    audience,
    onError,
    id
  } = {}) {
    /**
     * Set the current height on resize
     */
    this.handleResize = _throttle(entries => {
      const mainElement = entries[0].target;
      const height = mainElement.offsetHeight;
      this.emit({
        type: EVENTS.SET_SIZE,
        payload: {
          height
        }
      });
    }, 1000);
    if (audience) console && console.warn('Providing an audience is deprecated');
    this.audience = audience;
    this.id = id || audience;

    const noop = () => {};

    this.courier = new Courier({
      id: this.id
    });
    this.courier.on('error', onError || noop);
    this.queue = queue();
    this.ro = new ResizeObserver(this.handleResize);
  }
  /**
   * Set up the communication channel with Workgrid.
   * A loading overlay will be displayed until `initialize` is invoked.
   */


  initialize() {
    // Tell the host we're ready
    this.ready().then(() => {
      // Flush the queue
      this.queue.flush(); // Listen for resize

      this.subscribe();
    });
  }
  /**
   * Start the resize observer
   */


  subscribe() {
    this.ro.observe(window.document.documentElement);
    return this.unsubscribe; // why not... lol
  }
  /**
   * Stop the resize observer
   */


  unsubscribe() {
    this.ro.disconnect();
  }
  /**
   * Retrieve a token to validate the user in your API.
   * The token will be a JWT that includes `email`, `workgrid_space_id` and `workgrid_tenant_id`.
   */


  async getToken() {
    if (this.token && !isExpired(this.token)) return this.token;
    return this.token = await this.send({
      type: EVENTS.REFRESH_TOKEN,
      payload: {
        audience: this.audience
      }
    });
  }
  /**
   * Update the title of the detail panel. This will let you reflect
   */


  updateTitle(title) {
    this.emit({
      type: EVENTS.UPDATE_TITLE,
      payload: {
        title
      }
    });
  }
  /**
   * Show the detail with the given url and optional title.
   * The page shown must also be configured as a micro app, otherwise an error will be thrown.
   */


  showDetail({
    url,
    title
  }) {
    if (!url) throw new Error('URL is required to show details');
    this.emit({
      type: EVENTS.SHOW_DETAIL,
      payload: {
        title,
        url
      }
    });
  }
  /**
   * Hide the detail if it's visible.
   */


  hideDetail() {
    this.emit({
      type: EVENTS.HIDE_DETAIL
    });
  } // Private API

  /**
   * Wrap the event emitter in a queue that is flushed when the host is ready.
   */


  emit(...args) {
    this.queue.push(() => {
      this.courier.emit(...args);
    });
  }
  /**
   * Wrap the event sender in a queue that is flushed when the host is ready.
   */


  send(...args) {
    return new Promise((resolve, reject) => {
      this.queue.push(() => {
        this.courier.send(...args).then(resolve, reject);
      });
    });
  }
  /**
   * Tell the host we're ready every 100ms until the message is acknowledged.
   * This fires off two techniques "serial" and "cascade", Android needs serial for now; cascade is superior
   */


  async ready() {
    // Sigh.. don't ask me why Android can't do the cascade variety
    return pAny([this.serialReady(), this.cascadeReady()]);
  }

  serialReady(attempt = 1) {
    const payload = {
      height: window.document.documentElement.offsetHeight
    };
    const promise = this.courier.send({
      type: EVENTS.READY,
      payload,
      timeout: READY_INTERVAL
    });
    return attempt >= READY_TIMEOUT / READY_INTERVAL ? promise : promise.catch(() => this.serialReady(attempt + 1));
  }

  cascadeReady() {
    const payload = {
      height: window.document.documentElement.offsetHeight
    };
    const sendPromises = [this.courier.send({
      type: EVENTS.READY,
      payload
    })];
    const interval = setInterval(() => {
      sendPromises.push(this.courier.send({
        type: EVENTS.READY,
        payload
      })); // create a new promise every 100ms for 10s

      if (sendPromises.length >= READY_TIMEOUT / READY_INTERVAL) {
        clearInterval(interval);
      }
    }, READY_INTERVAL);
    return pAny(sendPromises).then(() => {
      clearInterval(interval);
    });
  }

}

module.exports = MicroApp;
//# sourceMappingURL=micro-app.js.map
