forked from mozilla/gecko-dev
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Task.jsm
303 lines (277 loc) · 11.1 KB
/
Task.jsm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* 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";
this.EXPORTED_SYMBOLS = [
"Task"
];
/**
* This module implements a subset of "Task.js" <http://taskjs.org/>.
*
* Paraphrasing from the Task.js site, tasks make sequential, asynchronous
* operations simple, using the power of JavaScript's "yield" operator.
*
* Tasks are built upon generator functions and promises, documented here:
*
* <https://developer.mozilla.org/en/JavaScript/Guide/Iterators_and_Generators>
* <http://wiki.commonjs.org/wiki/Promises/A>
*
* The "Task.spawn" function takes a generator function and starts running it as
* a task. Every time the task yields a promise, it waits until the promise is
* fulfilled. "Task.spawn" returns a promise that is resolved when the task
* completes successfully, or is rejected if an exception occurs.
*
* -----------------------------------------------------------------------------
*
* Cu.import("resource://gre/modules/Task.jsm");
*
* Task.spawn(function* () {
*
* // This is our task. Let's create a promise object, wait on it and capture
* // its resolution value.
* let myPromise = getPromiseResolvedOnTimeoutWithValue(1000, "Value");
* let result = yield myPromise;
*
* // This part is executed only after the promise above is fulfilled (after
* // one second, in this imaginary example). We can easily loop while
* // calling asynchronous functions, and wait multiple times.
* for (let i = 0; i < 3; i++) {
* result += yield getPromiseResolvedOnTimeoutWithValue(50, "!");
* }
*
* return "Resolution result for the task: " + result;
* }).then(function (result) {
*
* // result == "Resolution result for the task: Value!!!"
*
* // The result is undefined if no value was returned.
*
* }, function (exception) {
*
* // Failure! We can inspect or report the exception.
*
* });
*
* -----------------------------------------------------------------------------
*
* This module implements only the "Task.js" interfaces described above, with no
* additional features to control the task externally, or do custom scheduling.
* It also provides the following extensions that simplify task usage in the
* most common cases:
*
* - The "Task.spawn" function also accepts an iterator returned by a generator
* function, in addition to a generator function. This way, you can call into
* the generator function with the parameters you want, and with "this" bound
* to the correct value. Also, "this" is never bound to the task object when
* "Task.spawn" calls the generator function.
*
* - In addition to a promise object, a task can yield the iterator returned by
* a generator function. The iterator is turned into a task automatically.
* This reduces the syntax overhead of calling "Task.spawn" explicitly when
* you want to recurse into other task functions.
*
* - The "Task.spawn" function also accepts a primitive value, or a function
* returning a primitive value, and treats the value as the result of the
* task. This makes it possible to call an externally provided function and
* spawn a task from it, regardless of whether it is an asynchronous generator
* or a synchronous function. This comes in handy when iterating over
* function lists where some items have been converted to tasks and some not.
*/
////////////////////////////////////////////////////////////////////////////////
//// Globals
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
// The following error types are considered programmer errors, which should be
// reported (possibly redundantly) so as to let programmers fix their code.
const ERRORS_TO_REPORT = ["EvalError", "RangeError", "ReferenceError", "TypeError"];
/**
* Detect whether a value is a generator.
*
* @param aValue
* The value to identify.
* @return A boolean indicating whether the value is a generator.
*/
function isGenerator(aValue) {
return Object.prototype.toString.call(aValue) == "[object Generator]";
}
////////////////////////////////////////////////////////////////////////////////
//// Task
/**
* This object provides the public module functions.
*/
this.Task = {
/**
* Creates and starts a new task.
*
* @param aTask
* - If you specify a generator function, it is called with no
* arguments to retrieve the associated iterator. The generator
* function is a task, that is can yield promise objects to wait
* upon.
* - If you specify the iterator returned by a generator function you
* called, the generator function is also executed as a task. This
* allows you to call the function with arguments.
* - If you specify a function that is not a generator, it is called
* with no arguments, and its return value is used to resolve the
* returned promise.
* - If you specify anything else, you get a promise that is already
* resolved with the specified value.
*
* @return A promise object where you can register completion callbacks to be
* called when the task terminates.
*/
spawn: function Task_spawn(aTask) {
if (aTask && typeof(aTask) == "function") {
try {
// Let's call into the function ourselves.
aTask = aTask();
} catch (ex if ex instanceof Task.Result) {
return Promise.resolve(ex.value);
} catch (ex) {
return Promise.reject(ex);
}
}
if (isGenerator(aTask)) {
// This is an iterator resulting from calling a generator function.
return new TaskImpl(aTask).deferred.promise;
}
// Just propagate the given value to the caller as a resolved promise.
return Promise.resolve(aTask);
},
/**
* Constructs a special exception that, when thrown inside a legacy generator
* function (non-star generator), allows the associated task to be resolved
* with a specific value.
*
* Example: throw new Task.Result("Value");
*/
Result: function Task_Result(aValue) {
this.value = aValue;
}
};
////////////////////////////////////////////////////////////////////////////////
//// TaskImpl
/**
* Executes the specified iterator as a task, and gives access to the promise
* that is fulfilled when the task terminates.
*/
function TaskImpl(iterator) {
this.deferred = Promise.defer();
this._iterator = iterator;
this._isStarGenerator = !("send" in iterator);
this._run(true);
}
TaskImpl.prototype = {
/**
* Includes the promise object where task completion callbacks are registered,
* and methods to resolve or reject the promise at task completion.
*/
deferred: null,
/**
* The iterator returned by the generator function associated with this task.
*/
_iterator: null,
/**
* Whether this Task is using a star generator.
*/
_isStarGenerator: false,
/**
* Main execution routine, that calls into the generator function.
*
* @param aSendResolved
* If true, indicates that we should continue into the generator
* function regularly (if we were waiting on a promise, it was
* resolved). If true, indicates that we should cause an exception to
* be thrown into the generator function (if we were waiting on a
* promise, it was rejected).
* @param aSendValue
* Resolution result or rejection exception, if any.
*/
_run: function TaskImpl_run(aSendResolved, aSendValue) {
if (this._isStarGenerator) {
try {
let result = aSendResolved ? this._iterator.next(aSendValue)
: this._iterator.throw(aSendValue);
if (result.done) {
// The generator function returned.
this.deferred.resolve(result.value);
} else {
// The generator function yielded.
this._handleResultValue(result.value);
}
} catch (ex) {
// The generator function failed with an uncaught exception.
this._handleException(ex);
}
} else {
try {
let yielded = aSendResolved ? this._iterator.send(aSendValue)
: this._iterator.throw(aSendValue);
this._handleResultValue(yielded);
} catch (ex if ex instanceof Task.Result) {
// The generator function threw the special exception that allows it to
// return a specific value on resolution.
this.deferred.resolve(ex.value);
} catch (ex if ex instanceof StopIteration) {
// The generator function terminated with no specific result.
this.deferred.resolve();
} catch (ex) {
// The generator function failed with an uncaught exception.
this._handleException(ex);
}
}
},
/**
* Handle a value yielded by a generator.
*
* @param aValue
* The yielded value to handle.
*/
_handleResultValue: function TaskImpl_handleResultValue(aValue) {
// If our task yielded an iterator resulting from calling another
// generator function, automatically spawn a task from it, effectively
// turning it into a promise that is fulfilled on task completion.
if (isGenerator(aValue)) {
aValue = Task.spawn(aValue);
}
if (aValue && typeof(aValue.then) == "function") {
// We have a promise object now. When fulfilled, call again into this
// function to continue the task, with either a resolution or rejection
// condition.
aValue.then(this._run.bind(this, true),
this._run.bind(this, false));
} else {
// If our task yielded a value that is not a promise, just continue and
// pass it directly as the result of the yield statement.
this._run(true, aValue);
}
},
/**
* Handle an uncaught exception thrown from a generator.
*
* @param aException
* The uncaught exception to handle.
*/
_handleException: function TaskImpl_handleException(aException) {
if (aException && typeof aException == "object" && "name" in aException &&
ERRORS_TO_REPORT.indexOf(aException.name) != -1) {
// We suspect that the exception is a programmer error, so we now
// display it using dump(). Note that we do not use Cu.reportError as
// we assume that this is a programming error, so we do not want end
// users to see it. Also, if the programmer handles errors correctly,
// they will either treat the error or log them somewhere.
let stack = ("stack" in aException) ? aException.stack : "not available";
dump("*************************\n");
dump("A coding exception was thrown and uncaught in a Task.\n\n");
dump("Full message: " + aException + "\n");
dump("Full stack: " + stack + "\n");
dump("*************************\n");
}
this.deferred.reject(aException);
}
};