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