summaryrefslogtreecommitdiffstats
path: root/devtools/shared/task.js
blob: 501e05e5b7414d7e5fa5e10b6832768046514546 (plain)
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
/* -*- indent-tabs-mode: nil; js-indent-level: 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";

/* eslint-disable spaced-comment */
/* globals StopIteration */

/**
 * This module implements a subset of "Task.js" <http://taskjs.org/>.
 * It is a copy of toolkit/modules/Task.jsm.  Please try not to
 * diverge the API here.
 *
 * 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.
 *
 * -----------------------------------------------------------------------------
 *
 * const {Task} = require("devtools/shared/task");
 *
 * 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 Promise = require("promise");
const defer = require("devtools/shared/defer");

// 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"];

/**
 * The Task currently being executed
 */
var gCurrentTask = null;

/**
 * If `true`, capture stacks whenever entering a Task and rewrite the
 * stack any exception thrown through a Task.
 */
var gMaintainStack = false;

/**
 * Iterate through the lines of a string.
 *
 * @return Iterator<string>
 */
function* linesOf(string) {
  let reLine = /([^\r\n])+/g;
  let match;
  while ((match = reLine.exec(string))) {
    yield [match[0], match.index];
  }
}

/**
 * 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(value) {
  return Object.prototype.toString.call(value) == "[object Generator]";
}

////////////////////////////////////////////////////////////////////////////////
//// Task

/**
 * This object provides the public module functions.
 */
