node.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /**
  2. * Copyright 2020 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. const URL = require('url');
  17. const VM = require('vm');
  18. const threads = require('worker_threads');
  19. const WORKER = Symbol.for('worker');
  20. const EVENTS = Symbol.for('events');
  21. class EventTarget {
  22. constructor() {
  23. Object.defineProperty(this, EVENTS, {
  24. value: new Map()
  25. });
  26. }
  27. dispatchEvent(event) {
  28. event.target = event.currentTarget = this;
  29. if (this['on' + event.type]) {
  30. try {
  31. this['on' + event.type](event);
  32. } catch (err) {
  33. console.error(err);
  34. }
  35. }
  36. const list = this[EVENTS].get(event.type);
  37. if (list == null) return;
  38. list.forEach(handler => {
  39. try {
  40. handler.call(this, event);
  41. } catch (err) {
  42. console.error(err);
  43. }
  44. });
  45. }
  46. addEventListener(type, fn) {
  47. let events = this[EVENTS].get(type);
  48. if (!events) this[EVENTS].set(type, events = []);
  49. events.push(fn);
  50. }
  51. removeEventListener(type, fn) {
  52. let events = this[EVENTS].get(type);
  53. if (events) {
  54. const index = events.indexOf(fn);
  55. if (index !== -1) events.splice(index, 1);
  56. }
  57. }
  58. }
  59. function Event(type, target) {
  60. this.type = type;
  61. this.timeStamp = Date.now();
  62. this.target = this.currentTarget = this.data = null;
  63. } // this module is used self-referentially on both sides of the
  64. // thread boundary, but behaves differently in each context.
  65. module.exports = threads.isMainThread ? mainThread() : workerThread();
  66. const baseUrl = URL.pathToFileURL(process.cwd() + '/');
  67. function mainThread() {
  68. /**
  69. * A web-compatible Worker implementation atop Node's worker_threads.
  70. * - uses DOM-style events (Event.data, Event.type, etc)
  71. * - supports event handler properties (worker.onmessage)
  72. * - Worker() constructor accepts a module URL
  73. * - accepts the {type:'module'} option
  74. * - emulates WorkerGlobalScope within the worker
  75. * @param {string} url The URL or module specifier to load
  76. * @param {object} [options] Worker construction options
  77. * @param {string} [options.name] Available as `self.name` within the Worker
  78. * @param {string} [options.type="classic"] Pass "module" to create a Module Worker.
  79. */
  80. class Worker extends EventTarget {
  81. constructor(url, options) {
  82. super();
  83. const {
  84. name,
  85. type
  86. } = options || {};
  87. url += '';
  88. let mod;
  89. if (/^data:/.test(url)) {
  90. mod = url;
  91. } else {
  92. mod = URL.fileURLToPath(new URL.URL(url, baseUrl));
  93. }
  94. const worker = new threads.Worker(__filename, {
  95. workerData: {
  96. mod,
  97. name,
  98. type
  99. }
  100. });
  101. Object.defineProperty(this, WORKER, {
  102. value: worker
  103. });
  104. worker.on('message', data => {
  105. const event = new Event('message');
  106. event.data = data;
  107. this.dispatchEvent(event);
  108. });
  109. worker.on('error', error => {
  110. error.type = 'error';
  111. this.dispatchEvent(error);
  112. });
  113. worker.on('exit', () => {
  114. this.dispatchEvent(new Event('close'));
  115. });
  116. }
  117. postMessage(data, transferList) {
  118. this[WORKER].postMessage(data, transferList);
  119. }
  120. terminate() {
  121. this[WORKER].terminate();
  122. }
  123. }
  124. Worker.prototype.onmessage = Worker.prototype.onerror = Worker.prototype.onclose = null;
  125. return Worker;
  126. }
  127. function workerThread() {
  128. let {
  129. mod,
  130. name,
  131. type
  132. } = threads.workerData; // turn global into a mock WorkerGlobalScope
  133. const self = global.self = global; // enqueue messages to dispatch after modules are loaded
  134. let q = [];
  135. function flush() {
  136. const buffered = q;
  137. q = null;
  138. buffered.forEach(event => {
  139. self.dispatchEvent(event);
  140. });
  141. }
  142. threads.parentPort.on('message', data => {
  143. const event = new Event('message');
  144. event.data = data;
  145. if (q == null) self.dispatchEvent(event);else q.push(event);
  146. });
  147. threads.parentPort.on('error', err => {
  148. err.type = 'Error';
  149. self.dispatchEvent(err);
  150. });
  151. class WorkerGlobalScope extends EventTarget {
  152. postMessage(data, transferList) {
  153. threads.parentPort.postMessage(data, transferList);
  154. } // Emulates https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/close
  155. close() {
  156. process.exit();
  157. }
  158. }
  159. let proto = Object.getPrototypeOf(global);
  160. delete proto.constructor;
  161. Object.defineProperties(WorkerGlobalScope.prototype, proto);
  162. proto = Object.setPrototypeOf(global, new WorkerGlobalScope());
  163. ['postMessage', 'addEventListener', 'removeEventListener', 'dispatchEvent'].forEach(fn => {
  164. proto[fn] = proto[fn].bind(global);
  165. });
  166. global.name = name;
  167. const isDataUrl = /^data:/.test(mod);
  168. if (type === 'module') {
  169. import(mod).catch(err => {
  170. if (isDataUrl && err.message === 'Not supported') {
  171. console.warn('Worker(): Importing data: URLs requires Node 12.10+. Falling back to classic worker.');
  172. return evaluateDataUrl(mod, name);
  173. }
  174. console.error(err);
  175. }).then(flush);
  176. } else {
  177. try {
  178. if (/^data:/.test(mod)) {
  179. evaluateDataUrl(mod, name);
  180. } else {
  181. require(mod);
  182. }
  183. } catch (err) {
  184. console.error(err);
  185. }
  186. Promise.resolve().then(flush);
  187. }
  188. }
  189. function evaluateDataUrl(url, name) {
  190. const {
  191. data
  192. } = parseDataUrl(url);
  193. return VM.runInThisContext(data, {
  194. filename: 'worker.<' + (name || 'data:') + '>'
  195. });
  196. }
  197. function parseDataUrl(url) {
  198. let [m, type, encoding, data] = url.match(/^data: *([^;,]*)(?: *; *([^,]*))? *,(.*)$/) || [];
  199. if (!m) throw Error('Invalid Data URL.');
  200. if (encoding) switch (encoding.toLowerCase()) {
  201. case 'base64':
  202. data = Buffer.from(data, 'base64').toString();
  203. break;
  204. default:
  205. throw Error('Unknown Data URL encoding "' + encoding + '"');
  206. }
  207. return {
  208. type,
  209. data
  210. };
  211. }