thread-stream 0.9.0 → 0.11.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.
- package/.github/workflows/ci.yml +5 -5
- package/.github/workflows/package-manager-ci.yml +59 -0
- package/.taprc +2 -0
- package/README.md +24 -0
- package/index.js +102 -33
- package/lib/worker.js +38 -3
- package/package.json +7 -5
- package/test/close-on-gc.js +39 -0
- package/test/commonjs-fallback.test.js +29 -0
- package/test/error.js +14 -0
- package/test/esm.test.mjs +2 -22
- package/test/helper.js +8 -1
- package/test/string-limit.test.js +40 -0
- package/test/thread-management.test.js +70 -1
- package/test/uncaughtException.js +21 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -19,10 +19,10 @@ jobs:
|
|
|
19
19
|
|
|
20
20
|
steps:
|
|
21
21
|
|
|
22
|
-
- uses: actions/checkout@v2.3.
|
|
22
|
+
- uses: actions/checkout@v2.3.5
|
|
23
23
|
|
|
24
24
|
- name: Use Node.js
|
|
25
|
-
uses: actions/setup-node@v2.1
|
|
25
|
+
uses: actions/setup-node@v2.4.1
|
|
26
26
|
with:
|
|
27
27
|
node-version: ${{ matrix.node-version }}
|
|
28
28
|
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
npm run test:ci
|
|
36
36
|
|
|
37
37
|
- name: Coveralls Parallel
|
|
38
|
-
uses: coverallsapp/github-action@
|
|
38
|
+
uses: coverallsapp/github-action@1.1.3
|
|
39
39
|
with:
|
|
40
40
|
github-token: ${{ secrets.github_token }}
|
|
41
41
|
parallel: true
|
|
@@ -46,7 +46,7 @@ jobs:
|
|
|
46
46
|
runs-on: ubuntu-latest
|
|
47
47
|
steps:
|
|
48
48
|
- name: Coveralls Finished
|
|
49
|
-
uses: coverallsapp/github-action@
|
|
49
|
+
uses: coverallsapp/github-action@1.1.3
|
|
50
50
|
with:
|
|
51
51
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
52
52
|
parallel-finished: true
|
|
@@ -55,6 +55,6 @@ jobs:
|
|
|
55
55
|
needs: test
|
|
56
56
|
runs-on: ubuntu-latest
|
|
57
57
|
steps:
|
|
58
|
-
- uses: fastify/github-action-merge-dependabot@v2.
|
|
58
|
+
- uses: fastify/github-action-merge-dependabot@v2.4.0
|
|
59
59
|
with:
|
|
60
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.5
|
|
21
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v2.4.1
|
|
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.5
|
|
43
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
44
|
+
uses: actions/setup-node@v2.4.1
|
|
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/.taprc
ADDED
package/README.md
CHANGED
|
@@ -62,6 +62,30 @@ Make sure that the stream emits `'close'` when the stream completes.
|
|
|
62
62
|
This can usually be achieved by passing the [`autoDestroy: true`](https://nodejs.org/api/stream.html#stream_new_stream_writable_options)
|
|
63
63
|
flag your stream classes.
|
|
64
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
|
+
|
|
65
89
|
## License
|
|
66
90
|
|
|
67
91
|
MIT
|
package/index.js
CHANGED
|
@@ -9,6 +9,34 @@ const {
|
|
|
9
9
|
WRITE_INDEX,
|
|
10
10
|
READ_INDEX
|
|
11
11
|
} = require('./lib/indexes')
|
|
12
|
+
const buffer = require('buffer')
|
|
13
|
+
|
|
14
|
+
// V8 limit for string size
|
|
15
|
+
const MAX_STRING = buffer.constants.MAX_STRING_LENGTH
|
|
16
|
+
|
|
17
|
+
class FakeWeakRef {
|
|
18
|
+
constructor (value) {
|
|
19
|
+
this._value = value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
deref () {
|
|
23
|
+
return this._value
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FinalizationRegistry = global.FinalizationRegistry || class FakeFinalizationRegistry {
|
|
28
|
+
register () {}
|
|
29
|
+
unregister () {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const WeakRef = global.WeakRef || FakeWeakRef
|
|
33
|
+
|
|
34
|
+
const registry = new FinalizationRegistry((worker) => {
|
|
35
|
+
if (worker.exited) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
worker.terminate()
|
|
39
|
+
})
|
|
12
40
|
|
|
13
41
|
function createWorker (stream, opts) {
|
|
14
42
|
const { filename, workerData } = opts
|
|
@@ -27,6 +55,14 @@ function createWorker (stream, opts) {
|
|
|
27
55
|
}
|
|
28
56
|
})
|
|
29
57
|
|
|
58
|
+
// We keep a strong reference for now,
|
|
59
|
+
// we need to start writing first
|
|
60
|
+
worker.stream = new FakeWeakRef(stream)
|
|
61
|
+
|
|
62
|
+
worker.on('message', onWorkerMessage)
|
|
63
|
+
worker.on('exit', onWorkerExit)
|
|
64
|
+
registry.register(stream, worker)
|
|
65
|
+
|
|
30
66
|
return worker
|
|
31
67
|
}
|
|
32
68
|
|
|
@@ -90,6 +126,66 @@ function nextFlush (stream) {
|
|
|
90
126
|
}
|
|
91
127
|
}
|
|
92
128
|
|
|
129
|
+
function onWorkerMessage (msg) {
|
|
130
|
+
const stream = this.stream.deref()
|
|
131
|
+
if (stream === undefined) {
|
|
132
|
+
this.exited = true
|
|
133
|
+
// Terminate the worker.
|
|
134
|
+
this.terminate()
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
switch (msg.code) {
|
|
139
|
+
case 'READY':
|
|
140
|
+
// Replace the FakeWeakRef with a
|
|
141
|
+
// proper one.
|
|
142
|
+
this.stream = new WeakRef(stream)
|
|
143
|
+
if (stream._sync) {
|
|
144
|
+
stream.ready = true
|
|
145
|
+
stream.flushSync()
|
|
146
|
+
stream.emit('ready')
|
|
147
|
+
} else {
|
|
148
|
+
stream.once('drain', function () {
|
|
149
|
+
stream.flush(() => {
|
|
150
|
+
stream.ready = true
|
|
151
|
+
stream.emit('ready')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
nextFlush(stream)
|
|
155
|
+
}
|
|
156
|
+
break
|
|
157
|
+
case 'ERROR':
|
|
158
|
+
stream.closed = true
|
|
159
|
+
stream.worker.exited = true
|
|
160
|
+
// TODO only remove our own
|
|
161
|
+
stream.worker.removeAllListeners('exit')
|
|
162
|
+
stream.worker.terminate().then(null, () => {})
|
|
163
|
+
process.nextTick(() => {
|
|
164
|
+
stream.emit('error', msg.err)
|
|
165
|
+
})
|
|
166
|
+
break
|
|
167
|
+
default:
|
|
168
|
+
throw new Error('this should not happen: ' + msg.code)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function onWorkerExit (code) {
|
|
173
|
+
const stream = this.stream.deref()
|
|
174
|
+
if (stream === undefined) {
|
|
175
|
+
// Nothing to do, the worker already exit
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
registry.unregister(stream)
|
|
179
|
+
stream.closed = true
|
|
180
|
+
stream.worker.exited = true
|
|
181
|
+
setImmediate(function () {
|
|
182
|
+
if (code !== 0) {
|
|
183
|
+
stream.emit('error', new Error('The worker thread exited'))
|
|
184
|
+
}
|
|
185
|
+
stream.emit('close')
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
93
189
|
class ThreadStream extends EventEmitter {
|
|
94
190
|
constructor (opts = {}) {
|
|
95
191
|
super()
|
|
@@ -110,39 +206,6 @@ class ThreadStream extends EventEmitter {
|
|
|
110
206
|
this.closed = false
|
|
111
207
|
|
|
112
208
|
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
|
-
default:
|
|
132
|
-
throw new Error('this should not happen: ' + msg.code)
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
this.worker.on('exit', (code) => {
|
|
137
|
-
this.closed = true
|
|
138
|
-
setImmediate(() => {
|
|
139
|
-
if (code === 0) {
|
|
140
|
-
this.emit('close')
|
|
141
|
-
} else {
|
|
142
|
-
this.emit('error', new Error('The worker thread exited'))
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
209
|
}
|
|
147
210
|
|
|
148
211
|
_write (data, cb) {
|
|
@@ -166,6 +229,12 @@ class ThreadStream extends EventEmitter {
|
|
|
166
229
|
throw new Error('the worker has exited')
|
|
167
230
|
}
|
|
168
231
|
|
|
232
|
+
if (this.flushing && this.buf.length + data.length >= MAX_STRING) {
|
|
233
|
+
// process._rawDebug('write: flushing')
|
|
234
|
+
this._writeSync()
|
|
235
|
+
this.flushing = true // we are still flushing
|
|
236
|
+
}
|
|
237
|
+
|
|
169
238
|
if (!this.ready || this.flushing) {
|
|
170
239
|
this.buf += data
|
|
171
240
|
return this._hasSpace()
|
package/lib/worker.js
CHANGED
|
@@ -15,9 +15,34 @@ 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
|
|
|
39
|
+
destination.on('error', function (err) {
|
|
40
|
+
parentPort.postMessage({
|
|
41
|
+
code: 'ERROR',
|
|
42
|
+
err
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
21
46
|
destination.on('close', function () {
|
|
22
47
|
// process._rawDebug('worker close emitted')
|
|
23
48
|
const end = Atomics.load(state, WRITE_INDEX)
|
|
@@ -82,7 +107,17 @@ function run () {
|
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
process.on('unhandledRejection', function (err) {
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
})
|
|
87
122
|
process.exit(1)
|
|
88
123
|
})
|
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thread-stream",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.2",
|
|
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
|
-
"tap": "^15.0.0"
|
|
12
|
+
"tap": "^15.0.0",
|
|
13
|
+
"why-is-node-running": "^2.2.0"
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
|
-
"test": "standard && tap
|
|
16
|
+
"test": "standard && tap test/*.test.*js",
|
|
16
17
|
"test:ci": "standard && tap \"test/**/*.test.*js\" --no-check-coverage --coverage-report=lcovonly",
|
|
18
|
+
"test:yarn": "tap \"test/**/*.test.js\" --no-check-coverage",
|
|
17
19
|
"prepare": "husky install"
|
|
18
20
|
},
|
|
19
21
|
"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/error.js
ADDED
package/test/esm.test.mjs
CHANGED
|
@@ -1,29 +1,9 @@
|
|
|
1
1
|
import { test } from 'tap'
|
|
2
|
-
import {
|
|
3
|
-
import { unlinkSync, readFile } from 'fs'
|
|
2
|
+
import { readFile } from 'fs'
|
|
4
3
|
import ThreadStream from '../index.js'
|
|
5
4
|
import { join } from 'desm'
|
|
6
|
-
import path from 'path'
|
|
7
5
|
import { pathToFileURL } from 'url'
|
|
8
|
-
|
|
9
|
-
const files = []
|
|
10
|
-
let count = 0
|
|
11
|
-
|
|
12
|
-
function file () {
|
|
13
|
-
const file = path.join(tmpdir(), `thread-stream-${process.pid}-${process.hrtime().toString()}-${count++}`)
|
|
14
|
-
files.push(file)
|
|
15
|
-
return file
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
process.on('beforeExit', () => {
|
|
19
|
-
for (const file of files) {
|
|
20
|
-
try {
|
|
21
|
-
unlinkSync(file)
|
|
22
|
-
} catch (e) {
|
|
23
|
-
console.log(e)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
})
|
|
6
|
+
import { file } from './helper.js'
|
|
27
7
|
|
|
28
8
|
function basic (text, filename) {
|
|
29
9
|
test(text, function (t) {
|
package/test/helper.js
CHANGED
|
@@ -3,24 +3,31 @@
|
|
|
3
3
|
const { join } = require('path')
|
|
4
4
|
const { tmpdir } = require('os')
|
|
5
5
|
const { unlinkSync } = require('fs')
|
|
6
|
+
const why = require('why-is-node-running')
|
|
7
|
+
const t = require('tap')
|
|
6
8
|
|
|
7
9
|
const files = []
|
|
8
10
|
let count = 0
|
|
9
11
|
|
|
10
12
|
function file () {
|
|
11
|
-
const file = join(tmpdir(), `thread-stream-${process.pid}-${
|
|
13
|
+
const file = join(tmpdir(), `thread-stream-${process.pid}-${count++}`)
|
|
12
14
|
files.push(file)
|
|
13
15
|
return file
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
process.on('beforeExit', () => {
|
|
19
|
+
t.comment('unlink files')
|
|
17
20
|
for (const file of files) {
|
|
18
21
|
try {
|
|
22
|
+
t.comment(`unliking ${file}`)
|
|
19
23
|
unlinkSync(file)
|
|
20
24
|
} catch (e) {
|
|
21
25
|
console.log(e)
|
|
22
26
|
}
|
|
23
27
|
}
|
|
28
|
+
t.comment('unlink completed')
|
|
24
29
|
})
|
|
25
30
|
|
|
26
31
|
module.exports.file = file
|
|
32
|
+
|
|
33
|
+
setInterval(why, 10000).unref()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('tap')
|
|
4
|
+
const { join } = require('path')
|
|
5
|
+
const { file } = require('./helper')
|
|
6
|
+
const { stat } = require('fs')
|
|
7
|
+
const ThreadStream = require('..')
|
|
8
|
+
|
|
9
|
+
t.setTimeout(30000)
|
|
10
|
+
|
|
11
|
+
const dest = file()
|
|
12
|
+
const stream = new ThreadStream({
|
|
13
|
+
filename: join(__dirname, 'to-file.js'),
|
|
14
|
+
workerData: { dest },
|
|
15
|
+
sync: false
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
let length = 0
|
|
19
|
+
|
|
20
|
+
stream.on('ready', () => {
|
|
21
|
+
t.pass('ready emitted')
|
|
22
|
+
|
|
23
|
+
const buf = Buffer.alloc(1024).fill('x').toString() // 1 KB
|
|
24
|
+
|
|
25
|
+
// This writes 1 GB of data
|
|
26
|
+
for (let i = 0; i < 1024 * 1024; i++) {
|
|
27
|
+
length += buf.length
|
|
28
|
+
stream.write(buf)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
stream.end()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
stream.on('close', () => {
|
|
35
|
+
stat(dest, (err, f) => {
|
|
36
|
+
t.error(err)
|
|
37
|
+
t.equal(f.size, length)
|
|
38
|
+
t.end()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -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,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
|