123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- // Fork from https://github.com/dollarshaveclub/postmate
- /**
- * The type of messages our frames our sending
- * @type {String}
- */
- export const messageType = 'application/x-postmate-v1+json'
- /**
- * The maximum number of attempts to send a handshake request to the parent
- * @type {Number}
- */
- export const maxHandshakeRequests = 5
- /**
- * A unique message ID that is used to ensure responses are sent to the correct requests
- * @type {Number}
- */
- let _messageId = 0
- /**
- * Increments and returns a message ID
- * @return {Number} A unique ID for a message
- */
- export const generateNewMessageId = () => ++_messageId
- /**
- * Postmate logging function that enables/disables via config
- */
- export const log = (...args) => Postmate.debug ? console.log(...args) : null
- /**
- * Takes a URL and returns the origin
- * @param {String} url The full URL being requested
- * @return {String} The URLs origin
- */
- export const resolveOrigin = (url) => {
- const a = document.createElement('a')
- a.href = url
- const protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol
- const host = a.host.length ? ((a.port === '80' || a.port === '443') ? a.hostname : a.host) : window.location.host
- return a.origin || `${protocol}//${host}`
- }
- const messageTypes = {
- handshake: 1,
- 'handshake-reply': 1,
- call: 1,
- emit: 1,
- reply: 1,
- request: 1,
- }
- /**
- * Ensures that a message is safe to interpret
- * @param {Object} message The postmate message being sent
- * @param {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check
- * @return {Boolean}
- */
- export const sanitize = (message, allowedOrigin) => {
- if (
- typeof allowedOrigin === 'string' &&
- message.origin !== allowedOrigin
- ) return false
- if (!message.data) return false
- if (
- typeof message.data === 'object' &&
- !('postmate' in message.data)
- ) return false
- if (message.data.type !== messageType) return false
- if (!messageTypes[message.data.postmate]) return false
- return true
- }
- /**
- * Takes a model, and searches for a value by the property
- * @param {Object} model The dictionary to search against
- * @param {String} property A path within a dictionary (i.e. 'window.location.href')
- * passed to functions in the child model
- * @return {Promise}
- */
- export const resolveValue = (model, property) => {
- const unwrappedContext = typeof model[property] === 'function'
- ? model[property]() : model[property]
- return Promise.resolve(unwrappedContext)
- }
- /**
- * Composes an API to be used by the parent
- * @param {Object} info Information on the consumer
- */
- export class ParentAPI {
- public parent: Window
- public frame: HTMLIFrameElement
- public child: Window
- public events = {}
- public childOrigin: string
- public listener: (e: any) => void
- constructor (info: Postmate) {
- this.parent = info.parent
- this.frame = info.frame
- this.child = info.child
- this.childOrigin = info.childOrigin
- if (process.env.NODE_ENV !== 'production') {
- log('Parent: Registering API')
- log('Parent: Awaiting messages...')
- }
- this.listener = (e) => {
- if (!sanitize(e, this.childOrigin)) return false
- /**
- * the assignments below ensures that e, data, and value are all defined
- */
- const { data, name } = (((e || {}).data || {}).value || {})
- if (e.data.postmate === 'emit') {
- if (process.env.NODE_ENV !== 'production') {
- log(`Parent: Received event emission: ${name}`)
- }
- if (name in this.events) {
- this.events[name].forEach(callback => {
- callback.call(this, data)
- })
- }
- }
- }
- this.parent.addEventListener('message', this.listener, false)
- if (process.env.NODE_ENV !== 'production') {
- log('Parent: Awaiting event emissions from Child')
- }
- }
- get (property) {
- return new Promise((resolve) => {
- // Extract data from response and kill listeners
- const uid = generateNewMessageId()
- const transact = (e) => {
- if (e.data.uid === uid && e.data.postmate === 'reply') {
- this.parent.removeEventListener('message', transact, false)
- resolve(e.data.value)
- }
- }
- // Prepare for response from Child...
- this.parent.addEventListener('message', transact, false)
- // Then ask child for information
- this.child.postMessage({
- postmate: 'request',
- type: messageType,
- property,
- uid,
- }, this.childOrigin)
- })
- }
- call (property, data) {
- // Send information to the child
- this.child.postMessage({
- postmate: 'call',
- type: messageType,
- property,
- data,
- }, this.childOrigin)
- }
- on (eventName, callback) {
- if (!this.events[eventName]) {
- this.events[eventName] = []
- }
- this.events[eventName].push(callback)
- }
- destroy () {
- if (process.env.NODE_ENV !== 'production') {
- log('Parent: Destroying Postmate instance')
- }
- window.removeEventListener('message', this.listener, false)
- this.frame.parentNode.removeChild(this.frame)
- }
- }
- /**
- * Composes an API to be used by the child
- * @param {Object} info Information on the consumer
- */
- export class ChildAPI {
- private model: any
- private parent: Window
- private parentOrigin: string
- private child: Window
- constructor (info: Model) {
- this.model = info.model
- this.parent = info.parent
- this.parentOrigin = info.parentOrigin
- this.child = info.child
- if (process.env.NODE_ENV !== 'production') {
- log('Child: Registering API')
- log('Child: Awaiting messages...')
- }
- this.child.addEventListener('message', (e) => {
- if (!sanitize(e, this.parentOrigin)) return
- if (process.env.NODE_ENV !== 'production') {
- log('Child: Received request', e.data)
- }
- const { property, uid, data } = e.data
- if (e.data.postmate === 'call') {
- if (property in this.model && typeof this.model[property] === 'function') {
- this.model[property](data)
- }
- return
- }
- // Reply to Parent
- resolveValue(this.model, property)
- .then(value => {
- // @ts-ignore
- e.source.postMessage({
- property,
- postmate: 'reply',
- type: messageType,
- uid,
- value,
- }, e.origin)
- })
- })
- }
- emit (name, data) {
- if (process.env.NODE_ENV !== 'production') {
- log(`Child: Emitting Event "${name}"`, data)
- }
- this.parent.postMessage({
- postmate: 'emit',
- type: messageType,
- value: {
- name,
- data,
- },
- }, this.parentOrigin)
- }
- }
- export type PostMateOptions = {
- container: HTMLElement
- url: string
- classListArray?: Array<string>
- name?: string
- model?: any
- }
- /**
- * The entry point of the Parent.
- */
- export class Postmate {
- static debug = false // eslint-disable-line no-undef
- public container?: HTMLElement
- public parent: Window
- public frame: HTMLIFrameElement
- public child?: Window
- public childOrigin?: string
- public url: string
- public model: any
- static Model: any
- /**
- * @param opts
- */
- constructor (opts: PostMateOptions) {
- this.container = opts.container
- this.url = opts.url
- this.parent = window
- this.frame = document.createElement('iframe')
- this.frame.name = opts.name || ''
- this.frame.classList.add.apply(this.frame.classList, opts.classListArray || [])
- this.container.appendChild(this.frame)
- this.child = this.frame.contentWindow
- this.model = opts.model || {}
- }
- /**
- * Begins the handshake strategy
- * @param {String} url The URL to send a handshake request to
- * @return {Promise} Promise that resolves when the handshake is complete
- */
- sendHandshake (url?: string) {
- url = url || this.url
- const childOrigin = resolveOrigin(url)
- let attempt = 0
- let responseInterval
- return new Promise((resolve, reject) => {
- const reply = (e: any) => {
- if (!sanitize(e, childOrigin)) return false
- if (e.data.postmate === 'handshake-reply') {
- clearInterval(responseInterval)
- if (process.env.NODE_ENV !== 'production') {
- log('Parent: Received handshake reply from Child')
- }
- this.parent.removeEventListener('message', reply, false)
- this.childOrigin = e.origin
- if (process.env.NODE_ENV !== 'production') {
- log('Parent: Saving Child origin', this.childOrigin)
- }
- return resolve(new ParentAPI(this))
- }
- // Might need to remove since parent might be receiving different messages
- // from different hosts
- if (process.env.NODE_ENV !== 'production') {
- log('Parent: Invalid handshake reply')
- }
- return reject('Failed handshake')
- }
- this.parent.addEventListener('message', reply, false)
- const doSend = () => {
- attempt++
- if (process.env.NODE_ENV !== 'production') {
- log(`Parent: Sending handshake attempt ${attempt}`, { childOrigin })
- }
- this.child.postMessage({
- postmate: 'handshake',
- type: messageType,
- model: this.model,
- }, childOrigin)
- if (attempt === maxHandshakeRequests) {
- clearInterval(responseInterval)
- }
- }
- const loaded = () => {
- doSend()
- responseInterval = setInterval(doSend, 500)
- }
- this.frame.addEventListener('load', loaded)
- if (process.env.NODE_ENV !== 'production') {
- log('Parent: Loading frame', { url })
- }
- this.frame.src = url
- })
- }
- }
- /**
- * The entry point of the Child
- */
- export class Model {
- public child: Window
- public model: any
- public parent: Window
- public parentOrigin: string
- /**
- * Initializes the child, model, parent, and responds to the Parents handshake
- * @param {Object} model Hash of values, functions, or promises
- * @return {Promise} The Promise that resolves when the handshake has been received
- */
- constructor (model) {
- this.child = window
- this.model = model
- this.parent = this.child.parent
- }
- /**
- * Responds to a handshake initiated by the Parent
- * @return {Promise} Resolves an object that exposes an API for the Child
- */
- sendHandshakeReply () {
- return new Promise((resolve, reject) => {
- const shake = (e) => {
- if (!e.data.postmate) {
- return
- }
- if (e.data.postmate === 'handshake') {
- if (process.env.NODE_ENV !== 'production') {
- log('Child: Received handshake from Parent')
- }
- this.child.removeEventListener('message', shake, false)
- if (process.env.NODE_ENV !== 'production') {
- log('Child: Sending handshake reply to Parent')
- }
- e.source.postMessage({
- postmate: 'handshake-reply',
- type: messageType,
- }, e.origin)
- this.parentOrigin = e.origin
- // Extend model with the one provided by the parent
- const defaults = e.data.model
- if (defaults) {
- Object.keys(defaults).forEach(key => {
- this.model[key] = defaults[key]
- })
- if (process.env.NODE_ENV !== 'production') {
- log('Child: Inherited and extended model from Parent')
- }
- }
- if (process.env.NODE_ENV !== 'production') {
- log('Child: Saving Parent origin', this.parentOrigin)
- }
- return resolve(new ChildAPI(this))
- }
- return reject('Handshake Reply Failed')
- }
- this.child.addEventListener('message', shake, false)
- })
- }
- }
|