summaryrefslogtreecommitdiffstats
path: root/toolkit/components/promiseworker/worker/PromiseWorker.template.worker.js
blob: c8203db48fc83aae25ea3ef2813ad1a12516fa2d (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
/* 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/. */

/* eslint-env commonjs */

/**
 * A wrapper around `self` with extended capabilities designed
 * to simplify main thread-to-worker thread asynchronous function calls.
 *
 * This wrapper:
 * - groups requests and responses as a method `post` that returns a `Promise`;
 * - ensures that exceptions thrown on the worker thread are correctly serialized;
 * - provides some utilities for benchmarking various operations.
 *
 * Generally, you should use PromiseWorker.js or PromiseWorker.mjs along with
 * its main thread-side counterpart PromiseWorker.sys.mjs.
 */

// #BEGIN_SCRIPT_ONLY
"use strict";
// #END_SCRIPT_ONLY

if (typeof Components != "undefined") {
  throw new Error("This module is meant to be used from the worker thread");
}
// #BEGIN_SCRIPT_ONLY
if (typeof require == "undefined" || typeof module == "undefined") {
  throw new Error(
    "this module is meant to be imported using the implementation of require() at resource://gre/modules/workers/require.js"
  );
}

/* import-globals-from /toolkit/components/workerloader/require.js */
importScripts("resource://gre/modules/workers/require.js");
// #END_SCRIPT_ONLY

/**
 * Built-in JavaScript exceptions that may be serialized without
 * loss of information.
 */
const EXCEPTION_NAMES = {
  EvalError: "EvalError",
  InternalError: "InternalError",
  RangeError: "RangeError",
  ReferenceError: "ReferenceError",
  SyntaxError: "SyntaxError",
  TypeError: "TypeError",
  URIError: "URIError",
};

/**
 * A constructor used to return data to the caller thread while
 * also executing some specific treatment (e.g. shutting down
 * the current thread, transmitting data instead of copying it).
 *
 * @param {object=} data The data to return to the caller thread.
 * @param {object=} meta Additional instructions, as an object
 * that may contain the following fields:
 * - {bool} shutdown If |true|, shut down the current thread after
 *   having sent the result.
 * - {Array} transfers An array of objects that should be transferred
 *   instead of being copied.
 *
 * @constructor
 */
function Meta(data, meta) {
  this.data = data;
  this.meta = meta;
}
// #EXPORT Meta

/**
 * Base class for a worker.
 *
 * Derived classes are expected to provide the following methods:
 * {
 *   dispatch: function(method, args) {
 *     // Dispatch a call to method `method` with args `args`
 *   },
 *   log: function(...msg) {
 *     // Log (or discard) messages (optional)
 *   },
 *   postMessage: function(message, ...transfers) {
 *     // Post a message to the main thread
 *   },
 *   close: function() {
 *     // Close the worker
 *   }
 * }
 *
 * By default, the AbstractWorker is not connected to a message port,
 * hence will not receive anything.
 *
 * To connect it, use `onmessage`, as follows:
 *   self.addEventListener("message", msg => myWorkerInstance.handleMessage(msg));
 * To handle rejected promises we receive from handleMessage, we must connect it to
 * the onError handler as follows:
 *   self.addEventListener("unhandledrejection", function(error) {
 *    throw error.reason;
 *   });
 */
function AbstractWorker(agent) {
  this._agent = agent;
}
AbstractWorker.prototype = {
  // Default logger: discard all messages
  log() {},

  /**
   * Handle a message.
   */
  async handleMessage(msg) {
    let data = msg.data;
    this.log("Received message", data);
    let id = data.id;

    let start;
    let options;
    if (data.args) {
      options = data.args[data.args.length - 1];
    }
    // If |outExecutionDuration| option was supplied, start measuring the
    // duration of the operation.
    if (
      options &&
      typeof options === "object" &&
      "outExecutionDuration" in options
    ) {
      start = Date.now();
    }

    let result;
    let exn;
    let durationMs;
    let method = data.fun;
    try {
      this.log("Calling method", method);
      result = await this.dispatch(method, data.args);
      this.log("Method", method, "succeeded");
    } catch (ex) {
      exn = ex;
      this.log(
        "Error while calling agent method",
        method,
        exn,
        exn.moduleStack || exn.stack || ""
      );
    }

    if (start) {
      // Record duration
      durationMs = Date.now() - start;
      this.log("Method took", durationMs, "ms");
    }

    // Now, post a reply, possibly as an uncaught error.
    // We post this message from outside the |try ... catch| block
    // to avoid capturing errors that take place during |postMessage| and
    // built-in serialization.
    if (!exn) {
      this.log("Sending positive reply", result, "id is", id);
      if (result instanceof Meta) {
        if ("transfers" in result.meta) {
          // Take advantage of zero-copy transfers
          this.postMessage(
            { ok: result.data, id, durationMs },
            result.meta.transfers
          );
        } else {
          this.postMessage({ ok: result.data, id, durationMs });
        }
        if (result.meta.shutdown || false) {
          // Time to close the worker
          this.close();
        }
      } else {
        this.postMessage({ ok: result, id, durationMs });
      }
    } else if (exn.constructor.name == "DOMException") {
      // We can receive instances of DOMExceptions with file I/O.
      // DOMExceptions are not yet serializable (Bug 1561357) and must be
      // handled differently, as they only have a name and message
      this.log("Sending back DOM exception", exn.constructor.name);
      let error = {
        exn: exn.constructor.name,
        message: exn.message,
      };
      this.postMessage({ fail: error, id, durationMs });
    } else if (exn.constructor.name in EXCEPTION_NAMES) {
      // Rather than letting the DOM mechanism [de]serialize built-in
      // JS errors, which loses lots of information (in particular,
      // the constructor name, the moduleName and the moduleStack),
      // we [de]serialize them manually with a little more care.
      this.log("Sending back exception", exn.constructor.name, "id is", id);
      let error = {
        exn: exn.constructor.name,
        message: exn.message,
        fileName: exn.moduleName || exn.fileName,
        lineNumber: exn.lineNumber,
        stack: exn.moduleStack,
      };
      this.postMessage({ fail: error, id, durationMs });
    } else if ("toMsg" in exn) {
      // Extension mechanism for exception [de]serialization. We
      // assume that any exception with a method `toMsg()` knows how
      // to serialize itself. The other side is expected to have
      // registered a deserializer using the `ExceptionHandlers`
      // object.
      this.log(
        "Sending back an error that knows how to serialize itself",
        exn,
        "id is",
        id
      );
      let msg = exn.toMsg();
      this.postMessage({ fail: msg, id, durationMs });
    } else {
      // If we encounter an exception for which we have no
      // serialization mechanism in place, we have no choice but to
      // let the DOM handle said [de]serialization. We can just
      // attempt to mitigate the data loss by injecting `moduleName` and
      // `moduleStack`.
      this.log(
        "Sending back regular error",
        exn,
        exn.moduleStack || exn.stack,
        "id is",
        id
      );

      try {
        // Attempt to introduce human-readable filename and stack
        exn.filename = exn.moduleName;
        exn.stack = exn.moduleStack;
      } catch (_) {
        // Nothing we can do
      }
      throw exn;
    }
  },
};
// #EXPORT AbstractWorker