Source: index.js

const MESSAGE = 'message'
let _messageId = 0

function resolveOrigin(url) {
  const a = document.createElement('a')
  a.href = url
  return a.origin || `${a.protocol}//${a.hostname}`
}

function resolveValue(model, property) {
  const unwrappedContext = typeof model[property] === 'function'
    ? model[property]() : model[property];
  return Promise.resolve(unwrappedContext);
}

function messageUID() {
  return ++_messageId
}

function distrust(e, origin) {
  if (e.origin !== origin) return false
  if (typeof e.data === 'object') return false
  if (!('type' in e.data)) return false
}

/**
 * parent frame api
 */
class ParentAPI {
  constructor(info) {
    this.parent = info.parent
    this.child = info.child
    this.childOrigin = info.childOrigin
    this.frame = info.frame

    this.events = {}

    this.listener = (e) => {
      if (distrust(e, this.childOrigin)) return

      if (e.data.type === 'emit') {
        const {name, data} = e.data.value
        if (name in this.events) {
          this.events[name].call(this, data)
        }
      }
    }

    this.parent.addEventListener(MESSAGE, this.listener, false)
  }

  /**
   * get iframe model property
   * @param {string} property iframe modle field name
   * @returns {Promise}
   */
  get(property) {
    return new Promise((resolve, reject) => {
      const uid = messageUID()
      const transact = e => {
        if (e.data.type === 'reply' && e.data.uid === uid) {
          this.parent.removeEventListener(MESSAGE, transact, false);
          resolve(e.data.value);
        }
      }
      this.parent.addEventListener(MESSAGE, transact, false)

      this.child.postMessage({
        type: 'request',
        property,
        uid
      }, this.childOrigin)
    })
  }

  /**
   * invoke iframe model function property
   * @param {string} property iframe model function name
   * @param {Object} data functoin argument
   */
  call(property, data) {
    this.child.postMessage({
      type: 'call',
      property,
      data
    }, this.childOrigin)
  }

  /**
   * add iframe event handle
   * @param {string} name iframe emit event name
   * @param {function} callback event handler function
   */
  on(name, callback) {
    this.events[name] = callback
  }

  /**
   * destroy iframe
   */
  destroy() {
    this.parent.removeEventListener(MESSAGE, this.listener, false)
    this.frame.parentNode.removeChild(this.frame)
  }
}

/**
 * child frame api
 */
class ChildAPI {
  constructor(info) {
    this.parent = info.parent
    this.parentOrigin = info.parentOrigin
    this.child = info.child
    this.model = info.model

    this.child.addEventListener(MESSAGE, e => {
      if (distrust(e, this.parentOrigin)) return

      const {type, property, uid, data} = e.data
      if (type === 'call') {
        if (property in this.model && typeof this.model[property] === 'function')
          this.model[property].call(this, data)
      } else if (type === 'request') {
        resolveValue(this.model, property).then(value => {
          e.source.postMessage({
            type: 'reply',
            value,
            uid
          }, e.origin)
        })
      }
    }, false)
  }

  /**
   * iframe emit event and data to parent
   * @param {string} name event name
   * @param {Object} data transfer data
   */
  emit(name, data) {
    this.parent.postMessage({
      type: 'emit',
      value: {name, data}
    }, this.parentOrigin)
  }
}

class Client {
  /**
   * iframe communicate component, defined in child frame
   * @param {Object} model iframe model
   * @returns {Promise} return {@link ChildAPI} Promise Object
   */
  constructor(model) {
    this.child = window
    this.parent = this.child.parent
    this.model = model
    return this.sendHandshakeReply()
  }

  sendHandshakeReply() {
    return new Promise((resolve, reject) => {
      const shake = (e) => {
        if (e.data.type === 'handshake') {
          this.child.removeEventListener(MESSAGE, shake, false)
          this.parentOrigin = e.origin

          e.source.postMessage({
            type: 'handshake-reply',
          }, e.origin)

          Object.assign(this.model, e.data.model)

          resolve(new ChildAPI(this))
        } else
          reject('Handshake reply failed.')
      }

      this.child.addEventListener(MESSAGE, shake, false)
    })
  }
}

class Postbox {
  /**
   * iframe communicate component, defined in parent frame
   * @param {Object} options
   * @param {element} [options.container=document.body] element to inject iframe into
   * @param {string} options.url iframe's url
   * @param {Object} [options.model] model send to iframe
   * @returns {Promise} return {@link ParentAPI} Promise Object
   */
  constructor(options) {
    const {container, url, model} = options

    this.parent = window
    this.frame = document.createElement('iframe');
    (container || document.body).appendChild(this.frame)
    this.child = this.frame.contentWindow || this.frame.contentDocument.parentWindow
    this.model = model || {}

    return this.sendHandshake(url)
  }

  sendHandshake(url) {
    const childOrigin = resolveOrigin(url)
    return new Promise((resolve, reject) => {
      const reply = (e) => {

        // receive handshake reply from iframe
        if (e.data.type === 'handshake-reply') {
          this.parent.removeEventListener(MESSAGE, reply, false)
          this.childOrigin = e.origin

          resolve(new ParentAPI(this))
        } else
          reject('Handshake failed')
      }

      this.parent.addEventListener(MESSAGE, reply, false)

      // send handshake to iframe
      const loaded = (e) => {
        this.child.postMessage({
          type: 'handshake',
          model: this.model
        }, childOrigin)
      }

      this.frame.onload = loaded
      this.frame.src = url
    })
  }
}

/**
 *
 * @type {Client}
 */
Postbox.Client = Client

module.exports = Postbox