├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── docs └── API.md ├── jest.config.js ├── package.json ├── prettier.config.js ├── src ├── __tests__ │ ├── combineLatest.test.ts │ ├── concurrentMap.test.ts │ ├── debounce.test.ts │ ├── distinctUntilChanged.test.ts │ ├── every.test.ts │ ├── filter.test.ts │ ├── first.test.ts │ ├── flatMap.test.ts │ ├── flatten.test.ts │ ├── interval.test.ts │ ├── last.test.ts │ ├── lookahead.test.ts │ ├── map.test.ts │ ├── merge.test.ts │ ├── pipe.test.ts │ ├── pluck.test.ts │ ├── range.test.ts │ ├── reduce.test.ts │ ├── scan.test.ts │ ├── skipwhile.test.ts │ ├── some.test.ts │ ├── spanAll.test.ts │ ├── subject.test.ts │ ├── take.test.ts │ ├── takeWhile.test.ts │ ├── throttle.test.ts │ ├── toCallbacks.test.ts │ └── zip.test.ts ├── combineLatest.ts ├── concat.ts ├── concurrentMap.ts ├── count.ts ├── debounce.ts ├── deferred.ts ├── distinctUntilChanged.ts ├── every.ts ├── filter.ts ├── first.ts ├── flatMap.ts ├── flatten.ts ├── from.ts ├── fromEvent.ts ├── fromLineReader.ts ├── fromNodeStream.ts ├── index.ts ├── insert.ts ├── interval.ts ├── iteratorToIterable.ts ├── last.ts ├── lookahead.ts ├── map.ts ├── merge.ts ├── of.ts ├── pipe.ts ├── pluck.ts ├── range.ts ├── reduce.ts ├── scan.ts ├── skip.ts ├── skipwhile.ts ├── some.ts ├── spanAll.ts ├── subject.ts ├── sum.ts ├── take.ts ├── takeWhile.ts ├── tap.ts ├── throttle.ts ├── toArray.ts ├── toCallbacks.ts ├── wait.ts └── zip.ts ├── tsconfig.es5.json ├── tsconfig.esnext.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Please add sample code that reproduces the issue or even better, create a PR with a test case. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Environment:** 17 | - NodeJS version: [e.g. NodeJS 10.x] 18 | - Browser [e.g. chrome, safari] 19 | - Version [e.g. 22] 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Provide links to other libraries that implement similar functionality** 14 | e.g. RxJS, lodash, most etc. 15 | 16 | **Additional context** 17 | Add any other context about the feature request here. 18 | 19 | **Describe the feature** 20 | A clear and concise description of what the feature is. 21 | 22 | **Check out the contributing guide** 23 | The [contributing guide](https://github.com/jamiemccrindle/axax/blob/master/CONTRIBUTING.md) will help you get started. 24 | 25 | **Have a look at how axax operators are written** 26 | Please have a look at how the other methods are written in axax e.g. they typically are curried e.g. here's map: 27 | 28 | ```javascript 29 | /** 30 | * Runs a mapping function over an asynchronous iterable 31 | */ 32 | export function map( 33 | mapper: (t: TFrom, index: number) => Promise | TTo 34 | ) { 35 | return async function* inner(source: AsyncIterable) { 36 | let index = 0; 37 | for await (const item of source) { 38 | yield await mapper(item, index++); 39 | } 40 | }; 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 16 * * 2' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | 35 | # Initializes the CodeQL tools for scanning. 36 | - name: Initialize CodeQL 37 | uses: github/codeql-action/init@v1 38 | with: 39 | languages: ${{ matrix.language }} 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 44 | 45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 46 | # If this step fails, then you should remove it and run the build manually (see below) 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v1 49 | 50 | # ℹ️ Command-line programs to run using the OS shell. 51 | # 📚 https://git.io/JvXDl 52 | 53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 54 | # and modify them (or add more) to build your code if your project 55 | # uses a compiled language 56 | 57 | #- run: | 58 | # make bootstrap 59 | # make release 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v1 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | esnext/ 4 | es5/ 5 | *.log 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jamiemccrindle@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone. By participating in this project, you agree 4 | to abide by the axax [code of conduct]. 5 | 6 | [code of conduct]: ./CODE_OF_CONDUCT.md 7 | 8 | Make sure you have a recent version of nodejs (10.x+) 9 | 10 | Fork the repo: 11 | 12 | git clone git@github.com:jamiemccrindle/axax.git 13 | 14 | Set up your machine: 15 | 16 | yarn install 17 | 18 | Make sure the tests pass: 19 | 20 | yarn lint 21 | yarn test 22 | 23 | Make your change. 24 | Write tests. 25 | Update the [API documentation](./docs/API.md) 26 | We use prettier to format the code. 27 | Make the tests pass: 28 | 29 | yarn lint 30 | yarn test 31 | 32 | Write a good commit message. 33 | Push to your fork. 34 | [Submit a pull request][pr]. 35 | 36 | [pr]: https://github.com/jamiemccrindle/axax/compare/ 37 | 38 | Wait for us. 39 | We try to at least comment on pull requests within two business days. 40 | We may suggest changes. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jamie McCrindle 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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | A few sentences describing the overall goals of the pull request's commits. 3 | 4 | ## Issue 5 | Issue you're closing 6 | 7 | ## Todos 8 | - [x] Tests 9 | - [x] API Documentation 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Async Iterator Extensions 2 | 3 | A library of async iterator extensions for JavaScript including ```map```, ```reduce```, 4 | ```filter```, ```flatMap```, ```pipe``` and [more](https://github.com/jamiemccrindle/axax/blob/master/docs/API.md#functions). 5 | 6 | 7 | # Installation 8 | 9 | ```bash 10 | npm install axax # or yarn add axax 11 | ``` 12 | 13 | # Why Axax? 14 | 15 | Async iterators are a useful way to handle asynchronous streams. This library adds a number 16 | of utility methods similar to those found in lodash, underscore, Ramda or RxJs. 17 | 18 | ## es5 vs esnext 19 | 20 | Axax contains both transpiled es5 code as well as esnext code, the difference being that 21 | esnext uses the native ```for await``` syntax. In nodejs 10.x that gives approximately a 40% speedup. 22 | 23 | ```javascript 24 | // use es5 if you want to support more browsers 25 | import { map } from "axax/es5/map"; 26 | 27 | // use esnext if you're only using node 10.x or supporting very new browsers 28 | import { map } from "axax/esnext/map"; 29 | ``` 30 | 31 | # Reference Documentation 32 | 33 | * [API Reference](https://github.com/jamiemccrindle/axax/blob/master/docs/API.md) 34 | 35 | # Examples 36 | 37 | ## fromEvent 38 | 39 | ```fromEvent``` turns DOM events into an iterable. 40 | 41 | ```javascript 42 | import { fromEvent } from "axax/es5/fromEvent"; 43 | 44 | const clicks = fromEvent(document, 'click'); 45 | 46 | for await (const click of clicks) { 47 | console.log('a button was clicked'); 48 | } 49 | ``` 50 | 51 | ## pipe, map, filter, fromLineReader 52 | 53 | ```fromLineReader``` turns a NodeJS LineReader into an async iterable. 54 | The example below prints the lines from a file in upper case after 55 | filtering out the empty ones. 56 | 57 | ```javascript 58 | // create the line reading async iterable 59 | const lines = fromLineReader( 60 | require("readline").createInterface({ 61 | input: require("fs").createReadStream("./data/example.txt") 62 | }) 63 | ); 64 | 65 | // create a filter that removes empty lines 66 | const notEmpty = filter(line => line.length > 0); 67 | 68 | // convert to uppercase 69 | const toUpperCase = map(line => line.toUpperCase()); 70 | 71 | // go through each of the non empty lines 72 | for await (const line of pipe(notEmpty, toUpperCase)(lines)) { 73 | console.log(line); 74 | } 75 | ``` 76 | 77 | ## Subject 78 | 79 | ```Subject``` makes it easy to turn stream of events into an iterable. The code below 80 | is essentially how ```fromEvent``` was implemented. 81 | 82 | ```javascript 83 | import { Subject } from "axax/es5/subject"; 84 | 85 | const subject = new Subject(); 86 | 87 | // set up a callback that calls value on the subject 88 | const callback = value => subject.onNext(value); 89 | 90 | // attach the callback to the click event 91 | document.addEventListener('click', callback); 92 | 93 | // remove the callback when / if the iterable stops 94 | subject.finally(() => document.removeEventListener('click', callback)); 95 | 96 | // go through all the click events 97 | for await (const click of subject.iterator) { 98 | console.log('a button was clicked'); 99 | } 100 | ``` 101 | 102 | # Avoiding leaks 103 | 104 | It's possible to have an async iterator leak if it never returns a value e.g.: 105 | 106 | ```javascript 107 | const subject1 = new Subject(); 108 | const subject2 = new Subject(); 109 | 110 | async function* neverEnds() { 111 | try { 112 | for await(const i of subject2.iterator) { 113 | yield i; 114 | } 115 | } finally { 116 | console.log("never called") 117 | } 118 | } 119 | 120 | async function* run() { 121 | for await(const i of merge(subject1.iterator,neverEnds())) { 122 | break; 123 | } 124 | } 125 | 126 | run() 127 | subject1.onNext(1) 128 | ``` 129 | 130 | If you need to be able to cancel async iterators that may never return values, 131 | consider Rx or regular Observables for now. 132 | 133 | (Thanks to [@awto](https://github.com/awto) for the example) 134 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # Axax API Reference 2 | 3 | ## Functions 4 | 5 | - [combineLatest](#combinelatest) 6 | - [concat](#concat) 7 | - [concurrentMap](#concurrentmap) 8 | - [count](#count) 9 | - [debounce](#debounce) 10 | - [distinctUntilChanged](#distinctuntilchanged) 11 | - [every](#every) 12 | - [filter](#filter) 13 | - [first](#first) 14 | - [flatMap](#flatmap) 15 | - [flatten](#flatten) 16 | - [from](#from) 17 | - [fromEvent](#fromevent) 18 | - [fromLineReader](#fromlinereader) 19 | - [fromNodeStream](#fromnodestream) 20 | - [insert](#insert) 21 | - [interval](#interval) 22 | - [last](#last) 23 | - [map](#map) 24 | - [merge](#merge) 25 | - [of](#of) 26 | - [pipe](#pipe) 27 | - [pluck](#pluck) 28 | - [range](#range) 29 | - [reduce](#reduce) 30 | - [scan](#scan) 31 | - [skip](#skip) 32 | - [skipWhile](#skipwhile) 33 | - [some](#some) 34 | - [sum](#sum) 35 | - [take](#take) 36 | - [takeWhile](#takewhile) 37 | - [tap](#tap) 38 | - [throttle](#throttle) 39 | - [zip](#zip) 40 | 41 | 42 | ## Classes 43 | 44 | - [Subject](#subject) 45 | - [Subject.callback](#subjectcallback) 46 | - [Subject.iterator](#subjectiterator) 47 | - [Subject.finally](#subjectfinally) 48 | - [Subject.onNext](#subjectonnext) 49 | - [Subject.onCompleted](#subjectoncompleted) 50 | - [Subject.onError](#subjectonerror) 51 | 52 | # Functions 53 | 54 | ## combineLatest 55 | 56 | Combines the output of a number of async iterators. 57 | It will only start outputting values when it has received 58 | at least one value from all of the source iterators. 59 | The result is an array with the latest value from each source 60 | async iterator. 61 | 62 | ```javascript 63 | import { combineLatest } from "axax/es5/combineLatest"; 64 | import { of } from "axax/es5/of"; 65 | import { wait } from "axax/es5/wait"; 66 | 67 | const combined = combineLatest( 68 | async function* first() { 69 | yield 1; 70 | await wait(200); 71 | yield 2; 72 | }, 73 | async function* second() { 74 | await wait(100) 75 | yield 3; 76 | await wait(200); 77 | yield 4; 78 | } 79 | ); 80 | 81 | for await(const item of combined) { 82 | console.log(item); // outputs [[1, 3], [2, 3], [2, 4]] 83 | } 84 | ``` 85 | 86 | ### Call signature 87 | 88 | ```javascript 89 | function combineLatest(...sources: AsyncIterable[]): AsyncIterableIterator 90 | ``` 91 | 92 | ## concat 93 | 94 | Concatenate 2 iterables in order 95 | 96 | ```javascript 97 | import { concat } from "axax/es5/concat"; 98 | import { of } from "axax/es5/of"; 99 | 100 | const concatted = concat( 101 | of(1, 2) 102 | )(of(3, 4)); 103 | 104 | for await(const item of concatted) { 105 | console.log(item); // outputs 1, 2, 3, 4 106 | } 107 | ``` 108 | 109 | ### Call signature 110 | 111 | ```javascript 112 | function concat(first: AsyncIterable): (second: AsyncIterable) => AsyncIterableIterator 113 | ``` 114 | 115 | ## concurrentMap 116 | 117 | Concurrently go through each item in the iterable and run a mapping function. 118 | The mapping function must return a promise. The result will be a new iterable with 119 | the transformed values. 120 | 121 | ```javascript 122 | import { concurrentMap } from "axax/es5/concurrentMap"; 123 | import { of } from "axax/es5/of"; 124 | 125 | const mapped = concurrentMap( 126 | async (value) => value * 2, // async mapping function 127 | 2 // run 2 concurrently 128 | )(of(1, 2, 3)); 129 | 130 | for await(const item of mapped) { 131 | console.log(item); // outputs 2, 4, 6 in no particular order 132 | } 133 | ``` 134 | 135 | ### Call signature 136 | 137 | ```javascript 138 | function concurrentMap( 139 | mapper: (value) => Promise, 140 | concurrency: number 141 | ): (source: AsyncIterable) => AsyncIterableIterator 142 | ``` 143 | 144 | ## count 145 | 146 | Counts the values returned by an async iterator 147 | 148 | ```javascript 149 | import { count } from "axax/es5/count"; 150 | import { of } from "axax/es5/of"; 151 | 152 | const counted = await count(of(1, 2, 3, 4)); 153 | console.log(counted); // outputs 4 154 | ``` 155 | 156 | ### Call signature 157 | 158 | ```javascript 159 | function count(source: AsyncIterable): Promise 160 | ``` 161 | 162 | ## Debounce 163 | 164 | Prevents emitting of values until the promise returned by the timer is resolved. 165 | When the timer resolves, emit the latest value from the async iterator. 166 | 167 | ```javascript 168 | import { debounce } from "axax/es5/debounce"; 169 | import { interval } from "axax/es5/interval"; 170 | import { take } from "axax/es5/take"; 171 | import { wait} from "axax/es5/wait"; 172 | 173 | const timer = () => wait(100); 174 | const counterToTenFiveMsIntervals = take(10)(interval(5)); 175 | const debounced = debounce(timer)(counterToTenFiveMsIntervals); 176 | 177 | for await (const item of debounced) { 178 | console.log(item); // prints 1, 3, 5, 7, 9 179 | } 180 | ``` 181 | 182 | ### Call signature 183 | 184 | ```javascript 185 | function debounce(timer: (value) => Promise): (source: AsyncIterable) => AsyncIterableIterator 186 | ``` 187 | 188 | ## distinctUntilChanged 189 | 190 | Only emit when the current value is different than the last. 191 | 192 | ```javascript 193 | import { distinctUntilChanged } from "axax/es5/distinctUntilChanged"; 194 | 195 | const distinct = await distinctUntilChanged(of(0, 1, 1, 1, 3, 3, 4, 5, 6, 6, 6, 6)); 196 | console.log(distinct); // outputs 0, 1, 3, 4, 5, 6 197 | ``` 198 | 199 | ### Call signature 200 | 201 | ```javascript 202 | function distinctUntilChanged(): (source: AsyncIterable) => AsyncIterableIterator 203 | ``` 204 | 205 | ## every 206 | 207 | If all values pass predicate before completion return true, else false. 208 | 209 | ```javascript 210 | import { every } from "axax/es5/every"; 211 | 212 | const everyFalseCase = await every(value => value % 2 === 0)(of(1, 2, 3, 4, 5, 6)); 213 | console.log(everyFalseCase); // outputs false 214 | 215 | const everyTrueCase = await every(value => value % 2 === 0)(of( 2, 4, 6)); 216 | console.log(everyTrueCase); // outputs true 217 | ``` 218 | 219 | ### Call signature 220 | 221 | ```javascript 222 | function every(predicate?: (value) => boolean): (source: AsyncIterable) => Promise 223 | ``` 224 | 225 | ## filter 226 | 227 | Filter an iterable based on some criteria. 228 | 229 | ```javascript 230 | import { filter } from "axax/es5/filter"; 231 | import { of } from "axax/es5/of"; 232 | 233 | const filtered = filter( 234 | value => value % 2 === 0 235 | )(of(1, 2, 3, 4, 5, 6)); 236 | 237 | for await(const item of filtered) { 238 | console.log(item); // outputs 2, 4, 6 239 | } 240 | ``` 241 | 242 | ### Call signature 243 | 244 | ```javascript 245 | function filter(predicate: (value) => boolean | Promise): (source: AsyncIterable) => AsyncIterableIterator 246 | ``` 247 | 248 | ## first 249 | 250 | Take the first value of async iterable that fullfills the predicate. If not predicate is provided, it returns the first value of the async iterable. 251 | 252 | ```javascript 253 | import { first } from "axax/es5/first"; 254 | import { of } from "axax/es5/of"; 255 | 256 | const firsted = first( 257 | value => value % 2 === 0 258 | )(of(1, 2, 3, 4, 5, 6)); 259 | 260 | for await(const item of firsted) { 261 | console.log(item); // outputs 2 262 | } 263 | ``` 264 | 265 | ### Call signature 266 | 267 | ```javascript 268 | function first(predicate?: (value) => boolean): (source: AsyncIterable) => AsyncIterableIterator 269 | ``` 270 | 271 | ## flatMap 272 | 273 | Go through each item in the iterable and run a mapping function that returns an async iterable. The 274 | result is then flattened. 275 | 276 | ```javascript 277 | import { flatMap } from "axax/es5/flatMap"; 278 | import { of } from "axax/es5/of"; 279 | 280 | const mapped = flatMap( 281 | async function* (value) { 282 | yield value; 283 | yield value; 284 | } 285 | )(of(1, 2, 3); 286 | 287 | for await(const item of mapped) { 288 | console.log(item); // outputs 1, 1, 2, 2, 3, 3 289 | } 290 | ``` 291 | 292 | ### Call signature 293 | 294 | ```javascript 295 | function flatMap(mapper: (value) => AsyncIterable): (source: AsyncIterable) => AsyncIterableIterator 296 | ``` 297 | 298 | ## flatten 299 | 300 | Flattens an async iterable of async iterables. 301 | 302 | ```javascript 303 | import { flatten } from "axax/es5/flatten"; 304 | import { of } from "axax/es5/of"; 305 | 306 | const flattened = flatten(of(of(1), of(2, 3))); 307 | 308 | for await(const item of flattened) { 309 | console.log(item); // prints 1, 2, 3 310 | } 311 | ``` 312 | 313 | ### Call signature 314 | 315 | ```javascript 316 | function flatten(source: AsyncIterable): AsyncIterableIterator 317 | ``` 318 | 319 | ## from 320 | 321 | Turn an array into an async iterable 322 | 323 | ```javascript 324 | import { from } from "axax/es5/from"; 325 | 326 | const values = from([1, 2, 3]); 327 | 328 | for await(const item of values) { 329 | console.log(item); // outputs 1, 2, 3 330 | } 331 | ``` 332 | 333 | ### Call signature 334 | 335 | ```javascript 336 | function from(values: Array): AsyncIterableIterator 337 | ``` 338 | 339 | ## fromEvent 340 | 341 | `fromEvents` turns DOM events into an iterable. 342 | 343 | ```javascript 344 | import { fromEvent } from "axax/es5/fromEvent"; 345 | 346 | const clicks = fromEvent(document, 'click'); 347 | 348 | for await (const click of clicks) { 349 | console.log('a button was clicked'); 350 | } 351 | ``` 352 | ## fromLineReader 353 | Turns a readline event listener from the package `readline` into an iterable. 354 | 355 | ```javascript 356 | import { fromLineReader } from "axax/es5/fromLineReader" 357 | 358 | const rl = readline('./foo.txt') 359 | const iterable = fromLineReader(rl); 360 | for await (const item of iterable) { 361 | console.log(item); // outputs line in file 362 | } 363 | ``` 364 | 365 | ### Call signature 366 | 367 | ```javascript 368 | function fromLineReader(eventSource, type: string): AsyncIterableIterator 369 | ``` 370 | 371 | ## fromNodeStream 372 | Turns a Node stream into an iterable 373 | ```javascript 374 | import { fromNodeStream } from "axax/es5/fromNodeStream" 375 | 376 | const stream = fs.createReadStream('foo.txt'); 377 | const iterable = fromNodeStream(stream); 378 | for await (const item of iterable) { 379 | console.log(item); // outputs chunk of buffer 380 | } 381 | 382 | ``` 383 | 384 | ### Call signature 385 | 386 | ```javascript 387 | function fromNodeStream(stream: fs.ReadStream): AsyncIterableIterator 388 | ``` 389 | 390 | ## insert 391 | 392 | Insert values at the beginning of an async iterable. 393 | 394 | ```javascript 395 | import { insert } from "axax/es5/insert"; 396 | import { of } from "axax/es5/of"; 397 | 398 | const inserted = insert( 399 | 1, 2, 3 400 | )(of(4, 5, 6)); 401 | 402 | for await(const item of inserted) { 403 | console.log(item); // outputs 1, 2, 3, 4, 5, 6 404 | } 405 | ``` 406 | 407 | ### Call signature 408 | 409 | ```javascript 410 | function insert(...values: Array): (source: AsyncIterable) => AsyncIterableIterator 411 | ``` 412 | 413 | ## interval 414 | 415 | Keep returning an incrementing number with a fixed delay between 416 | each number. 417 | 418 | ```javascript 419 | import { interval } from "axax/es5/interval"; 420 | 421 | for await (const item of interval(1000)) { 422 | console.log(item); // will output 0 to 10 423 | if(item >= 10) { 424 | break; // stop the iterable 425 | } 426 | } 427 | ``` 428 | 429 | ### Call signature 430 | 431 | ```javascript 432 | function interval(period: number, timeout?: (callback: () => void, delay: number) => void): AsyncIterableIterator 433 | ``` 434 | 435 | ## last 436 | 437 | Take the last value of async iterable that fullfills the predicate. If not predicate is provided, it returns the last value of the async iterable. Optionally include a second argument that will be returned if no value of the async iterable fulfills the predicate. 438 | 439 | ```javascript 440 | import { last } from "axax/es5/last"; 441 | import { of } from "axax/es5/of"; 442 | 443 | const lasted = last( 444 | value => value % 2 === 0 445 | )(of(1, 2, 3, 4, 5, 6, 7)); 446 | 447 | for await(const item of lasted) { 448 | console.log(item); // outputs 6 449 | } 450 | ``` 451 | 452 | ### Call signature 453 | 454 | ```javascript 455 | function last(predicate?: (value) => boolean, defaultValue?): (source: AsyncIterable) => AsyncIterableIterator 456 | ``` 457 | 458 | ## map 459 | 460 | Go through each item in the iterable and run a mapping function. The result will be a new 461 | iterable with the transformed values. 462 | 463 | ```javascript 464 | import { map } from "axax/es5/map"; 465 | import { of } from "axax/es5/of"; 466 | 467 | const mapped = map(value => value * 2)(of(1, 2, 3)); 468 | 469 | for await(const item of mapped) { 470 | console.log(item); // outputs 2, 4, 6 471 | } 472 | ``` 473 | 474 | ### Call signature 475 | 476 | ```javascript 477 | function map(mapper: (value, index: number) => T | Promise): (source: AsyncIterable) => AsyncIterableIterator 478 | ``` 479 | 480 | ## merge 481 | 482 | Merge a number of async iterators into one concurrently. Order is not important. 483 | 484 | ```javascript 485 | import { merge } from "axax/es5/merge"; 486 | import { of } from "axax/es5/of"; 487 | 488 | const merged = merge( 489 | of(1, 2), of(3, 4) 490 | ); 491 | 492 | for await(const item of merged) { 493 | console.log(item); // outputs 1, 2, 3, 4 in no particular order 494 | } 495 | ``` 496 | 497 | ### Call signature 498 | 499 | ```javascript 500 | function merge(...sources: AsyncIterable[]): AsyncIterableIterator 501 | ``` 502 | 503 | ## of 504 | 505 | Construct a new async iterable from a series 506 | of values. 507 | 508 | ```javascript 509 | import { of } from "axax/es5/of"; 510 | 511 | const values = of(1, 2, 3); 512 | 513 | for await(const item of values) { 514 | console.log(item); // outputs 1, 2, 3 515 | } 516 | ``` 517 | 518 | ### Call signature 519 | 520 | ```javascript 521 | function of(...values: Array): AsyncIterableIterator 522 | ``` 523 | 524 | ## pipe 525 | 526 | Pipe together a number of axax operators to use on a source async iterator. 527 | Operators are applied left to right 528 | 529 | ```javascript 530 | import { filter } from "axax/es5/filter"; 531 | import { pipe } from "axax/es5/pipe"; 532 | import { map } from "axax/es5/map"; 533 | import { of } from "axax/es5/of"; 534 | 535 | const piped = pipe( 536 | filter(value => value % 2 === 0), 537 | map(value => value * 2)) 538 | (of(1, 2, 3, 4)); 539 | 540 | for await(const item of piped) { 541 | console.log(item); // prints 4, 8 542 | } 543 | ``` 544 | 545 | ### Call signature 546 | 547 | ```javascript 548 | function pipe(...funcs: ((iterable: AsyncIterable) => any)[]): (source: AsyncIterable) => AsyncIterable 549 | ``` 550 | 551 | ## pluck 552 | 553 | Map source objects to a property specified by the given keys. 554 | Returns the unchanged source on empty input or undefined 555 | when any property in the path is undefined. 556 | 557 | ```javascript 558 | import { from } from "axax/es5/from"; 559 | import { pipe } from "axax/es5/pipe"; 560 | import { pluck } from "axax/es5/pluck"; 561 | 562 | const persons = [ 563 | { 564 | name: "Anna", 565 | age: 29, 566 | }, 567 | { 568 | name: "Max", 569 | age: 41, 570 | }, 571 | ] 572 | 573 | const piped = pipe( 574 | pluck("name")) 575 | (from(persons)); 576 | 577 | for await(const item of piped) { 578 | console.log(item); // prints "Anna", "Max" 579 | } 580 | 581 | ``` 582 | 583 | ### Call signature 584 | 585 | ```javascript 586 | function pluck(...path: string[]): (source: AsyncIterable) => AsyncIterableIterator 587 | ``` 588 | 589 | ## range 590 | 591 | Creates an iterable of numbers (positive and/or negative) 592 | progressing from start up to, but not including, end. A step 593 | of -1 is used if a negative start is specified without an end 594 | or step. If end is not specified, it's set to start with start 595 | then set to 0. 596 | 597 | ```javascript 598 | import { range } from "axax/es5/range"; 599 | import { of } from "axax/es5/of"; 600 | 601 | const ranged = range(1, 3); 602 | 603 | for await(const item of ranged) { 604 | console.log(item); // prints 1, 2 605 | } 606 | ``` 607 | 608 | ### Call signature 609 | 610 | ```javascript 611 | function range(startOrEnd: number, end: number, step?: number): AsyncIterableIterator 612 | ``` 613 | 614 | ## reduce 615 | 616 | Reduce a series of values to a single result. The series of values is 617 | reduced by a function that compbines a running total or accumulator with 618 | the next value to produce the new total or accumulator. 619 | 620 | ```javascript 621 | import { reduce } from "axax/es5/reduce"; 622 | import { of } from "axax/es5/of"; 623 | 624 | const reduced = reduce( 625 | (accumulator, next) => accumulator + next, // sum the values together 626 | 0 627 | )(of(1, 2, 3)); 628 | 629 | console.log(reduced); // 6 630 | ``` 631 | 632 | ### Call signature 633 | 634 | ```javascript 635 | function reduce(reducer: (accumulator, next) => A | Promise, init: A | Promise): (source: AsyncIterable) => Promise 636 | ``` 637 | 638 | ## scan 639 | 640 | Similar to a reduce except that it outputs the accumulator as it goes. 641 | 642 | ```javascript 643 | import { scan } from "axax/es5/scan"; 644 | import { of } from "axax/es5/of"; 645 | 646 | const scanned = scan((accumulator, value) => accumulator + value, 0)(of(1, 2, 3)); 647 | 648 | for await(const item of scanned) { 649 | console.log(item); // prints 0, 1, 3, 6 650 | } 651 | ``` 652 | 653 | ### Call signature 654 | 655 | ```javascript 656 | function scan(scanner: (accumulator, next) => A | Promise, init: A | Promise): (source: AsyncIterable) => AsyncIterableIterator 657 | ``` 658 | 659 | ## skip 660 | 661 | skips the first x values from an async iterable 662 | 663 | ```javascript 664 | import { skip } from "axax/es5/skip"; 665 | 666 | const skip = await skip(2)(of(1, 2, 3, 4)); 667 | console.log(skip); // outputs 3, 4 668 | ``` 669 | 670 | ### Call signature 671 | 672 | ```javascript 673 | function skip(scanner: (accumulator, next) => A | Promise, init: A | Promise): (source: AsyncIterable) => AsyncIterableIterator 674 | ``` 675 | 676 | ## skipWhile 677 | 678 | Skipwhile emitted values from source until provided expression is false. 679 | 680 | ```javascript 681 | import { skipWhile } from "axax/es5/skipWhile"; 682 | 683 | const skip = await skipWhile(value => value < 2)(of(0, 1, 2, 3, 4, 5, 6, 1, 2)); 684 | console.log(skip); // outputs 2, 3, 4, 5, 6, 1, 2 685 | ``` 686 | 687 | ### Call signature 688 | 689 | ```javascript 690 | function skipWhile(predicate?: (value) => boolean): (source: AsyncIterable) => AsyncIterableIterator 691 | ``` 692 | 693 | ## some 694 | 695 | If any values pass predicate before completion return true, else false. 696 | 697 | ```javascript 698 | import { some } from "axax/es5/some"; 699 | 700 | const someFalseCase = await some(value => value % 2 === 0)(of(1, 3, 5, 7, 9)); 701 | console.log(someFalseCase); // outputs false 702 | 703 | const someTrueCase = await some(value => value % 2 === 0)(of(1, 2, 3)); 704 | console.log(someTrueCase); // outputs true 705 | ``` 706 | 707 | ### Call signature 708 | 709 | ```javascript 710 | function some(predicate?: (value) => boolean): (source: AsyncIterable) => Promise 711 | ``` 712 | 713 | ## sum 714 | 715 | Sum the values returned by an async iterator 716 | 717 | ```javascript 718 | import { sum } from "axax/es5/sum"; 719 | import { of } from "axax/es5/of"; 720 | 721 | const summed = await sum(of(1, 2, 3, 4)); 722 | console.log(summed); // outputs 10 723 | ``` 724 | 725 | ### Call signature 726 | 727 | ```javascript 728 | function sum(source: AsyncIterable): Promise 729 | ``` 730 | 731 | ## take 732 | 733 | Take the first x values from the async iterator 734 | 735 | ```javascript 736 | import { take } from "axax/es5/take"; 737 | import { of } from "axax/es5/of"; 738 | 739 | const taken = await take(2)(of(1, 2, 3, 4)); 740 | console.log(taken); // outputs 1, 2 741 | ``` 742 | 743 | ### Call signature 744 | 745 | ```javascript 746 | function take(numberToTake: number): (source: AsyncIterable) => AsyncIterableIterator 747 | ``` 748 | 749 | ## takeWhile 750 | 751 | Take values while a predicate holds true 752 | 753 | ```javascript 754 | import { takeWhile } from "axax/es5/takeWhile"; 755 | import { of } from "axax/es5/of"; 756 | 757 | const taken = await takeWhile(value => value < 3)(of(1, 2, 3, 4)); 758 | console.log(taken); // outputs 1, 2 759 | ``` 760 | 761 | ### Call signature 762 | 763 | ```javascript 764 | function takeWhile(predicate: (value) => boolean): (source: AsyncIterable) => AsyncIterableIterator 765 | ``` 766 | 767 | ## tap 768 | 769 | 'Taps' an async iterable. Allows you to run a function for 770 | every item in the iterable but doesn't do anything with the 771 | result of the function. Typically used for side effects like 772 | logging. 773 | 774 | ```javascript 775 | import { tap } from "axax/es5/tap"; 776 | import { of } from "axax/es5/of"; 777 | 778 | const tapped = tap( 779 | value => console.log(value) // prints 1, 2, 3 780 | )(of(1, 2, 3)); 781 | 782 | for await(const item of tapped) { 783 | console.log(item); // prints 1, 2, 3 784 | } 785 | ``` 786 | 787 | ### Call signature 788 | 789 | ```javascript 790 | function tap(func: (value) => void): (source: AsyncIterable) => AsyncIterableIterator 791 | ``` 792 | 793 | ## Throttle 794 | Emits a value and then drops all values until the promise returned by the timer is resolved. 795 | 796 | ```javascript 797 | import { interval } from "axax/es5/interval"; 798 | import { take } from "axax/es5/take"; 799 | import { throttle } from "axax/es5/throttle"; 800 | import { wait} from "axax/es5/wait"; 801 | 802 | const timer = () => wait(100); 803 | const counterToTenFiveMsIntervals = take(10)(interval(5)); 804 | const throttled = throttle(timer)(counterToTenFiveMsIntervals); 805 | for await (const item of throttled) { 806 | console.log(item); // prints 0, 2, 4, 6, 8 807 | } 808 | ``` 809 | 810 | ### Call signature 811 | 812 | ```javascript 813 | function throttle(timer: (value) => Promise): (source: AsyncIterable) => AsyncIterableIterator 814 | ``` 815 | 816 | ## zip 817 | 818 | Creates a new iterable out of the two supplied by pairing up equally-positioned items from both iterables. The returned iterable is truncated to the length of the shorter of the two input iterables. 819 | 820 | ```javascript 821 | import { zip } from "axax/es5/zip"; 822 | import { of } from "axax/es5/of"; 823 | 824 | const zipped = zip( 825 | of(1, 2) 826 | )(of(1, 2)); 827 | 828 | for await(const item of zipped) { 829 | console.log(item); // prints [1, 1], [2, 2] 830 | } 831 | ``` 832 | 833 | ### Call signature 834 | 835 | ```javascript 836 | function zip(first: AsyncIterable): (second: AsyncIterable) => AsyncIterableIterator 837 | ``` 838 | 839 | # Classes 840 | 841 | ## Subject 842 | 843 | `Subject` makes it easy to turn stream of events into an iterable. 844 | 845 | You typically interact with `Subject` in 3 ways: 846 | 847 | - To send data into the `Subject` call the `onNext` function with a value 848 | - To complete sending data, call `onComplete` 849 | - To read data from the `Subject` use the `iterator` property. 850 | 851 | ### Example 852 | 853 | ```javascript 854 | import { Subject } from "axax/es5/subject"; 855 | 856 | const subject = new Subject(); 857 | 858 | // set up a callback that calls value on the subject 859 | const callback = value => subject.onNext(value); 860 | 861 | // attach the callback to the click event 862 | document.addEventListener('click', callback); 863 | 864 | // remove the callback when / if the iterable stops 865 | subject.finally(() => document.removeEventListener('click', callback)); 866 | 867 | // go through all the click events 868 | for await (const click of subject.iterator) { 869 | console.log('a button was clicked'); 870 | } 871 | ``` 872 | 873 | ### Subject.callback 874 | 875 | The callback to call to send a `IteratorResult` into the `Subject`. An `IteratorResult` 876 | has a `done` boolean property and a `value` property. 877 | 878 | ```javascript 879 | import { Subject } from "axax/es5/subject"; 880 | 881 | const subject = new Subject(); 882 | subject.callback({ done: false, value: 1 }); 883 | subject.callback({ done: false, value: 2 }); 884 | subject.callback({ done: false, value: 3 }); 885 | subject.callback({ done: true }); 886 | 887 | for await (const item of subject.iterator) { 888 | console.log(item); // prints 1, 2, 3 889 | } 890 | ``` 891 | 892 | ### Subject.iterator 893 | 894 | An `AsyncIterable` that returns values supplied by calling `callback`. 895 | 896 | ```javascript 897 | import { Subject } from "axax/es5/subject"; 898 | 899 | const subject = new Subject(); 900 | subject.callback({ done: false, value: 1 }); 901 | subject.callback({ done: false, value: 2 }); 902 | subject.callback({ done: false, value: 3 }); 903 | subject.callback({ done: true }); 904 | 905 | for await (const item of subject.iterator) { 906 | console.log(item); // prints 1, 2, 3 907 | } 908 | ``` 909 | 910 | ### Subject.finally 911 | 912 | The callback supplied to `finally` is called when the iterable finishes either 913 | because it's run out of values, it has returned or an error was thrown. 914 | 915 | ### Subject.onNext 916 | 917 | A helper method to pass a value to the Subject. Calling 918 | `subject.onNext('test')` is the same as calling 919 | `subject.callback({ done: false, value: 'test'} )`. 920 | 921 | ### Subject.onCompleted 922 | 923 | A helper method to signal the last value to Subject. Calling 924 | `subject.onCompleted()` is the same as calling 925 | `subject.callback({ done: true} )`. 926 | 927 | ### Subject.onError 928 | 929 | Signal that an error has occured. 930 | 931 | ```javascript 932 | import { Subject } from "axax/es5/subject"; 933 | 934 | const subject = new Subject(); 935 | subject.onNext(1); 936 | subject.onError("something went wrong"); 937 | 938 | try { 939 | for await (const item of subject.iterable) { 940 | console.log(item); // outputs 1 941 | } 942 | } catch(e) { 943 | console.log(e); // outputs "something went wrong" 944 | } 945 | ``` 946 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 9 | "moduleFileExtensions": [ 10 | "ts", 11 | "tsx", 12 | "js", 13 | "jsx", 14 | "json", 15 | "node" 16 | ], 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axax", 3 | "version": "0.2.2", 4 | "license": "MIT", 5 | "repository": { 6 | "url": "https://github.com/jamiemccrindle/axax", 7 | "type": "git" 8 | }, 9 | "keywords": [ 10 | "async", 11 | "async iterators", 12 | "async generators", 13 | "flatMap", 14 | "map", 15 | "merge", 16 | "filter", 17 | "map", 18 | "reduce" 19 | ], 20 | "devDependencies": { 21 | "@types/jest": "^24.0.18", 22 | "@types/node": "^12.7.8", 23 | "jest": "^24.9.0", 24 | "prettier": "^1.14.3", 25 | "rimraf": "^3.0.0", 26 | "ts-jest": "^24.1.0", 27 | "ts-node": "^8.4.1", 28 | "tslint": "^5.11.0", 29 | "typescript": "^3.1.1" 30 | }, 31 | "scripts": { 32 | "lint": "tslint --project tsconfig.json", 33 | "test": "jest", 34 | "clean": "rimraf es5 esnext", 35 | "build": "yarn clean && yarn lint && tsc -p tsconfig.es5.json && tsc -p tsconfig.esnext.json" 36 | }, 37 | "main": "es5/index.js", 38 | "types": "es5/index.d.ts", 39 | "files": [ 40 | "es5", 41 | "esnext" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 2 4 | }; 5 | -------------------------------------------------------------------------------- /src/__tests__/combineLatest.test.ts: -------------------------------------------------------------------------------- 1 | import { combineLatest } from "../combineLatest"; 2 | import { Subject } from "../subject"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("combineLatest", async () => { 6 | const subjects = [ 7 | new Subject(), 8 | new Subject(), 9 | new Subject(), 10 | ]; 11 | const combined = combineLatest(...subjects.map(s => s.iterator)); 12 | subjects[0].onNext(1); 13 | subjects[1].onNext(2); 14 | subjects[2].onNext(3); 15 | subjects[0].onCompleted(); 16 | subjects[1].onCompleted(); 17 | subjects[2].onCompleted(); 18 | const result = (await toArray(combined)).sort(); 19 | expect(result).toEqual([[1, 2, 3]]); 20 | }); 21 | 22 | test("combineLatest not enough values", async () => { 23 | const subjects = [ 24 | new Subject(), 25 | new Subject(), 26 | new Subject(), 27 | ]; 28 | const combined = combineLatest(...subjects.map(s => s.iterator)); 29 | subjects[1].onNext(2); 30 | subjects[2].onNext(3); 31 | subjects[0].onCompleted(); 32 | subjects[1].onCompleted(); 33 | subjects[2].onCompleted(); 34 | const result = (await toArray(combined)).sort(); 35 | expect(result).toEqual([]); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__tests__/concurrentMap.test.ts: -------------------------------------------------------------------------------- 1 | import { concurrentMap } from "../concurrentMap"; 2 | import { of } from "../of"; 3 | import { range } from "../range"; 4 | import { toArray } from "../toArray"; 5 | import { wait } from "../wait"; 6 | 7 | test("concurrentMap", async () => { 8 | const result = await toArray( 9 | concurrentMap(async (value: number) => value * 2, 2)(of(1, 2, 3)) 10 | ); 11 | result.sort(); 12 | expect(result).toEqual([2, 4, 6]); 13 | }); 14 | 15 | test("concurrentMap async", async () => { 16 | let maxInParallel = 0; 17 | let currentInParallel = 0; 18 | const increment = () => { 19 | currentInParallel += 1; 20 | if (currentInParallel > maxInParallel) { 21 | maxInParallel = currentInParallel; 22 | } 23 | }; 24 | const decrement = () => { 25 | currentInParallel -= 1; 26 | }; 27 | for await (const item of concurrentMap(async () => { 28 | increment(); 29 | await wait(100); 30 | decrement(); 31 | }, 4)(range(1, 10))) { 32 | // noop 33 | } 34 | expect(maxInParallel).toEqual(4); 35 | }); 36 | -------------------------------------------------------------------------------- /src/__tests__/debounce.test.ts: -------------------------------------------------------------------------------- 1 | import { debounce } from "../debounce"; 2 | import { interval } from "../interval"; 3 | import { of } from "../of"; 4 | import { take } from "../take"; 5 | import { toArray } from "../toArray"; 6 | import { wait } from "../wait"; 7 | 8 | test("debounce", async () => { 9 | const result = await toArray( 10 | debounce(() => wait(100))(of(1, 2, 3, 4, 5, 6)) 11 | ); 12 | expect(result).toEqual([6]); 13 | }); 14 | 15 | test("debounce one value", async () => { 16 | const startTime = Date.now(); 17 | const result = await toArray( 18 | debounce(() => wait(100))(of(1)) 19 | ); 20 | const endTime = Date.now(); 21 | expect(result).toEqual([1]); 22 | expect(endTime - startTime).toBeGreaterThanOrEqual(100); 23 | }); 24 | 25 | // test("debounce interval", async () => { 26 | // const result = await toArray( 27 | // debounce(() => wait(100))(take(50)(interval(10))) 28 | // ); 29 | // expect(result).toEqual([9, 19, 29, 39, 49]); 30 | // }); 31 | 32 | test("debounce timer count", async () => { 33 | let counter = 0; 34 | const result = await toArray( 35 | debounce(() => { 36 | counter++; 37 | return wait(100); 38 | })(take(50)(interval(10))) 39 | ); 40 | expect(counter).toBeLessThan(10); 41 | }); 42 | 43 | test("debounce increasing timeout", async () => { 44 | const result = await toArray( 45 | debounce(val => wait(200 * (val + 1)))(take(10)(interval(100))) 46 | ); 47 | expect(result).toEqual([1, 7, 9]); 48 | }); 49 | -------------------------------------------------------------------------------- /src/__tests__/distinctUntilChanged.test.ts: -------------------------------------------------------------------------------- 1 | import { distinctUntilChanged } from "../distinctUntilChanged"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("distinctUntilChanged", async () => { 6 | const result = await toArray(distinctUntilChanged()(of(0, 1, 1, 1, 3, 3, 4, 5, 6, 6, 6, 6))); 7 | expect(result).toEqual([0, 1, 3, 4, 5, 6]); 8 | }); 9 | -------------------------------------------------------------------------------- /src/__tests__/every.test.ts: -------------------------------------------------------------------------------- 1 | import { every } from "../every"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("every with predicate and with false as a result", async () => { 6 | const result = await every((value: number) => value % 2 === 0)(of(1, 2, 3, 4, 5, 6)); 7 | expect(result).toEqual(false); 8 | }); 9 | 10 | test("every with predicate and with true as a result", async () => { 11 | const result = await every((value: number) => value % 2 === 0)(of(2, 4, 6)); 12 | expect(result).toEqual(true); 13 | }); 14 | 15 | test("every with predicate and with false as a result : edge case scenario", async () => { 16 | const result = await every((value: number) => value % 2 === 0)(of(2, 4, 6, 7)); 17 | expect(result).toEqual(false); 18 | }); 19 | 20 | test("every with predicate and with false as a result : edge case scenario", async () => { 21 | const result = await every((value: number) => value % 2 === 0)(of(1, 3, 5, 6)); 22 | expect(result).toEqual(false); 23 | }); 24 | 25 | test("every with predicate and with false as a result : edge case scenario", async () => { 26 | const result = await every((value: number) => value % 2 === 0)(of(2, 3, 5, 7)); 27 | expect(result).toEqual(false); 28 | }); 29 | 30 | test("every with predicate and with false as a result : edge case scenario", async () => { 31 | const result = await every((value: number) => value % 2 === 0)(of(1, 2, 4, 6)); 32 | expect(result).toEqual(false); 33 | }); 34 | -------------------------------------------------------------------------------- /src/__tests__/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { filter } from "../filter"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("filter", async () => { 6 | const result = await toArray( 7 | filter((value: number) => value % 2 === 0)(of(1, 2, 3, 4, 5, 6)) 8 | ); 9 | expect(result).toEqual([2, 4, 6]); 10 | }); 11 | 12 | test("async filter", async () => { 13 | const result = await toArray( 14 | filter(async () => false)(of(1, 2, 3)) 15 | ); 16 | expect(result).toEqual([]); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/first.test.ts: -------------------------------------------------------------------------------- 1 | import { first } from "../first"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("first", async () => { 6 | const result = await toArray(first()(of(1, 2, 3, 4, 5, 6))); 7 | expect(result).toEqual([1]); 8 | }); 9 | 10 | test("first with predicate", async () => { 11 | const result = await toArray( 12 | first((value: number) => value % 5 === 0)(of(1, 2, 3, 4, 5, 6))); 13 | expect(result).toEqual([5]); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__tests__/flatMap.test.ts: -------------------------------------------------------------------------------- 1 | import { flatMap } from "../flatMap"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("flatMap", async () => { 6 | const result = await toArray( 7 | flatMap(async function*(value: number) { 8 | yield value * 2; 9 | })(of(1, 2, 3)) 10 | ); 11 | expect(result).toEqual([2, 4, 6]); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/flatten.test.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from "../flatten"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("flatten", async () => { 6 | const result = await toArray(flatten(of(of(1), of(2), of(3)))); 7 | expect(result).toEqual([1, 2, 3]); 8 | }); 9 | -------------------------------------------------------------------------------- /src/__tests__/interval.test.ts: -------------------------------------------------------------------------------- 1 | import { interval } from "../interval"; 2 | 3 | test("interval", async () => { 4 | const iterable = interval(100, (cb, delay) => { 5 | cb(); 6 | }); 7 | const result = []; 8 | for await (const item of iterable) { 9 | result.push(item); 10 | if (item === 5) { 11 | break; 12 | } 13 | } 14 | expect(result).toEqual([0, 1, 2, 3, 4, 5]); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/last.test.ts: -------------------------------------------------------------------------------- 1 | import { last } from "../last"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("last", async () => { 6 | const result = await toArray(last()(of(1, 2, 3, 4, 5, 6, 7))); 7 | expect(result).toEqual([7]); 8 | }); 9 | 10 | test("last with predicate", async () => { 11 | const result = await toArray( 12 | last((value: number) => value % 2 === 0)(of(1, 2, 3, 4, 5, 6, 7))); 13 | expect(result).toEqual([6]); 14 | }); 15 | 16 | test("last with no value fulfilling predicate", async () => { 17 | const result = await toArray( 18 | last((value: number) => value % 8 === 0)(of(1, 2, 3, 4, 5, 6))); 19 | expect(result).toEqual([]); 20 | }); 21 | 22 | test("last with default value", async () => { 23 | const result = await toArray( 24 | last((value: number) => value % 8 === 0, 42)(of(1, 2, 3, 4, 5, 6))); 25 | expect(result).toEqual([42]); 26 | }); 27 | -------------------------------------------------------------------------------- /src/__tests__/lookahead.test.ts: -------------------------------------------------------------------------------- 1 | import { lookahead } from "../lookahead"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("lookahead", async () => { 6 | const result = await lookahead(2)(of(1, 2, 3)); 7 | expect(result.values).toEqual([1, 2]); 8 | expect(await toArray(result.iterable)).toEqual([1, 2, 3]); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/map.test.ts: -------------------------------------------------------------------------------- 1 | import { map } from "../map"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("map", async () => { 6 | const result = await toArray( 7 | map((value: number) => value * 2)(of(1, 2, 3)) 8 | ); 9 | expect(result).toEqual([2, 4, 6]); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/merge.test.ts: -------------------------------------------------------------------------------- 1 | import { merge } from "../merge"; 2 | import { of } from "../of"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("merge", async () => { 6 | const merged = merge(of(1, 2, 3, 4, 5), of(6), of(7, 8, 9)); 7 | const result = (await toArray(merged)).sort(); 8 | expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/pipe.test.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from "../flatten"; 2 | import { map } from "../map"; 3 | import { of } from "../of"; 4 | import { pipe } from "../pipe"; 5 | import { toArray } from "../toArray"; 6 | 7 | test("pipe", async () => { 8 | const result = await toArray( 9 | pipe( 10 | map(x => of(x)), 11 | flatten 12 | )(of(1, 2, 3)) 13 | ); 14 | expect(result).toEqual([1, 2, 3]); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/pluck.test.ts: -------------------------------------------------------------------------------- 1 | import { from } from "../from"; 2 | import { of } from "../of"; 3 | import { pluck } from "../pluck"; 4 | import { toArray } from "../toArray"; 5 | 6 | test("pluck", async () => { 7 | const source = { 8 | a: "plucked", 9 | x: 1, 10 | y: 2, 11 | z: {}, 12 | }; 13 | 14 | const result = await toArray( 15 | pluck("a")(of(source)) 16 | ); 17 | 18 | expect(result).toEqual(["plucked"]); 19 | }); 20 | 21 | test("pluck with multiple items", async () => { 22 | const source = [ 23 | { 24 | a: "item1", 25 | x: 1, 26 | }, 27 | { 28 | a: "item2", 29 | x: 1, 30 | } 31 | ]; 32 | 33 | const result = await toArray( 34 | pluck("a")(from(source)) 35 | ); 36 | 37 | expect(result).toEqual(["item1", "item2"]); 38 | }); 39 | 40 | test("pluck with nested properties", async () => { 41 | const source = { 42 | a: { 43 | b: 1 44 | }, 45 | x: 2, 46 | }; 47 | 48 | const result = await toArray( 49 | pluck("a", "b")(of(source)) 50 | ); 51 | 52 | expect(result).toEqual([1]); 53 | }); 54 | 55 | test("pluck with undefined properties", async () => { 56 | const source = { 57 | a: { 58 | b: 2 59 | }, 60 | b: 3, 61 | x: 4, 62 | }; 63 | 64 | const result = await toArray( 65 | pluck("a", "b", "c")(of(source)) 66 | ); 67 | 68 | expect(result).toEqual([undefined]); 69 | }); 70 | 71 | test("pluck with no parameters", async () => { 72 | const source = { 73 | a: 1, 74 | }; 75 | 76 | const result = await toArray( 77 | pluck()(of(source)) 78 | ); 79 | 80 | expect(result).toEqual([source]); 81 | }); 82 | 83 | test("pluck with non-object sources", async () => { 84 | const source = [ 85 | 1, 86 | 2, 87 | 4, 88 | ]; 89 | 90 | const result = await toArray( 91 | pluck("test")(of(source)) 92 | ); 93 | 94 | expect(result).toEqual([undefined]); 95 | }); 96 | -------------------------------------------------------------------------------- /src/__tests__/range.test.ts: -------------------------------------------------------------------------------- 1 | import { range } from "../range"; 2 | import { toArray } from "../toArray"; 3 | 4 | test("range", async () => { 5 | const result = await toArray( 6 | range(1, 3) 7 | ); 8 | expect(result).toEqual([1, 2]); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/reduce.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { reduce } from "../reduce"; 3 | 4 | test("reduce", async () => { 5 | const result = await reduce( 6 | (accumulator: number, next: number) => accumulator + next, 7 | 0 8 | )(of(1, 2, 3)); 9 | expect(result).toEqual(6); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/scan.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { scan } from "../scan"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("scan", async () => { 6 | const result = await toArray(scan( 7 | (accumulator: number, next: number) => accumulator + next, 8 | 0 9 | )(of(1, 2, 3))); 10 | expect(result).toEqual([0, 1, 3, 6]); 11 | }); 12 | -------------------------------------------------------------------------------- /src/__tests__/skipwhile.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { skipWhile } from "../skipwhile"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("skipWhile", async () => { 6 | const result = await toArray( 7 | skipWhile((value: number) => value < 2)( 8 | of(0, 1, 2, 3, 4, 5, 1, 2, 3, 4) 9 | ) 10 | ); 11 | expect(result).toEqual([2, 3, 4, 5, 1, 2, 3, 4]); 12 | }); 13 | 14 | test("skipWhile", async () => { 15 | const result = await toArray( 16 | skipWhile((value: number) => value > 2)( 17 | of(0, 1, 2, 3, 4, 5, 1, 2, 3, 4) 18 | ) 19 | ); 20 | expect(result).toEqual([0, 1, 2, 3, 4, 5, 1, 2, 3, 4]); 21 | }); 22 | 23 | test("skipWhile", async () => { 24 | const result = await toArray( 25 | skipWhile((value: number) => value <= 2 || value >= 10)( 26 | of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) 27 | ) 28 | ); 29 | expect(result).toEqual([3, 4, 5, 6, 7, 8, 9, 10, 11]); 30 | }); 31 | 32 | test("skipWhile", async () => { 33 | const result = await toArray( 34 | skipWhile((value: number) => value < 2 || value < 5)( 35 | of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) 36 | ) 37 | ); 38 | expect(result).toEqual([5, 6, 7, 8, 9, 10, 11]); 39 | }); 40 | -------------------------------------------------------------------------------- /src/__tests__/some.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { some } from "../some"; 3 | 4 | test("some with inline predicate and with true as a result", async () => { 5 | const result = await some((value: number) => value % 2 === 0)(of(1, 2, 1)); 6 | expect(result).toEqual(true); 7 | }); 8 | 9 | test("some with inline predicate and with false as a result", async () => { 10 | const result = await some((value: number) => value % 2 === 0)(of(1, 3, 5)); 11 | expect(result).toEqual(false); 12 | }); 13 | 14 | test("some with Boolean predicate, number values, and with true as a result", async () => { 15 | const result = await some(Boolean)(of(0, 1, 0)); 16 | expect(result).toEqual(true); 17 | }); 18 | 19 | test("some with Boolean predicate, number values, and with false as a result", async () => { 20 | const result = await some(Boolean)(of(0, 0, 0)); 21 | expect(result).toEqual(false); 22 | }); 23 | 24 | test("some with Boolean predicate, array values, and with true as a result", async () => { 25 | const result = await some(Boolean)(of([], [1, 2, 3], [])); 26 | expect(result).toEqual(true); 27 | }); 28 | 29 | test("some with Boolean predicate, array values, and with false as a result", async () => { 30 | const result = await some(Boolean)(of([], [], [])); 31 | expect(result).toEqual(true); 32 | }); 33 | 34 | test("some with predicate and with true as a result : edge case scenario", async () => { 35 | const result = await some((value: number) => value % 2 === 0)(of(1, 3, 5, 7, 8)); 36 | expect(result).toEqual(true); 37 | }); 38 | 39 | test("some with predicate and with true as a result : edge case scenario", async () => { 40 | const result = await some((value: number) => value % 2 === 0)(of(2, 3, 5, 7, 9)); 41 | expect(result).toEqual(true); 42 | }); 43 | -------------------------------------------------------------------------------- /src/__tests__/spanAll.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { spanAll } from "../spanAll"; 3 | 4 | test("spanAll", async () => { 5 | const iterables = spanAll((value: number) => value % 2 === 0)( 6 | of(1, 2, 3, 4, 5, 6) 7 | ); 8 | const result = []; 9 | for await (const iterable of iterables) { 10 | const innerResult = []; 11 | for await (const item of iterable) { 12 | innerResult.push(item); 13 | } 14 | result.push(innerResult); 15 | } 16 | expect(result).toEqual([[1], [3], [5], []]); 17 | }); 18 | 19 | test("spanAll break", async () => { 20 | const iterables = spanAll((value: number) => value % 2 === 0)( 21 | of(1, 1, 1, 2, 3, 3, 3) 22 | ); 23 | const result = []; 24 | for await (const iterable of iterables) { 25 | for await (const item of iterable) { 26 | result.push(item); 27 | break; 28 | } 29 | } 30 | expect(result).toEqual([1, 3]); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/subject.test.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "../subject"; 2 | 3 | test("Subject", async () => { 4 | const d = new Subject(); 5 | d.onNext(1); 6 | d.onNext(2); 7 | d.onNext(3); 8 | d.onCompleted(); 9 | const result = []; 10 | for await (const item of d.iterator) { 11 | result.push(item); 12 | } 13 | expect(result).toEqual([1, 2, 3]); 14 | }); 15 | 16 | test("Subject", async () => { 17 | const d = new Subject(); 18 | const iterator = d.iterator; 19 | const [, { value, done }] = await Promise.all([d.onNext(1), iterator.next()]); 20 | expect(done).toBeFalsy(); 21 | expect(value).toBe(1); 22 | }); 23 | -------------------------------------------------------------------------------- /src/__tests__/take.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { take } from "../take"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("take", async () => { 6 | const result = await toArray(take(2)(of(1, 2, 3, 4, 5, 6))); 7 | expect(result).toEqual([1, 2]); 8 | }); 9 | -------------------------------------------------------------------------------- /src/__tests__/takeWhile.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { takeWhile } from "../takeWhile"; 3 | import { toArray } from "../toArray"; 4 | 5 | test("takeWhile", async () => { 6 | const result = await toArray(takeWhile(value => value < 3)(of(1, 2, 3, 4, 5, 6))); 7 | expect(result).toEqual([1, 2]); 8 | }); 9 | -------------------------------------------------------------------------------- /src/__tests__/throttle.test.ts: -------------------------------------------------------------------------------- 1 | import { interval } from "../interval"; 2 | import { of } from "../of"; 3 | import { take } from "../take"; 4 | import { throttle } from "../throttle"; 5 | import { toArray } from "../toArray"; 6 | import { wait } from "../wait"; 7 | 8 | test("throttle", async () => { 9 | const result = await toArray( 10 | throttle(() => wait(100))(of(1, 2, 3, 4, 5, 6)) 11 | ); 12 | expect(result).toEqual([1]); 13 | }); 14 | 15 | test("throttle one value", async () => { 16 | const startTime = Date.now(); 17 | const result = await toArray( 18 | throttle(() => wait(100))(of(1)) 19 | ); 20 | const endTime = Date.now(); 21 | expect(result).toEqual([1]); 22 | expect(endTime - startTime).toBeLessThan(100); 23 | }); 24 | 25 | // todo: fix these 26 | 27 | // test("throttle interval", async () => { 28 | // const result = await toArray( 29 | // throttle(() => wait(100))(take(50)(interval(10))) 30 | // ); 31 | // expect(result).toEqual([0, 10, 20, 30, 40]); 32 | // }); 33 | 34 | test("throttle timer count", async () => { 35 | let counter = 0; 36 | const result = await toArray( 37 | throttle(() => { 38 | counter++; 39 | return wait(100); 40 | })(take(50)(interval(10))) 41 | ); 42 | expect(counter).toBeLessThan(10); 43 | }); 44 | 45 | test("throttle increasing timeout", async () => { 46 | const result = await toArray( 47 | throttle(val => wait(200 * (val + 1)))(take(10)(interval(100))) 48 | ); 49 | expect(result).toEqual([0, 2, 8]); 50 | }); 51 | -------------------------------------------------------------------------------- /src/__tests__/toCallbacks.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { toCallbacks } from "../toCallbacks"; 3 | import { wait } from "../wait"; 4 | 5 | test("toCallback", async () => { 6 | let callbackCounter = 0; 7 | const callback = async (result: IteratorResult) => { 8 | // add some race condition when first value appears 9 | if (callbackCounter === 0) { 10 | await wait(10); 11 | } 12 | 13 | expect(result.value).toEqual(callbackCounter); 14 | callbackCounter += 1; 15 | }; 16 | 17 | const values = [0, 1, 2]; 18 | await toCallbacks(callback)(of(...values)); 19 | 20 | expect(callbackCounter).toEqual(values.length); 21 | }); 22 | -------------------------------------------------------------------------------- /src/__tests__/zip.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from "../of"; 2 | import { toArray } from "../toArray"; 3 | import { zip } from "../zip"; 4 | 5 | test("zip", async () => { 6 | const result = await toArray(zip(of(1, 3, 5))(of(2, 4, 6))); 7 | expect(result).toEqual([[1, 2], [3, 4], [5, 6]]); 8 | }); 9 | 10 | test("zip with empty iterable", async () => { 11 | const result = await toArray(zip(of(1, 3, 5))(of())); 12 | expect(result).toEqual([]); 13 | }); 14 | 15 | test("zip empty iterable", async () => { 16 | const result = await toArray(zip(of())(of(2, 4, 6))); 17 | expect(result).toEqual([]); 18 | }); 19 | 20 | test("zip different lengths", async () => { 21 | const result = await toArray(zip(of(1, 3, 5))(of(2, 4))); 22 | expect(result).toEqual([[1, 2], [3, 4]]); 23 | }); 24 | -------------------------------------------------------------------------------- /src/combineLatest.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "./subject"; 2 | import { StopError, toCallbacks } from "./toCallbacks"; 3 | 4 | /** 5 | * Combines iterables 6 | * 7 | * @param sources the iterables to combine 8 | */ 9 | export function combineLatest(...sources: Array>) { 10 | const results = new Array(sources.length); 11 | const resultsAvailable = new Array(sources.length).fill(false); 12 | const subject = new Subject(); 13 | let done = false; 14 | subject.finally(() => { 15 | done = true; 16 | }); 17 | let doneCount = 0; 18 | sources.map((source, index) => { 19 | return toCallbacks(result => { 20 | if (result.done) { 21 | doneCount += 1; 22 | if (doneCount >= sources.length) { 23 | done = true; 24 | return subject.callback({ done: true, value: [] as T[] }); 25 | } 26 | } 27 | if (done) { 28 | throw new StopError(); 29 | } else if (!result.done) { 30 | results[index] = result.value; 31 | resultsAvailable[index] = true; 32 | if (resultsAvailable.every(value => value)) { 33 | return subject.callback({ done: false, value: Array.from(results) }); 34 | } else { 35 | return Promise.resolve(); 36 | } 37 | } else { 38 | return Promise.resolve(); 39 | } 40 | })(source); 41 | }); 42 | return subject.iterator; 43 | } 44 | -------------------------------------------------------------------------------- /src/concat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Concatenates two iterables 3 | */ 4 | export function concat(first: AsyncIterable) { 5 | return async function* inner(second: AsyncIterable) { 6 | for await (const item of first) { 7 | yield item; 8 | } 9 | for await (const item of second) { 10 | yield item; 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/concurrentMap.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from "./deferred"; 2 | import { Subject } from "./subject"; 3 | import { StopError, toCallbacks } from "./toCallbacks"; 4 | 5 | /** 6 | * Runs a mapping function over an asynchronous iterable 7 | */ 8 | export function concurrentMap( 9 | mapper: (t: TFrom) => Promise, 10 | concurrency: number 11 | ) { 12 | return function inner(source: AsyncIterable) { 13 | const subject = new Subject(); 14 | let done = false; 15 | subject.finally(() => { 16 | done = true; 17 | }); 18 | let running = 0; 19 | let deferred = new Deferred(); 20 | toCallbacks(result => { 21 | if (done) { 22 | throw new StopError(); 23 | } 24 | if (!result.done) { 25 | running += 1; 26 | if (running >= concurrency) { 27 | deferred = new Deferred(); 28 | } 29 | mapper(result.value).then(value => { 30 | running -= 1; 31 | subject.onNext(value); 32 | if (running < concurrency) { 33 | deferred.resolve(); 34 | } 35 | }); 36 | return deferred.promise; 37 | } else { 38 | subject.onCompleted(); 39 | return Promise.resolve(); 40 | } 41 | })(source); 42 | return subject.iterator; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/count.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Count the number of items in an async interable 3 | */ 4 | export async function count(source: AsyncIterable): Promise { 5 | let total = 0; 6 | for await (const item of source) { 7 | total++; 8 | } 9 | return total; 10 | } 11 | -------------------------------------------------------------------------------- /src/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Debounce an async iterator. Only emit values when no new value has been received during the provided timer duration. 3 | */ 4 | export function debounce(timer: (value: T) => Promise) { 5 | return async function* inner(source: AsyncIterable) { 6 | const sourceIterator = source[Symbol.asyncIterator](); 7 | let itemPromise = sourceIterator.next(); 8 | let pendingTimerPromise = null; 9 | let item = await itemPromise; 10 | let previousItem = item; 11 | while (!item.done) { 12 | pendingTimerPromise = pendingTimerPromise || timer(item.value); 13 | 14 | const raceResult = await Promise.race([itemPromise, pendingTimerPromise]); 15 | // raceResult is the value of the first promise to resolve. If raceResult 16 | // is not undefined, it _should_ be the result of the itemPromise since 17 | // pendingTimerPromise resolves to void. Just to be sure though, we check 18 | // that the result has the shape of an IteratorResult. 19 | if (raceResult && raceResult.hasOwnProperty("value") && raceResult.hasOwnProperty("done")) { 20 | // itemPromise won the race! 21 | previousItem = item; 22 | item = raceResult; 23 | itemPromise = sourceIterator.next(); 24 | } else { 25 | // pendingTimerPromise won the race! 26 | yield item.value; 27 | pendingTimerPromise = null; 28 | previousItem = item; 29 | item = await itemPromise; 30 | itemPromise = sourceIterator.next(); 31 | } 32 | } 33 | 34 | if (pendingTimerPromise) { 35 | await pendingTimerPromise; 36 | yield previousItem.value; 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/deferred.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A classic deferred 3 | */ 4 | export class Deferred { 5 | 6 | public promise: Promise; 7 | 8 | constructor() { 9 | this.promise = new Promise((resolve, reject) => { 10 | this.resolve = resolve; 11 | this.reject = reject; 12 | }); 13 | } 14 | 15 | public resolve: (value?: T | PromiseLike) => void = (value) => { return; }; 16 | public reject: (reason?: any) => void = () => { return; }; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/distinctUntilChanged.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Only emit when the current value is different than the last. 3 | */ 4 | export function distinctUntilChanged() { 5 | return async function* inner(source: AsyncIterable) { 6 | let previousItem; 7 | for await (const item of source) { 8 | if (previousItem !== item) { 9 | yield item; 10 | } 11 | previousItem = item; 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/every.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If all values pass predicate before completion return true, else false. 3 | */ 4 | export function every(predicate: (t: T) => boolean = () => true) { 5 | return async function inner(source: AsyncIterable) { 6 | let status = true; 7 | for await (const item of source) { 8 | if (! predicate(item)) { 9 | status = false; 10 | } 11 | } 12 | return status; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters items from an iterable 3 | * 4 | * @param source the source iterable to filter 5 | * @param predicate the predicate to apply to filter items 6 | */ 7 | export function filter(predicate: (t: T) => Promise | boolean) { 8 | return async function* inner(source: AsyncIterable) { 9 | for await (const item of source) { 10 | if (await predicate(item)) { 11 | yield await item; 12 | } 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/first.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Take the first value or the first value to pass predicate from an async iterable 3 | */ 4 | export function first( 5 | predicate: (t: T) => boolean = () => true, 6 | ) { 7 | return async function* inner(source: AsyncIterable) { 8 | for await (const item of source) { 9 | if (predicate(item)) { 10 | yield item; 11 | break; 12 | } 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/flatMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Go through the elements of an iterable and return zero or more of them using 3 | * an async iterable. 4 | * 5 | * @param mapper the mapper function to run over the async iterable 6 | */ 7 | export function flatMap(mapper: (t: TFrom) => AsyncIterable) { 8 | return async function* inner(source: AsyncIterable) { 9 | for await (const item of source) { 10 | for await (const nestedItem of mapper(item)) { 11 | yield nestedItem; 12 | } 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/flatten.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Flattens an iterable of iterables 3 | */ 4 | export async function* flatten(source: AsyncIterable>) { 5 | for await (const child of source) { 6 | for await (const item of child) { 7 | yield item; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/from.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts array to an async iterable 3 | */ 4 | export async function* from(values: T[]) { 5 | for (const item of values) { 6 | yield item; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/fromEvent.ts: -------------------------------------------------------------------------------- 1 | import { Subject} from "./subject"; 2 | 3 | export interface IEventSource { 4 | addEventListener: (type: string, callback: (event: T) => void) => void; 5 | removeEventListener: (type: string, callback: any) => void; 6 | } 7 | 8 | export function fromEvent(source: IEventSource, type: string) { 9 | const subject = new Subject(); 10 | const callback = (event: T) => { 11 | subject.onNext(event); 12 | }; 13 | source.addEventListener(type, callback); 14 | subject.finally(() => source.removeEventListener(type, callback)); 15 | return subject.iterator; 16 | } 17 | -------------------------------------------------------------------------------- /src/fromLineReader.ts: -------------------------------------------------------------------------------- 1 | import readline from "readline"; 2 | import { Subject } from "./subject"; 3 | 4 | export function fromLineReader(lineReader: readline.ReadLine) { 5 | const subject = new Subject(); 6 | 7 | lineReader.on("line", (line: string) => subject.onNext(line)); 8 | 9 | lineReader.on("close", () => subject.onCompleted()); 10 | 11 | subject.finally(() => lineReader.close()); 12 | 13 | return subject.iterator; 14 | } 15 | -------------------------------------------------------------------------------- /src/fromNodeStream.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { Subject } from "./subject"; 3 | 4 | export function fromNodeStream(stream: fs.ReadStream) { 5 | const subject = new Subject(); 6 | 7 | stream.on("data", (chunk: T) => subject.onNext(chunk)); 8 | 9 | stream.on("end", () => subject.onCompleted()); 10 | stream.on("error", e => subject.onError(e)); 11 | 12 | return subject.iterator; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Subject } from "./subject"; 2 | export { of } from "./of"; 3 | export { merge } from "./merge"; 4 | export { lookahead } from "./lookahead"; 5 | export { toCallbacks } from "./toCallbacks"; 6 | export { iteratorToIterable } from "./iteratorToIterable"; 7 | export { map } from "./map"; 8 | export { flatMap } from "./flatMap"; 9 | export { interval } from "./interval"; 10 | export { insert } from "./insert"; 11 | export { zip } from "./zip"; 12 | export { toArray } from "./toArray"; 13 | export { filter } from "./filter"; 14 | export { first } from "./first"; 15 | export { last } from "./last"; 16 | export { concat } from "./concat"; 17 | export { fromEvent } from "./fromEvent"; 18 | export { tap } from "./tap"; 19 | export { pipe } from "./pipe"; 20 | export { flatten } from "./flatten"; 21 | export { concurrentMap } from "./concurrentMap"; 22 | export { from } from "./from"; 23 | export { take } from "./take"; 24 | export { takeWhile } from "./takeWhile"; 25 | export { throttle } from "./throttle"; 26 | export { range } from "./range"; 27 | export { count } from "./count"; 28 | export { sum } from "./sum"; 29 | export { debounce } from "./debounce"; 30 | export { skip } from "./skip"; 31 | export { skipWhile } from "./skipWhile"; 32 | export { wait } from "./wait"; 33 | 34 | -------------------------------------------------------------------------------- /src/insert.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inserts values at the start of the iterable 3 | */ 4 | export function insert(...values: T[]) { 5 | return async function* inner(source: AsyncIterable) { 6 | for (const value of values) { 7 | yield value; 8 | } 9 | for await (const item of source) { 10 | yield item; 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/interval.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "./subject"; 2 | 3 | /** 4 | * Emit numbers in sequence 5 | */ 6 | export function interval( 7 | period: number, 8 | timeout?: ((callback: () => void, delay: number) => void) 9 | ) { 10 | const subject = new Subject(); 11 | let counter = 0; 12 | async function inner() { 13 | if (!subject.isDone()) { 14 | await subject.onNext(counter++); 15 | (timeout || setTimeout)(inner, period); 16 | } 17 | } 18 | inner(); 19 | return subject.iterator; 20 | } 21 | -------------------------------------------------------------------------------- /src/iteratorToIterable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to turn an iterator into an iterable 3 | * 4 | * @param iterator An iteratable 5 | */ 6 | export async function* iteratorToIterable(iterator: AsyncIterator) { 7 | while (true) { 8 | const next = await iterator.next(); 9 | if (next.done) { return; } 10 | yield next.value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/last.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Take the last value or the last value to pass predicate from an async iterable 3 | */ 4 | export function last( 5 | predicate: (t: T) => boolean = () => true, 6 | defaultValue?: T, 7 | ) { 8 | return async function* inner(source: AsyncIterable) { 9 | let lastItem = defaultValue; 10 | for await (const item of source) { 11 | if (predicate(item)) { 12 | lastItem = item; 13 | } 14 | } 15 | 16 | if (lastItem) { 17 | yield lastItem; 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/lookahead.ts: -------------------------------------------------------------------------------- 1 | import { insert } from "./insert"; 2 | import { iteratorToIterable } from "./iteratorToIterable"; 3 | 4 | /** 5 | * Lookahead into the async iteratable 6 | * 7 | * @param source The source iterable to look into 8 | * @param howFar How far to look ahead 9 | */ 10 | export function lookahead(howFar: number) { 11 | return async function inner( 12 | source: AsyncIterable, 13 | ): Promise<{ values: T[]; iterable: AsyncIterable }> { 14 | const iterator = source[Symbol.asyncIterator](); 15 | const values: T[] = []; 16 | for (let i = 0; i < howFar; i++) { 17 | const next = await iterator.next(); 18 | if (next.done) { break; } 19 | values.push(next.value); 20 | } 21 | return { 22 | iterable: insert(...values)(iteratorToIterable(iterator)), 23 | values, 24 | }; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs a mapping function over an asynchronous iterable 3 | */ 4 | export function map( 5 | mapper: (t: TFrom, index: number) => Promise | TTo 6 | ) { 7 | return async function* inner(source: AsyncIterable) { 8 | let index = 0; 9 | for await (const item of source) { 10 | yield await mapper(item, index++); 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/merge.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "./subject"; 2 | import { StopError, toCallbacks } from "./toCallbacks"; 3 | 4 | /** 5 | * Merges iterables 6 | * 7 | * @param sources the iterables to merge 8 | */ 9 | export function merge(...sources: Array>) { 10 | const subject = new Subject(); 11 | let done = false; 12 | subject.finally(() => { 13 | done = true; 14 | }); 15 | let doneCount = 0; 16 | sources.map(source => { 17 | return toCallbacks(result => { 18 | if (result.done) { 19 | doneCount += 1; 20 | if (doneCount >= sources.length) { 21 | done = true; 22 | return subject.callback(result); 23 | } 24 | } 25 | if (done) { 26 | throw new StopError(); 27 | } else if (!result.done) { 28 | return subject.callback(result); 29 | } else { 30 | return Promise.resolve(); 31 | } 32 | })(source); 33 | }); 34 | return subject.iterator; 35 | } 36 | -------------------------------------------------------------------------------- /src/of.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param values values to be returned by the async iterable 3 | */ 4 | export async function* of(...values: T[]) { 5 | for (const item of values) { 6 | yield item; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pipes an async iterator through a list of functions that take an async iterator 3 | * as an argument. 4 | * 5 | * @param funcs a series of functions that operate on AsyncIterables 6 | */ 7 | export function pipe( 8 | ...funcs: Array<(iterable: AsyncIterable) => any> 9 | ) { 10 | return function inner(source: AsyncIterable) { 11 | let current = source; 12 | for (const func of funcs) { 13 | current = func(current); 14 | } 15 | return current as AsyncIterable; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/pluck.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Maps the source objects to their values at the path specified 3 | */ 4 | export function pluck(...path: string[]) { 5 | return async function* inner(source: AsyncIterable) { 6 | for await (const item of source) { 7 | let value: any = item; 8 | for (const key of path) { 9 | if (value[key] === undefined) { 10 | value = undefined; 11 | break; 12 | } 13 | value = value[key]; 14 | } 15 | yield value as TTo; 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/range.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an iterable of numbers (positive and/or negative) 3 | * progressing from start up to, but not including, end. A step 4 | * of -1 is used if a negative start is specified without an end 5 | * or step. If end is not specified, it's set to start with start 6 | * then set to 0. 7 | */ 8 | export async function* range( 9 | startOrEnd: number, 10 | end: number, 11 | step: number = 1, 12 | ) { 13 | let actualStart: number; 14 | let actualEnd: number; 15 | if (end === undefined) { 16 | actualStart = 0; 17 | actualEnd = startOrEnd; 18 | } else { 19 | actualStart = startOrEnd; 20 | actualEnd = end; 21 | } 22 | for ( 23 | let i = actualStart; 24 | step > 0 ? i < actualEnd : i > actualEnd; 25 | i += step 26 | ) { 27 | yield i; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/reduce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reduces values 3 | */ 4 | export function reduce(reducer: (accumulator: A, next: T) => Promise | A, init: Promise | A) { 5 | return async function inner(source: AsyncIterable) { 6 | let accumulator = await init; 7 | for await (const next of source) { 8 | accumulator = await reducer(accumulator, next); 9 | } 10 | return accumulator; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/scan.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Goes through a iterable applying the scanner function to the accumulator 3 | * returning the accumulator at each step 4 | */ 5 | export function scan(scanner: (accumulator: A, next: T) => Promise | A, init: Promise | A) { 6 | return async function* inner(source: AsyncIterable) { 7 | let accumulator = await init; 8 | for await (const next of source) { 9 | yield accumulator; 10 | accumulator = await scanner(accumulator, next); 11 | } 12 | yield accumulator; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/skip.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Skip the first numberToSkip values 3 | */ 4 | export function skip(numberToSkip: number) { 5 | return async function* inner(source: AsyncIterable) { 6 | let count = 0; 7 | for await (const item of source) { 8 | if (count++ > numberToSkip) { 9 | yield item; 10 | } 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/skipwhile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Skipwhile emitted values from source until provided expression is false. 3 | */ 4 | export function skipWhile(predicate: (t: T) => boolean = () => true) { 5 | return async function* inner(source: AsyncIterable) { 6 | let skipping = false; 7 | for await (const item of source) { 8 | if (skipping || !predicate(item)) { 9 | skipping = true; 10 | yield item; 11 | } 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/some.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If any values pass predicate before completion return true, else false. 3 | * 4 | * @param predicate the predicate to apply to the source iterable 5 | */ 6 | export function some(predicate: (t: T) => boolean = () => true) { 7 | return async function inner(source: AsyncIterable) { 8 | for await (const item of source) { 9 | if (predicate(item)) { 10 | return true; 11 | } 12 | } 13 | return false; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/spanAll.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "./subject"; 2 | import { StopError, toCallbacks } from "./toCallbacks"; 3 | 4 | export function spanAll(predicate: (t: T) => boolean) { 5 | return function inner(source: AsyncIterable) { 6 | let done = false; 7 | const spanSubject = new Subject>(); 8 | spanSubject.finally(() => { 9 | done = true; 10 | }); 11 | let currentSubject = new Subject(); 12 | spanSubject.onNext(currentSubject.iterator); 13 | toCallbacks(result => { 14 | if (done) { 15 | throw new StopError(); 16 | } 17 | if (result.done) { 18 | currentSubject.onCompleted(); 19 | spanSubject.onCompleted(); 20 | return Promise.resolve(); 21 | } 22 | if (predicate(result.value)) { 23 | currentSubject.onCompleted(); 24 | currentSubject = new Subject(); 25 | spanSubject.onNext(currentSubject.iterator); 26 | } else { 27 | currentSubject.onNext(result.value); 28 | } 29 | // not handling back pressure at the moment 30 | return Promise.resolve(); 31 | })(source); 32 | return spanSubject.iterator; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/subject.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from "./deferred"; 2 | 3 | /** 4 | * The async iterator equivalent of a deferred 5 | */ 6 | export class Subject { 7 | public iterator: AsyncIterableIterator; 8 | 9 | private doneValue = { 10 | done: true, 11 | value: {} as T 12 | }; 13 | 14 | private queue: Array> = []; 15 | private deferreds: Array>> = []; 16 | private done: boolean = false; 17 | private noMoreResults: boolean = false; 18 | private backPressureDeferred = new Deferred(); 19 | private finallyCallbacks: Array<() => void> = []; 20 | private error?: any = undefined; 21 | 22 | constructor() { 23 | const self = this; 24 | 25 | this.iterator = { 26 | throw(e?: any) { 27 | self.done = true; 28 | self.finallyCallbacks.map(cb => cb()); 29 | // fail any waiting deferreds 30 | for (const deferred of self.deferreds) { 31 | deferred.reject(e); 32 | } 33 | return Promise.reject(e); 34 | }, 35 | return(value?: any) { 36 | self.done = true; 37 | self.finallyCallbacks.map(cb => cb()); 38 | // fail any waiting deferreds 39 | for (const deferred of self.deferreds) { 40 | deferred.resolve({ 41 | done: true, 42 | value: {} as T 43 | }); 44 | } 45 | return Promise.resolve(self.doneValue); 46 | }, 47 | next(value?: any) { 48 | if (self.error) { 49 | return Promise.reject(self.error); 50 | } 51 | const queuedItem = self.queue.shift(); 52 | if (self.queue.length === 0) { 53 | self.backPressureDeferred.resolve(); 54 | self.backPressureDeferred = new Deferred(); 55 | } 56 | if (queuedItem !== undefined) { 57 | return Promise.resolve(queuedItem); 58 | } else { 59 | if (self.noMoreResults && !self.done) { 60 | self.done = true; 61 | self.finallyCallbacks.map(cb => cb()); 62 | } 63 | if (self.done) { 64 | return Promise.resolve({ 65 | done: true, 66 | value: {} as T 67 | }); 68 | } 69 | const deferred = new Deferred>(); 70 | self.deferreds.push(deferred); 71 | return deferred.promise; 72 | } 73 | }, 74 | [Symbol.asyncIterator]() { 75 | return this; 76 | } 77 | }; 78 | } 79 | 80 | public finally(callback: () => void) { 81 | this.finallyCallbacks.push(callback); 82 | } 83 | 84 | public onCompleted() { 85 | return this.callback({ done: true, value: {} as T }); 86 | } 87 | 88 | public onNext(value: T) { 89 | return this.callback({ done: false, value }); 90 | } 91 | 92 | public onError(error: any) { 93 | this.error = error; 94 | for (const queuedDeferred of this.deferreds) { 95 | queuedDeferred.reject(error); 96 | } 97 | this.noMoreResults = true; 98 | } 99 | 100 | public isDone() { 101 | return this.done; 102 | } 103 | 104 | public callback(result: IteratorResult) { 105 | if (!(this && this instanceof Subject)) { 106 | const errorMessage = "This must be a Subject. Have you bound this?"; 107 | // tslint:disable-next-line:no-console 108 | console.log(errorMessage); 109 | throw new Error(errorMessage); 110 | } 111 | if (result.done) { 112 | for (const queuedDeferred of this.deferreds) { 113 | queuedDeferred.resolve(result); 114 | } 115 | this.noMoreResults = true; 116 | return Promise.resolve(); 117 | } 118 | const deferred = this.deferreds.pop(); 119 | if (deferred !== undefined) { 120 | deferred.resolve(result); 121 | return Promise.resolve(); 122 | } else { 123 | this.queue.push(result); 124 | return this.backPressureDeferred.promise; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/sum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sums the values of the async iterable 3 | */ 4 | export async function sum(source: AsyncIterable): Promise { 5 | let total = 0; 6 | for await (const item of source) { 7 | total += item; 8 | } 9 | return total; 10 | } 11 | -------------------------------------------------------------------------------- /src/take.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Take the first x values from an async iterable 3 | */ 4 | export function take(numberToTake: number) { 5 | return async function* inner(source: AsyncIterable) { 6 | let index = 0; 7 | for await (const item of source) { 8 | if (index++ >= numberToTake) { 9 | break; 10 | } 11 | yield item; 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/takeWhile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Take while a predicate holds true 3 | */ 4 | export function takeWhile(predicate: (t: T) => boolean) { 5 | return async function* inner(source: AsyncIterable) { 6 | for await (const item of source) { 7 | if (!predicate(item)) { 8 | break; 9 | } 10 | yield item; 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/tap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tap an iterable 3 | */ 4 | export function tap(func: (t: T) => void) { 5 | return async function* inner(source: AsyncIterable) { 6 | for await (const item of source) { 7 | func(item); 8 | yield item; 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/throttle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Throttles an async iterator. Emits a value and then prevents emits until timer has completed. 3 | */ 4 | export function throttle(timer: (value: T) => Promise) { 5 | return async function* inner(source: AsyncIterable) { 6 | let pendingTimerPromise = null; 7 | for await (const item of source) { 8 | if (!pendingTimerPromise) { 9 | yield item; 10 | pendingTimerPromise = timer(item).then(() => { 11 | pendingTimerPromise = null; 12 | }); 13 | } 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/toArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Turn an async iterable to a promise of an array. The promise will resolve 3 | * only when the async iterator returns 4 | * 5 | * @param source an async interable 6 | */ 7 | export async function toArray(source: AsyncIterable) { 8 | const result = []; 9 | for await (const item of source) { 10 | result.push(item); 11 | } 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /src/toCallbacks.ts: -------------------------------------------------------------------------------- 1 | /** Provides a way for callbacks to signal early completion */ 2 | export class StopError extends Error {} 3 | 4 | /** 5 | * Converts an async iterable into a series of callbacks. The function returns 6 | * a promise that resolves when the stream is done 7 | * 8 | * @param callback the callback that gets called for each value 9 | */ 10 | export function toCallbacks( 11 | callback: (result: IteratorResult) => Promise 12 | ) { 13 | return async function inner(source: AsyncIterable) { 14 | const iterator = source[Symbol.asyncIterator](); 15 | while (true) { 16 | const result = await iterator.next(); 17 | try { 18 | await callback(result); 19 | } catch (StopError) { 20 | return; 21 | } 22 | if (result.done) { 23 | return; 24 | } 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/wait.ts: -------------------------------------------------------------------------------- 1 | export function wait(delay: number): Promise { 2 | return new Promise(resolve => { 3 | setTimeout(() => { 4 | resolve(); 5 | }, delay); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/zip.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Zips two iterables 3 | */ 4 | export function zip(first: AsyncIterable) { 5 | return async function* inner(second: AsyncIterable) { 6 | const iterators = [first, second].map(value => 7 | value[Symbol.asyncIterator]() 8 | ); 9 | while (true) { 10 | const [firstNext, secondNext] = await Promise.all( 11 | iterators.map(iterator => iterator.next()) 12 | ); 13 | if (firstNext.done || secondNext.done) { 14 | return; 15 | } 16 | yield [firstNext.value, secondNext.value] as [T, T]; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "outDir": "./es5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "./esnext", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": 5 | "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 6 | "module": 7 | "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 8 | "lib": [ 9 | "esnext", 10 | "dom" 11 | ] /* Specify library files to be included in the compilation. */, 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./lib" /* Redirect output structure to the directory. */, 19 | "rootDir": 20 | "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 30 | "strictNullChecks": true /* Enable strict null checks. */, 31 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 32 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 33 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 34 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { }, 5 | "rules": { "arrow-parens": false, "trailing-comma": false }, 6 | "rulesDirectory": [] 7 | } 8 | --------------------------------------------------------------------------------