node.js 5.8 KB

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