Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/labs/pubsub/broadcastpubsub.js
2868 views
1
// Copyright 2014 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
goog.provide('goog.labs.pubsub.BroadcastPubSub');
16
17
18
goog.require('goog.Disposable');
19
goog.require('goog.Timer');
20
goog.require('goog.array');
21
goog.require('goog.async.run');
22
goog.require('goog.events.EventHandler');
23
goog.require('goog.events.EventType');
24
goog.require('goog.json');
25
goog.require('goog.log');
26
goog.require('goog.math');
27
goog.require('goog.pubsub.PubSub');
28
goog.require('goog.storage.Storage');
29
goog.require('goog.storage.mechanism.HTML5LocalStorage');
30
goog.require('goog.string');
31
goog.require('goog.userAgent');
32
33
34
35
/**
36
* Topic-based publish/subscribe messaging implementation that provides
37
* communication between browsing contexts that share the same origin.
38
*
39
* Wrapper around PubSub that utilizes localStorage to broadcast publications to
40
* all browser windows with the same origin as the publishing context. This
41
* allows for topic-based publish/subscribe implementation of strings shared by
42
* all browser contexts that share the same origin.
43
*
44
* Delivery is guaranteed on all browsers except IE8 where topics expire after a
45
* timeout. Publishing of a topic within a callback function provides no
46
* guarantee on ordering in that there is a possibility that separate origin
47
* contexts may see topics in a different order.
48
*
49
* This class is not secure and in certain cases (e.g., a browser crash) data
50
* that is published can persist in localStorage indefinitely. Do not use this
51
* class to communicate private or confidential information.
52
*
53
* On IE8, localStorage is shared by the http and https origins. An attacker
54
* could possibly leverage this to publish to the secure origin.
55
*
56
* goog.labs.pubsub.BroadcastPubSub wraps an instance of PubSub rather than
57
* subclassing because the base PubSub class allows publishing of arbitrary
58
* objects.
59
*
60
* Special handling is done for the IE8 browsers. See the IE8_EVENTS_KEY_
61
* constant and the {@code publish} function for more information.
62
*
63
*
64
* @constructor @struct @extends {goog.Disposable}
65
*/
66
goog.labs.pubsub.BroadcastPubSub = function() {
67
goog.labs.pubsub.BroadcastPubSub.base(this, 'constructor');
68
goog.labs.pubsub.BroadcastPubSub.instances_.push(this);
69
70
/** @private @const */
71
this.pubSub_ = new goog.pubsub.PubSub();
72
this.registerDisposable(this.pubSub_);
73
74
/** @private @const */
75
this.handler_ = new goog.events.EventHandler(this);
76
this.registerDisposable(this.handler_);
77
78
/** @private @const */
79
this.logger_ = goog.log.getLogger('goog.labs.pubsub.BroadcastPubSub');
80
81
/** @private @const */
82
this.mechanism_ = new goog.storage.mechanism.HTML5LocalStorage();
83
84
/** @private {goog.storage.Storage} */
85
this.storage_ = null;
86
87
/** @private {Object<string, number>} */
88
this.ie8LastEventTimes_ = null;
89
90
/** @private {number} */
91
this.ie8StartupTimestamp_ = goog.now() - 1;
92
93
if (this.mechanism_.isAvailable()) {
94
this.storage_ = new goog.storage.Storage(this.mechanism_);
95
96
var target = window;
97
if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {
98
this.ie8LastEventTimes_ = {};
99
100
target = document;
101
}
102
this.handler_.listen(
103
target, goog.events.EventType.STORAGE, this.handleStorageEvent_);
104
}
105
};
106
goog.inherits(goog.labs.pubsub.BroadcastPubSub, goog.Disposable);
107
108
109
/** @private @const {!Array<!goog.labs.pubsub.BroadcastPubSub>} */
110
goog.labs.pubsub.BroadcastPubSub.instances_ = [];
111
112
113
/**
114
* SitePubSub namespace for localStorage.
115
* @private @const
116
*/
117
goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_ = '_closure_bps';
118
119
120
/**
121
* Handle the storage event and possibly dispatch topics.
122
* @param {!goog.events.BrowserEvent} e Event object.
123
* @private
124
*/
125
goog.labs.pubsub.BroadcastPubSub.prototype.handleStorageEvent_ = function(e) {
126
if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {
127
// Even though we have the event, IE8 doesn't update our localStorage until
128
// after we handle the actual event.
129
goog.async.run(this.handleIe8StorageEvent_, this);
130
return;
131
}
132
133
var browserEvent = e.getBrowserEvent();
134
if (browserEvent.key != goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_) {
135
return;
136
}
137
138
var data = goog.json.parse(browserEvent.newValue);
139
var args = goog.isObject(data) && data['args'];
140
if (goog.isArray(args) && goog.array.every(args, goog.isString)) {
141
this.dispatch_(args);
142
} else {
143
goog.log.warning(this.logger_, 'storage event contained invalid arguments');
144
}
145
};
146
147
148
/**
149
* Dispatches args on the internal pubsub queue.
150
* @param {!Array<string>} args The arguments to publish.
151
* @private
152
*/
153
goog.labs.pubsub.BroadcastPubSub.prototype.dispatch_ = function(args) {
154
goog.pubsub.PubSub.prototype.publish.apply(this.pubSub_, args);
155
};
156
157
158
/**
159
* Publishes a message to a topic. Remote subscriptions in other tabs/windows
160
* are dispatched via local storage events. Local subscriptions are called
161
* asynchronously via Timer event in order to simulate remote behavior locally.
162
* @param {string} topic Topic to publish to.
163
* @param {...string} var_args String arguments that are applied to each
164
* subscription function.
165
*/
166
goog.labs.pubsub.BroadcastPubSub.prototype.publish = function(topic, var_args) {
167
var args = goog.array.toArray(arguments);
168
169
// Dispatch to localStorage.
170
if (this.storage_) {
171
// Update topics to use the optional prefix.
172
var now = goog.now();
173
var data = {'args': args, 'timestamp': now};
174
175
if (!goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {
176
// Generated events will contain all the data in modern browsers.
177
this.storage_.set(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_, data);
178
this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_);
179
} else {
180
// With IE8 we need to manage our own events queue.
181
var events = null;
182
183
try {
184
events =
185
this.storage_.get(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
186
} catch (ex) {
187
goog.log.error(
188
this.logger_, 'publish encountered invalid event queue at ' +
189
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
190
}
191
if (!goog.isArray(events)) {
192
events = [];
193
}
194
// Avoid a race condition where we're publishing in the same
195
// millisecond that another event that may be getting
196
// processed. In short, we try go guarantee that whatever event
197
// we put on the event queue has a timestamp that is older than
198
// any other timestamp in the queue.
199
var lastEvent = events[events.length - 1];
200
var lastTimestamp =
201
lastEvent && lastEvent['timestamp'] || this.ie8StartupTimestamp_;
202
if (lastTimestamp >= now) {
203
now = lastTimestamp +
204
goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_;
205
data['timestamp'] = now;
206
}
207
events.push(data);
208
this.storage_.set(
209
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_, events);
210
211
// Cleanup this event in IE8_EVENT_LIFETIME_MS_ milliseconds.
212
goog.Timer.callOnce(
213
goog.bind(this.cleanupIe8StorageEvents_, this, now),
214
goog.labs.pubsub.BroadcastPubSub.IE8_EVENT_LIFETIME_MS_);
215
}
216
}
217
218
// W3C spec is to not dispatch the storage event to the same window that
219
// modified localStorage. For conforming browsers we have to manually dispatch
220
// the publish event to subscriptions on instances of BroadcastPubSub in the
221
// current window.
222
if (!goog.userAgent.IE) {
223
// Dispatch the publish event to local instances asynchronously to fix some
224
// quirks with timings. The result is that all subscriptions are dispatched
225
// before any future publishes are processed. The effect is that
226
// subscriptions in the same window are dispatched as if they are the result
227
// of a publish from another tab.
228
goog.array.forEach(
229
goog.labs.pubsub.BroadcastPubSub.instances_, function(instance) {
230
goog.async.run(goog.bind(instance.dispatch_, instance, args));
231
});
232
}
233
};
234
235
236
/**
237
* Unsubscribes a function from a topic. Only deletes the first match found.
238
* Returns a Boolean indicating whether a subscription was removed.
239
* @param {string} topic Topic to unsubscribe from.
240
* @param {Function} fn Function to unsubscribe.
241
* @param {Object=} opt_context Object in whose context the function was to be
242
* called (the global scope if none).
243
* @return {boolean} Whether a matching subscription was removed.
244
*/
245
goog.labs.pubsub.BroadcastPubSub.prototype.unsubscribe = function(
246
topic, fn, opt_context) {
247
return this.pubSub_.unsubscribe(topic, fn, opt_context);
248
};
249
250
251
/**
252
* Removes a subscription based on the key returned by {@link #subscribe}. No-op
253
* if no matching subscription is found. Returns a Boolean indicating whether a
254
* subscription was removed.
255
* @param {number} key Subscription key.
256
* @return {boolean} Whether a matching subscription was removed.
257
*/
258
goog.labs.pubsub.BroadcastPubSub.prototype.unsubscribeByKey = function(key) {
259
return this.pubSub_.unsubscribeByKey(key);
260
};
261
262
263
/**
264
* Subscribes a function to a topic. The function is invoked as a method on the
265
* given {@code opt_context} object, or in the global scope if no context is
266
* specified. Subscribing the same function to the same topic multiple times
267
* will result in multiple function invocations while publishing. Returns a
268
* subscription key that can be used to unsubscribe the function from the topic
269
* via {@link #unsubscribeByKey}.
270
* @param {string} topic Topic to subscribe to.
271
* @param {Function} fn Function to be invoked when a message is published to
272
* the given topic.
273
* @param {Object=} opt_context Object in whose context the function is to be
274
* called (the global scope if none).
275
* @return {number} Subscription key.
276
*/
277
goog.labs.pubsub.BroadcastPubSub.prototype.subscribe = function(
278
topic, fn, opt_context) {
279
return this.pubSub_.subscribe(topic, fn, opt_context);
280
};
281
282
283
/**
284
* Subscribes a single-use function to a topic. The function is invoked as a
285
* method on the given {@code opt_context} object, or in the global scope if no
286
* context is specified, and is then unsubscribed. Returns a subscription key
287
* that can be used to unsubscribe the function from the topic via {@link
288
* #unsubscribeByKey}.
289
* @param {string} topic Topic to subscribe to.
290
* @param {Function} fn Function to be invoked once and then unsubscribed when
291
* a message is published to the given topic.
292
* @param {Object=} opt_context Object in whose context the function is to be
293
* called (the global scope if none).
294
* @return {number} Subscription key.
295
*/
296
goog.labs.pubsub.BroadcastPubSub.prototype.subscribeOnce = function(
297
topic, fn, opt_context) {
298
return this.pubSub_.subscribeOnce(topic, fn, opt_context);
299
};
300
301
302
/**
303
* Returns the number of subscriptions to the given topic (or all topics if
304
* unspecified). This number will not change while publishing any messages.
305
* @param {string=} opt_topic The topic (all topics if unspecified).
306
* @return {number} Number of subscriptions to the topic.
307
*/
308
goog.labs.pubsub.BroadcastPubSub.prototype.getCount = function(opt_topic) {
309
return this.pubSub_.getCount(opt_topic);
310
};
311
312
313
/**
314
* Clears the subscription list for a topic, or all topics if unspecified.
315
* @param {string=} opt_topic Topic to clear (all topics if unspecified).
316
*/
317
goog.labs.pubsub.BroadcastPubSub.prototype.clear = function(opt_topic) {
318
this.pubSub_.clear(opt_topic);
319
};
320
321
322
/** @override */
323
goog.labs.pubsub.BroadcastPubSub.prototype.disposeInternal = function() {
324
goog.array.remove(goog.labs.pubsub.BroadcastPubSub.instances_, this);
325
if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_ &&
326
goog.isDefAndNotNull(this.storage_) &&
327
goog.labs.pubsub.BroadcastPubSub.instances_.length == 0) {
328
this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
329
}
330
goog.labs.pubsub.BroadcastPubSub.base(this, 'disposeInternal');
331
};
332
333
334
/**
335
* Prefix for IE8 storage event queue keys.
336
* @private @const
337
*/
338
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ = '_closure_bps_ie8evt';
339
340
341
/**
342
* Time (in milliseconds) that IE8 events should live. If they are not
343
* processed by other windows in this time they will be removed.
344
* @private @const
345
*/
346
goog.labs.pubsub.BroadcastPubSub.IE8_EVENT_LIFETIME_MS_ = 1000 * 10;
347
348
349
/**
350
* Time (in milliseconds) that the IE8 event queue should live.
351
* @private @const
352
*/
353
goog.labs.pubsub.BroadcastPubSub.IE8_QUEUE_LIFETIME_MS_ = 1000 * 30;
354
355
356
/**
357
* Time delta that is used to distinguish between timestamps of events that
358
* happen in the same millisecond.
359
* @private @const
360
*/
361
goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_ = .01;
362
363
364
/**
365
* Name for this window/tab's storage key that stores its IE8 event queue.
366
*
367
* The browsers storage events are supposed to track the key which was changed,
368
* the previous value for that key, and the new value of that key. Our
369
* implementation is dependent on this information but IE8 doesn't provide it.
370
* We implement our own event queue using local storage to track this
371
* information in IE8. Since all instances share the same localStorage context
372
* in a particular tab, we share the events queue.
373
*
374
* This key is a static member shared by all instances of BroadcastPubSub in the
375
* same Window context. To avoid read-update-write contention, this key is only
376
* written in a single context in the cleanupIe8StorageEvents_ function. Since
377
* instances in other contexts will read this key there is code in the {@code
378
* publish} function to make sure timestamps are unique even within the same
379
* millisecond.
380
*
381
* @private @const {string}
382
*/
383
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_ =
384
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ +
385
goog.math.randomInt(1e9);
386
387
388
/**
389
* All instances of this object should access elements using strings and not
390
* attributes. Since we are communicating across browser tabs we could be
391
* dealing with different versions of javascript and thus may have different
392
* obfuscation in each tab.
393
* @private @typedef {{'timestamp': number, 'args': !Array<string>}}
394
*/
395
goog.labs.pubsub.BroadcastPubSub.Ie8Event_;
396
397
398
/** @private @const */
399
goog.labs.pubsub.BroadcastPubSub.IS_IE8_ =
400
goog.userAgent.IE && goog.userAgent.DOCUMENT_MODE == 8;
401
402
403
/**
404
* Validates an event object.
405
* @param {!Object} obj The object to validate as an Event.
406
* @return {?goog.labs.pubsub.BroadcastPubSub.Ie8Event_} A valid
407
* event object or null if the object is invalid.
408
* @private
409
*/
410
goog.labs.pubsub.BroadcastPubSub.validateIe8Event_ = function(obj) {
411
if (goog.isObject(obj) && goog.isNumber(obj['timestamp']) &&
412
goog.array.every(obj['args'], goog.isString)) {
413
return {'timestamp': obj['timestamp'], 'args': obj['args']};
414
}
415
return null;
416
};
417
418
419
/**
420
* Returns an array of valid IE8 events.
421
* @param {!Array<!Object>} events Possible IE8 events.
422
* @return {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>}
423
* Valid IE8 events.
424
* @private
425
*/
426
goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_ = function(events) {
427
return goog.array.filter(
428
goog.array.map(
429
events, goog.labs.pubsub.BroadcastPubSub.validateIe8Event_),
430
goog.isDefAndNotNull);
431
};
432
433
434
/**
435
* Returns the IE8 events that have a timestamp later than the provided
436
* timestamp.
437
* @param {number} timestamp Expired timestamp.
438
* @param {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>} events
439
* Possible IE8 events.
440
* @return {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>}
441
* Unexpired IE8 events.
442
* @private
443
*/
444
goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_ = function(
445
timestamp, events) {
446
return goog.array.filter(
447
events, function(event) { return event['timestamp'] > timestamp; });
448
};
449
450
451
/**
452
* Processes the events array for key if all elements are valid IE8 events.
453
* @param {string} key The key in localStorage where the event queue is stored.
454
* @param {!Array<!Object>} events Array of possible events stored at key.
455
* @return {boolean} Return true if all elements in the array are valid
456
* events, false otherwise.
457
* @private
458
*/
459
goog.labs.pubsub.BroadcastPubSub.prototype.maybeProcessIe8Events_ = function(
460
key, events) {
461
if (!events.length) {
462
return false;
463
}
464
465
var validEvents =
466
goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_(events);
467
if (validEvents.length == events.length) {
468
var lastTimestamp = goog.array.peek(validEvents)['timestamp'];
469
var previousTime =
470
this.ie8LastEventTimes_[key] || this.ie8StartupTimestamp_;
471
if (lastTimestamp > previousTime -
472
goog.labs.pubsub.BroadcastPubSub.IE8_QUEUE_LIFETIME_MS_) {
473
this.ie8LastEventTimes_[key] = lastTimestamp;
474
validEvents = goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_(
475
previousTime, validEvents);
476
for (var i = 0, event; event = validEvents[i]; i++) {
477
this.dispatch_(event['args']);
478
}
479
return true;
480
}
481
} else {
482
goog.log.warning(this.logger_, 'invalid events found in queue ' + key);
483
}
484
485
return false;
486
};
487
488
489
/**
490
* Handle the storage event and possibly dispatch events. Looks through all keys
491
* in localStorage for valid keys.
492
* @private
493
*/
494
goog.labs.pubsub.BroadcastPubSub.prototype.handleIe8StorageEvent_ = function() {
495
var numKeys = this.mechanism_.getCount();
496
for (var idx = 0; idx < numKeys; idx++) {
497
var key = this.mechanism_.key(idx);
498
// Don't process events we generated. The W3C standard says that storage
499
// events should be queued by the browser for each window whose document's
500
// storage object is affected by a change in localStorage. Chrome, Firefox,
501
// and modern IE don't dispatch the event to the window which made the
502
// change. This code simulates that behavior in IE8.
503
if (!(goog.isString(key) &&
504
goog.string.startsWith(
505
key, goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_))) {
506
continue;
507
}
508
509
var events = null;
510
511
try {
512
events = this.storage_.get(key);
513
} catch (ex) {
514
goog.log.warning(this.logger_, 'invalid remote event queue ' + key);
515
}
516
517
if (!(goog.isArray(events) && this.maybeProcessIe8Events_(key, events))) {
518
// Events is not an array, empty, contains invalid events, or expired.
519
this.storage_.remove(key);
520
}
521
}
522
};
523
524
525
/**
526
* Cleanup our IE8 event queue by removing any events that come at or before the
527
* given timestamp.
528
* @param {number} timestamp Maximum timestamp to remove from the queue.
529
* @private
530
*/
531
goog.labs.pubsub.BroadcastPubSub.prototype.cleanupIe8StorageEvents_ = function(
532
timestamp) {
533
var events = null;
534
535
try {
536
events =
537
this.storage_.get(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
538
} catch (ex) {
539
goog.log.error(
540
this.logger_, 'cleanup encountered invalid event queue key ' +
541
goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
542
}
543
if (!goog.isArray(events)) {
544
this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
545
return;
546
}
547
548
events = goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_(
549
timestamp,
550
goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_(events));
551
552
if (events.length > 0) {
553
this.storage_.set(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_, events);
554
} else {
555
this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
556
}
557
};
558
559