pubsub.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. // Copyright 2007 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. * @fileoverview Topic-based publish/subscribe channel implementation.
  16. *
  17. * @author attila@google.com (Attila Bodis)
  18. */
  19. goog.provide('goog.pubsub.PubSub');
  20. goog.require('goog.Disposable');
  21. goog.require('goog.array');
  22. goog.require('goog.async.run');
  23. /**
  24. * Topic-based publish/subscribe channel. Maintains a map of topics to
  25. * subscriptions. When a message is published to a topic, all functions
  26. * subscribed to that topic are invoked in the order they were added.
  27. * Uncaught errors abort publishing.
  28. *
  29. * Topics may be identified by any nonempty string, <strong>except</strong>
  30. * strings corresponding to native Object properties, e.g. "constructor",
  31. * "toString", "hasOwnProperty", etc.
  32. *
  33. * @constructor
  34. * @param {boolean=} opt_async Enable asynchronous behavior. Recommended for
  35. * new code. See notes on the publish() method.
  36. * @extends {goog.Disposable}
  37. */
  38. goog.pubsub.PubSub = function(opt_async) {
  39. goog.pubsub.PubSub.base(this, 'constructor');
  40. /**
  41. * The next available subscription key. Internally, this is an index into the
  42. * sparse array of subscriptions.
  43. *
  44. * @private {number}
  45. */
  46. this.key_ = 1;
  47. /**
  48. * Array of subscription keys pending removal once publishing is done.
  49. *
  50. * @private {!Array<number>}
  51. * @const
  52. */
  53. this.pendingKeys_ = [];
  54. /**
  55. * Lock to prevent the removal of subscriptions during publishing. Incremented
  56. * at the beginning of {@link #publish}, and decremented at the end.
  57. *
  58. * @private {number}
  59. */
  60. this.publishDepth_ = 0;
  61. /**
  62. * Sparse array of subscriptions. Each subscription is represented by a tuple
  63. * comprising a topic identifier, a function, and an optional context object.
  64. * Each tuple occupies three consecutive positions in the array, with the
  65. * topic identifier at index n, the function at index (n + 1), the context
  66. * object at index (n + 2), the next topic at index (n + 3), etc. (This
  67. * representation minimizes the number of object allocations and has been
  68. * shown to be faster than an array of objects with three key-value pairs or
  69. * three parallel arrays, especially on IE.) Once a subscription is removed
  70. * via {@link #unsubscribe} or {@link #unsubscribeByKey}, the three
  71. * corresponding array elements are deleted, and never reused. This means the
  72. * total number of subscriptions during the lifetime of the pubsub channel is
  73. * limited by the maximum length of a JavaScript array to (2^32 - 1) / 3 =
  74. * 1,431,655,765 subscriptions, which should suffice for most applications.
  75. *
  76. * @private {!Array<?>}
  77. * @const
  78. */
  79. this.subscriptions_ = [];
  80. /**
  81. * Map of topics to arrays of subscription keys.
  82. *
  83. * @private {!Object<!Array<number>>}
  84. */
  85. this.topics_ = {};
  86. /**
  87. * @private @const {boolean}
  88. */
  89. this.async_ = Boolean(opt_async);
  90. };
  91. goog.inherits(goog.pubsub.PubSub, goog.Disposable);
  92. /**
  93. * Subscribes a function to a topic. The function is invoked as a method on
  94. * the given {@code opt_context} object, or in the global scope if no context
  95. * is specified. Subscribing the same function to the same topic multiple
  96. * times will result in multiple function invocations while publishing.
  97. * Returns a subscription key that can be used to unsubscribe the function from
  98. * the topic via {@link #unsubscribeByKey}.
  99. *
  100. * @param {string} topic Topic to subscribe to.
  101. * @param {Function} fn Function to be invoked when a message is published to
  102. * the given topic.
  103. * @param {Object=} opt_context Object in whose context the function is to be
  104. * called (the global scope if none).
  105. * @return {number} Subscription key.
  106. */
  107. goog.pubsub.PubSub.prototype.subscribe = function(topic, fn, opt_context) {
  108. var keys = this.topics_[topic];
  109. if (!keys) {
  110. // First subscription to this topic; initialize subscription key array.
  111. keys = this.topics_[topic] = [];
  112. }
  113. // Push the tuple representing the subscription onto the subscription array.
  114. var key = this.key_;
  115. this.subscriptions_[key] = topic;
  116. this.subscriptions_[key + 1] = fn;
  117. this.subscriptions_[key + 2] = opt_context;
  118. this.key_ = key + 3;
  119. // Push the subscription key onto the list of subscriptions for the topic.
  120. keys.push(key);
  121. // Return the subscription key.
  122. return key;
  123. };
  124. /**
  125. * Subscribes a single-use function to a topic. The function is invoked as a
  126. * method on the given {@code opt_context} object, or in the global scope if
  127. * no context is specified, and is then unsubscribed. Returns a subscription
  128. * key that can be used to unsubscribe the function from the topic via
  129. * {@link #unsubscribeByKey}.
  130. *
  131. * @param {string} topic Topic to subscribe to.
  132. * @param {Function} fn Function to be invoked once and then unsubscribed when
  133. * a message is published to the given topic.
  134. * @param {Object=} opt_context Object in whose context the function is to be
  135. * called (the global scope if none).
  136. * @return {number} Subscription key.
  137. */
  138. goog.pubsub.PubSub.prototype.subscribeOnce = function(topic, fn, opt_context) {
  139. // Keep track of whether the function was called. This is necessary because
  140. // in async mode, multiple calls could be scheduled before the function has
  141. // the opportunity to unsubscribe itself.
  142. var called = false;
  143. // Behold the power of lexical closures!
  144. var key = this.subscribe(topic, function(var_args) {
  145. if (!called) {
  146. called = true;
  147. // Unsubuscribe before calling function so the function is unscubscribed
  148. // even if it throws an exception.
  149. this.unsubscribeByKey(key);
  150. fn.apply(opt_context, arguments);
  151. }
  152. }, this);
  153. return key;
  154. };
  155. /**
  156. * Unsubscribes a function from a topic. Only deletes the first match found.
  157. * Returns a Boolean indicating whether a subscription was removed.
  158. *
  159. * @param {string} topic Topic to unsubscribe from.
  160. * @param {Function} fn Function to unsubscribe.
  161. * @param {Object=} opt_context Object in whose context the function was to be
  162. * called (the global scope if none).
  163. * @return {boolean} Whether a matching subscription was removed.
  164. */
  165. goog.pubsub.PubSub.prototype.unsubscribe = function(topic, fn, opt_context) {
  166. var keys = this.topics_[topic];
  167. if (keys) {
  168. // Find the subscription key for the given combination of topic, function,
  169. // and context object.
  170. var subscriptions = this.subscriptions_;
  171. var key = goog.array.find(keys, function(k) {
  172. return subscriptions[k + 1] == fn && subscriptions[k + 2] == opt_context;
  173. });
  174. // Zero is not a valid key.
  175. if (key) {
  176. return this.unsubscribeByKey(key);
  177. }
  178. }
  179. return false;
  180. };
  181. /**
  182. * Removes a subscription based on the key returned by {@link #subscribe}.
  183. * No-op if no matching subscription is found. Returns a Boolean indicating
  184. * whether a subscription was removed.
  185. *
  186. * @param {number} key Subscription key.
  187. * @return {boolean} Whether a matching subscription was removed.
  188. */
  189. goog.pubsub.PubSub.prototype.unsubscribeByKey = function(key) {
  190. var topic = this.subscriptions_[key];
  191. if (topic) {
  192. // Subscription tuple found.
  193. var keys = this.topics_[topic];
  194. if (this.publishDepth_ != 0) {
  195. // Defer removal until after publishing is complete, but replace the
  196. // function with a no-op so it isn't called.
  197. this.pendingKeys_.push(key);
  198. this.subscriptions_[key + 1] = goog.nullFunction;
  199. } else {
  200. if (keys) {
  201. goog.array.remove(keys, key);
  202. }
  203. delete this.subscriptions_[key];
  204. delete this.subscriptions_[key + 1];
  205. delete this.subscriptions_[key + 2];
  206. }
  207. }
  208. return !!topic;
  209. };
  210. /**
  211. * Publishes a message to a topic. Calls functions subscribed to the topic in
  212. * the order in which they were added, passing all arguments along.
  213. *
  214. * If this object was created with async=true, subscribed functions are called
  215. * via goog.async.run(). Otherwise, the functions are called directly, and if
  216. * any of them throw an uncaught error, publishing is aborted.
  217. *
  218. * @param {string} topic Topic to publish to.
  219. * @param {...*} var_args Arguments that are applied to each subscription
  220. * function.
  221. * @return {boolean} Whether any subscriptions were called.
  222. */
  223. goog.pubsub.PubSub.prototype.publish = function(topic, var_args) {
  224. var keys = this.topics_[topic];
  225. if (keys) {
  226. // Copy var_args to a new array so they can be passed to subscribers.
  227. // Note that we can't use Array.slice or goog.array.toArray for this for
  228. // performance reasons. Using those with the arguments object will cause
  229. // deoptimization.
  230. var args = new Array(arguments.length - 1);
  231. for (var i = 1, len = arguments.length; i < len; i++) {
  232. args[i - 1] = arguments[i];
  233. }
  234. if (this.async_) {
  235. // For each key in the list of subscription keys for the topic, schedule
  236. // the function to be applied to the arguments in the appropriate context.
  237. for (i = 0; i < keys.length; i++) {
  238. var key = keys[i];
  239. goog.pubsub.PubSub.runAsync_(
  240. this.subscriptions_[key + 1], this.subscriptions_[key + 2], args);
  241. }
  242. } else {
  243. // We must lock subscriptions and remove them at the end, so we don't
  244. // adversely affect the performance of the common case by cloning the key
  245. // array.
  246. this.publishDepth_++;
  247. try {
  248. // For each key in the list of subscription keys for the topic, apply
  249. // the function to the arguments in the appropriate context. The length
  250. // of the array must be fixed during the iteration, since subscribers
  251. // may add new subscribers during publishing.
  252. for (i = 0, len = keys.length; i < len; i++) {
  253. var key = keys[i];
  254. this.subscriptions_[key + 1].apply(
  255. this.subscriptions_[key + 2], args);
  256. }
  257. } finally {
  258. // Always unlock subscriptions, even if a subscribed method throws an
  259. // uncaught exception. This makes it possible for users to catch
  260. // exceptions themselves and unsubscribe remaining subscriptions.
  261. this.publishDepth_--;
  262. if (this.pendingKeys_.length > 0 && this.publishDepth_ == 0) {
  263. var pendingKey;
  264. while ((pendingKey = this.pendingKeys_.pop())) {
  265. this.unsubscribeByKey(pendingKey);
  266. }
  267. }
  268. }
  269. }
  270. // At least one subscriber was called.
  271. return i != 0;
  272. }
  273. // No subscribers were found.
  274. return false;
  275. };
  276. /**
  277. * Runs a function asynchronously with the given context and arguments.
  278. * @param {!Function} func The function to call.
  279. * @param {*} context The context in which to call {@code func}.
  280. * @param {!Array} args The arguments to pass to {@code func}.
  281. * @private
  282. */
  283. goog.pubsub.PubSub.runAsync_ = function(func, context, args) {
  284. goog.async.run(function() { func.apply(context, args); });
  285. };
  286. /**
  287. * Clears the subscription list for a topic, or all topics if unspecified.
  288. * @param {string=} opt_topic Topic to clear (all topics if unspecified).
  289. */
  290. goog.pubsub.PubSub.prototype.clear = function(opt_topic) {
  291. if (opt_topic) {
  292. var keys = this.topics_[opt_topic];
  293. if (keys) {
  294. goog.array.forEach(keys, this.unsubscribeByKey, this);
  295. delete this.topics_[opt_topic];
  296. }
  297. } else {
  298. this.subscriptions_.length = 0;
  299. this.topics_ = {};
  300. // We don't reset key_ on purpose, because we want subscription keys to be
  301. // unique throughout the lifetime of the application. Reusing subscription
  302. // keys could lead to subtle errors in client code.
  303. }
  304. };
  305. /**
  306. * Returns the number of subscriptions to the given topic (or all topics if
  307. * unspecified). This number will not change while publishing any messages.
  308. * @param {string=} opt_topic The topic (all topics if unspecified).
  309. * @return {number} Number of subscriptions to the topic.
  310. */
  311. goog.pubsub.PubSub.prototype.getCount = function(opt_topic) {
  312. if (opt_topic) {
  313. var keys = this.topics_[opt_topic];
  314. return keys ? keys.length : 0;
  315. }
  316. var count = 0;
  317. for (var topic in this.topics_) {
  318. count += this.getCount(topic);
  319. }
  320. return count;
  321. };
  322. /** @override */
  323. goog.pubsub.PubSub.prototype.disposeInternal = function() {
  324. goog.pubsub.PubSub.base(this, 'disposeInternal');
  325. this.clear();
  326. this.pendingKeys_.length = 0;
  327. };