├── test ├── .gitkeep └── index.js ├── .eslintignore ├── .gitattributes ├── .npmrc ├── .eslintrc ├── .prettierignore ├── .editorconfig ├── .github └── workflows │ ├── release.yml │ └── dev.yml ├── CHANGELOG.md ├── index.js ├── package.json ├── LICENSE ├── .gitignore └── README.md /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "gulp" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | .nyc_output/ 3 | CHANGELOG.md 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | end_of_line = lf 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | 8 | jobs: 9 | release-please: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: GoogleCloudPlatform/release-please-action@v2 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | release-type: node 16 | package-name: release-please-action 17 | bump-minor-pre-major: true 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [4.0.0](https://www.github.com/gulpjs/lead/compare/v3.0.0...v4.0.0) (2022-09-22) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * Remove piping to a Writable and instead call resume on stream 9 | 10 | ### Features 11 | 12 | * Remove piping to a Writable and instead call resume on stream ([27324d6](https://www.github.com/gulpjs/lead/commit/27324d6ed5f3998e61054d511f715d6be7dba0e1)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * Ensure project works with different streams ([#8](https://www.github.com/gulpjs/lead/issues/8)) ([27324d6](https://www.github.com/gulpjs/lead/commit/27324d6ed5f3998e61054d511f715d6be7dba0e1)) 18 | * Use listenerCount API on streams ([27324d6](https://www.github.com/gulpjs/lead/commit/27324d6ed5f3998e61054d511f715d6be7dba0e1)) 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function hasListeners(stream) { 4 | return !!(stream.listenerCount('readable') || stream.listenerCount('data')); 5 | } 6 | 7 | function sink(stream) { 8 | var sinkAdded = false; 9 | 10 | function addSink() { 11 | if (sinkAdded) { 12 | return; 13 | } 14 | 15 | if (hasListeners(stream)) { 16 | return; 17 | } 18 | 19 | sinkAdded = true; 20 | stream.resume(); 21 | } 22 | 23 | function removeSink(evt) { 24 | if (evt !== 'readable' && evt !== 'data') { 25 | return; 26 | } 27 | 28 | if (hasListeners(stream)) { 29 | sinkAdded = false; 30 | } 31 | 32 | process.nextTick(addSink); 33 | } 34 | 35 | function markSink() { 36 | sinkAdded = true; 37 | } 38 | 39 | stream.on('newListener', removeSink); 40 | stream.on('removeListener', removeSink); 41 | stream.on('piping', markSink); 42 | 43 | // Sink the stream to start flowing 44 | // Do this on nextTick, it will flow at slowest speed of piped streams 45 | process.nextTick(addSink); 46 | 47 | return stream; 48 | } 49 | 50 | module.exports = sink; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lead", 3 | "version": "4.0.0", 4 | "description": "Sink your streams.", 5 | "author": "Gulp Team (https://gulpjs.com/)", 6 | "contributors": [ 7 | "Blaine Bublitz " 8 | ], 9 | "repository": "gulpjs/lead", 10 | "license": "MIT", 11 | "engines": { 12 | "node": ">=10.13.0" 13 | }, 14 | "main": "index.js", 15 | "files": [ 16 | "LICENSE", 17 | "index.js" 18 | ], 19 | "scripts": { 20 | "lint": "eslint .", 21 | "pretest": "npm run lint", 22 | "test": "nyc mocha --async-only" 23 | }, 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "eslint": "^7.32.0", 27 | "eslint-config-gulp": "^5.0.1", 28 | "eslint-plugin-node": "^11.1.0", 29 | "expect": "^27.4.2", 30 | "mocha": "^8.4.0", 31 | "nyc": "^15.1.0", 32 | "readable-stream": "^3.6.0", 33 | "streamx": "^2.12.0" 34 | }, 35 | "nyc": { 36 | "reporter": [ 37 | "lcov", 38 | "text-summary" 39 | ] 40 | }, 41 | "prettier": { 42 | "singleQuote": true 43 | }, 44 | "keywords": [ 45 | "streams", 46 | "sink", 47 | "through", 48 | "writeable" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017, 2020-2021 Blaine Bublitz and Eric Schoffstall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Garbage files 64 | .DS_Store 65 | 66 | # Test results 67 | test.xunit 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | # lead 8 | 9 | [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][ci-image]][ci-url] [![Coveralls Status][coveralls-image]][coveralls-url] 10 | 11 | Sink your streams. 12 | 13 | ## Usage 14 | 15 | ```js 16 | var { Readable, Transform } = require('streamx'); 17 | var sink = require('lead'); 18 | 19 | // Might be used as a Transform or Writeable 20 | var maybeThrough = new Transform({ 21 | transform(chunk, cb) { 22 | // processing 23 | cb(null, chunk); 24 | }, 25 | }); 26 | 27 | Readable.from(['hello', 'world']) 28 | // Sink it to behave like a Writeable 29 | .pipe(sink(maybeThrough)); 30 | ``` 31 | 32 | ## API 33 | 34 | ### `sink(stream)` 35 | 36 | Takes a `stream` to sink and returns the same stream. Sets up event listeners to infer if the stream is being used as a `Transform` or `Writeable` stream and sinks it on `nextTick` if necessary. If the stream is being used as a `Transform` stream but becomes unpiped, it will be sunk. Respects `pipe`, `on('data')` and `on('readable')` handlers. 37 | 38 | ## License 39 | 40 | MIT 41 | 42 | 43 | [downloads-image]: https://img.shields.io/npm/dm/lead.svg?style=flat-square 44 | [npm-url]: https://www.npmjs.com/package/lead 45 | [npm-image]: https://img.shields.io/npm/v/lead.svg?style=flat-square 46 | 47 | [ci-url]: https://github.com/gulpjs/lead/actions?query=workflow:dev 48 | [ci-image]: https://img.shields.io/github/actions/workflow/status/gulpjs/lead/dev.yml?branch=master&style=flat-square 49 | 50 | [coveralls-url]: https://coveralls.io/r/gulpjs/lead 51 | [coveralls-image]: https://img.shields.io/coveralls/gulpjs/lead/master.svg?style=flat-square 52 | 53 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: dev 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | prettier: 13 | name: Format code 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event_name == 'push' }} 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Prettier 22 | uses: gulpjs/prettier_action@v3.0 23 | with: 24 | commit_message: 'chore: Run prettier' 25 | prettier_options: '--write .' 26 | 27 | test: 28 | name: Tests for Node ${{ matrix.node }} on ${{ matrix.os }} 29 | runs-on: ${{ matrix.os }} 30 | 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | node: [10, 12, 14, 16] 35 | os: [ubuntu-latest, windows-latest, macos-latest] 36 | 37 | steps: 38 | - name: Clone repository 39 | uses: actions/checkout@v2 40 | 41 | - name: Set Node.js version 42 | uses: actions/setup-node@v2 43 | with: 44 | node-version: ${{ matrix.node }} 45 | 46 | - run: node --version 47 | - run: npm --version 48 | 49 | - name: Install npm dependencies 50 | run: npm install 51 | 52 | - name: Run lint 53 | run: npm run lint 54 | 55 | - name: Run tests 56 | run: npm test 57 | 58 | - name: Coveralls 59 | uses: coverallsapp/github-action@v1.1.2 60 | with: 61 | github-token: ${{ secrets.GITHUB_TOKEN }} 62 | flag-name: ${{matrix.os}}-node-${{ matrix.node }} 63 | parallel: true 64 | 65 | coveralls: 66 | needs: test 67 | name: Finish up 68 | 69 | runs-on: ubuntu-latest 70 | steps: 71 | - name: Coveralls Finished 72 | uses: coverallsapp/github-action@v1.1.2 73 | with: 74 | github-token: ${{ secrets.GITHUB_TOKEN }} 75 | parallel-finished: true 76 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('expect'); 4 | 5 | var sink = require('../'); 6 | 7 | function noop() {} 8 | 9 | suite('stream'); 10 | suite('streamx'); 11 | suite('readable-stream'); 12 | 13 | function suite(moduleName) { 14 | var stream = require(moduleName); 15 | 16 | function count(value) { 17 | var count = 0; 18 | return new stream.Transform({ 19 | objectMode: true, 20 | transform: function (file, enc, cb) { 21 | if (typeof enc === 'function') { 22 | cb = enc; 23 | } 24 | 25 | count++; 26 | cb(null, file); 27 | }, 28 | flush: function (cb) { 29 | expect(count).toEqual(value); 30 | cb(); 31 | }, 32 | }); 33 | } 34 | 35 | function slowCount(value) { 36 | var count = 0; 37 | return new stream.Writable({ 38 | objectMode: true, 39 | write: function (file, enc, cb) { 40 | if (typeof enc === 'function') { 41 | cb = enc; 42 | } 43 | 44 | count++; 45 | 46 | setTimeout(function () { 47 | cb(null, file); 48 | }, 250); 49 | }, 50 | flush: function (cb) { 51 | expect(count).toEqual(value); 52 | cb(); 53 | }, 54 | }); 55 | } 56 | describe('lead (' + moduleName + ')', function () { 57 | it('can wrap binary stream', function (done) { 58 | var write = sink(new stream.PassThrough()); 59 | 60 | function assert(err) { 61 | // Forced an object through a non-object stream 62 | expect(err).toBeFalsy(); 63 | done(); 64 | } 65 | 66 | stream.pipeline([stream.Readable.from(['1', '2', '3']), write], assert); 67 | }); 68 | 69 | it('can wrap object stream', function (done) { 70 | var write = sink(new stream.PassThrough({ objectMode: true })); 71 | 72 | function assert(err) { 73 | // Forced an object through a non-object stream 74 | expect(err).toBeFalsy(); 75 | done(); 76 | } 77 | 78 | stream.pipeline([stream.Readable.from([{}, {}, {}]), write], assert); 79 | }); 80 | 81 | if (moduleName !== 'streamx') { 82 | // TODO: Remove this test when we drop node <15 83 | it('does not convert between object and binary stream', function (done) { 84 | // Node core made a terrible decision in https://github.com/nodejs/node/pull/31831 85 | // and decided to start throwing on invalid encoding types, so we skip this test 86 | if ( 87 | process.version.startsWith('v14') || 88 | process.version.startsWith('v16') 89 | ) { 90 | this.skip(); 91 | return; 92 | } 93 | 94 | var write = sink(new stream.PassThrough()); 95 | 96 | function assert(err) { 97 | // Forced an object through a non-object stream 98 | expect(err).toBeTruthy(); 99 | done(); 100 | } 101 | 102 | stream.pipeline([stream.Readable.from([{}, {}, {}]), write], assert); 103 | }); 104 | } else { 105 | // Streamx does automatic conversion between object and binary streams 106 | it('converts between object and binary stream', function (done) { 107 | var write = sink(new stream.PassThrough()); 108 | 109 | function assert(err) { 110 | expect(err).toBeFalsy(); 111 | done(); 112 | } 113 | 114 | stream.pipeline( 115 | [ 116 | stream.Readable.from([{}, {}, {}]), 117 | // Must be in the Writable position to test this 118 | // So concat-stream cannot be used 119 | write, 120 | ], 121 | assert 122 | ); 123 | }); 124 | } 125 | 126 | it('does not get clogged by highWaterMark', function (done) { 127 | var expectedCount = 17; 128 | var highwatermarkObjs = []; 129 | for (var idx = 0; idx < expectedCount; idx++) { 130 | highwatermarkObjs.push({}); 131 | } 132 | 133 | var write = sink(new stream.PassThrough({ objectMode: true })); 134 | 135 | stream.pipeline( 136 | [stream.Readable.from(highwatermarkObjs), count(expectedCount), write], 137 | done 138 | ); 139 | }); 140 | 141 | it('allows backpressure when piped to another, slower stream', function (done) { 142 | this.timeout(20000); 143 | 144 | var expectedCount = 24; 145 | var highwatermarkObjs = []; 146 | for (var idx = 0; idx < expectedCount; idx++) { 147 | highwatermarkObjs.push({}); 148 | } 149 | 150 | var write = sink(new stream.PassThrough({ objectMode: true })); 151 | 152 | stream.pipeline( 153 | [ 154 | stream.Readable.from(highwatermarkObjs), 155 | count(expectedCount), 156 | write, 157 | slowCount(expectedCount), 158 | ], 159 | done 160 | ); 161 | }); 162 | 163 | it('respects readable listeners on wrapped stream', function (done) { 164 | var write = sink(new stream.PassThrough({ objectMode: true })); 165 | 166 | var readables = 0; 167 | write.on('readable', function () { 168 | while (write.read()) { 169 | readables++; 170 | } 171 | }); 172 | 173 | function assert(err) { 174 | expect(readables).toEqual(1); 175 | done(err); 176 | } 177 | 178 | stream.pipeline([stream.Readable.from([{}]), write], assert); 179 | }); 180 | 181 | it('respects data listeners on wrapped stream', function (done) { 182 | var write = sink(new stream.PassThrough({ objectMode: true })); 183 | 184 | var datas = 0; 185 | write.on('data', function () { 186 | datas++; 187 | }); 188 | 189 | function assert(err) { 190 | expect(datas).toEqual(1); 191 | done(err); 192 | } 193 | 194 | stream.pipeline([stream.Readable.from([{}]), write], assert); 195 | }); 196 | 197 | it('sinks the stream if all the readable event handlers are removed', function (done) { 198 | var expectedCount = 17; 199 | var highwatermarkObjs = []; 200 | for (var idx = 0; idx < expectedCount; idx++) { 201 | highwatermarkObjs.push({}); 202 | } 203 | 204 | var write = sink(new stream.PassThrough({ objectMode: true })); 205 | 206 | write.on('readable', noop); 207 | 208 | stream.pipeline( 209 | [stream.Readable.from(highwatermarkObjs), count(expectedCount), write], 210 | done 211 | ); 212 | 213 | process.nextTick(function () { 214 | write.removeListener('readable', noop); 215 | }); 216 | }); 217 | 218 | it('does not sink the stream if an event handler still exists when one is removed', function (done) { 219 | var expectedCount = 17; 220 | var highwatermarkObjs = []; 221 | for (var idx = 0; idx < expectedCount; idx++) { 222 | highwatermarkObjs.push({}); 223 | } 224 | 225 | var write = sink(new stream.PassThrough({ objectMode: true })); 226 | 227 | write.on('readable', noop); 228 | var readables = 0; 229 | write.on('readable', function () { 230 | while (write.read()) { 231 | readables++; 232 | } 233 | }); 234 | 235 | function assert(err) { 236 | expect(readables).toEqual(expectedCount); 237 | done(err); 238 | } 239 | 240 | stream.pipeline( 241 | [stream.Readable.from(highwatermarkObjs), count(expectedCount), write], 242 | assert 243 | ); 244 | 245 | process.nextTick(function () { 246 | write.removeListener('readable', noop); 247 | }); 248 | }); 249 | 250 | it('sinks the stream if all the data event handlers are removed', function (done) { 251 | var expectedCount = 17; 252 | var highwatermarkObjs = []; 253 | for (var idx = 0; idx < expectedCount; idx++) { 254 | highwatermarkObjs.push({}); 255 | } 256 | 257 | var write = sink(new stream.PassThrough({ objectMode: true })); 258 | 259 | write.on('data', noop); 260 | 261 | stream.pipeline( 262 | [stream.Readable.from(highwatermarkObjs), count(expectedCount), write], 263 | done 264 | ); 265 | 266 | process.nextTick(function () { 267 | write.removeListener('data', noop); 268 | }); 269 | }); 270 | }); 271 | } 272 | --------------------------------------------------------------------------------