michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "DeferredTask", michael@0: ]; michael@0: michael@0: /** michael@0: * Sets up a function or an asynchronous task whose execution can be triggered michael@0: * after a defined delay. Multiple attempts to run the task before the delay michael@0: * has passed are coalesced. The task cannot be re-entered while running, but michael@0: * can be executed again after a previous run finished. michael@0: * michael@0: * A common use case occurs when a data structure should be saved into a file michael@0: * every time the data changes, using asynchronous calls, and multiple changes michael@0: * to the data may happen within a short time: michael@0: * michael@0: * let saveDeferredTask = new DeferredTask(function* () { michael@0: * yield OS.File.writeAtomic(...); michael@0: * // Any uncaught exception will be reported. michael@0: * }, 2000); michael@0: * michael@0: * // The task is ready, but will not be executed until requested. michael@0: * michael@0: * The "arm" method can be used to start the internal timer that will result in michael@0: * the eventual execution of the task. Multiple attempts to arm the timer don't michael@0: * introduce further delays: michael@0: * michael@0: * saveDeferredTask.arm(); michael@0: * michael@0: * // The task will be executed in 2 seconds from now. michael@0: * michael@0: * yield waitOneSecond(); michael@0: * saveDeferredTask.arm(); michael@0: * michael@0: * // The task will be executed in 1 second from now. michael@0: * michael@0: * The timer can be disarmed to reset the delay, or just to cancel execution: michael@0: * michael@0: * saveDeferredTask.disarm(); michael@0: * saveDeferredTask.arm(); michael@0: * michael@0: * // The task will be executed in 2 seconds from now. michael@0: * michael@0: * When the internal timer fires and the execution of the task starts, the task michael@0: * cannot be canceled anymore. It is however possible to arm the timer again michael@0: * during the execution of the task, in which case the task will need to finish michael@0: * before the timer is started again, thus guaranteeing a time of inactivity michael@0: * between executions that is at least equal to the provided delay. michael@0: * michael@0: * The "finalize" method can be used to ensure that the task terminates michael@0: * properly. The promise it returns is resolved only after the last execution michael@0: * of the task is finished. To guarantee that the task is executed for the michael@0: * last time, the method prevents any attempt to arm the timer again. michael@0: * michael@0: * If the timer is already armed when the "finalize" method is called, then the michael@0: * task is executed immediately. If the task was already running at this point, michael@0: * then one last execution from start to finish will happen again, immediately michael@0: * after the current execution terminates. If the timer is not armed, the michael@0: * "finalize" method only ensures that any running task terminates. michael@0: * michael@0: * For example, during shutdown, you may want to ensure that any pending write michael@0: * is processed, using the latest version of the data if the timer is armed: michael@0: * michael@0: * AsyncShutdown.profileBeforeChange.addBlocker( michael@0: * "Example service: shutting down", michael@0: * () => saveDeferredTask.finalize() michael@0: * ); michael@0: * michael@0: * Instead, if you are going to delete the saved data from disk anyways, you michael@0: * might as well prevent any pending write from starting, while still ensuring michael@0: * that any write that is currently in progress terminates, so that the file is michael@0: * not in use anymore: michael@0: * michael@0: * saveDeferredTask.disarm(); michael@0: * saveDeferredTask.finalize().then(() => OS.File.remove(...)) michael@0: * .then(null, Components.utils.reportError); michael@0: */ michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", michael@0: "initWithCallback"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DeferredTask michael@0: michael@0: /** michael@0: * Sets up a task whose execution can be triggered after a delay. michael@0: * michael@0: * @param aTaskFn michael@0: * Function or generator function to execute. This argument is passed to michael@0: * the "Task.spawn" method every time the task should be executed. This michael@0: * task is never re-entered while running. michael@0: * @param aDelayMs michael@0: * Time between executions, in milliseconds. Multiple attempts to run michael@0: * the task before the delay has passed are coalesced. This time of michael@0: * inactivity is guaranteed to pass between multiple executions of the michael@0: * task, except on finalization, when the task may restart immediately michael@0: * after the previous execution finished. michael@0: */ michael@0: this.DeferredTask = function (aTaskFn, aDelayMs) { michael@0: this._taskFn = aTaskFn; michael@0: this._delayMs = aDelayMs; michael@0: } michael@0: michael@0: this.DeferredTask.prototype = { michael@0: /** michael@0: * Function or generator function to execute. michael@0: */ michael@0: _taskFn: null, michael@0: michael@0: /** michael@0: * Time between executions, in milliseconds. michael@0: */ michael@0: _delayMs: null, michael@0: michael@0: /** michael@0: * Indicates whether the task is currently requested to start again later, michael@0: * regardless of whether it is currently running. michael@0: */ michael@0: get isArmed() this._armed, michael@0: _armed: false, michael@0: michael@0: /** michael@0: * Indicates whether the task is currently running. This is always true when michael@0: * read from code inside the task function, but can also be true when read michael@0: * from external code, in case the task is an asynchronous generator function. michael@0: */ michael@0: get isRunning() !!this._runningPromise, michael@0: michael@0: /** michael@0: * Promise resolved when the current execution of the task terminates, or null michael@0: * if the task is not currently running. michael@0: */ michael@0: _runningPromise: null, michael@0: michael@0: /** michael@0: * nsITimer used for triggering the task after a delay, or null in case the michael@0: * task is running or there is no task scheduled for execution. michael@0: */ michael@0: _timer: null, michael@0: michael@0: /** michael@0: * Actually starts the timer with the delay specified on construction. michael@0: */ michael@0: _startTimer: function () michael@0: { michael@0: this._timer = new Timer(this._timerCallback.bind(this), this._delayMs, michael@0: Ci.nsITimer.TYPE_ONE_SHOT); michael@0: }, michael@0: michael@0: /** michael@0: * Requests the execution of the task after the delay specified on michael@0: * construction. Multiple calls don't introduce further delays. If the task michael@0: * is running, the delay will start when the current execution finishes. michael@0: * michael@0: * The task will always be executed on a different tick of the event loop, michael@0: * even if the delay specified on construction is zero. Multiple "arm" calls michael@0: * within the same tick of the event loop are guaranteed to result in a single michael@0: * execution of the task. michael@0: * michael@0: * @note By design, this method doesn't provide a way for the caller to detect michael@0: * when the next execution terminates, or collect a result. In fact, michael@0: * doing that would often result in duplicate processing or logging. If michael@0: * a special operation or error logging is needed on completion, it can michael@0: * be better handled from within the task itself, for example using a michael@0: * try/catch/finally clause in the task. The "finalize" method can be michael@0: * used in the common case of waiting for completion on shutdown. michael@0: */ michael@0: arm: function () michael@0: { michael@0: if (this._finalized) { michael@0: throw new Error("Unable to arm timer, the object has been finalized."); michael@0: } michael@0: michael@0: this._armed = true; michael@0: michael@0: // In case the timer callback is running, do not create the timer now, michael@0: // because this will be handled by the timer callback itself. Also, the michael@0: // timer is not restarted in case it is already running. michael@0: if (!this._runningPromise && !this._timer) { michael@0: this._startTimer(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Cancels any request for a delayed the execution of the task, though the michael@0: * task itself cannot be canceled in case it is already running. michael@0: * michael@0: * This method stops any currently running timer, thus the delay will restart michael@0: * from its original value in case the "arm" method is called again. michael@0: */ michael@0: disarm: function () { michael@0: this._armed = false; michael@0: if (this._timer) { michael@0: // Calling the "cancel" method and discarding the timer reference makes michael@0: // sure that the timer callback will not be called later, even if the michael@0: // timer thread has already posted the timer event on the main thread. michael@0: this._timer.cancel(); michael@0: this._timer = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that any pending task is executed from start to finish, while michael@0: * preventing any attempt to arm the timer again. michael@0: * michael@0: * - If the task is running and the timer is armed, then one last execution michael@0: * from start to finish will happen again, immediately after the current michael@0: * execution terminates, then the returned promise will be resolved. michael@0: * - If the task is running and the timer is not armed, the returned promise michael@0: * will be resolved when the current execution terminates. michael@0: * - If the task is not running and the timer is armed, then the task is michael@0: * started immediately, and the returned promise resolves when the new michael@0: * execution terminates. michael@0: * - If the task is not running and the timer is not armed, the method returns michael@0: * a resolved promise. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves After the last execution of the task is finished. michael@0: * @rejects Never. michael@0: */ michael@0: finalize: function () { michael@0: if (this._finalized) { michael@0: throw new Error("The object has been already finalized."); michael@0: } michael@0: this._finalized = true; michael@0: michael@0: // If the timer is armed, it means that the task is not running but it is michael@0: // scheduled for execution. Cancel the timer and run the task immediately. michael@0: if (this._timer) { michael@0: this.disarm(); michael@0: this._timerCallback(); michael@0: } michael@0: michael@0: // Wait for the operation to be completed, or resolve immediately. michael@0: if (this._runningPromise) { michael@0: return this._runningPromise; michael@0: } michael@0: return Promise.resolve(); michael@0: }, michael@0: _finalized: false, michael@0: michael@0: /** michael@0: * Timer callback used to run the delayed task. michael@0: */ michael@0: _timerCallback: function () michael@0: { michael@0: let runningDeferred = Promise.defer(); michael@0: michael@0: // All these state changes must occur at the same time directly inside the michael@0: // timer callback, to prevent race conditions and to ensure that all the michael@0: // methods behave consistently even if called from inside the task. This michael@0: // means that the assignment of "this._runningPromise" must complete before michael@0: // the task gets a chance to start. michael@0: this._timer = null; michael@0: this._armed = false; michael@0: this._runningPromise = runningDeferred.promise; michael@0: michael@0: runningDeferred.resolve(Task.spawn(function () { michael@0: // Execute the provided function asynchronously. michael@0: yield Task.spawn(this._taskFn).then(null, Cu.reportError); michael@0: michael@0: // Now that the task has finished, we check the state of the object to michael@0: // determine if we should restart the task again. michael@0: if (this._armed) { michael@0: if (!this._finalized) { michael@0: this._startTimer(); michael@0: } else { michael@0: // Execute the task again immediately, for the last time. The isArmed michael@0: // property should return false while the task is running, and should michael@0: // remain false after the last execution terminates. michael@0: this._armed = false; michael@0: yield Task.spawn(this._taskFn).then(null, Cu.reportError); michael@0: } michael@0: } michael@0: michael@0: // Indicate that the execution of the task has finished. This happens michael@0: // synchronously with the previous state changes in the function. michael@0: this._runningPromise = null; michael@0: }.bind(this)).then(null, Cu.reportError)); michael@0: }, michael@0: };