Path: blob/trunk/third_party/closure/goog/pubsub/pubsub.js
2868 views
// Copyright 2007 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.1314/**15* @fileoverview Topic-based publish/subscribe channel implementation.16*17* @author [email protected] (Attila Bodis)18*/1920goog.provide('goog.pubsub.PubSub');2122goog.require('goog.Disposable');23goog.require('goog.array');24goog.require('goog.async.run');25262728/**29* Topic-based publish/subscribe channel. Maintains a map of topics to30* subscriptions. When a message is published to a topic, all functions31* subscribed to that topic are invoked in the order they were added.32* Uncaught errors abort publishing.33*34* Topics may be identified by any nonempty string, <strong>except</strong>35* strings corresponding to native Object properties, e.g. "constructor",36* "toString", "hasOwnProperty", etc.37*38* @constructor39* @param {boolean=} opt_async Enable asynchronous behavior. Recommended for40* new code. See notes on the publish() method.41* @extends {goog.Disposable}42*/43goog.pubsub.PubSub = function(opt_async) {44goog.pubsub.PubSub.base(this, 'constructor');4546/**47* The next available subscription key. Internally, this is an index into the48* sparse array of subscriptions.49*50* @private {number}51*/52this.key_ = 1;5354/**55* Array of subscription keys pending removal once publishing is done.56*57* @private {!Array<number>}58* @const59*/60this.pendingKeys_ = [];6162/**63* Lock to prevent the removal of subscriptions during publishing. Incremented64* at the beginning of {@link #publish}, and decremented at the end.65*66* @private {number}67*/68this.publishDepth_ = 0;6970/**71* Sparse array of subscriptions. Each subscription is represented by a tuple72* comprising a topic identifier, a function, and an optional context object.73* Each tuple occupies three consecutive positions in the array, with the74* topic identifier at index n, the function at index (n + 1), the context75* object at index (n + 2), the next topic at index (n + 3), etc. (This76* representation minimizes the number of object allocations and has been77* shown to be faster than an array of objects with three key-value pairs or78* three parallel arrays, especially on IE.) Once a subscription is removed79* via {@link #unsubscribe} or {@link #unsubscribeByKey}, the three80* corresponding array elements are deleted, and never reused. This means the81* total number of subscriptions during the lifetime of the pubsub channel is82* limited by the maximum length of a JavaScript array to (2^32 - 1) / 3 =83* 1,431,655,765 subscriptions, which should suffice for most applications.84*85* @private {!Array<?>}86* @const87*/88this.subscriptions_ = [];8990/**91* Map of topics to arrays of subscription keys.92*93* @private {!Object<!Array<number>>}94*/95this.topics_ = {};9697/**98* @private @const {boolean}99*/100this.async_ = Boolean(opt_async);101};102goog.inherits(goog.pubsub.PubSub, goog.Disposable);103104105/**106* Subscribes a function to a topic. The function is invoked as a method on107* the given {@code opt_context} object, or in the global scope if no context108* is specified. Subscribing the same function to the same topic multiple109* times will result in multiple function invocations while publishing.110* Returns a subscription key that can be used to unsubscribe the function from111* the topic via {@link #unsubscribeByKey}.112*113* @param {string} topic Topic to subscribe to.114* @param {Function} fn Function to be invoked when a message is published to115* the given topic.116* @param {Object=} opt_context Object in whose context the function is to be117* called (the global scope if none).118* @return {number} Subscription key.119*/120goog.pubsub.PubSub.prototype.subscribe = function(topic, fn, opt_context) {121var keys = this.topics_[topic];122if (!keys) {123// First subscription to this topic; initialize subscription key array.124keys = this.topics_[topic] = [];125}126127// Push the tuple representing the subscription onto the subscription array.128var key = this.key_;129this.subscriptions_[key] = topic;130this.subscriptions_[key + 1] = fn;131this.subscriptions_[key + 2] = opt_context;132this.key_ = key + 3;133134// Push the subscription key onto the list of subscriptions for the topic.135keys.push(key);136137// Return the subscription key.138return key;139};140141142/**143* Subscribes a single-use function to a topic. The function is invoked as a144* method on the given {@code opt_context} object, or in the global scope if145* no context is specified, and is then unsubscribed. Returns a subscription146* key that can be used to unsubscribe the function from the topic via147* {@link #unsubscribeByKey}.148*149* @param {string} topic Topic to subscribe to.150* @param {Function} fn Function to be invoked once and then unsubscribed when151* a message is published to the given topic.152* @param {Object=} opt_context Object in whose context the function is to be153* called (the global scope if none).154* @return {number} Subscription key.155*/156goog.pubsub.PubSub.prototype.subscribeOnce = function(topic, fn, opt_context) {157// Keep track of whether the function was called. This is necessary because158// in async mode, multiple calls could be scheduled before the function has159// the opportunity to unsubscribe itself.160var called = false;161162// Behold the power of lexical closures!163var key = this.subscribe(topic, function(var_args) {164if (!called) {165called = true;166167// Unsubuscribe before calling function so the function is unscubscribed168// even if it throws an exception.169this.unsubscribeByKey(key);170171fn.apply(opt_context, arguments);172}173}, this);174return key;175};176177178/**179* Unsubscribes a function from a topic. Only deletes the first match found.180* Returns a Boolean indicating whether a subscription was removed.181*182* @param {string} topic Topic to unsubscribe from.183* @param {Function} fn Function to unsubscribe.184* @param {Object=} opt_context Object in whose context the function was to be185* called (the global scope if none).186* @return {boolean} Whether a matching subscription was removed.187*/188goog.pubsub.PubSub.prototype.unsubscribe = function(topic, fn, opt_context) {189var keys = this.topics_[topic];190if (keys) {191// Find the subscription key for the given combination of topic, function,192// and context object.193var subscriptions = this.subscriptions_;194var key = goog.array.find(keys, function(k) {195return subscriptions[k + 1] == fn && subscriptions[k + 2] == opt_context;196});197// Zero is not a valid key.198if (key) {199return this.unsubscribeByKey(key);200}201}202203return false;204};205206207/**208* Removes a subscription based on the key returned by {@link #subscribe}.209* No-op if no matching subscription is found. Returns a Boolean indicating210* whether a subscription was removed.211*212* @param {number} key Subscription key.213* @return {boolean} Whether a matching subscription was removed.214*/215goog.pubsub.PubSub.prototype.unsubscribeByKey = function(key) {216var topic = this.subscriptions_[key];217if (topic) {218// Subscription tuple found.219var keys = this.topics_[topic];220221if (this.publishDepth_ != 0) {222// Defer removal until after publishing is complete, but replace the223// function with a no-op so it isn't called.224this.pendingKeys_.push(key);225this.subscriptions_[key + 1] = goog.nullFunction;226} else {227if (keys) {228goog.array.remove(keys, key);229}230delete this.subscriptions_[key];231delete this.subscriptions_[key + 1];232delete this.subscriptions_[key + 2];233}234}235236return !!topic;237};238239240/**241* Publishes a message to a topic. Calls functions subscribed to the topic in242* the order in which they were added, passing all arguments along.243*244* If this object was created with async=true, subscribed functions are called245* via goog.async.run(). Otherwise, the functions are called directly, and if246* any of them throw an uncaught error, publishing is aborted.247*248* @param {string} topic Topic to publish to.249* @param {...*} var_args Arguments that are applied to each subscription250* function.251* @return {boolean} Whether any subscriptions were called.252*/253goog.pubsub.PubSub.prototype.publish = function(topic, var_args) {254var keys = this.topics_[topic];255if (keys) {256// Copy var_args to a new array so they can be passed to subscribers.257// Note that we can't use Array.slice or goog.array.toArray for this for258// performance reasons. Using those with the arguments object will cause259// deoptimization.260var args = new Array(arguments.length - 1);261for (var i = 1, len = arguments.length; i < len; i++) {262args[i - 1] = arguments[i];263}264265if (this.async_) {266// For each key in the list of subscription keys for the topic, schedule267// the function to be applied to the arguments in the appropriate context.268for (i = 0; i < keys.length; i++) {269var key = keys[i];270goog.pubsub.PubSub.runAsync_(271this.subscriptions_[key + 1], this.subscriptions_[key + 2], args);272}273} else {274// We must lock subscriptions and remove them at the end, so we don't275// adversely affect the performance of the common case by cloning the key276// array.277this.publishDepth_++;278279try {280// For each key in the list of subscription keys for the topic, apply281// the function to the arguments in the appropriate context. The length282// of the array must be fixed during the iteration, since subscribers283// may add new subscribers during publishing.284for (i = 0, len = keys.length; i < len; i++) {285var key = keys[i];286this.subscriptions_[key + 1].apply(287this.subscriptions_[key + 2], args);288}289} finally {290// Always unlock subscriptions, even if a subscribed method throws an291// uncaught exception. This makes it possible for users to catch292// exceptions themselves and unsubscribe remaining subscriptions.293this.publishDepth_--;294295if (this.pendingKeys_.length > 0 && this.publishDepth_ == 0) {296var pendingKey;297while ((pendingKey = this.pendingKeys_.pop())) {298this.unsubscribeByKey(pendingKey);299}300}301}302}303304// At least one subscriber was called.305return i != 0;306}307308// No subscribers were found.309return false;310};311312313/**314* Runs a function asynchronously with the given context and arguments.315* @param {!Function} func The function to call.316* @param {*} context The context in which to call {@code func}.317* @param {!Array} args The arguments to pass to {@code func}.318* @private319*/320goog.pubsub.PubSub.runAsync_ = function(func, context, args) {321goog.async.run(function() { func.apply(context, args); });322};323324325/**326* Clears the subscription list for a topic, or all topics if unspecified.327* @param {string=} opt_topic Topic to clear (all topics if unspecified).328*/329goog.pubsub.PubSub.prototype.clear = function(opt_topic) {330if (opt_topic) {331var keys = this.topics_[opt_topic];332if (keys) {333goog.array.forEach(keys, this.unsubscribeByKey, this);334delete this.topics_[opt_topic];335}336} else {337this.subscriptions_.length = 0;338this.topics_ = {};339// We don't reset key_ on purpose, because we want subscription keys to be340// unique throughout the lifetime of the application. Reusing subscription341// keys could lead to subtle errors in client code.342}343};344345346/**347* Returns the number of subscriptions to the given topic (or all topics if348* unspecified). This number will not change while publishing any messages.349* @param {string=} opt_topic The topic (all topics if unspecified).350* @return {number} Number of subscriptions to the topic.351*/352goog.pubsub.PubSub.prototype.getCount = function(opt_topic) {353if (opt_topic) {354var keys = this.topics_[opt_topic];355return keys ? keys.length : 0;356}357358var count = 0;359for (var topic in this.topics_) {360count += this.getCount(topic);361}362363return count;364};365366367/** @override */368goog.pubsub.PubSub.prototype.disposeInternal = function() {369goog.pubsub.PubSub.base(this, 'disposeInternal');370this.clear();371this.pendingKeys_.length = 0;372};373374375