| // Copyright 2010 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview A class that wraps several types of HTML5 message-passing |
| * entities ({@link MessagePort}s, {@link WebWorker}s, and {@link Window}s), |
| * providing a unified interface. |
| * |
| * This is tested under Chrome, Safari, and Firefox. Since Firefox 3.6 has an |
| * incomplete implementation of web workers, it doesn't support sending ports |
| * over Window connections. IE has no web worker support at all, and so is |
| * unsupported by this class. |
| * |
| */ |
| |
| goog.provide('goog.messaging.PortChannel'); |
| |
| goog.require('goog.Timer'); |
| goog.require('goog.array'); |
| goog.require('goog.async.Deferred'); |
| goog.require('goog.debug'); |
| goog.require('goog.events'); |
| goog.require('goog.events.EventType'); |
| goog.require('goog.json'); |
| goog.require('goog.log'); |
| goog.require('goog.messaging.AbstractChannel'); |
| goog.require('goog.messaging.DeferredChannel'); |
| goog.require('goog.object'); |
| goog.require('goog.string'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * A wrapper for several types of HTML5 message-passing entities |
| * ({@link MessagePort}s and {@link WebWorker}s). This class implements the |
| * {@link goog.messaging.MessageChannel} interface. |
| * |
| * This class can be used in conjunction with other communication on the port. |
| * It sets {@link goog.messaging.PortChannel.FLAG} to true on all messages it |
| * sends. |
| * |
| * @param {!MessagePort|!WebWorker} underlyingPort The message-passing |
| * entity to wrap. If this is a {@link MessagePort}, it should be started. |
| * The remote end should also be wrapped in a PortChannel. This will be |
| * disposed along with the PortChannel; this means terminating it if it's a |
| * worker or removing it from the DOM if it's an iframe. |
| * @constructor |
| * @extends {goog.messaging.AbstractChannel} |
| * @final |
| */ |
| goog.messaging.PortChannel = function(underlyingPort) { |
| goog.messaging.PortChannel.base(this, 'constructor'); |
| |
| /** |
| * The wrapped message-passing entity. |
| * @type {!MessagePort|!WebWorker} |
| * @private |
| */ |
| this.port_ = underlyingPort; |
| |
| /** |
| * The key for the event listener. |
| * @type {goog.events.Key} |
| * @private |
| */ |
| this.listenerKey_ = goog.events.listen( |
| this.port_, goog.events.EventType.MESSAGE, this.deliver_, false, this); |
| }; |
| goog.inherits(goog.messaging.PortChannel, goog.messaging.AbstractChannel); |
| |
| |
| /** |
| * Create a PortChannel that communicates with a window embedded in the current |
| * page (e.g. an iframe contentWindow). The code within the window should call |
| * {@link forGlobalWindow} to establish the connection. |
| * |
| * It's possible to use this channel in conjunction with other messages to the |
| * embedded window. However, only one PortChannel should be used for a given |
| * window at a time. |
| * |
| * @param {!Window} window The window object to communicate with. |
| * @param {string} peerOrigin The expected origin of the window. See |
| * http://dev.w3.org/html5/postmsg/#dom-window-postmessage. |
| * @param {goog.Timer=} opt_timer The timer that regulates how often the initial |
| * connection message is attempted. This will be automatically disposed once |
| * the connection is established, or when the connection is cancelled. |
| * @return {!goog.messaging.DeferredChannel} The PortChannel. Although this is |
| * not actually an instance of the PortChannel class, it will behave like |
| * one in that MessagePorts may be sent across it. The DeferredChannel may |
| * be cancelled before a connection is established in order to abort the |
| * attempt to make a connection. |
| */ |
| goog.messaging.PortChannel.forEmbeddedWindow = function( |
| window, peerOrigin, opt_timer) { |
| var timer = opt_timer || new goog.Timer(50); |
| |
| var disposeTimer = goog.partial(goog.dispose, timer); |
| var deferred = new goog.async.Deferred(disposeTimer); |
| deferred.addBoth(disposeTimer); |
| |
| timer.start(); |
| // Every tick, attempt to set up a connection by sending in one end of an |
| // HTML5 MessageChannel. If the inner window posts a response along a channel, |
| // then we'll use that channel to create the PortChannel. |
| // |
| // As per http://dev.w3.org/html5/postmsg/#ports-and-garbage-collection, any |
| // ports that are not ultimately used to set up the channel will be garbage |
| // collected (since there are no references in this context, and the remote |
| // context hasn't seen them). |
| goog.events.listen(timer, goog.Timer.TICK, function() { |
| var channel = new MessageChannel(); |
| var gotMessage = function(e) { |
| channel.port1.removeEventListener( |
| goog.events.EventType.MESSAGE, gotMessage, true); |
| // If the connection has been cancelled, don't create the channel. |
| if (!timer.isDisposed()) { |
| deferred.callback(new goog.messaging.PortChannel(channel.port1)); |
| } |
| }; |
| channel.port1.start(); |
| // Don't use goog.events because we don't want any lingering references to |
| // the ports to prevent them from getting GCed. Only modern browsers support |
| // these APIs anyway, so we don't need to worry about event API |
| // compatibility. |
| channel.port1.addEventListener( |
| goog.events.EventType.MESSAGE, gotMessage, true); |
| |
| var msg = {}; |
| msg[goog.messaging.PortChannel.FLAG] = true; |
| window.postMessage(msg, peerOrigin, [channel.port2]); |
| }); |
| |
| return new goog.messaging.DeferredChannel(deferred); |
| }; |
| |
| |
| /** |
| * Create a PortChannel that communicates with the document in which this window |
| * is embedded (e.g. within an iframe). The enclosing document should call |
| * {@link forEmbeddedWindow} to establish the connection. |
| * |
| * It's possible to use this channel in conjunction with other messages posted |
| * to the global window. However, only one PortChannel should be used for the |
| * global window at a time. |
| * |
| * @param {string} peerOrigin The expected origin of the enclosing document. See |
| * http://dev.w3.org/html5/postmsg/#dom-window-postmessage. |
| * @return {!goog.messaging.MessageChannel} The PortChannel. Although this may |
| * not actually be an instance of the PortChannel class, it will behave like |
| * one in that MessagePorts may be sent across it. |
| */ |
| goog.messaging.PortChannel.forGlobalWindow = function(peerOrigin) { |
| var deferred = new goog.async.Deferred(); |
| // Wait for the external page to post a message containing the message port |
| // which we'll use to set up the PortChannel. Ignore all other messages. Once |
| // we receive the port, notify the other end and then set up the PortChannel. |
| var key = goog.events.listen( |
| window, goog.events.EventType.MESSAGE, function(e) { |
| var browserEvent = e.getBrowserEvent(); |
| var data = browserEvent.data; |
| if (!goog.isObject(data) || !data[goog.messaging.PortChannel.FLAG]) { |
| return; |
| } |
| |
| if (peerOrigin != '*' && peerOrigin != browserEvent.origin) { |
| return; |
| } |
| |
| var port = browserEvent.ports[0]; |
| // Notify the other end of the channel that we've received our port |
| port.postMessage({}); |
| |
| port.start(); |
| deferred.callback(new goog.messaging.PortChannel(port)); |
| goog.events.unlistenByKey(key); |
| }); |
| return new goog.messaging.DeferredChannel(deferred); |
| }; |
| |
| |
| /** |
| * The flag added to messages that are sent by a PortChannel, and are meant to |
| * be handled by one on the other side. |
| * @type {string} |
| */ |
| goog.messaging.PortChannel.FLAG = '--goog.messaging.PortChannel'; |
| |
| |
| /** |
| * Whether the messages sent across the channel must be JSON-serialized. This is |
| * required for older versions of Webkit, which can only send string messages. |
| * |
| * Although Safari and Chrome have separate implementations of message passing, |
| * both of them support passing objects by Webkit 533. |
| * |
| * @type {boolean} |
| * @private |
| */ |
| goog.messaging.PortChannel.REQUIRES_SERIALIZATION_ = goog.userAgent.WEBKIT && |
| goog.string.compareVersions(goog.userAgent.VERSION, '533') < 0; |
| |
| |
| /** |
| * Logger for this class. |
| * @type {goog.log.Logger} |
| * @protected |
| * @override |
| */ |
| goog.messaging.PortChannel.prototype.logger = |
| goog.log.getLogger('goog.messaging.PortChannel'); |
| |
| |
| /** |
| * Sends a message over the channel. |
| * |
| * As an addition to the basic MessageChannel send API, PortChannels can send |
| * objects that contain MessagePorts. Note that only plain Objects and Arrays, |
| * not their subclasses, can contain MessagePorts. |
| * |
| * As per {@link http://www.w3.org/TR/html5/comms.html#clone-a-port}, once a |
| * port is copied to be sent across a channel, the original port will cease |
| * being able to send or receive messages. |
| * |
| * @override |
| * @param {string} serviceName The name of the service this message should be |
| * delivered to. |
| * @param {string|!Object|!MessagePort} payload The value of the message. May |
| * contain MessagePorts or be a MessagePort. |
| */ |
| goog.messaging.PortChannel.prototype.send = function(serviceName, payload) { |
| var ports = []; |
| payload = this.extractPorts_(ports, payload); |
| var message = {'serviceName': serviceName, 'payload': payload}; |
| message[goog.messaging.PortChannel.FLAG] = true; |
| |
| if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_) { |
| message = goog.json.serialize(message); |
| } |
| |
| this.port_.postMessage(message, ports); |
| }; |
| |
| |
| /** |
| * Delivers a message to the appropriate service handler. If this message isn't |
| * a GearsWorkerChannel message, it's ignored and passed on to other handlers. |
| * |
| * @param {goog.events.Event} e The event. |
| * @private |
| */ |
| goog.messaging.PortChannel.prototype.deliver_ = function(e) { |
| var browserEvent = e.getBrowserEvent(); |
| var data = browserEvent.data; |
| |
| if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_) { |
| try { |
| data = goog.json.parse(data); |
| } catch (error) { |
| // Ignore any non-JSON messages. |
| return; |
| } |
| } |
| |
| if (!goog.isObject(data) || !data[goog.messaging.PortChannel.FLAG]) { |
| return; |
| } |
| |
| if (this.validateMessage_(data)) { |
| var serviceName = data['serviceName']; |
| var payload = data['payload']; |
| var service = this.getService(serviceName, payload); |
| if (!service) { |
| return; |
| } |
| |
| payload = this.decodePayload( |
| serviceName, |
| this.injectPorts_(browserEvent.ports || [], payload), |
| service.objectPayload); |
| if (goog.isDefAndNotNull(payload)) { |
| service.callback(payload); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Checks whether the message is invalid in some way. |
| * |
| * @param {Object} data The contents of the message. |
| * @return {boolean} True if the message is valid, false otherwise. |
| * @private |
| */ |
| goog.messaging.PortChannel.prototype.validateMessage_ = function(data) { |
| if (!('serviceName' in data)) { |
| goog.log.warning(this.logger, |
| 'Message object doesn\'t contain service name: ' + |
| goog.debug.deepExpose(data)); |
| return false; |
| } |
| |
| if (!('payload' in data)) { |
| goog.log.warning(this.logger, |
| 'Message object doesn\'t contain payload: ' + |
| goog.debug.deepExpose(data)); |
| return false; |
| } |
| |
| return true; |
| }; |
| |
| |
| /** |
| * Extracts all MessagePort objects from a message to be sent into an array. |
| * |
| * The message ports are replaced by placeholder objects that will be replaced |
| * with the ports again on the other side of the channel. |
| * |
| * @param {Array<MessagePort>} ports The array that will contain ports |
| * extracted from the message. Will be destructively modified. Should be |
| * empty initially. |
| * @param {string|!Object} message The message from which ports will be |
| * extracted. |
| * @return {string|!Object} The message with ports extracted. |
| * @private |
| */ |
| goog.messaging.PortChannel.prototype.extractPorts_ = function(ports, message) { |
| // Can't use instanceof here because MessagePort is undefined in workers |
| if (message && |
| Object.prototype.toString.call(/** @type {!Object} */ (message)) == |
| '[object MessagePort]') { |
| ports.push(message); |
| return {'_port': {'type': 'real', 'index': ports.length - 1}}; |
| } else if (goog.isArray(message)) { |
| return goog.array.map(message, goog.bind(this.extractPorts_, this, ports)); |
| // We want to compare the exact constructor here because we only want to |
| // recurse into object literals, not native objects like Date. |
| } else if (message && message.constructor == Object) { |
| return goog.object.map(/** @type {!Object} */(message), function(val, key) { |
| val = this.extractPorts_(ports, val); |
| return key == '_port' ? {'type': 'escaped', 'val': val} : val; |
| }, this); |
| } else { |
| return message; |
| } |
| }; |
| |
| |
| /** |
| * Injects MessagePorts back into a message received from across the channel. |
| * |
| * @param {Array<MessagePort>} ports The array of ports to be injected into the |
| * message. |
| * @param {string|!Object} message The message into which the ports will be |
| * injected. |
| * @return {string|!Object} The message with ports injected. |
| * @private |
| */ |
| goog.messaging.PortChannel.prototype.injectPorts_ = function(ports, message) { |
| if (goog.isArray(message)) { |
| return goog.array.map(message, goog.bind(this.injectPorts_, this, ports)); |
| } else if (message && message.constructor == Object) { |
| message = /** @type {!Object} */ (message); |
| if (message['_port'] && message['_port']['type'] == 'real') { |
| return /** @type {!MessagePort} */ (ports[message['_port']['index']]); |
| } |
| return goog.object.map(message, function(val, key) { |
| return this.injectPorts_(ports, key == '_port' ? val['val'] : val); |
| }, this); |
| } else { |
| return message; |
| } |
| }; |
| |
| |
| /** @override */ |
| goog.messaging.PortChannel.prototype.disposeInternal = function() { |
| goog.events.unlistenByKey(this.listenerKey_); |
| // Can't use instanceof here because MessagePort is undefined in workers and |
| // in Firefox |
| if (Object.prototype.toString.call(this.port_) == '[object MessagePort]') { |
| this.port_.close(); |
| // Worker is undefined in workers as well as of Chrome 9 |
| } else if (Object.prototype.toString.call(this.port_) == '[object Worker]') { |
| this.port_.terminate(); |
| } |
| delete this.port_; |
| goog.messaging.PortChannel.base(this, 'disposeInternal'); |
| }; |