Path: blob/trunk/third_party/closure/goog/labs/pubsub/broadcastpubsub.js
2868 views
// Copyright 2014 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.1314goog.provide('goog.labs.pubsub.BroadcastPubSub');151617goog.require('goog.Disposable');18goog.require('goog.Timer');19goog.require('goog.array');20goog.require('goog.async.run');21goog.require('goog.events.EventHandler');22goog.require('goog.events.EventType');23goog.require('goog.json');24goog.require('goog.log');25goog.require('goog.math');26goog.require('goog.pubsub.PubSub');27goog.require('goog.storage.Storage');28goog.require('goog.storage.mechanism.HTML5LocalStorage');29goog.require('goog.string');30goog.require('goog.userAgent');31323334/**35* Topic-based publish/subscribe messaging implementation that provides36* communication between browsing contexts that share the same origin.37*38* Wrapper around PubSub that utilizes localStorage to broadcast publications to39* all browser windows with the same origin as the publishing context. This40* allows for topic-based publish/subscribe implementation of strings shared by41* all browser contexts that share the same origin.42*43* Delivery is guaranteed on all browsers except IE8 where topics expire after a44* timeout. Publishing of a topic within a callback function provides no45* guarantee on ordering in that there is a possibility that separate origin46* contexts may see topics in a different order.47*48* This class is not secure and in certain cases (e.g., a browser crash) data49* that is published can persist in localStorage indefinitely. Do not use this50* class to communicate private or confidential information.51*52* On IE8, localStorage is shared by the http and https origins. An attacker53* could possibly leverage this to publish to the secure origin.54*55* goog.labs.pubsub.BroadcastPubSub wraps an instance of PubSub rather than56* subclassing because the base PubSub class allows publishing of arbitrary57* objects.58*59* Special handling is done for the IE8 browsers. See the IE8_EVENTS_KEY_60* constant and the {@code publish} function for more information.61*62*63* @constructor @struct @extends {goog.Disposable}64*/65goog.labs.pubsub.BroadcastPubSub = function() {66goog.labs.pubsub.BroadcastPubSub.base(this, 'constructor');67goog.labs.pubsub.BroadcastPubSub.instances_.push(this);6869/** @private @const */70this.pubSub_ = new goog.pubsub.PubSub();71this.registerDisposable(this.pubSub_);7273/** @private @const */74this.handler_ = new goog.events.EventHandler(this);75this.registerDisposable(this.handler_);7677/** @private @const */78this.logger_ = goog.log.getLogger('goog.labs.pubsub.BroadcastPubSub');7980/** @private @const */81this.mechanism_ = new goog.storage.mechanism.HTML5LocalStorage();8283/** @private {goog.storage.Storage} */84this.storage_ = null;8586/** @private {Object<string, number>} */87this.ie8LastEventTimes_ = null;8889/** @private {number} */90this.ie8StartupTimestamp_ = goog.now() - 1;9192if (this.mechanism_.isAvailable()) {93this.storage_ = new goog.storage.Storage(this.mechanism_);9495var target = window;96if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {97this.ie8LastEventTimes_ = {};9899target = document;100}101this.handler_.listen(102target, goog.events.EventType.STORAGE, this.handleStorageEvent_);103}104};105goog.inherits(goog.labs.pubsub.BroadcastPubSub, goog.Disposable);106107108/** @private @const {!Array<!goog.labs.pubsub.BroadcastPubSub>} */109goog.labs.pubsub.BroadcastPubSub.instances_ = [];110111112/**113* SitePubSub namespace for localStorage.114* @private @const115*/116goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_ = '_closure_bps';117118119/**120* Handle the storage event and possibly dispatch topics.121* @param {!goog.events.BrowserEvent} e Event object.122* @private123*/124goog.labs.pubsub.BroadcastPubSub.prototype.handleStorageEvent_ = function(e) {125if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {126// Even though we have the event, IE8 doesn't update our localStorage until127// after we handle the actual event.128goog.async.run(this.handleIe8StorageEvent_, this);129return;130}131132var browserEvent = e.getBrowserEvent();133if (browserEvent.key != goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_) {134return;135}136137var data = goog.json.parse(browserEvent.newValue);138var args = goog.isObject(data) && data['args'];139if (goog.isArray(args) && goog.array.every(args, goog.isString)) {140this.dispatch_(args);141} else {142goog.log.warning(this.logger_, 'storage event contained invalid arguments');143}144};145146147/**148* Dispatches args on the internal pubsub queue.149* @param {!Array<string>} args The arguments to publish.150* @private151*/152goog.labs.pubsub.BroadcastPubSub.prototype.dispatch_ = function(args) {153goog.pubsub.PubSub.prototype.publish.apply(this.pubSub_, args);154};155156157/**158* Publishes a message to a topic. Remote subscriptions in other tabs/windows159* are dispatched via local storage events. Local subscriptions are called160* asynchronously via Timer event in order to simulate remote behavior locally.161* @param {string} topic Topic to publish to.162* @param {...string} var_args String arguments that are applied to each163* subscription function.164*/165goog.labs.pubsub.BroadcastPubSub.prototype.publish = function(topic, var_args) {166var args = goog.array.toArray(arguments);167168// Dispatch to localStorage.169if (this.storage_) {170// Update topics to use the optional prefix.171var now = goog.now();172var data = {'args': args, 'timestamp': now};173174if (!goog.labs.pubsub.BroadcastPubSub.IS_IE8_) {175// Generated events will contain all the data in modern browsers.176this.storage_.set(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_, data);177this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_);178} else {179// With IE8 we need to manage our own events queue.180var events = null;181182try {183events =184this.storage_.get(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);185} catch (ex) {186goog.log.error(187this.logger_, 'publish encountered invalid event queue at ' +188goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);189}190if (!goog.isArray(events)) {191events = [];192}193// Avoid a race condition where we're publishing in the same194// millisecond that another event that may be getting195// processed. In short, we try go guarantee that whatever event196// we put on the event queue has a timestamp that is older than197// any other timestamp in the queue.198var lastEvent = events[events.length - 1];199var lastTimestamp =200lastEvent && lastEvent['timestamp'] || this.ie8StartupTimestamp_;201if (lastTimestamp >= now) {202now = lastTimestamp +203goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_;204data['timestamp'] = now;205}206events.push(data);207this.storage_.set(208goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_, events);209210// Cleanup this event in IE8_EVENT_LIFETIME_MS_ milliseconds.211goog.Timer.callOnce(212goog.bind(this.cleanupIe8StorageEvents_, this, now),213goog.labs.pubsub.BroadcastPubSub.IE8_EVENT_LIFETIME_MS_);214}215}216217// W3C spec is to not dispatch the storage event to the same window that218// modified localStorage. For conforming browsers we have to manually dispatch219// the publish event to subscriptions on instances of BroadcastPubSub in the220// current window.221if (!goog.userAgent.IE) {222// Dispatch the publish event to local instances asynchronously to fix some223// quirks with timings. The result is that all subscriptions are dispatched224// before any future publishes are processed. The effect is that225// subscriptions in the same window are dispatched as if they are the result226// of a publish from another tab.227goog.array.forEach(228goog.labs.pubsub.BroadcastPubSub.instances_, function(instance) {229goog.async.run(goog.bind(instance.dispatch_, instance, args));230});231}232};233234235/**236* Unsubscribes a function from a topic. Only deletes the first match found.237* Returns a Boolean indicating whether a subscription was removed.238* @param {string} topic Topic to unsubscribe from.239* @param {Function} fn Function to unsubscribe.240* @param {Object=} opt_context Object in whose context the function was to be241* called (the global scope if none).242* @return {boolean} Whether a matching subscription was removed.243*/244goog.labs.pubsub.BroadcastPubSub.prototype.unsubscribe = function(245topic, fn, opt_context) {246return this.pubSub_.unsubscribe(topic, fn, opt_context);247};248249250/**251* Removes a subscription based on the key returned by {@link #subscribe}. No-op252* if no matching subscription is found. Returns a Boolean indicating whether a253* subscription was removed.254* @param {number} key Subscription key.255* @return {boolean} Whether a matching subscription was removed.256*/257goog.labs.pubsub.BroadcastPubSub.prototype.unsubscribeByKey = function(key) {258return this.pubSub_.unsubscribeByKey(key);259};260261262/**263* Subscribes a function to a topic. The function is invoked as a method on the264* given {@code opt_context} object, or in the global scope if no context is265* specified. Subscribing the same function to the same topic multiple times266* will result in multiple function invocations while publishing. Returns a267* subscription key that can be used to unsubscribe the function from the topic268* via {@link #unsubscribeByKey}.269* @param {string} topic Topic to subscribe to.270* @param {Function} fn Function to be invoked when a message is published to271* the given topic.272* @param {Object=} opt_context Object in whose context the function is to be273* called (the global scope if none).274* @return {number} Subscription key.275*/276goog.labs.pubsub.BroadcastPubSub.prototype.subscribe = function(277topic, fn, opt_context) {278return this.pubSub_.subscribe(topic, fn, opt_context);279};280281282/**283* Subscribes a single-use function to a topic. The function is invoked as a284* method on the given {@code opt_context} object, or in the global scope if no285* context is specified, and is then unsubscribed. Returns a subscription key286* that can be used to unsubscribe the function from the topic via {@link287* #unsubscribeByKey}.288* @param {string} topic Topic to subscribe to.289* @param {Function} fn Function to be invoked once and then unsubscribed when290* a message is published to the given topic.291* @param {Object=} opt_context Object in whose context the function is to be292* called (the global scope if none).293* @return {number} Subscription key.294*/295goog.labs.pubsub.BroadcastPubSub.prototype.subscribeOnce = function(296topic, fn, opt_context) {297return this.pubSub_.subscribeOnce(topic, fn, opt_context);298};299300301/**302* Returns the number of subscriptions to the given topic (or all topics if303* unspecified). This number will not change while publishing any messages.304* @param {string=} opt_topic The topic (all topics if unspecified).305* @return {number} Number of subscriptions to the topic.306*/307goog.labs.pubsub.BroadcastPubSub.prototype.getCount = function(opt_topic) {308return this.pubSub_.getCount(opt_topic);309};310311312/**313* Clears the subscription list for a topic, or all topics if unspecified.314* @param {string=} opt_topic Topic to clear (all topics if unspecified).315*/316goog.labs.pubsub.BroadcastPubSub.prototype.clear = function(opt_topic) {317this.pubSub_.clear(opt_topic);318};319320321/** @override */322goog.labs.pubsub.BroadcastPubSub.prototype.disposeInternal = function() {323goog.array.remove(goog.labs.pubsub.BroadcastPubSub.instances_, this);324if (goog.labs.pubsub.BroadcastPubSub.IS_IE8_ &&325goog.isDefAndNotNull(this.storage_) &&326goog.labs.pubsub.BroadcastPubSub.instances_.length == 0) {327this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);328}329goog.labs.pubsub.BroadcastPubSub.base(this, 'disposeInternal');330};331332333/**334* Prefix for IE8 storage event queue keys.335* @private @const336*/337goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ = '_closure_bps_ie8evt';338339340/**341* Time (in milliseconds) that IE8 events should live. If they are not342* processed by other windows in this time they will be removed.343* @private @const344*/345goog.labs.pubsub.BroadcastPubSub.IE8_EVENT_LIFETIME_MS_ = 1000 * 10;346347348/**349* Time (in milliseconds) that the IE8 event queue should live.350* @private @const351*/352goog.labs.pubsub.BroadcastPubSub.IE8_QUEUE_LIFETIME_MS_ = 1000 * 30;353354355/**356* Time delta that is used to distinguish between timestamps of events that357* happen in the same millisecond.358* @private @const359*/360goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_ = .01;361362363/**364* Name for this window/tab's storage key that stores its IE8 event queue.365*366* The browsers storage events are supposed to track the key which was changed,367* the previous value for that key, and the new value of that key. Our368* implementation is dependent on this information but IE8 doesn't provide it.369* We implement our own event queue using local storage to track this370* information in IE8. Since all instances share the same localStorage context371* in a particular tab, we share the events queue.372*373* This key is a static member shared by all instances of BroadcastPubSub in the374* same Window context. To avoid read-update-write contention, this key is only375* written in a single context in the cleanupIe8StorageEvents_ function. Since376* instances in other contexts will read this key there is code in the {@code377* publish} function to make sure timestamps are unique even within the same378* millisecond.379*380* @private @const {string}381*/382goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_ =383goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ +384goog.math.randomInt(1e9);385386387/**388* All instances of this object should access elements using strings and not389* attributes. Since we are communicating across browser tabs we could be390* dealing with different versions of javascript and thus may have different391* obfuscation in each tab.392* @private @typedef {{'timestamp': number, 'args': !Array<string>}}393*/394goog.labs.pubsub.BroadcastPubSub.Ie8Event_;395396397/** @private @const */398goog.labs.pubsub.BroadcastPubSub.IS_IE8_ =399goog.userAgent.IE && goog.userAgent.DOCUMENT_MODE == 8;400401402/**403* Validates an event object.404* @param {!Object} obj The object to validate as an Event.405* @return {?goog.labs.pubsub.BroadcastPubSub.Ie8Event_} A valid406* event object or null if the object is invalid.407* @private408*/409goog.labs.pubsub.BroadcastPubSub.validateIe8Event_ = function(obj) {410if (goog.isObject(obj) && goog.isNumber(obj['timestamp']) &&411goog.array.every(obj['args'], goog.isString)) {412return {'timestamp': obj['timestamp'], 'args': obj['args']};413}414return null;415};416417418/**419* Returns an array of valid IE8 events.420* @param {!Array<!Object>} events Possible IE8 events.421* @return {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>}422* Valid IE8 events.423* @private424*/425goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_ = function(events) {426return goog.array.filter(427goog.array.map(428events, goog.labs.pubsub.BroadcastPubSub.validateIe8Event_),429goog.isDefAndNotNull);430};431432433/**434* Returns the IE8 events that have a timestamp later than the provided435* timestamp.436* @param {number} timestamp Expired timestamp.437* @param {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>} events438* Possible IE8 events.439* @return {!Array<!goog.labs.pubsub.BroadcastPubSub.Ie8Event_>}440* Unexpired IE8 events.441* @private442*/443goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_ = function(444timestamp, events) {445return goog.array.filter(446events, function(event) { return event['timestamp'] > timestamp; });447};448449450/**451* Processes the events array for key if all elements are valid IE8 events.452* @param {string} key The key in localStorage where the event queue is stored.453* @param {!Array<!Object>} events Array of possible events stored at key.454* @return {boolean} Return true if all elements in the array are valid455* events, false otherwise.456* @private457*/458goog.labs.pubsub.BroadcastPubSub.prototype.maybeProcessIe8Events_ = function(459key, events) {460if (!events.length) {461return false;462}463464var validEvents =465goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_(events);466if (validEvents.length == events.length) {467var lastTimestamp = goog.array.peek(validEvents)['timestamp'];468var previousTime =469this.ie8LastEventTimes_[key] || this.ie8StartupTimestamp_;470if (lastTimestamp > previousTime -471goog.labs.pubsub.BroadcastPubSub.IE8_QUEUE_LIFETIME_MS_) {472this.ie8LastEventTimes_[key] = lastTimestamp;473validEvents = goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_(474previousTime, validEvents);475for (var i = 0, event; event = validEvents[i]; i++) {476this.dispatch_(event['args']);477}478return true;479}480} else {481goog.log.warning(this.logger_, 'invalid events found in queue ' + key);482}483484return false;485};486487488/**489* Handle the storage event and possibly dispatch events. Looks through all keys490* in localStorage for valid keys.491* @private492*/493goog.labs.pubsub.BroadcastPubSub.prototype.handleIe8StorageEvent_ = function() {494var numKeys = this.mechanism_.getCount();495for (var idx = 0; idx < numKeys; idx++) {496var key = this.mechanism_.key(idx);497// Don't process events we generated. The W3C standard says that storage498// events should be queued by the browser for each window whose document's499// storage object is affected by a change in localStorage. Chrome, Firefox,500// and modern IE don't dispatch the event to the window which made the501// change. This code simulates that behavior in IE8.502if (!(goog.isString(key) &&503goog.string.startsWith(504key, goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_))) {505continue;506}507508var events = null;509510try {511events = this.storage_.get(key);512} catch (ex) {513goog.log.warning(this.logger_, 'invalid remote event queue ' + key);514}515516if (!(goog.isArray(events) && this.maybeProcessIe8Events_(key, events))) {517// Events is not an array, empty, contains invalid events, or expired.518this.storage_.remove(key);519}520}521};522523524/**525* Cleanup our IE8 event queue by removing any events that come at or before the526* given timestamp.527* @param {number} timestamp Maximum timestamp to remove from the queue.528* @private529*/530goog.labs.pubsub.BroadcastPubSub.prototype.cleanupIe8StorageEvents_ = function(531timestamp) {532var events = null;533534try {535events =536this.storage_.get(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);537} catch (ex) {538goog.log.error(539this.logger_, 'cleanup encountered invalid event queue key ' +540goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);541}542if (!goog.isArray(events)) {543this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);544return;545}546547events = goog.labs.pubsub.BroadcastPubSub.filterNewIe8Events_(548timestamp,549goog.labs.pubsub.BroadcastPubSub.filterValidIe8Events_(events));550551if (events.length > 0) {552this.storage_.set(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_, events);553} else {554this.storage_.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);555}556};557558559