Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Adding asap variations of the listener helpers.

`pubit.throttledListener` and `pubit.debouncedListener` now both take a third parameter, `asap`, which if true causes the aggregate listener to fire on the next turn of the event loop after the throttled/debounced listener is first called. In many cases, this gives the right blend between responsiveness and rate-limiting.
  • Loading branch information...
commit a53c6c7e8e5975d2ff3cdcffd0e386ca8d788eb9 1 parent f93ed3a
@domenic authored
View
32 lib/listenerHelpers.js
@@ -1,26 +1,35 @@
/*jshint curly: true, eqeqeq: true, immed: true, latedef: true, newcap: true, noarg: true, nonew: true, trailing: true, undef: true, white: true, es5: true, globalstrict: true, node: true */
"use strict";
-exports.throttledListener = function (aggregateListener, waitingPeriod) {
+exports.throttledListener = function (aggregateListener, waitingPeriod, asap) {
var pendingTimeout = null;
var aggregateArgs = [];
+ var asapCallHappened = false;
+
+ function onWaitingPeriodElapsed() {
+ aggregateListener.call(null, aggregateArgs);
+ pendingTimeout = null;
+ aggregateArgs = [];
+ }
return function () {
aggregateArgs.push(arguments[0]);
if (!pendingTimeout) {
- pendingTimeout = setTimeout(function () {
- aggregateListener.call(null, aggregateArgs);
- pendingTimeout = null;
- aggregateArgs = [];
- }, waitingPeriod);
+ if (asap && !asapCallHappened) {
+ process.nextTick(onWaitingPeriodElapsed);
+ asapCallHappened = true;
+ } else {
+ pendingTimeout = setTimeout(onWaitingPeriodElapsed, waitingPeriod);
+ }
}
};
};
-exports.debouncedListener = function (aggregateListener, waitingPeriod) {
+exports.debouncedListener = function (aggregateListener, waitingPeriod, asap) {
var pendingTimeout = null;
var aggregateArgs = [];
+ var asapCallHappened = false;
function onWaitingPeriodElapsed() {
aggregateListener.call(null, aggregateArgs);
@@ -31,7 +40,12 @@ exports.debouncedListener = function (aggregateListener, waitingPeriod) {
return function () {
aggregateArgs.push(arguments[0]);
- clearTimeout(pendingTimeout);
- pendingTimeout = setTimeout(onWaitingPeriodElapsed, waitingPeriod);
+ if (asap && !asapCallHappened) {
+ process.nextTick(onWaitingPeriodElapsed);
+ asapCallHappened = true;
+ } else {
+ clearTimeout(pendingTimeout);
+ pendingTimeout = setTimeout(onWaitingPeriodElapsed, waitingPeriod);
+ }
};
};
View
1  lib/pubit.js
@@ -5,7 +5,6 @@
// * publisher.mixinEmitter(that)? publish = pubit.makeEmitter(that)?
// * restricted event names? would be a constructor parameter
// * more argument validation (or remove argument validation, not sure yet)
-// * Debounced/asap variations of throttled listener.
// * think about the role of async (publisher? subscriber? always?)
var dict = require("dict");
View
74 test/4 listenerHelpers.coffee
@@ -72,6 +72,43 @@ describe "Listener-creating helpers", ->
sinon.assert.calledWithExactly(aggregateListener, ["A"])
+ describe "with the asap parameter set to true", ->
+ beforeEach ->
+ throttledListener = pubit.throttledListener(aggregateListener, 100, true)
+
+ it "should call the aggregate listener on the next turn with the first value", (next) ->
+ throttledListener(1)
+
+ # Should not be called synchronously, for consistency: throttledListener always returns a function that
+ # executes in a future turn of the event loop. It also allows us to aggregate all values that happen in
+ # this initial turn, as shown in the next test.
+ sinon.assert.notCalled(aggregateListener)
+
+ # After a turn of the event loop, it's been called.
+ process.nextTick ->
+ sinon.assert.calledWithExactly(aggregateListener, [1])
+ next()
+
+ it "should call the aggregate listener once with all initial values, then later as usual", (next) ->
+ throttledListener(1)
+ throttledListener(2)
+ throttledListener(3)
+
+ sinon.assert.notCalled(aggregateListener)
+
+ process.nextTick ->
+ sinon.assert.calledWithExactly(aggregateListener, [1, 2, 3])
+
+ throttledListener("A")
+ clock.tick(20)
+ throttledListener("B")
+ clock.tick(20)
+ throttledListener("C")
+ clock.tick(61)
+ sinon.assert.calledWithExactly(aggregateListener, ["A", "B", "C"])
+
+ next()
+
describe "debouncedListener", ->
debouncedListener = null
@@ -131,3 +168,40 @@ describe "Listener-creating helpers", ->
clock.tick(101)
sinon.assert.calledWithExactly(aggregateListener, ["A"])
+
+ describe "with the asap parameter set to true", ->
+ beforeEach ->
+ debouncedListener = pubit.debouncedListener(aggregateListener, 100, true)
+
+ it "should call the aggregate listener on the next turn with the first value", (next) ->
+ debouncedListener(1)
+
+ # Should not be called synchronously, for consistency: debouncedListener always returns a function that
+ # executes in a future turn of the event loop. It also allows us to aggregate all values that happen in
+ # this initial turn, as shown in the next test.
+ sinon.assert.notCalled(aggregateListener)
+
+ # After a turn of the event loop, it's been called.
+ process.nextTick ->
+ sinon.assert.calledWithExactly(aggregateListener, [1])
+ next()
+
+ it "should call the aggregate listener once with all initial values, then later as usual", (next) ->
+ debouncedListener(1)
+ debouncedListener(2)
+ debouncedListener(3)
+
+ sinon.assert.notCalled(aggregateListener)
+
+ process.nextTick ->
+ sinon.assert.calledWithExactly(aggregateListener, [1, 2, 3])
+
+ debouncedListener("A")
+ clock.tick(20)
+ debouncedListener("B")
+ clock.tick(20)
+ debouncedListener("C")
+ clock.tick(101)
+ sinon.assert.calledWithExactly(aggregateListener, ["A", "B", "C"])
+
+ next()
Please sign in to comment.
Something went wrong with that request. Please try again.