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.
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: '/'
5
+ schedule:
6
+ interval: daily
7
+ open-pull-requests-limit: 10
8
+ - package-ecosystem: npm
9
+ directory: '/'
10
+ schedule:
11
+ interval: daily
12
+ open-pull-requests-limit: 10
@@ -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
+ [![npm version](https://img.shields.io/npm/v/thread-stream)](https://www.npmjs.com/package/thread-stream)
3
+ [![Build Status](https://img.shields.io/github/workflow/status/pinojs/thread-stream/CI)](https://github.com/pinojs/thread-stream/actions)
4
+ [![Known Vulnerabilities](https://snyk.io/test/github/pinojs/thread-stream/badge.svg)](https://snyk.io/test/github/pinojs/thread-stream)
5
+ [![Coverage Status](https://coveralls.io/repos/github/pinojs/thread-stream/badge.svg?branch=master)](https://coveralls.io/github/pinojs/thread-stream?branch=master)
6
+ [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](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
- const fn = (await import(workerData.filename)).default
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('finish', function () {
39
+ destination.on('error', function (err) {
22
40
  parentPort.postMessage({
23
- code: 'FINISH'
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
- // TODO transfer this to main
90
- console.error(err)
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.8.1",
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": "^5.1.3",
10
- "sonic-boom": "^1.3.2",
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
+ })
@@ -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
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const { Writable } = require('stream')
4
+
5
+ async function run (opts) {
6
+ const stream = new Writable({
7
+ write (chunk, enc, cb) {
8
+ cb(new Error('kaboom'))
9
+ }
10
+ })
11
+ return stream
12
+ }
13
+
14
+ module.exports = run
package/test/port.js CHANGED
@@ -5,6 +5,7 @@ const { Writable } = require('stream')
5
5
  function run (opts) {
6
6
  const { port } = opts
7
7
  return new Writable({
8
+ autoDestroy: true,
8
9
  write (chunk, enc, cb) {
9
10
  port.postMessage(chunk.toString())
10
11
  cb()
@@ -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, 'The worker thread exited')
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