summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/utils/timer
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/static/src/utils/timer')
-rw-r--r--addons/mail/static/src/utils/timer/timer.js165
-rw-r--r--addons/mail/static/src/utils/timer/timer_tests.js427
2 files changed, 592 insertions, 0 deletions
diff --git a/addons/mail/static/src/utils/timer/timer.js b/addons/mail/static/src/utils/timer/timer.js
new file mode 100644
index 00000000..56d7f58e
--- /dev/null
+++ b/addons/mail/static/src/utils/timer/timer.js
@@ -0,0 +1,165 @@
+odoo.define('mail/static/src/utils/timer/timer.js', function (require) {
+'use strict';
+
+const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
+
+//------------------------------------------------------------------------------
+// Errors
+//------------------------------------------------------------------------------
+
+/**
+ * List of Timer errors.
+ */
+
+ /**
+ * Error when timer has been cleared with `.clear()` or `.reset()`. Used to
+ * let know caller of timer that the countdown has been aborted, which
+ * means the inner function will not be called. Usually caller should just
+ * accept it and kindly treated this error as a polite warning.
+ */
+ class TimerClearedError extends Error {
+ /**
+ * @override
+ */
+ constructor(timerId, ...args) {
+ super(...args);
+ this.name = 'TimerClearedError';
+ this.timerId = timerId;
+ }
+}
+
+//------------------------------------------------------------------------------
+// Private
+//------------------------------------------------------------------------------
+
+/**
+ * This class creates a timer which, when times out, calls a function.
+ * Note that the timer is not started on initialization (@see start method).
+ */
+class Timer {
+
+ /**
+ * @param {Object} env the OWL env
+ * @param {function} onTimeout
+ * @param {integer} duration
+ * @param {Object} [param3={}]
+ * @param {boolean} [param3.silentCancelationErrors=true] if unset, caller
+ * of timer will observe some errors that come from current timer calls
+ * that has been cleared with `.clear()` or `.reset()`.
+ * @see TimerClearedError for when timer has been aborted from `.clear()`
+ * or `.reset()`.
+ */
+ constructor(env, onTimeout, duration, { silentCancelationErrors = true } = {}) {
+ this.env = env;
+ /**
+ * Determine whether the timer has a pending timeout.
+ */
+ this.isRunning = false;
+ /**
+ * Duration, in milliseconds, until timer times out and calls the
+ * timeout function.
+ */
+ this._duration = duration;
+ /**
+ * Determine whether the caller of timer `.start()` and `.reset()`
+ * should observe cancelation errors from `.clear()` or `.reset()`.
+ */
+ this._hasSilentCancelationErrors = silentCancelationErrors;
+ /**
+ * The function that is called when the timer times out.
+ */
+ this._onTimeout = onTimeout;
+ /**
+ * Deferred of a currently pending invocation to inner function on
+ * timeout.
+ */
+ this._timeoutDeferred = undefined;
+ /**
+ * Internal reference of `setTimeout()` that is used to invoke function
+ * when timer times out. Useful to clear it when timer is cleared/reset.
+ */
+ this._timeoutId = undefined;
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Clear the timer, which basically sets the state of timer as if it was
+ * just instantiated, without being started. This function makes sense only
+ * when this timer is running.
+ */
+ clear() {
+ this.env.browser.clearTimeout(this._timeoutId);
+ this.isRunning = false;
+ if (!this._timeoutDeferred) {
+ return;
+ }
+ this._timeoutDeferred.reject(new TimerClearedError(this.id));
+ }
+
+ /**
+ * Reset the timer, i.e. the pending timeout is refreshed with initial
+ * duration. This function makes sense only when this timer is running.
+ */
+ async reset() {
+ this.clear();
+ await this.start();
+ }
+
+ /**
+ * Starts the timer, i.e. after a certain duration, it times out and calls
+ * a function back. This function makes sense only when this timer is not
+ * yet running.
+ *
+ * @throws {Error} in case the timer is already running.
+ */
+ async start() {
+ if (this.isRunning) {
+ throw new Error("Cannot start a timer that is currently running.");
+ }
+ this.isRunning = true;
+ const timeoutDeferred = makeDeferred();
+ this._timeoutDeferred = timeoutDeferred;
+ const timeoutId = this.env.browser.setTimeout(
+ () => {
+ this.isRunning = false;
+ timeoutDeferred.resolve(this._onTimeout());
+ },
+ this._duration
+ );
+ this._timeoutId = timeoutId;
+ let result;
+ try {
+ result = await timeoutDeferred;
+ } catch (error) {
+ if (
+ !this._hasSilentCancelationErrors ||
+ !(error instanceof TimerClearedError) ||
+ error.timerId !== this.id
+ ) {
+ // This branching should never happens.
+ // Still defined in case of programming error.
+ throw error;
+ }
+ } finally {
+ this.env.browser.clearTimeout(timeoutId);
+ this._timeoutDeferred = undefined;
+ this.isRunning = false;
+ }
+ return result;
+ }
+
+}
+
+/**
+ * Make external timer errors accessible from timer class.
+ */
+Object.assign(Timer, {
+ TimerClearedError,
+});
+
+return Timer;
+
+});
diff --git a/addons/mail/static/src/utils/timer/timer_tests.js b/addons/mail/static/src/utils/timer/timer_tests.js
new file mode 100644
index 00000000..e2d33e91
--- /dev/null
+++ b/addons/mail/static/src/utils/timer/timer_tests.js
@@ -0,0 +1,427 @@
+odoo.define('mail/static/src/utils/timer/timer_tests.js', function (require) {
+'use strict';
+
+const { afterEach, beforeEach, nextTick, start } = require('mail/static/src/utils/test_utils.js');
+const Timer = require('mail/static/src/utils/timer/timer.js');
+
+const { TimerClearedError } = Timer;
+
+QUnit.module('mail', {}, function () {
+QUnit.module('utils', {}, function () {
+QUnit.module('timer', {}, function () {
+QUnit.module('timer_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+ this.timers = [];
+
+ this.start = async (params) => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ // Important: tests should cleanly intercept cancelation errors that
+ // may result from this teardown.
+ for (const timer of this.timers) {
+ timer.clear();
+ }
+ afterEach(this);
+ },
+});
+
+QUnit.test('timer does not timeout on initialization', async function (assert) {
+ assert.expect(3);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 0
+ )
+ );
+
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out on immediate initialization"
+ );
+
+ await this.env.testUtils.advanceTime(0);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out from initialization after 0ms"
+ );
+
+ await this.env.testUtils.advanceTime(1000 * 1000);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out from initialization after 1000s"
+ );
+});
+
+QUnit.test('timer start (duration: 0ms)', async function (assert) {
+ assert.expect(2);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 0
+ )
+ );
+
+ this.timers[0].start();
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+
+ await this.env.testUtils.advanceTime(0);
+ assert.ok(
+ hasTimedOut,
+ "timer should have timed out on start after 0ms"
+ );
+});
+
+QUnit.test('timer start observe termination (duration: 0ms)', async function (assert) {
+ assert.expect(6);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => {
+ hasTimedOut = true;
+ return 'timeout_result';
+ },
+ 0
+ )
+ );
+
+ this.timers[0].start()
+ .then(result => {
+ assert.strictEqual(
+ result,
+ 'timeout_result',
+ "value returned by start should be value returned by function on timeout"
+ );
+ assert.step('timeout');
+ });
+ await nextTick();
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+ assert.verifySteps(
+ [],
+ "timer.start() should not have yet observed timeout"
+ );
+
+ await this.env.testUtils.advanceTime(0);
+ assert.ok(
+ hasTimedOut,
+ "timer should have timed out on start after 0ms"
+ );
+ assert.verifySteps(
+ ['timeout'],
+ "timer.start() should have observed timeout after 0ms"
+ );
+});
+
+QUnit.test('timer start (duration: 1000s)', async function (assert) {
+ assert.expect(5);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 1000 * 1000
+ )
+ );
+
+ this.timers[0].start();
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+
+ await this.env.testUtils.advanceTime(0);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out on start after 0ms"
+ );
+
+ await this.env.testUtils.advanceTime(1000);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out on start after 1000ms"
+ );
+
+ await this.env.testUtils.advanceTime(998 * 1000 + 999);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out on start after 9999ms"
+ );
+
+ await this.env.testUtils.advanceTime(1);
+ assert.ok(
+ hasTimedOut,
+ "timer should have timed out on start after 10s"
+ );
+});
+
+QUnit.test('[no cancelation intercept] timer start then immediate clear (duration: 0ms)', async function (assert) {
+ assert.expect(4);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 0
+ )
+ );
+
+ this.timers[0].start();
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+
+ this.timers[0].clear();
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start and clear"
+ );
+
+ await this.env.testUtils.advanceTime(0);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out after 0ms of clear"
+ );
+
+ await this.env.testUtils.advanceTime(1000);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out after 1s of clear"
+ );
+});
+
+QUnit.test('[no cancelation intercept] timer start then clear before timeout (duration: 1000ms)', async function (assert) {
+ assert.expect(4);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 1000
+ )
+ );
+
+ this.timers[0].start();
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+
+ await this.env.testUtils.advanceTime(999);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after 999ms of start"
+ );
+
+ this.timers[0].clear();
+ await this.env.testUtils.advanceTime(1);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out after 1ms of clear that happens 999ms after start (globally 1s await)"
+ );
+
+ await this.env.testUtils.advanceTime(1000);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out after 1001ms after clear (timer fully cleared)"
+ );
+});
+
+QUnit.test('[no cancelation intercept] timer start then reset before timeout (duration: 1000ms)', async function (assert) {
+ assert.expect(5);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 1000
+ )
+ );
+
+ this.timers[0].start();
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+
+ await this.env.testUtils.advanceTime(999);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out after 999ms of start"
+ );
+
+ this.timers[0].reset();
+ await this.env.testUtils.advanceTime(1);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out after 1ms of reset which happens 999ms after start"
+ );
+
+ await this.env.testUtils.advanceTime(998);
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out after 999ms of reset"
+ );
+
+ await this.env.testUtils.advanceTime(1);
+ assert.ok(
+ hasTimedOut,
+ "timer should not have timed out after 1s of reset"
+ );
+});
+
+QUnit.test('[with cancelation intercept] timer start then immediate clear (duration: 0ms)', async function (assert) {
+ assert.expect(5);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 0,
+ { silentCancelationErrors: false }
+ )
+ );
+
+ this.timers[0].start()
+ .then(() => {
+ throw new Error("timer.start() should not be resolved (should have been canceled by clear)");
+ })
+ .catch(error => {
+ assert.ok(
+ error instanceof TimerClearedError,
+ "Should generate a Timer cleared error (from `.clear()`)"
+ );
+ assert.step('timer_cleared');
+ });
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+ await nextTick();
+ assert.verifySteps([], "should not have observed cleared timer (timer not yet cleared)");
+
+ this.timers[0].clear();
+ await nextTick();
+ assert.verifySteps(
+ ['timer_cleared'],
+ "timer.start() should have observed it has been cleared"
+ );
+});
+
+QUnit.test('[with cancelation intercept] timer start then immediate reset (duration: 0ms)', async function (assert) {
+ assert.expect(9);
+
+ await this.start({
+ hasTimeControl: true,
+ });
+
+ let hasTimedOut = false;
+ this.timers.push(
+ new Timer(
+ this.env,
+ () => hasTimedOut = true,
+ 0,
+ { silentCancelationErrors: false }
+ )
+ );
+
+ this.timers[0].start()
+ .then(() => {
+ throw new Error("timer.start() should not observe a timeout");
+ })
+ .catch(error => {
+ assert.ok(error instanceof TimerClearedError, "Should generate a Timer cleared error (from `.reset()`)");
+ assert.step('timer_cleared');
+ });
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after start"
+ );
+ await nextTick();
+ assert.verifySteps([], "should not have observed cleared timer (timer not yet cleared)");
+
+ this.timers[0].reset()
+ .then(() => assert.step('timer_reset_timeout'));
+ await nextTick();
+ assert.verifySteps(
+ ['timer_cleared'],
+ "timer.start() should have observed it has been cleared"
+ );
+ assert.notOk(
+ hasTimedOut,
+ "timer should not have timed out immediately after reset"
+ );
+
+ await this.env.testUtils.advanceTime(0);
+ assert.ok(
+ hasTimedOut,
+ "timer should have timed out after reset timeout"
+ );
+ assert.verifySteps(
+ ['timer_reset_timeout'],
+ "timer.reset() should have observed it has timed out"
+ );
+});
+
+});
+});
+});
+
+});