Bug 1145187 - Add a Poller utility object for togglign on polling functions. r=jryans

This commit is contained in:
Jordan Santell 2015-05-04 11:46:30 -07:00
parent 8343dadf03
commit 07fd15b012
5 changed files with 269 additions and 0 deletions

View File

@ -55,6 +55,7 @@ EXTRA_JS_MODULES.devtools.shared += [
'node-attribute-parser.js',
'observable-object.js',
'options-view.js',
'poller.js',
'source-utils.js',
'telemetry.js',
'theme-switching.js',

View File

@ -0,0 +1,115 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
loader.lazyRequireGetter(this, "timers",
"resource://gre/modules/Timer.jsm");
loader.lazyRequireGetter(this, "defer",
"sdk/core/promise", true);
/**
* @constructor Poller
* Takes a function that is to be called on an interval,
* and can be turned on and off via methods to execute `fn` on the interval
* specified during `on`. If `fn` returns a promise, the polling waits for
* that promise to resolve before waiting the interval to call again.
*
* Specify the `wait` duration between polling here, and optionally
* an `immediate` boolean, indicating whether the function should be called
* immediately when toggling on.
*
* @param {function} fn
* @param {number} wait
* @param {boolean?} immediate
*/
function Poller (fn, wait, immediate) {
this._fn = fn;
this._wait = wait;
this._immediate = immediate;
this._poll = this._poll.bind(this);
this._preparePoll = this._preparePoll.bind(this);
}
exports.Poller = Poller;
/**
* Returns a boolean indicating whether or not poller
* is polling.
*
* @return {boolean}
*/
Poller.prototype.isPolling = function pollerIsPolling () {
return !!this._timer;
};
/**
* Turns polling on.
*
* @return {Poller}
*/
Poller.prototype.on = function pollerOn () {
if (this._destroyed) {
throw Error("Poller cannot be turned on after destruction.");
}
if (this._timer) {
this.off();
}
this._immediate ? this._poll() : this._preparePoll();
return this;
};
/**
* Turns off polling. Returns a promise that resolves when
* the last outstanding `fn` call finishes if it's an async function.
*
* @return {Promise}
*/
Poller.prototype.off = function pollerOff () {
let { resolve, promise } = defer();
if (this._timer) {
timers.clearTimeout(this._timer);
this._timer = null;
}
// Settle an inflight poll call before resolving
// if using a promise-backed poll function
if (this._inflight) {
this._inflight.then(resolve);
} else {
resolve();
}
return promise;
};
/**
* Turns off polling and removes the reference to the poller function.
* Resolves when the last outstanding `fn` call finishes if it's an async function.
*/
Poller.prototype.destroy = function pollerDestroy () {
return this.off().then(() => {
this._destroyed = true;
this._fn = null
});
};
Poller.prototype._preparePoll = function pollerPrepare () {
this._timer = timers.setTimeout(this._poll, this._wait);
};
Poller.prototype._poll = function pollerPoll () {
let response = this._fn();
if (response && typeof response.then === "function") {
// Store the most recent in-flight polling
// call so we can clean it up when disabling
this._inflight = response;
response.then(() => {
// Only queue up the next call if poller was not turned off
// while this async poll call was in flight.
if (this._timer) {
this._preparePoll();
}
});
} else {
this._preparePoll();
}
};

View File

@ -84,6 +84,7 @@ skip-if = e10s # Layouthelpers test should not run in a content page.
[browser_options-view-01.js]
[browser_outputparser.js]
skip-if = e10s # Test intermittently fails with e10s. Bug 1124162.
[browser_poller.js]
[browser_prefs-01.js]
[browser_prefs-02.js]
[browser_require_basic.js]

View File

@ -0,0 +1,131 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests the Poller class.
const { Poller } = devtools.require("devtools/shared/poller");
add_task(function* () {
let count1 = 0, count2 = 0, count3 = 0;
let poller1 = new Poller(function () {
count1++;
}, 1000000000, true);
let poller2 = new Poller(function () {
count2++;
}, 10);
let poller3 = new Poller(function () {
count3++;
}, 1000000000);
poller2.on();
ok(!poller1.isPolling(), "isPolling() returns false for an off poller");
ok(poller2.isPolling(), "isPolling() returns true for an on poller");
yield waitUntil(() => count2 > 10);
ok(count2 > 10, "poller that was turned on polled several times");
ok(count1 === 0, "poller that was never turned on never polled");
yield poller2.off();
let currentCount2 = count2;
poller1.on(); // Really high poll time!
poller3.on(); // Really high poll time!
yield waitUntil(() => count1 === 1);
ok(true, "Poller calls fn immediately when `immediate` is true");
ok(count3 === 0, "Poller does not call fn immediately when `immediate` is not set");
ok(count2 === currentCount2, "a turned off poller does not continue to poll");
yield poller2.off();
yield poller2.off();
yield poller2.off();
ok(true, "Poller.prototype.off() is idempotent");
// This should still have not polled a second time
is(count1, 1, "wait time works");
ok(poller1.isPolling(), "isPolling() returns true for an on poller");
ok(!poller2.isPolling(), "isPolling() returns false for an off poller");
});
add_task(function *() {
let count = -1;
// Create a poller that returns a promise.
// The promise is resolved asynchronously after adding 9 to the count, ensuring
// that on every poll, we have a multiple of 10.
let asyncPoller = new Poller(function () {
count++;
ok(!(count%10), `Async poller called with a multiple of 10: ${count}`);
return new Promise(function (resolve, reject) {
let add9 = 9;
let interval = setInterval(() => {
if (add9--) {
count++;
} else {
clearInterval(interval);
resolve();
}
}, 10);
});
});
asyncPoller.on(1);
yield waitUntil(() => count > 50);
yield asyncPoller.off();
});
add_task(function *() {
// Create a poller that returns a promise. This poll call
// is called immediately, and then subsequently turned off.
// The call to `off` should not resolve until the inflight call
// finishes.
let inflightFinished = null;
let pollCalls = 0;
let asyncPoller = new Poller(function () {
pollCalls++;
return new Promise(function (resolve, reject) {
setTimeout(() => {
inflightFinished = true;
resolve();
}, 1000);
});
}, 1, true);
asyncPoller.on();
yield asyncPoller.off();
ok(inflightFinished, "off() method does not resolve until remaining inflight poll calls finish");
is(pollCalls, 1, "should only be one poll call to occur before turning off polling");
});
add_task(function *() {
// Create a poller that returns a promise. This poll call
// is called immediately, and then subsequently turned off.
// The call to `off` should not resolve until the inflight call
// finishes.
let inflightFinished = null;
let pollCalls = 0;
let asyncPoller = new Poller(function () {
pollCalls++;
return new Promise(function (resolve, reject) {
setTimeout(() => {
inflightFinished = true;
resolve();
}, 1000);
});
}, 1, true);
asyncPoller.on();
yield asyncPoller.destroy();
ok(inflightFinished, "destroy() method does not resolve until remaining inflight poll calls finish");
is(pollCalls, 1, "should only be one poll call to occur before destroying polling");
try {
asyncPoller.on();
ok(false, "Calling on() after destruction should throw");
} catch (e) {
ok(true, "Calling on() after destruction should throw");
}
});

View File

@ -242,3 +242,24 @@ function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) {
yield gDevTools.closeToolbox(target);
}
}
/**
* Waits until a predicate returns true.
*
* @param function predicate
* Invoked once in a while until it returns true.
* @param number interval [optional]
* How often the predicate is invoked, in milliseconds.
*/
function waitUntil(predicate, interval = 10) {
if (predicate()) {
return Promise.resolve(true);
}
return new Promise(resolve => {
setTimeout(function() {
waitUntil(predicate).then(() => resolve(true));
}, interval);
});
}
// EventUtils just doesn't work!