| // Copyright 2007 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 Topic-based publish/subscribe channel implementation. |
| * |
| * @author attila@google.com (Attila Bodis) |
| */ |
| |
| goog.provide('goog.pubsub.PubSub'); |
| |
| goog.require('goog.Disposable'); |
| goog.require('goog.array'); |
| |
| |
| |
| /** |
| * Topic-based publish/subscribe channel. Maintains a map of topics to |
| * subscriptions. When a message is published to a topic, all functions |
| * subscribed to that topic are invoked in the order they were added. |
| * Uncaught errors abort publishing. |
| * |
| * Topics may be identified by any nonempty string, <strong>except</strong> |
| * strings corresponding to native Object properties, e.g. "constructor", |
| * "toString", "hasOwnProperty", etc. |
| * |
| * @constructor |
| * @extends {goog.Disposable} |
| */ |
| goog.pubsub.PubSub = function() { |
| goog.Disposable.call(this); |
| this.subscriptions_ = []; |
| this.topics_ = {}; |
| }; |
| goog.inherits(goog.pubsub.PubSub, goog.Disposable); |
| |
| |
| /** |
| * Sparse array of subscriptions. Each subscription is represented by a tuple |
| * comprising a topic identifier, a function, and an optional context object. |
| * Each tuple occupies three consecutive positions in the array, with the topic |
| * identifier at index n, the function at index (n + 1), the context object at |
| * index (n + 2), the next topic at index (n + 3), etc. (This representation |
| * minimizes the number of object allocations and has been shown to be faster |
| * than an array of objects with three key-value pairs or three parallel arrays, |
| * especially on IE.) Once a subscription is removed via {@link #unsubscribe} |
| * or {@link #unsubscribeByKey}, the three corresponding array elements are |
| * deleted, and never reused. This means the total number of subscriptions |
| * during the lifetime of the pubsub channel is limited by the maximum length |
| * of a JavaScript array to (2^32 - 1) / 3 = 1,431,655,765 subscriptions, which |
| * should suffice for most applications. |
| * |
| * @type {!Array<?>} |
| * @private |
| */ |
| goog.pubsub.PubSub.prototype.subscriptions_; |
| |
| |
| /** |
| * The next available subscription key. Internally, this is an index into the |
| * sparse array of subscriptions. |
| * |
| * @type {number} |
| * @private |
| */ |
| goog.pubsub.PubSub.prototype.key_ = 1; |
| |
| |
| /** |
| * Map of topics to arrays of subscription keys. |
| * |
| * @type {!Object<!Array<number>>} |
| * @private |
| */ |
| goog.pubsub.PubSub.prototype.topics_; |
| |
| |
| /** |
| * Array of subscription keys pending removal once publishing is done. |
| * |
| * @type {Array<number>} |
| * @private |
| */ |
| goog.pubsub.PubSub.prototype.pendingKeys_; |
| |
| |
| /** |
| * Lock to prevent the removal of subscriptions during publishing. Incremented |
| * at the beginning of {@link #publish}, and decremented at the end. |
| * |
| * @type {number} |
| * @private |
| */ |
| goog.pubsub.PubSub.prototype.publishDepth_ = 0; |
| |
| |
| /** |
| * Subscribes a function to a topic. The function is invoked as a method on |
| * the given {@code opt_context} object, or in the global scope if no context |
| * is specified. Subscribing the same function to the same topic multiple |
| * times will result in multiple function invocations while publishing. |
| * Returns a subscription key that can be used to unsubscribe the function from |
| * the topic via {@link #unsubscribeByKey}. |
| * |
| * @param {string} topic Topic to subscribe to. |
| * @param {Function} fn Function to be invoked when a message is published to |
| * the given topic. |
| * @param {Object=} opt_context Object in whose context the function is to be |
| * called (the global scope if none). |
| * @return {number} Subscription key. |
| */ |
| goog.pubsub.PubSub.prototype.subscribe = function(topic, fn, opt_context) { |
| var keys = this.topics_[topic]; |
| if (!keys) { |
| // First subscription to this topic; initialize subscription key array. |
| keys = this.topics_[topic] = []; |
| } |
| |
| // Push the tuple representing the subscription onto the subscription array. |
| var key = this.key_; |
| this.subscriptions_[key] = topic; |
| this.subscriptions_[key + 1] = fn; |
| this.subscriptions_[key + 2] = opt_context; |
| this.key_ = key + 3; |
| |
| // Push the subscription key onto the list of subscriptions for the topic. |
| keys.push(key); |
| |
| // Return the subscription key. |
| return key; |
| }; |
| |
| |
| /** |
| * Subscribes a single-use function to a topic. The function is invoked as a |
| * method on the given {@code opt_context} object, or in the global scope if |
| * no context is specified, and is then unsubscribed. Returns a subscription |
| * key that can be used to unsubscribe the function from the topic via |
| * {@link #unsubscribeByKey}. |
| * |
| * @param {string} topic Topic to subscribe to. |
| * @param {Function} fn Function to be invoked once and then unsubscribed when |
| * a message is published to the given topic. |
| * @param {Object=} opt_context Object in whose context the function is to be |
| * called (the global scope if none). |
| * @return {number} Subscription key. |
| */ |
| goog.pubsub.PubSub.prototype.subscribeOnce = function(topic, fn, opt_context) { |
| // Behold the power of lexical closures! |
| var key = this.subscribe(topic, function(var_args) { |
| fn.apply(opt_context, arguments); |
| this.unsubscribeByKey(key); |
| }, this); |
| return key; |
| }; |
| |
| |
| /** |
| * Unsubscribes a function from a topic. Only deletes the first match found. |
| * Returns a Boolean indicating whether a subscription was removed. |
| * |
| * @param {string} topic Topic to unsubscribe from. |
| * @param {Function} fn Function to unsubscribe. |
| * @param {Object=} opt_context Object in whose context the function was to be |
| * called (the global scope if none). |
| * @return {boolean} Whether a matching subscription was removed. |
| */ |
| goog.pubsub.PubSub.prototype.unsubscribe = function(topic, fn, opt_context) { |
| var keys = this.topics_[topic]; |
| if (keys) { |
| // Find the subscription key for the given combination of topic, function, |
| // and context object. |
| var subscriptions = this.subscriptions_; |
| var key = goog.array.find(keys, function(k) { |
| return subscriptions[k + 1] == fn && subscriptions[k + 2] == opt_context; |
| }); |
| // Zero is not a valid key. |
| if (key) { |
| return this.unsubscribeByKey(/** @type {number} */ (key)); |
| } |
| } |
| |
| return false; |
| }; |
| |
| |
| /** |
| * Removes a subscription based on the key returned by {@link #subscribe}. |
| * No-op if no matching subscription is found. Returns a Boolean indicating |
| * whether a subscription was removed. |
| * |
| * @param {number} key Subscription key. |
| * @return {boolean} Whether a matching subscription was removed. |
| */ |
| goog.pubsub.PubSub.prototype.unsubscribeByKey = function(key) { |
| if (this.publishDepth_ != 0) { |
| // Defer removal until after publishing is complete. |
| if (!this.pendingKeys_) { |
| this.pendingKeys_ = []; |
| } |
| this.pendingKeys_.push(key); |
| return false; |
| } |
| |
| var topic = this.subscriptions_[key]; |
| if (topic) { |
| // Subscription tuple found. |
| var keys = this.topics_[topic]; |
| if (keys) { |
| goog.array.remove(keys, key); |
| } |
| delete this.subscriptions_[key]; |
| delete this.subscriptions_[key + 1]; |
| delete this.subscriptions_[key + 2]; |
| } |
| |
| return !!topic; |
| }; |
| |
| |
| /** |
| * Publishes a message to a topic. Calls functions subscribed to the topic in |
| * the order in which they were added, passing all arguments along. If any of |
| * the functions throws an uncaught error, publishing is aborted. |
| * |
| * @param {string} topic Topic to publish to. |
| * @param {...*} var_args Arguments that are applied to each subscription |
| * function. |
| * @return {boolean} Whether any subscriptions were called. |
| */ |
| goog.pubsub.PubSub.prototype.publish = function(topic, var_args) { |
| var keys = this.topics_[topic]; |
| if (keys) { |
| // We must lock subscriptions and remove them at the end, so we don't |
| // adversely affect the performance of the common case by cloning the key |
| // array. |
| this.publishDepth_++; |
| |
| // Copy var_args to a new array so they can be passed to subscribers. |
| // Note that we can't use Array.slice or goog.array.toArray for this for |
| // performance reasons. Using those with the arguments object will cause |
| // deoptimization. |
| var args = new Array(arguments.length - 1); |
| for (var i = 1, len = arguments.length; i < len; i++) { |
| args[i - 1] = arguments[i]; |
| } |
| |
| // For each key in the list of subscription keys for the topic, apply the |
| // function to the arguments in the appropriate context. The length of the |
| // array mush be fixed during the iteration, since subscribers may add new |
| // subscribers during publishing. |
| for (var i = 0, len = keys.length; i < len; i++) { |
| var key = keys[i]; |
| this.subscriptions_[key + 1].apply(this.subscriptions_[key + 2], args); |
| } |
| |
| // Unlock subscriptions. |
| this.publishDepth_--; |
| |
| if (this.pendingKeys_ && this.publishDepth_ == 0) { |
| var pendingKey; |
| while ((pendingKey = this.pendingKeys_.pop())) { |
| this.unsubscribeByKey(pendingKey); |
| } |
| } |
| |
| // At least one subscriber was called. |
| return i != 0; |
| } |
| |
| // No subscribers were found. |
| return false; |
| }; |
| |
| |
| /** |
| * Clears the subscription list for a topic, or all topics if unspecified. |
| * @param {string=} opt_topic Topic to clear (all topics if unspecified). |
| */ |
| goog.pubsub.PubSub.prototype.clear = function(opt_topic) { |
| if (opt_topic) { |
| var keys = this.topics_[opt_topic]; |
| if (keys) { |
| goog.array.forEach(keys, this.unsubscribeByKey, this); |
| delete this.topics_[opt_topic]; |
| } |
| } else { |
| this.subscriptions_.length = 0; |
| this.topics_ = {}; |
| // We don't reset key_ on purpose, because we want subscription keys to be |
| // unique throughout the lifetime of the application. Reusing subscription |
| // keys could lead to subtle errors in client code. |
| } |
| }; |
| |
| |
| /** |
| * Returns the number of subscriptions to the given topic (or all topics if |
| * unspecified). |
| * @param {string=} opt_topic The topic (all topics if unspecified). |
| * @return {number} Number of subscriptions to the topic. |
| */ |
| goog.pubsub.PubSub.prototype.getCount = function(opt_topic) { |
| if (opt_topic) { |
| var keys = this.topics_[opt_topic]; |
| return keys ? keys.length : 0; |
| } |
| |
| var count = 0; |
| for (var topic in this.topics_) { |
| count += this.getCount(topic); |
| } |
| |
| return count; |
| }; |
| |
| |
| /** @override */ |
| goog.pubsub.PubSub.prototype.disposeInternal = function() { |
| goog.pubsub.PubSub.superClass_.disposeInternal.call(this); |
| delete this.subscriptions_; |
| delete this.topics_; |
| delete this.pendingKeys_; |
| }; |