├── .gitignore ├── .npmignore ├── .npmrc ├── README.md ├── binding.gyp ├── lib ├── Pool.js ├── Thread.js ├── index.js ├── preload.js └── util.js ├── package.json ├── src ├── NativeUtil.cc ├── NativeUtil.h ├── Worker.cc ├── Worker.h ├── threads.cc └── util.h ├── test ├── buffer.js ├── catch.js ├── console.js ├── dlopen.js ├── error.js ├── locks.js ├── native.cc ├── performance.js ├── pi.js ├── references.js └── sending.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | preload.build.js 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | lib/preload.js 3 | lib/util-format.js 4 | build 5 | README.md 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm][download-badge]][npm] 2 | [![David][dep-badge]][dep-link] 3 | 4 | [![NPM][large-badge]][stats-link] 5 | 6 | # threads [![Version Badge][version-badge]][npm] 7 | 8 | These are real threads that don't crash **and** run your js without exploding. 9 | 10 | ```javascript 11 | const Thread = require('threads'); 12 | const assert = require('assert'); 13 | 14 | const t = new Thread((a, b) => a + b, 1, 2); 15 | 16 | t.join().then((r) => { 17 | assert(r === 3); 18 | }); 19 | ``` 20 | 21 | TODO: 22 | 23 | - [X] Console in thread 24 | - [X] Buffer in thread (freoss/buffer) 25 | - [ ] Allow passing "references" instead of copies 26 | 27 | 28 | --- 29 | 30 | 31 | # Docs 32 | 33 | 34 | 35 | ## Thread 36 | **Kind**: global class 37 | 38 | * [Thread](#Thread) 39 | * [new Thread(fn, props, references)](#new_Thread_new) 40 | * _instance_ 41 | * [.send(value)](#Thread+send) 42 | * [.join()](#Thread+join) ⇒ Promise.<\*> 43 | * [.terminate()](#Thread+terminate) 44 | * [.lock()](#Thread+lock) ⇒ boolean 45 | * [.unlock()](#Thread+unlock) 46 | * _inner_ 47 | * [~Context](#Thread..Context) : Object 48 | * [~fnCallback](#Thread..fnCallback) : function 49 | 50 | 51 | 52 | ### new Thread(fn, props, references) 53 | Create a thread 54 | 55 | 56 | | Param | Type | Description | 57 | | --- | --- | --- | 58 | | fn | [fnCallback](#Thread..fnCallback) | Function that will run in a new thread | 59 | | props | Array.<\*> | Values to pass to the thread callback | 60 | | references | Object | References to functions | 61 | 62 | 63 | 64 | ### thread.send(value) 65 | Send a value to the thread 66 | 67 | **Kind**: instance method of [Thread](#Thread) 68 | 69 | | Param | Type | Description | 70 | | --- | --- | --- | 71 | | value | \* | Value to send | 72 | 73 | 74 | 75 | ### thread.join() ⇒ Promise.<\*> 76 | Return a promise that resolves when the thread finishes with the return value 77 | or rejects when there is an execution error in the thread. 78 | 79 | **Kind**: instance method of [Thread](#Thread) 80 | 81 | 82 | ### thread.terminate() 83 | Terminate the thread 84 | 85 | **Kind**: instance method of [Thread](#Thread) 86 | 87 | 88 | ### thread.lock() ⇒ boolean 89 | Lock the thread's context's mutex, analogous to std::mutex::try_lock 90 | 91 | **Kind**: instance method of [Thread](#Thread) 92 | **Returns**: boolean - If the lock was successfully obtained 93 | 94 | 95 | ### thread.unlock() 96 | Unlock the thread context's mutex, analogous to std::mutex::unlock 97 | 98 | **Kind**: instance method of [Thread](#Thread) 99 | 100 | 101 | ### Thread~Context : Object 102 | **Kind**: inner typedef of [Thread](#Thread) 103 | **Properties** 104 | 105 | | Name | Type | 106 | | --- | --- | 107 | | on | function | 108 | | send | function | 109 | | terminate | function | 110 | | lock | function | 111 | | unlock | function | 112 | 113 | 114 | 115 | ### Thread~fnCallback : function 116 | **Kind**: inner typedef of [Thread](#Thread) 117 | 118 | | Param | Type | Description | 119 | | --- | --- | --- | 120 | | ...args | args | Arguments from the [Thread](#Thread) constructor | 121 | | context | [Context](#Thread..Context) | | 122 | 123 | [npm]: https://npmjs.org/package/@snek/threads 124 | [large-badge]: https://nodei.co/npm/@snek/threads.png?downloads=true&downloadRank=true&stars=true 125 | [stats-link]: https://nodei.co/npm/@snek/threads/ 126 | [version-badge]: https://versionbadge.now.sh/npm/@snek/threads.svg 127 | [download-badge]: https://img.shields.io/npm/dt/@snek/threads.svg?maxAge=3600 128 | [dep-badge]: https://david-dm.org/devsnek/threads.svg 129 | [dep-link]: https://david-dm.org/devsnek/threads 130 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'target_defaults': { 3 | 'default_configuration': 'Release', 4 | 'configurations': { 5 | 'Release': { 6 | 'xcode_settings': { 7 | 'GCC_OPTIMIZATION_LEVEL': '3', 8 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'NO', 9 | }, 10 | 'msvs_settings': { 11 | 'VCCLCompilerTool': { 12 | 'Optimization': 3, 13 | 'FavorSizeOrSpeed': 1, 14 | }, 15 | }, 16 | }, 17 | 'Debug': { 18 | 'xcode_settings': { 19 | 'GCC_OPTIMIZATION_LEVEL': '0', 20 | 'GCC_GENERATE_DEBUGGING_SYMBOLS': 'YES', 21 | }, 22 | 'msvs_settings': { 23 | 'VCCLCompilerTool': { 24 | 'Optimization': 0, 25 | 'FavorSizeOrSpeed': 0, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | 'targets': [ 32 | { 33 | 'target_name': 'threads', 34 | 'cflags_cc': [ '-std=c++14' ], 35 | 'cflags_cc!': [ '-fno-exceptions', '-fno-rtti' ], 36 | 'xcode_settings': { 37 | 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', 38 | 'GCC_ENABLE_CPP_RTTI': 'YES', 39 | 'CLANG_CXX_LANGUAGE_STANDARD': 'c++14', 40 | }, 41 | 'msvs_settings': { 42 | 'VCCLCompilerTool': { 43 | 'ExceptionHandling': '1', 44 | 'RuntimeTypeInfo': 'true', 45 | 'AdditionalOptions': [ '/GR' ], 46 | }, 47 | }, 48 | 'msvs_disabled_warnings': [ 4068 ], # Unknown pragma 49 | 'conditions': [ 50 | [ 'OS == "win"', 51 | { 'defines': [ 'NOMINMAX' ] }, 52 | ], 53 | ], 54 | 'sources': [ 55 | 'src/NativeUtil.cc', 56 | 'src/Worker.cc', 57 | 'src/threads.cc', 58 | ], 59 | }, 60 | ], 61 | } 62 | -------------------------------------------------------------------------------- /lib/Pool.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thread = require('.'); 4 | 5 | const kFn = Symbol('fn'); 6 | 7 | class Pool { 8 | constructor(fn) { 9 | this[kFn] = fn; 10 | } 11 | 12 | run(count, args) { 13 | return Array.from({ length: count }, () => new Thread(this[kFn], ...args).join()); 14 | } 15 | } 16 | 17 | module.exports = Pool; 18 | -------------------------------------------------------------------------------- /lib/Thread.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Worker, constants, setPreload } = require('bindings')('threads'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const EventEmitter = require('events'); 7 | const util = require('util'); 8 | 9 | setPreload(fs.readFileSync(path.resolve(__dirname, '..', 'preload.build.js')).toString()); 10 | 11 | const kWorker = Symbol('kWorker'); 12 | const stateKeys = Object.keys(constants); 13 | 14 | /** 15 | * @typedef {Object} Thread~Context 16 | * @prop {Function} on 17 | * @prop {Function} send 18 | * @prop {Function} terminate 19 | * @prop {Function} lock 20 | * @prop {Function} unlock 21 | */ 22 | 23 | /** 24 | * @callback Thread~fnCallback 25 | * @param {...args} args Arguments from the {@link Thread} constructor 26 | * @param {Thread~Context} context 27 | */ 28 | 29 | class Thread extends EventEmitter { 30 | /** 31 | * Create a thread 32 | * @param {Thread~fnCallback} fn Function that will run in a new thread 33 | * @param {Array<*>} props Values to pass to the thread callback 34 | * @param {Object} references References to functions 35 | */ 36 | constructor(fn, props = [], references = {}) { 37 | super(); 38 | 39 | if (typeof props === 'object' && !Array.isArray(props)) { 40 | references = props; 41 | props = []; 42 | } 43 | 44 | if (!Array.isArray(props)) 45 | throw new TypeError('props must be an array'); 46 | 47 | 48 | for (const key of Object.keys(references)) { 49 | const v = references[key]; 50 | if (!v || (typeof v !== 'object' && typeof v !== 'function')) 51 | throw new TypeError('references must be functions or objects'); 52 | } 53 | 54 | const worker = new Worker(`(${fn})`, props, references); 55 | 56 | Object.defineProperties(this, { 57 | [kWorker]: { 58 | value: worker, 59 | writable: false, 60 | enumerable: false, 61 | configurable: false, 62 | }, 63 | id: { 64 | value: worker.getId(), 65 | writable: false, 66 | enumerable: true, 67 | configurable: false, 68 | }, 69 | }); 70 | 71 | const messageBuffer = [false, undefined]; 72 | const tickHandle = () => { 73 | worker.checkOutgoingMessages(messageBuffer); 74 | if (messageBuffer[0] === true) { 75 | this.emit('message', messageBuffer[1]); 76 | messageBuffer[0] = false; 77 | } 78 | 79 | if (worker.getState() === constants.running) 80 | process.nextTick(tickHandle); 81 | }; 82 | this.on('newListener', (event) => { 83 | if (event !== 'message') 84 | return; 85 | 86 | process.nextTick(tickHandle); 87 | }); 88 | } 89 | 90 | get state() { 91 | return stateKeys[this[kWorker].getState()]; 92 | } 93 | 94 | /** 95 | * Send a value to the thread 96 | * @param {*} value Value to send 97 | */ 98 | send(value) { 99 | this[kWorker].send(value); 100 | } 101 | 102 | /** 103 | * Return a promise that resolves when the thread finishes with the return value 104 | * or rejects when there is an execution error in the thread. 105 | * @returns {Promise<*>} 106 | */ 107 | join() { 108 | return this[kWorker].getPromise(); 109 | } 110 | 111 | /** 112 | * Terminate the thread 113 | */ 114 | terminate() { 115 | this[kWorker].terminate(); 116 | } 117 | 118 | /** 119 | * Lock the thread's context's mutex, analogous to std::mutex::try_lock 120 | * @returns {boolean} If the lock was successfully obtained 121 | */ 122 | lock() { 123 | return this[kWorker].lock(); 124 | } 125 | 126 | /** 127 | * Unlock the thread context's mutex, analogous to std::mutex::unlock 128 | */ 129 | unlock() { 130 | this[kWorker].unlock(); 131 | } 132 | 133 | [util.inspect.custom]() { 134 | return `Thread ${this.id.toString().padStart(2, 0)} { ${this.state} }`; 135 | } 136 | } 137 | 138 | module.exports = Thread; 139 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Thread = require('./Thread'); 4 | const Pool = require('./Pool'); 5 | 6 | Thread.Pool = Pool; 7 | 8 | module.exports = Thread; 9 | -------------------------------------------------------------------------------- /lib/preload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | global.Buffer = require('../node_modules/buffer/index.js').Buffer; 4 | 5 | { 6 | const _console = global._console; 7 | delete global._console; 8 | 9 | const { formatWithOptions } = require('./util'); 10 | 11 | global.console = {}; 12 | const methods = ['log', 'debug', 'info', 'warn', 'error']; 13 | for (const method of methods) 14 | global.console[method] = (...args) => _console(method, formatWithOptions({ colors: true }, ...args)); 15 | } 16 | 17 | { 18 | const _hrtime = global.performance._hrtime; 19 | delete global.performance._hrtime; 20 | const hrValues = new Uint32Array(3); 21 | 22 | global.performance.now = () => { 23 | _hrtime(hrValues); 24 | return (((hrValues[0] * 0x100000000) + hrValues[1]) * 1000) + (hrValues[2] / 1e6); 25 | }; 26 | } 27 | 28 | /* 29 | { 30 | const dlopen = global._util.dlopen; 31 | global.dlopen = (path) => { 32 | const module = { exports: {} }; 33 | dlopen(module, path); 34 | return module.exports; 35 | }; 36 | } 37 | */ 38 | 39 | delete global._util; 40 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | getPromiseDetails, 5 | kPending, 6 | kRejected, 7 | } = global._util; 8 | 9 | const propertyIsEnumerable = Object.prototype.propertyIsEnumerable; 10 | const regExpToString = RegExp.prototype.toString; 11 | const dateToISOString = Date.prototype.toISOString; 12 | const errorToString = Error.prototype.toString; 13 | 14 | /* eslint-disable */ 15 | const strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/; 16 | const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; 17 | /* eslint-enable */ 18 | const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; 19 | const colorRegExp = /\u001b\[\d\d?m/g; 20 | const numberRegExp = /^(0|[1-9][0-9]*)$/; 21 | 22 | const readableRegExps = {}; 23 | 24 | const MIN_LINE_LENGTH = 16; 25 | 26 | // Escaped special characters. Use empty strings to fill up unused entries. 27 | const meta = [ 28 | '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', 29 | '\\u0005', '\\u0006', '\\u0007', '\\b', '\\t', 30 | '\\n', '\\u000b', '\\f', '\\r', '\\u000e', 31 | '\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013', 32 | '\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018', 33 | '\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d', 34 | '\\u001e', '\\u001f', '', '', '', 35 | '', '', '', '', "\\'", '', '', '', '', '', 36 | '', '', '', '', '', '', '', '', '', '', 37 | '', '', '', '', '', '', '', '', '', '', 38 | '', '', '', '', '', '', '', '', '', '', 39 | '', '', '', '', '', '', '', '', '', '', 40 | '', '', '', '', '', '', '', '\\\\', 41 | ]; 42 | 43 | const escapeFn = (str) => meta[str.charCodeAt(0)]; 44 | 45 | // Escape control characters, single quotes and the backslash. 46 | // This is similar to JSON stringify escaping. 47 | function strEscape(str) { 48 | // Some magic numbers that worked out fine while benchmarking with v8 6.0 49 | if (str.length < 5000 && !strEscapeSequencesRegExp.test(str)) 50 | return `'${str}'`; 51 | if (str.length > 100) 52 | return `'${str.replace(strEscapeSequencesReplacer, escapeFn)}'`; 53 | var result = ''; 54 | var last = 0; 55 | for (var i = 0; i < str.length; i++) { 56 | const point = str.charCodeAt(i); 57 | if (point === 39 || point === 92 || point < 32) { 58 | if (last === i) 59 | result += meta[point]; 60 | else 61 | result += `${str.slice(last, i)}${meta[point]}`; 62 | 63 | last = i + 1; 64 | } 65 | } 66 | if (last === 0) 67 | result = str; 68 | else if (last !== i) 69 | result += str.slice(last); 70 | 71 | return `'${result}'`; 72 | } 73 | 74 | const inspectDefaultOptions = Object.seal({ 75 | showHidden: false, 76 | depth: 2, 77 | colors: false, 78 | showProxy: false, 79 | maxArrayLength: 100, 80 | breakLength: 60, 81 | compact: true, 82 | }); 83 | 84 | /* eslint-disable prefer-rest-params */ 85 | 86 | const emptyOptions = {}; 87 | function format(...args) { 88 | return formatWithOptions(emptyOptions, ...args); 89 | } 90 | 91 | function formatWithOptions(inspectOptions, f) { 92 | var i, tempStr; 93 | if (typeof f !== 'string') { 94 | if (arguments.length === 1) 95 | return ''; 96 | var res = ''; 97 | for (i = 1; i < arguments.length - 1; i++) { 98 | res += inspect(arguments[i], inspectOptions); 99 | res += ' '; 100 | } 101 | res += inspect(arguments[i], inspectOptions); 102 | return res; 103 | } 104 | 105 | if (arguments.length === 2) 106 | return f; 107 | 108 | var str = ''; 109 | var a = 2; 110 | var lastPos = 0; 111 | for (i = 0; i < f.length - 1; i++) { 112 | if (f.charCodeAt(i) === 37) { // '%' 113 | const nextChar = f.charCodeAt(++i); 114 | if (a !== arguments.length) { 115 | switch (nextChar) { 116 | case 115: // 's' 117 | tempStr = String(arguments[a++]); 118 | break; 119 | case 106: // 'j' 120 | tempStr = tryStringify(arguments[a++]); 121 | break; 122 | case 100: // 'd' 123 | tempStr = `${Number(arguments[a++])}`; 124 | break; 125 | case 79: // 'O' 126 | tempStr = inspect(arguments[a++], inspectOptions); 127 | break; 128 | case 111: { // 'o' 129 | const opts = Object.assign({}, inspectOptions, { 130 | showHidden: true, 131 | showProxy: true, 132 | }); 133 | tempStr = inspect(arguments[a++], opts); 134 | break; 135 | } 136 | case 105: // 'i' 137 | tempStr = `${parseInt(arguments[a++])}`; 138 | break; 139 | case 102: // 'f' 140 | tempStr = `${parseFloat(arguments[a++])}`; 141 | break; 142 | case 37: // '%' 143 | str += f.slice(lastPos, i); 144 | lastPos = i + 1; 145 | continue; 146 | default: // any other character is not a correct placeholder 147 | continue; 148 | } 149 | if (lastPos !== i - 1) 150 | str += f.slice(lastPos, i - 1); 151 | str += tempStr; 152 | lastPos = i + 1; 153 | } else if (nextChar === 37) { 154 | str += f.slice(lastPos, i); 155 | lastPos = i + 1; 156 | } 157 | } 158 | } 159 | if (lastPos === 0) 160 | str = f; 161 | else if (lastPos < f.length) 162 | str += f.slice(lastPos); 163 | while (a < arguments.length) { 164 | const x = arguments[a++]; 165 | if ((typeof x !== 'object' && typeof x !== 'symbol') || x === null) 166 | str += ` ${x}`; 167 | else 168 | str += ` ${inspect(x, inspectOptions)}`; 169 | } 170 | return str; 171 | } 172 | 173 | var CIRCULAR_ERROR_MESSAGE; 174 | 175 | function tryStringify(arg) { 176 | try { 177 | return JSON.stringify(arg); 178 | } catch (err) { 179 | // Populate the circular error message lazily 180 | if (!CIRCULAR_ERROR_MESSAGE) { 181 | try { 182 | const a = {}; 183 | a.a = a; 184 | JSON.stringify(a); 185 | } catch (e) { 186 | CIRCULAR_ERROR_MESSAGE = e.message; 187 | } 188 | } 189 | if (err.name === 'TypeError' && err.message === CIRCULAR_ERROR_MESSAGE) 190 | return '[Circular]'; 191 | throw err; 192 | } 193 | } 194 | 195 | module.exports = { format, formatWithOptions, inspect }; 196 | 197 | function inspect(value, opts, arg2, arg3) { 198 | // Default options 199 | const ctx = { 200 | seen: [], 201 | stylize: stylizeNoColor, 202 | showHidden: inspectDefaultOptions.showHidden, 203 | depth: inspectDefaultOptions.depth, 204 | colors: inspectDefaultOptions.colors, 205 | showProxy: inspectDefaultOptions.showProxy, 206 | maxArrayLength: inspectDefaultOptions.maxArrayLength, 207 | breakLength: inspectDefaultOptions.breakLength, 208 | indentationLvl: 0, 209 | compact: inspectDefaultOptions.compact, 210 | }; 211 | // Legacy... 212 | if (arguments.length > 2) { 213 | if (arg2 !== undefined) 214 | ctx.depth = arg2; 215 | 216 | if (arguments.length > 3 && arg3 !== undefined) 217 | ctx.colors = arg3; 218 | } 219 | // Set user-specified options 220 | if (typeof opts === 'boolean') { 221 | ctx.showHidden = opts; 222 | } else if (opts) { 223 | const optKeys = Object.keys(opts); 224 | for (var i = 0; i < optKeys.length; i++) 225 | ctx[optKeys[i]] = opts[optKeys[i]]; 226 | } 227 | if (ctx.colors) 228 | ctx.stylize = stylizeWithColor; 229 | if (ctx.maxArrayLength === null) 230 | ctx.maxArrayLength = Infinity; 231 | return formatValue(ctx, value, ctx.depth); 232 | } 233 | 234 | // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics 235 | inspect.colors = Object.assign(Object.create(null), { 236 | bold: [1, 22], 237 | italic: [3, 23], 238 | underline: [4, 24], 239 | inverse: [7, 27], 240 | white: [37, 39], 241 | grey: [90, 39], 242 | black: [30, 39], 243 | blue: [34, 39], 244 | cyan: [36, 39], 245 | green: [32, 39], 246 | magenta: [35, 39], 247 | red: [31, 39], 248 | yellow: [33, 39], 249 | }); 250 | 251 | inspect.styles = Object.assign(Object.create(null), { 252 | special: 'cyan', 253 | bigint: 'blue', 254 | number: 'blue', 255 | boolean: 'yellow', 256 | undefined: 'grey', 257 | null: 'bold', 258 | string: 'green', 259 | symbol: 'green', 260 | date: 'magenta', 261 | // "name": intentionally not styling 262 | regexp: 'red', 263 | }); 264 | 265 | function stylizeNoColor(str) { 266 | return str; 267 | } 268 | 269 | function stylizeWithColor(str, styleType) { 270 | const style = inspect.styles[styleType]; 271 | if (style !== undefined) { 272 | const color = inspect.colors[style]; 273 | return `\u001b[${color[0]}m${str}\u001b[${color[1]}m`; 274 | } 275 | return str; 276 | } 277 | 278 | function formatValue(ctx, value, recurseTimes, ln) { 279 | // Primitive types cannot have properties 280 | if (typeof value !== 'object' && typeof value !== 'function') 281 | return formatPrimitive(ctx.stylize, value, ctx); 282 | 283 | if (value === null) 284 | return ctx.stylize('null', 'null'); 285 | 286 | var keys; 287 | var symbols = Object.getOwnPropertySymbols(value); 288 | 289 | // Look up the keys of the object. 290 | if (ctx.showHidden) { 291 | keys = Object.getOwnPropertyNames(value); 292 | } else { 293 | keys = Object.keys(value); 294 | if (symbols.length !== 0) 295 | symbols = symbols.filter((key) => propertyIsEnumerable.call(value, key)); 296 | } 297 | 298 | const keyLength = keys.length + symbols.length; 299 | 300 | const { constructor, tag } = getIdentificationOf(value); 301 | var prefix = ''; 302 | if (constructor && tag && constructor !== tag) 303 | prefix = `${constructor} [${tag}] `; 304 | else if (constructor) 305 | prefix = `${constructor} `; 306 | else if (tag) 307 | prefix = `[${tag}] `; 308 | var base = ''; 309 | var formatter = formatObject; 310 | var braces; 311 | var noIterator = true; 312 | var raw; 313 | // Iterators and the rest are split to reduce checks 314 | if (value[Symbol.iterator]) { 315 | noIterator = false; 316 | if (Array.isArray(value)) { 317 | // Only set the constructor for non ordinary ("Array [...]") arrays. 318 | braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']']; 319 | if (value.length === 0 && keyLength === 0) 320 | return `${braces[0]}]`; 321 | formatter = formatArray; 322 | } else if (isSet(value)) { 323 | if (value.size === 0 && keyLength === 0) 324 | return `${prefix}{}`; 325 | braces = [`${prefix}{`, '}']; 326 | formatter = formatSet; 327 | } else if (isMap(value)) { 328 | if (value.size === 0 && keyLength === 0) 329 | return `${prefix}{}`; 330 | braces = [`${prefix}{`, '}']; 331 | formatter = formatMap; 332 | } else if (isTypedArray(value)) { 333 | braces = [`${prefix}[`, ']']; 334 | formatter = formatTypedArray; 335 | } else { 336 | // Check for boxed strings with valueOf() 337 | // The .valueOf() call can fail for a multitude of reasons 338 | try { 339 | raw = value.valueOf(); 340 | } catch (e) { /* ignore */ } 341 | if (typeof raw === 'string') { 342 | const formatted = formatPrimitive(stylizeNoColor, raw, ctx); 343 | if (keyLength === raw.length) 344 | return ctx.stylize(`[String: ${formatted}]`, 'string'); 345 | base = `[String: ${formatted}]`; 346 | // For boxed Strings, we have to remove the 0-n indexed entries, 347 | // since they just noisy up the output and are redundant 348 | // Make boxed primitive Strings look like such 349 | keys = keys.slice(value.length); 350 | braces = ['{', '}']; 351 | } else { 352 | noIterator = true; 353 | } 354 | } 355 | } 356 | if (noIterator) { 357 | braces = ['{', '}']; 358 | if (prefix === 'Object ') { 359 | // Object fast path 360 | if (keyLength === 0) 361 | return '{}'; 362 | } else if (typeof value === 'function') { 363 | const name = 364 | `${constructor || tag}${value.name ? `: ${value.name}` : ''}`; 365 | if (keyLength === 0) 366 | return ctx.stylize(`[${name}]`, 'special'); 367 | base = `[${name}]`; 368 | } else if (isRegExp(value)) { 369 | // Make RegExps say that they are RegExps 370 | if (keyLength === 0 || recurseTimes < 0) 371 | return ctx.stylize(regExpToString.call(value), 'regexp'); 372 | base = `${regExpToString.call(value)}`; 373 | } else if (isDate(value)) { 374 | if (keyLength === 0) { 375 | if (Number.isNaN(value.getTime())) 376 | return ctx.stylize(value.toString(), 'date'); 377 | return ctx.stylize(dateToISOString.call(value), 'date'); 378 | } 379 | // Make dates with properties first say the date 380 | base = `${dateToISOString.call(value)}`; 381 | } else if (isError(value)) { 382 | // Make error with message first say the error 383 | if (keyLength === 0) 384 | return formatError(value); 385 | base = `${formatError(value)}`; 386 | } else if (isAnyArrayBuffer(value)) { 387 | // Fast path for ArrayBuffer and SharedArrayBuffer. 388 | // Can't do the same for DataView because it has a non-primitive 389 | // .buffer property that we need to recurse for. 390 | if (keyLength === 0) { 391 | return `${prefix 392 | }{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`; 393 | } 394 | braces[0] = `${prefix}{`; 395 | keys.unshift('byteLength'); 396 | } else if (isDataView(value)) { 397 | braces[0] = `${prefix}{`; 398 | // .buffer goes last, it's not a primitive like the others. 399 | keys.unshift('byteLength', 'byteOffset', 'buffer'); 400 | } else if (isPromise(value)) { 401 | braces[0] = `${prefix}{`; 402 | formatter = formatPromise; 403 | } else { 404 | // Check boxed primitives other than string with valueOf() 405 | // NOTE: `Date` has to be checked first! 406 | // The .valueOf() call can fail for a multitude of reasons 407 | try { 408 | raw = value.valueOf(); 409 | } catch (e) { /* ignore */ } 410 | if (typeof raw === 'number') { 411 | // Make boxed primitive Numbers look like such 412 | const formatted = formatPrimitive(stylizeNoColor, raw); 413 | if (keyLength === 0) 414 | return ctx.stylize(`[Number: ${formatted}]`, 'number'); 415 | base = `[Number: ${formatted}]`; 416 | } else if (typeof raw === 'boolean') { 417 | // Make boxed primitive Booleans look like such 418 | const formatted = formatPrimitive(stylizeNoColor, raw); 419 | if (keyLength === 0) 420 | return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean'); 421 | base = `[Boolean: ${formatted}]`; 422 | } else if (typeof raw === 'symbol') { 423 | const formatted = formatPrimitive(stylizeNoColor, raw); 424 | return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol'); 425 | } else if (keyLength === 0) { 426 | if (isExternal(value)) 427 | return ctx.stylize('[External]', 'special'); 428 | return `${prefix}{}`; 429 | } else { 430 | braces[0] = `${prefix}{`; 431 | } 432 | } 433 | } 434 | // Using an array here is actually better for the average case than using 435 | // a Set. `seen` will only check for the depth and will never grow too large. 436 | if (ctx.seen.indexOf(value) !== -1) 437 | return ctx.stylize('[Circular]', 'special'); 438 | if (recurseTimes != null) { // eslint-disable-line eqeqeq 439 | if (recurseTimes < 0) 440 | return ctx.stylize(`[${constructor || tag || 'Object'}]`, 'special'); 441 | recurseTimes -= 1; 442 | } 443 | 444 | ctx.seen.push(value); 445 | const output = formatter(ctx, value, recurseTimes, keys); 446 | 447 | for (var i = 0; i < symbols.length; i++) 448 | output.push(formatProperty(ctx, value, recurseTimes, symbols[i], 0)); 449 | 450 | ctx.seen.pop(); 451 | 452 | return reduceToSingleString(ctx, output, base, braces, ln); 453 | } 454 | 455 | function isMap(o) { 456 | return o instanceof Map; 457 | } 458 | 459 | function isSet(o) { 460 | return o instanceof Set; 461 | } 462 | 463 | function isExternal() { 464 | return false; 465 | } 466 | 467 | function isPromise(o) { 468 | return o instanceof Promise; 469 | } 470 | 471 | function isRegExp(o) { 472 | return o instanceof RegExp; 473 | } 474 | 475 | function isTypedArray(o) { 476 | for (const type of [ 477 | Int8Array, 478 | Uint8Array, 479 | Uint8ClampedArray, 480 | Int16Array, 481 | Uint16Array, 482 | Int32Array, 483 | Uint32Array, 484 | Float32Array, 485 | Float64Array, 486 | ]) { 487 | if (o instanceof type) 488 | return true; 489 | } 490 | 491 | return false; 492 | } 493 | 494 | function isDate(o) { 495 | return o instanceof Date; 496 | } 497 | 498 | function isError(o) { 499 | return o instanceof Error; 500 | } 501 | 502 | function isDataView(o) { 503 | return ArrayBuffer.isView(o); 504 | } 505 | 506 | function isAnyArrayBuffer(o) { 507 | return o instanceof ArrayBuffer; 508 | } 509 | 510 | function formatNumber(fn, value) { 511 | // Format -0 as '-0'. Checking `value === -0` won't distinguish 0 from -0. 512 | if (Object.is(value, -0)) 513 | return fn('-0', 'number'); 514 | return fn(`${value}`, 'number'); 515 | } 516 | 517 | function formatPrimitive(fn, value, ctx) { 518 | if (typeof value === 'string') { 519 | if (ctx.compact === false && 520 | value.length > MIN_LINE_LENGTH && 521 | ctx.indentationLvl + value.length > ctx.breakLength) { 522 | // eslint-disable-next-line max-len 523 | const minLineLength = Math.max(ctx.breakLength - ctx.indentationLvl, MIN_LINE_LENGTH); 524 | // eslint-disable-next-line max-len 525 | const averageLineLength = Math.ceil(value.length / Math.ceil(value.length / minLineLength)); 526 | const divisor = Math.max(averageLineLength, MIN_LINE_LENGTH); 527 | var res = ''; 528 | if (readableRegExps[divisor] === undefined) { 529 | // Build a new RegExp that naturally breaks text into multiple lines. 530 | // 531 | // Rules 532 | // 1. Greedy match all text up the max line length that ends with a 533 | // whitespace or the end of the string. 534 | // 2. If none matches, non-greedy match any text up to a whitespace or 535 | // the end of the string. 536 | // 537 | // eslint-disable-next-line max-len, no-unescaped-regexp-dot 538 | readableRegExps[divisor] = new RegExp(`(.|\\n){1,${divisor}}(\\s|$)|(\\n|.)+?(\\s|$)`, 'gm'); 539 | } 540 | const indent = ' '.repeat(ctx.indentationLvl); 541 | const matches = value.match(readableRegExps[divisor]); 542 | if (matches.length > 1) { 543 | res += `${fn(strEscape(matches[0]), 'string')} +\n`; 544 | for (var i = 1; i < matches.length - 1; i++) 545 | res += `${indent} ${fn(strEscape(matches[i]), 'string')} +\n`; 546 | 547 | res += `${indent} ${fn(strEscape(matches[i]), 'string')}`; 548 | return res; 549 | } 550 | } 551 | return fn(strEscape(value), 'string'); 552 | } 553 | if (typeof value === 'bigint') // eslint-disable-line valid-typeof 554 | return fn(`${value}n`, 'bigint'); 555 | if (typeof value === 'number') 556 | return formatNumber(fn, value); 557 | if (typeof value === 'boolean') 558 | return fn(`${value}`, 'boolean'); 559 | if (typeof value === 'undefined') 560 | return fn('undefined', 'undefined'); 561 | // es6 symbol primitive 562 | return fn(value.toString(), 'symbol'); 563 | } 564 | 565 | function formatError(value) { 566 | return value.stack || `[${errorToString.call(value)}]`; 567 | } 568 | 569 | function formatObject(ctx, value, recurseTimes, keys) { 570 | const len = keys.length; 571 | const output = new Array(len); 572 | for (var i = 0; i < len; i++) 573 | output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 0); 574 | return output; 575 | } 576 | 577 | // The array is sparse and/or has extra keys 578 | function formatSpecialArray(ctx, value, recurseTimes, keys, maxLength, valLen) { 579 | const output = []; 580 | const keyLen = keys.length; 581 | var visibleLength = 0; 582 | var i = 0; 583 | if (keyLen !== 0 && numberRegExp.test(keys[0])) { 584 | for (const key of keys) { 585 | if (visibleLength === maxLength) 586 | break; 587 | const index = +key; 588 | // Arrays can only have up to 2^32 - 1 entries 589 | if (index > (2 ** 32) - 2) 590 | break; 591 | if (i !== index) { 592 | if (!numberRegExp.test(key)) 593 | break; 594 | const emptyItems = index - i; 595 | const ending = emptyItems > 1 ? 's' : ''; 596 | const message = `<${emptyItems} empty item${ending}>`; 597 | output.push(ctx.stylize(message, 'undefined')); 598 | i = index; 599 | if (++visibleLength === maxLength) 600 | break; 601 | } 602 | output.push(formatProperty(ctx, value, recurseTimes, key, 1)); 603 | visibleLength++; 604 | i++; 605 | } 606 | } 607 | if (i < valLen && visibleLength !== maxLength) { 608 | const len = valLen - i; 609 | const ending = len > 1 ? 's' : ''; 610 | const message = `<${len} empty item${ending}>`; 611 | output.push(ctx.stylize(message, 'undefined')); 612 | i = valLen; 613 | if (keyLen === 0) 614 | return output; 615 | } 616 | const remaining = valLen - i; 617 | if (remaining > 0) 618 | output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`); 619 | 620 | if (ctx.showHidden && keys[keyLen - 1] === 'length') { 621 | // No extra keys 622 | output.push(formatProperty(ctx, value, recurseTimes, 'length', 2)); 623 | } else if (valLen === 0 || (keyLen > valLen && keys[valLen - 1] === `${valLen - 1}`)) { 624 | // The array is not sparse 625 | for (i = valLen; i < keyLen; i++) 626 | output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); 627 | } else if (keys[keyLen - 1] !== `${valLen - 1}`) { 628 | const extra = []; 629 | // Only handle special keys 630 | var key; 631 | for (i = keys.length - 1; i >= 0; i--) { 632 | key = keys[i]; 633 | if (numberRegExp.test(key) && +key < (2 ** 32) - 1) 634 | break; 635 | extra.push(formatProperty(ctx, value, recurseTimes, key, 2)); 636 | } 637 | for (i = extra.length - 1; i >= 0; i--) 638 | output.push(extra[i]); 639 | } 640 | return output; 641 | } 642 | 643 | function formatArray(ctx, value, recurseTimes, keys) { 644 | const len = Math.min(Math.max(0, ctx.maxArrayLength), value.length); 645 | const hidden = ctx.showHidden ? 1 : 0; 646 | const valLen = value.length; 647 | const keyLen = keys.length - hidden; 648 | if (keyLen !== valLen || keys[keyLen - 1] !== `${valLen - 1}`) 649 | return formatSpecialArray(ctx, value, recurseTimes, keys, len, valLen); 650 | 651 | const remaining = valLen - len; 652 | const output = new Array(len + (remaining > 0 ? 1 : 0) + hidden); 653 | for (var i = 0; i < len; i++) 654 | output[i] = formatProperty(ctx, value, recurseTimes, keys[i], 1); 655 | if (remaining > 0) 656 | output[i++] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; 657 | if (ctx.showHidden === true) 658 | output[i] = formatProperty(ctx, value, recurseTimes, 'length', 2); 659 | return output; 660 | } 661 | 662 | function formatTypedArray(ctx, value, recurseTimes, keys) { 663 | const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length); 664 | const remaining = value.length - maxLength; 665 | const output = new Array(maxLength + (remaining > 0 ? 1 : 0)); 666 | for (var i = 0; i < maxLength; ++i) 667 | output[i] = formatNumber(ctx.stylize, value[i]); 668 | if (remaining > 0) 669 | output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`; 670 | if (ctx.showHidden) { 671 | // .buffer goes last, it's not a primitive like the others. 672 | const extraKeys = [ 673 | 'BYTES_PER_ELEMENT', 674 | 'length', 675 | 'byteLength', 676 | 'byteOffset', 677 | 'buffer', 678 | ]; 679 | for (i = 0; i < extraKeys.length; i++) { 680 | const str = formatValue(ctx, value[extraKeys[i]], recurseTimes); 681 | output.push(`[${extraKeys[i]}]: ${str}`); 682 | } 683 | } 684 | // TypedArrays cannot have holes. Therefore it is safe to assume that all 685 | // extra keys are indexed after value.length. 686 | for (i = value.length; i < keys.length; i++) 687 | output.push(formatProperty(ctx, value, recurseTimes, keys[i], 2)); 688 | 689 | return output; 690 | } 691 | 692 | function formatSet(ctx, value, recurseTimes, keys) { 693 | const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); 694 | var i = 0; 695 | for (const v of value) 696 | output[i++] = formatValue(ctx, v, recurseTimes); 697 | // With `showHidden`, `length` will display as a hidden property for 698 | // arrays. For consistency's sake, do the same for `size`, even though this 699 | // property isn't selected by Object.getOwnPropertyNames(). 700 | if (ctx.showHidden) 701 | output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; 702 | for (var n = 0; n < keys.length; n++) 703 | output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); 704 | 705 | return output; 706 | } 707 | 708 | function formatMap(ctx, value, recurseTimes, keys) { 709 | const output = new Array(value.size + keys.length + (ctx.showHidden ? 1 : 0)); 710 | var i = 0; 711 | for (const [k, v] of value) { 712 | output[i++] = `${formatValue(ctx, k, recurseTimes)} => ${ 713 | formatValue(ctx, v, recurseTimes)}`; 714 | } 715 | // See comment in formatSet 716 | if (ctx.showHidden) 717 | output[i++] = `[size]: ${ctx.stylize(`${value.size}`, 'number')}`; 718 | for (var n = 0; n < keys.length; n++) 719 | output[i++] = formatProperty(ctx, value, recurseTimes, keys[n], 0); 720 | 721 | return output; 722 | } 723 | 724 | function formatPromise(ctx, value, recurseTimes, keys) { 725 | var output; 726 | const [state, result] = getPromiseDetails(value); 727 | if (state === kPending) { 728 | output = ['']; 729 | } else { 730 | const str = formatValue(ctx, result, recurseTimes); 731 | output = [state === kRejected ? ` ${str}` : str]; 732 | } 733 | for (var n = 0; n < keys.length; n++) 734 | output.push(formatProperty(ctx, value, recurseTimes, keys[n], 0)); 735 | return output; 736 | } 737 | 738 | function formatProperty(ctx, value, recurseTimes, key, array) { 739 | var name, str; 740 | const desc = Object.getOwnPropertyDescriptor(value, key) || 741 | { value: value[key], enumerable: true }; 742 | if (desc.value !== undefined) { 743 | const diff = array !== 0 || ctx.compact === false ? 2 : 3; 744 | ctx.indentationLvl += diff; 745 | str = formatValue(ctx, desc.value, recurseTimes, array === 0); 746 | ctx.indentationLvl -= diff; 747 | } else if (desc.get !== undefined) { 748 | if (desc.set !== undefined) 749 | str = ctx.stylize('[Getter/Setter]', 'special'); 750 | else 751 | str = ctx.stylize('[Getter]', 'special'); 752 | } else if (desc.set !== undefined) { 753 | str = ctx.stylize('[Setter]', 'special'); 754 | } else { 755 | str = ctx.stylize('undefined', 'undefined'); 756 | } 757 | if (array === 1) 758 | return str; 759 | 760 | if (typeof key === 'symbol') 761 | name = `[${ctx.stylize(key.toString(), 'symbol')}]`; 762 | else if (desc.enumerable === false) 763 | name = `[${key}]`; 764 | else if (keyStrRegExp.test(key)) 765 | name = ctx.stylize(key, 'name'); 766 | else 767 | name = ctx.stylize(strEscape(key), 'string'); 768 | 769 | 770 | return `${name}: ${str}`; 771 | } 772 | 773 | function reduceToSingleString(ctx, output, base, braces, addLn) { 774 | const breakLength = ctx.breakLength; 775 | var i = 0; 776 | if (ctx.compact === false) { 777 | const indentation = ' '.repeat(ctx.indentationLvl); 778 | var res = `${base ? `${base} ` : ''}${braces[0]}\n${indentation} `; 779 | for (; i < output.length - 1; i++) 780 | res += `${output[i]},\n${indentation} `; 781 | 782 | res += `${output[i]}\n${indentation}${braces[1]}`; 783 | return res; 784 | } 785 | if (output.length * 2 <= breakLength) { 786 | var length = 0; 787 | for (; i < output.length && length <= breakLength; i++) { 788 | if (ctx.colors) 789 | length += output[i].replace(colorRegExp, '').length + 1; 790 | else 791 | length += output[i].length + 1; 792 | } 793 | if (length <= breakLength) { 794 | return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ${ 795 | braces[1]}`; 796 | } 797 | } 798 | // If the opening "brace" is too large, like in the case of "Set {", 799 | // we need to force the first item to be on the next line or the 800 | // items will not line up correctly. 801 | const indentation = ' '.repeat(ctx.indentationLvl); 802 | const extraLn = addLn === true ? `\n${indentation}` : ''; 803 | const ln = base === '' && braces[0].length === 1 ? 804 | ' ' : `${base ? ` ${base}` : base}\n${indentation} `; 805 | const str = join(output, `,\n${indentation} `); 806 | return `${extraLn}${braces[0]}${ln}${str} ${braces[1]}`; 807 | } 808 | 809 | function getIdentificationOf(obj) { 810 | const original = obj; 811 | let constructor; 812 | let tag; 813 | 814 | while (obj) { 815 | if (constructor === undefined) { 816 | const desc = Object.getOwnPropertyDescriptor(obj, 'constructor'); 817 | if (desc !== undefined && 818 | typeof desc.value === 'function' && 819 | desc.value.name !== '') 820 | constructor = desc.value.name; 821 | } 822 | 823 | if (tag === undefined) { 824 | const desc = Object.getOwnPropertyDescriptor(obj, Symbol.toStringTag); 825 | if (desc !== undefined) { 826 | if (typeof desc.value === 'string') { 827 | tag = desc.value; 828 | } else if (desc.get !== undefined) { 829 | tag = desc.get.call(original); 830 | if (typeof tag !== 'string') // eslint-disable-line max-depth 831 | tag = undefined; 832 | } 833 | } 834 | } 835 | 836 | if (constructor !== undefined && tag !== undefined) 837 | break; 838 | 839 | obj = Object.getPrototypeOf(obj); 840 | } 841 | 842 | return { constructor, tag }; 843 | } 844 | 845 | function join(output, separator) { 846 | var str = ''; 847 | if (output.length !== 0) { 848 | for (var i = 0; i < output.length - 1; i++) { 849 | // It is faster not to use a template string here 850 | str += output[i]; 851 | str += separator; 852 | } 853 | str += output[i]; 854 | } 855 | return str; 856 | } 857 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@snek/threads", 3 | "version": "0.0.2", 4 | "description": "thread like a pro", 5 | "main": "lib/index.js", 6 | "dependencies": { 7 | "bindings": "^1.3.0" 8 | }, 9 | "devDependencies": { 10 | "buffer": "^5.0.8", 11 | "uglifyjs-webpack-plugin": "^1.1.4", 12 | "webpack": "^3.10.0" 13 | }, 14 | "scripts": { 15 | "build": "node-gyp rebuild", 16 | "build:preload": "webpack", 17 | "prepublishOnly": "webpack" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/devsnek/threads.git" 22 | }, 23 | "author": "snek ", 24 | "license": "MIT", 25 | "gypfile": true, 26 | "bugs": { 27 | "url": "https://github.com/devsnek/threads/issues" 28 | }, 29 | "homepage": "https://github.com/devsnek/threads#readme" 30 | } 31 | -------------------------------------------------------------------------------- /src/NativeUtil.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "util.h" 3 | 4 | using namespace v8; 5 | 6 | static void GetPromiseDetails(const FunctionCallbackInfo& info) { 7 | Isolate* isolate = info.GetIsolate(); 8 | 9 | Local ret = Array::New(isolate, 2); 10 | info.GetReturnValue().Set(ret); 11 | 12 | if (!info[0]->IsPromise()) 13 | return; 14 | 15 | Local promise = info[0].As(); 16 | 17 | int state = promise->State(); 18 | ret->Set(0, Integer::New(isolate, state)); 19 | if (state != Promise::PromiseState::kPending) 20 | ret->Set(1, promise->Result()); 21 | } 22 | 23 | Local MakeNativeUtil(Isolate* isolate) { 24 | Local context = isolate->GetCurrentContext(); 25 | 26 | Local obj = Object::New(isolate); 27 | 28 | #define V(name, fn) \ 29 | USE(obj->Set(context, String::NewFromUtf8(isolate, name), FunctionTemplate::New(isolate, fn)->GetFunction())) 30 | V("getPromiseDetails", GetPromiseDetails); 31 | #undef V 32 | 33 | #define V(name, val) \ 34 | USE(obj->Set(context, String::NewFromUtf8(isolate, name), Integer::New(isolate, val))) 35 | V("kPending", Promise::PromiseState::kPending); 36 | V("kFulfilled", Promise::PromiseState::kFulfilled); 37 | V("kRejected", Promise::PromiseState::kRejected); 38 | #undef V 39 | 40 | return obj; 41 | } 42 | -------------------------------------------------------------------------------- /src/NativeUtil.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | v8::Local MakeNativeUtil(v8::Isolate*); 4 | -------------------------------------------------------------------------------- /src/Worker.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "Worker.h" 6 | #include "util.h" 7 | #include "NativeUtil.h" 8 | 9 | using namespace v8; 10 | 11 | uint64_t timeOrigin = 0; 12 | 13 | uv_thread_t main_thread; 14 | bool inThread() { 15 | uv_thread_t this_thread = uv_thread_self(); 16 | return !uv_thread_equal(&main_thread, &this_thread); 17 | } 18 | 19 | Persistent Worker::constructor; 20 | 21 | const char* preload = ""; 22 | 23 | void SetPreload(const FunctionCallbackInfo& info) { 24 | Isolate* isolate = info.GetIsolate(); 25 | String::Utf8Value preload_utf8(isolate, info[0].As()); 26 | preload = strdup(*preload_utf8); 27 | } 28 | 29 | void Worker::Init(Local exports) { 30 | Isolate* isolate = Isolate::GetCurrent(); 31 | HandleScope scope(isolate); 32 | 33 | Local tpl = FunctionTemplate::New(isolate, New); 34 | tpl->SetClassName(String::NewFromUtf8(isolate, "Worker")); 35 | tpl->InstanceTemplate()->SetInternalFieldCount(1); 36 | 37 | NODE_SET_PROTOTYPE_METHOD(tpl, "getPromise", GetPromise); 38 | NODE_SET_PROTOTYPE_METHOD(tpl, "getId", GetId); 39 | NODE_SET_PROTOTYPE_METHOD(tpl, "getState", GetState); 40 | NODE_SET_PROTOTYPE_METHOD(tpl, "send", Send); 41 | NODE_SET_PROTOTYPE_METHOD(tpl, "terminate", Terminate); 42 | NODE_SET_PROTOTYPE_METHOD(tpl, "checkOutgoingMessages", CheckOutgoingMessages); 43 | NODE_SET_PROTOTYPE_METHOD(tpl, "lock", Lock); 44 | NODE_SET_PROTOTYPE_METHOD(tpl, "unlock", Unlock); 45 | 46 | Local constants = Object::New(isolate); 47 | #define V(name, val) \ 48 | constants->Set(String::NewFromUtf8(isolate, name), Integer::New(isolate, val)); 49 | V("created", Worker::State::created); 50 | V("running", Worker::State::running); 51 | V("terminated", Worker::State::terminated); 52 | #undef V 53 | exports->Set(String::NewFromUtf8(isolate, "constants"), constants); 54 | 55 | exports->Set(String::NewFromUtf8(isolate, "setPreload"), FunctionTemplate::New(isolate, SetPreload)->GetFunction()); 56 | 57 | Local fn = tpl->GetFunction(); 58 | constructor.Reset(isolate, fn); 59 | exports->Set(String::NewFromUtf8(isolate, "Worker"), fn); 60 | } 61 | 62 | uint32_t WorkerId = 0; 63 | 64 | Worker::Worker(Local resolver, Worker::Source source) { 65 | Isolate* isolate = Isolate::GetCurrent(); 66 | 67 | this->source_ = source; 68 | this->persistent_.Reset(isolate, resolver); 69 | this->id = WorkerId++; 70 | 71 | this->async_.data = this; 72 | uv_async_init(uv_default_loop(), &this->async_, Worker::WorkCallback); 73 | 74 | uv_thread_t* thread = new uv_thread_t; 75 | uv_thread_create(thread, Worker::WorkThread, this); 76 | } 77 | 78 | Worker::~Worker() { 79 | this->persistent_.Reset(); 80 | } 81 | 82 | void Worker::New(const FunctionCallbackInfo& info) { 83 | Isolate* isolate = info.GetIsolate(); 84 | HandleScope scope(isolate); 85 | 86 | main_thread = uv_thread_self(); 87 | 88 | Local context = isolate->GetCurrentContext(); 89 | 90 | if (info.IsConstructCall()) { 91 | Local local = Promise::Resolver::New(isolate); 92 | 93 | Worker::Source source; 94 | 95 | String::Utf8Value code_utf8(isolate, info[0].As()); 96 | char* buffer = strdup(*code_utf8); 97 | source.code = buffer; 98 | 99 | source.arguments = serialize(isolate, info[1].As()); 100 | 101 | Local references = info[2].As(); 102 | Local refkeys = references->GetPropertyNames().As(); 103 | source.references_length = refkeys->Length() > 128 ? 128 : refkeys->Length(); 104 | for (int i = 0; i < source.references_length; i++) { 105 | String::Utf8Value ref_utf8(isolate, refkeys->Get(i).As()); 106 | char* copy = strdup(*ref_utf8); 107 | source.references[i] = copy; 108 | } 109 | 110 | Worker* worker = new Worker(local, source); 111 | worker->Wrap(info.This()); 112 | 113 | info.GetReturnValue().Set(info.This()); 114 | } else { 115 | const int argc = 2; 116 | Local argv[argc] = { info[0], info[1] }; 117 | Local cons = Local::New(isolate, constructor); 118 | info.GetReturnValue().Set(cons->NewInstance(context, argc, argv).ToLocalChecked()); 119 | } 120 | } 121 | 122 | void Worker::WorkThread(void *arg) { 123 | Worker* worker = (Worker*) arg; // work->data; 124 | 125 | worker->state = Worker::State::running; 126 | 127 | Worker::Source source = worker->source_; 128 | 129 | 130 | timeOrigin = uv_hrtime(); 131 | 132 | Isolate::CreateParams create_params; 133 | create_params.array_buffer_allocator = 134 | ArrayBuffer::Allocator::NewDefaultAllocator(); 135 | Isolate* isolate = Isolate::New(create_params); 136 | 137 | { 138 | Locker locker(isolate); 139 | HandleScope scope(isolate); 140 | 141 | Local tpl = ObjectTemplate::New(isolate); 142 | tpl->SetInternalFieldCount(1); 143 | Local context = Context::New(isolate, nullptr, tpl); 144 | 145 | Context::Scope context_scope(context); 146 | TryCatch try_catch(isolate); 147 | #define CHECK_ERR() \ 148 | if (try_catch.HasCaught()) { \ 149 | worker->error_ = serialize(isolate, try_catch.StackTrace()); \ 150 | worker->state = Worker::State::terminated; \ 151 | uv_async_send(&worker->async_); \ 152 | return; \ 153 | } 154 | 155 | Local global = context->Global(); 156 | global->SetAlignedPointerInInternalField(0, worker); 157 | USE(global->Set(context, String::NewFromUtf8(isolate, "global"), global)); 158 | 159 | USE(global->Set(context, String::NewFromUtf8(isolate, "_console"), FunctionTemplate::New(isolate, ThreadConsole)->GetFunction())); 160 | 161 | Local perf = Object::New(isolate); 162 | #define V(name, val) \ 163 | USE(perf->Set(context, String::NewFromUtf8(isolate, name), val)) 164 | 165 | V("_hrtime",FunctionTemplate::New(isolate, ThreadHrtime)->GetFunction()); 166 | V("timeOrigin", Number::New(isolate, timeOrigin / 1e6)); 167 | #undef V 168 | USE(global->Set(context, String::NewFromUtf8(isolate, "performance"), perf)); 169 | 170 | USE(global->Set(context, String::NewFromUtf8(isolate, "_util"), MakeNativeUtil(isolate))); 171 | 172 | CHECK_ERR(); 173 | 174 | ScriptOrigin origin(String::NewFromUtf8(isolate, "Thread"), // file name 175 | Integer::New(isolate, 0), // line offset 176 | Integer::New(isolate, 0), // column offset 177 | False(isolate), // is cross origin 178 | Local(), // script id 179 | Local(), // source map URL 180 | False(isolate), // is opaque (?) 181 | False(isolate), // is WASM 182 | False(isolate)); // is ES6 module 183 | 184 | USE(Script::Compile(context, String::NewFromUtf8(isolate, preload)).ToLocalChecked()->Run(context)); 185 | 186 | Local code = String::NewFromUtf8(isolate, source.code); 187 | MaybeLocal