like-thread 1.0.0 → 1.0.1

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 +197 -272
  2. package/package.json +4 -2
  3. package/worker.js +77 -0
package/index.js CHANGED
@@ -1,369 +1,294 @@
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
+ })
105
+ }
135
106
 
136
- return
137
- }
107
+ _onMessage (msg) {
108
+ const req = this.requests.get(msg.id)
138
109
 
139
- if (msg.done) {
140
- this.requests.delete(msg.id)
110
+ if (!req) {
111
+ return
112
+ }
141
113
 
142
- req.iterator.close()
143
-
144
- return
145
- }
114
+ // console.log('_onMessage', msg, !!req.iterator ? 'iterator' : 'promise')
146
115
 
116
+ if (msg.error) {
147
117
  this.requests.delete(msg.id)
148
118
 
119
+ const err = new Error(msg.error)
120
+
149
121
  if (req.resolver) {
150
- req.resolver.reject(new Error('Invalid message'))
122
+ req.resolver.reject(err)
151
123
  } else {
152
- req.iterator.fail(new Error('Invalid message'))
124
+ req.iterator.fail(err)
153
125
  }
126
+
127
+ return
154
128
  }
155
129
 
156
- call (...args) {
157
- if (this.id === 0xffffffff) {
158
- this.id = 1
130
+ // msg.value can be a falsy value like 0 or null
131
+ if (msg.v) {
132
+ if (req.resolver) {
133
+ req.resolver.resolve(msg.value)
134
+ } else {
135
+ req.iterator.push(msg.value)
159
136
  }
160
137
 
161
- const id = this.id++
138
+ return
139
+ }
162
140
 
163
- const sab = new SharedArrayBuffer(4)
164
- const control = new Int32Array(sab)
141
+ if (msg.done) {
142
+ this.requests.delete(msg.id)
165
143
 
166
- Atomics.store(control, 0, 0)
144
+ req.iterator.close()
167
145
 
168
- if (!this.isGenerator) {
169
- const resolver = promiseWithResolvers()
146
+ return
147
+ }
170
148
 
171
- this.requests.set(id, { resolver })
149
+ this.requests.delete(msg.id)
172
150
 
173
- this.worker.postMessage({ id, args, sab })
151
+ if (req.resolver) {
152
+ req.resolver.reject(new Error('Invalid message'))
153
+ } else {
154
+ req.iterator.fail(new Error('Invalid message'))
155
+ }
156
+ }
174
157
 
175
- return resolver.promise
176
- }
158
+ call (...args) {
159
+ if (this.id === 0xffffffff) {
160
+ this.id = 1
161
+ }
177
162
 
178
- const iterator = {
179
- resolver: promiseWithResolvers(),
180
- queue: [],
181
- closed: false,
182
- push: function (value) {
183
- if (this.closed) {
184
- return
185
- }
163
+ const id = this.id++
186
164
 
187
- this.queue.push({ value, done: false })
188
- this.resolver.resolve()
189
- },
190
- fail: function (err) {
191
- if (this.closed) {
192
- return
193
- }
165
+ const sab = new SharedArrayBuffer(4)
166
+ const control = new Int32Array(sab)
194
167
 
195
- this.closed = true
196
- this.resolver.resolve(err)
197
- },
198
- close: function () {
199
- if (this.closed) {
200
- return
201
- }
168
+ Atomics.store(control, 0, 0)
202
169
 
203
- this.closed = true
204
- this.resolver.resolve()
205
- }
206
- }
170
+ if (!this.isGenerator) {
171
+ const resolver = promiseWithResolvers()
207
172
 
208
- this.requests.set(id, { iterator })
173
+ this.requests.set(id, { resolver })
209
174
 
210
175
  this.worker.postMessage({ id, args, sab })
211
176
 
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
177
+ return resolver.promise
178
+ }
222
179
 
223
- if (err) {
224
- throw err
225
- }
180
+ const iterator = {
181
+ resolver: promiseWithResolvers(),
182
+ queue: [],
183
+ closed: false,
184
+ push: function (value) {
185
+ if (this.closed) {
186
+ return
187
+ }
226
188
 
227
- if (iterator.queue.length === 0) {
228
- return { done: true }
229
- }
189
+ this.queue.push({ value, done: false })
190
+ this.resolver.resolve()
191
+ },
192
+ fail: function (err) {
193
+ if (this.closed) {
194
+ return
195
+ }
230
196
 
231
- if (iterator.queue.length === 1) {
232
- iterator.resolver = promiseWithResolvers()
233
- }
197
+ this.closed = true
198
+ this.resolver.resolve(err)
199
+ },
200
+ close: function () {
201
+ if (this.closed) {
202
+ return
203
+ }
234
204
 
235
- const item = iterator.queue.shift()
205
+ this.closed = true
206
+ this.resolver.resolve()
207
+ }
208
+ }
236
209
 
237
- return item
238
- },
210
+ this.requests.set(id, { iterator })
239
211
 
240
- return: () => {
241
- iterator.closed = true
212
+ this.worker.postMessage({ id, args, sab })
242
213
 
243
- Atomics.store(control, 0, 2)
244
- Atomics.notify(control, 0, 1)
214
+ return {
215
+ [Symbol.asyncIterator] () {
216
+ return this
217
+ },
245
218
 
246
- // this.worker.postMessage({ id, command: 'stop' })
247
- // return
219
+ async next () {
220
+ Atomics.store(control, 0, 1)
221
+ Atomics.notify(control, 0, 1)
248
222
 
249
- // iterator.queue.length = 0
250
- // iterator.resolver.resolve()
223
+ const err = await iterator.resolver.promise
251
224
 
252
- return Promise.resolve({ done: true })
225
+ if (err) {
226
+ throw err
253
227
  }
254
- }
255
- }
256
228
 
257
- async * [Symbol.asyncIterator] () {
258
- for await (const [value] of this.iterator) {
259
- yield value
260
- }
261
- }
262
-
263
- async next () {
264
- const { done, value } = await this.iterator.next()
229
+ if (iterator.queue.length === 0) {
230
+ return { done: true }
231
+ }
265
232
 
266
- return { done, value: done ? undefined : value[0] }
267
- }
233
+ if (iterator.queue.length === 1) {
234
+ iterator.resolver = promiseWithResolvers()
235
+ }
268
236
 
269
- return () {
270
- return this.iterator.return()
271
- }
237
+ const item = iterator.queue.shift()
272
238
 
273
- then (onFulfilled, onRejected) {
274
- return this.promise.then(onFulfilled, onRejected)
275
- }
239
+ return item
240
+ },
276
241
 
277
- catch (onRejected) {
278
- return this.promise.catch(onRejected)
279
- }
242
+ return: () => {
243
+ iterator.closed = true
280
244
 
281
- finally (onFinally) {
282
- return this.promise.finally(onFinally)
283
- }
245
+ Atomics.store(control, 0, 2)
246
+ Atomics.notify(control, 0, 1)
284
247
 
285
- async close () {
286
- this.worker.postMessage({ command: 'exit' })
248
+ // this.worker.postMessage({ id, command: 'stop' })
249
+ // return
287
250
 
288
- await this.promise
251
+ // iterator.queue.length = 0
252
+ // iterator.resolver.resolve()
289
253
 
290
- // await this.worker.terminate()
254
+ return Promise.resolve({ done: true })
255
+ }
291
256
  }
292
257
  }
293
- } else {
294
- const entrypoint = workerData.fn.match(/(async )?function( ?\* ?)? ([a-zA-Z0-9]+)? ?\(([^)]*?)\)/i)
295
258
 
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
-
303
- parentPort.on('message', async (msg) => {
304
- if (msg.command === 'exit') {
305
- process.exit(0)
306
- return
259
+ async * [Symbol.asyncIterator] () {
260
+ for await (const [value] of this.iterator) {
261
+ yield value
307
262
  }
263
+ }
308
264
 
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()
265
+ async next () {
266
+ const { done, value } = await this.iterator.next()
341
267
 
342
- Atomics.store(control, 0, 0)
268
+ return { done, value: done ? undefined : value[0] }
269
+ }
343
270
 
344
- if (done) {
345
- parentPort.postMessage({ id: msg.id, done: true })
346
- break
347
- }
271
+ return () {
272
+ return this.iterator.return()
273
+ }
348
274
 
349
- // Keep "v" because "value" can be falsy like 0 or null
350
- parentPort.postMessage({ id: msg.id, value, v: true })
351
- }
275
+ then (onFulfilled, onRejected) {
276
+ return this.promise.then(onFulfilled, onRejected)
277
+ }
352
278
 
353
- return
354
- }
279
+ catch (onRejected) {
280
+ return this.promise.catch(onRejected)
281
+ }
355
282
 
356
- if (isAsync) {
357
- const value = await out
283
+ finally (onFinally) {
284
+ return this.promise.finally(onFinally)
285
+ }
358
286
 
359
- parentPort.postMessage({ id: msg.id, value, v: true })
287
+ async close () {
288
+ this.worker.postMessage({ command: 'exit' })
360
289
 
361
- return
362
- }
290
+ await this.promise
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.worker.terminate()
293
+ }
369
294
  }
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "like-thread",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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(0)
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
+ })