aboutsummaryrefslogtreecommitdiff
path: root/contexts/data/lib/closure-library/closure/goog/messaging/portchannel.js
blob: ae867cc7b17cef97793f698af43e00875faa8453 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
// 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.debug.Logger');
goog.require('goog.dom');
goog.require('goog.dom.DomHelper');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.json');
goog.require('goog.messaging.AbstractChannel');
goog.require('goog.messaging.DeferredChannel');
goog.require('goog.object');
goog.require('goog.string');



/**
 * 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}
 */
goog.messaging.PortChannel = function(underlyingPort) {
  goog.base(this);

  /**
   * The wrapped message-passing entity.
   * @type {!MessagePort|!WebWorker}
   * @private
   */
  this.port_ = underlyingPort;

  /**
   * The key for the event listener.
   * @type {?number}
   * @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, [channel.port2], peerOrigin);
  });

  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.debug.Logger}
 * @protected
 * @override
 */
goog.messaging.PortChannel.prototype.logger =
    goog.debug.Logger.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)) {
    this.logger.warning('Message object doesn\'t contain service name: ' +
                        goog.debug.deepExpose(data));
    return false;
  }

  if (!('payload' in data)) {
    this.logger.warning('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.base(this, 'disposeInternal');
};