portchannel.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. // Copyright 2010 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview A class that wraps several types of HTML5 message-passing
  16. * 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 an
  20. * incomplete implementation of web workers, it doesn't support sending ports
  21. * over Window connections. IE has no web worker support at all, and so is
  22. * unsupported by this class.
  23. *
  24. */
  25. goog.provide('goog.messaging.PortChannel');
  26. goog.require('goog.Timer');
  27. goog.require('goog.array');
  28. goog.require('goog.async.Deferred');
  29. goog.require('goog.debug');
  30. goog.require('goog.events');
  31. goog.require('goog.events.EventType');
  32. goog.require('goog.json');
  33. goog.require('goog.log');
  34. goog.require('goog.messaging.AbstractChannel');
  35. goog.require('goog.messaging.DeferredChannel');
  36. goog.require('goog.object');
  37. goog.require('goog.string');
  38. goog.require('goog.userAgent');
  39. /**
  40. * A wrapper for several types of HTML5 message-passing entities
  41. * ({@link MessagePort}s and {@link WebWorker}s). This class implements the
  42. * {@link goog.messaging.MessageChannel} interface.
  43. *
  44. * This class can be used in conjunction with other communication on the port.
  45. * It sets {@link goog.messaging.PortChannel.FLAG} to true on all messages it
  46. * sends.
  47. *
  48. * @param {!MessagePort|!WebWorker} underlyingPort The message-passing
  49. * entity to wrap. If this is a {@link MessagePort}, it should be started.
  50. * The remote end should also be wrapped in a PortChannel. This will be
  51. * disposed along with the PortChannel; this means terminating it if it's a
  52. * worker or removing it from the DOM if it's an iframe.
  53. * @constructor
  54. * @extends {goog.messaging.AbstractChannel}
  55. * @final
  56. */
  57. goog.messaging.PortChannel = function(underlyingPort) {
  58. goog.messaging.PortChannel.base(this, 'constructor');
  59. /**
  60. * The wrapped message-passing entity.
  61. * @type {!MessagePort|!WebWorker}
  62. * @private
  63. */
  64. this.port_ = underlyingPort;
  65. /**
  66. * The key for the event listener.
  67. * @type {goog.events.Key}
  68. * @private
  69. */
  70. this.listenerKey_ = goog.events.listen(
  71. this.port_, goog.events.EventType.MESSAGE, this.deliver_, false, this);
  72. };
  73. goog.inherits(goog.messaging.PortChannel, goog.messaging.AbstractChannel);
  74. /**
  75. * Create a PortChannel that communicates with a window embedded in the current
  76. * page (e.g. an iframe contentWindow). The code within the window should call
  77. * {@link forGlobalWindow} to establish the connection.
  78. *
  79. * It's possible to use this channel in conjunction with other messages to the
  80. * embedded window. However, only one PortChannel should be used for a given
  81. * window at a time.
  82. *
  83. * @param {!Window} peerWindow The window object to communicate with.
  84. * @param {string} peerOrigin The expected origin of the window. See
  85. * http://dev.w3.org/html5/postmsg/#dom-window-postmessage.
  86. * @param {goog.Timer=} opt_timer The timer that regulates how often the initial
  87. * connection message is attempted. This will be automatically disposed once
  88. * the connection is established, or when the connection is cancelled.
  89. * @return {!goog.messaging.DeferredChannel} The PortChannel. Although this is
  90. * not actually an instance of the PortChannel class, it will behave like
  91. * one in that MessagePorts may be sent across it. The DeferredChannel may
  92. * be cancelled before a connection is established in order to abort the
  93. * attempt to make a connection.
  94. */
  95. goog.messaging.PortChannel.forEmbeddedWindow = function(
  96. peerWindow, peerOrigin, opt_timer) {
  97. if (peerOrigin == '*') {
  98. return new goog.messaging.DeferredChannel(
  99. goog.async.Deferred.fail(new Error('Invalid origin')));
  100. }
  101. var timer = opt_timer || new goog.Timer(50);
  102. var disposeTimer = goog.partial(goog.dispose, timer);
  103. var deferred = new goog.async.Deferred(disposeTimer);
  104. deferred.addBoth(disposeTimer);
  105. timer.start();
  106. // Every tick, attempt to set up a connection by sending in one end of an
  107. // HTML5 MessageChannel. If the inner window posts a response along a channel,
  108. // then we'll use that channel to create the PortChannel.
  109. //
  110. // As per http://dev.w3.org/html5/postmsg/#ports-and-garbage-collection, any
  111. // ports that are not ultimately used to set up the channel will be garbage
  112. // collected (since there are no references in this context, and the remote
  113. // context hasn't seen them).
  114. goog.events.listen(timer, goog.Timer.TICK, function() {
  115. var channel = new MessageChannel();
  116. var gotMessage = function(e) {
  117. channel.port1.removeEventListener(
  118. goog.events.EventType.MESSAGE, gotMessage, true);
  119. // If the connection has been cancelled, don't create the channel.
  120. if (!timer.isDisposed()) {
  121. deferred.callback(new goog.messaging.PortChannel(channel.port1));
  122. }
  123. };
  124. channel.port1.start();
  125. // Don't use goog.events because we don't want any lingering references to
  126. // the ports to prevent them from getting GCed. Only modern browsers support
  127. // these APIs anyway, so we don't need to worry about event API
  128. // compatibility.
  129. channel.port1.addEventListener(
  130. goog.events.EventType.MESSAGE, gotMessage, true);
  131. var msg = {};
  132. msg[goog.messaging.PortChannel.FLAG] = true;
  133. peerWindow.postMessage(msg, peerOrigin, [channel.port2]);
  134. });
  135. return new goog.messaging.DeferredChannel(deferred);
  136. };
  137. /**
  138. * Create a PortChannel that communicates with the document in which this window
  139. * is embedded (e.g. within an iframe). The enclosing document should call
  140. * {@link forEmbeddedWindow} to establish the connection.
  141. *
  142. * It's possible to use this channel in conjunction with other messages posted
  143. * to the global window. However, only one PortChannel should be used for the
  144. * global window at a time.
  145. *
  146. * @param {string} peerOrigin The expected origin of the enclosing document. See
  147. * http://dev.w3.org/html5/postmsg/#dom-window-postmessage.
  148. * @return {!goog.messaging.MessageChannel} The PortChannel. Although this may
  149. * not actually be an instance of the PortChannel class, it will behave like
  150. * one in that MessagePorts may be sent across it.
  151. */
  152. goog.messaging.PortChannel.forGlobalWindow = function(peerOrigin) {
  153. if (peerOrigin == '*') {
  154. return new goog.messaging.DeferredChannel(
  155. goog.async.Deferred.fail(new Error('Invalid origin')));
  156. }
  157. var deferred = new goog.async.Deferred();
  158. // Wait for the external page to post a message containing the message port
  159. // which we'll use to set up the PortChannel. Ignore all other messages. Once
  160. // we receive the port, notify the other end and then set up the PortChannel.
  161. var key =
  162. goog.events.listen(window, goog.events.EventType.MESSAGE, function(e) {
  163. var browserEvent = e.getBrowserEvent();
  164. var data = browserEvent.data;
  165. if (!goog.isObject(data) || !data[goog.messaging.PortChannel.FLAG]) {
  166. return;
  167. }
  168. if (window.parent != browserEvent.source ||
  169. peerOrigin != browserEvent.origin) {
  170. return;
  171. }
  172. var port = browserEvent.ports[0];
  173. // Notify the other end of the channel that we've received our port
  174. port.postMessage({});
  175. port.start();
  176. deferred.callback(new goog.messaging.PortChannel(port));
  177. goog.events.unlistenByKey(key);
  178. });
  179. return new goog.messaging.DeferredChannel(deferred);
  180. };
  181. /**
  182. * The flag added to messages that are sent by a PortChannel, and are meant to
  183. * be handled by one on the other side.
  184. * @type {string}
  185. */
  186. goog.messaging.PortChannel.FLAG = '--goog.messaging.PortChannel';
  187. /**
  188. * Whether the messages sent across the channel must be JSON-serialized. This is
  189. * required for older versions of Webkit, which can only send string messages.
  190. *
  191. * Although Safari and Chrome have separate implementations of message passing,
  192. * both of them support passing objects by Webkit 533.
  193. *
  194. * @type {boolean}
  195. * @private
  196. */
  197. goog.messaging.PortChannel.REQUIRES_SERIALIZATION_ = goog.userAgent.WEBKIT &&
  198. goog.string.compareVersions(goog.userAgent.VERSION, '533') < 0;
  199. /**
  200. * Logger for this class.
  201. * @type {goog.log.Logger}
  202. * @protected
  203. * @override
  204. */
  205. goog.messaging.PortChannel.prototype.logger =
  206. goog.log.getLogger('goog.messaging.PortChannel');
  207. /**
  208. * Sends a message over the channel.
  209. *
  210. * As an addition to the basic MessageChannel send API, PortChannels can send
  211. * objects that contain MessagePorts. Note that only plain Objects and Arrays,
  212. * not their subclasses, can contain MessagePorts.
  213. *
  214. * As per {@link http://www.w3.org/TR/html5/comms.html#clone-a-port}, once a
  215. * port is copied to be sent across a channel, the original port will cease
  216. * being able to send or receive messages.
  217. *
  218. * @override
  219. * @param {string} serviceName The name of the service this message should be
  220. * delivered to.
  221. * @param {string|!Object|!MessagePort} payload The value of the message. May
  222. * contain MessagePorts or be a MessagePort.
  223. */
  224. goog.messaging.PortChannel.prototype.send = function(serviceName, payload) {
  225. var ports = [];
  226. payload = this.extractPorts_(ports, payload);
  227. var message = {'serviceName': serviceName, 'payload': payload};
  228. message[goog.messaging.PortChannel.FLAG] = true;
  229. if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_) {
  230. message = goog.json.serialize(message);
  231. }
  232. this.port_.postMessage(message, ports);
  233. };
  234. /**
  235. * Delivers a message to the appropriate service handler. If this message isn't
  236. * a GearsWorkerChannel message, it's ignored and passed on to other handlers.
  237. *
  238. * @param {goog.events.Event} e The event.
  239. * @private
  240. */
  241. goog.messaging.PortChannel.prototype.deliver_ = function(e) {
  242. var browserEvent = e.getBrowserEvent();
  243. var data = browserEvent.data;
  244. if (goog.messaging.PortChannel.REQUIRES_SERIALIZATION_) {
  245. try {
  246. data = goog.json.parse(data);
  247. } catch (error) {
  248. // Ignore any non-JSON messages.
  249. return;
  250. }
  251. }
  252. if (!goog.isObject(data) || !data[goog.messaging.PortChannel.FLAG]) {
  253. return;
  254. }
  255. if (this.validateMessage_(data)) {
  256. var serviceName = data['serviceName'];
  257. var payload = data['payload'];
  258. var service = this.getService(serviceName, payload);
  259. if (!service) {
  260. return;
  261. }
  262. payload = this.decodePayload(
  263. serviceName, this.injectPorts_(browserEvent.ports || [], payload),
  264. service.objectPayload);
  265. if (goog.isDefAndNotNull(payload)) {
  266. service.callback(payload);
  267. }
  268. }
  269. };
  270. /**
  271. * Checks whether the message is invalid in some way.
  272. *
  273. * @param {Object} data The contents of the message.
  274. * @return {boolean} True if the message is valid, false otherwise.
  275. * @private
  276. */
  277. goog.messaging.PortChannel.prototype.validateMessage_ = function(data) {
  278. if (!('serviceName' in data)) {
  279. goog.log.warning(
  280. this.logger, 'Message object doesn\'t contain service name: ' +
  281. goog.debug.deepExpose(data));
  282. return false;
  283. }
  284. if (!('payload' in data)) {
  285. goog.log.warning(
  286. this.logger, 'Message object doesn\'t contain payload: ' +
  287. goog.debug.deepExpose(data));
  288. return false;
  289. }
  290. return true;
  291. };
  292. /**
  293. * Extracts all MessagePort objects from a message to be sent into an array.
  294. *
  295. * The message ports are replaced by placeholder objects that will be replaced
  296. * with the ports again on the other side of the channel.
  297. *
  298. * @param {Array<MessagePort>} ports The array that will contain ports
  299. * extracted from the message. Will be destructively modified. Should be
  300. * empty initially.
  301. * @param {string|!Object} message The message from which ports will be
  302. * extracted.
  303. * @return {string|!Object} The message with ports extracted.
  304. * @private
  305. */
  306. goog.messaging.PortChannel.prototype.extractPorts_ = function(ports, message) {
  307. // Can't use instanceof here because MessagePort is undefined in workers
  308. if (message &&
  309. Object.prototype.toString.call(/** @type {!Object} */ (message)) ==
  310. '[object MessagePort]') {
  311. ports.push(/** @type {MessagePort} */ (message));
  312. return {'_port': {'type': 'real', 'index': ports.length - 1}};
  313. } else if (goog.isArray(message)) {
  314. return goog.array.map(message, goog.bind(this.extractPorts_, this, ports));
  315. // We want to compare the exact constructor here because we only want to
  316. // recurse into object literals, not native objects like Date.
  317. } else if (message && message.constructor == Object) {
  318. return goog.object.map(
  319. /** @type {!Object} */ (message), function(val, key) {
  320. val = this.extractPorts_(ports, val);
  321. return key == '_port' ? {'type': 'escaped', 'val': val} : val;
  322. }, this);
  323. } else {
  324. return message;
  325. }
  326. };
  327. /**
  328. * Injects MessagePorts back into a message received from across the channel.
  329. *
  330. * @param {Array<MessagePort>} ports The array of ports to be injected into the
  331. * message.
  332. * @param {string|!Object} message The message into which the ports will be
  333. * injected.
  334. * @return {string|!Object} The message with ports injected.
  335. * @private
  336. */
  337. goog.messaging.PortChannel.prototype.injectPorts_ = function(ports, message) {
  338. if (goog.isArray(message)) {
  339. return goog.array.map(message, goog.bind(this.injectPorts_, this, ports));
  340. } else if (message && message.constructor == Object) {
  341. message = /** @type {!Object} */ (message);
  342. if (message['_port'] && message['_port']['type'] == 'real') {
  343. return /** @type {!MessagePort} */ (ports[message['_port']['index']]);
  344. }
  345. return goog.object.map(message, function(val, key) {
  346. return this.injectPorts_(ports, key == '_port' ? val['val'] : val);
  347. }, this);
  348. } else {
  349. return message;
  350. }
  351. };
  352. /** @override */
  353. goog.messaging.PortChannel.prototype.disposeInternal = function() {
  354. goog.events.unlistenByKey(this.listenerKey_);
  355. // Can't use instanceof here because MessagePort is undefined in workers and
  356. // in Firefox
  357. if (Object.prototype.toString.call(this.port_) == '[object MessagePort]') {
  358. this.port_.close();
  359. // Worker is undefined in workers as well as of Chrome 9
  360. } else if (Object.prototype.toString.call(this.port_) == '[object Worker]') {
  361. this.port_.terminate();
  362. }
  363. delete this.port_;
  364. goog.messaging.PortChannel.base(this, 'disposeInternal');
  365. };