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.
@@ -29,7 +29,7 @@ jobs:
29
29
  persist-credentials: false
30
30
 
31
31
  - name: Dependency review
32
- uses: actions/dependency-review-action@v1
32
+ uses: actions/dependency-review-action@v2
33
33
 
34
34
  test:
35
35
  name: Test
@@ -14,7 +14,7 @@ jobs:
14
14
  runs-on: ${{ matrix.os }}
15
15
  strategy:
16
16
  matrix:
17
- os: [macOS-latest, windows-latest]
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: [macOS-latest]
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
- constructor(opts: {})
5
- write (data: string): boolean
6
- end (): void
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
- throw new Error('overwritten')
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
- throw new Error('this should not happen: ' + msg.code)
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('The worker thread exited') : null)
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
- throw new Error('the worker has exited')
221
+ error(this, new Error('the worker has exited'))
222
+ return false
215
223
  }
216
224
 
217
225
  if (this[kImpl].ending) {
218
- throw new Error('the worker is ending')
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.emit('error', err)
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
- throw new Error('end() failed')
417
+ destroy(stream, new Error('end() failed'))
418
+ return
403
419
  }
404
420
 
405
421
  if (++spins === 10) {
406
- throw new Error('end() took too long (10s)')
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 new Error('_flushSync failed')
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.1",
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.1.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, 'The worker thread exited')
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
  })
@@ -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
+ })
@@ -1,6 +1,12 @@
1
1
  'use strict'
2
2
 
3
3
  const t = require('tap')
4
+
5
+ if (process.env.CI) {
6
+ t.skip('skip on CI')
7
+ process.exit(0)
8
+ }
9
+
4
10
  const { join } = require('path')
5
11
  const { file } = require('./helper')
6
12
  const { createReadStream } = require('fs')
@@ -1,6 +1,12 @@
1
1
  'use strict'
2
2
 
3
3
  const t = require('tap')
4
+
5
+ if (process.env.CI) {
6
+ t.skip('skip on CI')
7
+ process.exit(0)
8
+ }
9
+
4
10
  const { join } = require('path')
5
11
  const { file } = require('./helper')
6
12
  const { stat } = require('fs')
@@ -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
- const [err] = await once(stream, 'error')
33
- t.equal(err.message, 'The worker thread exited')
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
- test('emit error if thread exits', async function (t) {
51
- const stream = new ThreadStream({
52
- filename: join(__dirname, 'exit.js'),
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.on('ready', () => {
57
- stream.write('hello world\n')
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
- const [err] = await once(stream, 'error')
54
+ let [err] = await once(stream, 'error')
89
55
  t.equal(err.message, 'kaboom')
90
56
 
91
- try {
92
- stream.write('noop')
93
- t.fail('unreacheable')
94
- } catch (err) {
95
- t.equal(err.message, 'the worker has exited')
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
- const [err] = await once(stream, 'error')
76
+ let [err] = await once(stream, 'error')
117
77
  t.equal(err.message, 'kaboom')
118
78
 
119
- try {
120
- stream.write('noop')
121
- t.fail('unreacheable')
122
- } catch (err) {
123
- t.equal(err.message, 'the worker has exited')
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
- const [err] = await once(stream, 'error')
98
+ let [err] = await once(stream, 'error')
145
99
  t.equal(err.message, 'kaboom')
146
100
 
147
- try {
148
- stream.write('noop')
149
- t.fail('unreacheable')
150
- } catch (err) {
151
- t.equal(err.message, 'the worker has exited')
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) {