like-thread 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +199 -272
  2. package/package.json +4 -2
  3. package/worker.js +77 -0
package/index.js CHANGED
@@ -1,369 +1,296 @@
1
- const { isMainThread, Worker, workerData, parentPort } = require('worker_threads')
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { Worker } = require('worker_threads')
4
+ const promiseWithResolvers = require('promise-resolvers')
2
5
 
3
- if (isMainThread) {
4
- const promiseWithResolvers = require('promise-resolvers')
6
+ const WORKER_SCRIPT = fs.readFileSync(path.join(__dirname, 'worker.js'), 'utf8')
5
7
 
6
- module.exports = class Thread {
7
- static go (args, fn) {
8
- const thread = new this(fn, args)
8
+ module.exports = class Thread {
9
+ static go (args, fn) {
10
+ const thread = new this(fn, args)
9
11
 
10
- if (!thread.isGenerator) {
11
- const value = thread.call(...args)
12
+ if (!thread.isGenerator) {
13
+ const value = thread.call(...args)
12
14
 
13
- return value.finally(() => thread.close())
14
- }
15
+ return value.finally(() => thread.close())
16
+ }
15
17
 
16
- const iterator = thread.call(...args)
18
+ const iterator = thread.call(...args)
17
19
 
18
- return {
19
- [Symbol.asyncIterator] () {
20
- return this
21
- },
20
+ return {
21
+ [Symbol.asyncIterator] () {
22
+ return this
23
+ },
22
24
 
23
- async next () {
24
- const result = await iterator.next()
25
+ async next () {
26
+ const result = await iterator.next()
25
27
 
26
- if (result.done) {
27
- await thread.close()
28
- }
28
+ if (result.done) {
29
+ await thread.close()
30
+ }
29
31
 
30
- return result
31
- },
32
+ return result
33
+ },
32
34
 
33
- async return (value) {
34
- try {
35
- return await iterator.return(value)
36
- } finally {
37
- await thread.close()
38
- }
35
+ async return (value) {
36
+ try {
37
+ return await iterator.return(value)
38
+ } finally {
39
+ await thread.close()
39
40
  }
40
41
  }
41
42
  }
43
+ }
42
44
 
43
- constructor (fn, opts = {}) {
44
- const workerData = {
45
- fn: fn.toString()
46
- }
45
+ constructor (fn, opts = {}) {
46
+ const workerData = {
47
+ fn: fn.toString()
48
+ }
47
49
 
48
- const entrypoint = workerData.fn.match(/(async )?function( ?\* ?)? ([a-zA-Z0-9]+)? ?\(([^)]*?)\)/i)
50
+ const entrypoint = workerData.fn.match(/(async )?function( ?\* ?)? ([a-zA-Z0-9]+)? ?\(([^)]*?)\)/i)
49
51
 
50
- this.isAsync = !!entrypoint[1]
51
- this.isGenerator = !!entrypoint[2]
52
+ this.isAsync = !!entrypoint[1]
53
+ this.isGenerator = !!entrypoint[2]
52
54
 
53
- this.worker = new Worker(__filename, { workerData, argv: opts.argv || null })
55
+ this.worker = new Worker(WORKER_SCRIPT, { eval: true, workerData, argv: opts.argv || null })
54
56
 
55
- this.id = 1
56
- this.requests = new Map()
57
+ this.id = 1
58
+ this.requests = new Map()
57
59
 
58
- this.worker.on('message', this._onMessage.bind(this))
60
+ this.worker.on('message', this._onMessage.bind(this))
59
61
 
60
- this.promise = new Promise((resolve, reject) => {
61
- let error = null
62
+ this.promise = new Promise((resolve, reject) => {
63
+ let error = null
62
64
 
63
- this.worker.on('error', err => {
64
- error = err
65
+ this.worker.on('error', err => {
66
+ error = err
65
67
 
66
- for (const [id, req] of this.requests) {
67
- this.requests.delete(id)
68
+ for (const [id, req] of this.requests) {
69
+ this.requests.delete(id)
68
70
 
69
- if (req.resolver) {
70
- req.resolver.reject(err)
71
- } else {
72
- req.iterator.fail(err)
73
- }
71
+ if (req.resolver) {
72
+ req.resolver.reject(err)
73
+ } else {
74
+ req.iterator.fail(err)
74
75
  }
76
+ }
75
77
 
76
- reject(err)
77
- })
78
-
79
- this.worker.on('exit', (exitCode, signal) => {
80
- if (error) return
81
-
82
- const err = new Error('Worker closed (' + exitCode + ' and ' + signal + ')')
78
+ reject(err)
79
+ })
83
80
 
84
- for (const [id, req] of this.requests) {
85
- this.requests.delete(id)
81
+ this.worker.on('exit', (exitCode, signal) => {
82
+ if (error) return
86
83
 
87
- if (req.resolver) {
88
- req.resolver.reject(err)
89
- } else {
90
- req.iterator.fail(err)
91
- }
92
- }
84
+ const err = new Error('Worker closed (' + exitCode + ' and ' + signal + ')')
93
85
 
94
- const success = exitCode === 0 || exitCode === 130 || signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP'
86
+ for (const [id, req] of this.requests) {
87
+ this.requests.delete(id)
95
88
 
96
- if (success) {
97
- resolve()
89
+ if (req.resolver) {
90
+ req.resolver.reject(err)
98
91
  } else {
99
- reject(err)
92
+ req.iterator.fail(err)
100
93
  }
101
- })
102
- })
103
- }
104
-
105
- _onMessage (msg) {
106
- const req = this.requests.get(msg.id)
107
-
108
- if (!req) {
109
- return
110
- }
111
-
112
- // console.log('_onMessage', msg, !!req.iterator ? 'iterator' : 'promise')
113
-
114
- if (msg.error) {
115
- this.requests.delete(msg.id)
116
-
117
- const err = new Error(msg.error)
118
-
119
- if (req.resolver) {
120
- req.resolver.reject(err)
121
- } else {
122
- req.iterator.fail(err)
123
94
  }
124
95
 
125
- return
126
- }
96
+ const success = exitCode === 0 || exitCode === 130 || signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP'
127
97
 
128
- // msg.value can be a falsy value like 0 or null
129
- if (msg.v) {
130
- if (req.resolver) {
131
- req.resolver.resolve(msg.value)
98
+ if (success) {
99
+ resolve()
132
100
  } else {
133
- req.iterator.push(msg.value)
101
+ reject(err)
134
102
  }
103
+ })
104
+ })
135
105
 
136
- return
137
- }
106
+ this.promise.catch(noop)
107
+ }
138
108
 
139
- if (msg.done) {
140
- this.requests.delete(msg.id)
109
+ _onMessage (msg) {
110
+ const req = this.requests.get(msg.id)
141
111
 
142
- req.iterator.close()
112
+ if (!req) {
113
+ return
114
+ }
143
115
 
144
- return
145
- }
116
+ // console.log('_onMessage', msg, !!req.iterator ? 'iterator' : 'promise')
146
117
 
118
+ if (msg.error) {
147
119
  this.requests.delete(msg.id)
148
120
 
121
+ const err = new Error(msg.error)
122
+
149
123
  if (req.resolver) {
150
- req.resolver.reject(new Error('Invalid message'))
124
+ req.resolver.reject(err)
151
125
  } else {
152
- req.iterator.fail(new Error('Invalid message'))
126
+ req.iterator.fail(err)
153
127
  }
128
+
129
+ return
154
130
  }
155
131
 
156
- call (...args) {
157
- if (this.id === 0xffffffff) {
158
- this.id = 1
132
+ // msg.value can be a falsy value like 0 or null
133
+ if (msg.v) {
134
+ if (req.resolver) {
135
+ req.resolver.resolve(msg.value)
136
+ } else {
137
+ req.iterator.push(msg.value)
159
138
  }
160
139
 
161
- const id = this.id++
140
+ return
141
+ }
162
142
 
163
- const sab = new SharedArrayBuffer(4)
164
- const control = new Int32Array(sab)
143
+ if (msg.done) {
144
+ this.requests.delete(msg.id)
165
145
 
166
- Atomics.store(control, 0, 0)
146
+ req.iterator.close()
167
147
 
168
- if (!this.isGenerator) {
169
- const resolver = promiseWithResolvers()
148
+ return
149
+ }
170
150
 
171
- this.requests.set(id, { resolver })
151
+ this.requests.delete(msg.id)
172
152
 
173
- this.worker.postMessage({ id, args, sab })
153
+ if (req.resolver) {
154
+ req.resolver.reject(new Error('Invalid message'))
155
+ } else {
156
+ req.iterator.fail(new Error('Invalid message'))
157
+ }
158
+ }
174
159
 
175
- return resolver.promise
176
- }
160
+ call (...args) {
161
+ if (this.id === 0xffffffff) {
162
+ this.id = 1
163
+ }
177
164
 
178
- const iterator = {
179
- resolver: promiseWithResolvers(),
180
- queue: [],
181
- closed: false,
182
- push: function (value) {
183
- if (this.closed) {
184
- return
185
- }
165
+ const id = this.id++
186
166
 
187
- this.queue.push({ value, done: false })
188
- this.resolver.resolve()
189
- },
190
- fail: function (err) {
191
- if (this.closed) {
192
- return
193
- }
167
+ const sab = new SharedArrayBuffer(4)
168
+ const control = new Int32Array(sab)
194
169
 
195
- this.closed = true
196
- this.resolver.resolve(err)
197
- },
198
- close: function () {
199
- if (this.closed) {
200
- return
201
- }
170
+ Atomics.store(control, 0, 0)
202
171
 
203
- this.closed = true
204
- this.resolver.resolve()
205
- }
206
- }
172
+ if (!this.isGenerator) {
173
+ const resolver = promiseWithResolvers()
207
174
 
208
- this.requests.set(id, { iterator })
175
+ this.requests.set(id, { resolver })
209
176
 
210
177
  this.worker.postMessage({ id, args, sab })
211
178
 
212
- return {
213
- [Symbol.asyncIterator] () {
214
- return this
215
- },
216
-
217
- async next () {
218
- Atomics.store(control, 0, 1)
219
- Atomics.notify(control, 0, 1)
220
-
221
- const err = await iterator.resolver.promise
179
+ return resolver.promise
180
+ }
222
181
 
223
- if (err) {
224
- throw err
225
- }
182
+ const iterator = {
183
+ resolver: promiseWithResolvers(),
184
+ queue: [],
185
+ closed: false,
186
+ push: function (value) {
187
+ if (this.closed) {
188
+ return
189
+ }
226
190
 
227
- if (iterator.queue.length === 0) {
228
- return { done: true }
229
- }
191
+ this.queue.push({ value, done: false })
192
+ this.resolver.resolve()
193
+ },
194
+ fail: function (err) {
195
+ if (this.closed) {
196
+ return
197
+ }
230
198
 
231
- if (iterator.queue.length === 1) {
232
- iterator.resolver = promiseWithResolvers()
233
- }
199
+ this.closed = true
200
+ this.resolver.resolve(err)
201
+ },
202
+ close: function () {
203
+ if (this.closed) {
204
+ return
205
+ }
234
206
 
235
- const item = iterator.queue.shift()
207
+ this.closed = true
208
+ this.resolver.resolve()
209
+ }
210
+ }
236
211
 
237
- return item
238
- },
212
+ this.requests.set(id, { iterator })
239
213
 
240
- return: () => {
241
- iterator.closed = true
214
+ this.worker.postMessage({ id, args, sab })
242
215
 
243
- Atomics.store(control, 0, 2)
244
- Atomics.notify(control, 0, 1)
216
+ return {
217
+ [Symbol.asyncIterator] () {
218
+ return this
219
+ },
245
220
 
246
- // this.worker.postMessage({ id, command: 'stop' })
247
- // return
221
+ async next () {
222
+ Atomics.store(control, 0, 1)
223
+ Atomics.notify(control, 0, 1)
248
224
 
249
- // iterator.queue.length = 0
250
- // iterator.resolver.resolve()
225
+ const err = await iterator.resolver.promise
251
226
 
252
- return Promise.resolve({ done: true })
227
+ if (err) {
228
+ throw err
253
229
  }
254
- }
255
- }
256
230
 
257
- async * [Symbol.asyncIterator] () {
258
- for await (const [value] of this.iterator) {
259
- yield value
260
- }
261
- }
231
+ if (iterator.queue.length === 0) {
232
+ return { done: true }
233
+ }
262
234
 
263
- async next () {
264
- const { done, value } = await this.iterator.next()
235
+ if (iterator.queue.length === 1) {
236
+ iterator.resolver = promiseWithResolvers()
237
+ }
265
238
 
266
- return { done, value: done ? undefined : value[0] }
267
- }
239
+ const item = iterator.queue.shift()
268
240
 
269
- return () {
270
- return this.iterator.return()
271
- }
241
+ return item
242
+ },
272
243
 
273
- then (onFulfilled, onRejected) {
274
- return this.promise.then(onFulfilled, onRejected)
275
- }
244
+ return: () => {
245
+ iterator.closed = true
276
246
 
277
- catch (onRejected) {
278
- return this.promise.catch(onRejected)
279
- }
280
-
281
- finally (onFinally) {
282
- return this.promise.finally(onFinally)
283
- }
247
+ Atomics.store(control, 0, 2)
248
+ Atomics.notify(control, 0, 1)
284
249
 
285
- async close () {
286
- this.worker.postMessage({ command: 'exit' })
250
+ // this.worker.postMessage({ id, command: 'stop' })
251
+ // return
287
252
 
288
- await this.promise
253
+ // iterator.queue.length = 0
254
+ // iterator.resolver.resolve()
289
255
 
290
- // await this.worker.terminate()
256
+ return Promise.resolve({ done: true })
257
+ }
291
258
  }
292
259
  }
293
- } else {
294
- const entrypoint = workerData.fn.match(/(async )?function( ?\* ?)? ([a-zA-Z0-9]+)? ?\(([^)]*?)\)/i)
295
-
296
- const isAsync = !!entrypoint[1]
297
- const isGenerator = !!entrypoint[2]
298
- const functionName = entrypoint[3] || '_WORKER_MAIN_'
299
- const functionArgs = entrypoint[4] || ''
300
-
301
- const fn = workerData.fn.replace(entrypoint[0], (isAsync ? 'async ' : '') + 'function ' + (isGenerator ? '* ' : '') + functionName + ' (' + (functionArgs) + ')')
302
260
 
303
- parentPort.on('message', async (msg) => {
304
- if (msg.command === 'exit') {
305
- process.exit(0)
306
- return
261
+ async * [Symbol.asyncIterator] () {
262
+ for await (const [value] of this.iterator) {
263
+ yield value
307
264
  }
265
+ }
308
266
 
309
- try {
310
- // TODO: Optimize by creating the function only once by fixing the args
311
- const source = fn + '\n\nmodule.exports = ' + functionName + '(...' + JSON.stringify(msg.args || []) + ')'
312
-
313
- const wrap = new Function('require', '__dirname', '__filename', 'module', 'exports', 'parentPort', source) // eslint-disable-line no-new-func
314
-
315
- const mod = { exports: {} }
316
-
317
- wrap(require, __dirname, __filename, mod, mod.exports, parentPort)
318
-
319
- const out = mod.exports
320
-
321
- if (isGenerator) {
322
- const control = new Int32Array(msg.sab)
323
- const it = out[Symbol.asyncIterator] ? out[Symbol.asyncIterator]() : out[Symbol.iterator]()
324
-
325
- while (true) {
326
- while (Atomics.load(control, 0) === 0) {
327
- const res = Atomics.waitAsync(control, 0, 0)
328
-
329
- if (res.async) {
330
- await res.value
331
- } else {
332
- break
333
- }
334
- }
335
-
336
- if (Atomics.load(control, 0) === 2) {
337
- break
338
- }
339
-
340
- const { value, done } = await it.next()
341
-
342
- Atomics.store(control, 0, 0)
267
+ async next () {
268
+ const { done, value } = await this.iterator.next()
343
269
 
344
- if (done) {
345
- parentPort.postMessage({ id: msg.id, done: true })
346
- break
347
- }
270
+ return { done, value: done ? undefined : value[0] }
271
+ }
348
272
 
349
- // Keep "v" because "value" can be falsy like 0 or null
350
- parentPort.postMessage({ id: msg.id, value, v: true })
351
- }
273
+ return () {
274
+ return this.iterator.return()
275
+ }
352
276
 
353
- return
354
- }
277
+ then (onFulfilled, onRejected) {
278
+ return this.promise.then(onFulfilled, onRejected)
279
+ }
355
280
 
356
- if (isAsync) {
357
- const value = await out
281
+ catch (onRejected) {
282
+ return this.promise.catch(onRejected)
283
+ }
358
284
 
359
- parentPort.postMessage({ id: msg.id, value, v: true })
285
+ finally (onFinally) {
286
+ return this.promise.finally(onFinally)
287
+ }
360
288
 
361
- return
362
- }
289
+ async close () {
290
+ this.worker.postMessage({ command: 'exit' })
363
291
 
364
- parentPort.postMessage({ id: msg.id, value: out, v: true })
365
- } catch (err) {
366
- parentPort.postMessage({ id: msg.id, error: err })
367
- }
368
- })
292
+ await this.promise.catch(noop)
293
+ }
369
294
  }
295
+
296
+ function noop () {}
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "like-thread",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
- "files": [],
6
+ "files": [
7
+ "worker.js"
8
+ ],
7
9
  "scripts": {
8
10
  "test": "standard && brittle test.js"
9
11
  },
package/worker.js ADDED
@@ -0,0 +1,77 @@
1
+ const { workerData, parentPort } = require('worker_threads')
2
+
3
+ const entrypoint = workerData.fn.match(/(async )?function( ?\* ?)? ([a-zA-Z0-9]+)? ?\(([^)]*?)\)/i)
4
+
5
+ const isAsync = !!entrypoint[1]
6
+ const isGenerator = !!entrypoint[2]
7
+ const functionName = entrypoint[3] || '_WORKER_MAIN_'
8
+ const functionArgs = entrypoint[4] || ''
9
+
10
+ const fn = workerData.fn.replace(entrypoint[0], (isAsync ? 'async ' : '') + 'function ' + (isGenerator ? '* ' : '') + functionName + ' (' + (functionArgs) + ')')
11
+
12
+ parentPort.on('message', async (msg) => {
13
+ if (msg.command === 'exit') {
14
+ process.exit()
15
+ return
16
+ }
17
+
18
+ try {
19
+ // TODO: Optimize by creating the function only once by fixing the args
20
+ const source = fn + '\n\nmodule.exports = ' + functionName + '(...' + JSON.stringify(msg.args || []) + ')'
21
+
22
+ const wrap = new Function('require', '__dirname', '__filename', 'module', 'exports', 'parentPort', source) // eslint-disable-line no-new-func
23
+
24
+ const mod = { exports: {} }
25
+
26
+ wrap(require, __dirname, __filename, mod, mod.exports, parentPort)
27
+
28
+ const out = mod.exports
29
+
30
+ if (isGenerator) {
31
+ const control = new Int32Array(msg.sab)
32
+ const it = out[Symbol.asyncIterator] ? out[Symbol.asyncIterator]() : out[Symbol.iterator]()
33
+
34
+ while (true) {
35
+ while (Atomics.load(control, 0) === 0) {
36
+ const res = Atomics.waitAsync(control, 0, 0)
37
+
38
+ if (res.async) {
39
+ await res.value
40
+ } else {
41
+ break
42
+ }
43
+ }
44
+
45
+ if (Atomics.load(control, 0) === 2) {
46
+ break
47
+ }
48
+
49
+ const { value, done } = await it.next()
50
+
51
+ Atomics.store(control, 0, 0)
52
+
53
+ if (done) {
54
+ parentPort.postMessage({ id: msg.id, done: true })
55
+ break
56
+ }
57
+
58
+ // Keep "v" because "value" can be falsy like 0 or null
59
+ parentPort.postMessage({ id: msg.id, value, v: true })
60
+ }
61
+
62
+ return
63
+ }
64
+
65
+ if (isAsync) {
66
+ const value = await out
67
+
68
+ parentPort.postMessage({ id: msg.id, value, v: true })
69
+
70
+ return
71
+ }
72
+
73
+ parentPort.postMessage({ id: msg.id, value: out, v: true })
74
+ } catch (err) {
75
+ parentPort.postMessage({ id: msg.id, error: err })
76
+ }
77
+ })