var Task = {
  /**
   * Creates and starts a new task.
   *
   * @param task
   *        - 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) {
    return createAsyncFunction(task).call(undefined);
  },

  /**
   * Create and return an 'async function' that starts a new task.
   *
   * This is similar to 'spawn' except that it doesn't immediately start
   * the task, it binds the task to the async function's 'this' object and
   * arguments, and it requires the task to be a function.
   *
   * It simplifies the common pattern of implementing a method via a task,
   * like this simple object with a 'greet' method that has a 'name' parameter
   * and spawns a task to send a greeting and return its reply:
   *
   * let greeter = {
   *   message: "Hello, NAME!",
   *   greet: function(name) {
   *     return Task.spawn((function* () {
   *       return yield sendGreeting(this.message.replace(/NAME/, name));
   *     }).bind(this);
   *   })
   * };
   *
   * With Task.async, the method can be declared succinctly:
   *
   * let greeter = {
   *   message: "Hello, NAME!",
   *   greet: Task.async(function* (name) {
   *     return yield sendGreeting(this.message.replace(/NAME/, name));
   *   })
   * };
   *
   * While maintaining identical semantics:
   *
   * greeter.greet("Mitchell").then((reply) => { ... }); // behaves the same
   *
   * @param task
   *        The task function to start.
   *
   * @return A function that starts the task function and returns its promise.
   */
  async: function (task) {
    if (typeof (task) != "function") {
      throw new TypeError("task argument must be a function");
    }

    return createAsyncFunction(task);
  },

  /**
   * 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 (value) {
    this.value = value;
  }
};

function createAsyncFunction(task) {
  let asyncFunction = function () {
    let result = task;
    if (task && typeof (task) == "function") {
      if (task.isAsyncFunction) {
        throw new TypeError(
          "Cannot use an async function in place of a promise. " +
          "You should either invoke the async function first " +
          "or use 'Task.spawn' instead of 'Task.async' to start " +
          "the Task and return its promise.");
      }

      try {
        // Let's call into the function ourselves.
        result = task.apply(this, arguments);
      } catch (ex) {
        if (ex instanceof Task.Result) {
          return Promise.resolve(ex.value);
        }
        return Promise.reject(ex);
      }
    }

    if (isGenerator(result)) {
      // This is an iterator resulting from calling a generator function.
      return new TaskImpl(result).deferred.promise;
    }

    // Just propagate the given value to the caller as a resolved promise.
    return Promise.resolve(result);
  };

  asyncFunction.isAsyncFunction = true;

  return asyncFunction;
}

////////////////////////////////////////////////////////////////////////////////
//// TaskImpl

/**
 * Executes the specified iterator as a task, and gives access to the promise
 * that is fulfilled when the task terminates.
 */
function TaskImpl(iterator) {
  if (gMaintainStack) {
    this._stack = (new Error()).stack;
  }
  this.deferred = 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 sendResolved
   *        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 sendValue
   *        Resolution result or rejection exception, if any.
   */
  _run: function (sendResolved, sendValue) {
    try {
      gCurrentTask = this;

      if (this._isStarGenerator) {
        try {
          let result = sendResolved ? this._iterator.next(sendValue)
                                    : this._iterator.throw(sendValue);

          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 = sendResolved ? this._iterator.send(sendValue)
                                     : this._iterator.throw(sendValue);
          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);
          } else if (ex instanceof StopIteration) {
            // The generator function terminated with no specific result.
            this.deferred.resolve(undefined);
          } else {
            // The generator function failed with an uncaught exception.
            this._handleException(ex);
          }
        }
      }
    } finally {
      //
      // At this stage, the Task may have finished executing, or have
      // walked through a `yield` or passed control to a sub-Task.
      // Regardless, if we still own `gCurrentTask`, reset it. If we
      // have not finished execution of this Task, re-entering `_run`
      // will set `gCurrentTask` to `this` as needed.
      //
      // We just need to be careful here in case we hit the following
      // pattern:
      //
      //   Task.spawn(foo);
      //   Task.spawn(bar);
      //
      // Here, `foo` and `bar` may be interleaved, so when we finish
      // executing `foo`, `gCurrentTask` may actually either `foo` or
      // `bar`. If `gCurrentTask` has already been set to `bar`, leave
      // it be and it will be reset to `null` once `bar` is complete.
      //
      if (gCurrentTask == this) {
        gCurrentTask = null;
      }
    }
  },

  /**
   * Handle a value yielded by a generator.
   *
   * @param value
   *        The yielded value to handle.
   */
  _handleResultValue: function (value) {
    // 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(value)) {
      value = Task.spawn(value);
    }

    if (value && typeof (value.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.
      value.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, value);
    }
  },

  /**
   * Handle an uncaught exception thrown from a generator.
   *
   * @param exception
   *        The uncaught exception to handle.
   */
  _handleException: function (exception) {
    gCurrentTask = this;

    if (exception && typeof exception == "object" && "stack" in exception) {
      let stack = exception.stack;

      if (gMaintainStack &&
          exception._capturedTaskStack != this._stack &&
          typeof stack == "string") {
        // Rewrite the stack for more readability.

        let bottomStack = this._stack;

        stack = Task.Debugging.generateReadableStack(stack);

        exception.stack = stack;

        // If exception is reinjected in the same task and rethrown,
        // we don't want to perform the rewrite again.
        exception._capturedTaskStack = bottomStack;
      } else if (!stack) {
        stack = "Not available";
      }

      if ("name" in exception &&
          ERRORS_TO_REPORT.indexOf(exception.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.

        dump("*************************\n");
        dump("A coding exception was thrown and uncaught in a Task.\n\n");
        dump("Full message: " + exception + "\n");
        dump("Full stack: " + exception.stack + "\n");
        dump("*************************\n");
      }
    }

    this.deferred.reject(exception);
  },

  get callerStack() {
    // Cut `this._stack` at the last line of the first block that
    // contains task.js, keep the tail.
    for (let [line, index] of linesOf(this._stack || "")) {
      if (line.indexOf("/task.js:") == -1) {
        return this._stack.substring(index);
      }
    }
    return "";
  }
};

Task.Debugging = {

  /**
   * Control stack rewriting.
   *
   * If `true`, any exception thrown from a Task will be rewritten to
   * provide a human-readable stack trace. Otherwise, stack traces will
   * be left unchanged.
   *
   * There is a (small but existing) runtime cost associated to stack
   * rewriting, so you should probably not activate this in production
   * code.
   *
   * @type {bool}
   */
  get maintainStack() {
    return gMaintainStack;
  },
  set maintainStack(x) {
    if (!x) {
      gCurrentTask = null;
    }
    gMaintainStack = x;
    return x;
  },

  /**
   * Generate a human-readable stack for an error raised in
   * a Task.
   *
   * @param {string} topStack The stack provided by the error.
   * @param {string=} prefix Optionally, a prefix for each line.
   */
  generateReadableStack: function (topStack, prefix = "") {
    if (!gCurrentTask) {
      return topStack;
    }

    // Cut `topStack` at the first line that contains task.js, keep the head.
    let lines = [];
    for (let [line] of linesOf(topStack)) {
      if (line.indexOf("/task.js:") != -1) {
        break;
      }
      lines.push(prefix + line);
    }
    if (!prefix) {
      lines.push(gCurrentTask.callerStack);
    } else {
      for (let [line] of linesOf(gCurrentTask.callerStack)) {
        lines.push(prefix + line);
      }
    }

    return lines.join("\n");
  }
};

exports.Task = Task;