thread-stream 1.0.1 → 2.1.0
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.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/package-manager-ci.yml +2 -2
- package/README.md +25 -0
- package/index.d.ts +67 -3
- package/index.js +26 -9
- package/lib/worker.js +3 -0
- package/package.json +2 -2
- package/test/base.test.js +1 -1
- package/test/bundlers.test.js +28 -1
- package/test/emit-event.js +22 -0
- package/test/event.test.js +23 -0
- package/test/string-limit-2.test.js +6 -0
- package/test/string-limit.test.js +6 -0
- package/test/thread-management.test.js +32 -84
package/.github/workflows/ci.yml
CHANGED
|
@@ -14,7 +14,7 @@ jobs:
|
|
|
14
14
|
runs-on: ${{ matrix.os }}
|
|
15
15
|
strategy:
|
|
16
16
|
matrix:
|
|
17
|
-
os: [
|
|
17
|
+
os: [ubuntu-latest]
|
|
18
18
|
node-version: [14, 16, 18]
|
|
19
19
|
steps:
|
|
20
20
|
- uses: actions/checkout@v3
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
runs-on: ${{ matrix.os }}
|
|
38
38
|
strategy:
|
|
39
39
|
matrix:
|
|
40
|
-
os: [
|
|
40
|
+
os: [ubuntu-latest]
|
|
41
41
|
node-version: [14, 16, 18]
|
|
42
42
|
steps:
|
|
43
43
|
- uses: actions/checkout@v3
|
package/README.md
CHANGED
|
@@ -84,6 +84,31 @@ stream.end()
|
|
|
84
84
|
|
|
85
85
|
This module works with `yarn` in PnP (plug'n play) mode too!
|
|
86
86
|
|
|
87
|
+
### Emit events
|
|
88
|
+
|
|
89
|
+
You can emit events on the ThreadStream from your worker using [`worker.parentPort.postMessage()`](https://nodejs.org/api/worker_threads.html#workerparentport).
|
|
90
|
+
The message (JSON object) must have the following data structure:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
parentPort.postMessage({
|
|
94
|
+
code: 'EVENT',
|
|
95
|
+
name: 'eventName',
|
|
96
|
+
args: ['list', 'of', 'args', 123, new Error('Boom')]
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
On your ThreadStream, you can add a listener function for this event name:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const stream = new ThreadStream({
|
|
104
|
+
filename: join(__dirname, 'worker.js'),
|
|
105
|
+
workerData: {},
|
|
106
|
+
})
|
|
107
|
+
stream.on('eventName', function (a, b, c, n, err) {
|
|
108
|
+
console.log('received:', a, b, c, n, err) // received: list of args 123 Error: Boom
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
87
112
|
## License
|
|
88
113
|
|
|
89
114
|
MIT
|
package/index.d.ts
CHANGED
|
@@ -1,9 +1,73 @@
|
|
|
1
1
|
import { EventEmitter } from 'events'
|
|
2
|
+
import workerThreads from 'worker_threads'
|
|
3
|
+
|
|
4
|
+
interface ThreadStreamOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The size (in bytes) of the buffer.
|
|
7
|
+
* Must be greater than 4 (i.e. it must at least fit a 4-byte utf-8 char).
|
|
8
|
+
*
|
|
9
|
+
* Default: `4 * 1024 * 1024` = `4194304`
|
|
10
|
+
*/
|
|
11
|
+
bufferSize?: number,
|
|
12
|
+
/**
|
|
13
|
+
* The path to the Worker's main script or module.
|
|
14
|
+
* Must be either an absolute path or a relative path (i.e. relative to the current working directory) starting with ./ or ../, or a WHATWG URL object using file: or data: protocol.
|
|
15
|
+
* When using a data: URL, the data is interpreted based on MIME type using the ECMAScript module loader.
|
|
16
|
+
*
|
|
17
|
+
* {@link workerThreads.Worker()}
|
|
18
|
+
*/
|
|
19
|
+
filename: string | URL,
|
|
20
|
+
/**
|
|
21
|
+
* If `true`, write data synchronously; otherwise write data asynchronously.
|
|
22
|
+
*
|
|
23
|
+
* Default: `false`.
|
|
24
|
+
*/
|
|
25
|
+
sync?: boolean,
|
|
26
|
+
/**
|
|
27
|
+
* {@link workerThreads.WorkerOptions.workerData}
|
|
28
|
+
*
|
|
29
|
+
* Default: `{}`
|
|
30
|
+
*/
|
|
31
|
+
workerData?: any,
|
|
32
|
+
/**
|
|
33
|
+
* {@link workerThreads.WorkerOptions}
|
|
34
|
+
*
|
|
35
|
+
* Default: `{}`
|
|
36
|
+
*/
|
|
37
|
+
workerOpts?: workerThreads.WorkerOptions
|
|
38
|
+
}
|
|
39
|
+
|
|
2
40
|
|
|
3
41
|
declare class ThreadStream extends EventEmitter {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
42
|
+
/**
|
|
43
|
+
* @param {ThreadStreamOptions} opts
|
|
44
|
+
*/
|
|
45
|
+
constructor(opts: ThreadStreamOptions)
|
|
46
|
+
/**
|
|
47
|
+
* Write some data to the stream.
|
|
48
|
+
*
|
|
49
|
+
* **Please note that this method should not throw an {Error} if something goes wrong but emit an error event.**
|
|
50
|
+
* @param {string} data data to write.
|
|
51
|
+
* @returns {boolean} false if the stream wishes for the calling code to wait for the 'drain' event to be emitted before continuing to write additional data or if it fails to write; otherwise true.
|
|
52
|
+
*/
|
|
53
|
+
write(data: string): boolean
|
|
54
|
+
/**
|
|
55
|
+
* Signal that no more data will be written.
|
|
56
|
+
*
|
|
57
|
+
* **Please note that this method should not throw an {Error} if something goes wrong but emit an error event.**
|
|
58
|
+
*
|
|
59
|
+
* Calling the {@link write()} method after calling {@link end()} will emit an error.
|
|
60
|
+
*/
|
|
61
|
+
end(): void
|
|
62
|
+
/**
|
|
63
|
+
* Flush the stream synchronously.
|
|
64
|
+
* This method should be called in the shutdown phase to make sure that all data has been flushed.
|
|
65
|
+
*
|
|
66
|
+
* **Please note that this method will throw an {Error} if something goes wrong.**
|
|
67
|
+
*
|
|
68
|
+
* @throws {Error} if the stream is already flushing, if it fails to flush or if it takes more than 10 seconds to flush.
|
|
69
|
+
*/
|
|
70
|
+
flushSync(): void
|
|
7
71
|
}
|
|
8
72
|
|
|
9
73
|
export = ThreadStream;
|
package/index.js
CHANGED
|
@@ -136,7 +136,7 @@ function nextFlush (stream) {
|
|
|
136
136
|
})
|
|
137
137
|
} else {
|
|
138
138
|
// This should never happen
|
|
139
|
-
|
|
139
|
+
destroy(stream, new Error('overwritten'))
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -163,8 +163,15 @@ function onWorkerMessage (msg) {
|
|
|
163
163
|
case 'ERROR':
|
|
164
164
|
destroy(stream, msg.err)
|
|
165
165
|
break
|
|
166
|
+
case 'EVENT':
|
|
167
|
+
if (Array.isArray(msg.args)) {
|
|
168
|
+
stream.emit(msg.name, ...msg.args)
|
|
169
|
+
} else {
|
|
170
|
+
stream.emit(msg.name, msg.args)
|
|
171
|
+
}
|
|
172
|
+
break
|
|
166
173
|
default:
|
|
167
|
-
|
|
174
|
+
destroy(stream, new Error('this should not happen: ' + msg.code))
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
177
|
|
|
@@ -177,7 +184,7 @@ function onWorkerExit (code) {
|
|
|
177
184
|
registry.unregister(stream)
|
|
178
185
|
stream.worker.exited = true
|
|
179
186
|
stream.worker.off('exit', onWorkerExit)
|
|
180
|
-
destroy(stream, code !== 0 ? new Error('
|
|
187
|
+
destroy(stream, code !== 0 ? new Error('the worker thread exited') : null)
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
class ThreadStream extends EventEmitter {
|
|
@@ -211,11 +218,13 @@ class ThreadStream extends EventEmitter {
|
|
|
211
218
|
|
|
212
219
|
write (data) {
|
|
213
220
|
if (this[kImpl].destroyed) {
|
|
214
|
-
|
|
221
|
+
error(this, new Error('the worker has exited'))
|
|
222
|
+
return false
|
|
215
223
|
}
|
|
216
224
|
|
|
217
225
|
if (this[kImpl].ending) {
|
|
218
|
-
|
|
226
|
+
error(this, new Error('the worker is ending'))
|
|
227
|
+
return false
|
|
219
228
|
}
|
|
220
229
|
|
|
221
230
|
if (this[kImpl].flushing && this[kImpl].buf.length + data.length >= MAX_STRING) {
|
|
@@ -338,6 +347,12 @@ class ThreadStream extends EventEmitter {
|
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
349
|
|
|
350
|
+
function error (stream, err) {
|
|
351
|
+
setImmediate(() => {
|
|
352
|
+
stream.emit('error', err)
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
341
356
|
function destroy (stream, err) {
|
|
342
357
|
if (stream[kImpl].destroyed) {
|
|
343
358
|
return
|
|
@@ -346,7 +361,7 @@ function destroy (stream, err) {
|
|
|
346
361
|
|
|
347
362
|
if (err) {
|
|
348
363
|
stream[kImpl].errored = err
|
|
349
|
-
stream
|
|
364
|
+
error(stream, err)
|
|
350
365
|
}
|
|
351
366
|
|
|
352
367
|
if (!stream.worker.exited) {
|
|
@@ -399,11 +414,13 @@ function end (stream) {
|
|
|
399
414
|
readIndex = Atomics.load(stream[kImpl].state, READ_INDEX)
|
|
400
415
|
|
|
401
416
|
if (readIndex === -2) {
|
|
402
|
-
|
|
417
|
+
destroy(stream, new Error('end() failed'))
|
|
418
|
+
return
|
|
403
419
|
}
|
|
404
420
|
|
|
405
421
|
if (++spins === 10) {
|
|
406
|
-
|
|
422
|
+
destroy(stream, new Error('end() took too long (10s)'))
|
|
423
|
+
return
|
|
407
424
|
}
|
|
408
425
|
}
|
|
409
426
|
|
|
@@ -482,7 +499,7 @@ function flushSync (stream) {
|
|
|
482
499
|
const readIndex = Atomics.load(stream[kImpl].state, READ_INDEX)
|
|
483
500
|
|
|
484
501
|
if (readIndex === -2) {
|
|
485
|
-
throw
|
|
502
|
+
throw Error('_flushSync failed')
|
|
486
503
|
}
|
|
487
504
|
|
|
488
505
|
// process._rawDebug(`(flushSync) readIndex (${readIndex}) writeIndex (${writeIndex})`)
|
package/lib/worker.js
CHANGED
|
@@ -44,6 +44,9 @@ async function start () {
|
|
|
44
44
|
if ((error.code === 'ENOTDIR' || error.code === 'ERR_MODULE_NOT_FOUND') &&
|
|
45
45
|
filename.startsWith('file://')) {
|
|
46
46
|
fn = realRequire(decodeURIComponent(filename.replace('file://', '')))
|
|
47
|
+
} else if (error.code === undefined) {
|
|
48
|
+
// When bundled with pkg, an undefined error is thrown when called with realImport
|
|
49
|
+
fn = realRequire(decodeURIComponent(filename.replace(process.platform === 'win32' ? 'file:///' : 'file://', '')))
|
|
47
50
|
} else {
|
|
48
51
|
throw error
|
|
49
52
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thread-stream",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A streaming way to send data to a Node.js Worker Thread",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"real-require": "^0.
|
|
8
|
+
"real-require": "^0.2.0"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@types/node": "^18.0.0",
|
package/test/base.test.js
CHANGED
|
@@ -263,7 +263,7 @@ test('destroy does not error', function (t) {
|
|
|
263
263
|
})
|
|
264
264
|
|
|
265
265
|
stream.on('error', (err) => {
|
|
266
|
-
t.equal(err.message, '
|
|
266
|
+
t.equal(err.message, 'the worker thread exited')
|
|
267
267
|
stream.flush((err) => {
|
|
268
268
|
t.equal(err.message, 'the worker has exited')
|
|
269
269
|
})
|
package/test/bundlers.test.js
CHANGED
|
@@ -5,7 +5,7 @@ const { join } = require('path')
|
|
|
5
5
|
const { file } = require('./helper')
|
|
6
6
|
const ThreadStream = require('..')
|
|
7
7
|
|
|
8
|
-
test('bundlers support', function (t) {
|
|
8
|
+
test('bundlers support with .js file', function (t) {
|
|
9
9
|
t.plan(1)
|
|
10
10
|
|
|
11
11
|
globalThis.__bundlerPathsOverrides = {
|
|
@@ -31,3 +31,30 @@ test('bundlers support', function (t) {
|
|
|
31
31
|
|
|
32
32
|
stream.end()
|
|
33
33
|
})
|
|
34
|
+
|
|
35
|
+
test('bundlers support with .mjs file', function (t) {
|
|
36
|
+
t.plan(1)
|
|
37
|
+
|
|
38
|
+
globalThis.__bundlerPathsOverrides = {
|
|
39
|
+
'thread-stream-worker': join(__dirname, 'custom-worker.js')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dest = file()
|
|
43
|
+
|
|
44
|
+
process.on('uncaughtException', error => {
|
|
45
|
+
console.log(error)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const stream = new ThreadStream({
|
|
49
|
+
filename: join(__dirname, 'to-file.mjs'),
|
|
50
|
+
workerData: { dest },
|
|
51
|
+
sync: true
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
stream.worker.removeAllListeners('message')
|
|
55
|
+
stream.worker.once('message', message => {
|
|
56
|
+
t.equal(message.code, 'CUSTOM-WORKER-CALLED')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
stream.end()
|
|
60
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Writable } = require('stream')
|
|
4
|
+
const parentPort = require('worker_threads').parentPort
|
|
5
|
+
|
|
6
|
+
async function run () {
|
|
7
|
+
return new Writable({
|
|
8
|
+
autoDestroy: true,
|
|
9
|
+
write (chunk, enc, cb) {
|
|
10
|
+
if (parentPort) {
|
|
11
|
+
parentPort.postMessage({
|
|
12
|
+
code: 'EVENT',
|
|
13
|
+
name: 'socketError',
|
|
14
|
+
args: ['list', 'of', 'args', 123, new Error('unable to write data to the TCP socket')]
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
cb()
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = run
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const { join } = require('path')
|
|
5
|
+
const ThreadStream = require('..')
|
|
6
|
+
|
|
7
|
+
test('event propagate', function (t) {
|
|
8
|
+
const stream = new ThreadStream({
|
|
9
|
+
filename: join(__dirname, 'emit-event.js'),
|
|
10
|
+
workerData: {},
|
|
11
|
+
sync: true
|
|
12
|
+
})
|
|
13
|
+
stream.on('socketError', function (a, b, c, n, error) {
|
|
14
|
+
t.same(a, 'list')
|
|
15
|
+
t.same(b, 'of')
|
|
16
|
+
t.same(c, 'args')
|
|
17
|
+
t.same(n, 123)
|
|
18
|
+
t.same(error, new Error('unable to write data to the TCP socket'))
|
|
19
|
+
t.end()
|
|
20
|
+
})
|
|
21
|
+
stream.write('hello')
|
|
22
|
+
stream.end()
|
|
23
|
+
})
|
|
@@ -29,50 +29,16 @@ test('emit error if thread exits', async function (t) {
|
|
|
29
29
|
stream.write('hello world\n')
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
t.equal(err.message, '
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
stream.write('noop')
|
|
37
|
-
t.fail('unreacheable')
|
|
38
|
-
} catch (err) {
|
|
39
|
-
t.equal(err.message, 'the worker has exited')
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
stream.write('noop')
|
|
44
|
-
t.fail('unreacheable')
|
|
45
|
-
} catch (err) {
|
|
46
|
-
t.equal(err.message, 'the worker has exited')
|
|
47
|
-
}
|
|
48
|
-
})
|
|
32
|
+
let [err] = await once(stream, 'error')
|
|
33
|
+
t.equal(err.message, 'the worker thread exited')
|
|
49
34
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
sync: true
|
|
54
|
-
})
|
|
35
|
+
stream.write('noop');
|
|
36
|
+
[err] = await once(stream, 'error')
|
|
37
|
+
t.equal(err.message, 'the worker has exited')
|
|
55
38
|
|
|
56
|
-
stream.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const [err] = await once(stream, 'error')
|
|
61
|
-
t.equal(err.message, 'The worker thread exited')
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
stream.write('noop')
|
|
65
|
-
t.fail('unreacheable')
|
|
66
|
-
} catch (err) {
|
|
67
|
-
t.equal(err.message, 'the worker has exited')
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
stream.write('noop')
|
|
72
|
-
t.fail('unreacheable')
|
|
73
|
-
} catch (err) {
|
|
74
|
-
t.equal(err.message, 'the worker has exited')
|
|
75
|
-
}
|
|
39
|
+
stream.write('noop');
|
|
40
|
+
[err] = await once(stream, 'error')
|
|
41
|
+
t.equal(err.message, 'the worker has exited')
|
|
76
42
|
})
|
|
77
43
|
|
|
78
44
|
test('emit error if thread have unhandledRejection', async function (t) {
|
|
@@ -85,22 +51,16 @@ test('emit error if thread have unhandledRejection', async function (t) {
|
|
|
85
51
|
stream.write('hello world\n')
|
|
86
52
|
})
|
|
87
53
|
|
|
88
|
-
|
|
54
|
+
let [err] = await once(stream, 'error')
|
|
89
55
|
t.equal(err.message, 'kaboom')
|
|
90
56
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
stream.write('noop')
|
|
100
|
-
t.fail('unreacheable')
|
|
101
|
-
} catch (err) {
|
|
102
|
-
t.equal(err.message, 'the worker has exited')
|
|
103
|
-
}
|
|
57
|
+
stream.write('noop');
|
|
58
|
+
[err] = await once(stream, 'error')
|
|
59
|
+
t.equal(err.message, 'the worker has exited')
|
|
60
|
+
|
|
61
|
+
stream.write('noop');
|
|
62
|
+
[err] = await once(stream, 'error')
|
|
63
|
+
t.equal(err.message, 'the worker has exited')
|
|
104
64
|
})
|
|
105
65
|
|
|
106
66
|
test('emit error if worker stream emit error', async function (t) {
|
|
@@ -113,22 +73,16 @@ test('emit error if worker stream emit error', async function (t) {
|
|
|
113
73
|
stream.write('hello world\n')
|
|
114
74
|
})
|
|
115
75
|
|
|
116
|
-
|
|
76
|
+
let [err] = await once(stream, 'error')
|
|
117
77
|
t.equal(err.message, 'kaboom')
|
|
118
78
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
stream.write('noop')
|
|
128
|
-
t.fail('unreacheable')
|
|
129
|
-
} catch (err) {
|
|
130
|
-
t.equal(err.message, 'the worker has exited')
|
|
131
|
-
}
|
|
79
|
+
stream.write('noop');
|
|
80
|
+
[err] = await once(stream, 'error')
|
|
81
|
+
t.equal(err.message, 'the worker has exited')
|
|
82
|
+
|
|
83
|
+
stream.write('noop');
|
|
84
|
+
[err] = await once(stream, 'error')
|
|
85
|
+
t.equal(err.message, 'the worker has exited')
|
|
132
86
|
})
|
|
133
87
|
|
|
134
88
|
test('emit error if thread have uncaughtException', async function (t) {
|
|
@@ -141,22 +95,16 @@ test('emit error if thread have uncaughtException', async function (t) {
|
|
|
141
95
|
stream.write('hello world\n')
|
|
142
96
|
})
|
|
143
97
|
|
|
144
|
-
|
|
98
|
+
let [err] = await once(stream, 'error')
|
|
145
99
|
t.equal(err.message, 'kaboom')
|
|
146
100
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
stream.write('noop')
|
|
156
|
-
t.fail('unreacheable')
|
|
157
|
-
} catch (err) {
|
|
158
|
-
t.equal(err.message, 'the worker has exited')
|
|
159
|
-
}
|
|
101
|
+
stream.write('noop');
|
|
102
|
+
[err] = await once(stream, 'error')
|
|
103
|
+
t.equal(err.message, 'the worker has exited')
|
|
104
|
+
|
|
105
|
+
stream.write('noop');
|
|
106
|
+
[err] = await once(stream, 'error')
|
|
107
|
+
t.equal(err.message, 'the worker has exited')
|
|
160
108
|
})
|
|
161
109
|
|
|
162
110
|
test('close the work if out of scope on gc', { skip: !global.WeakRef }, async function (t) {
|