|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 this.EXPORTED_SYMBOLS = [ |
|
10 "DeferredTask", |
|
11 ]; |
|
12 |
|
13 /** |
|
14 * Sets up a function or an asynchronous task whose execution can be triggered |
|
15 * after a defined delay. Multiple attempts to run the task before the delay |
|
16 * has passed are coalesced. The task cannot be re-entered while running, but |
|
17 * can be executed again after a previous run finished. |
|
18 * |
|
19 * A common use case occurs when a data structure should be saved into a file |
|
20 * every time the data changes, using asynchronous calls, and multiple changes |
|
21 * to the data may happen within a short time: |
|
22 * |
|
23 * let saveDeferredTask = new DeferredTask(function* () { |
|
24 * yield OS.File.writeAtomic(...); |
|
25 * // Any uncaught exception will be reported. |
|
26 * }, 2000); |
|
27 * |
|
28 * // The task is ready, but will not be executed until requested. |
|
29 * |
|
30 * The "arm" method can be used to start the internal timer that will result in |
|
31 * the eventual execution of the task. Multiple attempts to arm the timer don't |
|
32 * introduce further delays: |
|
33 * |
|
34 * saveDeferredTask.arm(); |
|
35 * |
|
36 * // The task will be executed in 2 seconds from now. |
|
37 * |
|
38 * yield waitOneSecond(); |
|
39 * saveDeferredTask.arm(); |
|
40 * |
|
41 * // The task will be executed in 1 second from now. |
|
42 * |
|
43 * The timer can be disarmed to reset the delay, or just to cancel execution: |
|
44 * |
|
45 * saveDeferredTask.disarm(); |
|
46 * saveDeferredTask.arm(); |
|
47 * |
|
48 * // The task will be executed in 2 seconds from now. |
|
49 * |
|
50 * When the internal timer fires and the execution of the task starts, the task |
|
51 * cannot be canceled anymore. It is however possible to arm the timer again |
|
52 * during the execution of the task, in which case the task will need to finish |
|
53 * before the timer is started again, thus guaranteeing a time of inactivity |
|
54 * between executions that is at least equal to the provided delay. |
|
55 * |
|
56 * The "finalize" method can be used to ensure that the task terminates |
|
57 * properly. The promise it returns is resolved only after the last execution |
|
58 * of the task is finished. To guarantee that the task is executed for the |
|
59 * last time, the method prevents any attempt to arm the timer again. |
|
60 * |
|
61 * If the timer is already armed when the "finalize" method is called, then the |
|
62 * task is executed immediately. If the task was already running at this point, |
|
63 * then one last execution from start to finish will happen again, immediately |
|
64 * after the current execution terminates. If the timer is not armed, the |
|
65 * "finalize" method only ensures that any running task terminates. |
|
66 * |
|
67 * For example, during shutdown, you may want to ensure that any pending write |
|
68 * is processed, using the latest version of the data if the timer is armed: |
|
69 * |
|
70 * AsyncShutdown.profileBeforeChange.addBlocker( |
|
71 * "Example service: shutting down", |
|
72 * () => saveDeferredTask.finalize() |
|
73 * ); |
|
74 * |
|
75 * Instead, if you are going to delete the saved data from disk anyways, you |
|
76 * might as well prevent any pending write from starting, while still ensuring |
|
77 * that any write that is currently in progress terminates, so that the file is |
|
78 * not in use anymore: |
|
79 * |
|
80 * saveDeferredTask.disarm(); |
|
81 * saveDeferredTask.finalize().then(() => OS.File.remove(...)) |
|
82 * .then(null, Components.utils.reportError); |
|
83 */ |
|
84 |
|
85 //////////////////////////////////////////////////////////////////////////////// |
|
86 //// Globals |
|
87 |
|
88 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; |
|
89 |
|
90 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
91 |
|
92 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
93 "resource://gre/modules/Promise.jsm"); |
|
94 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
95 "resource://gre/modules/Task.jsm"); |
|
96 |
|
97 const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", |
|
98 "initWithCallback"); |
|
99 |
|
100 //////////////////////////////////////////////////////////////////////////////// |
|
101 //// DeferredTask |
|
102 |
|
103 /** |
|
104 * Sets up a task whose execution can be triggered after a delay. |
|
105 * |
|
106 * @param aTaskFn |
|
107 * Function or generator function to execute. This argument is passed to |
|
108 * the "Task.spawn" method every time the task should be executed. This |
|
109 * task is never re-entered while running. |
|
110 * @param aDelayMs |
|
111 * Time between executions, in milliseconds. Multiple attempts to run |
|
112 * the task before the delay has passed are coalesced. This time of |
|
113 * inactivity is guaranteed to pass between multiple executions of the |
|
114 * task, except on finalization, when the task may restart immediately |
|
115 * after the previous execution finished. |
|
116 */ |
|
117 this.DeferredTask = function (aTaskFn, aDelayMs) { |
|
118 this._taskFn = aTaskFn; |
|
119 this._delayMs = aDelayMs; |
|
120 } |
|
121 |
|
122 this.DeferredTask.prototype = { |
|
123 /** |
|
124 * Function or generator function to execute. |
|
125 */ |
|
126 _taskFn: null, |
|
127 |
|
128 /** |
|
129 * Time between executions, in milliseconds. |
|
130 */ |
|
131 _delayMs: null, |
|
132 |
|
133 /** |
|
134 * Indicates whether the task is currently requested to start again later, |
|
135 * regardless of whether it is currently running. |
|
136 */ |
|
137 get isArmed() this._armed, |
|
138 _armed: false, |
|
139 |
|
140 /** |
|
141 * Indicates whether the task is currently running. This is always true when |
|
142 * read from code inside the task function, but can also be true when read |
|
143 * from external code, in case the task is an asynchronous generator function. |
|
144 */ |
|
145 get isRunning() !!this._runningPromise, |
|
146 |
|
147 /** |
|
148 * Promise resolved when the current execution of the task terminates, or null |
|
149 * if the task is not currently running. |
|
150 */ |
|
151 _runningPromise: null, |
|
152 |
|
153 /** |
|
154 * nsITimer used for triggering the task after a delay, or null in case the |
|
155 * task is running or there is no task scheduled for execution. |
|
156 */ |
|
157 _timer: null, |
|
158 |
|
159 /** |
|
160 * Actually starts the timer with the delay specified on construction. |
|
161 */ |
|
162 _startTimer: function () |
|
163 { |
|
164 this._timer = new Timer(this._timerCallback.bind(this), this._delayMs, |
|
165 Ci.nsITimer.TYPE_ONE_SHOT); |
|
166 }, |
|
167 |
|
168 /** |
|
169 * Requests the execution of the task after the delay specified on |
|
170 * construction. Multiple calls don't introduce further delays. If the task |
|
171 * is running, the delay will start when the current execution finishes. |
|
172 * |
|
173 * The task will always be executed on a different tick of the event loop, |
|
174 * even if the delay specified on construction is zero. Multiple "arm" calls |
|
175 * within the same tick of the event loop are guaranteed to result in a single |
|
176 * execution of the task. |
|
177 * |
|
178 * @note By design, this method doesn't provide a way for the caller to detect |
|
179 * when the next execution terminates, or collect a result. In fact, |
|
180 * doing that would often result in duplicate processing or logging. If |
|
181 * a special operation or error logging is needed on completion, it can |
|
182 * be better handled from within the task itself, for example using a |
|
183 * try/catch/finally clause in the task. The "finalize" method can be |
|
184 * used in the common case of waiting for completion on shutdown. |
|
185 */ |
|
186 arm: function () |
|
187 { |
|
188 if (this._finalized) { |
|
189 throw new Error("Unable to arm timer, the object has been finalized."); |
|
190 } |
|
191 |
|
192 this._armed = true; |
|
193 |
|
194 // In case the timer callback is running, do not create the timer now, |
|
195 // because this will be handled by the timer callback itself. Also, the |
|
196 // timer is not restarted in case it is already running. |
|
197 if (!this._runningPromise && !this._timer) { |
|
198 this._startTimer(); |
|
199 } |
|
200 }, |
|
201 |
|
202 /** |
|
203 * Cancels any request for a delayed the execution of the task, though the |
|
204 * task itself cannot be canceled in case it is already running. |
|
205 * |
|
206 * This method stops any currently running timer, thus the delay will restart |
|
207 * from its original value in case the "arm" method is called again. |
|
208 */ |
|
209 disarm: function () { |
|
210 this._armed = false; |
|
211 if (this._timer) { |
|
212 // Calling the "cancel" method and discarding the timer reference makes |
|
213 // sure that the timer callback will not be called later, even if the |
|
214 // timer thread has already posted the timer event on the main thread. |
|
215 this._timer.cancel(); |
|
216 this._timer = null; |
|
217 } |
|
218 }, |
|
219 |
|
220 /** |
|
221 * Ensures that any pending task is executed from start to finish, while |
|
222 * preventing any attempt to arm the timer again. |
|
223 * |
|
224 * - If the task is running and the timer is armed, then one last execution |
|
225 * from start to finish will happen again, immediately after the current |
|
226 * execution terminates, then the returned promise will be resolved. |
|
227 * - If the task is running and the timer is not armed, the returned promise |
|
228 * will be resolved when the current execution terminates. |
|
229 * - If the task is not running and the timer is armed, then the task is |
|
230 * started immediately, and the returned promise resolves when the new |
|
231 * execution terminates. |
|
232 * - If the task is not running and the timer is not armed, the method returns |
|
233 * a resolved promise. |
|
234 * |
|
235 * @return {Promise} |
|
236 * @resolves After the last execution of the task is finished. |
|
237 * @rejects Never. |
|
238 */ |
|
239 finalize: function () { |
|
240 if (this._finalized) { |
|
241 throw new Error("The object has been already finalized."); |
|
242 } |
|
243 this._finalized = true; |
|
244 |
|
245 // If the timer is armed, it means that the task is not running but it is |
|
246 // scheduled for execution. Cancel the timer and run the task immediately. |
|
247 if (this._timer) { |
|
248 this.disarm(); |
|
249 this._timerCallback(); |
|
250 } |
|
251 |
|
252 // Wait for the operation to be completed, or resolve immediately. |
|
253 if (this._runningPromise) { |
|
254 return this._runningPromise; |
|
255 } |
|
256 return Promise.resolve(); |
|
257 }, |
|
258 _finalized: false, |
|
259 |
|
260 /** |
|
261 * Timer callback used to run the delayed task. |
|
262 */ |
|
263 _timerCallback: function () |
|
264 { |
|
265 let runningDeferred = Promise.defer(); |
|
266 |
|
267 // All these state changes must occur at the same time directly inside the |
|
268 // timer callback, to prevent race conditions and to ensure that all the |
|
269 // methods behave consistently even if called from inside the task. This |
|
270 // means that the assignment of "this._runningPromise" must complete before |
|
271 // the task gets a chance to start. |
|
272 this._timer = null; |
|
273 this._armed = false; |
|
274 this._runningPromise = runningDeferred.promise; |
|
275 |
|
276 runningDeferred.resolve(Task.spawn(function () { |
|
277 // Execute the provided function asynchronously. |
|
278 yield Task.spawn(this._taskFn).then(null, Cu.reportError); |
|
279 |
|
280 // Now that the task has finished, we check the state of the object to |
|
281 // determine if we should restart the task again. |
|
282 if (this._armed) { |
|
283 if (!this._finalized) { |
|
284 this._startTimer(); |
|
285 } else { |
|
286 // Execute the task again immediately, for the last time. The isArmed |
|
287 // property should return false while the task is running, and should |
|
288 // remain false after the last execution terminates. |
|
289 this._armed = false; |
|
290 yield Task.spawn(this._taskFn).then(null, Cu.reportError); |
|
291 } |
|
292 } |
|
293 |
|
294 // Indicate that the execution of the task has finished. This happens |
|
295 // synchronously with the previous state changes in the function. |
|
296 this._runningPromise = null; |
|
297 }.bind(this)).then(null, Cu.reportError)); |
|
298 }, |
|
299 }; |