(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ var bridge = { 'service': require('../src/service'), 'client': require('../src/client'), _m: require('../src/message') }; if ((typeof define)[0] != 'u') define([], () => bridge); else self['bridge'] = bridge; },{"../src/client":2,"../src/message":4,"../src/service":6}],2:[function(require,module,exports){ 'use strict'; /** * Dependencies * @ignore */ var createPort = require('./message/port-adaptors'); var Emitter = require('./emitter'); var message = require('./message'); var uuid = require('./utils').uuid; /** * Exports * @ignore */ module.exports = Client; /** * Mini Logger * * 0: off * 1: performance * 2: console.log * * @type {Function} * @private */ var debug = { 0: () => {}, 1: arg => performance.mark(`[${self.constructor.name}][Client] - ${arg}`), 2: (arg1, ...args) => { var type = `[${self.constructor.name}][${location.pathname}]`; console.log(`[Client]${type} - "${arg1}"`, ...args); } }[0]; /** * The type environment. * @type {String} * @private */ var env = self.constructor.name; /** * A Client is a remote interface * to a Service within a given endpoint. * * See {@tutorial What's an endpoint?} * for more information on 'endpoints'. * * @example * * var endpoint = document.querySelector('iframe'); * var client = bridge.client('my-service', endpoint); * * @constructor * @param {String} service The service name to connect to * @param {(Iframe|Worker|MessagePort|BroadcastChannel|Window)} endpoint * @param {Number} [timeout] Override default response timeout * The context/thread this service can be found in. * @public */ function Client(service, endpoint, timeout) { if (!(this instanceof Client)) return new Client(service, endpoint, timeout); // Parameters can be passed as single object if (typeof service == 'object') { endpoint = service.endpoint; timeout = service.timeout; service = service.service; } this.id = uuid(); this.service = service; this.timeout = timeout; // Keep a reference to the original endpoint // so that it's not garbage collected (Workers) this.endpoint = endpoint || this.endpoint; if (!this.endpoint) throw error(1); this.setPort(this.endpoint); this.pending = new Set(); this.receiver = message.receiver(this.id) .on('_push', this.onPush.bind(this)); debug('initialized', service); } Client.prototype = { /** * Connect with the Service. Called * automatically internally, so * only required if you have * perposely called .disconnect(). * * @public */ connect() { debug('connect'); if (this.connected) return this.connected; debug('connecting...', this.service); var mc = new MessageChannel(); this.channel = mc.port1; this.channel.start(); var data = { clientId: this.id, service: this.service, originEnv: env }; return this.connected = this.message('_connect') .set('transfer', [mc.port2]) .set('data', data) .listen(mc.port1) .send() .then(response => { debug('connected', response); // Check if the response came back on // the MessageChannel. If it did then // update the endpoint so that all // subsequent messaging uses this channel. var usingChannel = response.event.target === this.channel; if (usingChannel) this.setPort(this.channel); else { this.channel.close(); delete this.channel; } // Begin listening so that Clients can // respond to service pushed messages this.receiver.listen(this.port); }) // In the event of message timeout we // upgrade the message to something more // informative. console.error() is used to // makesure the message is seen even when // the user hasn't registered a .catch() handler. .catch(err => { var msg = err && err.message; if (msg == 'timeout') { err = error(2, this.service); console.error(err.message); } throw err; }); }, /** * Disconnect from the `Service`. * * @public */ disconnect(options) { if (!this.connected) return Promise.resolve(); debug('disconnecting ...'); var config = { noRespond: options && options.noRespond, data: this.id }; this.cancelPending(); return this.message('_disconnect') .set(config) .send() .then(() => this.onDisconnected()); }, /** * Call a method on the connected Service. * * @example * * client.method('greet', 'wilson').then(result => { * console.log(result); //=> 'hello wilson' * }); * * // my-service.js: * * service.method('greet', name => { * return 'hello ' + name; * }); * * @param {String} name The method name * @param {...*} [args] Arguments to send * @return {Promise} */ method(name, ...args) { return this.connect() .then(() => { debug('method', name); return this.message('_method') .set({ recipient: this.service, data: { name: name, args: args } }) .send(); }) // Only send back the response value .then(response => response.value) // In the event of message timeout we // upgrade the message to something more // informative. console.error() is used to // make sure the message is seen even when // the user hasn't registered a .catch() handler. .catch(err => { var msg = err && err.message; if (msg == 'timeout') { err = error(3, name); console.error(err.message); } throw err; }); }, /** * Use a plugin with this Client. * See {@tutorial Writing plugins}. * * @example * * client.plugin(megaPlugin); * * @param {Function} fn The plugin * @return {this} for chaining * @public */ plugin(fn) { fn(this, { 'Emitter': Emitter, 'uuid': uuid }); return this; }, /** * A wrapper around Message that * ensures pending messages are * noted and the Client's endpoint * is predefined. * * @param {String} type The message type * @return {Message} * @private */ message(type) { debug('create message', type); var msg = message(type) .set('port', this.port) .set('timeout', this.timeout) .on('response', () => this.pending.delete(msg)) .on('cancel', () => this.pending.delete(msg)); this.pending.add(msg); return msg; }, /** * Cancel any message that we have * not recieved a response from yet. * * @private */ cancelPending() { debug('cancel pending'); this.pending.forEach(msg => { msg.cancel();}); this.pending.clear(); }, /** * Returns a Promise that resolves * once all pending messages have * responded. * * @private * @return {Promise} */ pendingResponded() { var responded = []; this.pending.forEach(msg => responded.push(msg.responded)); return Promise.all(responded); }, /** * Emits a event when a 'push' Message * is recieved from the Service. * * @private * @param {Message} message The pushed message */ onPush(message) { debug('on push', message.data); this._emit(message.data.type, message.data.data); }, // Needs testing! onDisconnected() { delete this.connected; this.pendingResponded().then(() => { debug('disconnected'); if (this.channel) this.channel.close(); this._emit('disconnected'); }); }, /** * Set the port which all messages * will be sent over. This can differ * to the endpoint if we successfully * upgrade transport to MessageChannel. * * @param {(Iframe|Worker|MessagePort|BroadcastChannel|Window)} endpoint */ setPort(endpoint) { debug('set port'); this.port = createPort(endpoint); }, /** * Destroy the Client. Waits from all * pending Messages to have responded. * * @example * * client.destroy().then(() => ...); * * @public * @return {Promise} */ destroy: function() { return this.disconnect() .then(() => { if (this.destroyed) return; debug('destroy'); this.destroyed = true; this.receiver.destroy(); this._off(); // Wipe references this.port = this.endpoint = this.receiver = null; }); }, _on: Emitter.prototype.on, _off: Emitter.prototype.off, _emit: Emitter.prototype.emit }; /** * Listen to a Service .broadcast() or .push(). * * Services get notified whenever a Client * starts listening to a particular event. * * @example * * client * .on('importantevent', data => ...) * .on('thingchanged', thing => ...); * * @param {String} name The event name * @param {Function} fn Callback function * @return {this} for chaining * @public */ Client.prototype.on = function(name, fn) { this.connect().then(() => { debug('bind on', name); Emitter.prototype.on.call(this, name, fn); this.message('_on') .set('noRespond', true) .set('data', { name: name, clientId: this.id }) .send(this.port); }); return this; }; /** * Unlisten to a Service event. * * @example * * client * .off('importantevent') // remove all * .off('thingchanged', onThingChanged); // remove one * * @this Client * @param {String} name The event name * @param {Function} fn Callback function * @return {this} for chaining * @public */ Client.prototype.off = function(name, fn) { this.connect().then(() => { Emitter.prototype.off.call(this, name, fn); this.message('_off') .set('noRespond', true) .set('data', { name: name, clientId: this.id }) .send(this.port); }); return this; }; /** * Creates new `Error` from registery. * * @param {Number} id Error Id * @return {Error} * @private */ function error(id, ...args) { /*jshint maxlen:false*/ var help = 'Either the target endpoint is not alive or the Service is not `.listen()`ing.'; return new Error({ 1: 'an endpoint must be defined', 2: `Unable to establish a connection with "${args[0]}". ${help}`, 3: `Method "${args[0]}" didn't get a response. ${help}` }[id]); } },{"./emitter":3,"./message":4,"./message/port-adaptors":5,"./utils":7}],3:[function(require,module,exports){ 'use strict'; /** * Exports * @ignore */ module.exports = Emitter; /** * Simple logger * * @type {Function} * @private */ var debug = 0 ? console.log.bind(console, '[Emitter]') : () => {}; /** * Create new `Emitter` * * @class Emitter */ function Emitter(host) { if (host) return Object.assign(host, Emitter.prototype); } Emitter.prototype = { /** * Add an event listener. * * It is possible to subscript to * events. * * @param {String} type * @param {Function} callback * @return {this} for chaining */ on: function(type, callback) { debug('on', type, callback); if (!this._callbacks) this._callbacks = {}; if (!this._callbacks[type]) this._callbacks[type] = []; this._callbacks[type].push(callback); return this; }, /** * Remove an event listener. * * @example * * emitter.off('name', fn); // remove one callback * emitter.off('name'); // remove all callbacks for 'name' * emitter.off(); // remove all callbacks * * @param {String} [type] * @param {Function} [callback] * @return {this} for chaining */ off: function(type, callback) { debug('off', type, callback); if (this._callbacks) { switch (arguments.length) { case 0: this._callbacks = {}; break; case 1: delete this._callbacks[type]; break; default: var typeListeners = this._callbacks[type]; if (!typeListeners) return; var i = typeListeners.indexOf(callback); if (~i) typeListeners.splice(i, 1); } } return this; }, /** * Emit an event. * * @example * * emitter.emit('name', { some: 'data' }); * * @param {String} type * @param {*} [data] * @return {this} for chaining */ emit: function(type, data) { debug('emit', type, data); if (this._callbacks) { var fns = this._callbacks[type] || []; fns = fns.concat(this._callbacks['*'] || []); for (var i = 0; i < fns.length; i++) fns[i].call(this, data, type); } return this; } }; var p = Emitter.prototype; p['off'] = p.off; p['on'] = p.on; },{}],4:[function(require,module,exports){ 'use strict'; /** * Dependencies * @ignore */ var createPort = require('./port-adaptors'); var Emitter = require('../emitter'); var utils = require('../utils'); var defer = utils.deferred; var uuid = utils.uuid; /** * Exports * @ignore */ exports = module.exports = type => new Message(type); exports.receiver = (id, n) => new Receiver(id, n); exports.Receiver = Receiver; exports.Message = Message; /** * Mini Logger * * 0: off * 1: performance * 2: console.log * * @type {Function} * @private */ var debug = { 0: () => {}, 1: arg => performance.mark(`[${self.constructor.name}][Message] - ${arg}`), 2: (arg1, ...args) => { var type = `[${self.constructor.name}][${location.pathname}]`; console.log(`[Message]${type} - "${arg1}"`, ...args); } }[0]; /** * Default response timeout. * * @type {Number} * @private */ var TIMEOUT = 2000; /** * Initialize a new `Message` * * @class Message * @borrows Emitter#on as #on * @borrows Emitter#off as #off * @borrows Emitter#emit as #emit * @param {String} type Message type */ function Message(type) { this.cancelled = false; this.listeners = []; this.deferred = defer(); this.listen = this.listen.bind(this); this.onMessage = this.onMessage.bind(this); this.onTimeout = this.onTimeout.bind(this); if (typeof type === 'object') this.setupInbound(type); else this.setupOutbound(type); debug('initialized', this.type); } Message.prototype = { setupOutbound(type) { this.id = uuid(); this.type = type; this.sent = false; this.recipient = '*'; }, setupInbound (e) { debug('inbound'); this.hasResponded = false; this.setSourcePort(e.source || e.target); // Keep a reference to the MessageEvent this.event = e; // Mixin the properties of the original message Object.assign(this, e.data); }, setSourcePort(endpoint) { debug('set source', endpoint.constructor.name); this.sourcePort = createPort(endpoint, { ready: true }); return this; }, set(key, value) { debug('set', key, value); if (typeof key == 'object') Object.assign(this, key); else this[key] = value; return this; }, serialize() { return { id: this.id, type: this.type, data: this.data, recipient: this.recipient, noRespond: this.noRespond }; }, preventDefault() { debug('prevent default'); this.defaultPrevented = true; }, /** * Send the message to an endpoint. * * @param {(Iframe|Window|Worker|MessagePort)} endpoint * @return {Promise} */ send(endpoint) { debug('send', this.type); if (this.sent) throw error(1); var serialized = this.serialize(); var expectsResponse = !this.noRespond; // A port is resolved from either a predefined // port, or an endpoint given as first argument this.port = endpoint ? createPort(endpoint) : this.port; if (!this.port) throw error(3); // If we're expecting a response listen // on the port else resolve promise instantly if (expectsResponse) { this.listen(this.port); this.setResponseTimeout(); } else this.deferred.resolve(); this.port.postMessage(serialized, this.getTransfer()); debug('sent', serialized); return this.deferred.promise; }, /** * Set the response timeout. * * When set to `false` no timeout * is installed. * * @private */ setResponseTimeout() { if (this.timeout === false) return; var ms = this.timeout || TIMEOUT; this._timer = setTimeout(this.onTimeout, ms); }, /** * Clear the response timeout. * * @private */ clearResponseTimeout() { clearTimeout(this._timer); }, getTransfer() { return this.transfer || this.event && this.event.ports; }, onMessage(e) { var valid = !!e.data.response && e.data.id === this.id && !this.cancelled; if (valid) this.onResponse(e); }, onTimeout() { debug('response timeout', this.type); if (!this.silentTimeout) this.deferred.reject(error(4)); this.teardown(); }, listen(thing) { debug('add response listener', thing); var port = createPort(thing); port.addListener(this.onMessage, this.listen); this.listeners.push(port); return this; }, unlisten() { debug('remove response listeners'); this.listeners.forEach(port => port.removeListener(this.onMessage)); this.listeners = []; }, /** * Cancel a pending Message. * * @example * * var msg = message('foo') * * msg.send(new Worker('my-worker.js')) * .then(response => { * // this will never run * }) * * msg.cancel(); * * @public */ cancel() { this.teardown(); this.cancelled = true; this.emit('cancel'); }, teardown() { this.clearResponseTimeout(); this.unlisten(); }, /** * Respond to a message. * * @example * * receiver.on('hello', message => { * message.respond('world'); * }); * * @public * @param {*} [result] Data to send back with the response */ respond(result) { debug('respond', result, this.id); if (this.hasResponded) throw error(2); if (!this.sourcePort) return; if (this.noRespond) return; this.hasResponded = true; var self = this; // Reject when result is an `Error` if (this.error) reject(this.error); // Call the handler and make // sure return value is a promise. // If the returned value is unclonable // then the send() method will throw, // the .catch() will reject in this case. Promise.resolve(result) .then(resolve, reject) .catch(reject); function resolve(value) { debug('resolve', value); respond({ type: 'resolve', value: value }); } function reject(err) { var serialized = serializeError(err); debug('reject', serialized); respond({ type: 'reject', value: serialized }); } function respond(response) { self.response = response; self.sourcePort.postMessage({ id: self.id, response: response }, self.transfer); debug('responded with:', response); } }, /** * Forward a `Message` onto another endpoint. * * The `silentTrue` option prevents the * message request timing out should * the response come back via an * alternative route. * * TODO: If forwarded message errors * check it reaches origin (#86). * * @param {(HTMLIframeElement|MessagePort|Window)} endpoint * @public */ forward(endpoint) { debug('forward'); return this .set('silentTimeout', true) .send(endpoint) .then(result => this.respond(result.value)); }, onResponse(e) { debug('on response', e.data); var response = e.data.response; var type = response.type; var value = type == 'reject' ? response.value : response; response.event = e; this.response = response; this.teardown(); this.deferred[this.response.type](value); this.emit('response', response); } }; // Mixin Emitter methods Emitter(Message.prototype); /** * Initialize a new `Receiver`. * * @class Receiver * @extends Emitter * @param {String} name - corresponds to `Message.recipient` */ function Receiver(name) { this.name = name; this.ports = new Set(); this.onMessage = this.onMessage.bind(this); this.listen = this.listen.bind(this); this.unlisten = this.unlisten.bind(this); debug('receiver initialized', name); } Receiver.prototype = { /** * Begin listening for inbound messages. * * @example * * // When no arguments are given * // messages will be listened for * // on the default global scope * .listen(); * * // When an endpoint is out of reach * // BroadcastChannel can be used. * .listen(new BroadcastChannel('foo')); * * @param {(HTMLIframeElement|Worker|MessagePort| * BroadcastChannel|Window|Object)} [thing] * @public */ listen(thing) { debug('listen'); var _port = createPort(thing || self, { receiver: true }); if (this.ports.has(_port)) return; _port.addListener(this.onMessage, this.listen); this.ports.add(_port); return this; }, /** * Stop listening for inbound messages * on all endpoints listened to prior. * * @public */ unlisten() { debug('unlisten'); this.ports.forEach(port => port.removeListener(this.onMessage)); }, /** * Callback to handle inbound messages. * * @param {MessageEvent} e * @private */ onMessage(e) { if (!e.data.id) return; if (!e.data.type) return; if (!this.isRecipient(e.data.recipient)) return; debug('receiver on message', e.data); var message = new Message(e); // Before hook this.emit('message', message); if (message.defaultPrevented) return; try { this.emit(message.type, message); } catch (e) { message.error = e; message.respond(); throw e; } }, isRecipient(recipient) { return recipient == this.name || recipient == '*' || this.name == '*'; }, destroy: function() { this.unlisten(); delete this.name; return this; } }; // Mixin Emitter methods Emitter(Receiver.prototype); /** * Error object can't be sent via * .postMessage() so we have to * serialize them into an error-like * Object that can be sent. * * @param {*} err * @return {(Object|*)} * @private */ function serializeError(err) { switch (err && err.constructor.name) { case 'DOMException': case 'Error': return { message: err.message }; case 'DOMError': return { message: err.message, name: err.name }; default: return err; } } /** * Creates new `Error` from registry. * * @param {Number} id Error Id * @return {Error} * @private */ function error(id, ...args) { return new Error({ 1: '.send() can only be called once', 2: 'response already sent for this message', 3: 'a port must be defined', 4: 'timeout' }[id]); } },{"../emitter":3,"../utils":7,"./port-adaptors":5}],5:[function(require,module,exports){ 'use strict'; /** * Dependencies * @ignore */ var deferred = require('../utils').deferred; /** * Message event name * @type {String} */ const MSG = 'message'; /** * Mini Logger * @type {Function} * @private */ var debug = 0 ? function(arg1, ...args) { var type = `[${self.constructor.name}][${location.pathname}]`; console.log(`[PortAdaptor]${type} - "${arg1}"`, ...args); } : () => {}; /** * Creates a bridge.js port abstraction * with a consistent interface. * * @param {Object} target * @param {Object} options * @return {PortAdaptor} */ module.exports = function create(target, options) { if (!target) throw error(1); if (isAdaptor(target)) return target; var type = target.constructor.name; var CustomAdaptor = adaptors[type]; debug('creating port adaptor for', type); if (CustomAdaptor) return CustomAdaptor(target, options); return new PortAdaptor(target, options); }; /** * The default adaptor. * @private */ function PortAdaptor(target) { debug('PortAdaptor'); this.target = target; } var PortAdaptorProto = PortAdaptor.prototype = { constructor: PortAdaptor, addListener(callback) { on(this.target, MSG, callback); }, removeListener(callback) { off(this.target, MSG, callback); }, postMessage(data, transfer) { this.target.postMessage(data, transfer); } }; /** * A registry of specific adaptors * for when the default PortAdaptor * is not suitable. * * @type {Object} */ var adaptors = { /** * Create an HTMLIframeElement PortAdaptor. * * @param {HTMLIframeElement} iframe */ 'HTMLIFrameElement': function(iframe) { debug('HTMLIFrameElement'); var ready = windowReady(iframe); return { addListener(callback) { on(window, MSG, callback); }, removeListener(callback) { off(window, MSG, callback); }, postMessage(data, transfer) { ready.then(() => postMessageSync(iframe.contentWindow, data, transfer)); } }; }, /** * Create a BroadcastChannel port-adaptor. * * @param {Object} channel * @param {[type]} options [description] */ 'BroadcastChannel': function(channel, options) { debug('BroadcastChannel', channel.name); var receiver = options && options.receiver; var ready = options && options.ready; var sendReady = () => { channel.postMessage('ready'); debug('sent ready'); }; ready = ready || receiver ? Promise.resolve() : setupSender(); if (receiver) { sendReady(); on(channel, MSG, e => { if (e.data != 'ready?') return; sendReady(); }); } function setupSender() { debug('setup sender'); var promise = deferred(); channel.postMessage('ready?'); on(channel, MSG, function fn(e) { if (e.data != 'ready') return; off(channel, MSG, fn); debug('BroadcastChannel: ready'); promise.resolve(); }); return promise.promise; } return { target: channel, addListener: PortAdaptorProto.addListener, removeListener: PortAdaptorProto.removeListener, postMessage(data, transfer) { ready.then(() => channel.postMessage(data, transfer)); } }; }, 'Window': function(win, options) { debug('Window'); var ready = options && options.ready || win === parent // parent always ready || win === self; // self always ready ready = ready ? Promise.resolve() : windowReady(win); return { addListener(callback) { on(window, MSG, callback); }, removeListener(callback) { off(window, MSG, callback); }, postMessage(data, transfer) { ready.then(() => postMessageSync(win, data, transfer)); } }; }, 'SharedWorker': function(worker) { worker.port.start(); return new PortAdaptor(worker.port); }, 'SharedWorkerGlobalScope': function() { var ports = []; return { postMessage() {}, // noop addListener(callback, listen) { this.onconnect = e => { var port = e.ports[0]; ports.push(port); port.start(); listen(port); }; on(self, 'connect', this.onconnect); }, removeListener(callback) { off(self, 'connect', this.onconnect); ports.forEach(port => { port.close(); port.removeEventListener(MSG, callback); }); } }; } }; /** * Return a Promise that resolves * when a Window is ready to start * receiving messages. * * @param {Window} target * @return {Promise} */ var windowReady = (function() { if (typeof window == 'undefined') return; var parent = window.opener || window.parent; var domReady = 'DOMContentLoaded'; var windows = new WeakSet(); // Side B: Dispatches 'load' // from the child window if (parent != self) { if (document.readyState === 'loading') { on(window, domReady, function fn() { off(window, domReady, fn); postMessageSync(parent, 'load'); }); } else { postMessageSync(parent, 'load'); } } // Side A: Listens for 'ready' in the parent window on(self, 'message', e => e.data == 'load' && windows.add(e.source)); return target => { var win = target.contentWindow || target; // Ready if the target has previously announces itself ready if (windows.has(win)) return Promise.resolve(); // Ready if the target is the parent window if (win == window.parent) return Promise.resolve(); var def = deferred(); debug('waiting for Window to be ready ...'); on(window, 'message', function fn(e) { if (e.data == 'load' && e.source == win) { debug('Window ready'); off(window, 'message', fn); def.resolve(); } }); return def.promise; }; })(); /** * Utils * @ignore */ function isAdaptor(thing) { return !!(thing && thing.addListener); } // Shorthand function on(target, name, fn) { target.addEventListener(name, fn); } function off(target, name, fn) { target.removeEventListener(name, fn); } /** * Dispatches syncronous 'message' * event on a Window. * * We use this because standard * window.postMessage() gets blocked * until the main-thread is free. * * @param {Window} win * @param {*} data * @private */ function postMessageSync(win, data, transfer) { var event = { data: data, source: self }; if (transfer) event.ports = transfer; win.dispatchEvent(new MessageEvent('message', event)); } /** * Creates new `Error` from registery. * * @param {Number} id Error Id * @return {Error} * @private */ function error(id) { return new Error({ 1: 'target is undefined' }[id]); } },{"../utils":7}],6:[function(require,module,exports){ 'use strict'; /** * Dependencies * @ignore */ var uuid = require('./utils').uuid; var message = require('./message'); var Receiver = message.Receiver; /** * Exports * @ignore */ module.exports = Service; /** * Debug logger * * 0: off * 1: performance * 2: console.log * * @type {Function} * @private */ var debug = { 0: () => {}, 1: arg => performance.mark(`[${self.constructor.name}][Service] - ${arg}`), 2: (arg1, ...args) => { var type = `[${self.constructor.name}][${location.pathname}]`; console.log(`[Service]${type} - "${arg1}"`, ...args); } }[0]; /** * Extends `Receiver` * @ignore */ Service.prototype = Object.create(Receiver.prototype); /** * A `Service` is a collection of methods * exposed to a `Client`. Methods can be * sync or async (using Promises). * * @example * * bridge.service('my-service') * .method('ping', param => 'pong: ' + param) * .listen(); * * @class Service * @extends Receiver * @param {String} name The service name * @public */ function Service(name) { if (!(this instanceof Service)) return new Service(name); message.Receiver.call(this, name); // call super this.clients = {}; this.methods = {}; this .on('_disconnect', this.onDisconnect.bind(this)) .on('_connect', this.onConnect.bind(this)) .on('_method', this.onMethod.bind(this)) .on('_off', this.onOff.bind(this)) .on('_on', this.onOn.bind(this)); this.destroy = this.destroy.bind(this); debug('initialized', name); } Service.prototype.inWindow = constructor.name === 'Window'; /** * Define a method to expose to Clients. * The return value of the result of a * returned Promise will be sent back * to the Client. * * @example * * bridge.service('my-service') * * // sync return value * .method('myMethod', function(param) { * return 'hello: ' + param; * }) * * // or async Promise * .method('myOtherMethod', function() { * return new Promise(resolve => { * setTimeout(() => resolve('result'), 1000); * }); * }) * * .listen(); * * @param {String} name * @param {Function} fn * @return {this} for chaining */ Service.prototype.method = function(name, fn) { this.methods[name] = fn; return this; }; /** * Broadcast's an event from a `Service` * to connected `Client`s. * * The third argument can be used to * target selected clients by their * `client.id`. * * @example * * service.broadcast('my-event', { some: data }); // all clients * service.broadcast('my-event', { some: data }, [ clientId ]); // one client * * @memberof Service * @param {String} type The message type/name * @param {*} [data] Data to send with the event * @param {Array} [only] A select list of clients to message * @return {this} */ Service.prototype.broadcast = function(type, data, only) { debug('broadcast', type, data, only); this.eachClient(client => { if (only && !~only.indexOf(client.id)) return; debug('broadcasting to', client.id); this.push(type, data, client.id, { noRespond: true }); }); return this; }; /** * Push message to a single connected Client. * * @example * * client.on('my-event', data => ...) * * ... * * service.push('my-event', { some: data}, clientId) * .then(() => console.log('sent')); * * @public * @param {String} type * @param {Object} data * @param {String} clientId The Id of the Client to push to * @param {Object} options Optional parameters * @param {Boolean} options.noResponse Tell the Client not to respond * (Promise resolves instantly) * @return {Promise} */ Service.prototype.push = function(type, data, clientId, options) { var noRespond = options && options.noRespond; var client = this.getClient(clientId); return message('_push') .set({ recipient: clientId, noRespond: noRespond, data: { type: type, data: data } }).send(client.port); }; Service.prototype.eachClient = function(fn) { for (var id in this.clients) fn(this.clients[id]); }; Service.prototype.getClient = function(id) { return this.clients[id]; }; /** * @fires Service#before-connect * @fires Service#connected * @param {Message} message * @private */ Service.prototype.onConnect = function(message) { debug('connection attempt', message.data, this.name); var data = message.data; var clientId = data.clientId; if (!clientId) return; if (data.service !== this.name) return; if (this.clients[clientId]) return; // before hook this.emit('before-connect', message); if (message.defaultPrevented) return; this.upgradeChannel(message); this.addClient(clientId, message.sourcePort); message.respond(); this.emit('connected', clientId); debug('connected', clientId); }; /** * When a Client attempt to connect we * can sometimes upgrade the to a direct * MessageChannel 'pipe' to prevent * hopping threads. * * We only do this if both: * * A. `MessagePort` was supplied with the 'connect' event. * B. The Client and Service are not both in `Window` contexts * (it's faster to use sync messaging window -> window). * * @param {Message} message the 'connect' message * @private */ Service.prototype.upgradeChannel = function(message) { if (this.inWindow && message.data.originEnv === 'Window') return; var ports = message.event.ports; var channel = ports && ports[0]; if (channel) { message.setSourcePort(channel); this.listen(channel); channel.start(); } debug('channel upgraded'); }; /** * @fires Service#before-disconnect * @fires Service#disconnected * @param {Message} message * @private */ Service.prototype.onDisconnect = function(message) { var client = this.clients[message.data]; if (!client) return; // before hook this.emit('before-disconnect', message); if (message.defaultPrevented) return; this.removeClient(client.id); message.respond(); this.emit('disconnected', client.id); debug('disconnected', client.id); }; /** * @fires Service#before-method * @param {Message} message * @private */ Service.prototype.onMethod = function(message) { debug('on method', message.data); this.emit('before-method', message); if (message.defaultPrevented) return; var method = message.data; var name = method.name; var fn = this.methods[name]; var result; if (!fn) throw error(4, name); try { result = fn.apply(this, method.args); } catch (err) { message.error = err; } message.respond(result); }; /** * @fires Service#on * @param {Message} message * @private */ Service.prototype.onOn = function(message) { debug('on on', message.data); this.emit('on', message.data); }; /** * @fires Service#off * @param {Message} message * @private */ Service.prototype.onOff = function(message) { debug('on off'); this.emit('off', message.data); }; Service.prototype.addClient = function(id, port) { this.clients[id] = { id: id, port: port }; }; Service.prototype.removeClient = function(id) { delete this.clients[id]; }; /** * Use a plugin with this Service. * @param {Function} fn Plugin function * @return {this} for chaining * @public */ Service.prototype.plugin = function(fn) { fn(this, { 'uuid': uuid }); return this; }; /** * Disconnect a Client from the Service. * @param {Object} client * @private */ Service.prototype.disconnect = function(client) { this.removeClient(client.id); message('disconnect') .set({ recipient: client.id, noRespond: true }) .send(client.port); }; /** * Destroy the Service. * @public */ Service.prototype.destroy = function() { delete this.clients; this.unlisten(); this.off(); }; var sp = Service.prototype; sp['broadcast'] = sp.broadcast; sp['destroy'] = sp.destroy; sp['method'] = sp.method; sp['plugin'] = sp.plugin; /** * Creates new `Error` from registery. * * @param {Number} id Error Id * @return {Error} * @private */ function error(id) { var args = [].slice.call(arguments, 1); return new Error({ 4: 'method "' + args[0] + '" doesn\'t exist' }[id]); } /** * Fires before the default 'connect' logic. * This event acts as a hook for plugin authors * to override default 'connect' behaviour. * * @example * * service.on('before-connect', message => { * message.preventDefault(); * // alternative connection logic ... * }); * * @event Service#before-connect * @param {Message} message - The connect message */ /** * Signals that a Client has connected. * * @example * * service.on('connected', clientId => { * console.log('client (%s) has connected', clientId); * }); * * @event Service#connected * @param {String} clientId - The id of the connected Client */ /** * Fires before the default 'disconnect' logic. * This event acts as a hook for plugin authors * to override default 'disconnect' behaviour. * * @example * * service.on('before-disconnect', message => { * message.preventDefault(); * // alternative disconnection logic ... * }); * * @event Service#before-disconnect * @param {Message} message - The disconnect message */ /** * Signals that a Client has disconnected. * * @example * * service.on('disconnected', clientId => { * console.log('client (%s) has disconnected', clientId); * }); * * @event Service#disconnected * @param {String} clientId - The id of the disconnected Client */ /** * Signals that a Client has begun * listening to a broadcast event. * * @example * * service.on('on', data => { * console.log('client (%s) is listening to %s', data.clientId, data.name); * }); * * @event Service#on * @type {Object} * @property {String} name - The broadcast name * @property {String} clientId - The id of the Client that started listening */ /** * Signals that a Client has stopped * listening to a broadcast event. * * @example * * service.on('off', data => { * console.log('client (%s) stopped listening to %s', data.clientId, data.name); * }); * * @event Service#off * @param {Object} data * @param {String} data.name - The broadcast name * @param {String} data.clientId - The id of the Client that stopped listening */ },{"./message":4,"./utils":7}],7:[function(require,module,exports){ 'use strict'; /** * Create a UUID string. * * http://jsperf.com/guid-generation-stackoverflow * * @return {String} */ exports.uuid = function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); }; exports.deferred = function() { var promise = {}; promise.promise = new Promise((resolve, reject) => { promise.resolve = resolve; promise.reject = reject; }); return promise; }; },{}]},{},[1]);