You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2012/09/18 17:42:52 UTC

js commit: [all] Separate Channel into sticky and non-sticky versions

Updated Branches:
  refs/heads/master d30179b30 -> c6917ee59


[all] Separate Channel into sticky and non-sticky versions

- This is a retry of aa15ac60d6cbabae83f619880a3a6e8be14817ce.
- It fixes forgetting to update channel.fired -> channel.state == 2 in iOS/exec.js
- It removes an extra ) in android/platform.js


Project: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/commit/c6917ee5
Tree: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/tree/c6917ee5
Diff: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/diff/c6917ee5

Branch: refs/heads/master
Commit: c6917ee59031ec2cb11dc46d73f3e7b7a8a1cd34
Parents: d30179b
Author: Andrew Grieve <ag...@chromium.org>
Authored: Tue Sep 18 11:39:58 2012 -0400
Committer: Andrew Grieve <ag...@chromium.org>
Committed: Tue Sep 18 11:39:58 2012 -0400

----------------------------------------------------------------------
 lib/android/platform.js          |    2 +-
 lib/bada/plugin/bada/device.js   |    2 +-
 lib/common/channel.js            |  166 ++++++++---------
 lib/common/plugin/device.js      |    2 +-
 lib/common/plugin/network.js     |    2 +-
 lib/cordova.js                   |   13 +-
 lib/ios/exec.js                  |    2 +-
 lib/scripts/bootstrap.js         |    2 +-
 lib/tizen/plugin/tizen/Device.js |    2 +-
 test/test.channel.js             |  327 +++++++++++++++++++--------------
 10 files changed, 280 insertions(+), 240 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/android/platform.js
