Path: blob/trunk/third_party/closure/goog/messaging/portchannel.js
2868 views
// Copyright 2010 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview A class that wraps several types of HTML5 message-passing16* entities ({@link MessagePort}s, {@link WebWorker}s, and {@link Window}s),17* providing a unified interface.18*19* This is tested under Chrome, Safari, and Firefox. Since Firefox 3.6 has an20* incomplete implementation of web workers, it doesn't support sending ports21* over Window connections. IE has no web worker support at all, and so is22* unsupported by this class.23*24*/2526goog.provide('goog.messaging.PortChannel');2728goog.require('goog.Timer');29goog.require('goog.array');30goog.require('goog.async.Deferred');31goog.require('goog.debug');32goog.require('goog.events');33goog.require('goog.events.EventType');34goog.require('goog.json');35goog.require('goog.log');36goog.require('goog.messaging.AbstractChannel');37goog.require('goog.messaging.DeferredChannel');38goog.require('goog.object');39goog.require('goog.string');40goog.require('goog.userAgent');41424344/**45* A wrapper for several types of HTML5 message-passing entities46* ({@link MessagePort}s and {@link WebWorker}s). This class implements the47* {@link goog.messaging.MessageChannel} interface.48*49* This class can be used in conjunction with other communication on the port.50* It sets {@link goog.messaging.PortChannel.FLAG} to true on all messages it51* sends.52*53* @param {!MessagePort|!WebWorker} underlyingPort The message-passing54* entity to wrap. If this is a {@link MessagePort}, it should be started.55* The remote end should also be wrapped in a PortChannel. This will be56* disposed along with the PortChannel; this means terminating it if it's a57* worker or removing it from the DOM if it's an iframe.58* @constructor59* @extends {goog.messaging.AbstractChannel}60* @final61*/62goog.messaging.PortChannel = function(underlyingPort) {63goog.messaging.PortChannel.base(this, 'constructor');6465/**66* The wrapped message-passing entity.67* @type {!MessagePort|!WebWorker}68* @private69*/70this.port_ = underlyingPort;7172/**73* The key for the event listener.74* @type {goog.events.Key}75* @private76*/77this.listenerKey_ = goog.events.listen(78this.port_, goog.events.EventType.MESSAGE, this.deliver_, false, this);79};80goog.inherits(goog.messaging.PortChannel, goog.messaging.AbstractChannel);818283/**84* Create a PortChannel that communicates with a window embedded in the current85* page (e.g. an iframe contentWindow). The code within the window should call86* {@link forGlobalWindow} to establish the connection.87*88* It's possible to use this channel in conjunction with other messages to the89* embedded window. However, only one PortChannel should be used for a given90* window at a time.91*92* @param {!Window} peerWindow The window object to communicate with.93* @param {string} peerOrigin The expected origin of the window. See94* http://dev.w3.org/html5/postmsg/#dom-window-postmessage.95* @param {goog.Timer=} opt_timer The timer that regulates how often the initial96* connection message is attempted. This will be automatically disposed once97* the connection is established, or when the connection is cancelled.98* @return {!goog.messaging.DeferredChannel} The PortChannel. Although this is99* not actually an instance of the PortChannel class, it will behave like100* one in that MessagePorts may be sent across it. The DeferredChannel may101* be cancelled before a connection is established in order to abort the102* attempt to make a connection.103*/104goog.messaging.PortChannel.forEmbeddedWindow = function(105peerWindow, peerOrigin, opt_timer) {106if (peerOrigin == '*') {107return new goog.messaging.DeferredChannel(108goog.async.Deferred.fail(new Error('Invalid origin')));109}110111var timer = opt_timer || new goog.Timer(50);112113var disposeTimer = goog.partial(goog.dispose, timer);114var deferred = new goog.async.Deferred(disposeTimer);115deferred.addBoth(disposeTimer);116117timer.start();118// Every tick, attempt to set up a connection by sending in one end of an119// HTML5 MessageChannel. If the inner window posts a response along a channel,120// then we'll use that channel to create the PortChannel.121//122// As per http://dev.w3.org/html5/postmsg/#ports-and-garbage-collection, any123// ports that are not ultimately used to set up the channel will be garbage124// collected (since there are no references in this context, and the remote125// context hasn't seen them).126goog.events.listen(timer, goog.Timer.TICK, function() {127var channel = new MessageChannel();128var gotMessage = function(e) {129channel.port1.removeEventListener(130goog.events.EventType.MESSAGE, gotMessage, true);131// If the connection has been cancelled, don't create the channel.132if (!timer.isDisposed()) {133deferred.callback(new goog.messaging.PortChannel(channel.port1));134}135};136channel.port1.start();137// Don't use goog.events because we don't want any lingering references to138// the ports to prevent them from getting GCed. Only modern browsers support139// these APIs anyway, so we don't need to worry about event API140// compatibility.141channel.port1.addEventListener(142goog.events.EventType.MESSAGE, gotMessage, true);143144var msg = {};145msg[goog.messaging.PortChannel.FLAG] = true;146peerWindow.postMessage(msg, peerOrigin, [channel.port2]);147});148149return new goog.messaging.DeferredChannel(deferred);150};151152153/**154* Create a PortChannel that communicates with the document in which this window155* is embedded (e.g. within an iframe). The enclosing document should call156* {@link forEmbeddedWindow} to establish the connection.157*158* It's possible to use this channel in conjunction with other messages posted159* to the global window. However, only one PortChannel should be used for the160* global window at a time.161*162* @param {string} peerOrigin The expected origin of the enclosing document. See163* http://dev.w3.org/html5/postmsg/#dom-window-postmessage.164* @return {!goog.messaging.MessageChannel} The PortChannel. Although this may165* not actually be an instance of the PortChannel class, it will behave like166* one in that MessagePorts may be sent across it.167*/168goog.messaging.PortChannel.forGlobalWindow = function(peerOrigin) {169if (peerOrigin == '*') {170return new goog.messaging.DeferredChannel(171goog.async.Deferred.fail(new Error('Invalid origin')));172}173174var deferred = new goog.async.Deferred();175// Wait for the external page to post a message containing the message port176// which we'll use to set up the PortChannel. Ignore all other messages. Once177// we receive the port, notify the other end and then set up the PortChannel.178var key =179goog.events.listen(window, goog.events.EventType.MESSAGE, function(e) {180var browserEvent = e.getBrowserEvent();181var data = browserEvent.data;182if (!goog.isObject(data) || !data[goog.messaging.PortChannel.FLAG]) {183return;184}185186if (window.parent != browserEvent.source ||187peerOrigin != browserEvent.origin) {188return;189}190191var port = browserEvent.ports[0];192// Notify the other end of the channel that we've received our port193port.postMessage({});194195port.start();196deferred.callback(new goog.messaging.PortChannel(port));197goog.events.unlistenByKey(key);198});199return new goog.messaging.DeferredChannel(deferred);200};201202203/**204* The flag added to messages that are sent by a PortChannel, and are meant to205* be handled by one on the other side.206* @type {string}207*/208goog.messaging.PortChannel.FLAG = '--goog.messaging.PortChannel';209210211/**212* Whether the messages sent across the channel must be JSON-serialized. This is213* required for older versions of Webkit, which can only send string messages.214*215* Although Safari and Chrome have separate implementations of message passing,216* both of them support passing objects by Webkit 533.217*218* @type {boolean}219* @private220*/221goog.messaging.PortChannel.REQUIRES_SERIALIZATION_ = goog.userAgent.WEBKIT &&222goog.string.compareVersions(goog.userAgent.VERSION, '533') < 0;223224225/**226* Logger for this class.227* @type {goog.log.Logger}228* @protected229* @override230*/231goog.messaging.PortChannel.prototype.logger =232goog.log.getLogger('goog.messaging.PortChannel');233234235/**236* Sends a message over the channel.237*238* As an addition to the basic MessageChannel send API, PortChannels can send239* objects that contain MessagePorts. Note that only plain Objects and Arrays,240* not their subclasses, can contain MessagePorts.241*242* As per {@link http://www.w3.org/TR/html5/comms.html#clone-a-port}, once a243* port is copied to be sent across a channel, the original port will cease244* being able to send or receive messages.245*246* @override247* @param {string} serviceName The name of the service this message should be248* delivered to.249* @param {string|!Object|!MessagePort} payload The value of the message. May250* contain MessagePorts or be a MessagePort.251*/252goog.messaging.PortChannel.prototype.send = function(serviceName, payload) {253var ports = [];254payload = this.extractPorts_(ports, payload);255var message = {'serviceName': serviceName, 'payload': payload};256message[goog.messaging.PortChannel.FLAG] = true;257258if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_) {259message = goog.json.serialize(message);260}261262this.port_.postMessage(message, ports);263};264265266/**267* Delivers a message to the appropriate service handler. If this message isn't268* a GearsWorkerChannel message, it's ignored and passed on to other handlers.269*270* @param {goog.events.Event} e The event.271* @private272*/273goog.messaging.PortChannel.prototype.deliver_ = function(e) {274var browserEvent = e.getBrowserEvent();275var data = browserEvent.data;276277if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_) {278try {279data = goog.json.parse(data);280} catch (error) {281// Ignore any non-JSON messages.282return;283}284}285286if (!goog.isObject(data) || !data[goog.messaging.PortChannel.FLAG]) {287return;288}289290if (this.validateMessage_(data)) {291var serviceName = data['serviceName'];292var payload = data['payload'];293var service = this.getService(serviceName, payload);294if (!service) {295return;296}297298payload = this.decodePayload(299serviceName, this.injectPorts_(browserEvent.ports || [], payload),300service.objectPayload);301if (goog.isDefAndNotNull(payload)) {302service.callback(payload);303}304}305};306307308/**309* Checks whether the message is invalid in some way.310*311* @param {Object} data The contents of the message.312* @return {boolean} True if the message is valid, false otherwise.313* @private314*/315goog.messaging.PortChannel.prototype.validateMessage_ = function(data) {316if (!('serviceName' in data)) {317goog.log.warning(318this.logger, 'Message object doesn\'t contain service name: ' +319goog.debug.deepExpose(data));320return false;321}322323if (!('payload' in data)) {324goog.log.warning(325this.logger, 'Message object doesn\'t contain payload: ' +326goog.debug.deepExpose(data));327return false;328}329330return true;331};332333334/**335* Extracts all MessagePort objects from a message to be sent into an array.336*337* The message ports are replaced by placeholder objects that will be replaced338* with the ports again on the other side of the channel.339*340* @param {Array<MessagePort>} ports The array that will contain ports341* extracted from the message. Will be destructively modified. Should be342* empty initially.343* @param {string|!Object} message The message from which ports will be344* extracted.345* @return {string|!Object} The message with ports extracted.346* @private347*/348goog.messaging.PortChannel.prototype.extractPorts_ = function(ports, message) {349// Can't use instanceof here because MessagePort is undefined in workers350if (message &&351Object.prototype.toString.call(/** @type {!Object} */ (message)) ==352'[object MessagePort]') {353ports.push(/** @type {MessagePort} */ (message));354return {'_port': {'type': 'real', 'index': ports.length - 1}};355} else if (goog.isArray(message)) {356return goog.array.map(message, goog.bind(this.extractPorts_, this, ports));357// We want to compare the exact constructor here because we only want to358// recurse into object literals, not native objects like Date.359} else if (message && message.constructor == Object) {360return goog.object.map(361/** @type {!Object} */ (message), function(val, key) {362val = this.extractPorts_(ports, val);363return key == '_port' ? {'type': 'escaped', 'val': val} : val;364}, this);365} else {366return message;367}368};369370371/**372* Injects MessagePorts back into a message received from across the channel.373*374* @param {Array<MessagePort>} ports The array of ports to be injected into the375* message.376* @param {string|!Object} message The message into which the ports will be377* injected.378* @return {string|!Object} The message with ports injected.379* @private380*/381goog.messaging.PortChannel.prototype.injectPorts_ = function(ports, message) {382if (goog.isArray(message)) {383return goog.array.map(message, goog.bind(this.injectPorts_, this, ports));384} else if (message && message.constructor == Object) {385message = /** @type {!Object} */ (message);386if (message['_port'] && message['_port']['type'] == 'real') {387return /** @type {!MessagePort} */ (ports[message['_port']['index']]);388}389return goog.object.map(message, function(val, key) {390return this.injectPorts_(ports, key == '_port' ? val['val'] : val);391}, this);392} else {393return message;394}395};396397398/** @override */399goog.messaging.PortChannel.prototype.disposeInternal = function() {400goog.events.unlistenByKey(this.listenerKey_);401// Can't use instanceof here because MessagePort is undefined in workers and402// in Firefox403if (Object.prototype.toString.call(this.port_) == '[object MessagePort]') {404this.port_.close();405// Worker is undefined in workers as well as of Chrome 9406} else if (Object.prototype.toString.call(this.port_) == '[object Worker]') {407this.port_.terminate();408}409delete this.port_;410goog.messaging.PortChannel.base(this, 'disposeInternal');411};412413414