├── .eslintrc ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── nodejs.yml │ └── release.yml ├── LICENSE ├── src └── index.ts ├── package.json ├── CHANGELOG.md ├── README.md └── test └── index.test.ts /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.seed 2 | *.log 3 | *.csv 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | 9 | pids 10 | logs 11 | results 12 | 13 | node_modules 14 | npm-debug.log 15 | coverage/ 16 | .tshy/ 17 | dist/ 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext", 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | Job: 12 | name: Node.js 13 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 14 | with: 15 | os: 'ubuntu-latest' 16 | version: '16, 18, 20' 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [ master ] 5 | 6 | jobs: 7 | release: 8 | name: Node.js 9 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 10 | secrets: 11 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 12 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 13 | with: 14 | checkTest: false 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2016-present node-modules and other contributors 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'node:stream'; 2 | 3 | const READABLE_STATE_KEY = '_readableState'; 4 | 5 | export function sendToWormhole(stream: Readable, throwError = false) { 6 | return new Promise((resolve, reject) => { 7 | if (typeof stream.resume !== 'function') { 8 | return resolve(); 9 | } 10 | 11 | // unpipe it 12 | stream.unpipe && stream.unpipe(); 13 | // enable resume first 14 | stream.resume(); 15 | 16 | if (stream.listenerCount && stream.listenerCount('readable') > 0) { 17 | // https://npm.taobao.org/mirrors/node/latest/docs/api/stream.html#stream_readable_resume 18 | // node 10.0.0: The resume() has no effect if there is a 'readable' event listening. 19 | stream.removeAllListeners('readable'); 20 | // call resume again in nextTick 21 | process.nextTick(() => stream.resume()); 22 | } 23 | 24 | if (!stream.readable || stream.destroyed) { 25 | return resolve(); 26 | } 27 | if (stream.closed || stream.readableEnded) { 28 | return resolve(); 29 | } 30 | 31 | const readableState = Reflect.get(stream, READABLE_STATE_KEY); 32 | if (readableState?.ended) { 33 | return resolve(); 34 | } 35 | 36 | function cleanup() { 37 | stream.removeListener('end', onEnd); 38 | stream.removeListener('close', onEnd); 39 | stream.removeListener('error', onError); 40 | } 41 | 42 | function onEnd() { 43 | cleanup(); 44 | resolve(); 45 | } 46 | 47 | function onError(err: Error) { 48 | cleanup(); 49 | // don't throw error by default 50 | if (throwError) { 51 | reject(err); 52 | } else { 53 | resolve(); 54 | } 55 | } 56 | 57 | stream.on('end', onEnd); 58 | stream.on('close', onEnd); 59 | stream.on('error', onError); 60 | }); 61 | } 62 | 63 | export default sendToWormhole; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-wormhole", 3 | "version": "2.0.1", 4 | "description": "Pipe ReadStream to a wormhole", 5 | "files": [ 6 | "dist", 7 | "src" 8 | ], 9 | "type": "module", 10 | "tshy": { 11 | "exports": { 12 | "./package.json": "./package.json", 13 | ".": "./src/index.ts" 14 | } 15 | }, 16 | "exports": { 17 | "./package.json": "./package.json", 18 | ".": { 19 | "import": { 20 | "types": "./dist/esm/index.d.ts", 21 | "default": "./dist/esm/index.js" 22 | }, 23 | "require": { 24 | "types": "./dist/commonjs/index.d.ts", 25 | "default": "./dist/commonjs/index.js" 26 | } 27 | } 28 | }, 29 | "scripts": { 30 | "test": "egg-bin test", 31 | "lint": "eslint src test", 32 | "ci": "npm run lint && egg-bin cov", 33 | "contributor": "git-contributor", 34 | "prepublishOnly": "tshy && tshy-after" 35 | }, 36 | "dependencies": {}, 37 | "devDependencies": { 38 | "@eggjs/tsconfig": "^1.3.3", 39 | "@types/mocha": "^10.0.1", 40 | "@types/node": "^20.6.1", 41 | "egg-bin": "^6.5.2", 42 | "eslint": "^8.49.0", 43 | "eslint-config-egg": "^12.3.0", 44 | "git-contributor": "^2.1.5", 45 | "tshy": "^1.0.0", 46 | "tshy-after": "^1.0.0", 47 | "typescript": "^5.2.2" 48 | }, 49 | "homepage": "https://github.com/node-modules/stream-wormhole", 50 | "repository": { 51 | "type": "git", 52 | "url": "git://github.com/node-modules/stream-wormhole.git" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/node-modules/stream-wormhole/issues" 56 | }, 57 | "keywords": [ 58 | "stream-wormhole", 59 | "wormhole", 60 | "stream" 61 | ], 62 | "engines": { 63 | "node": ">=16.0.0" 64 | }, 65 | "ci": { 66 | "version": "16, 18, 20" 67 | }, 68 | "author": "fengmk2", 69 | "license": "MIT", 70 | "types": "./dist/commonjs/index.d.ts" 71 | } 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.1](https://github.com/node-modules/stream-wormhole/compare/v2.0.0...v2.0.1) (2023-09-17) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * use types instead of typings ([30b24a5](https://github.com/node-modules/stream-wormhole/commit/30b24a5b59132ef8e769ce492a68fdee26cb4902)) 9 | 10 | ## [2.0.0](https://github.com/node-modules/stream-wormhole/compare/v1.1.1...v2.0.0) (2023-09-16) 11 | 12 | 13 | ### ⚠ BREAKING CHANGES 14 | 15 | * Drop Node.js < 16 support 16 | 17 | ### Features 18 | 19 | * refactor with typescript ([#8](https://github.com/node-modules/stream-wormhole/issues/8)) ([bc61b2b](https://github.com/node-modules/stream-wormhole/commit/bc61b2b6c9354243e015642837166b753199bbe0)) 20 | 21 | 1.1.1 / 2018-08-23 22 | ================== 23 | 24 | **fixes** 25 | * [[`c92e738`](http://github.com/node-modules/stream-wormhole/commit/c92e7384caf247529e9552b40ceaeac47c8bf92e)] - fix: should handle readable listeners (#7) (fengmk2 <>) 26 | 27 | 1.1.0 / 2018-08-15 28 | ================== 29 | 30 | **features** 31 | * [[`53ec1b2`](http://github.com/node-modules/stream-wormhole/commit/53ec1b21d0847c5c2d32391f60302cc9e96461f4)] - feat: support piped read stream (fengmk2 <>) 32 | 33 | **fixes** 34 | * [[`b2b1aaf`](http://github.com/node-modules/stream-wormhole/commit/b2b1aaf4dcd7741c13b76449ed9ef604a34e1f35)] - fix: should removeListener error handlers (fengmk2 <>) 35 | 36 | 1.0.4 / 2018-07-17 37 | ================== 38 | 39 | **fixes** 40 | * [[`968c008`](http://github.com/node-modules/stream-wormhole/commit/968c0088d18bbaaee7feaced306fd50f891d1743)] - fix: should resume stream first (#5) (fengmk2 <>) 41 | 42 | 1.0.3 / 2016-07-25 43 | ================== 44 | 45 | * chore: remove black-hole dep (#1) 46 | 47 | 1.0.2 / 2016-07-15 48 | ================== 49 | 50 | * fix: black-hole-stream should be deps 51 | 52 | 1.0.1 / 2016-07-15 53 | ================== 54 | 55 | * fix: should not throw error by default 56 | 57 | 1.0.0 / 2016-07-15 58 | ================== 59 | 60 | * init version 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stream-wormhole 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI](https://github.com/node-modules/stream-wormhole/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/stream-wormhole/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![npm download][download-image]][download-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/stream-wormhole.svg?style=flat-square 9 | [npm-url]: https://npmjs.org/package/stream-wormhole 10 | [codecov-image]: https://codecov.io/github/node-modules/stream-wormhole/coverage.svg?branch=master 11 | [codecov-url]: https://codecov.io/github/node-modules/stream-wormhole?branch=master 12 | [download-image]: https://img.shields.io/npm/dm/stream-wormhole.svg?style=flat-square 13 | [download-url]: https://npmjs.org/package/stream-wormhole 14 | 15 | Pipe `ReadStream` / `Readable` to a wormhole. 16 | 17 | ## Usage 18 | 19 | ```ts 20 | import sendToWormhole from 'stream-wormhole'; 21 | import fs from 'node:fs'; 22 | 23 | const readStream = fs.createReadStream(__filename); 24 | 25 | // ignore all error by default 26 | sendToWormhole(readStream) 27 | .then(() => console.log('done')); 28 | 29 | // throw error 30 | sendToWormhole(readStream, true) 31 | .then(() => console.log('done')) 32 | .catch(err => console.error(err)); 33 | ``` 34 | 35 | ## License 36 | 37 | [MIT](LICENSE) 38 | 39 | 40 | 41 | ## Contributors 42 | 43 | |[
fengmk2](https://github.com/fengmk2)
|[
denghongcai](https://github.com/denghongcai)
|[
dead-horse](https://github.com/dead-horse)
| 44 | | :---: | :---: | :---: | 45 | 46 | 47 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sat Sep 16 2023 14:11:38 GMT+0800`. 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import path from 'node:path'; 3 | import fs from 'node:fs'; 4 | import { fileURLToPath } from 'node:url'; 5 | import { Writable, PassThrough } from 'node:stream'; 6 | import { pipeline } from 'node:stream/promises'; 7 | import wormhole from '../src/index.js'; 8 | import { sendToWormhole } from '../src/index.js'; 9 | 10 | describe('test/index.test.ts', () => { 11 | const bigtext = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures/big.txt'); 12 | 13 | it('should work with read stream', () => { 14 | const stream = fs.createReadStream(bigtext); 15 | return wormhole(stream); 16 | }); 17 | 18 | it('should work with readableEnded', async () => { 19 | const stream = fs.createReadStream(bigtext); 20 | await pipeline(stream, new PassThrough().resume()); 21 | await wormhole(stream); 22 | }); 23 | 24 | it('should call multi times work', async () => { 25 | const stream = fs.createReadStream(bigtext); 26 | await sendToWormhole(stream); 27 | await sendToWormhole(stream); 28 | return await sendToWormhole(stream); 29 | }); 30 | 31 | it('should work with read stream after pipe', done => { 32 | let writeSize = 0; 33 | class PauseStream extends Writable { 34 | _write(...args: any[]) { 35 | console.log('PauseStream1 write buffer size: %d', args[0].length); 36 | writeSize += args[0].length; 37 | // do nothing 38 | } 39 | } 40 | 41 | const stream = fs.createReadStream(bigtext); 42 | stream.pipe(new PauseStream()); 43 | // mock delay 44 | setTimeout(() => { 45 | assert(writeSize > 0); 46 | sendToWormhole(stream).then(done); 47 | }, 100); 48 | }); 49 | 50 | it('should work with read stream after listening readable', () => { 51 | const stream = fs.createReadStream(bigtext); 52 | let data: any; 53 | stream.on('readable', () => { 54 | if (!data) { 55 | data = stream.read(); 56 | console.log('read data %d', data && data.length); 57 | } 58 | }); 59 | return sendToWormhole(stream).then(() => { 60 | assert(!data); 61 | }); 62 | }); 63 | 64 | it('should work with read stream after readable emitted', done => { 65 | const stream = fs.createReadStream(bigtext); 66 | let data: any; 67 | stream.on('readable', () => { 68 | if (!data) { 69 | data = stream.read(); 70 | console.log('read data %d', data && data.length); 71 | } 72 | }); 73 | setTimeout(() => { 74 | sendToWormhole(stream).then(() => { 75 | assert(data); 76 | done(); 77 | }); 78 | }, 500); 79 | }); 80 | 81 | it('should call multi times work with read stream after pipe', done => { 82 | let writeSize = 0; 83 | class PauseStream extends Writable { 84 | _write(...args: any[]) { 85 | console.log('PauseStream2 write buffer size: %d', args[0].length); 86 | writeSize += args[0].length; 87 | // do nothing 88 | } 89 | } 90 | 91 | const stream = fs.createReadStream(bigtext); 92 | stream.pipe(new PauseStream()); 93 | // mock delay 94 | setTimeout(() => { 95 | assert(writeSize > 0); 96 | sendToWormhole(stream).then(() => { 97 | sendToWormhole(stream).then(() => { 98 | sendToWormhole(stream).then(done); 99 | }); 100 | }); 101 | }, 100); 102 | }); 103 | 104 | it('should not throw error by default when stream error', () => { 105 | const stream = fs.createReadStream(bigtext + '-not-exists'); 106 | return sendToWormhole(stream); 107 | }); 108 | 109 | it('should throw error when stream error', done => { 110 | const stream = fs.createReadStream(bigtext + '-not-exists'); 111 | sendToWormhole(stream, true).catch(err => { 112 | assert.equal(err.code, 'ENOENT'); 113 | done(); 114 | }); 115 | }); 116 | 117 | it('should pass ended', done => { 118 | const stream = fs.createReadStream(bigtext); 119 | stream.resume(); 120 | stream.on('end', () => { 121 | sendToWormhole(stream).then(done); 122 | }); 123 | }); 124 | 125 | it('should mock destroyed', () => { 126 | const stream = { 127 | destroyed: true, 128 | resume() { 129 | // ignore 130 | }, 131 | }; 132 | return sendToWormhole(stream as any); 133 | }); 134 | 135 | it('should mock fake read stream', () => { 136 | const stream = {}; 137 | return sendToWormhole(stream as any); 138 | }); 139 | 140 | it('should mock readable = false', () => { 141 | const stream = { 142 | readable: false, 143 | resume() { 144 | // ignore 145 | }, 146 | }; 147 | return sendToWormhole(stream as any); 148 | }); 149 | 150 | it('should work on Promise', async () => { 151 | const stream = fs.createReadStream(bigtext); 152 | await sendToWormhole(stream); 153 | assert.equal(stream.readable, false); 154 | assert(stream.destroyed); 155 | // again should work 156 | await sendToWormhole(stream); 157 | assert.equal(stream.readable, false); 158 | assert(stream.destroyed); 159 | await sendToWormhole(stream); 160 | assert.equal(stream.readable, false); 161 | assert(stream.destroyed); 162 | await sendToWormhole(stream); 163 | assert.equal(stream.readable, false); 164 | assert(stream.destroyed); 165 | }); 166 | }); 167 | --------------------------------------------------------------------------------