├── .gitpod.yml ├── spec └── support │ └── jasmine.json ├── tsconfig.json ├── LICENSE ├── .gitignore ├── package.json ├── src ├── transducer.spec.ts └── transducer.ts └── README.md /.gitpod.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "src", 3 | "spec_files": ["**/*[sS]pec.ts"], 4 | "helpers": ["helpers/**/*.js"], 5 | "stopSpecOnExpectationFailure": false, 6 | "random": true 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017"], 4 | "target": "es5", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "downlevelIteration": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 rasmusvhansen 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 (http://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 | dist 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxjs-transducer", 3 | "version": "1.1.1", 4 | "description": "Transducer implementation leveraging RxJS operators to map, filter, reduce, take, skip on potentially infinite sequences", 5 | "main": "dist/transducer.js", 6 | "types": "dist/transducer.d.ts", 7 | "scripts": { 8 | "test:watch": "nodemon --ext ts --exec npm run test", 9 | "test": "ts-node node_modules/jasmine/bin/jasmine", 10 | "build": "tsc" 11 | }, 12 | "files": [ 13 | "dist/transducer.d.ts", 14 | "dist/transducer.js", 15 | "src" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/rasmusvhansen/rxjs-transducer.git" 20 | }, 21 | "keywords": [ 22 | "transducer", 23 | "rxjs", 24 | "map", 25 | "filter", 26 | "reduce", 27 | "take", 28 | "skip", 29 | "lazy", 30 | "infinite", 31 | "array", 32 | "sequence" 33 | ], 34 | "author": "Rasmus Vestergaard Hansen", 35 | "license": "ISC", 36 | "bugs": { 37 | "url": "https://github.com/rasmusvhansen/rxjs-transducer/issues" 38 | }, 39 | "homepage": "https://github.com/rasmusvhansen/rxjs-transducer#readme", 40 | "devDependencies": { 41 | "@types/jasmine": "^3.3.7", 42 | "jasmine": "^3.3.1", 43 | "nodemon": "^2.0.4", 44 | "rxjs": "^6.3.3", 45 | "ts-node": "^8.0.1", 46 | "typescript": "^3.2.4" 47 | }, 48 | "peerDependencies": { 49 | "rxjs": "^6.3.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/transducer.spec.ts: -------------------------------------------------------------------------------- 1 | import 'jasmine'; 2 | import { transducer } from './transducer'; 3 | import { map, filter, reduce, skip, take } from 'rxjs/operators'; 4 | const source = ['a', 'ab', 'abc', 'abcd', 'abcde']; 5 | 6 | describe('Transducer', () => { 7 | it('should be able to filter and map', () => { 8 | const result = transducer(source)(map(word => word.toUpperCase()), filter(word => word.length > 2)); 9 | expect(result.length).toBe(3); 10 | expect(result).toEqual(['ABC', 'ABCD', 'ABCDE']); 11 | }); 12 | 13 | it('should be able to filter and map and reduce', () => { 14 | const result = transducer(source)( 15 | map(word => word.toUpperCase()), 16 | filter(word => word.length > 2), 17 | reduce((acc, s) => `${acc}-${s}`) 18 | ); 19 | expect(result.length).toBe(1); 20 | expect(result).toEqual(['ABC-ABCD-ABCDE']); 21 | }); 22 | 23 | it('should be able to skip and take', () => { 24 | const result = transducer(source)(skip(1), take(2)); 25 | expect(result.length).toBe(2); 26 | expect(result).toEqual(['ab', 'abc']); 27 | }); 28 | 29 | it('should work with infinite sequences', () => { 30 | const result = transducer(integers())(map(i => i * 2), filter(i => i % 10 === 0), skip(10), take(5)); 31 | expect(result.length).toBe(5); 32 | expect(result).toEqual([100, 110, 120, 130, 140]); 33 | }); 34 | }); 35 | 36 | function* integers() { 37 | let i = 0; 38 | while (true) { 39 | yield i++; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rxjs-transducer 2 | 3 | A transducer implementation using the excellent and well known operators from RxJS. 4 | The benefits are: 5 | 6 | - Performance: Doing a array.map().filter().reduce() causes the array to be iterated 3 times. Using rxjs-transducers, the array is only iterated once. Doing a `filter().map().Math.max()` on an array with 1,000,000 items is roughly three times as fast with the transducer as with normal array operations. 7 | - Ability to work with lazy and infinite collections (generators) 8 | - Access to a huge library of well tested operators from RxJS such as `map`, `filter`, `reduce`, `skip`, `take`, `takeWhile`, and many others 9 | - Full TypeScript support 10 | 11 | ## Installation 12 | 13 | ```sh 14 | npm install rxjs-transducer --save 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### TypeScript / ES6 20 | 21 | ```typescript 22 | import { transducer } from 'rxjs-transducer'; 23 | import { map, filter, reduce, skip, take } from 'rxjs/operators'; 24 | const source = ['a', 'ab', 'abc', 'abcd', 'abcde']; 25 | 26 | // Works as standard array map and filter, but faster (only one iteration) 27 | const result = transducer(source)( 28 | map(word => word.toUpperCase()), 29 | filter(word => word.length > 2) 30 | ); 31 | // result -> ['ABC', 'ABCD', 'ABCDE'] 32 | 33 | // Note that results is always an array, even if the final operator 34 | // only produces a single result 35 | const result = transducer(source)( 36 | map(word => word.toUpperCase()), 37 | filter(word => word.length > 2), 38 | reduce((acc, s) => `${acc}-${s}`) 39 | ); 40 | // result -> ['ABC-ABCD-ABCDE'] 41 | 42 | // Works with infinte sequences too 43 | const result = transducer(integers())( 44 | map(i => i * 2), 45 | filter(i => i % 10 === 0), 46 | skip(10), 47 | take(5) 48 | ); 49 | // result -> [100, 110, 120, 130, 140] 50 | 51 | // Infinite sequence of integers from zero -> infinite 52 | function* integers() { 53 | let i = 0; 54 | while (true) { 55 | yield i++; 56 | } 57 | } 58 | ``` 59 | 60 | ### Javascript 61 | 62 | ```javascript 63 | const { map, filter, reduce, take, skip } = require('rxjs/operators'); 64 | const { transducer } = require('rxjs-transducer'); 65 | const source = ['a', 'ab', 'abc', 'abcd', 'abcde']; 66 | 67 | const result = transducer(source)( 68 | map(word => word.toUpperCase()), 69 | filter(word => word.length > 2) 70 | ); 71 | // result -> ['ABC', 'ABCD', 'ABCDE'] 72 | ``` 73 | 74 | ## Test 75 | 76 | ```sh 77 | npm run test 78 | ``` 79 | -------------------------------------------------------------------------------- /src/transducer.ts: -------------------------------------------------------------------------------- 1 | import { from, Observable } from 'rxjs'; 2 | import { scan } from 'rxjs/operators'; 3 | 4 | /** 5 | * Returns a function that takes any number of rxjs operators to lazily transform the input 6 | * collection. Runs synchronously. 7 | * Note that the result will always we collected in an array. Even if the result is only one value (reduce) 8 | */ 9 | export function transducer(collection: T[] | IterableIterator): IPipe { 10 | return function pipe(...operators: Array>) { 11 | let result: Array = []; 12 | const source = from(collection); 13 | const collect = scan( 14 | (acc, res) => { 15 | acc.push(res); 16 | return acc; 17 | }, 18 | >[] 19 | ); 20 | source.pipe.apply(source, [...operators, collect]).subscribe(res => (result = res)); 21 | return result; 22 | }; 23 | } 24 | 25 | export type UnaryFunction = (source: T) => R; 26 | export interface OperatorFunction extends UnaryFunction, Observable> {} 27 | 28 | export interface IPipe { 29 | (): T[]; 30 | (op1: OperatorFunction): A[]; 31 | (op1: OperatorFunction, op2: OperatorFunction): B[]; 32 | (op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): C[]; 33 | ( 34 | op1: OperatorFunction, 35 | op2: OperatorFunction, 36 | op3: OperatorFunction, 37 | op4: OperatorFunction 38 | ): D[]; 39 | ( 40 | op1: OperatorFunction, 41 | op2: OperatorFunction, 42 | op3: OperatorFunction, 43 | op4: OperatorFunction, 44 | op5: OperatorFunction 45 | ): E[]; 46 | ( 47 | op1: OperatorFunction, 48 | op2: OperatorFunction, 49 | op3: OperatorFunction, 50 | op4: OperatorFunction, 51 | op5: OperatorFunction, 52 | op6: OperatorFunction 53 | ): F[]; 54 | ( 55 | op1: OperatorFunction, 56 | op2: OperatorFunction, 57 | op3: OperatorFunction, 58 | op4: OperatorFunction, 59 | op5: OperatorFunction, 60 | op6: OperatorFunction, 61 | op7: OperatorFunction 62 | ): G[]; 63 | ( 64 | op1: OperatorFunction, 65 | op2: OperatorFunction, 66 | op3: OperatorFunction, 67 | op4: OperatorFunction, 68 | op5: OperatorFunction, 69 | op6: OperatorFunction, 70 | op7: OperatorFunction, 71 | op8: OperatorFunction 72 | ): H[]; 73 | ( 74 | op1: OperatorFunction, 75 | op2: OperatorFunction, 76 | op3: OperatorFunction, 77 | op4: OperatorFunction, 78 | op5: OperatorFunction, 79 | op6: OperatorFunction, 80 | op7: OperatorFunction, 81 | op8: OperatorFunction, 82 | op9: OperatorFunction 83 | ): I[]; 84 | ( 85 | op1: OperatorFunction, 86 | op2: OperatorFunction, 87 | op3: OperatorFunction, 88 | op4: OperatorFunction, 89 | op5: OperatorFunction, 90 | op6: OperatorFunction, 91 | op7: OperatorFunction, 92 | op8: OperatorFunction, 93 | op9: OperatorFunction, 94 | ...operations: OperatorFunction[] 95 | ): {}[]; 96 | } 97 | --------------------------------------------------------------------------------