----------------------------------------------------------------------
diff --git a/lib/android/platform.js b/lib/android/platform.js
index af5ab4f..b2cde22 100644
--- a/lib/android/platform.js
+++ b/lib/android/platform.js
@@ -32,7 +32,7 @@ module.exports = {
             // If we just attached the first handler or detached the last handler,
             // let native know we need to override the back button.
             exec(null, null, "App", "overrideBackbutton", [this.numHandlers == 1]);
-        });
+        };
 
         // Add hardware MENU and SEARCH button handlers
         cordova.addDocumentEventHandler('menubutton');

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/bada/plugin/bada/device.js
----------------------------------------------------------------------
diff --git a/lib/bada/plugin/bada/device.js b/lib/bada/plugin/bada/device.js
index 3d34166..b1da422 100644
--- a/lib/bada/plugin/bada/device.js
+++ b/lib/bada/plugin/bada/device.js
@@ -34,7 +34,7 @@ function Device() {
 
     var me = this;
 
-    channel.onCordovaReady.subscribeOnce(function() {
+    channel.onCordovaReady.subscribe(function() {
        me.getDeviceInfo(function (device) {
            me.platform = device.platform;
            me.version  = device.version;

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/common/channel.js
----------------------------------------------------------------------
diff --git a/lib/common/channel.js b/lib/common/channel.js
index 21f0e6e..1ad7227 100644
--- a/lib/common/channel.js
+++ b/lib/common/channel.js
@@ -25,19 +25,22 @@ var utils = require('cordova/utils'),
 /**
  * Custom pub-sub "channel" that can have functions subscribed to it
  * This object is used to define and control firing of events for
- * cordova initialization.
+ * cordova initialization, as well as for custom events thereafter.
  *
  * The order of events during page load and Cordova startup is as follows:
  *
- * onDOMContentLoaded         Internal event that is received when the web page is loaded and parsed.
- * onNativeReady              Internal event that indicates the Cordova native side is ready.
- * onCordovaReady             Internal event fired when all Cordova JavaScript objects have been created.
- * onCordovaInfoReady         Internal event fired when device properties are available.
- * onCordovaConnectionReady   Internal event fired when the connection property has been set.
- * onDeviceReady              User event fired to indicate that Cordova is ready
- * onResume                   User event fired to indicate a start/resume lifecycle event
- * onPause                    User event fired to indicate a pause lifecycle event
- * onDestroy                  Internal event fired when app is being destroyed (User should use window.onunload event, not this one).
+ * onDOMContentLoaded*         Internal event that is received when the web page is loaded and parsed.
+ * onNativeReady*              Internal event that indicates the Cordova native side is ready.
+ * onCordovaReady*             Internal event fired when all Cordova JavaScript objects have been created.
+ * onCordovaInfoReady*         Internal event fired when device properties are available.
+ * onCordovaConnectionReady*   Internal event fired when the connection property has been set.
+ * onDeviceReady*              User event fired to indicate that Cordova is ready
+ * onResume                    User event fired to indicate a start/resume lifecycle event
+ * onPause                     User event fired to indicate a pause lifecycle event
+ * onDestroy*                  Internal event fired when app is being destroyed (User should use window.onunload event, not this one).
+ *
+ * The events marked with an * are sticky. Once they have fired, they will stay in the fired state.
+ * All listeners that subscribe after the event is fired will be executed right away.
  *
  * The only Cordova events that user code should register for are:
  *      deviceready           Cordova native code is initialized and Cordova APIs can be called from JavaScript
@@ -60,12 +63,16 @@ var utils = require('cordova/utils'),
  * @constructor
  * @param type  String the channel name
  */
-var Channel = function(type) {
+var Channel = function(type, sticky) {
     this.type = type;
+    // Map of guid -> function.
     this.handlers = {};
+    // 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired.
+    this.state = sticky ? 1 : 0;
+    // Used in sticky mode to remember args passed to fire().
+    this.fireArgs = null;
+    // Used by onHasSubscribersChange to know if there are any listeners.
     this.numHandlers = 0;
-    this.fired = false;
-    this.enabled = true;
     // Function that is called when the first listener is subscribed, or when
     // the last listener is unsubscribed.
     this.onHasSubscribersChange = null;
@@ -73,22 +80,27 @@ var Channel = function(type) {
     channel = {
         /**
          * Calls the provided function only after all of the channels specified
-         * have been fired.
+         * have been fired. All channels must be sticky channels.
          */
-        join: function (h, c) {
-            var i = c.length;
-            var len = i;
-            var f = function() {
-                if (!(--i)) h();
-            };
+        join: function(h, c) {
+            var len = c.length,
+                i = len,
+                f = function() {
+                    if (!(--i)) h();
+                };
             for (var j=0; j<len; j++) {
-                !c[j].fired?c[j].subscribeOnce(f):i--;
+                if (c[j].state == 0) {
+                    throw Error('Can only use join with sticky channels.')
+                }
+                c[j].subscribe(f);
             }
-            if (!i) h();
+            if (!len) h();
+        },
+        create: function(type) {
+            return channel[type] = new Channel(type, false);
         },
-        create: function (type, opts) {
-            channel[type] = new Channel(type, opts);
-            return channel[type];
+        createSticky: function(type) {
+            return channel[type] = new Channel(type, true);
         },
 
         /**
@@ -106,13 +118,7 @@ var Channel = function(type) {
          */
         waitForInitialization: function(feature) {
             if (feature) {
-                var c = null;
-                if (this[feature]) {
-                    c = this[feature];
-                }
-                else {
-                    c = this.create(feature);
-                }
+                var c = channel[feature] || this.createSticky(feature);
                 this.deviceReadyChannelsMap[feature] = c;
                 this.deviceReadyChannelsArray.push(c);
             }
@@ -132,7 +138,7 @@ var Channel = function(type) {
     };
 
 function forceFunction(f) {
-    if (f === null || f === undefined || typeof f != 'function') throw "Function required as first argument!";
+    if (typeof f != 'function') throw "Function required as first argument!";
 }
 
 /**
@@ -142,67 +148,46 @@ function forceFunction(f) {
  * and a guid that can be used to stop subscribing to the channel.
  * Returns the guid.
  */
-Channel.prototype.subscribe = function(f, c, g) {
+Channel.prototype.subscribe = function(f, c) {
     // need a function to call
     forceFunction(f);
+    if (this.state == 2) {
+        f.apply(c || this, this.fireArgs);
+        return;
+    }
 
-    var func = f;
+    var func = f,
+        guid = f.observer_guid;
     if (typeof c == "object") { func = utils.close(c, f); }
 
-    g = g || func.observer_guid || f.observer_guid;
-    if (!g) {
+    if (!guid) {
         // first time any channel has seen this subscriber
-        g = nextGuid++;
+        guid = '' + nextGuid++;
     }
-    func.observer_guid = g;
-    f.observer_guid = g;
+    func.observer_guid = guid;
+    f.observer_guid = guid;
 
     // Don't add the same handler more than once.
-    if (!this.handlers[g]) {
-        this.handlers[g] = func;
+    if (!this.handlers[guid]) {
+        this.handlers[guid] = func;
         this.numHandlers++;
         if (this.numHandlers == 1) {
             this.onHasSubscribersChange && this.onHasSubscribersChange();
         }
-        if (this.fired) func.apply(this, this.fireArgs);
     }
-    return g;
-};
-
-/**
- * Like subscribe but the function is only called once and then it
- * auto-unsubscribes itself.
- */
-Channel.prototype.subscribeOnce = function(f, c) {
-    // need a function to call
-    forceFunction(f);
-
-    var g = null;
-    var _this = this;
-    if (this.fired) {
-        f.apply(c || null, this.fireArgs);
-    } else {
-        g = this.subscribe(function() {
-            _this.unsubscribe(g);
-            f.apply(c || null, arguments);
-        });
-        f.observer_guid = g;
-    }
-    return g;
 };
 
 /**
  * Unsubscribes the function with the given guid from the channel.
  */
-Channel.prototype.unsubscribe = function(g) {
+Channel.prototype.unsubscribe = function(f) {
     // need a function to unsubscribe
-    if (g === null || g === undefined) { throw "You must pass _something_ into Channel.unsubscribe"; }
+    forceFunction(f);
 
-    if (typeof g == 'function') { g = g.observer_guid; }
-    var handler = this.handlers[g];
+    var guid = f.observer_guid,
+        handler = this.handlers[guid];
     if (handler) {
-        if (handler.observer_guid) handler.observer_guid=null;
-        delete this.handlers[g];
+        delete this.handlers[guid];
         this.numHandlers--;
         if (this.numHandlers == 0) {
             this.onHasSubscribersChange && this.onHasSubscribersChange();
@@ -214,10 +199,14 @@ Channel.prototype.unsubscribe = function(g) {
  * Calls all functions subscribed to this channel.
  */
 Channel.prototype.fire = function(e) {
-    if (this.enabled) {
-        var fail = false;
-        this.fired = true;
-        this.fireArgs = arguments;
+    var fail = false,
+        fireArgs = Array.prototype.slice.call(arguments);
+    // Apply stickiness.
+    if (this.state == 1) {
+        this.state = 2;
+        this.fireArgs = fireArgs;
+    }
+    if (this.numHandlers) {
         // Copy the values first so that it is safe to modify it from within
         // callbacks.
         var toCall = [];
@@ -225,33 +214,36 @@ Channel.prototype.fire = function(e) {
             toCall.push(this.handlers[item]);
         }
         for (var i = 0; i < toCall.length; ++i) {
-            var rv = (toCall[i].apply(this, arguments)===false);
-            fail = fail || rv;
+            toCall[i].apply(this, fireArgs);
+        }
+        if (this.state == 2 && this.numHandlers) {
+            this.numHandlers = 0;
+            this.handlers = {};
+            this.onHasSubscribersChange && this.onHasSubscribersChange();
         }
-        return !fail;
     }
-    return true;
 };
 
+
 // defining them here so they are ready super fast!
 // DOM event that is received when the web page is loaded and parsed.
-channel.create('onDOMContentLoaded');
+channel.createSticky('onDOMContentLoaded');
 
 // Event to indicate the Cordova native side is ready.
-channel.create('onNativeReady');
+channel.createSticky('onNativeReady');
 
 // Event to indicate that all Cordova JavaScript objects have been created
 // and it's time to run plugin constructors.
-channel.create('onCordovaReady');
+channel.createSticky('onCordovaReady');
 
 // Event to indicate that device properties are available
-channel.create('onCordovaInfoReady');
+channel.createSticky('onCordovaInfoReady');
 
 // Event to indicate that the connection property has been set.
-channel.create('onCordovaConnectionReady');
+channel.createSticky('onCordovaConnectionReady');
 
 // Event to indicate that Cordova is ready
-channel.create('onDeviceReady');
+channel.createSticky('onDeviceReady');
 
 // Event to indicate a resume lifecycle event
 channel.create('onResume');
@@ -260,7 +252,7 @@ channel.create('onResume');
 channel.create('onPause');
 
 // Event to indicate a destroy lifecycle event
-channel.create('onDestroy');
+channel.createSticky('onDestroy');
 
 // Channels that must fire before "deviceready" is fired.
 channel.waitForInitialization('onCordovaReady');

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/common/plugin/device.js
----------------------------------------------------------------------
diff --git a/lib/common/plugin/device.js b/lib/common/plugin/device.js
index 3477ff8..905bfe1 100644
--- a/lib/common/plugin/device.js
+++ b/lib/common/plugin/device.js
@@ -41,7 +41,7 @@ function Device() {
 
     var me = this;
 
-    channel.onCordovaReady.subscribeOnce(function() {
+    channel.onCordovaReady.subscribe(function() {
         me.getInfo(function(info) {
             me.available = true;
             me.platform = info.platform;

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/common/plugin/network.js
----------------------------------------------------------------------
diff --git a/lib/common/plugin/network.js b/lib/common/plugin/network.js
index 637ea89..adaba5a 100644
--- a/lib/common/plugin/network.js
+++ b/lib/common/plugin/network.js
@@ -41,7 +41,7 @@ var NetworkConnection = function () {
 
     var me = this;
 
-    channel.onCordovaReady.subscribeOnce(function() {
+    channel.onCordovaReady.subscribe(function() {
         me.getInfo(function (info) {
             me.type = info;
             if (info === "none") {

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/cordova.js
----------------------------------------------------------------------
diff --git a/lib/cordova.js b/lib/cordova.js
index e1650fa..fc4a744 100644
--- a/lib/cordova.js
+++ b/lib/cordova.js
@@ -50,11 +50,7 @@ var documentEventHandlers = {},
 document.addEventListener = function(evt, handler, capture) {
     var e = evt.toLowerCase();
     if (typeof documentEventHandlers[e] != 'undefined') {
-        if (evt === 'deviceready') {
-            documentEventHandlers[e].subscribeOnce(handler);
-        } else {
-            documentEventHandlers[e].subscribe(handler);
-        }
+        documentEventHandlers[e].subscribe(handler);
     } else {
         m_document_addEventListener.call(document, evt, handler, capture);
     }
@@ -117,6 +113,9 @@ var cordova = {
     addWindowEventHandler:function(event) {
         return (windowEventHandlers[event] = channel.create(event));
     },
+    addStickyDocumentEventHandler:function(event) {
+        return (documentEventHandlers[event] = channel.createSticky(event));
+    },
     addDocumentEventHandler:function(event) {
         return (documentEventHandlers[event] = channel.create(event));
     },
@@ -234,7 +233,7 @@ var cordova = {
         }
     },
     addConstructor: function(func) {
-        channel.onCordovaReady.subscribeOnce(function() {
+        channel.onCordovaReady.subscribe(function() {
             try {
                 func();
             } catch(e) {
@@ -247,6 +246,6 @@ var cordova = {
 // Register pause, resume and deviceready channels as events on document.
 channel.onPause = cordova.addDocumentEventHandler('pause');
 channel.onResume = cordova.addDocumentEventHandler('resume');
-channel.onDeviceReady = cordova.addDocumentEventHandler('deviceready');
+channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready');
 
 module.exports = cordova;

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/ios/exec.js
----------------------------------------------------------------------
diff --git a/lib/ios/exec.js b/lib/ios/exec.js
index 95b49e8..5d91540 100644
--- a/lib/ios/exec.js
+++ b/lib/ios/exec.js
@@ -65,7 +65,7 @@ function shouldBundleCommandJson() {
 }
 
 function iOSExec() {
-    if (!channel.onCordovaReady.fired) {
+    if (channel.onCordovaReady.state != 2) {
         utils.alert("ERROR: Attempting to call cordova.exec()" +
               " before 'deviceready'. Ignoring.");
         return;

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/scripts/bootstrap.js
----------------------------------------------------------------------
diff --git a/lib/scripts/bootstrap.js b/lib/scripts/bootstrap.js
index 2bf8075..6aa08af 100644
--- a/lib/scripts/bootstrap.js
+++ b/lib/scripts/bootstrap.js
@@ -69,7 +69,7 @@
         };
 
     // boot up once native side is ready
-    channel.onNativeReady.subscribeOnce(_self.boot);
+    channel.onNativeReady.subscribe(_self.boot);
 
     // _nativeReady is global variable that the native side can set
     // to signify that the native code is ready. It is a global since

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/lib/tizen/plugin/tizen/Device.js
----------------------------------------------------------------------
diff --git a/lib/tizen/plugin/tizen/Device.js b/lib/tizen/plugin/tizen/Device.js
index 908f05d..c5532fb 100644
--- a/lib/tizen/plugin/tizen/Device.js
+++ b/lib/tizen/plugin/tizen/Device.js
@@ -45,7 +45,7 @@ function Device() {
         console.log("error initializing cordova: " + error);
     }
 
-    channel.onCordovaReady.subscribeOnce(function() {
+    channel.onCordovaReady.subscribe(function() {
         me.getDeviceInfo(onSuccessCallback, onErrorCallback);
     });
 }

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/c6917ee5/test/test.channel.js
----------------------------------------------------------------------
diff --git a/test/test.channel.js b/test/test.channel.js
index e71f993..cd14d38 100644
--- a/test/test.channel.js
+++ b/test/test.channel.js
@@ -21,266 +21,315 @@
 
 describe("channel", function () {
     var channel = require('cordova/channel'),
-        c;
-
+        multiChannel,
+        stickyChannel;
+
+    function callCount(spy) {
+        return spy.argsForCall.length;
+    }
+    function expectCallCount(spy, count) {
+        expect(callCount(spy)).toEqual(count);
+    }
     beforeEach(function() {
-        c = channel.create('masterexploder');
+        multiChannel = channel.create('multiChannel');
+        stickyChannel = channel.createSticky('stickyChannel');
     });
 
     describe("subscribe method", function() {
         it("should throw an exception if no function is provided", function() {
             expect(function() {
-                c.subscribe();
+                multiChannel.subscribe();
             }).toThrow();
 
             expect(function() {
-                c.subscribe(null);
+                multiChannel.subscribe(null);
             }).toThrow();
 
             expect(function() {
-                c.subscribe(undefined);
+                multiChannel.subscribe(undefined);
             }).toThrow();
 
             expect(function() {
-                c.subscribe({apply:function(){},call:function(){}});
+                multiChannel.subscribe({apply:function(){},call:function(){}});
             }).toThrow();
         });
         it("should not change number of handlers if no function is provided", function() {
-            var initialLength = c.numHandlers;
+            var initialLength = multiChannel.numHandlers;
 
             try {
-                c.subscribe();
+                multiChannel.subscribe();
             } catch(e) {}
 
-            expect(c.numHandlers).toEqual(initialLength);
+            expect(multiChannel.numHandlers).toEqual(initialLength);
 
             try {
-                c.subscribe(null);
+                multiChannel.subscribe(null);
             } catch(e) {}
 
-            expect(c.numHandlers).toEqual(initialLength);
+            expect(multiChannel.numHandlers).toEqual(initialLength);
         });
         it("should not change number of handlers when subscribing same function multiple times", function() {
-            var initialLength = c.numHandlers;
-            var handler = function(){};
-
-            c.subscribe(handler);
-            c.subscribe(handler);
-            c.subscribe(handler);
-
-            expect(c.numHandlers).toEqual(initialLength+1);
-        });
-        it("should be able to use the same function with multiple channels.", function() {
-            var c2 = channel.create('jables');
             var handler = function(){};
 
-            c.subscribe(handler);
-            c2.subscribe(handler);
+            multiChannel.subscribe(handler);
+            multiChannel.subscribe(handler);
+            stickyChannel.subscribe(handler);
+            stickyChannel.subscribe(handler);
 
-            expect(c.numHandlers).toEqual(1);
-            expect(c2.numHandlers).toEqual(1);
+            expect(multiChannel.numHandlers).toEqual(1);
+            expect(stickyChannel.numHandlers).toEqual(1);
         });
     });
 
     describe("unsubscribe method", function() {
         it("should throw an exception if passed in null or undefined", function() {
             expect(function() {
-                c.unsubscribe();
+                multiChannel.unsubscribe();
             }).toThrow();
             expect(function() {
-                c.unsubscribe(null);
+                multiChannel.unsubscribe(null);
             }).toThrow();
         });
         it("should not decrement numHandlers if unsubscribing something that does not exist", function() {
-            var initialLength = c.numHandlers;
-            c.unsubscribe('blah');
-            expect(c.numHandlers).toEqual(initialLength);
-            c.unsubscribe(2);
-            expect(c.numHandlers).toEqual(initialLength);
-            c.unsubscribe({balls:false});
-            expect(c.numHandlers).toEqual(initialLength);
+            multiChannel.subscribe(function() {});
+            multiChannel.unsubscribe(function() {});
+            expect(multiChannel.numHandlers).toEqual(1);
         });
         it("should change the handlers length appropriately", function() {
             var firstHandler = function() {};
             var secondHandler = function() {};
             var thirdHandler = function() {};
 
-            c.subscribe(firstHandler);
-            c.subscribe(secondHandler);
-            c.subscribe(thirdHandler);
-
-            var initialLength = c.numHandlers;
+            multiChannel.subscribe(firstHandler);
+            multiChannel.subscribe(secondHandler);
+            multiChannel.subscribe(thirdHandler);
+            expect(multiChannel.numHandlers).toEqual(3);
 
-            c.unsubscribe(thirdHandler);
+            multiChannel.unsubscribe(thirdHandler);
+            expect(multiChannel.numHandlers).toEqual(2);
 
-            expect(c.numHandlers).toEqual(initialLength - 1);
+            multiChannel.unsubscribe(firstHandler);
+            multiChannel.unsubscribe(secondHandler);
 
-            c.unsubscribe(firstHandler);
-            c.unsubscribe(secondHandler);
-
-            expect(c.numHandlers).toEqual(0);
+            expect(multiChannel.numHandlers).toEqual(0);
         });
         it("should not decrement handlers length more than once if unsubing a single handler", function() {
             var firstHandler = function(){};
-            c.subscribe(firstHandler);
+            multiChannel.subscribe(firstHandler);
 
-            expect(c.numHandlers).toEqual(1);
+            expect(multiChannel.numHandlers).toEqual(1);
 
-            c.unsubscribe(firstHandler);
-            c.unsubscribe(firstHandler);
-            c.unsubscribe(firstHandler);
-            c.unsubscribe(firstHandler);
+            multiChannel.unsubscribe(firstHandler);
+            multiChannel.unsubscribe(firstHandler);
+            multiChannel.unsubscribe(firstHandler);
+            multiChannel.unsubscribe(firstHandler);
 
-            expect(c.numHandlers).toEqual(0);
+            expect(multiChannel.numHandlers).toEqual(0);
         });
         it("should not unregister a function registered with a different handler", function() {
             var cHandler = function(){};
             var c2Handler = function(){};
             var c2 = channel.create('jables');
-            c.subscribe(cHandler);
+            multiChannel.subscribe(cHandler);
             c2.subscribe(c2Handler);
 
-            expect(c.numHandlers).toEqual(1);
+            expect(multiChannel.numHandlers).toEqual(1);
             expect(c2.numHandlers).toEqual(1);
 
-            c.unsubscribe(c2Handler);
+            multiChannel.unsubscribe(c2Handler);
             c2.unsubscribe(cHandler);
 
-            expect(c.numHandlers).toEqual(1);
+            expect(multiChannel.numHandlers).toEqual(1);
             expect(c2.numHandlers).toEqual(1);
         });
-        it("should be able to unsubscribe a subscribeOnce.", function() {
-            var handler = function(){};
-            c.subscribeOnce(handler);
-
-            expect(c.numHandlers).toEqual(1);
-
-            c.unsubscribe(handler);
-
-            expect(c.numHandlers).toEqual(0);
-        });
     });
 
-    describe("fire method", function() {
+    function commonFireTests(multi) {
         it("should fire all subscribed handlers", function() {
+            var testChannel = multi ? multiChannel : stickyChannel;
             var handler = jasmine.createSpy();
             var anotherOne = jasmine.createSpy();
 
-            c.subscribe(handler);
-            c.subscribe(anotherOne);
+            testChannel.subscribe(handler);
+            testChannel.subscribe(anotherOne);
+
+            testChannel.fire();
 
-            c.fire();
+            expectCallCount(handler, 1);
+            expectCallCount(anotherOne, 1);
+        });
+        it("should pass params to handlers", function() {
+            var testChannel = multi ? multiChannel : stickyChannel;
+            var handler = jasmine.createSpy();
+
+            testChannel.subscribe(handler);
 
-            expect(handler).toHaveBeenCalled();
-            expect(anotherOne).toHaveBeenCalled();
+            testChannel.fire(1, 2, 3);
+            expect(handler.argsForCall[0]).toEqual({0:1, 1:2, 2:3});
         });
         it("should not fire a handler that was unsubscribed", function() {
+            var testChannel = multi ? multiChannel : stickyChannel;
             var handler = jasmine.createSpy();
             var anotherOne = jasmine.createSpy();
 
-            c.subscribe(handler);
-            c.subscribe(anotherOne);
-            c.unsubscribe(handler);
+            testChannel.subscribe(handler);
+            testChannel.subscribe(anotherOne);
+            testChannel.unsubscribe(handler);
 
-            c.fire();
+            testChannel.fire();
 
-            expect(handler).not.toHaveBeenCalled();
-            expect(anotherOne).toHaveBeenCalled();
+            expectCallCount(handler, 0);
+            expectCallCount(anotherOne, 1);
         });
         it("should not fire a handler more than once if it was subscribed more than once", function() {
-            var count = 0;
-            var handler = jasmine.createSpy().andCallFake(function() { count++; });
+            var testChannel = multi ? multiChannel : stickyChannel;
+            var handler = jasmine.createSpy();
 
-            c.subscribe(handler);
-            c.subscribe(handler);
-            c.subscribe(handler);
+            testChannel.subscribe(handler);
+            testChannel.subscribe(handler);
+            testChannel.subscribe(handler);
 
-            c.fire();
+            testChannel.fire();
 
-            expect(handler).toHaveBeenCalled();
-            expect(count).toEqual(1);
+            expectCallCount(handler, 1);
         });
         it("handler should be called when subscribed, removed, and subscribed again", function() {
-            var count = 0;
-            var handler = jasmine.createSpy().andCallFake(function() { count++; });
-
-            c.subscribe(handler);
-            c.unsubscribe(handler);
-            c.subscribe(handler);
+            var testChannel = multi ? multiChannel : stickyChannel;
+            var handler = jasmine.createSpy();
 
-            c.fire();
+            testChannel.subscribe(handler);
+            testChannel.unsubscribe(handler);
+            testChannel.subscribe(handler);
 
-            expect(handler).toHaveBeenCalled();
-            expect(count).toEqual(1);
+            testChannel.fire();
 
+            expectCallCount(handler, 1);
+        });
+        it("should not prevent a callback from firing when it is removed during firing.", function() {
+            var testChannel = multi ? multiChannel : stickyChannel;
+            var handler = jasmine.createSpy().andCallFake(function() { testChannel.unsubscribe(handler2); });
+            var handler2 = jasmine.createSpy();
+            testChannel.subscribe(handler);
+            testChannel.subscribe(handler2);
+            testChannel.fire();
+            expectCallCount(handler, 1);
+            expectCallCount(handler2, 1);
         });
+    }
+    describe("fire method for sticky channels", function() {
+        commonFireTests(false);
         it("should instantly trigger the callback if the event has already been fired", function () {
-            var chan = channel.create("foo"),
-                before = jasmine.createSpy('before'),
+            var before = jasmine.createSpy('before'),
                 after = jasmine.createSpy('after');
 
-            chan.subscribe(before);
-            chan.fire();
-            chan.subscribe(after);
+            stickyChannel.subscribe(before);
+            stickyChannel.fire(1, 2, 3);
+            stickyChannel.subscribe(after);
 
-            expect(before).toHaveBeenCalled();
-            expect(after).toHaveBeenCalled();
+            expectCallCount(before, 1);
+            expectCallCount(after, 1);
+            expect(after.argsForCall[0]).toEqual({0:1, 1:2, 2:3});
         });
         it("should instantly trigger the callback if the event is currently being fired.", function () {
-            var handler1 = jasmine.createSpy().andCallFake(function() { c.subscribe(handler2); }),
+            var handler1 = jasmine.createSpy().andCallFake(function() { stickyChannel.subscribe(handler2); }),
                 handler2 = jasmine.createSpy().andCallFake(function(arg1) { expect(arg1).toEqual('foo');});
 
-            c.subscribe(handler1);
-            c.fire('foo');
+            stickyChannel.subscribe(handler1);
+            stickyChannel.fire('foo');
 
-            expect(handler2).toHaveBeenCalled();
+            expectCallCount(handler2, 1);
+        });
+        it("should unregister all handlers after being fired.", function() {
+            var handler = jasmine.createSpy();
+            stickyChannel.subscribe(handler);
+            stickyChannel.fire();
+            stickyChannel.fire();
+            expectCallCount(handler, 1);
         });
     });
-    describe("subscribeOnce method", function() {
-        it("should be unregistered after being fired.", function() {
-            var count = 0;
-            var handler = jasmine.createSpy().andCallFake(function() { count++; });
-            c.subscribeOnce(handler);
-            c.fire();
-            c.fire();
-            expect(count).toEqual(1);
+    describe("fire method for multi channels", function() {
+        commonFireTests(true);
+        it("should not trigger the callback if the event has already been fired", function () {
+            var before = jasmine.createSpy('before'),
+                after = jasmine.createSpy('after');
+
+            multiChannel.subscribe(before);
+            multiChannel.fire();
+            multiChannel.subscribe(after);
+
+            expectCallCount(before, 1);
+            expectCallCount(after, 0);
         });
-        it("should be safe to add listeners from within callback.", function() {
-            var count = 0;
-            var handler = jasmine.createSpy().andCallFake(function() { count++; c.subscribeOnce(handler2); });
-            var handler2 = jasmine.createSpy().andCallFake(function() { count++; });
-            c.subscribeOnce(handler);
-            c.fire();
-            expect(count).toEqual(2);
+        it("should not trigger the callback if the event is currently being fired.", function () {
+            var handler1 = jasmine.createSpy().andCallFake(function() { multiChannel.subscribe(handler2); }),
+                handler2 = jasmine.createSpy();
+
+            multiChannel.subscribe(handler1);
+            multiChannel.fire();
+            multiChannel.fire();
+
+            expectCallCount(handler1, 2);
+            expectCallCount(handler2, 1);
         });
-        it("should not prevent a callback from firing when it is removed during firing.", function() {
-            var count = 0;
-            var handler = jasmine.createSpy().andCallFake(function() { count++; c.unsubscribe(handler2); });
-            var handler2 = jasmine.createSpy().andCallFake(function() { count++; });
-            c.subscribeOnce(handler);
-            c.subscribeOnce(handler2);
-            c.fire();
-            expect(count).toEqual(2);
+        it("should not unregister handlers after being fired.", function() {
+            var handler = jasmine.createSpy();
+            multiChannel.subscribe(handler);
+            multiChannel.fire();
+            multiChannel.fire();
+            expectCallCount(handler, 2);
+        });
+    });
+    describe("channel.join()", function() {
+        it("should be called when all functions start unfired", function() {
+            var handler = jasmine.createSpy(),
+                stickyChannel2 = channel.createSticky('stickyChannel');
+            channel.join(handler, [stickyChannel, stickyChannel2]);
+            expectCallCount(handler, 0);
+            stickyChannel.fire();
+            expectCallCount(handler, 0);
+            stickyChannel2.fire();
+            expectCallCount(handler, 1);
+        });
+        it("should be called when one functions start fired", function() {
+            var handler = jasmine.createSpy(),
+                stickyChannel2 = channel.createSticky('stickyChannel');
+            stickyChannel.fire();
+            channel.join(handler, [stickyChannel, stickyChannel2]);
+            expectCallCount(handler, 0);
+            stickyChannel2.fire();
+            expectCallCount(handler, 1);
+        });
+        it("should be called when all functions start fired", function() {
+            var handler = jasmine.createSpy(),
+                stickyChannel2 = channel.createSticky('stickyChannel');
+            stickyChannel.fire();
+            stickyChannel2.fire();
+            channel.join(handler, [stickyChannel, stickyChannel2]);
+            expectCallCount(handler, 1);
+        });
+        it("should throw if a channel is not sticky", function() {
+            expect(function() {
+                channel.join(function(){}, [stickyChannel, multiChannel]);
+            }).toThrow();
         });
     });
     describe("onHasSubscribersChange", function() {
         it("should be called only when the first subscriber is added and last subscriber is removed.", function() {
             var handler = jasmine.createSpy().andCallFake(function() {
-                var callCount = handler.argsForCall.length;
-                if (callCount == 1) {
+                if (callCount(handler) == 1) {
                     expect(this.numHandlers).toEqual(1);
                 } else {
                     expect(this.numHandlers).toEqual(0);
                 }
             });
-            c.onHasSubscribersChange = handler;
+            multiChannel.onHasSubscribersChange = handler;
             function foo1() {}
             function foo2() {}
-            c.subscribe(foo1);
-            c.subscribe(foo2);
-            c.unsubscribe(foo1);
-            c.unsubscribe(foo2);
-            expect(handler.argsForCall.length).toEqual(2);
+            multiChannel.subscribe(foo1);
+            multiChannel.subscribe(foo2);
+            multiChannel.unsubscribe(foo1);
+            multiChannel.unsubscribe(foo2);
+            expectCallCount(handler, 2);
         });
     });
 });