325 lines
9.4 KiB
JavaScript
325 lines
9.4 KiB
JavaScript
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS-IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview A timer class to which other classes and objects can listen on.
|
|
* This is only an abstraction above {@code setInterval}.
|
|
*
|
|
* @see ../demos/timers.html
|
|
*/
|
|
|
|
goog.provide('goog.Timer');
|
|
|
|
goog.require('goog.Promise');
|
|
goog.require('goog.events.EventTarget');
|
|
|
|
|
|
|
|
/**
|
|
* Class for handling timing events.
|
|
*
|
|
* @param {number=} opt_interval Number of ms between ticks (default: 1ms).
|
|
* @param {Object=} opt_timerObject An object that has {@code setTimeout},
|
|
* {@code setInterval}, {@code clearTimeout} and {@code clearInterval}
|
|
* (e.g., {@code window}).
|
|
* @constructor
|
|
* @extends {goog.events.EventTarget}
|
|
*/
|
|
goog.Timer = function(opt_interval, opt_timerObject) {
|
|
goog.events.EventTarget.call(this);
|
|
|
|
/**
|
|
* Number of ms between ticks
|
|
* @private {number}
|
|
*/
|
|
this.interval_ = opt_interval || 1;
|
|
|
|
/**
|
|
* An object that implements {@code setTimeout}, {@code setInterval},
|
|
* {@code clearTimeout} and {@code clearInterval}. We default to the window
|
|
* object. Changing this on {@link goog.Timer.prototype} changes the object
|
|
* for all timer instances which can be useful if your environment has some
|
|
* other implementation of timers than the {@code window} object.
|
|
* @private {{setTimeout:!Function, clearTimeout:!Function}}
|
|
*/
|
|
this.timerObject_ = /** @type {{setTimeout, clearTimeout}} */ (
|
|
opt_timerObject || goog.Timer.defaultTimerObject);
|
|
|
|
/**
|
|
* Cached {@code tick_} bound to the object for later use in the timer.
|
|
* @private {Function}
|
|
* @const
|
|
*/
|
|
this.boundTick_ = goog.bind(this.tick_, this);
|
|
|
|
/**
|
|
* Firefox browser often fires the timer event sooner (sometimes MUCH sooner)
|
|
* than the requested timeout. So we compare the time to when the event was
|
|
* last fired, and reschedule if appropriate. See also
|
|
* {@link goog.Timer.intervalScale}.
|
|
* @private {number}
|
|
*/
|
|
this.last_ = goog.now();
|
|
};
|
|
goog.inherits(goog.Timer, goog.events.EventTarget);
|
|
|
|
|
|
/**
|
|
* Maximum timeout value.
|
|
*
|
|
* Timeout values too big to fit into a signed 32-bit integer may cause overflow
|
|
* in FF, Safari, and Chrome, resulting in the timeout being scheduled
|
|
* immediately. It makes more sense simply not to schedule these timeouts, since
|
|
* 24.8 days is beyond a reasonable expectation for the browser to stay open.
|
|
*
|
|
* @private {number}
|
|
* @const
|
|
*/
|
|
goog.Timer.MAX_TIMEOUT_ = 2147483647;
|
|
|
|
|
|
/**
|
|
* A timer ID that cannot be returned by any known implementation of
|
|
* {@code window.setTimeout}. Passing this value to {@code window.clearTimeout}
|
|
* should therefore be a no-op.
|
|
*
|
|
* @private {number}
|
|
* @const
|
|
*/
|
|
goog.Timer.INVALID_TIMEOUT_ID_ = -1;
|
|
|
|
|
|
/**
|
|
* Whether this timer is enabled
|
|
* @type {boolean}
|
|
*/
|
|
goog.Timer.prototype.enabled = false;
|
|
|
|
|
|
/**
|
|
* An object that implements {@code setTimeout}, {@code setInterval},
|
|
* {@code clearTimeout} and {@code clearInterval}. We default to the global
|
|
* object. Changing {@code goog.Timer.defaultTimerObject} changes the object for
|
|
* all timer instances which can be useful if your environment has some other
|
|
* implementation of timers you'd like to use.
|
|
* @type {{setTimeout, clearTimeout}}
|
|
*/
|
|
goog.Timer.defaultTimerObject = goog.global;
|
|
|
|
|
|
/**
|
|
* Variable that controls the timer error correction. If the timer is called
|
|
* before the requested interval times {@code intervalScale}, which often
|
|
* happens on Mozilla, the timer is rescheduled.
|
|
* @see {@link #last_}
|
|
* @type {number}
|
|
*/
|
|
goog.Timer.intervalScale = 0.8;
|
|
|
|
|
|
/**
|
|
* Variable for storing the result of {@code setInterval}.
|
|
* @private {?number}
|
|
*/
|
|
goog.Timer.prototype.timer_ = null;
|
|
|
|
|
|
/**
|
|
* Gets the interval of the timer.
|
|
* @return {number} interval Number of ms between ticks.
|
|
*/
|
|
goog.Timer.prototype.getInterval = function() {
|
|
return this.interval_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the interval of the timer.
|
|
* @param {number} interval Number of ms between ticks.
|
|
*/
|
|
goog.Timer.prototype.setInterval = function(interval) {
|
|
this.interval_ = interval;
|
|
if (this.timer_ && this.enabled) {
|
|
// Stop and then start the timer to reset the interval.
|
|
this.stop();
|
|
this.start();
|
|
} else if (this.timer_) {
|
|
this.stop();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Callback for the {@code setTimeout} used by the timer.
|
|
* @private
|
|
*/
|
|
goog.Timer.prototype.tick_ = function() {
|
|
if (this.enabled) {
|
|
var elapsed = goog.now() - this.last_;
|
|
if (elapsed > 0 && elapsed < this.interval_ * goog.Timer.intervalScale) {
|
|
this.timer_ = this.timerObject_.setTimeout(
|
|
this.boundTick_, this.interval_ - elapsed);
|
|
return;
|
|
}
|
|
|
|
// Prevents setInterval from registering a duplicate timeout when called
|
|
// in the timer event handler.
|
|
if (this.timer_) {
|
|
this.timerObject_.clearTimeout(this.timer_);
|
|
this.timer_ = null;
|
|
}
|
|
|
|
this.dispatchTick();
|
|
// The timer could be stopped in the timer event handler.
|
|
if (this.enabled) {
|
|
this.timer_ =
|
|
this.timerObject_.setTimeout(this.boundTick_, this.interval_);
|
|
this.last_ = goog.now();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Dispatches the TICK event. This is its own method so subclasses can override.
|
|
*/
|
|
goog.Timer.prototype.dispatchTick = function() {
|
|
this.dispatchEvent(goog.Timer.TICK);
|
|
};
|
|
|
|
|
|
/**
|
|
* Starts the timer.
|
|
*/
|
|
goog.Timer.prototype.start = function() {
|
|
this.enabled = true;
|
|
|
|
// If there is no interval already registered, start it now
|
|
if (!this.timer_) {
|
|
// IMPORTANT!
|
|
// window.setInterval in FireFox has a bug - it fires based on
|
|
// absolute time, rather than on relative time. What this means
|
|
// is that if a computer is sleeping/hibernating for 24 hours
|
|
// and the timer interval was configured to fire every 1000ms,
|
|
// then after the PC wakes up the timer will fire, in rapid
|
|
// succession, 3600*24 times.
|
|
// This bug is described here and is already fixed, but it will
|
|
// take time to propagate, so for now I am switching this over
|
|
// to setTimeout logic.
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=376643
|
|
//
|
|
this.timer_ = this.timerObject_.setTimeout(this.boundTick_, this.interval_);
|
|
this.last_ = goog.now();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Stops the timer.
|
|
*/
|
|
goog.Timer.prototype.stop = function() {
|
|
this.enabled = false;
|
|
if (this.timer_) {
|
|
this.timerObject_.clearTimeout(this.timer_);
|
|
this.timer_ = null;
|
|
}
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.Timer.prototype.disposeInternal = function() {
|
|
goog.Timer.superClass_.disposeInternal.call(this);
|
|
this.stop();
|
|
delete this.timerObject_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Constant for the timer's event type.
|
|
* @const
|
|
*/
|
|
goog.Timer.TICK = 'tick';
|
|
|
|
|
|
/**
|
|
* Calls the given function once, after the optional pause.
|
|
* <p>
|
|
* The function is always called asynchronously, even if the delay is 0. This
|
|
* is a common trick to schedule a function to run after a batch of browser
|
|
* event processing.
|
|
*
|
|
* @param {function(this:SCOPE)|{handleEvent:function()}|null} listener Function
|
|
* or object that has a handleEvent method.
|
|
* @param {number=} opt_delay Milliseconds to wait; default is 0.
|
|
* @param {SCOPE=} opt_handler Object in whose scope to call the listener.
|
|
* @return {number} A handle to the timer ID.
|
|
* @template SCOPE
|
|
*/
|
|
goog.Timer.callOnce = function(listener, opt_delay, opt_handler) {
|
|
if (goog.isFunction(listener)) {
|
|
if (opt_handler) {
|
|
listener = goog.bind(listener, opt_handler);
|
|
}
|
|
} else if (listener && typeof listener.handleEvent == 'function') {
|
|
// using typeof to prevent strict js warning
|
|
listener = goog.bind(listener.handleEvent, listener);
|
|
} else {
|
|
throw Error('Invalid listener argument');
|
|
}
|
|
|
|
if (Number(opt_delay) > goog.Timer.MAX_TIMEOUT_) {
|
|
// Timeouts greater than MAX_INT return immediately due to integer
|
|
// overflow in many browsers. Since MAX_INT is 24.8 days, just don't
|
|
// schedule anything at all.
|
|
return goog.Timer.INVALID_TIMEOUT_ID_;
|
|
} else {
|
|
return goog.Timer.defaultTimerObject.setTimeout(listener, opt_delay || 0);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Clears a timeout initiated by {@link #callOnce}.
|
|
* @param {?number} timerId A timer ID.
|
|
*/
|
|
goog.Timer.clear = function(timerId) {
|
|
goog.Timer.defaultTimerObject.clearTimeout(timerId);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {number} delay Milliseconds to wait.
|
|
* @param {(RESULT|goog.Thenable<RESULT>|Thenable)=} opt_result The value
|
|
* with which the promise will be resolved.
|
|
* @return {!goog.Promise<RESULT>} A promise that will be resolved after
|
|
* the specified delay, unless it is canceled first.
|
|
* @template RESULT
|
|
*/
|
|
goog.Timer.promise = function(delay, opt_result) {
|
|
var timerKey = null;
|
|
return new goog
|
|
.Promise(function(resolve, reject) {
|
|
timerKey =
|
|
goog.Timer.callOnce(function() { resolve(opt_result); }, delay);
|
|
if (timerKey == goog.Timer.INVALID_TIMEOUT_ID_) {
|
|
reject(new Error('Failed to schedule timer.'));
|
|
}
|
|
})
|
|
.thenCatch(function(error) {
|
|
// Clear the timer. The most likely reason is "cancel" signal.
|
|
goog.Timer.clear(timerKey);
|
|
throw error;
|
|
});
|
|
};
|