thread-stream 0.8.1 → 0.11.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.
- package/.github/dependabot.yml +12 -0
- package/.github/workflows/ci.yml +60 -0
- package/.github/workflows/package-manager-ci.yml +59 -0
- package/README.md +33 -0
- package/index.js +104 -36
- package/lib/worker.js +38 -7
- package/package.json +5 -3
- package/test/close-on-gc.js +39 -0
- package/test/commonjs-fallback.test.js +29 -0
- package/test/end.test.js +77 -0
- package/test/error.js +14 -0
- package/test/port.js +1 -0
- package/test/thread-management.test.js +70 -1
- package/test/to-file-on-destroy.js +23 -0
- package/test/to-file-on-final.js +24 -0
- package/test/uncaughtException.js +21 -0
- package/.github/workflows/nodejs.yml +0 -24
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
paths-ignore:
|
|
5
|
+
- 'docs/**'
|
|
6
|
+
- '*.md'
|
|
7
|
+
pull_request:
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- 'docs/**'
|
|
10
|
+
- '*.md'
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
node-version: [12, 14, 16]
|
|
18
|
+
os: [macos-latest, ubuntu-latest, windows-latest]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
|
|
22
|
+
- uses: actions/checkout@v2.3.4
|
|
23
|
+
|
|
24
|
+
- name: Use Node.js
|
|
25
|
+
uses: actions/setup-node@v2.4.0
|
|
26
|
+
with:
|
|
27
|
+
node-version: ${{ matrix.node-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install
|
|
30
|
+
run: |
|
|
31
|
+
npm install --ignore-scripts
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: |
|
|
35
|
+
npm run test:ci
|
|
36
|
+
|
|
37
|
+
- name: Coveralls Parallel
|
|
38
|
+
uses: coverallsapp/github-action@1.1.3
|
|
39
|
+
with:
|
|
40
|
+
github-token: ${{ secrets.github_token }}
|
|
41
|
+
parallel: true
|
|
42
|
+
flag-name: run-${{ matrix.node-version }}-${{ matrix.os }}
|
|
43
|
+
|
|
44
|
+
coverage:
|
|
45
|
+
needs: test
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- name: Coveralls Finished
|
|
49
|
+
uses: coverallsapp/github-action@1.1.3
|
|
50
|
+
with:
|
|
51
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
52
|
+
parallel-finished: true
|
|
53
|
+
|
|
54
|
+
automerge:
|
|
55
|
+
needs: test
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: fastify/github-action-merge-dependabot@v2.4.0
|
|
59
|
+
with:
|
|
60
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: package-manager-ci
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
paths-ignore:
|
|
5
|
+
- 'docs/**'
|
|
6
|
+
- '*.md'
|
|
7
|
+
pull_request:
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- 'docs/**'
|
|
10
|
+
- '*.md'
|
|
11
|
+
jobs:
|
|
12
|
+
pnpm:
|
|
13
|
+
name: pnpm package manager on ${{ matrix.node-version }} ${{ matrix.os }}
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
os: [macOS-latest, windows-latest]
|
|
18
|
+
node-version: [12, 14, 16]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v2.3.4
|
|
21
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v2.4.0
|
|
23
|
+
with:
|
|
24
|
+
node-version: ${{ matrix.node-version }}
|
|
25
|
+
- name: Use pnpm
|
|
26
|
+
uses: pnpm/action-setup@v2.0.1
|
|
27
|
+
with:
|
|
28
|
+
version: ^6.0.0
|
|
29
|
+
- name: Install dependancies
|
|
30
|
+
run: pnpm install
|
|
31
|
+
- name: Tests
|
|
32
|
+
run: pnpm run test:ci
|
|
33
|
+
|
|
34
|
+
yarn-pnp:
|
|
35
|
+
name: yarn-pnp package manager on ${{ matrix.node-version }} ${{ matrix.os }}
|
|
36
|
+
runs-on: ${{ matrix.os }}
|
|
37
|
+
strategy:
|
|
38
|
+
matrix:
|
|
39
|
+
os: [macOS-latest]
|
|
40
|
+
node-version: [12, 14, 16]
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v2.3.4
|
|
43
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
44
|
+
uses: actions/setup-node@v2.4.0
|
|
45
|
+
with:
|
|
46
|
+
node-version: ${{ matrix.node-version }}
|
|
47
|
+
- name: Use yarn
|
|
48
|
+
run: |
|
|
49
|
+
npm install -g yarn
|
|
50
|
+
yarn set version berry
|
|
51
|
+
echo "nodeLinker: pnp" >> .yarnrc.yml
|
|
52
|
+
echo "pnpMode: loose" >> .yarnrc.yml
|
|
53
|
+
yarn add -D pino-elasticsearch@^6.0.0
|
|
54
|
+
yarn install
|
|
55
|
+
env:
|
|
56
|
+
# needed due the yarn.lock file in repository's .gitignore
|
|
57
|
+
YARN_ENABLE_IMMUTABLE_INSTALLS: false
|
|
58
|
+
- name: Tests
|
|
59
|
+
run: yarn run test:yarn
|
package/README.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
# thread-stream
|
|
2
|
+
[](https://www.npmjs.com/package/thread-stream)
|
|
3
|
+
[](https://github.com/pinojs/thread-stream/actions)
|
|
4
|
+
[](https://snyk.io/test/github/pinojs/thread-stream)
|
|
5
|
+
[](https://coveralls.io/github/pinojs/thread-stream?branch=master)
|
|
6
|
+
[](https://standardjs.com/)
|
|
2
7
|
|
|
3
8
|
A streaming way to send data to a Node.js Worker Thread.
|
|
4
9
|
|
|
@@ -53,6 +58,34 @@ async function run (opts) {
|
|
|
53
58
|
module.exports = run
|
|
54
59
|
```
|
|
55
60
|
|
|
61
|
+
Make sure that the stream emits `'close'` when the stream completes.
|
|
62
|
+
This can usually be achieved by passing the [`autoDestroy: true`](https://nodejs.org/api/stream.html#stream_new_stream_writable_options)
|
|
63
|
+
flag your stream classes.
|
|
64
|
+
|
|
65
|
+
The underlining worker is automatically closed if the stream is garbage collected.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
### External modules
|
|
69
|
+
|
|
70
|
+
You may use this module within compatible external modules, that exports the `worker.js` interface.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const ThreadStream = require('thread-stream')
|
|
74
|
+
|
|
75
|
+
const modulePath = require.resolve('pino-elasticsearch')
|
|
76
|
+
|
|
77
|
+
const stream = new ThreadStream({
|
|
78
|
+
filename: modulePath,
|
|
79
|
+
workerData: { node: 'http://localhost:9200' }
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
stream.write('log to elasticsearch!')
|
|
83
|
+
stream.flushSync()
|
|
84
|
+
stream.end()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This module works with `yarn` in PnP (plug'n play) mode too!
|
|
88
|
+
|
|
56
89
|
## License
|
|
57
90
|
|
|
58
91
|
MIT
|
package/index.js
CHANGED
|
@@ -10,6 +10,27 @@ const {
|
|
|
10
10
|
READ_INDEX
|
|
11
11
|
} = require('./lib/indexes')
|
|
12
12
|
|
|
13
|
+
class FakeWeakRef {
|
|
14
|
+
constructor (value) {
|
|
15
|
+
this._value = value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
deref () {
|
|
19
|
+
return this._value
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const FinalizationRegistry = global.FinalizationRegistry || class FakeFinalizationRegistry {
|
|
24
|
+
register () {}
|
|
25
|
+
unregister () {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const WeakRef = global.WeakRef || FakeWeakRef
|
|
29
|
+
|
|
30
|
+
const registry = new FinalizationRegistry((worker) => {
|
|
31
|
+
worker.terminate()
|
|
32
|
+
})
|
|
33
|
+
|
|
13
34
|
function createWorker (stream, opts) {
|
|
14
35
|
const { filename, workerData } = opts
|
|
15
36
|
|
|
@@ -27,6 +48,14 @@ function createWorker (stream, opts) {
|
|
|
27
48
|
}
|
|
28
49
|
})
|
|
29
50
|
|
|
51
|
+
// We keep a strong reference for now,
|
|
52
|
+
// we need to start writing first
|
|
53
|
+
worker.stream = new FakeWeakRef(stream)
|
|
54
|
+
|
|
55
|
+
worker.on('message', onWorkerMessage)
|
|
56
|
+
worker.on('exit', onWorkerExit)
|
|
57
|
+
registry.register(stream, worker)
|
|
58
|
+
|
|
30
59
|
return worker
|
|
31
60
|
}
|
|
32
61
|
|
|
@@ -90,6 +119,63 @@ function nextFlush (stream) {
|
|
|
90
119
|
}
|
|
91
120
|
}
|
|
92
121
|
|
|
122
|
+
function onWorkerMessage (msg) {
|
|
123
|
+
const stream = this.stream.deref()
|
|
124
|
+
if (stream === undefined) {
|
|
125
|
+
// Terminate the worker.
|
|
126
|
+
this.terminate()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switch (msg.code) {
|
|
131
|
+
case 'READY':
|
|
132
|
+
// Replace the FakeWeakRef with a
|
|
133
|
+
// proper one.
|
|
134
|
+
this.stream = new WeakRef(stream)
|
|
135
|
+
if (stream._sync) {
|
|
136
|
+
stream.ready = true
|
|
137
|
+
stream.flushSync()
|
|
138
|
+
stream.emit('ready')
|
|
139
|
+
} else {
|
|
140
|
+
stream.once('drain', function () {
|
|
141
|
+
stream.flush(() => {
|
|
142
|
+
stream.ready = true
|
|
143
|
+
stream.emit('ready')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
nextFlush(stream)
|
|
147
|
+
}
|
|
148
|
+
break
|
|
149
|
+
case 'ERROR':
|
|
150
|
+
stream.closed = true
|
|
151
|
+
// TODO only remove our own
|
|
152
|
+
stream.worker.removeAllListeners('exit')
|
|
153
|
+
stream.worker.terminate().then(null, () => {})
|
|
154
|
+
process.nextTick(() => {
|
|
155
|
+
stream.emit('error', msg.err)
|
|
156
|
+
})
|
|
157
|
+
break
|
|
158
|
+
default:
|
|
159
|
+
throw new Error('this should not happen: ' + msg.code)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function onWorkerExit (code) {
|
|
164
|
+
const stream = this.stream.deref()
|
|
165
|
+
if (stream === undefined) {
|
|
166
|
+
// Nothing to do, the worker already exit
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
registry.unregister(stream)
|
|
170
|
+
stream.closed = true
|
|
171
|
+
setImmediate(function () {
|
|
172
|
+
if (code !== 0) {
|
|
173
|
+
stream.emit('error', new Error('The worker thread exited'))
|
|
174
|
+
}
|
|
175
|
+
stream.emit('close')
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
93
179
|
class ThreadStream extends EventEmitter {
|
|
94
180
|
constructor (opts = {}) {
|
|
95
181
|
super()
|
|
@@ -110,42 +196,6 @@ class ThreadStream extends EventEmitter {
|
|
|
110
196
|
this.closed = false
|
|
111
197
|
|
|
112
198
|
this.buf = ''
|
|
113
|
-
|
|
114
|
-
this.worker.on('message', (msg) => {
|
|
115
|
-
switch (msg.code) {
|
|
116
|
-
case 'READY':
|
|
117
|
-
if (this._sync) {
|
|
118
|
-
this.ready = true
|
|
119
|
-
this.flushSync()
|
|
120
|
-
this.emit('ready')
|
|
121
|
-
} else {
|
|
122
|
-
this.once('drain', function () {
|
|
123
|
-
this.flush(() => {
|
|
124
|
-
this.ready = true
|
|
125
|
-
this.emit('ready')
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
nextFlush(this)
|
|
129
|
-
}
|
|
130
|
-
break
|
|
131
|
-
case 'FINISH':
|
|
132
|
-
this.emit('finish')
|
|
133
|
-
break
|
|
134
|
-
default:
|
|
135
|
-
throw new Error('this should not happen: ' + msg.code)
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
this.worker.on('exit', (code) => {
|
|
140
|
-
this.closed = true
|
|
141
|
-
setImmediate(() => {
|
|
142
|
-
if (code === 0) {
|
|
143
|
-
this.emit('close')
|
|
144
|
-
} else {
|
|
145
|
-
this.emit('error', new Error('The worker thread exited'))
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
})
|
|
149
199
|
}
|
|
150
200
|
|
|
151
201
|
_write (data, cb) {
|
|
@@ -210,10 +260,28 @@ class ThreadStream extends EventEmitter {
|
|
|
210
260
|
|
|
211
261
|
this.flushSync()
|
|
212
262
|
|
|
263
|
+
let read = Atomics.load(this._state, READ_INDEX)
|
|
264
|
+
|
|
213
265
|
// process._rawDebug('writing index')
|
|
214
266
|
Atomics.store(this._state, WRITE_INDEX, -1)
|
|
215
267
|
// process._rawDebug(`(end) readIndex (${Atomics.load(this._state, READ_INDEX)}) writeIndex (${Atomics.load(this._state, WRITE_INDEX)})`)
|
|
216
268
|
Atomics.notify(this._state, WRITE_INDEX)
|
|
269
|
+
|
|
270
|
+
// Wait for the process to complete
|
|
271
|
+
let spins = 0
|
|
272
|
+
while (read !== -1) {
|
|
273
|
+
// process._rawDebug(`read = ${read}`)
|
|
274
|
+
Atomics.wait(this._state, READ_INDEX, read, 1000)
|
|
275
|
+
read = Atomics.load(this._state, READ_INDEX)
|
|
276
|
+
|
|
277
|
+
if (++spins === 10) {
|
|
278
|
+
throw new Error('end() took too long (10s)')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
process.nextTick(() => {
|
|
283
|
+
this.emit('finish')
|
|
284
|
+
})
|
|
217
285
|
// process._rawDebug('end finished...')
|
|
218
286
|
}
|
|
219
287
|
|
package/lib/worker.js
CHANGED
|
@@ -15,16 +15,39 @@ const state = new Int32Array(stateBuf)
|
|
|
15
15
|
const data = Buffer.from(dataBuf)
|
|
16
16
|
|
|
17
17
|
async function start () {
|
|
18
|
-
|
|
18
|
+
let fn
|
|
19
|
+
try {
|
|
20
|
+
fn = (await import(workerData.filename)).default
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// A yarn user that tries to start a ThreadStream for an external module
|
|
23
|
+
// provides a filename pointing to a zip file.
|
|
24
|
+
// eg. require.resolve('pino-elasticsearch') // returns /foo/pino-elasticsearch-npm-6.1.0-0c03079478-6915435172.zip/bar.js
|
|
25
|
+
// The `import` will fail to try to load it.
|
|
26
|
+
// This catch block executes the `require` fallback to load the module correctly.
|
|
27
|
+
// In fact, yarn modifies the `require` function to manage the zipped path.
|
|
28
|
+
// More details at https://github.com/pinojs/pino/pull/1113
|
|
29
|
+
// The error codes may change based on the node.js version (ENOTDIR > 12, ERR_MODULE_NOT_FOUND <= 12 )
|
|
30
|
+
if ((error.code === 'ENOTDIR' || error.code === 'ERR_MODULE_NOT_FOUND') &&
|
|
31
|
+
workerData.filename.startsWith('file://')) {
|
|
32
|
+
fn = require(workerData.filename.replace('file://', ''))
|
|
33
|
+
} else {
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
destination = await fn(workerData.workerData)
|
|
20
38
|
|
|
21
|
-
destination.on('
|
|
39
|
+
destination.on('error', function (err) {
|
|
22
40
|
parentPort.postMessage({
|
|
23
|
-
code: '
|
|
41
|
+
code: 'ERROR',
|
|
42
|
+
err
|
|
24
43
|
})
|
|
25
44
|
})
|
|
26
45
|
|
|
27
46
|
destination.on('close', function () {
|
|
47
|
+
// process._rawDebug('worker close emitted')
|
|
48
|
+
const end = Atomics.load(state, WRITE_INDEX)
|
|
49
|
+
Atomics.store(state, READ_INDEX, end)
|
|
50
|
+
Atomics.notify(state, READ_INDEX)
|
|
28
51
|
setImmediate(() => {
|
|
29
52
|
process.exit(0)
|
|
30
53
|
})
|
|
@@ -60,8 +83,6 @@ function run () {
|
|
|
60
83
|
// process._rawDebug(`post state ${current} ${end}`)
|
|
61
84
|
|
|
62
85
|
if (end === -1) {
|
|
63
|
-
Atomics.store(state, READ_INDEX, end)
|
|
64
|
-
Atomics.notify(state, READ_INDEX)
|
|
65
86
|
// process._rawDebug('end')
|
|
66
87
|
destination.end()
|
|
67
88
|
return
|
|
@@ -86,7 +107,17 @@ function run () {
|
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
process.on('unhandledRejection', function (err) {
|
|
89
|
-
|
|
90
|
-
|
|
110
|
+
parentPort.postMessage({
|
|
111
|
+
code: 'ERROR',
|
|
112
|
+
err
|
|
113
|
+
})
|
|
114
|
+
process.exit(1)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
process.on('uncaughtException', function (err) {
|
|
118
|
+
parentPort.postMessage({
|
|
119
|
+
code: 'ERROR',
|
|
120
|
+
err
|
|
121
|
+
})
|
|
91
122
|
process.exit(1)
|
|
92
123
|
})
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thread-stream",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "A streaming way to send data to a Node.js Worker Thread",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"desm": "^1.1.0",
|
|
8
8
|
"fastbench": "^1.0.1",
|
|
9
|
-
"husky": "^
|
|
10
|
-
"sonic-boom": "^
|
|
9
|
+
"husky": "^7.0.0",
|
|
10
|
+
"sonic-boom": "^2.0.1",
|
|
11
11
|
"standard": "^16.0.3",
|
|
12
12
|
"tap": "^15.0.0"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "standard && tap --no-check-coverage test/*.test.*js",
|
|
16
|
+
"test:ci": "standard && tap \"test/**/*.test.*js\" --no-check-coverage --coverage-report=lcovonly",
|
|
17
|
+
"test:yarn": "tap \"test/**/*.test.js\" --no-check-coverage",
|
|
16
18
|
"prepare": "husky install"
|
|
17
19
|
},
|
|
18
20
|
"repository": {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { join } = require('path')
|
|
4
|
+
const ThreadStream = require('..')
|
|
5
|
+
const assert = require('assert')
|
|
6
|
+
|
|
7
|
+
let worker = null
|
|
8
|
+
|
|
9
|
+
function setup () {
|
|
10
|
+
const stream = new ThreadStream({
|
|
11
|
+
filename: join(__dirname, 'to-file.js'),
|
|
12
|
+
workerData: { dest: process.argv[2] },
|
|
13
|
+
sync: true
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
worker = stream.worker
|
|
17
|
+
|
|
18
|
+
stream.on('ready', function () {
|
|
19
|
+
stream.write('hello')
|
|
20
|
+
stream.write(' ')
|
|
21
|
+
stream.write('world\n')
|
|
22
|
+
stream.flushSync()
|
|
23
|
+
stream.unref()
|
|
24
|
+
|
|
25
|
+
// the stream object goes out of scope here
|
|
26
|
+
setImmediate(gc) // eslint-disable-line
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setup()
|
|
31
|
+
|
|
32
|
+
let exitEmitted = false
|
|
33
|
+
worker.on('exit', function () {
|
|
34
|
+
exitEmitted = true
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
process.on('exit', function () {
|
|
38
|
+
assert.strictEqual(exitEmitted, true)
|
|
39
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const ThreadStream = require('..')
|
|
5
|
+
|
|
6
|
+
const isYarnPnp = process.versions.pnp !== undefined
|
|
7
|
+
|
|
8
|
+
test('yarn module resolution', { skip: !isYarnPnp }, t => {
|
|
9
|
+
t.plan(5)
|
|
10
|
+
|
|
11
|
+
const modulePath = require.resolve('pino-elasticsearch')
|
|
12
|
+
t.match(modulePath, /.*\.zip.*/)
|
|
13
|
+
|
|
14
|
+
const stream = new ThreadStream({
|
|
15
|
+
filename: modulePath,
|
|
16
|
+
workerData: { node: null },
|
|
17
|
+
sync: true
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
stream.on('error', (err) => {
|
|
21
|
+
t.pass('error emitted')
|
|
22
|
+
t.equal(err.message, 'Missing node(s) option', 'module custom error')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
t.ok(stream.write('hello world\n'))
|
|
26
|
+
t.ok(stream.writable)
|
|
27
|
+
|
|
28
|
+
stream.end()
|
|
29
|
+
})
|
package/test/end.test.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const { join } = require('path')
|
|
5
|
+
const { readFile } = require('fs')
|
|
6
|
+
const { file } = require('./helper')
|
|
7
|
+
const ThreadStream = require('..')
|
|
8
|
+
|
|
9
|
+
test('destroy support', function (t) {
|
|
10
|
+
t.plan(9)
|
|
11
|
+
|
|
12
|
+
const dest = file()
|
|
13
|
+
const stream = new ThreadStream({
|
|
14
|
+
filename: join(__dirname, 'to-file-on-destroy.js'),
|
|
15
|
+
workerData: { dest },
|
|
16
|
+
sync: true
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
stream.on('drain', () => {
|
|
20
|
+
t.pass('drain')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
stream.on('ready', () => {
|
|
24
|
+
t.pass('ready emitted')
|
|
25
|
+
|
|
26
|
+
t.ok(stream.write('hello world\n'))
|
|
27
|
+
t.ok(stream.write('something else\n'))
|
|
28
|
+
t.ok(stream.writable)
|
|
29
|
+
|
|
30
|
+
stream.end()
|
|
31
|
+
|
|
32
|
+
readFile(dest, 'utf8', (err, data) => {
|
|
33
|
+
t.error(err)
|
|
34
|
+
t.equal(data, 'hello world\nsomething else\n')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
stream.on('close', () => {
|
|
39
|
+
t.notOk(stream.writable)
|
|
40
|
+
t.pass('close emitted')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('synchronous _final support', function (t) {
|
|
45
|
+
t.plan(9)
|
|
46
|
+
|
|
47
|
+
const dest = file()
|
|
48
|
+
const stream = new ThreadStream({
|
|
49
|
+
filename: join(__dirname, 'to-file-on-final.js'),
|
|
50
|
+
workerData: { dest },
|
|
51
|
+
sync: true
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
stream.on('drain', () => {
|
|
55
|
+
t.pass('drain')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
stream.on('ready', () => {
|
|
59
|
+
t.pass('ready emitted')
|
|
60
|
+
|
|
61
|
+
t.ok(stream.write('hello world\n'))
|
|
62
|
+
t.ok(stream.write('something else\n'))
|
|
63
|
+
t.ok(stream.writable)
|
|
64
|
+
|
|
65
|
+
stream.end()
|
|
66
|
+
|
|
67
|
+
readFile(dest, 'utf8', (err, data) => {
|
|
68
|
+
t.error(err)
|
|
69
|
+
t.equal(data, 'hello world\nsomething else\n')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
stream.on('close', () => {
|
|
74
|
+
t.notOk(stream.writable)
|
|
75
|
+
t.pass('close emitted')
|
|
76
|
+
})
|
|
77
|
+
})
|
package/test/error.js
ADDED
package/test/port.js
CHANGED
|
@@ -86,7 +86,7 @@ test('emit error if thread have unhandledRejection', async function (t) {
|
|
|
86
86
|
})
|
|
87
87
|
|
|
88
88
|
const [err] = await once(stream, 'error')
|
|
89
|
-
t.equal(err.message, '
|
|
89
|
+
t.equal(err.message, 'kaboom')
|
|
90
90
|
|
|
91
91
|
try {
|
|
92
92
|
stream.write('noop')
|
|
@@ -102,3 +102,72 @@ test('emit error if thread have unhandledRejection', async function (t) {
|
|
|
102
102
|
t.equal(err.message, 'the worker has exited')
|
|
103
103
|
}
|
|
104
104
|
})
|
|
105
|
+
|
|
106
|
+
test('emit error if worker stream emit error', async function (t) {
|
|
107
|
+
const stream = new ThreadStream({
|
|
108
|
+
filename: join(__dirname, 'error.js'),
|
|
109
|
+
sync: true
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
stream.on('ready', function () {
|
|
113
|
+
stream.write('hello world\n')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const [err] = await once(stream, 'error')
|
|
117
|
+
t.equal(err.message, 'kaboom')
|
|
118
|
+
|
|
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
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('emit error if thread have uncaughtException', async function (t) {
|
|
135
|
+
const stream = new ThreadStream({
|
|
136
|
+
filename: join(__dirname, 'uncaughtException.js'),
|
|
137
|
+
sync: true
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
stream.on('ready', function () {
|
|
141
|
+
stream.write('hello world\n')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const [err] = await once(stream, 'error')
|
|
145
|
+
t.equal(err.message, 'kaboom')
|
|
146
|
+
|
|
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
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('close the work if out of scope on gc', { skip: !global.WeakRef }, async function (t) {
|
|
163
|
+
const dest = file()
|
|
164
|
+
const child = fork(join(__dirname, 'close-on-gc.js'), [dest], {
|
|
165
|
+
execArgv: ['--expose-gc']
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const [code] = await once(child, 'exit')
|
|
169
|
+
t.equal(code, 0)
|
|
170
|
+
|
|
171
|
+
const data = await readFile(dest, 'utf8')
|
|
172
|
+
t.equal(data, 'hello world\n')
|
|
173
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const { Writable } = require('stream')
|
|
5
|
+
|
|
6
|
+
function run (opts) {
|
|
7
|
+
let data = ''
|
|
8
|
+
return new Writable({
|
|
9
|
+
autoDestroy: true,
|
|
10
|
+
write (chunk, enc, cb) {
|
|
11
|
+
data += chunk.toString()
|
|
12
|
+
cb()
|
|
13
|
+
},
|
|
14
|
+
destroy (err, cb) {
|
|
15
|
+
// process._rawDebug('destroy called')
|
|
16
|
+
fs.writeFile(opts.dest, data, function (err2) {
|
|
17
|
+
cb(err2 || err)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = run
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const { Writable } = require('stream')
|
|
5
|
+
|
|
6
|
+
function run (opts) {
|
|
7
|
+
let data = ''
|
|
8
|
+
return new Writable({
|
|
9
|
+
autoDestroy: true,
|
|
10
|
+
write (chunk, enc, cb) {
|
|
11
|
+
data += chunk.toString()
|
|
12
|
+
cb()
|
|
13
|
+
},
|
|
14
|
+
final (cb) {
|
|
15
|
+
setTimeout(function () {
|
|
16
|
+
fs.writeFile(opts.dest, data, function (err) {
|
|
17
|
+
cb(err)
|
|
18
|
+
})
|
|
19
|
+
}, 100)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = run
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Writable } = require('stream')
|
|
4
|
+
|
|
5
|
+
// Nop console.error to avoid printing things out
|
|
6
|
+
console.error = () => {}
|
|
7
|
+
|
|
8
|
+
setImmediate(function () {
|
|
9
|
+
throw new Error('kaboom')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
async function run (opts) {
|
|
13
|
+
const stream = new Writable({
|
|
14
|
+
write (chunk, enc, cb) {
|
|
15
|
+
cb()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
return stream
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = run
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
name: Node.js CI
|
|
2
|
-
|
|
3
|
-
on: [ push, pull_request ]
|
|
4
|
-
|
|
5
|
-
jobs:
|
|
6
|
-
test:
|
|
7
|
-
runs-on: ${{ matrix.os }}
|
|
8
|
-
strategy:
|
|
9
|
-
matrix:
|
|
10
|
-
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
11
|
-
node-version: [12.x, 14.x, 15.x]
|
|
12
|
-
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v2
|
|
15
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
16
|
-
uses: actions/setup-node@v1
|
|
17
|
-
with:
|
|
18
|
-
node-version: ${{ matrix.node-version }}
|
|
19
|
-
- run: node --version
|
|
20
|
-
- run: npm --version
|
|
21
|
-
- run: npm install
|
|
22
|
-
- run: npm test
|
|
23
|
-
env:
|
|
24
|
-
CI: true
|