/** * This module provides a simple RPC mechanism for cross-document * (iframe / window.postMessage) communication. */ // Since `Symbol` is not clonable, we use a UUID to identify RPCs. const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6'; /** * The CallbackManager is used to manage callbacks for RPCs. * It is used by the dehydrator and hydrator to store and retrieve * the functions that are being called remotely. */ export class CallbackManager { #messageId = 0; constructor () { this.callbacks = new Map(); } register_callback (callback) { const id = this.#messageId++; this.callbacks.set(id, callback); return id; } attach_to_source (source) { source.addEventListener('message', event => { const { data } = event; console.log( 'test-app got message from window', data, ); debugger; if (data && typeof data === 'object' && data.$SCOPE === $SCOPE) { const { id, args } = data; const callback = this.callbacks.get(id); if (callback) { callback(...args); } } }); } } /** * The dehydrator replaces functions in an object with identifiers, * so that hydrate() can be called on the other side of the frame * to bind RPC stubs. The original functions are stored in a map * so that they can be called when the RPC is invoked. */ export class Dehydrator { constructor ({ callbackManager }) { this.callbackManager = callbackManager; } dehydrate (value) { return this.dehydrate_value_(value); } dehydrate_value_ (value) { if (typeof value === 'function') { const id = this.callbackManager.register_callback(value); return { $SCOPE, id }; } else if (Array.isArray(value)) { return value.map(this.dehydrate_value_.bind(this)); } else if (typeof value === 'object' && value !== null) { const result = {}; for (const key in value) { result[key] = this.dehydrate_value_(value[key]); } return result; } else { return value; } } } /** * The hydrator binds RPC stubs to the functions that were * previously dehydrated. This allows the RPC to be invoked * on the other side of the frame. */ export class Hydrator { constructor ({ target }) { this.target = target; } hydrate (value) { return this.hydrate_value_(value); } hydrate_value_ (value) { if ( value && typeof value === 'object' && value.$SCOPE === $SCOPE ) { const { id } = value; return (...args) => { console.log('sending message', { $SCOPE, id, args }); console.log('target', this.target); this.target.postMessage({ $SCOPE, id, args }, '*'); }; } else if (Array.isArray(value)) { return value.map(this.hydrate_value_.bind(this)); } else if (typeof value === 'object' && value !== null) { const result = {}; for (const key in value) { result[key] = this.hydrate_value_(value[key]); } return result; } return value; } }