├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yaml └── workflows │ ├── coverage.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cSpell.json ├── package-lock.json ├── package.json ├── samples └── pipeToCaps.js ├── src ├── end-to-end.test.ts ├── index.ts ├── rxToStream.test.ts ├── rxToStream.ts ├── streamToRx.test.ts └── streamToRx.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{yaml,yml}] 12 | indent_size = 2 13 | 14 | [{package*.json,lerna.json}] 15 | indent_size = 2 16 | 17 | [*.md] 18 | indent_size = 2 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | [Tt]emp/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type { import("eslint").Linter.Config } 3 | */ 4 | const config = { 5 | env: { 6 | es2020: true, 7 | node: true, 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:node/recommended', 12 | 'plugin:import/errors', 13 | 'plugin:import/warnings', 14 | 'plugin:promise/recommended', 15 | 'plugin:prettier/recommended', 16 | ], 17 | parserOptions: { 18 | ecmaVersion: 11, 19 | sourceType: 'module', 20 | }, 21 | overrides: [ 22 | { 23 | files: '**/*.ts', 24 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript'], 25 | parser: '@typescript-eslint/parser', 26 | plugins: ['@typescript-eslint'], 27 | rules: { 28 | 'no-unused-vars': 0, // off - caught by the compiler 29 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 30 | 'node/no-missing-import': [ 31 | 'error', 32 | { 33 | tryExtensions: ['.js', '.d.ts', '.ts'], 34 | }, 35 | ], 36 | 'node/no-unsupported-features/es-syntax': [ 37 | 'error', 38 | { 39 | ignores: ['modules'], 40 | }, 41 | ], 42 | }, 43 | }, 44 | ], 45 | }; 46 | 47 | module.exports = config; 48 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | # Workflow files stored in the 15 | # default location of `.github/workflows` 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | coverage: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | 18 | - run: npm ci 19 | 20 | - run: npm run coverage 21 | 22 | - name: Upload coverage Coveralls 23 | uses: coverallsapp/github-action@1.1.3 24 | with: 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | path-to-lcov: ./coverage/lcov.info 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: "lint" 2 | on: # rebuild any PRs and main branch changes 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: # make sure build/ci work properly 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Use Node.js 15 | uses: actions/setup-node@v3 16 | 17 | - run: | 18 | npm install 19 | npm run lint 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "build-test" 2 | on: # rebuild any PRs and main branch changes 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test-node-versions: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | node-version: 15 | - 14.x 16 | - 16.x 17 | - 18.x 18 | 19 | os: 20 | - ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | 30 | - run: | 31 | npm install 32 | npm test 33 | 34 | test-os: 35 | runs-on: ${{ matrix.os }} 36 | 37 | strategy: 38 | matrix: 39 | os: 40 | # - ubuntu-latest 41 | - windows-latest 42 | - macos-latest 43 | 44 | node-version: 45 | - 18.x 46 | 47 | steps: 48 | - uses: actions/checkout@v3 49 | 50 | - name: Use Node.js ${{ matrix.node-version }} 51 | uses: actions/setup-node@v3 52 | with: 53 | node-version: ${{ matrix.node-version }} 54 | 55 | - run: | 56 | npm install 57 | npm test 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Ignore Built files 40 | dist/** 41 | temp 42 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore test files 2 | **/*.test.* 3 | **/*.spec.* 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 160, 3 | "singleQuote": true, 4 | "overrides": [ 5 | { 6 | "files": "**/*.{yaml,yml}", 7 | "options": { 8 | "singleQuote": false 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "12" 5 | - "10" 6 | install: 7 | - npm ci 8 | script: 9 | - npm test 10 | - npm run lint-travis 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/dist/index.js", 12 | "preLaunchTask": "tsc: build - tsconfig.json", 13 | "outFiles": [ 14 | "${workspaceFolder}/dist/**/*.js" 15 | ] 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Run Mocha Current File", 21 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 22 | "args": [ 23 | "--timeout","999999", 24 | "--colors", 25 | "--require", "ts-node/register", 26 | "${file}" 27 | ], 28 | "internalConsoleOptions": "openOnSessionStart" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [5.0.0](https://github.com/Jason3S/rx-stream/compare/v4.0.2...v5.0.0) (2022-06-07) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * Drop support for Node 12 11 | 12 | Note: The code has not changed, but the cost of maintaining the tools is too high. 13 | 14 | ### Features 15 | 16 | * Drop support for Node 12 ([#349](https://github.com/Jason3S/rx-stream/issues/349)) ([db331d6](https://github.com/Jason3S/rx-stream/commit/db331d6ba238d971964693857242d6f1b52f1fe2)) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * Update `devDependencies` ([#258](https://github.com/Jason3S/rx-stream/issues/258)) ([df64568](https://github.com/Jason3S/rx-stream/commit/df64568f726efdf68a927c2dc1a2c4cfb3d476e3)) 22 | * Update dependencies and workflows ([#348](https://github.com/Jason3S/rx-stream/issues/348)) ([3d54289](https://github.com/Jason3S/rx-stream/commit/3d54289a27b2722a28dce15b1bfc21cd78682c4c)) 23 | * Update dev dependencies ([#304](https://github.com/Jason3S/rx-stream/issues/304)) ([6b6a404](https://github.com/Jason3S/rx-stream/commit/6b6a404504ad906d65e6947c5adc975312adffd5)) 24 | * upgrade rxjs from 7.5.4 to 7.5.5 ([#291](https://github.com/Jason3S/rx-stream/issues/291)) ([4e89239](https://github.com/Jason3S/rx-stream/commit/4e8923973a298036bbfee0a28642903d5939d900)) 25 | 26 | ### [4.0.2](https://github.com/Jason3S/rx-stream/compare/v4.0.1...v4.0.2) (2021-12-13) 27 | 28 | ### [4.0.1](https://github.com/Jason3S/rx-stream/compare/v3.3.0...v4.0.1) (2021-12-13) 29 | 30 | ## [4.0.0] (https://github.com/Jason3S/rx-stream/compare/v3.3.0...v4.0.0) (2021-12-08) 31 | 32 | * Move to RxJs 7 33 | 34 | ## [3.3.0](https://github.com/Jason3S/rx-stream/compare/v3.2.1...v3.3.0) (2021-07-18) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * Add option to subscribe immediately to an observable. ([#109](https://github.com/Jason3S/rx-stream/issues/109)) ([81fcec5](https://github.com/Jason3S/rx-stream/commit/81fcec5462849246290e00d77796642781bb3156)), closes [#106](https://github.com/Jason3S/rx-stream/issues/106) 40 | 41 | ## [3.2.0] 42 | * Support Object streams [How do we create an Object stream from Observable? · Issue #17](https://github.com/Jason3S/rx-stream/issues/17) 43 | 44 | ## [3.1.1] 45 | * Address [Back Pressure support · Issue #13](https://github.com/Jason3S/rx-stream/issues/13) 46 | 47 | ## [3.1.0] 48 | * Fix [stream never unsubscribes from the source observable when destroyed · Issue #12](https://github.com/Jason3S/rx-stream/issues/12) 49 | 50 | ## [3.0.1] 51 | * Added a unit test that reads / writes to a file. I was investigating when rx-stream broke with rxjs 6.3. In the end, it was due to an issue with rxjs. 52 | See: [rxjs #4071](https://github.com/ReactiveX/rxjs/issues/4071), [rxjs #4072](https://github.com/ReactiveX/rxjs/issues/4072), [rxjs #4073](https://github.com/ReactiveX/rxjs/issues/4073) 53 | 54 | ## [3.0.0] 55 | * Breaking change: [streamToRx - now returns Observable](https://github.com/Jason3S/rx-stream/pull/3) 56 | * [refactor(rxToStream): removes recursion, reduces overall size of impl… #4](https://github.com/Jason3S/rx-stream/pull/4) 57 | 58 | ## [2.0.0] 59 | * Move to RxJs 6 60 | 61 | ## [1.3.0] 62 | * Move to RxJs 5.5 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jason Dent 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rxjs-stream 2 | 3 | > **Note** 4 | > With the addition of [Async Iternables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols), 5 | > this library is no longer necessary. 6 | 7 | This is a simple library for converting to and from NodeJS stream and [RxJS](https://rxjs.dev/) 7. 8 | 9 | This was created to fill the gap left by [rx-node](https://www.npmjs.com/package/rx-node), 10 | which only works with rxjs 4. 11 | 12 | ## Installation 13 | 14 | ```sh 15 | npm install --save rxjs rxjs-stream 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### Writing to a stream. 21 | 22 | ```typescript 23 | import { rxToStream } from 'rxjs-stream'; 24 | 25 | let data = 'This is a bit of text to have some fun with'; 26 | let src = Rx.Observable.from(data.split(' ')); 27 | rxToStream(src).pipe(process.stdout); 28 | ``` 29 | 30 | ### Writing objects to a stream 31 | 32 | To write objects, you must pass in the `ReadableOptions` with `objectMode` to be true: `{ objectMode: true }` 33 | 34 | ```typescript 35 | import { rxToStream } from 'rxjs-stream'; 36 | 37 | let data = 'This is a bit of text to have some fun with'; 38 | let wordObj = data.split(' ').map((text) => ({ text })); 39 | let src = Rx.Observable.from(wordObj); 40 | let stream = rxToStream(src, { objectMode: true }); 41 | ``` 42 | 43 | ### Read from a stream 44 | 45 | ```typescript 46 | import { rxToStream, streamToStringRx } from 'rxjs-stream'; 47 | 48 | // Read stdin and make it upper case then send it to stdout 49 | let ob = streamToStringRx(process.stdin).map((text) => text.toUpperCase()); 50 | 51 | rxToStream(ob).pipe(process.stdout); 52 | ``` 53 | 54 | ## Performance 55 | 56 | It is recommended to buffer observable values before sending them to the stream. 57 | Node streams work better with fewer calls of a large amount of data than with many 58 | calls with a small amount of data. 59 | 60 | Example: 61 | 62 | ```typescript 63 | import * as loremIpsum from 'lorem-ipsum'; 64 | import { rxToStream } from 'rxjs-stream'; 65 | 66 | let book = loremIpsum({ count: 1000, format: 'plain', units: 'paragraphs' }); 67 | let words = Rx.Observable.from(book.split(/\b/)); 68 | let wordsBuffered = words.bufferCount(1000).map((words) => words.join('')); 69 | let stream = rxToStream(wordsBuffered); 70 | 71 | stream.pipe(process.stdout); 72 | ``` 73 | 74 | ## Compatibility 75 | 76 | This library is tested with Node 12 and above. 77 | 78 | | rx-stream | RxJS | Node | 79 | | --------- | ---- | ---- | 80 | | 4.x | 7.x | >=12 | 81 | | 3.x | 6.x | >=10 | 82 | -------------------------------------------------------------------------------- /cSpell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1", 3 | "language": "en", 4 | "words": [ 5 | "api's", 6 | "coverallsapp", 7 | "lcov", 8 | "Observables", 9 | "Pausable", 10 | "Streamable" 11 | ], 12 | "ignorePaths": [ 13 | "package*.json", 14 | "node_modules" 15 | ] 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-stream", 3 | "version": "5.0.0", 4 | "description": "nodejs streams for rxjs 7", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "scripts": { 8 | "lint": "npm run check-spelling && eslint . --fix", 9 | "lint-travis": "npm run check-spelling && eslint .", 10 | "check-spelling": "cspell \"src/**\" \"*.md\" \"samples/**\"", 11 | "build": "npm run compile", 12 | "clean": "rimraf ./dist", 13 | "clean-build": "npm run clean && npm run build", 14 | "compile": "tsc -p .", 15 | "watch": "tsc --watch -p .", 16 | "tsc": "tsc -p .", 17 | "coverage": "npm run generate-code-coverage", 18 | "generate-code-coverage": "NODE_ENV=test nyc npm run test-ts", 19 | "test-ts": "NODE_ENV=test mocha --require ts-node/register --recursive --bail \"src/**/*.test.ts\"", 20 | "test-watch": "npm run build && mocha --require ts-node/register --watch --recursive \"src/**/*.test.ts\"", 21 | "prepare": "npm run clean-build", 22 | "prepublishOnly": "npm test", 23 | "coverage-coveralls": "nyc report --reporter=text-lcov | coveralls", 24 | "release": "npx standard-version", 25 | "travis-coverage": "npm run generate-code-coverage && npm run coverage-coveralls", 26 | "test": "mocha --recursive \"dist/**/*.test.js\"", 27 | "update-packages": "npm i && npx npm-check-updates -t minor -u && rimraf node_modules package-lock.json && npm i" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/Jason3S/rx-stream.git" 32 | }, 33 | "keywords": [ 34 | "rxjs", 35 | "node", 36 | "stream" 37 | ], 38 | "author": "Jason Dent", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/Jason3S/rx-stream/issues" 42 | }, 43 | "homepage": "https://github.com/Jason3S/rx-stream#readme", 44 | "devDependencies": { 45 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 46 | "@types/chai": "^4.3.4", 47 | "@types/mocha": "^10.0.1", 48 | "@types/node": "^18.11.18", 49 | "@typescript-eslint/eslint-plugin": "^5.50.0", 50 | "@typescript-eslint/parser": "^5.50.0", 51 | "chai": "^4.3.7", 52 | "coveralls": "^3.1.1", 53 | "cspell": "^6.19.2", 54 | "eslint": "^8.33.0", 55 | "eslint-config-prettier": "^8.6.0", 56 | "eslint-plugin-import": "^2.27.5", 57 | "eslint-plugin-node": "^11.1.0", 58 | "eslint-plugin-prettier": "^4.2.1", 59 | "eslint-plugin-promise": "^6.1.1", 60 | "fs-extra": "^10.1.0", 61 | "lorem-ipsum": "^2.0.8", 62 | "mocha": "^10.2.0", 63 | "nyc": "^15.1.0", 64 | "prettier": "^2.8.3", 65 | "rimraf": "^3.0.2", 66 | "rxjs": "^7.8.0", 67 | "ts-node": "^10.9.1", 68 | "typescript": "^4.9.5" 69 | }, 70 | "dependencies": { 71 | "@types/fs-extra": "^9.0.13" 72 | }, 73 | "peerDependencies": { 74 | "rxjs": "^7.0.0" 75 | }, 76 | "engines": { 77 | "node": ">=14" 78 | }, 79 | "files": [ 80 | "dist", 81 | "!**/*.map", 82 | "!**/*.test.*", 83 | "!**/*.spec.*" 84 | ], 85 | "nyc": { 86 | "extends": "@istanbuljs/nyc-config-typescript", 87 | "all": true, 88 | "include": [ 89 | "src/**/*.ts" 90 | ], 91 | "exclude": [ 92 | "**/*.test.ts" 93 | ], 94 | "extension": [ 95 | ".ts" 96 | ], 97 | "reporter": [ 98 | "lcov", 99 | "json", 100 | "html" 101 | ] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /samples/pipeToCaps.js: -------------------------------------------------------------------------------- 1 | // See: [Cannot use stdin & stdout together · Issue #6](https://github.com/Jason3S/rx-stream/issues/6) 2 | 3 | const { map } = require('rxjs/operators'); 4 | // const { rxToStream, streamToStringRx } = require('rxjs-stream'); 5 | const { rxToStream, streamToStringRx } = require('../dist/index'); 6 | 7 | const ob = streamToStringRx(process.stdin).pipe(map((l) => l.toUpperCase())); 8 | 9 | rxToStream(ob).pipe(process.stdout); 10 | -------------------------------------------------------------------------------- /src/end-to-end.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { range } from 'rxjs'; 3 | import { map, tap, toArray } from 'rxjs/operators'; 4 | import { rxToStream, streamToRx } from './index'; 5 | import { loremIpsum } from 'lorem-ipsum'; 6 | 7 | interface AStreamObject { 8 | text: string; 9 | } 10 | 11 | describe('Validate end-to-end test', () => { 12 | it('rx-stream-rx', async () => { 13 | const record: AStreamObject[] = []; 14 | 15 | const max = 50; 16 | const src = range(1, max).pipe( 17 | map(() => loremIpsum({ count: 100, format: 'plain', units: 'words' })), 18 | map((text) => ({ text })), 19 | tap((s) => record.push(s)) 20 | ); 21 | const result = await streamToRx(rxToStream(src, { objectMode: true })) 22 | .pipe(toArray()) 23 | .toPromise(); 24 | expect(result).to.deep.equal(record); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rxToStream'; 2 | export * from './streamToRx'; 3 | -------------------------------------------------------------------------------- /src/rxToStream.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { from, range, Observable, Subscriber, timer, Subscription, Subject } from 'rxjs'; 3 | import { reduce, map, concatMap, take, finalize } from 'rxjs/operators'; 4 | import { rxToStream, streamToStringRx } from './index'; 5 | import { loremIpsum } from 'lorem-ipsum'; 6 | import * as path from 'path'; 7 | import { mkdirp } from 'fs-extra'; 8 | import * as fs from 'fs-extra'; 9 | import * as stream from 'stream'; 10 | 11 | import { Readable } from 'stream'; 12 | 13 | describe('Validate to Stream', () => { 14 | it('rxToStream', async () => { 15 | const data = 'This is a bit of text to have some fun with'; 16 | const src = from(data.split(' ')); 17 | 18 | const injectedSubscription = new Subscription(); 19 | 20 | const originalSubscribe = src.subscribe.bind(src); 21 | 22 | const mockedSubscribe = ((...params: Parameters) => { 23 | injectedSubscription.add(originalSubscribe(...params)); 24 | return injectedSubscription; 25 | }) as typeof src.subscribe; 26 | 27 | src.subscribe = mockedSubscribe; 28 | 29 | const stream = rxToStream(src); 30 | 31 | const result = await streamToStringRx(stream) 32 | .pipe( 33 | reduce((a, b) => a + ' ' + b), 34 | finalize(() => { 35 | // destroy source stream as this would be done by conventional stream api's 36 | // like https://nodejs.org/api/stream.html#stream_stream_pipeline_streams_callback 37 | stream.destroy(); 38 | }) 39 | ) 40 | .toPromise(); 41 | expect(result).to.equal(data); 42 | expect(injectedSubscription.closed).to.be.true; 43 | }); 44 | 45 | it('rxToStream with error', async () => { 46 | const src = new Observable((observer: Subscriber) => { 47 | setTimeout(() => observer.error(new Error('TEST_ERROR')), 1); 48 | }); 49 | 50 | const stream = rxToStream(src, undefined, (err: Error, readable: Readable) => { 51 | readable.emit('error', err); 52 | }); 53 | 54 | let errorCaught; 55 | stream.on('error', (err) => (errorCaught = err)); 56 | 57 | const r = await streamToStringRx(stream) 58 | .toPromise() 59 | .then( 60 | () => true, 61 | () => false 62 | ); 63 | expect(r).to.be.false; 64 | expect(errorCaught).to.have.property('message', 'TEST_ERROR'); 65 | }); 66 | 67 | it('rxToStream with promise error', async () => { 68 | // eslint-disable-next-line promise/param-names 69 | const promise: Promise = new Promise((_resolve: (value: string) => void, reject: (reason: Error) => void) => { 70 | reject(new Error('TEST_ERROR')); 71 | }); 72 | const src = from(promise); 73 | 74 | const stream = rxToStream(src, undefined, (err: Error, readable: Readable) => { 75 | readable.emit('error', err); 76 | }); 77 | 78 | let errorCaught; 79 | stream.on('error', (err) => (errorCaught = err)); 80 | 81 | const r = await streamToStringRx(stream) 82 | .toPromise() 83 | .then( 84 | () => true, 85 | () => false 86 | ); 87 | expect(r).to.be.false; 88 | expect(errorCaught).to.have.property('message', 'TEST_ERROR'); 89 | }); 90 | 91 | it('tests with a delayed hot observable', async () => { 92 | // This tests that we can send many small values to the stream one after another. 93 | // This is to make sure we do not run out of stack space. 94 | const max = 5; 95 | const src = timer(10, 1).pipe(take(max + 1)); 96 | const stream = rxToStream(src.pipe(map((a) => a.toString()))); 97 | 98 | const result = await streamToStringRx(stream) 99 | .pipe( 100 | map((a) => Number.parseInt(a)), 101 | reduce((a, b) => a + b) 102 | ) 103 | .toPromise(); 104 | expect(result).to.equal((max * (max + 1)) / 2); 105 | }); 106 | 107 | it('tests lazy = true', async () => { 108 | const text = 'Sometimes being lazy is fine.'; 109 | const src = from(text.split('')); 110 | const stream = rxToStream(src); 111 | const result = await streamToStringRx(stream) 112 | .pipe(reduce((a, b) => a + b)) 113 | .toPromise(); 114 | expect(result).to.equal(text); 115 | }); 116 | 117 | it('tests lazy = true for hot observable.', async () => { 118 | const text = 'It is not always good to be lazy'; 119 | const src = new Subject(); 120 | const stream = rxToStream(src); 121 | src.next(text); 122 | src.complete(); 123 | const result = await streamToStringRx(stream) 124 | .pipe(reduce((a, b) => a + b, '')) 125 | .toPromise(); 126 | expect(result).to.equal(''); 127 | }); 128 | 129 | it('tests lazy = false for hot observable.', async () => { 130 | const text = 'The early bird gets the worm.'; 131 | const src = new Subject(); 132 | const stream = rxToStream(src, { lazy: false, encoding: 'utf-8' }); 133 | src.next(text); 134 | src.complete(); 135 | const result = await streamToStringRx(stream) 136 | .pipe(reduce((a, b) => a + b, '')) 137 | .toPromise(); 138 | expect(result).to.equal(text); 139 | }); 140 | 141 | it('rxToStream large range', async () => { 142 | // This tests that we can send many small values to the stream one after another. 143 | // This is to make sure we do not run out of stack space. 144 | const max = 5000; 145 | const src = range(1, max); 146 | const stream = rxToStream(src.pipe(map((a) => a.toString()))); 147 | 148 | const result = await streamToStringRx(stream) 149 | .pipe( 150 | map((a) => Number.parseInt(a)), 151 | reduce((a, b) => a + b) 152 | ) 153 | .toPromise(); 154 | expect(result).to.equal((max * (max + 1)) / 2); 155 | }); 156 | 157 | it('tests writing an Observable and reading it back.', async () => { 158 | // cspell:ignore éåáí 159 | const text = loremIpsum({ count: 1000, format: 'plain', units: 'words' }) + ' éåáí'; 160 | const data = text.split(/\b/); 161 | const filename = path.join(__dirname, '..', 'temp', 'tests-writing-an-observable.txt'); 162 | 163 | const result = await from(mkdirp(path.dirname(filename))) 164 | .pipe( 165 | concatMap(() => writeToFileRxP(filename, from(data))), 166 | concatMap(() => fs.readFile(filename)), 167 | map((buffer) => buffer.toString()), 168 | reduce((a, b) => a + b) 169 | ) 170 | .toPromise(); 171 | expect(result).to.equal(text); 172 | }); 173 | 174 | function writeToFileRx(filename: string, data: Observable): fs.WriteStream { 175 | const sourceStream = rxToStream(data); 176 | 177 | const writeStream = fs.createWriteStream(filename); 178 | const zip = new stream.PassThrough(); 179 | 180 | return sourceStream.pipe(zip).pipe(writeStream); 181 | } 182 | 183 | function writeToFileRxP(filename: string, data: Observable): Promise { 184 | const stream = writeToFileRx(filename, data); 185 | return new Promise((resolve, reject) => { 186 | let resolved = false; 187 | const complete = () => { 188 | if (!resolved) resolve(); 189 | resolved = true; 190 | }; 191 | 192 | stream.on('finish', complete); 193 | stream.on('error', (e: Error) => reject(e)); 194 | stream.on('close', complete); 195 | }); 196 | } 197 | }); 198 | -------------------------------------------------------------------------------- /src/rxToStream.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Subscription } from 'rxjs'; 2 | import * as stream from 'stream'; 3 | 4 | export type Streamable = string | Buffer; 5 | 6 | export interface ReadableObservableStreamOptions extends stream.ReadableOptions { 7 | /** 8 | * Determines when to subscribe to the observable. 9 | * true - delay the subscription until the first read 10 | * false - subscribe immediately and buffer any data until the first read. 11 | * @default true 12 | */ 13 | lazy?: boolean; 14 | } 15 | 16 | export interface ObjectReadableOptions extends ReadableObservableStreamOptions { 17 | objectMode: true; 18 | } 19 | 20 | /** 21 | * Transform the output of an Observable into a node readable stream. 22 | */ 23 | export function rxToStream(src: Observable, options: ObjectReadableOptions, onError?: (error: Error, readable: stream.Readable) => void): stream.Readable; 24 | export function rxToStream( 25 | src: Observable, 26 | options?: ReadableObservableStreamOptions, 27 | onError?: (error: Error, readable: stream.Readable) => void 28 | ): stream.Readable; 29 | export function rxToStream( 30 | src: Observable, 31 | options: ReadableObservableStreamOptions = { encoding: 'utf8' }, 32 | onError?: (error: Error, readable: stream.Readable) => void 33 | ): stream.Readable { 34 | return new ReadableObservableStream(options, src, onError); 35 | } 36 | 37 | class ReadableObservableStream extends stream.Readable { 38 | private _isOpen = false; 39 | private _hasError = false; 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | private _error: any; 42 | private _waiting = false; 43 | private _subscription: Subscription; 44 | private _buffer: T[] = []; 45 | 46 | private emitError() { 47 | this.emit('error', this._error); 48 | if (this._onError) { 49 | this._onError(this._error, this); 50 | } 51 | } 52 | 53 | constructor( 54 | options: ReadableObservableStreamOptions, 55 | private _source: Observable, 56 | private _onError: ((error: Error, readable: stream.Readable) => void) | undefined 57 | ) { 58 | super(streamOptions(options)); 59 | if (options.lazy === false) { 60 | this.subscribeIfNecessary(); 61 | } 62 | } 63 | 64 | _destroy() { 65 | if (this._subscription) { 66 | this._subscription.unsubscribe(); 67 | } 68 | } 69 | 70 | _read() { 71 | const { _buffer } = this; 72 | 73 | this.subscribeIfNecessary(); 74 | 75 | if (_buffer.length > 0) { 76 | while (_buffer.length > 0) { 77 | this._waiting = this.push(_buffer.shift()); 78 | if (!this._waiting) break; 79 | } 80 | } else { 81 | if (this._isOpen) { 82 | this._waiting = true; 83 | } else { 84 | if (this._hasError) { 85 | this.emitError(); 86 | } else { 87 | this.push(null); 88 | } 89 | } 90 | } 91 | } 92 | 93 | private subscribeIfNecessary() { 94 | if (this._subscription) return; 95 | 96 | const { _buffer } = this; 97 | this._isOpen = true; 98 | this._waiting = true; 99 | this._subscription = this._source.subscribe({ 100 | next: (value) => { 101 | if (this._waiting) { 102 | this._waiting = this.push(value); 103 | } else { 104 | _buffer.push(value); 105 | } 106 | }, 107 | error: (err) => { 108 | this._isOpen = false; 109 | this._hasError = true; 110 | this._error = err; 111 | if (this._waiting) { 112 | this.emitError(); 113 | } 114 | }, 115 | complete: () => { 116 | this._isOpen = false; 117 | if (this._waiting) { 118 | this.push(null); 119 | } 120 | }, 121 | }); 122 | } 123 | } 124 | 125 | function streamOptions(options: ReadableObservableStreamOptions): stream.ReadableOptions { 126 | const { lazy: _, ...streamOptions } = options; 127 | return streamOptions; 128 | } 129 | -------------------------------------------------------------------------------- /src/streamToRx.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as stream from 'stream'; 3 | import { reduce, tap } from 'rxjs/operators'; 4 | import { streamToStringRx } from './index'; 5 | import { BehaviorSubject, Subject } from 'rxjs'; 6 | 7 | describe('Validate Rx From Stream', () => { 8 | it('tests stream to Rx', async () => { 9 | const data = 'This is a bit of text to have some fun with'; 10 | const bufferStream = new stream.PassThrough(); 11 | bufferStream.end(data, 'utf8'); 12 | const result = await streamToStringRx(bufferStream) 13 | .pipe(reduce((a, b) => a + b)) 14 | .toPromise(); 15 | expect(result).to.equal(data); 16 | }); 17 | 18 | it('tests closing a stream', async () => { 19 | const data = 'This is a bit of text to have some fun with'; 20 | const bufferStream = new stream.PassThrough(); 21 | const observable = streamToStringRx(bufferStream); 22 | bufferStream.write(data, 'utf8'); 23 | const result = observable.pipe(reduce((a, b) => a + b)).toPromise(); 24 | bufferStream.destroy ? bufferStream.destroy() : bufferStream.end(); 25 | expect(await result).to.equal(data); 26 | }); 27 | 28 | it('should not be a subject', async () => { 29 | const data = 'Some sample text'; 30 | const bufferStream = new stream.PassThrough(); 31 | const observable = streamToStringRx(bufferStream, 'utf8'); 32 | expect((observable as Subject).next).not.to.be.a('function'); 33 | bufferStream.end(data, 'utf8'); 34 | const result = await observable.pipe(reduce((a, b) => a + b)).toPromise(); 35 | expect(result).to.equal(data); 36 | }); 37 | 38 | it('tests stream pause', (done) => { 39 | const data = 'This is a bit of text to have some fun with'; 40 | const bufferStream = new stream.PassThrough(); 41 | const delayTime = 20; 42 | 43 | const pauser = new BehaviorSubject(false); 44 | bufferStream.write(data.slice(0, data.length / 2), 'utf8'); 45 | setTimeout(() => { 46 | bufferStream.end(data.slice(data.length / 2), 'utf8'); 47 | }, delayTime); 48 | 49 | streamToStringRx(bufferStream, 'utf8', pauser) 50 | .pipe( 51 | tap(() => { 52 | if (pauser.value === true) { 53 | throw new Error('should be paused'); 54 | } 55 | pauser.next(true); 56 | }), 57 | tap(() => { 58 | setTimeout(() => { 59 | pauser.next(false); 60 | }, delayTime * 2); 61 | }), 62 | reduce((a, b) => a + b) 63 | ) 64 | .subscribe({ 65 | next: (result) => { 66 | expect(result).to.equal(data); 67 | }, 68 | complete: () => { 69 | done(); 70 | }, 71 | error: (err) => { 72 | done(err); 73 | }, 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/streamToRx.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Subscription } from 'rxjs'; 2 | import { map, distinctUntilChanged } from 'rxjs/operators'; 3 | 4 | export function streamToRx(stream: NodeJS.ReadableStream, pauser?: Observable): Observable { 5 | return new Observable((subscriber) => { 6 | const endHandler = () => subscriber.complete(); 7 | const errorHandler = (e: Error) => subscriber.error(e); 8 | const dataHandler = (data: T) => subscriber.next(data); 9 | let pauseSubscription: Subscription; 10 | 11 | stream.addListener('end', endHandler); 12 | stream.addListener('close', endHandler); 13 | stream.addListener('error', errorHandler); 14 | stream.addListener('data', dataHandler); 15 | 16 | if (pauser) { 17 | pauseSubscription = pauser.pipe(distinctUntilChanged()).subscribe(function (b) { 18 | if (b === false) { 19 | stream.resume(); 20 | } else if (b === true) { 21 | stream.pause(); 22 | } 23 | }); 24 | } 25 | 26 | return () => { 27 | stream.removeListener('end', endHandler); 28 | stream.removeListener('close', endHandler); 29 | stream.removeListener('error', errorHandler); 30 | stream.removeListener('data', dataHandler); 31 | 32 | if (pauser) { 33 | pauseSubscription.unsubscribe(); 34 | } 35 | }; 36 | }); 37 | } 38 | 39 | export function streamToStringRx(stream: NodeJS.ReadableStream, encoding?: BufferEncoding, pauser?: Observable): Observable; 40 | export function streamToStringRx(stream: NodeJS.ReadableStream, encoding: BufferEncoding, pauser?: Observable): Observable; 41 | export function streamToStringRx(stream: NodeJS.ReadableStream, encoding: BufferEncoding = 'utf8', pauser?: Observable): Observable { 42 | return streamToRx(stream, pauser).pipe(map((buffer) => buffer.toString(encoding))); 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "alwaysStrict": true, 8 | "declaration": true, 9 | "noImplicitAny": false, 10 | "noImplicitThis": true, 11 | "strictNullChecks": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "outDir": "dist", 15 | "lib": ["dom", "dom.iterable", "es2015"], 16 | "forceConsistentCasingInFileNames": true 17 | }, 18 | "include": [ 19 | "src/**/*.ts", 20 | "src/**/*.test.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules/**", 24 | "dist/**", 25 | "dictionaries/**", 26 | "samples" 27 | ] 28 | } 29 | --------------------------------------------------------------------------------