├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── lib ├── cancellation.js ├── co-observable.js ├── fast-queue.js ├── index.js ├── no-data-error.js ├── riverify.js ├── shared.js ├── wise-river-base.js └── wise-river.js ├── package.json ├── test ├── 00.setup.js ├── 10.errors.js ├── 11.promise-interface.js ├── 12.low-level-semantics.js ├── 20.static-factory-functions.js ├── 21.is-river.js ├── 30.fork.js ├── 31.map.js ├── 32.for-each.js ├── 33.filter.js ├── 34.distinct.js ├── 35.throttle.js ├── 36.debounce.js ├── 37.timeout-between-each.js ├── 38.while.js ├── 39.until.js ├── 40.consume.js ├── 41.reduce.js ├── 42.all.js ├── 43.find.js ├── 44.includes.js ├── 45.first.js ├── 46.last.js ├── 47.drain.js ├── 48.drop.js ├── 49.stream.js ├── 50.decouple.js ├── 51.async-iterator.js ├── 52.riverify.js └── fast-queue.js └── tools ├── invalid-args.js ├── make-iterable.js └── make-stream.js /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: 12 | - released 13 | workflow_dispatch: {} 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu-latest 21 | - macos-latest 22 | - windows-latest 23 | name: Testing on ${{ matrix.os }} 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: lts/* 30 | - run: npm install 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joshua Wise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # wise-river [![test](https://github.com/WiseLibs/wise-river/actions/workflows/test.yml/badge.svg)](https://github.com/WiseLibs/wise-river/actions/workflows/test.yml) 3 | 4 | Rivers are a style of object streaming (observables) that provide: 5 | - Simple concurrency control 6 | - Automatic resource management 7 | - A familiar, intuitive, and powerful API 8 | - Seamless integration with itself, promises, and the Node.js ecosystem (*see below*) 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install --save wise-river 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```js 19 | const River = require('wise-river'); 20 | 21 | const messages = new River((resolve, reject, write, free) => { 22 | const socket = connectToServer(); 23 | socket.on('data', write); 24 | socket.on('end', resolve); 25 | socket.on('error', reject); 26 | free(() => socket.destroy()); 27 | }); 28 | 29 | messages 30 | .map(parseMessages) 31 | .forEach(logMessages) 32 | .consume(processMessages) 33 | .then(() => console.log('connection ended!')); 34 | ``` 35 | 36 | ## Why rivers? 37 | 38 | Most object streams in Node.js are highly opinionated, and they often don't compose well with promises (the sanctioned asynchronous primitive for JavaScript). Even the streams provided by the standard library exhibit many of these problems: 39 | 1. They usually require subclassing or other boilerplate to accomplish simple tasks 40 | 2. They don't propagate errors, destroying the composability promised by `.pipe()` 41 | 3. Their composability with the rest of the Node.js ecosystem is often weak and limited 42 | 43 | Object streams should *feel* like regular promises, but provide the ability to easily operate on promises as a collection (just like arrays of values). 44 | 45 | ## Unopinionated by default 46 | 47 | Unlike many styles of streams, a river does not preserve sequence/order, allowing for maximum concurrency by default. However, rivers give you total concurrency control, and therefore they can be made to process items in sequence if desired (see [Ordered Rivers](#ordered-rivers)). This flexibility makes accomplishing complicated tasks incredibly easy, making rivers feel very powerful when compared to other types of streams or observables. 48 | 49 | Rivers inherit from the native `Promise` ([`WisePromise`](https://github.com/WiseLibs/wise-promise)). If an error occurs in a river, the river will be rejected, along with all rivers that originate from it. If no error occurs, the river will be fulfilled with `undefined` when all of its items have been consumed. 50 | 51 | Rivers are also [async iterable objects](https://github.com/tc39/proposal-async-iteration), and can be used in `for await` loops. 52 | 53 | ```js 54 | for await (const item of River.from([1, 2, 3])) { 55 | console.log(item); 56 | } 57 | // => 1 58 | // => 2 59 | // => 3 60 | ``` 61 | 62 | ## Automatic resource management 63 | 64 | When a river is done being processed, it has the ability to destroy the underlying resources that the river relied on. If the river was spawned by reading from an existing river ("river chaining"), it can propagate cancellation upstream to the source. You don't need to remember to manually close resource handles—it all happens automatically. Even if you fork a river into multiple consumers, rivers are smart enough to keep the source alive until all consumers are finished. 65 | 66 | # API 67 | 68 | ## new River(*handler*) 69 | 70 | Creates and returns a new river. `handler` must be a function with the following signature: 71 | 72 | `function handler(resolve, reject, write, free)` 73 | 74 | 1. `write(x)` is used to give values (or promises of values) to the river. The river will not be fulfilled until all written values have been consumed. After the river is resolved, this becomes a no-op. 75 | 2. `resolve(x)` behaves the same as with regular promises, except that the fulfillment value of a River is always `undefined`. The river's fulfillment can still be delayed by passing a promise. After invoking this function you cannot `write` any more values to the river. 76 | 3. `reject(x)` behaves the same as with regular promises. After a river is rejected, all processing stops and any values in the river are discarded. 77 | 4. `free(fn)` is used to specify *destructor functions*, which will be invoked when the river is closed (regardless of success or failure). This is for freeing the underlying resources that the river relied on (if any). 78 | 79 | ### .pump([*concurrency*], *callback*) -> *function* 80 | 81 | *This is the most primitive method of a River. All high-level methods are derived from here.* 82 | 83 | Registers the `callback` function to be invoked for each item that enters the river. The callback can return a promise to indicate that it is "processing" the item. If a `concurrency` number is provided, only that many items will be processed at a time. The default is `0` which signifies infinite concurrency. 84 | 85 | If the `callback` throws an exception or returns a rejected promise, the river will stop and will be rejected with the same error. 86 | 87 | Rivers will buffer their content until `pump()` (or a higher-level method of consumption) is used. 88 | 89 | Each river can only have a single consumer. If you try to use `pump()` on the same river twice, a warning will be emitted and the second consumer will never receive any data. In other words, the river will look like an empty river (except to the first consumer). This way, consumers either get "all or nothing" — it's impossible to receive a partial representation of the river's content. 90 | 91 | This method returns a function (`"cancel"`). If `cancel` is called before the river is resolved, the river will be rejected with a `Cancellation` error, which is just a subclass of `Error`. If you're piping the river's content to a *new* river, you should pass `cancel` to the fourth parameter of the River constructor (`free()`). This allows consumers downstream to cancel the river chain if they are no longer interested in it. 92 | 93 | If you try to use `pump()` on same river twice, invocations after the first will return a no-op function; only the *real* consumer has authority over the river's cancellation. 94 | 95 | `Cancellation` is available at `River.Cancellation`. 96 | 97 | ### .fork(*count = 2*) -> *array of rivers* 98 | 99 | Forks a river into several destinations and returns an array of those rivers. By default it will fork into two branches, but you can specify exactly how many branches you want. 100 | 101 | ### .map([*concurrency*], *callback*) -> *river* 102 | 103 | Transforms the river's data through the provided `callback` function, and passes the resulting data to a new river returned by this method. If the `callback` returns a promise, its value will be awaited before being passed to the destination river. 104 | 105 | If a `concurrency` number is provided, only that many items will be processed at a time. The default is `0` which signifies infinite concurrency. 106 | 107 | If the `callback` throws an exception or returns a rejected promise, processing will stop and the river will be rejected with the same error. 108 | 109 | ```js 110 | River.from(['foo.txt', 'bar.txt']) 111 | .map(readFile) 112 | .consume(console.log); 113 | // => "this is bar!" 114 | // => "this is foo!" 115 | ``` 116 | 117 | The `.map()` method also doubles as a stereotypical `flatMap()`. If the `callback` returns a River, its values will be forwarded to the river returned by this method. 118 | 119 | ### .forEach([*concurrency*], *callback*) -> *river* 120 | 121 | Similar to [`.map()`](#mapconcurrency-callback---river), except the river's data will not be changed. If the callback returns a promise, it will still be awaited, but it will not determine the data that is passed to the destination river. This method is primarily used for side effects. 122 | 123 | ### .filter([*concurrency*], *callback*) -> *river* 124 | 125 | Similar to [`.forEach()`](#foreachconcurrency-callback---river), but the items will be filtered by the provided callback function (just like [`Array#filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)). Filtering will occur based on the truthiness of the callback's return value. If the callback returns a promise, its value will be awaited before being used in the filtering process. 126 | 127 | ### .distinct([*equalsFunction*]) -> *river* 128 | 129 | Returns a new river with the same content as the current one, except that it never emits two consecutive items of equal value. By default the `===` operator is used for checking equality, but you can optionally pass in a custom `equalsFunction` to be used instead. 130 | 131 | `equalsFunction` has the signature: `function equals(previousValue, nextValue) -> boolean` 132 | 133 | ### .throttle(*milliseconds*) -> *river* 134 | 135 | Returns a new river that will not emit more than one item every specified number of `milliseconds`. If the river receives data too quickly, some data will be discarded. 136 | 137 | ### .debounce(*milliseconds*) -> *river* 138 | 139 | Returns a new river that will defer its latest data event until the specified number of `milliseconds` has passed since receiving data. If the river receives data too quickly, all data (except the most recent) will be discarded. 140 | 141 | ### .timeoutBetweenEach(*milliseconds*, [*reason*]) -> *river* 142 | 143 | Returns a new river that will be rejected with a `TimeoutError` if the specified number of `milliseconds` passes without the river receiving any new data. The timer starts immediately when this method is invoked. 144 | 145 | If you specify a string `reason`, the `TimeoutError` will have `reason` as its message. Otherwise, a default message will be used. If `reason` is an `instanceof Error`, it will be used instead of a `TimeoutError`. 146 | 147 | `TimeoutError` is available at `River.TimeoutError`. 148 | 149 | ### .while([*concurrency*], *callback*) -> *river* 150 | 151 | Forwards the river's content to a new river until the provided `callback` function returns a falsey value (or a promise for a falsey value), at which point the returned river will be fulfilled and the source river will be cancelled. 152 | 153 | The `callback` will be invoked once for each item that enters the river. 154 | 155 | If the source river is fulfilled or rejected before the `callback` returns a falsey value, the returned river will also be fulfilled or rejected, respectively. 156 | 157 | ### .until(*promise*) -> *river* 158 | 159 | Forwards the river's content to a new river until the given `promise` is fulfilled, at which point the returned river will be fulfilled and the source river will be cancelled. 160 | 161 | If the `promise` is rejected before this river resolves, the returned river will be rejected with the same error. If the source river is fulfilled or rejected before the `promise` resolves, the returned river will also be fulfilled or rejected, respectively. 162 | 163 | ### .decouple() -> *river* 164 | 165 | Returns a new river with the same content as the current one, except that if the river chain downstream is cancelled, the source river will not be cancelled. 166 | 167 | ### .consume([*concurrency*], *callback*) -> *promise* 168 | 169 | Similar to [`.forEach()`](#foreachconcurrency-callback---river), but the river's content is discarded instead of being piped to a new river. This method returns a promise which will be fulfilled or rejected as the river is fulfilled or rejected. 170 | 171 | ```js 172 | new River(infiniteSource) 173 | .consume(processData); 174 | ``` 175 | 176 | ### .reduce(*callback*, [*initialValue*]) -> *promise* 177 | 178 | Applies the `callback` function against an accumulator and each piece of data in the river. This method returns a promise for the final result of the reduction. If no `initialValue` is provided and the river only receives one item, that item will become the fulfillment value without invoking the callback function. If no `initialValue` is provided and the river receives *no* items, the promise will be rejected with a `NoDataError`. 179 | 180 | `NoDataError` is available at `River.NoDataError`. 181 | 182 | If the `initialValue` is a promise, its value will be awaited before starting the reduction process. If the `callback` returns a promise, it will be awaited before processing the next piece of data against the accumulator. Keep in mind that the `callback` function will process data in the order that the river receives it. 183 | 184 | `callback` has the signature: `function callback(accumulator, value)` 185 | 186 | ```js 187 | River.from(['Jonathan', 'Robert', 'Jennifer']) 188 | .map(fetchNickname) 189 | .reduce((a, b) => a + ', ' + b) 190 | .log(); 191 | // => "Jen, John, Rob" 192 | ``` 193 | 194 | ### .all() -> *promise* 195 | 196 | Constructs an array of every item that enters the river, and returns a promise for that array. The items in the array will appear in the order that the river received them. 197 | 198 | ```js 199 | River.from(['a', 'b', 'c']) 200 | .forEach(delayByRandomAmount) 201 | .map(str => str + str) 202 | .all() 203 | .log(); 204 | // => ["bb", "cc", "aa"] 205 | ``` 206 | 207 | ### .find([*concurrency*], *predicate*) -> *promise* 208 | 209 | Returns a promise for the first item in the river to match the `predicate` function. When a match is found, the returned promise will be fulfilled with that item and the river will be cancelled. 210 | 211 | The `predicate` function will be invoked for each item in the river, and should return `true` if it's a match, or `false` otherwise. It can also return a promise for `true` or `false`, instead. 212 | 213 | If the river fulfills but no items matched the `predicate`, the returned promise will be rejected with a `NoDataError`. 214 | 215 | `NoDataError` is available at `River.NoDataError`. 216 | 217 | ### .includes(*value*, [*equalsFunction*]) -> *promise* 218 | 219 | Returns a promise for a boolean that indicates whether or not the given value is found in the stream. If found, the returned promise will be fulfilled with `true` and the river will be cancelled. Otherwise, the returned promise will be fulfilled with `false`. The given `value` can be a promise, in which case its value is awaited before the river is searched. 220 | 221 | By default the `===` operator is used for checking equality, but you can optionally pass in a custom `equalsFunction` to be used instead. 222 | 223 | `equalsFunction` has the signature: `function equals(expectedValue, actualValue) -> boolean` 224 | 225 | ### .first([*number*]) -> *promise* 226 | 227 | If used without any arguments, this method returns a promise for the first item in the river. If the river never received any data, the promise will be rejected with a `NoDataError`. 228 | 229 | If a `number` is provided, the returned promise will instead be fulfilled with an array of the first `number` of items in the river (or less, if the river gets fulfilled without receiving that many items). 230 | 231 | In either case, the river will be cancelled when the returned promise is resolved. 232 | 233 | `NoDataError` is available at `River.NoDataError`. 234 | 235 | ### .last([*number*]) -> *promise* 236 | 237 | If used without any arguments, this method returns a promise for the *last* item in river. If the river never received any data, the promise will be rejected with a `NoDataError`. 238 | 239 | If a `number` is provided, the returned promise will instead be fulfilled with an array of the last `number` of items in the river (or less, if the river gets fulfilled without receiving that many items). 240 | 241 | `NoDataError` is available at `River.NoDataError`. 242 | 243 | ### .drain() -> *promise* 244 | 245 | Rivers cannot be fulfilled until all of their data has been consumed. Sometimes the data is consumed by a new river (such as in [`.map()`](#mapconcurrency-callback---river)), while other times it is consumed by a process for a single value ([`.all()`](#all---promise), [`.reduce()`](#reducecallback-initialvalue---promise)). 246 | 247 | `.drain()` is the simplest method of consumption, simply discarding each item in the river. The returned promise will be fulfilled or rejected as the river is fulfilled or rejected. 248 | 249 | ```js 250 | new River(infiniteSource) 251 | .forEach(processData) 252 | .drain(); 253 | ``` 254 | 255 | ### .drop() -> *this* 256 | 257 | Shorthand for `river.pump(() => {})()`. This method will immediately cancel the river. If the river was previously consumed, this is a no-op; only the *real* consumer has authority over the river's cancellation. 258 | 259 | Keep in mind, if the river does not have a rejection handler, the cancellation will still cause an `unhandledRejection`. Therefore it's common to use this method in conjunction with [`.catchLater()`](https://github.com/WiseLibs/wise-promise#catchlater---this). 260 | 261 | ```js 262 | river.catchLater().drop(); 263 | ``` 264 | 265 | ### *static* River.reject(*reason*) -> *river* 266 | 267 | Returns a new river that is rejected with the given `reason`. 268 | 269 | ### *static* River.never() -> *river* 270 | 271 | Returns a new river that never emits any data and never resolves. 272 | 273 | ### *static* River.empty() -> *river* 274 | 275 | Returns a new river that is already fulfilled and never emits any data. 276 | 277 | ### *static* River.one(*value*) -> *river* 278 | 279 | Returns a new river that will simply emit the given `value` and then become fulfilled. If the given `value` is a promise, it will be awaited before being written to the river. 280 | 281 | ### *static* River.from(*iterable*) -> *river* 282 | 283 | Returns a new river containing the contents of the given `iterable` object. Promises found in the `iterable` object are awaited before being written to the river. 284 | 285 | ### *static* River.every(*milliseconds*) -> *river* 286 | 287 | Constructs a new river that will emit `undefined` upon every interval of `milliseconds`. 288 | 289 | ### *static* River.combine(*...rivers*) -> *river* 290 | 291 | Returns a new river that contains the combination of all the values of all the given rivers. The returned river will not be fulfilled until all the given rivers have been fulfilled. If any of the given rivers are rejected, this river is rejected too. 292 | 293 | You can pass an array of rivers or pass them as individual arguments (or a mix thereof). 294 | 295 | ### *static* River.riverify(*value*, [*options*]) -> *river* 296 | 297 | Converts a [Node.js style stream](https://nodejs.org/api/stream.html) or an [async iterable object](https://github.com/tc39/proposal-async-iteration) to a river. 298 | 299 | Currently, only one option is supported: 300 | - `decouple` 301 | * Setting this option to `true` means the resulting river will not destroy the source when the river becomes fulfilled or rejected/cancelled. This can be useful, for example, when riverifying one side of a duplex stream (since writing in the other direction may still be possible). 302 | 303 | ### *static* River.isRiver(*value*) -> *boolean* 304 | 305 | Returns whether the given value is a river. This is useful for differentiating between rivers and regular promises. 306 | 307 | ### Promise#stream() -> *river* 308 | 309 | After loading this package, [`WisePromise`](https://github.com/WiseLibs/wise-promise) will be augmented with the `.stream()` method, which returns a new river containing the eventual contents of the `iterable` object that the promise is fulfilled with. 310 | 311 | If the promise is fulfilled with something other than an `iterable` object, the river will be rejected with a `TypeError`. 312 | 313 | ## Ordered Rivers 314 | 315 | If you need a river to process its data *in order*, just set its `concurrency` to `1`. 316 | 317 | ```js 318 | new River(source) 319 | .filter(1, sanitizeData) 320 | .map(1, processData) 321 | .forEach(1, saveData) 322 | .drain(); 323 | ``` 324 | 325 | Some methods don't have concurrency control ([`.reduce()`](#reducecallback-initialvalue---promise), [`.distinct()`](#distinctequalsfunction---river), [`.fork()`](#forkcount--2---array-of-rivers), etc.). But don't worry, these methods will maintain order automatically. 326 | 327 | ## License 328 | 329 | [MIT](https://github.com/WiseLibs/wise-river/blob/master/LICENSE) 330 | -------------------------------------------------------------------------------- /lib/cancellation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const descriptor = { writable: true, enumerable: false, configurable: true, value: 'Cancellation' }; 3 | 4 | function Cancellation(message) { 5 | if (new.target !== Cancellation) return new Cancellation(message); 6 | Error.call(this, message); 7 | descriptor.value = '' + message; 8 | Object.defineProperty(this, 'message', descriptor); 9 | Error.captureStackTrace(this, Cancellation); 10 | } 11 | Object.setPrototypeOf(Cancellation.prototype, Error.prototype); 12 | Object.defineProperty(Cancellation.prototype, 'name', descriptor); 13 | 14 | module.exports = Cancellation; 15 | -------------------------------------------------------------------------------- /lib/co-observable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Promise = require('wise-promise'); 3 | const FastQueue = require('./fast-queue'); 4 | const Cancellation = require('./cancellation'); 5 | const shared = require('./shared'); 6 | const exception = { reason: undefined }; 7 | const alreadyResolved = Promise.resolve(); 8 | const handler = Symbol(); 9 | const processing = Symbol(); 10 | const concurrency = Symbol(); 11 | const flush = Symbol(); 12 | const disposers = Symbol(); 13 | const canProcessItem = Symbol(); 14 | const processItem = Symbol(); 15 | 16 | /* 17 | CoObservable is like a WiseRiverBase, except: 18 | - written items cannot be promises (and therefore they are all processed synchronously) 19 | - a function callback is used to indicate an error, instead of the promise interface 20 | - closing the coObservable happens immediately (not deferred), and writing/processing becomes a noop 21 | Just like WiseRiverBase, there is concurrency control (the handler can return promises) 22 | and all items are queued until attachHandler() is called. 23 | */ 24 | class CoObservable extends FastQueue { 25 | 26 | constructor(fn) { 27 | super(fn); 28 | this[handler] = unassigned; 29 | this[processing] = 1 >>> 0; 30 | this[concurrency] = 1 >>> 0; 31 | this[flush] = flushCoObservable(this); 32 | this[shared.onabort] = noop; // ASSERT: Calling this twice must be a noop 33 | this[shared.onflush] = noop; // ASSERT: Calling this after onabort() must be a noop 34 | this[disposers] = undefined; 35 | } 36 | 37 | [shared.write](item) { 38 | if (this[canProcessItem]() && super[shared.isEmpty]()) this[processItem](item); 39 | else super[shared.push](item); 40 | } 41 | 42 | [shared.attachHandler](handlerFunction, concurrencyLimit = 0) { 43 | if (this[handler] !== unassigned) { 44 | if (this[handler] === noop) shared.warn('This river was already resolved', CoObservable.prototype[shared.attachHandler]); 45 | else shared.warn('This river already has a destination (use .fork() instead)', CoObservable.prototype[shared.attachHandler]); 46 | return emptyDisposer; 47 | } 48 | if (this[flush] === noop) { 49 | this[handler] = noop; 50 | return emptyDisposer; 51 | } 52 | if (typeof handlerFunction !== 'function') { 53 | this[handler] = noop; 54 | this[shared.onabort](new TypeError('Expected argument to be a function')); 55 | return emptyDisposer; 56 | } 57 | if (!isUint32(concurrencyLimit)) { 58 | this[handler] = noop; 59 | this[shared.onabort](new TypeError('Expected concurrency to be an integer between 0 and 4294967295')); 60 | return emptyDisposer; 61 | } 62 | this[handler] = handlerFunction; 63 | this[concurrency] = concurrencyLimit >>> 0; 64 | alreadyResolved.then(this[flush]); 65 | return cancelCoObservable(this); 66 | } 67 | 68 | [shared.close]() { 69 | if (this[flush] !== noop) { 70 | super[shared.destroy](); 71 | this[concurrency] = 0 >>> 0; 72 | if (this[handler] !== unassigned) this[handler] = noop; 73 | this[flush] = noop; 74 | this[shared.onabort] = noop; 75 | this[shared.onflush] = noop; 76 | 77 | const xDisposers = this[disposers]; 78 | this[disposers] = undefined; 79 | if (xDisposers !== undefined) { 80 | if (Array.isArray(xDisposers)) { 81 | for (let i = xDisposers.length - 1; i >= 0; --i) tryThrow(xDisposers[i]); 82 | } else { 83 | tryThrow(xDisposers); 84 | } 85 | } 86 | } 87 | } 88 | 89 | [shared.isEmptyAndIdle]() { 90 | // An empty river can be fulfilled even if a handler was never attached (because of `this[handler] === unassigned`) 91 | return (this[processing] === 0 || this[handler] === unassigned) && super[shared.isEmpty](); 92 | } 93 | 94 | [shared.use](fn) { 95 | // ASSERT: fn must always be a function 96 | if (this[flush] === noop) return tryThrow(fn); 97 | const currentDisposers = this[disposers]; 98 | if (currentDisposers === undefined) this[disposers] = fn; 99 | else if (typeof currentDisposers === 'function') this[disposers] = [currentDisposers, fn]; 100 | else currentDisposers.push(fn); 101 | } 102 | 103 | [canProcessItem]() { 104 | return this[concurrency] === 0 || this[processing] !== this[concurrency]; 105 | } 106 | 107 | [processItem](item) { 108 | const ret = tryCatch(this[handler], item); 109 | if (ret === exception) { 110 | this[shared.onabort](exception.reason); 111 | } else if (Promise.isPromise(ret)) { 112 | this[processing] += 1; 113 | Promise.resolve(ret).then(this[flush], this[shared.onabort]); 114 | } 115 | } 116 | } 117 | 118 | const flushCoObservable = (obsv) => () => { 119 | obsv[processing] -= 1; 120 | if (!obsv[shared.isEmpty]()) { 121 | do { obsv[processItem](obsv[shared.shift]()); } 122 | while (obsv[canProcessItem]() && !obsv[shared.isEmpty]()) 123 | } 124 | obsv[shared.onflush](); 125 | }; 126 | 127 | const cancelCoObservable = (obsv) => () => { 128 | if (obsv[shared.onabort] !== noop) { 129 | obsv[shared.onabort](new Cancellation('A consumer downstream has cancelled the river chain')); 130 | } 131 | }; 132 | 133 | const rethrow = (() => { 134 | const hasNextTick = typeof process === 'object' && process !== null && typeof process.nextTick === 'function'; 135 | const schedule = hasNextTick ? process.nextTick : fn => setTimeout(fn, 0); 136 | return ((schedule) => (reason) => { schedule(() => { throw reason; }); })(schedule); 137 | })(); 138 | 139 | const tryThrow = (fn) => { 140 | try { 141 | const ret = fn(); 142 | if (Promise.isPromise(ret)) Promise.resolve(ret).catch(rethrow); 143 | } catch (err) { 144 | rethrow(err); 145 | } 146 | }; 147 | 148 | const tryCatch = (fn, arg) => { 149 | try { return fn(arg); } 150 | catch (err) { exception.reason = err; return exception; } 151 | }; 152 | 153 | const isUint32 = x => Number.isInteger(x) && x >= 0 && x <= 0xffffffff; 154 | const emptyDisposer = () => {}; 155 | const unassigned = () => {}; 156 | const noop = () => {}; 157 | 158 | module.exports = CoObservable; 159 | -------------------------------------------------------------------------------- /lib/fast-queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Promise = require('wise-promise'); 3 | const shared = require('./shared'); 4 | const array = Symbol(); 5 | const length = Symbol(); 6 | const front = Symbol(); 7 | 8 | /* 9 | FastQueue is a dynamically-sized queue implemented with a circular buffer. 10 | Its push() and shift() functions are very simple O(1) calculations. 11 | It performs much better than using a regular array as a queue. 12 | It's a subclass of Promise only because WiseRiverBase must be a subclass 13 | of Promise, and JavaScript does not support multiple inheritance. 14 | */ 15 | class FastQueue extends Promise { 16 | 17 | constructor(fn) { 18 | super(fn); 19 | this[array] = new Array(16); // ASSERT: This must be a power of 2 20 | this[length] = 0; 21 | this[front] = 0; 22 | } 23 | 24 | [shared.push](value) { 25 | const arr = this[array]; 26 | if (arr.length === this[length]) { 27 | arr.length *= 2; 28 | arrayMove(arr, this[length], this[front]); 29 | } 30 | arr[(this[front] + this[length]++) & (arr.length - 1)] = value; 31 | } 32 | 33 | // ASSERT: This must not be invoked if isEmpty() is true 34 | [shared.shift]() { 35 | const arr = this[array]; 36 | const frontIndex = this[front]; 37 | const ret = arr[frontIndex]; 38 | arr[frontIndex] = undefined; 39 | this[front] = (frontIndex + 1) & (arr.length - 1); 40 | this[length] -= 1; 41 | return ret; 42 | } 43 | 44 | [shared.peak]() { 45 | if (this[length] === 0) return; 46 | return this[array][this[front]]; 47 | } 48 | 49 | // ASSERT: The push() and shift() methods must not be invoked after this is invoked 50 | [shared.destroy]() { 51 | this[array] = undefined; 52 | this[length] = 0; 53 | } 54 | 55 | [shared.isEmpty]() { 56 | return this[length] === 0; 57 | } 58 | } 59 | 60 | const arrayMove = (arr, moveBy, len) => { 61 | for (let i = 0; i < len; ++i) { 62 | arr[i + moveBy] = arr[i]; 63 | arr[i] = undefined; 64 | } 65 | }; 66 | 67 | module.exports = FastQueue; 68 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./riverify'); 3 | const Promise = require('wise-promise'); 4 | const WiseRiver = require('./wise-river'); 5 | 6 | const notIterable = x => x == null || typeof x[Symbol.iterator] !== 'function'; 7 | Object.defineProperty(Promise.prototype, 'stream', { 8 | writable: true, 9 | configurable: true, 10 | value: function stream() { 11 | return new WiseRiver((resolve, reject, write) => { 12 | this.then((iterable) => { 13 | try { 14 | if (notIterable(iterable)) throw new TypeError('Expected promise to be resolved with an iterable object'); 15 | for (const item of iterable) write(item); 16 | resolve(); 17 | } catch (reason) { 18 | reject(reason); 19 | } 20 | }, reject); 21 | }); 22 | } 23 | }); 24 | 25 | Promise.River = WiseRiver; 26 | WiseRiver.TimeoutError = Promise.TimeoutError; 27 | WiseRiver.NoDataError = require('./no-data-error'); 28 | WiseRiver.Cancellation = require('./cancellation'); 29 | WiseRiver.Promise = Promise; 30 | 31 | module.exports = WiseRiver; 32 | -------------------------------------------------------------------------------- /lib/no-data-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const descriptor = { writable: true, enumerable: false, configurable: true, value: 'NoDataError' }; 3 | 4 | function NoDataError(message) { 5 | if (new.target !== NoDataError) return new NoDataError(message); 6 | Error.call(this, message); 7 | descriptor.value = '' + message; 8 | Object.defineProperty(this, 'message', descriptor); 9 | Error.captureStackTrace(this, NoDataError); 10 | } 11 | Object.setPrototypeOf(NoDataError.prototype, Error.prototype); 12 | Object.defineProperty(NoDataError.prototype, 'name', descriptor); 13 | 14 | module.exports = NoDataError; 15 | -------------------------------------------------------------------------------- /lib/riverify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Promise = require('wise-promise'); 3 | const WiseRiver = require('./wise-river'); 4 | const FastQueue = require('./fast-queue'); 5 | const NoDataError = require('./no-data-error'); 6 | const { push, shift, peak, destroy, isEmpty, warn } = require('./shared'); 7 | 8 | WiseRiver.riverify = (streamlike, { decouple = false } = {}) => { 9 | if (!isObject(streamlike)) return WiseRiver.reject(new TypeError('Expected argument to be an object')); 10 | if (isNodeStream(streamlike)) { 11 | return new WiseRiver((resolve, reject, write, free) => { 12 | streamlike.addListener('end', resolve); 13 | streamlike.addListener('error', reject); 14 | streamlike.addListener('data', write); 15 | streamlike.addListener('close', () => reject(new NoDataError('The stream was destroyed before finishing'))); 16 | decouple || free(() => { streamlike.destroy(); }); 17 | }); 18 | } 19 | if (isAsyncIterable(streamlike)) { 20 | return new WiseRiver((resolve, reject, write, free) => { 21 | const iterator = streamlike[Symbol.asyncIterator](); 22 | Promise.resolve(iterator.next()).then(function item(record) { 23 | try { 24 | if (record.done) return resolve(); 25 | write(record.value); 26 | Promise.resolve(iterator.next()).then(item, reject); 27 | } catch (reason) { 28 | reject(reason); 29 | } 30 | }, reject); 31 | if (!decouple && typeof iterator.return === 'function') { 32 | free(() => { 33 | // If the river is not cancelled, the iterable will trigger 34 | // its own cleanup without taking part in the river-style 35 | // rethrow mechanism. Therefore, to keep the cleanup process 36 | // somewhat deterministic, the best we can do is report a 37 | // warning should the cleanup process fail. 38 | try { Promise.resolve(iterator.return()).catch(warn); } 39 | catch (reason) { warn(reason); } 40 | }); 41 | } 42 | }); 43 | } 44 | return WiseRiver.reject(new TypeError('Expected argument to be a stream or async iterable object')); 45 | }; 46 | 47 | const asyncIterator = { [Symbol.asyncIterator]() { 48 | let done = false; 49 | let queue = new FastQueue(noop); 50 | const cancel = this.pump((item) => { 51 | if (typeof queue[peak]() === 'function') queue[shift]()(valueRecord(item)); 52 | else queue[push](valueRecord(item)); 53 | }); 54 | this.then(() => { 55 | done = true; 56 | if (typeof queue[peak]() === 'function') { 57 | do { queue[shift]()(doneRecord()); } while (!queue[isEmpty]()) 58 | queue[destroy](); 59 | } 60 | }, (reason) => { 61 | if (done) return; 62 | done = true; 63 | if (typeof queue[peak]() === 'function') { 64 | queue[shift]()(Promise.reject(reason)); 65 | while (!queue[isEmpty]()) queue[shift]()(doneRecord()); 66 | queue[destroy](); 67 | } else { 68 | queue = new FastQueue(noop); 69 | queue[push](Promise.reject(reason).catchLater()); 70 | } 71 | }); 72 | return { 73 | next() { 74 | if (typeof queue[peak]() === 'object') return Promise.resolve(queue[shift]()); 75 | if (done) return (queue[destroy](), Promise.resolve(doneRecord())); 76 | return new Promise((resolve) => { queue[push](resolve); }); 77 | }, 78 | return() { 79 | done = true; 80 | if (typeof queue[peak]() === 'function') { 81 | do { queue[shift]()(doneRecord()); } while (!queue[isEmpty]()) 82 | } 83 | queue[destroy](); 84 | cancel(); 85 | return Promise.resolve(doneRecord()); 86 | }, 87 | }; 88 | } }; 89 | 90 | const supportsAsyncIterators = typeof Symbol.asyncIterator === 'symbol'; 91 | const isObject = x => x != null && (typeof x === 'object' || typeof x === 'function'); 92 | const isNodeStream = x => x.readable === true && typeof x.pipe === 'function' && typeof x.addListener === 'function' && typeof x.destroy === 'function'; 93 | const isAsyncIterable = x => supportsAsyncIterators && typeof x[Symbol.asyncIterator] === 'function'; 94 | const valueRecord = value => ({ value, done: false }); 95 | const doneRecord = () => ({ value: undefined, done: true }); 96 | const noop = () => {}; 97 | 98 | if (supportsAsyncIterators) { 99 | Object.defineProperty(WiseRiver.prototype, Symbol.asyncIterator, { 100 | writable: true, 101 | configurable: true, 102 | value: asyncIterator[Symbol.asyncIterator], 103 | }); 104 | } 105 | asyncIterator[Symbol.asyncIterator] = undefined; 106 | -------------------------------------------------------------------------------- /lib/shared.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // FastQueue 4 | exports.push = Symbol(); 5 | exports.shift = Symbol(); 6 | exports.peak = Symbol(); 7 | exports.destroy = Symbol(); 8 | exports.isEmpty = Symbol(); 9 | 10 | // CoObservable 11 | exports.write = Symbol(); 12 | exports.attachHandler = Symbol(); 13 | exports.close = Symbol(); 14 | exports.isEmptyAndIdle = Symbol(); 15 | exports.use = Symbol(); 16 | exports.onabort = Symbol(); 17 | exports.onflush = Symbol(); 18 | 19 | // Warnings 20 | const emitWarning = (() => { 21 | const hasEmitWarning = typeof process === 'object' && process !== null && typeof process.emitWarning === 'function'; 22 | return hasEmitWarning ? process.emitWarning : msg => console.warn(msg); 23 | })(); 24 | const warningHolder = { name: '', message: '' }; 25 | exports.warn = (message, from) => { 26 | if (from !== undefined && typeof Error.captureStackTrace === 'function') { 27 | warningHolder.message = message; 28 | Error.captureStackTrace(warningHolder, from); 29 | message = warningHolder.stack; 30 | } else if (message instanceof Error && message.stack) { 31 | message = message.stack; 32 | } 33 | emitWarning(message); 34 | }; 35 | -------------------------------------------------------------------------------- /lib/wise-river-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Promise = require('wise-promise'); 3 | const CoObservable = require('./co-observable'); 4 | const shared = require('./shared'); 5 | const exception = { reason: undefined }; 6 | 7 | /* 8 | WiseRiverBase implements the low-level interface of WiseRiver. 9 | All other methods of WiseRiver are derived from methods in this class. 10 | */ 11 | class WiseRiverBase extends CoObservable { 12 | 13 | constructor(fn) { 14 | if (typeof fn !== 'function') throw new TypeError(`River resolver (${fn === null ? 'null' : typeof fn}) is not a function`); 15 | let xrs, xrj, open = true, racing = 0; 16 | super((rs, rj) => { xrs = rs; xrj = rj; }); 17 | 18 | const reject = (reason) => { open = false; xrj(reason); super[shared.close](); }; 19 | 20 | const resolve = (defer) => { 21 | if (open) { 22 | open = false; 23 | const close = () => { 24 | if (this[shared.onabort] === reject) { 25 | const finalize = () => { 26 | if (super[shared.isEmptyAndIdle]() && racing === 0) { 27 | xrs(); 28 | super[shared.close](); 29 | } 30 | }; 31 | finalize(); 32 | if (this[shared.onabort] === reject) { 33 | this[shared.onflush] = finalize; 34 | } 35 | } 36 | }; 37 | if (Promise.isPromise(defer)) Promise.resolve(defer).then(close, reject); 38 | else close(); 39 | } 40 | }; 41 | 42 | const superWrite = (item) => { 43 | racing -= 1; 44 | super[shared.write](item); 45 | this[shared.onflush](); 46 | }; 47 | 48 | const write = (item) => { 49 | if (open) { 50 | if (Promise.isPromise(item)) { 51 | racing += 1; 52 | Promise.resolve(item).then(superWrite, reject); 53 | } else { 54 | super[shared.write](item); 55 | } 56 | } else if (Promise.isPromise(item)) { 57 | Promise.resolve(item).catchLater(); 58 | } 59 | }; 60 | 61 | const free = (fn) => { 62 | if (typeof fn === 'function') super[shared.use](fn); 63 | }; 64 | 65 | this[shared.onabort] = reject; 66 | 67 | if (tryCatch(fn, resolve, reason => void(open && reject(reason)), write, free) === exception) { 68 | reject(exception.reason); 69 | } 70 | } 71 | 72 | pump(concurrency, handler) { 73 | if (typeof concurrency === 'function') { handler = arguments[0]; concurrency = arguments[1]; } 74 | return super[shared.attachHandler](handler, concurrency); 75 | } 76 | } 77 | 78 | const tryCatch = (fn, arg1, arg2, arg3, arg4) => { 79 | try { fn(arg1, arg2, arg3, arg4); } 80 | catch (err) { exception.reason = err; return exception; } 81 | }; 82 | 83 | module.exports = WiseRiverBase; 84 | -------------------------------------------------------------------------------- /lib/wise-river.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Promise = require('wise-promise'); 3 | const BaseClass = require('./wise-river-base'); 4 | const NoDataError = require('./no-data-error'); 5 | const privateSymbol = require('./shared').onabort; 6 | const TimeoutError = Promise.TimeoutError; 7 | const unassigned = Symbol(); 8 | 9 | class WiseRiver extends BaseClass { 10 | 11 | static get resolve() { return undefined; } 12 | static get all() { return undefined; } 13 | static get race() { return undefined; } 14 | static get any() { return undefined; } 15 | static get settle() { return undefined; } 16 | static get props() { return undefined; } 17 | static get after() { return undefined; } 18 | 19 | static reject(x) { return new WiseRiver((_, r) => { r(x); }); } 20 | static never() { return new WiseRiver(noop); } 21 | static empty() { return new WiseRiver(invoke); } 22 | static one(x) { return new WiseRiver((r, _, w) => { w(x); r(); }); } 23 | 24 | static from(iterable) { 25 | return new WiseRiver((resolve, reject, write) => { 26 | if (notIterable(iterable)) return reject(new TypeError('Expected argument to be an iterable object')); 27 | for (const item of iterable) write(item); 28 | resolve(); 29 | }); 30 | } 31 | 32 | static every(ms) { 33 | return new WiseRiver((resolve, reject, write, free) => { 34 | const timer = setInterval(write, ~~ms); 35 | free(() => { clearInterval(timer); }); 36 | }); 37 | } 38 | 39 | static combine(...args) { 40 | return new WiseRiver((resolve, reject, write, free) => { 41 | const rivers = []; 42 | for (const arg of args) { 43 | if (notIterable(arg)) rivers.push(arg); 44 | else rivers.push(...arg); 45 | } 46 | for (const river of rivers) { 47 | if (WiseRiver.isRiver(river)) free(river.pump(write)); 48 | } 49 | Promise.all(rivers).then(resolve, reject); 50 | }); 51 | } 52 | 53 | static isRiver(value) { 54 | return value != null && hasOwnProperty.call(value, privateSymbol); 55 | } 56 | 57 | fork(count = 2) { 58 | if (!validCount(count)) throw new TypeError('Expected count to be an integer between 1 and 4294967295'); 59 | const forks = new Array(count); 60 | const inners = new Array(count); 61 | const cancel = cancelForForks(count, pumpForForks(this, inners)); 62 | const assignInner = (resolve, reject, write, free) => { 63 | inners[i] = { resolve, reject, write }; 64 | free(cancel); 65 | }; 66 | let i = 0; 67 | for (; i < count; ++i) forks[i] = new WiseRiver(assignInner); 68 | return forks; 69 | } 70 | 71 | map(concurrency, handler) { 72 | return new WiseRiver((resolve, reject, write, free) => { 73 | if (typeof concurrency === 'function') { handler = arguments[0]; concurrency = arguments[1]; } 74 | free(this.pump(concurrency, upgrade(handler, (item) => { 75 | const mapped = handler(item); 76 | if (!Promise.isPromise(mapped)) return write(mapped); 77 | if (WiseRiver.isRiver(mapped)) { free(mapped.pump(write)); return mapped; } 78 | return Promise.resolve(mapped).then(write); 79 | }))); 80 | this.then(resolve, reject); 81 | }); 82 | } 83 | 84 | forEach(concurrency, handler) { 85 | return new WiseRiver((resolve, reject, write, free) => { 86 | if (typeof concurrency === 'function') { handler = arguments[0]; concurrency = arguments[1]; } 87 | free(this.pump(concurrency, upgrade(handler, (item) => { 88 | const defer = handler(item); 89 | if (!Promise.isPromise(defer)) return write(item); 90 | return Promise.resolve(defer).then(() => write(item)); 91 | }))); 92 | this.then(resolve, reject); 93 | }); 94 | } 95 | 96 | filter(concurrency, handler) { 97 | return new WiseRiver((resolve, reject, write, free) => { 98 | if (typeof concurrency === 'function') { handler = arguments[0]; concurrency = arguments[1]; } 99 | free(this.pump(concurrency, upgrade(handler, (item) => { 100 | const bool = handler(item); 101 | if (!Promise.isPromise(bool)) { if (bool) write(item); return; } 102 | return Promise.resolve(bool).then((bool) => { if (bool) write(item); }); 103 | }))); 104 | this.then(resolve, reject); 105 | }); 106 | } 107 | 108 | distinct(equals = strictEquals) { 109 | return new WiseRiver((resolve, reject, write, free) => { 110 | let lastItem = unassigned; 111 | free(this.pump(0, upgrade(equals, (item) => { 112 | if (lastItem === unassigned || !equals(lastItem, item)) write(item); 113 | lastItem = item; 114 | }))); 115 | this.then(resolve, reject); 116 | }); 117 | } 118 | 119 | throttle(ms) { 120 | return new WiseRiver((resolve, reject, write, free) => { 121 | const writeAndThrottle = () => { 122 | timer = setTimeout(whenAvailable, delay); 123 | write(lastItem); 124 | lastItem = unassigned; 125 | }; 126 | const whenAvailable = () => { 127 | if (timer !== undefined) { 128 | if (lastItem !== unassigned) writeAndThrottle(); 129 | else timer = undefined; 130 | } else { 131 | write(lastItem); 132 | resolve(); 133 | } 134 | }; 135 | const delay = ~~ms; 136 | let lastItem; 137 | let timer; 138 | free(this.pump((item) => { 139 | lastItem = item; 140 | if (timer === undefined) writeAndThrottle(); 141 | })); 142 | this.then(() => { 143 | if (timer === undefined) resolve(); 144 | else if (lastItem === unassigned) { clearTimeout(timer); resolve(); } 145 | else timer = undefined; 146 | }, (reason) => { 147 | clearTimeout(timer); 148 | reject(reason); 149 | }); 150 | }); 151 | } 152 | 153 | debounce(ms) { 154 | return new WiseRiver((resolve, reject, write, free) => { 155 | const writeLastItem = () => { 156 | write(lastItem); 157 | lastItem = undefined; 158 | if (timer !== undefined) timer = undefined; 159 | else resolve(); 160 | }; 161 | const delay = ~~ms; 162 | let lastItem; 163 | let timer; 164 | free(this.pump((item) => { 165 | lastItem = item; 166 | clearTimeout(timer); 167 | timer = setTimeout(writeLastItem, delay); 168 | })); 169 | this.then(() => { 170 | if (timer === undefined) resolve(); 171 | else timer = undefined; 172 | }, (reason) => { 173 | clearTimeout(timer); 174 | reject(reason); 175 | }); 176 | }); 177 | } 178 | 179 | timeoutBetweenEach(ms, reason) { 180 | return new WiseRiver((resolve, reject, write, free) => { 181 | const delay = ~~ms; 182 | let fail = () => { 183 | reject( 184 | reason == null ? new TimeoutError(`The river timed out after ${delay > 0 ? delay : 0}ms`) 185 | : reason instanceof Error ? reason : new TimeoutError(String(reason)) 186 | ); 187 | fail = noop; 188 | }; 189 | let timer; 190 | free(this.pump((item) => { 191 | if (fail !== noop) { 192 | clearTimeout(timer); 193 | timer = setTimeout(fail, delay); 194 | write(item); 195 | } 196 | })); 197 | this.then(() => { clearTimeout(timer); resolve(); }, 198 | (reason) => { clearTimeout(timer); reject(reason); }); 199 | timer = setTimeout(fail, delay); 200 | }); 201 | } 202 | 203 | while(concurrency, handler) { 204 | return new WiseRiver((resolve, reject, write, free) => { 205 | if (typeof concurrency === 'function') { handler = arguments[0]; concurrency = arguments[1]; } 206 | const cancel = this.pump(concurrency, upgrade(handler, (item) => { 207 | const bool = handler(item); 208 | if (!Promise.isPromise(bool)) { 209 | if (bool) write(item); 210 | else { resolve(); cancel(); } 211 | return; 212 | } 213 | return Promise.resolve(bool).then((bool) => { 214 | if (bool) write(item); 215 | else { resolve(); cancel(); } 216 | }); 217 | })); 218 | free(cancel); 219 | this.then(resolve, reject); 220 | }); 221 | } 222 | 223 | until(promise) { 224 | return new WiseRiver((resolve, reject, write, free) => { 225 | const cancel = this.pump(write); 226 | free(cancel); 227 | Promise.resolve(promise).then(() => { resolve(); cancel(); }, reject); 228 | this.then(resolve, reject); 229 | }); 230 | } 231 | 232 | decouple() { 233 | return new WiseRiver((resolve, reject, write) => { 234 | this.pump(write); 235 | this.then(resolve, reject); 236 | }); 237 | } 238 | 239 | consume(concurrency, handler) { 240 | return new Promise((resolve, reject) => { 241 | this.pump(concurrency, handler); 242 | this.then(resolve, reject); 243 | }); 244 | } 245 | 246 | reduce(handler, result) { 247 | return new Promise((resolve, reject) => { 248 | if (Promise.isPromise(result)) result = Promise.resolve(result).catchLater(); 249 | let firstItem = true; 250 | const calc = (a, b) => { 251 | const ret = handler(a, b); 252 | if (!Promise.isPromise(ret)) { result = ret; return; } 253 | return Promise.resolve(ret).then((ret) => { result = ret; }); 254 | }; 255 | this.pump(1, upgrade(handler, (item) => { 256 | if (firstItem) { 257 | firstItem = false; 258 | if (arguments.length < 2) { result = item; return; } 259 | if (!(result instanceof Promise)) return calc(result, item); 260 | return result.then(seed => calc(seed, item)); 261 | } 262 | return calc(result, item); 263 | })); 264 | this.then(() => { 265 | if (!firstItem || arguments.length >= 2) resolve(result); 266 | else reject(new NoDataError('Cannot reduce an empty river with no initial value')); 267 | }, reject); 268 | }); 269 | } 270 | 271 | all() { 272 | return new Promise((resolve, reject) => { 273 | const result = []; 274 | this.pump((item) => { result.push(item); }); 275 | this.then(() => resolve(result), reject); 276 | }); 277 | } 278 | 279 | find(concurrency, handler) { 280 | return new Promise((resolve, reject) => { 281 | if (typeof concurrency === 'function') { handler = arguments[0]; concurrency = arguments[1]; } 282 | const cancel = this.pump(concurrency, upgrade(handler, (item) => { 283 | const bool = handler(item); 284 | if (!Promise.isPromise(bool)) { 285 | if (bool) { resolve(item); cancel(); } 286 | return; 287 | } 288 | return Promise.resolve(bool).then((bool) => { 289 | if (bool) { resolve(item); cancel(); } 290 | }); 291 | })); 292 | this.then(() => reject(new NoDataError('No matching data found in the river')), reject); 293 | }); 294 | } 295 | 296 | includes(value, equals = strictEquals) { 297 | return new Promise((resolve, reject) => { 298 | const search = (value) => { 299 | const cancel = this.pump(0, upgrade(equals, (item) => { 300 | if (equals(value, item)) { resolve(true); cancel(); } 301 | })); 302 | this.then(() => resolve(false), reject); 303 | }; 304 | if (!Promise.isPromise(value)) return search(value); 305 | Promise.resolve(value).then(search, (reason) => { 306 | this.pump(noop)(); 307 | reject(reason); 308 | }); 309 | }); 310 | } 311 | 312 | first(count) { 313 | return new Promise((resolve, reject) => { 314 | if (arguments.length === 0) { 315 | const cancel = this.pump((item) => { resolve(item); cancel(); }); 316 | this.then(() => reject(new NoDataError('The river never received any data')), reject); 317 | } else if (validCount(count)) { 318 | let length = 0; 319 | const cancel = this.pump((item) => { 320 | result[length] = item; 321 | if (++length === result.length) { resolve(result); cancel(); } 322 | }); 323 | this.then(() => resolve(result.slice(0, length)), reject); 324 | const result = new Array(count); 325 | } else { 326 | this.pump(noop)(); 327 | this.then(undefined, noop); 328 | reject(new TypeError('Expected count to be an integer between 1 and 4294967295')); 329 | } 330 | }); 331 | } 332 | 333 | last(count) { 334 | return new Promise((resolve, reject) => { 335 | if (arguments.length === 0) { 336 | let lastItem = unassigned; 337 | this.pump((item) => { lastItem = item; }); 338 | this.then(() => { 339 | if (lastItem === unassigned) reject(new NoDataError('The river never received any data')); 340 | else resolve(lastItem); 341 | }, reject); 342 | } else if (validCount(count)) { 343 | let start = 0; 344 | let length = 0; 345 | this.pump((item) => { 346 | if (length === arr.length) { 347 | arr[start] = item; 348 | start += 1; 349 | if (start === length) start = 0; 350 | } else { 351 | arr[length] = item; 352 | length += 1; 353 | } 354 | }); 355 | this.then(() => { 356 | if (start === 0) { 357 | if (length === arr.length) resolve(arr); 358 | else resolve(arr.slice(0, length)); 359 | return; 360 | } 361 | const result = new Array(length); 362 | const len = length - start; 363 | for (let i = 0; i < len; ++i) result[i] = arr[i + start]; 364 | for (let i = len; i < length; ++i) result[i] = arr[i - len]; 365 | resolve(result); 366 | }, reject); 367 | const arr = new Array(count); 368 | } else { 369 | this.pump(noop)(); 370 | this.then(undefined, noop); 371 | reject(new TypeError('Expected count to be an integer between 1 and 4294967295')); 372 | } 373 | }); 374 | } 375 | 376 | drain() { 377 | return this.consume(noop); 378 | } 379 | 380 | drop() { 381 | this.pump(noop)(); 382 | return this; 383 | } 384 | 385 | stream() { 386 | return this; 387 | } 388 | } 389 | 390 | const pumpForForks = (river, forks) => { 391 | const count = forks.length; 392 | river.then(() => { 393 | for (let i = 0; i < count; ++i) forks[i].resolve(); 394 | }, (reason) => { 395 | for (let i = 0; i < count; ++i) forks[i].reject(reason); 396 | }); 397 | return river.pump((item) => { 398 | for (let i = 0; i < count; ++i) forks[i].write(item); 399 | }); 400 | }; 401 | 402 | const cancelForForks = (count, cancel) => { 403 | let i = 0; 404 | return () => { if (++i === count) cancel(); }; 405 | }; 406 | 407 | const { hasOwnProperty } = Object.prototype; 408 | const notIterable = x => x == null || typeof x[Symbol.iterator] !== 'function'; 409 | const upgrade = (original, wrapper) => typeof original !== 'function' ? original : wrapper; 410 | const validCount = x => Number.isInteger(x) && x >= 1 && x <= 0xffffffff; 411 | const strictEquals = (a, b) => a === b; 412 | const invoke = fn => fn(); 413 | const noop = () => {}; 414 | 415 | module.exports = WiseRiver; 416 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wise-river", 3 | "version": "1.0.1", 4 | "description": "Object streaming the way it should be", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha --no-warnings --bail --timeout 1000 --slow 99999" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/WiseLibs/wise-river.git" 12 | }, 13 | "keywords": [ 14 | "observables", 15 | "async", 16 | "iterable", 17 | "stream", 18 | "promise", 19 | "concurrency" 20 | ], 21 | "author": "Joshua Wise ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/WiseLibs/wise-river/issues" 25 | }, 26 | "homepage": "https://github.com/WiseLibs/wise-river#readme", 27 | "dependencies": { 28 | "wise-promise": ">=1.1.0" 29 | }, 30 | "devDependencies": { 31 | "chai": "^4.1.1", 32 | "chai-as-promised": "^7.1.1", 33 | "mocha": "^3.5.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/00.setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('chai').use(require('chai-as-promised')); 3 | process.on('unhandledRejection', (err) => { throw err; }); 4 | -------------------------------------------------------------------------------- /test/10.errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | 4 | ['NoDataError', 'Cancellation'].forEach((name) => { 5 | const CustomError = require('../.')[name]; 6 | describe('class ' + name, function () { 7 | it('should be a subclass of Error', function () { 8 | expect(CustomError).to.be.a('function'); 9 | expect(CustomError.prototype).to.be.an.instanceof(Error); 10 | expect(CustomError).to.not.equal(Error); 11 | }); 12 | it('should use regular Error properties', function () { 13 | const error = new CustomError('foobar'); 14 | expect(error.message).to.equal('foobar'); 15 | expect(error.name).to.equal(name); 16 | expect(typeof error.stack).to.equal(typeof (new Error('baz').stack)); 17 | }); 18 | it('should be callable as a function', function () { 19 | const error = CustomError('foobarbaz'); 20 | expect(error.message).to.equal('foobarbaz'); 21 | expect(error.name).to.equal(name); 22 | expect(typeof error.stack).to.equal(typeof (Error('qux').stack)); 23 | }); 24 | it('should have the same property descriptors as a regular Error', function () { 25 | const getOwnPropertyDescriptors = (obj) => { 26 | const ret = {}; 27 | for (const key of Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))) { 28 | ret[key] = Object.getOwnPropertyDescriptor(obj, key); 29 | } 30 | return ret; 31 | }; 32 | const aObject = Error('qux'); 33 | const bObject = CustomError('qux'); 34 | const a = getOwnPropertyDescriptors(aObject); 35 | const b = getOwnPropertyDescriptors(bObject); 36 | const aStack = (a.stack.value || a.stack.get.call(aObject)).split('\n')[0]; 37 | const bStack = (b.stack.value || b.stack.get.call(bObject)).split('\n')[0]; 38 | expect(bStack.replace(name, 'Error')).to.equal(aStack); 39 | expect(aStack).to.equal(String(aObject)); 40 | expect(bStack).to.equal(String(bObject)); 41 | a.stack.value = ''; 42 | b.stack.value = ''; 43 | expect(a).to.deep.equal(b); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/11.promise-interface.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('Promise interface', function () { 6 | it('should inherit from the native Promise', function () { 7 | expect(River.prototype).to.be.an.instanceof(Promise); 8 | expect(new River(() => {})).to.be.an.instanceof(Promise); 9 | }); 10 | it('should mirror native promises when a resolver function is not given', function () { 11 | expect(() => new Promise()).to.throw(TypeError); 12 | expect(() => new Promise(123)).to.throw(TypeError); 13 | expect(() => new Promise({})).to.throw(TypeError); 14 | expect(() => new River()).to.throw(TypeError); 15 | expect(() => new River(123)).to.throw(TypeError); 16 | expect(() => new River({})).to.throw(TypeError); 17 | }) 18 | it('should not return a river when using .then()', function () { 19 | const river = new River(() => {}); 20 | const promise = river.then(); 21 | expect(promise).to.not.be.an.instanceof(River); 22 | expect(promise).to.be.an.instanceof(Promise); 23 | }); 24 | it('should not allow the use of confusing inherited static methods', function () { 25 | expect(() => River.resolve()).to.throw(TypeError); 26 | expect(() => River.all([])).to.throw(TypeError); 27 | expect(() => River.race([])).to.throw(TypeError); 28 | expect(() => River.any([])).to.throw(TypeError); 29 | expect(() => River.settle([])).to.throw(TypeError); 30 | expect(() => River.props({})).to.throw(TypeError); 31 | expect(() => River.after(1)).to.throw(TypeError); 32 | }); 33 | it('should always be fulfilled with undefined', function () { 34 | return expect(new River((resolve, _) => resolve(123))) 35 | .to.become(undefined); 36 | }); 37 | it('should be rejected like regular promises', function () { 38 | const err = new Error('foobar'); 39 | return Promise.all([ 40 | expect(new River((_, reject) => reject(err))).to.be.rejectedWith(err), 41 | expect(new River((_, __) => { throw err; })).to.be.rejectedWith(err) 42 | ]); 43 | }); 44 | it('should hang pending like regular promises', function (done) { 45 | const fail = () => { done(new Error('This river should not have been resolved')); } 46 | new River(() => {}).then(fail, fail); 47 | setTimeout(done, 50); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/12.low-level-semantics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const invalidArgs = require('../tools/invalid-args'); 5 | 6 | const alphabetResolver = ((resolve, _, write) => { 7 | setTimeout(() => { write('f'); resolve(); }, 20); 8 | setImmediate(() => write('e')); 9 | Promise.resolve().then(() => write('d')); 10 | process.nextTick(() => write('c')); 11 | write('a'); 12 | write('b'); 13 | }); 14 | 15 | describe('Low-level semantics (constructor and .pump())', function () { 16 | it('should be a noop if the river is already resolved', function () { 17 | const err = new Error('foobar'); 18 | const r1 = new River((r) => r()); 19 | const r2 = new River((_, r) => r(err)); 20 | expect(r1.pump()).to.be.a('function'); 21 | expect(r2.pump()).to.be.a('function'); 22 | return expect(Promise.all([r1, r2.catch(x => x)])).to.become([undefined, err]); 23 | }); 24 | it('should reject the river if invalid arguments are given', function () { 25 | const pumped = (args) => { const r = new River(() => {}); expect(r.pump(...args)).to.be.a('function'); return r; } 26 | return Promise.all(invalidArgs().map(args => expect(pumped(args)).to.be.rejectedWith(TypeError))); 27 | }); 28 | it('should return a function that will cancel the river', function () { 29 | const err = new Error('foobar'); 30 | const cancelled = (fn) => { const r = new River(fn); r.pump(() => {})(); return r; } 31 | return Promise.all([ 32 | expect(cancelled(() => {})).to.be.rejectedWith(River.Cancellation), 33 | expect(cancelled((_, r) => r(err))).to.be.rejectedWith(err), 34 | expect(cancelled((r) => r())).to.become(undefined) 35 | ]); 36 | }); 37 | it('should feed written items into the registered handler, in order', function () { 38 | const river = new River(alphabetResolver); 39 | let str = ''; 40 | expect(river.pump(item => str += item)).to.be.a('function'); 41 | return expect(river.then(() => str)).to.become('abcdef'); 42 | }); 43 | it('should emit a warning and return a noop function if called more than once', function () { 44 | let warnings = 0; 45 | const onWarning = () => { warnings += 1; }; 46 | process.on('warning', onWarning); 47 | const err = new Error('foobar'); 48 | const pumped = (a, b, c, d) => { const r = new River((r) => setImmediate(r)); expect(r.pump(a, b)).to.be.a('function'); r.pump(c, d)(); return r; } 49 | const cancelled = (fn) => { const r = new River(fn); expect(r.pump(() => {})).to.be.a('function'); r.pump(() => {})(); return r; } 50 | const timeout = (ms, p) => Promise.race([p, new Promise((_, r) => setTimeout(() => r(new River.TimeoutError('The promise timed out')), ms))]); 51 | return Promise.all([ 52 | expect(cancelled((r) => setImmediate(r))).to.become(undefined), 53 | expect(cancelled((_, r) => setImmediate(r, err))).to.be.rejectedWith(err), 54 | expect(timeout(50, cancelled(() => {}))).to.be.rejectedWith(River.TimeoutError), 55 | expect(pumped(undefined, undefined, 0, () => {})).to.be.rejectedWith(TypeError), 56 | expect(pumped(() => {}, 0, undefined, undefined)).to.become(undefined), 57 | expect(pumped(0, () => {}, 123, 123)).to.become(undefined), 58 | ]).then(() => { 59 | process.removeListener('warning', onWarning); 60 | expect(warnings).to.equal(6); 61 | }, (reason) => { 62 | process.removeListener('warning', onWarning); 63 | throw reason; 64 | }); 65 | }); 66 | it('should not feed written items into handlers after the first', function () { 67 | const river = new River(alphabetResolver); 68 | let str1 = ''; 69 | let str2 = ''; 70 | expect(river.pump(item => str1 = str1 + item)).to.be.a('function'); 71 | river.pump(item => str2 = str2 + item)(/* no-op cancellation attempt */); 72 | return Promise.all([ 73 | expect(river.then(() => str1)).to.become('abcdef'), 74 | expect(river.then(() => str2)).to.become('') 75 | ]); 76 | }); 77 | it('should not invoke the handler synchronously after registering it', function (done) { 78 | const river = new River((_, __, write) => { write('a'); write('b'); write('c'); }); 79 | let str = ''; 80 | expect(river.pump(item => str += item)).to.be.a('function'); 81 | expect(str).to.equal(''); 82 | Promise.resolve().then(() => { 83 | if (str === 'abc') done(); 84 | else done(new Error(`Expected str to equal "abc", but it was "${str}"`)); 85 | }); 86 | }); 87 | it('should write non-promise values synchronously', function () { 88 | let num = 0; 89 | const river = new River((resolve, _, write) => { setImmediate(() => { 90 | num += 1; 91 | write(1); 92 | num += 5; 93 | write(6); 94 | num += 101; 95 | write(107); 96 | resolve(); 97 | num = 'foobar'; 98 | }); }); 99 | expect(river.pump((expected) => { 100 | expect(num).to.equal(expected); 101 | })).to.be.a('function'); 102 | return expect(river.then(() => num)).to.become('foobar'); 103 | }); 104 | // This broke in Node.js v10. It seems WisePromise.resolve(nativePromise) 105 | // now delays the promise's resolution by one extra tick, compared to 106 | // Promise.resolve(nativePromise), even though WisePromise is a subclass of 107 | // Promise. It's a minor enough issue that we can probably ignore it. 108 | it.skip('should treat write(promise) and promise.then(write) the same', function () { 109 | const afterWriting = (...args) => { 110 | const river = new River((resolve, _, write) => { 111 | let last; 112 | for (const arg of args) last = arg(write) || last; 113 | if (last) last.then(resolve); 114 | else resolve(); 115 | }); 116 | let str = ''; 117 | expect(river.pump(item => str += item)).to.be.a('function'); 118 | return river.then(() => str); 119 | }; 120 | const direct = (arg) => (write) => write(Promise.resolve(arg)); 121 | const indirect = (arg) => (write) => Promise.resolve(arg).then(write); 122 | return Promise.all([ 123 | expect(afterWriting(direct('a'), direct('b'), direct('c'))).to.become('abc'), 124 | expect(afterWriting(indirect('a'), direct('b'), direct('c'))).to.become('abc'), 125 | expect(afterWriting(direct('a'), indirect('b'), direct('c'))).to.become('abc'), 126 | expect(afterWriting(direct('a'), direct('b'), indirect('c'))).to.become('abc'), 127 | expect(afterWriting(indirect('a'), indirect('b'), direct('c'))).to.become('abc'), 128 | expect(afterWriting(indirect('a'), direct('b'), indirect('c'))).to.become('abc'), 129 | expect(afterWriting(direct('a'), indirect('b'), indirect('c'))).to.become('abc'), 130 | expect(afterWriting(indirect('a'), indirect('b'), indirect('c'))).to.become('abc') 131 | ]); 132 | }); 133 | it('should respect a given concurrency value', function () { 134 | const fn = Symbol(); 135 | const concurrencyTest = (max, args) => { 136 | const river = new River((resolve, _, write) => { 137 | write('a'); write('b'); write('c'); write('d'); write('e'); write('f'); write('g'); resolve(); 138 | }); 139 | let processing = 0; 140 | let reachedMax = false; 141 | let str = ''; 142 | if (args.includes(fn)) { 143 | args[args.indexOf(fn)] = (item) => { 144 | processing += 1; 145 | expect(processing).to.be.lte(max); 146 | if (processing === max) reachedMax = true; 147 | return new Promise(r => setTimeout(r, 2)).then(() => { str += item; processing -= 1; }); 148 | }; 149 | } 150 | river.pump(...args); 151 | return river.then(() => { expect(reachedMax).to.equal(max <= 7); }).then(() => str); 152 | }; 153 | return Promise.all([ 154 | expect(concurrencyTest(8, [() => {}])).to.become(''), 155 | expect(concurrencyTest(8, [fn])).to.become('abcdefg'), 156 | expect(concurrencyTest(8, [0, fn])).to.become('abcdefg'), 157 | expect(concurrencyTest(8, [fn, 0])).to.become('abcdefg'), 158 | expect(concurrencyTest(5, [5, fn])).to.become('abcdefg'), 159 | expect(concurrencyTest(5, [fn, 5])).to.become('abcdefg'), 160 | expect(concurrencyTest(2, [fn, 2])).to.become('abcdefg'), 161 | expect(concurrencyTest(2, [2, fn])).to.become('abcdefg'), 162 | expect(concurrencyTest(1, [1, fn])).to.become('abcdefg'), 163 | expect(concurrencyTest(1, [fn, 1])).to.become('abcdefg') 164 | ]); 165 | }); 166 | it('should not fulfill the river until processing and racing is done', function () { 167 | const river1 = new River((resolve, _, write) => { 168 | write(new Promise(r => setTimeout(r, 40))); resolve(); 169 | }); 170 | const river2 = new River((resolve, _, write) => { 171 | write(new Promise(r => setTimeout(r, 40))); resolve(); 172 | }); 173 | river1.pump(x => new Promise(r => setTimeout(r, 40))); 174 | river2.pump(() => {}); 175 | let timer1 = false; 176 | let timer2 = false; 177 | let timer3 = false; 178 | let timer4 = false; 179 | setTimeout(() => timer1 = true, 20); 180 | setTimeout(() => timer2 = true, 60); 181 | setTimeout(() => timer3 = true, 75); 182 | setTimeout(() => timer4 = true, 100); 183 | return Promise.all([ 184 | river1.then(() => { 185 | expect(timer1).to.equal(true); 186 | expect(timer2).to.equal(true); 187 | expect(timer3).to.equal(true); 188 | expect(timer4).to.equal(false); 189 | }), 190 | river2.then(() => { 191 | expect(timer1).to.equal(true); 192 | expect(timer2).to.equal(false); 193 | }) 194 | ]); 195 | }); 196 | it('should reject the river if the handler throws or returns rejected', function () { 197 | const handle = (fn) => { const r = new River(alphabetResolver); r.pump(fn); return r; }; 198 | const err1 = new Error('foo'); 199 | const err2 = new Error('bar'); 200 | return Promise.all([ 201 | expect(handle(() => { throw err1 })).to.be.rejectedWith(err1), 202 | expect(handle(() => Promise.reject(err2))).to.be.rejectedWith(err2) 203 | ]); 204 | }); 205 | it('should not be able to write any more values after resolve() is called', function () { 206 | const after = (ms, x) => new Promise(r => setTimeout(() => r(x), ms)); 207 | const cutoffTest = (process, a, b, c, wait) => { 208 | const river = new River((r, _, w) => { w(a); r(b); w(c); }); 209 | let str = ''; 210 | river.pump(x => process ? after(process.process).then(() => str += x) : str += x); 211 | return Promise.all([ 212 | expect(river.then(() => str)).to.become('a'), 213 | expect(after(wait).then(() => str)).to.become('a') 214 | ]); 215 | }; 216 | return Promise.all([ 217 | cutoffTest(null, 'a', 'b', 'c', 20), 218 | cutoffTest(null, after(10, 'a'), 'b', 'c', 40), 219 | cutoffTest(null, 'a', after(10, 'b'), 'c', 40), 220 | cutoffTest(null, 'a', 'b', after(10, 'c'), 40), 221 | cutoffTest(null, after(10, 'a'), after(20, 'b'), 'c', 80), 222 | cutoffTest(null, after(10, 'a'), 'b', after(2, 'c'), 40), 223 | cutoffTest(null, 'a', after(20, 'b'), after(2, 'c'), 60), 224 | cutoffTest(null, after(10, 'a'), after(20, 'b'), after(2, 'c'), 80), 225 | cutoffTest({ process: 10 }, 'a', 'b', 'c', 40), 226 | cutoffTest({ process: 10 }, after(10, 'a'), 'b', 'c', 60), 227 | cutoffTest({ process: 10 }, 'a', after(10, 'b'), 'c', 60), 228 | cutoffTest({ process: 10 }, 'a', 'b', after(10, 'c'), 60), 229 | cutoffTest({ process: 10 }, after(10, 'a'), after(20, 'b'), 'c', 100), 230 | cutoffTest({ process: 10 }, after(10, 'a'), 'b', after(2, 'c'), 60), 231 | cutoffTest({ process: 10 }, 'a', after(20, 'b'), after(2, 'c'), 80), 232 | cutoffTest({ process: 10 }, after(10, 'a'), after(20, 'b'), after(2, 'c'), 100) 233 | ]); 234 | }); 235 | it('should ignore outside calls after resolve(), even if still processing', function () { 236 | const err = new Error('foobar'); 237 | const rejected = Promise.reject(err); 238 | const after = (ms, x) => new Promise(r => setTimeout(() => r(x), ms)); 239 | const cutoffTest = (process, a, b, wait) => { 240 | const river = new River((rs, rj, w) => { w(a); rs(b); w(rejected); rs(rejected); rj(err); }); 241 | let str = ''; 242 | river.pump(x => process ? after(process.process).then(() => str += x) : str += x); 243 | return Promise.all([ 244 | expect(river.then(() => str)).to.become('a'), 245 | expect(after(wait).then(() => str)).to.become('a') 246 | ]); 247 | }; 248 | rejected.catch(() => {}); 249 | return Promise.all([ 250 | cutoffTest(null, 'a', 'b', 20), 251 | cutoffTest(null, after(10, 'a'), 'b', 40), 252 | cutoffTest(null, 'a', after(10, 'b'), 40), 253 | cutoffTest(null, after(10, 'a'), after(20, 'b'), 80), 254 | cutoffTest({ process: 10 }, 'a', 'b', 20), 255 | cutoffTest({ process: 10 }, after(10, 'a'), 'b', 60), 256 | cutoffTest({ process: 10 }, 'a', after(10, 'b'), 60), 257 | cutoffTest({ process: 10 }, after(10, 'a'), after(20, 'b'), 100) 258 | ]); 259 | }); 260 | it('should supress unhandled rejected promises written after resolve()', function () { 261 | let unhandled = false; 262 | const setUnhandled = () => unhandled = true; 263 | process.on('unhandledRejection', setUnhandled); 264 | const river = new River((r, _, w) => { r(new Promise(r => setTimeout(r, 10))); w(Promise.reject(new Error('foo'))); }); 265 | return river.then(() => new Promise(r => setTimeout(r, 10))).then(() => { 266 | process.removeListener('unhandledRejection', setUnhandled); 267 | expect(unhandled).to.equal(false); 268 | }, (reason) => { 269 | process.removeListener('unhandledRejection', setUnhandled); 270 | throw reason; 271 | }); 272 | }); 273 | it('should still be able to reject the river after calling resolve()', function () { 274 | const err = new Error('foobar'); 275 | const after = (ms, x) => new Promise(r => setTimeout(() => r(x), ms)); 276 | const rejectionTest = (args, delayed, shouldCancel) => { 277 | const river = new River((r, _, w) => { w(); delayed ? r(after(10)) : r(); }); 278 | const cancel = river.pump(...args); 279 | if (shouldCancel) cancel(); 280 | return river; 281 | }; 282 | return Promise.all([ 283 | expect(rejectionTest([() => { throw err; }], false, false)).to.be.rejectedWith(err), 284 | expect(rejectionTest([() => { throw err; }], true, false)).to.be.rejectedWith(err), 285 | expect(rejectionTest([() => Promise.reject(err)], false, false)).to.be.rejectedWith(err), 286 | expect(rejectionTest([() => Promise.reject(err)], true, false)).to.be.rejectedWith(err), 287 | expect(rejectionTest([() => {}], false, true)).to.be.rejectedWith(River.Cancellation), 288 | expect(rejectionTest([() => {}], true, true)).to.be.rejectedWith(River.Cancellation) 289 | ]); 290 | }); 291 | it('should not process racing values or queued values after being rejected', function () { 292 | const after = (ms, x) => new Promise(r => setTimeout(() => r(x), ms)); 293 | const river = new River((r, _, w) => { w('a'); w('b'); w(after(10, 'c')); setTimeout(() => w('d'), 20); r(); }); 294 | const err = new Error('foobar'); 295 | let correctValue = false; 296 | let invoked = 0; 297 | river.pump((item) => { 298 | correctValue = item === 'a'; 299 | invoked += 1; 300 | throw err; 301 | }); 302 | return Promise.all([ 303 | expect(river).to.be.rejectedWith(err), 304 | expect(after(30).then(() => correctValue)).to.become(true), 305 | expect(after(30).then(() => invoked)).to.become(1) 306 | ]); 307 | }); 308 | it('should support cleanup functions which are invoked regardless of fate', function () { 309 | let str = ''; 310 | const err = new Error('foobar'); 311 | const cleanup = (x) => () => str += x; 312 | const fulfilled = new River((r, _, __, f) => { f(cleanup('a')); setTimeout(r, 7); }); 313 | const rejected = new River((_, r, __, f) => { f(cleanup('b')); setTimeout(() => r(err), 15); }); 314 | expect(str).to.equal(''); 315 | return Promise.all([ 316 | expect(new Promise(r => setTimeout(() => r(str), 1))).to.become(''), 317 | expect(fulfilled).to.become(undefined), 318 | expect(rejected).to.be.rejectedWith(err), 319 | ]).then(() => { expect(str).to.equal('ab'); }); 320 | }); 321 | it('should synchronously invoke cleanup functions in LIFO order', function (done) { 322 | let str = ''; 323 | const cleanup = (x) => () => { str += x; return new Promise(r => setTimeout(r, 100)); } 324 | new River((resolve, _, __, free) => { 325 | free(cleanup('a')); 326 | free(cleanup('b')); 327 | setTimeout(() => { 328 | try { 329 | expect(str).to.equal(''); 330 | free(cleanup('c')); 331 | free(cleanup('d')); 332 | expect(str).to.equal(''); 333 | resolve(); 334 | expect(str).to.equal('dcba'); 335 | done(); 336 | } catch (err) { 337 | done(err); 338 | } 339 | }, 5); 340 | }); 341 | expect(str).to.equal(''); 342 | }); 343 | it('should immediately invoke cleanup functions if river is already resolved', function () { 344 | let str = ''; 345 | const cleanup = (x) => () => { str += x; return new Promise(r => setTimeout(r, 100)); } 346 | new River((resolve, _, __, free) => { 347 | free(cleanup('a')); 348 | free(cleanup('b')); 349 | free(() => { 350 | free(cleanup('x')); 351 | str += 'c'; 352 | free(cleanup('y')); 353 | }); 354 | resolve(); 355 | free(cleanup('d')); 356 | }); 357 | expect(str).to.equal('xcybad'); 358 | }); 359 | }); 360 | -------------------------------------------------------------------------------- /test/20.static-factory-functions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const makeIterable = require('../tools/make-iterable'); 5 | 6 | describe('River.reject()', function () { 7 | it('should return a rejected river', function () { 8 | const err = new Error('foobar'); 9 | const river = River.reject(err); 10 | expect(river).to.be.an.instanceof(River); 11 | return expect(river).to.be.rejectedWith(err); 12 | }); 13 | it('should return a river that is not cancellable', function () { 14 | const err = new Error('foobar'); 15 | const river = River.reject(err); 16 | river.pump(() => {})(); 17 | return expect(river).to.be.rejectedWith(err); 18 | }); 19 | }); 20 | 21 | describe('River.never()', function () { 22 | it('should return a river that never emits data and never resolves', function () { 23 | const river = River.never(); 24 | expect(river).to.be.an.instanceof(River); 25 | let noData = true; 26 | river.pump(() => noData = false); 27 | return Promise.race([river, new Promise(r => setTimeout(() => r(123), 50))]).then((result) => { 28 | expect(result).to.equal(123); 29 | expect(noData).to.equal(true); 30 | }); 31 | }); 32 | it('should return a river that can be cancelled', function () { 33 | const river = River.never(); 34 | river.pump(() => {})(); 35 | return expect(river).to.be.rejectedWith(River.Cancellation); 36 | }); 37 | }); 38 | 39 | describe('River.empty()', function () { 40 | it('should return a fulfilled river that never emits data', function () { 41 | const river = River.empty(); 42 | expect(river).to.be.an.instanceof(River); 43 | let noData = true; 44 | let tooLate = false; 45 | river.pump(() => noData = false); 46 | setImmediate(() => tooLate = true); 47 | return river.then(() => { 48 | expect(noData).to.equal(true); 49 | expect(tooLate).to.equal(false); 50 | }); 51 | }); 52 | it('should return a river that is not cancellable', function () { 53 | const river = River.empty(); 54 | river.pump(() => {})(); 55 | return expect(river).to.become(undefined); 56 | }); 57 | }); 58 | 59 | describe('River.one()', function () { 60 | it('should return a river that emits only one item', function () { 61 | const river = River.one(['foobar']); 62 | expect(river).to.be.an.instanceof(River); 63 | const data = []; 64 | let tooLate = false; 65 | river.pump(item => data.push(item)); 66 | setImmediate(() => tooLate = true); 67 | return river.then(() => { 68 | expect(data).to.deep.equal([['foobar']]); 69 | expect(tooLate).to.equal(false); 70 | }); 71 | }); 72 | it('should return a river that can be cancelled', function () { 73 | const river = River.one(['foobar']); 74 | river.pump(() => {})(); 75 | return expect(river).to.be.rejectedWith(River.Cancellation); 76 | }); 77 | }); 78 | 79 | describe('River.from()', function () { 80 | it('should return a river that emits each item in the array', function () { 81 | const river = River.from([['foo'], 'bar', 'baz']); 82 | expect(river).to.be.an.instanceof(River); 83 | const data = []; 84 | let tooLate = false; 85 | river.pump(item => data.push(item)); 86 | setImmediate(() => tooLate = true); 87 | return river.then(() => { 88 | expect(data).to.deep.equal([['foo'], 'bar', 'baz']); 89 | expect(tooLate).to.equal(false); 90 | }); 91 | }); 92 | it('should work with non-array iterable objects', function () { 93 | const river = River.from(makeIterable([['foo'], 'bar', 'baz'])); 94 | expect(river).to.be.an.instanceof(River); 95 | const data = []; 96 | let tooLate = false; 97 | river.pump(item => data.push(item)); 98 | setImmediate(() => tooLate = true); 99 | return river.then(() => { 100 | expect(data).to.deep.equal([['foo'], 'bar', 'baz']); 101 | expect(tooLate).to.equal(false); 102 | }); 103 | }); 104 | it('should return a rejected river if the argument is not an iterable object', function () { 105 | const river = River.from(123); 106 | expect(river).to.be.an.instanceof(River); 107 | return expect(river).to.be.rejectedWith(TypeError); 108 | }); 109 | it('should return a river that can be cancelled', function () { 110 | const river = River.from([['foo'], 'bar', 'baz']); 111 | river.pump(() => {})(); 112 | return expect(river).to.be.rejectedWith(River.Cancellation); 113 | }); 114 | }); 115 | 116 | describe('River.every()', function () { 117 | it('should return a cancellable river that emits on an interval', function () { 118 | const river = River.every(40); 119 | expect(river).to.be.an.instanceof(River); 120 | let count = 0; 121 | let successes = 0; 122 | let tooLate = false; 123 | const cancel = river.pump((x) => { expect(x).to.equal(undefined); count += 1 }); 124 | const check = (expected) => () => { if (count === expected) successes += 1; } 125 | setTimeout(check(0), 20); 126 | setTimeout(check(1), 60); 127 | setTimeout(check(2), 100); 128 | setTimeout(() => { check(3)(); cancel(); }, 140); 129 | setTimeout(() => tooLate = true, 160); 130 | river.catchLater(); 131 | return new Promise(r => setTimeout(r, 141)).then(() => { 132 | expect(count).to.equal(3); 133 | expect(successes).to.equal(4); 134 | return expect(river).to.be.rejectedWith(River.Cancellation); 135 | }).then(() => { 136 | expect(count).to.equal(3); 137 | expect(successes).to.equal(4); 138 | expect(tooLate).to.equal(false); 139 | return new Promise(r => setTimeout(r, 100)); 140 | }).then(() => { 141 | expect(count).to.equal(3); 142 | expect(successes).to.equal(4); 143 | }); 144 | }); 145 | it('should treat time values as 32-bit integers', function () { 146 | const river = River.every('foobar'); 147 | expect(river).to.be.an.instanceof(River); 148 | let count = 0; 149 | const cancel = river.pump((x) => { expect(x).to.equal(undefined); count += 1 }); 150 | return new Promise(r => setTimeout(r, 40)).then(() => { 151 | cancel(); 152 | expect(count).to.be.within(2, 60); 153 | }); 154 | }); 155 | }); 156 | 157 | describe('River.combine()', function () { 158 | it('should return a river with the combined data of the given rivers', function () { 159 | const after = (ms, x) => new Promise(r => setTimeout(() => r(x), ms)); 160 | const rivers = [ 161 | River.one('a'), 162 | River.empty(), 163 | River.from(['b', after(40, 'c'), 'd']), 164 | River.one('e') 165 | ]; 166 | const river = River.combine(rivers); 167 | let str = ''; 168 | river.pump(item => str += item); 169 | return after(16).then(() => { 170 | expect(str).to.equal('abde'); 171 | return Promise.race([river, after(1, 123)]); 172 | }).then((value) => { 173 | expect(value).to.equal(123); 174 | expect(str).to.equal('abde'); 175 | return Promise.race([river, after(30, 123)]); 176 | }).then((value) => { 177 | expect(value).to.equal(undefined); 178 | expect(str).to.equal('abdec'); 179 | }); 180 | }); 181 | it('should accept regular promises', function () { 182 | const after = (ms, x) => new Promise(r => setTimeout(() => r(x), ms)); 183 | const river = River.combine(123, after(40, 456), [789, River.from(['a', 'b', 'c'])]); 184 | let str = ''; 185 | river.pump(item => str += item); 186 | return after(20).then(() => { 187 | expect(str).to.equal('abc'); 188 | return Promise.race([river, after(2, 'qux')]); 189 | }).then((value) => { 190 | expect(value).to.equal('qux'); 191 | expect(str).to.equal('abc'); 192 | return Promise.race([river, after(30, 123)]); 193 | }).then((value) => { 194 | expect(value).to.equal(undefined); 195 | expect(str).to.equal('abc'); 196 | }); 197 | }); 198 | it('should not affect any arguments if iteration throws', function () { 199 | const called = 0; 200 | const err = new Error('foobar'); 201 | const promise = Promise.resolve(); 202 | promise.then = function (a, b) { called += 1; return Promise.prototype.then.call(this, a, b); }; 203 | const river = River.never(); 204 | const combined = River.combine([promise, river], { [Symbol.iterator]: () => ({ next() { throw err; } }) }); 205 | return expect(combined).to.be.rejectedWith(err).then(() => { 206 | return new Promise(r => setTimeout(r, 5)); 207 | }).then(() => { 208 | expect(called).to.equal(0); 209 | const fail = () => { throw new Error('This river should not have been resolved'); } 210 | return Promise.race([river.then(fail, fail), new Promise(r => setTimeout(r, 5))]); 211 | }); 212 | }); 213 | it('should cancel all given rivers when itself is cancelled', function () { 214 | const rivers = [River.one('foo'), River.empty(), River.every(5), River.never()]; 215 | const river = River.combine(rivers); 216 | river.pump(() => {})(); 217 | return Promise.all([ 218 | expect(river).to.be.rejectedWith(River.Cancellation), 219 | expect(rivers[0]).to.be.rejectedWith(River.Cancellation), 220 | expect(rivers[2]).to.be.rejectedWith(River.Cancellation), 221 | expect(rivers[3]).to.be.rejectedWith(River.Cancellation), 222 | expect(rivers[1]).to.become(undefined) 223 | ]); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /test/21.is-river.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('River.isRiver()', function () { 6 | describe('should return false when', function () { 7 | specify('given: undefined', function () { 8 | expect(River.isRiver()).to.be.false; 9 | expect(River.isRiver(undefined)).to.be.false; 10 | }); 11 | specify('given: null', function () { 12 | expect(River.isRiver(null)).to.be.false; 13 | }); 14 | specify('given: 0', function () { 15 | expect(River.isRiver(0)).to.be.false; 16 | }); 17 | specify('given: 123', function () { 18 | expect(River.isRiver(123)).to.be.false; 19 | }); 20 | specify('given: true', function () { 21 | expect(River.isRiver(true)).to.be.false; 22 | }); 23 | specify('given: false', function () { 24 | expect(River.isRiver(false)).to.be.false; 25 | }); 26 | specify('given: "foo"', function () { 27 | expect(River.isRiver('foo')).to.be.false; 28 | }); 29 | specify('given: {}', function () { 30 | expect(River.isRiver({})).to.be.false; 31 | }); 32 | specify('given: []', function () { 33 | expect(River.isRiver([])).to.be.false; 34 | }); 35 | specify('given: { then() {} }', function () { 36 | expect(River.isRiver({ then() {} })).to.be.false; 37 | }); 38 | specify('given: Promise.resolve()', function () { 39 | expect(River.isRiver(global.Promise.resolve())).to.be.false; 40 | }); 41 | specify('given: WisePromise.resolve()', function () { 42 | expect(River.isRiver(River.Promise.resolve())).to.be.false; 43 | }); 44 | specify('given: Object.create(WiseRiver.prototype)', function () { 45 | expect(River.isRiver(Object.create(River.prototype))).to.be.false; 46 | }); 47 | }); 48 | describe('should return true when', function () { 49 | specify('given: new River()', function () { 50 | expect(River.isRiver(new River(() => {}))).to.be.true; 51 | }); 52 | specify('given: River.reject()', function () { 53 | const river = River.reject(new Error('foo')); 54 | river.catchLater(); 55 | expect(River.isRiver(river)).to.be.true; 56 | }); 57 | specify('given: River.never()', function () { 58 | expect(River.isRiver(River.never())).to.be.true; 59 | }); 60 | specify('given: River.empty()', function () { 61 | expect(River.isRiver(River.empty())).to.be.true; 62 | }); 63 | specify('given: River.one()', function () { 64 | expect(River.isRiver(River.one({}))).to.be.true; 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/30.fork.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('.fork()', function () { 6 | it('should throw a TypeError if an invalid count is given', function () { 7 | expect(() => River.from(['a']).fork(null)).to.throw(TypeError); 8 | expect(() => River.from(['a']).fork(0)).to.throw(TypeError); 9 | expect(() => River.from(['a']).fork(-2)).to.throw(TypeError); 10 | expect(() => River.from(['a']).fork(2.000001)).to.throw(TypeError); 11 | expect(() => River.from(['a']).fork(NaN)).to.throw(TypeError); 12 | expect(() => River.from(['a']).fork(Infinity)).to.throw(TypeError); 13 | expect(() => River.from(['a']).fork('2')).to.throw(TypeError); 14 | expect(() => River.from(['a']).fork('foobar')).to.throw(TypeError); 15 | }); 16 | it('should return an array with the given number of branches', function () { 17 | const river = River.empty(); 18 | const countTest = (count, value) => { 19 | expect(Array.isArray(value)).to.equal(true); 20 | expect(value.length).to.equal(count); 21 | }; 22 | countTest(2, river.fork()); 23 | countTest(1, river.fork(1)); 24 | countTest(47, river.fork(47)); 25 | }); 26 | it('should propagate data to the branches', function () { 27 | const source = River.from(['a', 'b', 'c']); 28 | let str = ''; 29 | source.fork(3).forEach(f => f.pump(x => str += x)); 30 | return expect(source.then(() => str)).to.become('aaabbbccc'); 31 | }); 32 | it('should propagate fulfillment, but respect forks that are processing', function () { 33 | const err = new Error('bar'); 34 | const source = River.one('foo'); 35 | const forks = source.fork(4); 36 | let str = ''; 37 | source.then(() => str += '1'); 38 | forks[0].pump(() => str += 'a'); 39 | forks[1].pump(() => new Promise(r => setTimeout(() => { str += 'b'; r(); }, 20))); 40 | forks[2].pump(() => { throw err; }); 41 | forks[3].pump(() => str += 'd'); 42 | forks[0].then(() => str += 'w'); 43 | forks[1].then(() => str += 'x'); 44 | forks[2].then(() => str += 'y', () => {}); 45 | forks[3].then(() => str += 'z'); 46 | return Promise.all([ 47 | new Promise(r => setTimeout(r, 10)), 48 | expect(forks[2]).to.be.rejectedWith(err) 49 | ]).then(() => { 50 | expect(str).to.equal('ad1wz'); 51 | return new Promise(r => setTimeout(r, 20)); 52 | }).then(() => { 53 | expect(str).to.equal('ad1wzbx'); 54 | }); 55 | }); 56 | it('should propagate rejections, but respect forks that are resolved', function () { 57 | const err = new Error('foobar'); 58 | const source = River.from([new Promise((_, r) => setTimeout(() => r(err), 2)), 'a']); 59 | const forks = source.fork(3); 60 | let str = ''; 61 | forks.map(f => f.pump(x => str += x))[1](); 62 | return Promise.all([ 63 | expect(forks[0]).to.be.rejectedWith(err), 64 | expect(forks[1]).to.be.rejectedWith(River.Cancellation), 65 | expect(forks[2]).to.be.rejectedWith(err) 66 | ]).then(() => { 67 | expect(str).to.equal('aa'); 68 | }); 69 | }); 70 | it('should require all forks to be cancelled for the source to be cancelled', function () { 71 | const source = River.every(10); 72 | const forks = source.fork(4); 73 | let str = ''; 74 | forks[0].pump(() => {})(); 75 | const cancel1 = forks[1].pump(() => str += 'a'); 76 | const cancel2 = forks[2].pump(() => str += 'b'); 77 | const cancel3 = forks[3].pump(() => str += 'c'); 78 | source.catch(() => str += 'z'); 79 | return expect(forks[0]).to.be.rejectedWith(River.Cancellation).then(() => { 80 | expect(str).to.equal(''); 81 | return new Promise(r => setTimeout(r, 10)); 82 | }).then(() => { 83 | expect(str).to.equal('abc'); 84 | cancel1(); 85 | cancel3(); 86 | forks[1].catchLater(); 87 | forks[3].catchLater(); 88 | return new Promise(r => setTimeout(r, 10)); 89 | }).then(() => { 90 | expect(str).to.equal('abcb'); 91 | cancel2(); 92 | forks[2].catchLater(); 93 | }).then(() => { 94 | expect(str).to.equal('abcbz'); 95 | return expect(source).to.be.rejectedWith(River.Cancellation); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/31.map.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const invalidArgs = require('../tools/invalid-args'); 5 | 6 | describe('.map()', function () { 7 | it('should return a rejected river if invalid arguments are given', function () { 8 | const testRejected = (value) => { 9 | expect(value).to.be.an.instanceof(River); 10 | return expect(value).to.be.rejectedWith(TypeError); 11 | }; 12 | return Promise.all(invalidArgs().map(args => testRejected(River.from(['a']).map(...args)))); 13 | }); 14 | it('should propagate rejections to the returned river', function () { 15 | const err = new Error('foobar'); 16 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 17 | return expect(river.map(x => x)).to.be.rejectedWith(err); 18 | }); 19 | it('should propagate cancellations back to the source river', function () { 20 | const river = River.every(10); 21 | river.map(x => x).pump(() => {})(); 22 | return expect(river).to.be.rejectedWith(River.Cancellation); 23 | }); 24 | it('should map the data through the callback', function () { 25 | let str = ''; 26 | const river = River.from(['a', 'b', 'c']).map(x => x === 'b' ? Promise.resolve('foo') : x + x + '.'); 27 | river.pump(x => str += x); 28 | return expect(river.then(() => str)).to.become('aa.cc.foo'); 29 | }); 30 | it('should respect a given concurrency value', function () { 31 | let str = ''; 32 | const river = River.from(['a', 'b', 'c']).map(1, x => x === 'b' ? Promise.resolve('foo') : x + x + '.'); 33 | river.pump(x => str += x); 34 | return expect(river.then(() => str)).to.become('aa.foocc.'); 35 | }); 36 | it('should reject the stream if the handler throws', function () { 37 | const err = new Error('foobar'); 38 | let str = ''; 39 | return expect(River.from(['a', 'b', Promise.resolve('c')]).map((x) => { str += x; throw err; })) 40 | .to.be.rejectedWith(err) 41 | .then(() => new Promise(r => setImmediate(r))) 42 | .then(() => { expect(str).to.equal('a'); }); 43 | }); 44 | it('should reject the stream if the handler returns a rejected promise', function () { 45 | const err = new Error('foobar'); 46 | let str = ''; 47 | return expect(River.from(['a', new Promise(r => setImmediate(() => r('b'))), 'c']).map((x) => { str += x; return Promise.reject(err); })) 48 | .to.be.rejectedWith(err) 49 | .then(() => new Promise(r => setImmediate(r))) 50 | .then(() => { expect(str).to.equal('ac'); }); 51 | }); 52 | it('should treat returned rivers with flatMap semantics', function () { 53 | const river = River.from(['a', 'b', 'c']).map(x => x === 'a' ? 'a' : River.from(['x', new Promise(r => setImmediate(() => r('y'))), 'z'])); 54 | let str = ''; 55 | river.pump(x => str += x); 56 | return expect(river.then(() => str)).become('axzxzyy'); 57 | }); 58 | it('should respect a given concurrency value with flatMap semantics', function () { 59 | const river = River.from(['a', 'b', 'c']).map(1, x => x === 'a' ? 'a' : River.from(['x', new Promise(r => setImmediate(() => r('y'))), 'z'])); 60 | let str = ''; 61 | river.pump(x => str += x); 62 | return expect(river.then(() => str)).become('axzyxzy'); 63 | }); 64 | it('should allow cancellation of rivers provided through a flatMap process', function () { 65 | const flattened = []; 66 | const river = River.from(['a', 'b', 'c']).map((x) => { 67 | if (x === 'a') return 'a' 68 | const r = River.from(['x', new Promise(r => setImmediate(() => r('y'))), 'z']); 69 | flattened.push(r); 70 | return r; 71 | }); 72 | let str = ''; 73 | const cancel = river.pump((x) => { 74 | str += x; 75 | if (x === 'y') cancel(); 76 | }); 77 | return expect(river).to.be.rejectedWith(River.Cancellation).then(() => { 78 | expect(str).to.equal('axzxzy'); 79 | expect(flattened.length).to.equal(2); 80 | return Promise.all(flattened.map(f => expect(f).to.be.rejectedWith(River.Cancellation))); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/32.for-each.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const invalidArgs = require('../tools/invalid-args'); 5 | 6 | describe('.forEach()', function () { 7 | it('should return a rejected river if invalid arguments are given', function () { 8 | const testRejected = (value) => { 9 | expect(value).to.be.an.instanceof(River); 10 | return expect(value).to.be.rejectedWith(TypeError); 11 | }; 12 | return Promise.all(invalidArgs().map(args => testRejected(River.from(['a']).forEach(...args)))); 13 | }); 14 | it('should propagate rejections to the returned river', function () { 15 | const err = new Error('foobar'); 16 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 17 | return expect(river.forEach(x => x)).to.be.rejectedWith(err); 18 | }); 19 | it('should propagate cancellations back to the source river', function () { 20 | const river = River.every(10); 21 | river.forEach(x => x).pump(() => {})(); 22 | return expect(river).to.be.rejectedWith(River.Cancellation); 23 | }); 24 | it('should invoke the callback without changing the resulting data', function () { 25 | let str = ''; 26 | let invokedWith = ''; 27 | const river = River.from(['a', 'b', 'c']).forEach((x) => { 28 | invokedWith += x; 29 | return x === 'b' ? Promise.resolve('foo') : x + x + '.'; 30 | }); 31 | river.pump(x => str += x); 32 | return expect(river).to.become(undefined).then(() => { 33 | expect(invokedWith).to.equal('abc'); 34 | expect(str).to.equal('acb'); 35 | }); 36 | }); 37 | it('should respect a given concurrency value', function () { 38 | let str = ''; 39 | let invokedWith = ''; 40 | const river = River.from(['a', 'b', 'c']).forEach((x) => { 41 | invokedWith += x; 42 | return x === 'b' ? Promise.resolve('foo') : x + x + '.'; 43 | }, 1); 44 | river.pump(x => str += x); 45 | return expect(river).to.become(undefined).then(() => { 46 | expect(invokedWith).to.equal('abc'); 47 | expect(str).to.equal('abc'); 48 | }); 49 | }); 50 | it('should reject the stream if the handler throws', function () { 51 | const err = new Error('foobar'); 52 | let str = ''; 53 | return expect(River.from(['a', 'b', Promise.resolve('c')]).forEach((x) => { str += x; throw err; })) 54 | .to.be.rejectedWith(err) 55 | .then(() => new Promise(r => setImmediate(r))) 56 | .then(() => { expect(str).to.equal('a'); }); 57 | }); 58 | it('should reject the stream if the handler returns a rejected promise', function () { 59 | const err = new Error('foobar'); 60 | let str = ''; 61 | return expect(River.from(['a', new Promise(r => setImmediate(() => r('b'))), 'c']).forEach((x) => { str += x; return Promise.reject(err); })) 62 | .to.be.rejectedWith(err) 63 | .then(() => new Promise(r => setImmediate(r))) 64 | .then(() => { expect(str).to.equal('ac'); }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/33.filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const invalidArgs = require('../tools/invalid-args'); 5 | 6 | describe('.filter()', function () { 7 | it('should return a rejected river if invalid arguments are given', function () { 8 | const testRejected = (value) => { 9 | expect(value).to.be.an.instanceof(River); 10 | return expect(value).to.be.rejectedWith(TypeError); 11 | }; 12 | return Promise.all(invalidArgs().map(args => testRejected(River.from(['a']).filter(...args)))); 13 | }); 14 | it('should propagate rejections to the returned river', function () { 15 | const err = new Error('foobar'); 16 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 17 | return expect(river.filter(x => false)).to.be.rejectedWith(err); 18 | }); 19 | it('should propagate cancellations back to the source river', function () { 20 | const river = River.every(10); 21 | river.filter(x => false).pump(() => {})(); 22 | return expect(river).to.be.rejectedWith(River.Cancellation); 23 | }); 24 | it('should invoke the callback to filter the data', function () { 25 | let str = ''; 26 | let invokedWith = ''; 27 | const river = River.from(['a', 'b', 'c', 'd']).filter((x) => { 28 | invokedWith += x; 29 | return x === 'b' ? Promise.resolve(true) : x === 'd' ? Promise.resolve(false) : x === 'c'; 30 | }); 31 | river.pump(x => str += x); 32 | return expect(river).to.become(undefined).then(() => { 33 | expect(invokedWith).to.equal('abcd'); 34 | expect(str).to.equal('cb'); 35 | }); 36 | }); 37 | it('should respect a given concurrency value', function () { 38 | let str = ''; 39 | let invokedWith = ''; 40 | const river = River.from(['a', 'b', 'c', 'd']).filter(1, (x) => { 41 | invokedWith += x; 42 | return x === 'b' ? Promise.resolve(true) : x === 'd' ? Promise.resolve(false) : x === 'c'; 43 | }); 44 | river.pump(x => str += x); 45 | return expect(river).to.become(undefined).then(() => { 46 | expect(invokedWith).to.equal('abcd'); 47 | expect(str).to.equal('bc'); 48 | }); 49 | }); 50 | it('should reject the stream if the handler throws', function () { 51 | const err = new Error('foobar'); 52 | let str = ''; 53 | return expect(River.from(['a', 'b', Promise.resolve('c')]).filter((x) => { str += x; throw err; })) 54 | .to.be.rejectedWith(err) 55 | .then(() => new Promise(r => setImmediate(r))) 56 | .then(() => { expect(str).to.equal('a'); }); 57 | }); 58 | it('should reject the stream if the handler returns a rejected promise', function () { 59 | const err = new Error('foobar'); 60 | let str = ''; 61 | return expect(River.from(['a', new Promise(r => setImmediate(() => r('b'))), 'c']).filter((x) => { str += x; return Promise.reject(err); })) 62 | .to.be.rejectedWith(err) 63 | .then(() => new Promise(r => setImmediate(r))) 64 | .then(() => { expect(str).to.equal('ac'); }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/34.distinct.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('.distinct()', function () { 6 | it('should return a rejected river if invalid arguments are given', function () { 7 | const testRejected = (value) => { 8 | expect(value).to.be.an.instanceof(River); 9 | return expect(value).to.be.rejectedWith(TypeError); 10 | }; 11 | return Promise.all([ 12 | testRejected(River.from('a').distinct(null)), 13 | testRejected(River.from('a').distinct('foobar')), 14 | testRejected(River.from('a').distinct(123)), 15 | testRejected(River.from('a').distinct({})) 16 | ]); 17 | }); 18 | it('should propagate rejections to the returned river', function () { 19 | const err = new Error('foobar'); 20 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 21 | return expect(river.distinct()).to.be.rejectedWith(err); 22 | }); 23 | it('should propagate cancellations back to the source river', function () { 24 | const river = River.every(10); 25 | river.distinct().pump(() => {})(); 26 | return expect(river).to.be.rejectedWith(River.Cancellation); 27 | }); 28 | it('should filter out adjacent duplicate data', function () { 29 | let str = ''; 30 | const river = River.from(['a', ['q'], ['q'], 'b', 'b', 'c', 'c', 'c', Promise.resolve('x'), 'd', 'e', 'b', 'e', 'e', 'E', 'x']).distinct(); 31 | river.pump(x => str += x); 32 | return expect(river.then(() => str)).to.become('aqqbcdebeEx'); 33 | }); 34 | it('should accept a custom equals function', function () { 35 | let str = ''; 36 | let checkedOrder = false; 37 | const equals = (a, b) => { 38 | if (str === 'a') { 39 | checkedOrder = true; 40 | expect(a).to.equal('a'); 41 | expect(b).to.deep.equal(['q']); 42 | } 43 | return String(a).toLowerCase() === String(b).toLowerCase(); 44 | }; 45 | const river = River.from(['a', ['q'], ['q'], 'b', 'b', 'c', 'c', 'c', Promise.resolve('x'), 'd', 'e', 'b', 'e', 'e', 'E', 'x']).distinct(equals); 46 | river.pump(x => str += x); 47 | return expect(river.then(() => str)).to.become('aqbcdebex').then(() => { 48 | expect(checkedOrder).to.be.true; 49 | }); 50 | }); 51 | it('should reject the stream if the equals function throws', function () { 52 | const err = new Error('foobar'); 53 | const river = River.from(['a', ['q'], ['q'], 'b', 'b', 'c', 'c', 'c', Promise.resolve('x'), 'd', 'e', 'b', 'e', 'e', 'E', 'x']).distinct((x) => { str += x; throw err; }); 54 | let str = ''; 55 | return expect(river).to.be.rejectedWith(err) 56 | .then(() => new Promise(r => setImmediate(r))) 57 | .then(() => { expect(str).to.equal('a'); }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/35.throttle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.throttle()', function () { 10 | it('should propagate rejections to the returned river', function () { 11 | const err = new Error('foobar'); 12 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 13 | return Promise.all([ 14 | expect(river.throttle(1)).to.be.rejectedWith(err), 15 | expect(river.throttle(30)).to.be.rejectedWith(err), 16 | ]); 17 | }); 18 | it('should propagate cancellations back to the source river', function () { 19 | const river = River.every(20); 20 | river.throttle(1).pump(() => {})(); 21 | return expect(river).to.be.rejectedWith(River.Cancellation); 22 | }); 23 | it('should keep data that is received at an accepted pace', function () { 24 | let str = ''; 25 | const river = River.from([after(20, 'a'), after(100, 'b'), after(180, 'c')]).throttle(50); 26 | river.pump(x => str += x); 27 | return expect(river).to.become(undefined).then(() => { 28 | expect(str).to.equal('abc'); 29 | }); 30 | }); 31 | it('should defer the most recent piece of data', function () { 32 | let str = ''; 33 | const river = River.from([after(20, 'a'), after(40, 'b'), after(120, 'c')]).throttle(50); 34 | river.pump(x => str += x); 35 | return expect(river).to.become(undefined).then(() => { 36 | expect(str).to.equal('abc'); 37 | }); 38 | }); 39 | it('should discard data that is received too quickly', function () { 40 | let str = ''; 41 | const river = River.from([after(20, 'a'), after(40, 'b'), after(50, 'c')]).throttle(50); 42 | river.pump(x => str += x); 43 | return expect(river).to.become(undefined).then(() => { 44 | expect(str).to.equal('ac'); 45 | }); 46 | }); 47 | it('should cast any argument to a signed integer', function () { 48 | let str = ''; 49 | const river = River.from([after(20, 'a'), after(40, 'b'), after(50, 'c')]).throttle(4294967346); 50 | river.pump(x => str += x); 51 | return expect(river).to.become(undefined).then(() => { 52 | expect(str).to.equal('ac'); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/36.debounce.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.debounce()', function () { 10 | it('should propagate rejections to the returned river', function () { 11 | const err = new Error('foobar'); 12 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 13 | return Promise.all([ 14 | expect(river.debounce(1)).to.be.rejectedWith(err), 15 | expect(river.debounce(30)).to.be.rejectedWith(err), 16 | ]); 17 | }); 18 | it('should propagate cancellations back to the source river', function () { 19 | const river = River.every(10); 20 | river.debounce(1).pump(() => {})(); 21 | return expect(river).to.be.rejectedWith(River.Cancellation); 22 | }); 23 | it('should keep data that is received at an accepted pace', function () { 24 | let str = ''; 25 | const river = River.from([after(10, 'a'), after(50, 'b'), after(90, 'c')]).debounce(25); 26 | river.pump(x => str += x); 27 | return expect(river).to.become(undefined).then(() => { 28 | expect(str).to.equal('abc'); 29 | }); 30 | }); 31 | it('should discard data that is received too quickly', function () { 32 | let str = ''; 33 | const river = River.from([after(20, 'a'), after(40, 'b'), after(110, 'c')]).debounce(50); 34 | river.pump(x => str += x); 35 | return expect(river).to.become(undefined).then(() => { 36 | expect(str).to.equal('bc'); 37 | }); 38 | }); 39 | it('should cast any argument to a signed integer', function () { 40 | let str = ''; 41 | const river = River.from([after(20, 'a'), after(40, 'b'), after(110, 'c')]).debounce(4294967346); 42 | river.pump(x => str += x); 43 | return expect(river).to.become(undefined).then(() => { 44 | expect(str).to.equal('bc'); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/37.timeout-between-each.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.timeoutBetweenEach()', function () { 10 | it('should propagate rejections to the returned river', function () { 11 | const err = new Error('foobar'); 12 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 13 | return expect(river.timeoutBetweenEach(30)).to.be.rejectedWith(err); 14 | }); 15 | it('should propagate cancellations back to the source river', function () { 16 | const river = River.every(10); 17 | river.timeoutBetweenEach(30).pump(() => {})(); 18 | return expect(river).to.be.rejectedWith(River.Cancellation); 19 | }); 20 | it('should not timeout if data is regularly received', function () { 21 | const promise = River.from([after(10, 'a'), after(50, 'b'), after(90, 'c')]) 22 | .timeoutBetweenEach(60) 23 | .all() 24 | .then(arr => arr.join('')); 25 | return expect(promise).to.become('abc'); 26 | }); 27 | it('should timeout if data is not received soon enough', function () { 28 | const promise = River.from([after(10, 'a'), after(50, 'b'), after(200, 'c')]) 29 | .timeoutBetweenEach(60) 30 | .all(); 31 | return expect(promise).to.be.rejectedWith(River.TimeoutError); 32 | }); 33 | it('should accept a string reason', function () { 34 | const promise = River.from([after(10, 'a'), after(50, 'b'), after(200, 'c')]) 35 | .timeoutBetweenEach(60, 'foobar') 36 | .all(); 37 | return expect(promise).to.be.rejectedWith(/^foobar$/); 38 | }); 39 | it('should accept an Error reason', function () { 40 | const err = new TypeError('foobar'); 41 | const promise = River.from([after(10, 'a'), after(50, 'b'), after(200, 'c')]) 42 | .timeoutBetweenEach(60, err) 43 | .all(); 44 | return expect(promise).to.be.rejectedWith(err); 45 | }); 46 | it('should start the timer right away', function () { 47 | const promise = River.from([after(80, 'a'), after(90, 'b'), after(100, 'c')]) 48 | .timeoutBetweenEach(60) 49 | .all(); 50 | return expect(promise).to.be.rejectedWith(River.TimeoutError); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/38.while.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const invalidArgs = require('../tools/invalid-args'); 5 | 6 | describe('.while()', function () { 7 | it('should return a rejected river if invalid arguments are given', function () { 8 | const testRejected = (value) => { 9 | expect(value).to.be.an.instanceof(River); 10 | return expect(value).to.be.rejectedWith(TypeError); 11 | }; 12 | return Promise.all(invalidArgs().map(args => testRejected(River.from(['a']).while(...args)))); 13 | }); 14 | it('should propagate rejections to the returned river', function () { 15 | const err = new Error('foobar'); 16 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 17 | return expect(river.while(() => true)).to.be.rejectedWith(err); 18 | }); 19 | it('should propagate cancellations back to the source river', function () { 20 | const river = River.every(10); 21 | river.while(() => true).pump(() => {})(); 22 | return expect(river).to.be.rejectedWith(River.Cancellation); 23 | }); 24 | it('should invoke the callback to determine a stopping point', function () { 25 | let str = ''; 26 | let invokedWith = ''; 27 | const source = River.from(['a', 'b', 'c', 'd']); 28 | const dest = source.while((x) => { 29 | invokedWith += x; 30 | return x === 'c' ? Promise.resolve(false) : Promise.resolve(true); 31 | }); 32 | dest.pump(x => str += x); 33 | return expect(dest).to.become(undefined).then(() => { 34 | expect(invokedWith).to.equal('abcd'); 35 | expect(str).to.equal('ab'); 36 | return expect(source).to.be.rejectedWith(River.Cancellation); 37 | }); 38 | }); 39 | it('should respect a given concurrency value', function () { 40 | let str = ''; 41 | let invokedWith = ''; 42 | const source = River.from(['a', 'b', 'c', 'd']); 43 | const dest = source.while(1, (x) => { 44 | invokedWith += x; 45 | return x === 'c' ? Promise.resolve(false) : Promise.resolve(true); 46 | }); 47 | dest.pump(x => str += x); 48 | return expect(dest).to.become(undefined).then(() => { 49 | expect(invokedWith).to.equal('abc'); 50 | expect(str).to.equal('ab'); 51 | return expect(source).to.be.rejectedWith(River.Cancellation); 52 | }); 53 | }); 54 | it('should reject the stream if the handler throws', function () { 55 | const err = new Error('foobar'); 56 | let str = ''; 57 | return expect(River.from(['a', 'b', Promise.resolve('c')]).while((x) => { str += x; throw err; })) 58 | .to.be.rejectedWith(err) 59 | .then(() => new Promise(r => setImmediate(r))) 60 | .then(() => { expect(str).to.equal('a'); }); 61 | }); 62 | it('should reject the stream if the handler returns a rejected promise', function () { 63 | const err = new Error('foobar'); 64 | let str = ''; 65 | return expect(River.from(['a', new Promise(r => setImmediate(() => r('b'))), 'c']).while((x) => { str += x; return Promise.reject(err); })) 66 | .to.be.rejectedWith(err) 67 | .then(() => new Promise(r => setImmediate(r))) 68 | .then(() => { expect(str).to.equal('ac'); }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/39.until.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.until()', function () { 10 | it('should propagate rejections to the returned river', function () { 11 | const err = new Error('foobar'); 12 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 13 | return expect(river.until(after(50))).to.be.rejectedWith(err); 14 | }); 15 | it('should propagate cancellations back to the source river', function () { 16 | const river = River.every(10); 17 | river.until(after(50)).pump(() => {})(); 18 | return expect(river).to.be.rejectedWith(River.Cancellation); 19 | }); 20 | it('should short-circuit the returned river when the given promise resolves', function () { 21 | let str = ''; 22 | const source = River.from([after(10, 'a'), after(50, 'b'), after(90, 'c')]); 23 | const dest = source.until(after(70)); 24 | dest.pump(x => str += x); 25 | return expect(dest).to.become(undefined).then(() => { 26 | expect(str).to.equal('ab'); 27 | return expect(source).to.be.rejectedWith(River.Cancellation); 28 | }); 29 | }); 30 | it('should reject the returned river when the given promise is rejected', function () { 31 | let str = ''; 32 | const err = new Error('foobar'); 33 | const source = River.from([after(10, 'a'), after(50, 'b'), after(90, 'c')]); 34 | const dest = source.until(new Promise((_, r) => setTimeout(() => r(err), 70))); 35 | dest.pump(x => str += x); 36 | return expect(dest).to.be.rejectedWith(err).then(() => { 37 | expect(str).to.equal('ab'); 38 | return expect(source).to.be.rejectedWith(River.Cancellation); 39 | }); 40 | }); 41 | it('should treat synchronous values as already-fulfilled promises', function () { 42 | let str = ''; 43 | const source = River.from([after(10, 'a'), after(20, 'b'), after(30, 'c')]); 44 | const dest = source.until('foobar'); 45 | dest.pump(x => str += x); 46 | return expect(dest).to.become(undefined).then(() => { 47 | expect(str).to.equal(''); 48 | return expect(source).to.be.rejectedWith(River.Cancellation); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/40.consume.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const invalidArgs = require('../tools/invalid-args'); 5 | 6 | describe('.consume()', function () { 7 | it('should return a rejected promise if invalid arguments are given', function () { 8 | const testRejected = (value) => { 9 | expect(value).to.not.be.an.instanceof(River); 10 | expect(value).to.be.an.instanceof(Promise); 11 | return expect(value).to.be.rejectedWith(TypeError); 12 | }; 13 | return Promise.all(invalidArgs().map(args => testRejected(River.from(['a']).consume(...args)))); 14 | }); 15 | it('should propagate rejections to the returned promise', function () { 16 | const err = new Error('foobar'); 17 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 18 | return expect(river.consume(x => x)).to.be.rejectedWith(err); 19 | }); 20 | it('should invoke the callback for each item in the river', function () { 21 | let invokedWith = ''; 22 | let pending = 0; 23 | const promise = River.from(['a', 'b', 'c']).consume((x) => { 24 | invokedWith += x; 25 | if (x === 'a') { 26 | expect(pending).to.equal(0); 27 | return x; 28 | } 29 | if (x === 'b') { 30 | expect(pending).to.equal(0); 31 | pending += 1; 32 | return Promise.resolve('foo').then(() => { pending -= 1; }); 33 | } 34 | if (x === 'c') { 35 | expect(pending).to.equal(1); 36 | return x; 37 | } 38 | expect(false).to.be.true; 39 | }); 40 | return expect(promise).to.become(undefined).then(() => { 41 | expect(pending).to.equal(0); 42 | expect(invokedWith).to.equal('abc'); 43 | }); 44 | }); 45 | it('should respect a given concurrency value', function () { 46 | let invokedWith = ''; 47 | let pending = 0; 48 | const promise = River.from(['a', 'b', 'c']).consume((x) => { 49 | expect(pending).to.equal(0); 50 | invokedWith += x; 51 | if (x === 'a') { 52 | return x; 53 | } 54 | if (x === 'b') { 55 | pending += 1; 56 | return Promise.resolve('foo').then(() => { pending -= 1; }); 57 | } 58 | if (x === 'c') { 59 | return x; 60 | } 61 | expect(false).to.be.true; 62 | }, 1); 63 | return expect(promise).to.become(undefined).then(() => { 64 | expect(pending).to.equal(0); 65 | expect(invokedWith).to.equal('abc'); 66 | }); 67 | }); 68 | it('should reject the promise if the handler throws', function () { 69 | const err = new Error('foobar'); 70 | let str = ''; 71 | return expect(River.from(['a', 'b', Promise.resolve('c')]).consume((x) => { str += x; throw err; })) 72 | .to.be.rejectedWith(err) 73 | .then(() => new Promise(r => setImmediate(r))) 74 | .then(() => { expect(str).to.equal('a'); }); 75 | }); 76 | it('should reject the promise if the handler returns a rejected promise', function () { 77 | const err = new Error('foobar'); 78 | let str = ''; 79 | return expect(River.from(['a', new Promise(r => setImmediate(() => r('b'))), 'c']).consume((x) => { str += x; return Promise.reject(err); })) 80 | .to.be.rejectedWith(err) 81 | .then(() => new Promise(r => setImmediate(r))) 82 | .then(() => { expect(str).to.equal('ac'); }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/41.reduce.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { callbackOnly } = require('../tools/invalid-args'); 4 | const River = require('../.'); 5 | 6 | const after = (ms, value) => new Promise((resolve) => { 7 | setTimeout(() => resolve(value), ms); 8 | }); 9 | 10 | describe('.reduce()', function () { 11 | it('should return a rejected promise if invalid arguments are given', function () { 12 | const testRejected = (value) => { 13 | expect(value).to.not.be.an.instanceof(River); 14 | expect(value).to.be.an.instanceof(Promise); 15 | return expect(value).to.be.rejectedWith(TypeError); 16 | }; 17 | return Promise.all(callbackOnly().map(args => testRejected(River.from(['a']).reduce(...args)))); 18 | }); 19 | it('should propagate rejections to the returned promise', function () { 20 | const err = new Error('foobar'); 21 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 22 | return expect(river.reduce(x => x)).to.be.rejectedWith(err); 23 | }); 24 | it('should apply the reducer to a river of items', function () { 25 | let invokedWith = ''; 26 | const promise = River.from(['a', 'b', 'c']).reduce((x, y) => { 27 | invokedWith += `[${x}+${y}]`; 28 | if (y === 'c') return Promise.resolve(x + ',' + y + '(foo)'); 29 | return x + ',' + y; 30 | }); 31 | return expect(promise).to.become('a,b,c(foo)').then(() => { 32 | expect(invokedWith).to.equal('[a+b][a,b+c]'); 33 | }); 34 | }); 35 | it('should accept an initial value', function () { 36 | let invokedWith = ''; 37 | const promise = River.from(['a', 'b', 'c']).reduce((x, y) => { 38 | invokedWith += `[${x}+${y}]`; 39 | if (y === 'c') return Promise.resolve(x + ',' + y + '(foo)'); 40 | return x + ',' + y; 41 | }, '(bar)'); 42 | return expect(promise).to.become('(bar),a,b,c(foo)').then(() => { 43 | expect(invokedWith).to.equal('[(bar)+a][(bar),a+b][(bar),a,b+c]'); 44 | }); 45 | }); 46 | it('should accept an initial value as a promise', function () { 47 | let ready = false; 48 | let invokedWith = ''; 49 | const promise = River.from(['a', 'b', 'c']).reduce((x, y) => { 50 | expect(ready).to.be.true; 51 | invokedWith += `[${x}+${y}]`; 52 | if (y === 'c') return Promise.resolve(x + ',' + y + '(foo)'); 53 | return x + ',' + y; 54 | }, after(20).then(() => { ready = true; return '(bar)' })); 55 | return expect(promise).to.become('(bar),a,b,c(foo)').then(() => { 56 | expect(invokedWith).to.equal('[(bar)+a][(bar),a+b][(bar),a,b+c]'); 57 | }); 58 | }); 59 | it('should not invoke the callback when only one item is provided', function () { 60 | let invokedWith = ''; 61 | const promise = River.from(['a']).reduce((x, y) => { 62 | invokedWith += `[${x}+${y}]`; 63 | if (y === 'c') return Promise.resolve(x + ',' + y + '(foo)'); 64 | return x + ',' + y; 65 | }); 66 | return expect(promise).to.become('a').then(() => { 67 | expect(invokedWith).to.equal(''); 68 | }); 69 | }); 70 | it('should not invoke the callback when only an initial value is provided', function () { 71 | let ready = false; 72 | let invokedWith = ''; 73 | const promise = River.from([]).reduce((x, y) => { 74 | invokedWith += `[${x}+${y}]`; 75 | if (y === 'c') return Promise.resolve(x + ',' + y + '(foo)'); 76 | return x + ',' + y; 77 | }, after(20).then(() => { ready = true; return '(bar)' })); 78 | return expect(promise).to.become('(bar)').then(() => { 79 | expect(ready).to.be.true; 80 | expect(invokedWith).to.equal(''); 81 | }); 82 | }); 83 | it('should return a NoDataError when no values are provided', function () { 84 | let invokedWith = ''; 85 | const promise = River.from([]).reduce((x, y) => { 86 | invokedWith += `[${x}+${y}]`; 87 | if (y === 'c') return Promise.resolve(x + ',' + y + '(foo)'); 88 | return x + ',' + y; 89 | }); 90 | return expect(promise).to.be.rejectedWith(River.NoDataError).then(() => { 91 | expect(invokedWith).to.equal(''); 92 | }); 93 | }); 94 | it('should use a concurrency of 1 when the callback returns a promise', function () { 95 | let invokedWith = ''; 96 | let pending = 1; 97 | const promise = River.from(['a', 'b', 'c']).reduce((x, y) => { 98 | expect(pending).to.equal(0); 99 | invokedWith += `[${x}+${y}]`; 100 | if (y === 'a') { 101 | return x + ',' + y; 102 | } 103 | if (y === 'b') { 104 | pending += 1; 105 | return after(20).then(() => { pending -=1; return x + ',' + y + '(foo)'; }); 106 | } 107 | if (y === 'c') { 108 | return x + ',' + y; 109 | } 110 | expect(false).to.be.true; 111 | }, after(20).then(() => { pending -= 1; return '(bar)' })); 112 | return expect(promise).to.become('(bar),a,b(foo),c').then(() => { 113 | expect(pending).to.equal(0); 114 | expect(invokedWith).to.equal('[(bar)+a][(bar),a+b][(bar),a,b(foo)+c]'); 115 | }); 116 | }); 117 | it('should reject the promise if the handler throws', function () { 118 | const err = new Error('foobar'); 119 | let str = ''; 120 | return expect(River.from(['a', 'b', Promise.resolve('c')]).reduce((x) => { str += x; throw err; })) 121 | .to.be.rejectedWith(err) 122 | .then(() => new Promise(r => setImmediate(r))) 123 | .then(() => { expect(str).to.equal('a'); }); 124 | }); 125 | it('should reject the promise if the handler returns a rejected promise', function () { 126 | const err = new Error('foobar'); 127 | let str = ''; 128 | return expect(River.from(['a', new Promise(r => setImmediate(() => r('b'))), 'c']).reduce((x) => { str += x; return Promise.reject(err); })) 129 | .to.be.rejectedWith(err) 130 | .then(() => new Promise(r => setImmediate(r))) 131 | .then(() => { expect(str).to.equal('a'); }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/42.all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('.all()', function () { 6 | it('should propagate rejections to the returned promise', function () { 7 | const err = new Error('foobar'); 8 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 9 | return expect(river.all()).to.be.rejectedWith(err); 10 | }); 11 | it('should be fulfilled with an array of all items provided', function () { 12 | return expect(River.from(['a', Promise.resolve('b'), 'c']).all()).to.become(['a', 'c', 'b']); 13 | }); 14 | it('should be fulfilled with an empty array when no items are provided', function () { 15 | return expect(River.from([]).all()).to.become([]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/43.find.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | const invalidArgs = require('../tools/invalid-args'); 5 | 6 | const after = (ms, value) => new Promise((resolve) => { 7 | setTimeout(() => resolve(value), ms); 8 | }); 9 | 10 | describe('.find()', function () { 11 | it('should return a rejected promise if invalid arguments are given', function () { 12 | const testRejected = (value) => { 13 | expect(value).to.not.be.an.instanceof(River); 14 | expect(value).to.be.an.instanceof(Promise); 15 | return expect(value).to.be.rejectedWith(TypeError); 16 | }; 17 | return Promise.all(invalidArgs().map(args => testRejected(River.from(['a']).find(...args)))); 18 | }); 19 | it('should propagate rejections to the returned promise', function () { 20 | const err = new Error('foobar'); 21 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 22 | return expect(river.find(() => true)).to.be.rejectedWith(err); 23 | }); 24 | it('should invoke the callback to find an item', function () { 25 | let invokedWith = ''; 26 | const source = River.from(['a', 'b', 'c', 'd']); 27 | const promise = source.find((x) => { 28 | invokedWith += x; 29 | return (x === 'c' || x === 'd') ? Promise.resolve(true) : Promise.resolve(false); 30 | }); 31 | return expect(promise).to.become('c').then(() => { 32 | expect(invokedWith).to.equal('abcd'); 33 | return expect(source).to.be.rejectedWith(River.Cancellation); 34 | }); 35 | }); 36 | it('should find the first item to resolve to true', function () { 37 | let invokedWith = ''; 38 | const source = River.from(['a', 'b', 'c', 'd']); 39 | const promise = source.find((x) => { 40 | invokedWith += x; 41 | if (x === 'c') return after(20, true); 42 | if (x === 'd') return after(10, true); 43 | return Promise.resolve(false); 44 | }); 45 | return expect(promise).to.become('d').then(() => { 46 | expect(invokedWith).to.equal('abcd'); 47 | return expect(source).to.be.rejectedWith(River.Cancellation); 48 | }); 49 | }); 50 | it('should respect a given concurrency value', function () { 51 | let invokedWith = ''; 52 | const source = River.from(['a', 'b', 'c', 'd']); 53 | const promise = source.find(1, (x) => { 54 | invokedWith += x; 55 | return (x === 'c' || x === 'd') ? Promise.resolve(true) : Promise.resolve(false); 56 | }); 57 | return expect(promise).to.become('c').then(() => { 58 | expect(invokedWith).to.equal('abc'); 59 | return expect(source).to.be.rejectedWith(River.Cancellation); 60 | }); 61 | }); 62 | it('should reject with a NoDataError when no items match the predicate', function () { 63 | let invokedWith = ''; 64 | const source = River.from(['a', 'b', 'c', 'd']); 65 | const promise = source.find((x) => { 66 | invokedWith += x; 67 | return Promise.resolve(false); 68 | }); 69 | return Promise.all([ 70 | expect(source).to.become(undefined), 71 | expect(promise).to.be.rejectedWith(River.NoDataError), 72 | ]).then(() => { 73 | expect(invokedWith).to.equal('abcd'); 74 | }); 75 | }); 76 | it('should reject with a NoDataError when no items are provided', function () { 77 | let invokedWith = ''; 78 | const source = River.from([]); 79 | const promise = source.find((x) => { 80 | invokedWith += x; 81 | return Promise.resolve(true); 82 | }); 83 | return Promise.all([ 84 | expect(source).to.become(undefined), 85 | expect(promise).to.be.rejectedWith(River.NoDataError), 86 | ]).then(() => { 87 | expect(invokedWith).to.equal(''); 88 | }); 89 | }); 90 | it('should reject the promise if the handler throws', function () { 91 | const err = new Error('foobar'); 92 | let str = ''; 93 | return expect(River.from(['a', 'b', Promise.resolve('c')]).find((x) => { str += x; throw err; })) 94 | .to.be.rejectedWith(err) 95 | .then(() => new Promise(r => setImmediate(r))) 96 | .then(() => { expect(str).to.equal('a'); }); 97 | }); 98 | it('should reject the promise if the handler returns a rejected promise', function () { 99 | const err = new Error('foobar'); 100 | let str = ''; 101 | return expect(River.from(['a', new Promise(r => setImmediate(() => r('b'))), 'c']).find((x) => { str += x; return Promise.reject(err); })) 102 | .to.be.rejectedWith(err) 103 | .then(() => new Promise(r => setImmediate(r))) 104 | .then(() => { expect(str).to.equal('ac'); }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/44.includes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.includes()', function () { 10 | it('should propagate rejections to the returned promise', function () { 11 | const err = new Error('foobar'); 12 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 13 | return expect(river.includes(undefined)).to.be.rejectedWith(err); 14 | }); 15 | it('should be fulfilled with true when the given value is found', function () { 16 | const source = River.from(['a', 'b', 'c', 'd']); 17 | const promise = source.includes('c'); 18 | return expect(promise).to.become(true).then(() => { 19 | return expect(source).to.be.rejectedWith(River.Cancellation); 20 | }); 21 | }); 22 | it('should accept a promise of the value to search for', function () { 23 | const source = River.from(['a', 'b', 'c', 'd']); 24 | const promise = source.includes(after(20, 'c')); 25 | return expect(promise).to.become(true).then(() => { 26 | return expect(source).to.be.rejectedWith(River.Cancellation); 27 | }); 28 | }); 29 | it('should be fulfilled to false when the given value is not found', function () { 30 | const source = River.from(['a', 'b', 'c', 'd']); 31 | const promise = source.includes('e'); 32 | return expect(promise).to.become(false).then(() => { 33 | return expect(source).to.be.become(undefined); 34 | }); 35 | }); 36 | it('should reject the returned promise when the given promise is rejected', function () { 37 | const err = new Error('foobar'); 38 | const source = River.from(['a', 'b', 'c', 'd']); 39 | const promise = source.includes(new Promise((_, r) => setTimeout(() => r(err), 20))); 40 | return expect(promise).to.be.rejectedWith(err).then(() => { 41 | return expect(source).to.be.rejectedWith(River.Cancellation); 42 | }); 43 | }); 44 | it('should accept a custom equals function', function () { 45 | const equals = (x, y) => { 46 | expect(x).to.equal('c'); 47 | if (y === 'b') y = 'c'; 48 | else if (y === 'c') y = 'b'; 49 | return x === y; 50 | }; 51 | const test = (source, result, expectation) => { 52 | return expect(source.includes('c', equals)).to.become(result).then(() => { 53 | return expectation(expect(source)); 54 | }); 55 | }; 56 | return Promise.all([ 57 | test(River.from(['a', 'b', 'd']), true, x => x.to.be.rejectedWith(River.Cancellation)), 58 | test(River.from(['a', 'c', 'd']), false, x => x.to.become(undefined)), 59 | ]); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/45.first.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('.first()', function () { 6 | describe('with no arguments', function () { 7 | it('should propagate rejections to the returned promise', function () { 8 | const err = new Error('foobar'); 9 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 10 | return expect(river.first()).to.be.rejectedWith(err); 11 | }); 12 | it('should be fulfilled with the first item in the river', function () { 13 | const source = River.from(['a', 'b', 'c', 'd']); 14 | return expect(source.first()).to.become('a').then(() => { 15 | return expect(source).to.be.rejectedWith(River.Cancellation); 16 | }); 17 | }); 18 | it('should be rejected with a NoDataError if the river is empty', function () { 19 | const source = River.from([]); 20 | return expect(source.first()).to.be.rejectedWith(River.NoDataError).then(() => { 21 | return expect(source).to.be.become(undefined); 22 | }); 23 | }); 24 | }); 25 | describe('with a count argument', function () { 26 | it('should return a rejected promise if an invalid count is given', function () { 27 | const test = (count) => { 28 | const source = River.from(['a', 'b', 'c']); 29 | const promise = source.first(count); 30 | return expect(promise).to.be.rejectedWith(TypeError) 31 | .then(() => expect(source).to.be.rejectedWith(River.Cancellation)); 32 | }; 33 | return Promise.all([ 34 | test(null), 35 | test(0), 36 | test(-2), 37 | test(2.000001), 38 | test(NaN), 39 | test(Infinity), 40 | test('2'), 41 | test('foobar'), 42 | ]); 43 | }); 44 | it('should propagate rejections to the returned promise', function () { 45 | const err = new Error('foobar'); 46 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 47 | return expect(river.first(3)).to.be.rejectedWith(err); 48 | }); 49 | it('should be fulfilled with an array of the first provided items', function () { 50 | const source1 = River.from(['a', 'b', 'c', 'd', 'e', 'f', 'g']); 51 | const source2 = River.from(['a', 'b', 'c']); 52 | return Promise.all([ 53 | expect(source1.first(3)).to.become(['a', 'b', 'c']) 54 | .then(() => expect(source1).to.be.rejectedWith(River.Cancellation)), 55 | expect(source2.first(3)).to.become(['a', 'b', 'c']) 56 | .then(() => expect(source2).to.be.rejectedWith(River.Cancellation)), 57 | ]); 58 | }); 59 | it('should be fulfilled with a smaller array when fewer items are provided', function () { 60 | const source = River.from(['a', 'b']); 61 | return expect(source.first(3)).to.become(['a', 'b']).then(() => { 62 | return expect(source).to.become(undefined); 63 | }); 64 | }); 65 | it('should be fulfilled with an empty array when no items are provided', function () { 66 | const source = River.from([]); 67 | return expect(source.first(3)).to.become([]).then(() => { 68 | return expect(source).to.become(undefined); 69 | }); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/46.last.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('.last()', function () { 6 | describe('with no arguments', function () { 7 | it('should propagate rejections to the returned promise', function () { 8 | const err = new Error('foobar'); 9 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 10 | return expect(river.last()).to.be.rejectedWith(err); 11 | }); 12 | it('should be fulfilled with the last item in the river', function () { 13 | const source = River.from(['a', 'b', 'c', 'd']); 14 | return expect(source.last()).to.become('d'); 15 | }); 16 | it('should be rejected with a NoDataError if the river is empty', function () { 17 | const source = River.from([]); 18 | return expect(source.last()).to.be.rejectedWith(River.NoDataError); 19 | }); 20 | }); 21 | describe('with a count argument', function () { 22 | it('should return a rejected promise if an invalid count is given', function () { 23 | const test = (count) => { 24 | const source = River.from(['a', 'b', 'c']); 25 | const promise = source.last(count); 26 | return expect(promise).to.be.rejectedWith(TypeError) 27 | .then(() => expect(source).to.be.rejectedWith(River.Cancellation)); 28 | }; 29 | return Promise.all([ 30 | test(null), 31 | test(0), 32 | test(-2), 33 | test(2.000001), 34 | test(NaN), 35 | test(Infinity), 36 | test('2'), 37 | test('foobar'), 38 | ]); 39 | }); 40 | it('should propagate rejections to the returned promise', function () { 41 | const err = new Error('foobar'); 42 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 43 | return expect(river.last(3)).to.be.rejectedWith(err); 44 | }); 45 | it('should be fulfilled with an array of the last provided items', function () { 46 | return Promise.all([ 47 | expect(River.from(['a', 'b', 'c', 'd', 'e', 'f', 'g']).last(3)) 48 | .to.become(['e', 'f', 'g']), 49 | expect(River.from(['a', 'b', 'c']).last(3)) 50 | .to.become(['a', 'b', 'c']), 51 | ]); 52 | }); 53 | it('should be fulfilled with a smaller array when fewer items are provided', function () { 54 | const source = River.from(['a', 'b']); 55 | return expect(source.last(3)).to.become(['a', 'b']); 56 | }); 57 | it('should be fulfilled with an empty array when no items are provided', function () { 58 | const source = River.from([]); 59 | return expect(source.last(3)).to.become([]); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/47.drain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.drain()', function () { 10 | it('should propagate rejections to the returned promise', function () { 11 | const err = new Error('foobar'); 12 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 13 | return expect(river.drain()).to.be.rejectedWith(err); 14 | }); 15 | it('should be fulfilled with undefined', function () { 16 | const source = River.from(['a', Promise.resolve('b'), 'c']); 17 | return Promise.all([ 18 | expect(source.drain()).to.become(undefined), 19 | expect(source).to.become(undefined), 20 | ]); 21 | }); 22 | it('should wait for all items to be consumed', function () { 23 | const source = River.from([after(20), after(60), after(100)]); 24 | const startTime = Date.now(); 25 | return Promise.all([ 26 | expect(source.drain()).to.become(undefined), 27 | expect(source).to.become(undefined), 28 | ]).then(() => { 29 | expect(Date.now() - startTime).to.be.within(80, 120); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/48.drop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.drop()', function () { 10 | it('should cancel the river if not yet consumed', function () { 11 | const river = River.from(['a']); 12 | const result = river.drop(); 13 | expect(river).to.equal(result); 14 | return expect(river).to.be.rejectedWith(River.Cancellation); 15 | }); 16 | it('should do nothing if the river was consumed', function () { 17 | const river = River.from(['a']); 18 | river.pump(() => {}); 19 | const result = river.drop(); 20 | expect(river).to.equal(result); 21 | return expect(river).to.become(undefined); 22 | }); 23 | it('should do nothing if the river is already resolved', function () { 24 | const river = River.from([]); 25 | const result = river.drop(); 26 | expect(river).to.equal(result); 27 | return expect(river).to.become(undefined); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/49.stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const makeIterable = require('../tools/make-iterable'); 4 | const River = require('../.'); 5 | const WisePromise = River.Promise; 6 | 7 | describe('.stream()', function () { 8 | describe('for WiseRiver', function () { 9 | it('should just return the same river', function () { 10 | let str = ''; 11 | const river = River.from(['a', 'b', 'c']); 12 | const result = river.stream(); 13 | expect(river).to.equal(result); 14 | river.pump(x => str += x); 15 | return expect(river).to.become(undefined).then(() => { 16 | expect(str).to.equal('abc'); 17 | }); 18 | }); 19 | }); 20 | describe('for WisePromise', function () { 21 | it('should propagate rejections to the returned river', function () { 22 | const err = new Error('foobar'); 23 | const promise = new WisePromise((_, r) => setTimeout(() => r(err), 10)); 24 | const river = promise.stream(); 25 | expect(river).to.be.an.instanceof(River); 26 | return expect(river).to.be.rejectedWith(err); 27 | }); 28 | it('should return a river that emits each item in the obtained array', function () { 29 | const data = []; 30 | const river = WisePromise.resolve([['foo'], 'bar', 'baz']).stream(); 31 | expect(river).to.be.an.instanceof(River); 32 | river.pump(item => data.push(item)); 33 | return river.then(() => { 34 | expect(data).to.deep.equal([['foo'], 'bar', 'baz']); 35 | }); 36 | }); 37 | it('should work with promises of non-array iterable objects', function () { 38 | const data = []; 39 | const river = WisePromise.resolve(makeIterable([['foo'], 'bar', 'baz'])).stream(); 40 | expect(river).to.be.an.instanceof(River); 41 | river.pump(item => data.push(item)); 42 | return river.then(() => { 43 | expect(data).to.deep.equal([['foo'], 'bar', 'baz']); 44 | }); 45 | }); 46 | it('should return a rejected river if an iterable is not obtained', function () { 47 | const river = WisePromise.resolve(123).stream(); 48 | expect(river).to.be.an.instanceof(River); 49 | return expect(river).to.be.rejectedWith(TypeError); 50 | }); 51 | it('should return a rejected river if iteration throws an exception', function () { 52 | const err = new Error('foobar'); 53 | const river = WisePromise.resolve({ [Symbol.iterator]: () => ({ next() { throw err; } }) }).stream(); 54 | expect(river).to.be.an.instanceof(River); 55 | return expect(river).to.be.rejectedWith(err); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/50.decouple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | const after = (ms, value) => new Promise((resolve) => { 6 | setTimeout(() => resolve(value), ms); 7 | }); 8 | 9 | describe('.decouple()', function () { 10 | it('should propagate rejections to the returned river', function () { 11 | const err = new Error('foobar'); 12 | const river = River.one(new Promise((_, r) => setTimeout(() => r(err), 10))); 13 | return expect(river.decouple()).to.be.rejectedWith(err); 14 | }); 15 | it('should not propagate cancellations back to the source river', function () { 16 | const source = River.from([after(20), after(60), after(100)]); 17 | const dest = source.decouple(); 18 | dest.pump(() => {})(); 19 | const startTime = Date.now(); 20 | return Promise.all([ 21 | expect(source).to.become(undefined), 22 | expect(dest).to.be.rejectedWith(River.Cancellation), 23 | ]).then(() => { 24 | expect(Date.now() - startTime).to.be.within(80, 120); 25 | }); 26 | }); 27 | it('should return a river with the same contents as the source', function () { 28 | let str = ''; 29 | const source = River.from(['a', 'b', 'c']); 30 | const dest = source.decouple(); 31 | expect(source).to.not.equal(dest); 32 | dest.pump(x => str += x); 33 | return expect(dest).to.become(undefined).then(() => { 34 | expect(str).to.equal('abc'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/51.async-iterator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const River = require('../.'); 4 | 5 | describe('[Symbol.asyncIterator]()', function () { 6 | it('should be tested'); 7 | }); 8 | -------------------------------------------------------------------------------- /test/52.riverify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const { Writable } = require('stream'); 4 | const makeStream = require('../tools/make-stream'); 5 | const River = require('../.'); 6 | 7 | describe('River.riverify()', function () { 8 | describe('for unsupported values', function () { 9 | it('should return a rejected river', function () { 10 | const test = (...args) => expect(River.riverify(...args)).to.be.rejectedWith(TypeError); 11 | return Promise.all([ 12 | test(), 13 | test(undefined), 14 | test(null), 15 | test('foo'), 16 | test({}), 17 | test(123), 18 | test(NaN), 19 | test(Symbol()), 20 | test([]), 21 | test(() => {}), 22 | test(new Writable), 23 | ]); 24 | }); 25 | }); 26 | describe('for stream objects', function () { 27 | it('should return an empty river for an empty stream', function () { 28 | const stream = makeStream([]); 29 | const river = River.riverify(stream); 30 | let str = ''; 31 | river.pump(x => str += x); 32 | return expect(river).to.become(undefined).then(() => { 33 | expect(str).to.equal(''); 34 | expect(stream.dead).to.be.true; 35 | }); 36 | }); 37 | it('should return a river emitting the same data as the stream', function () { 38 | const stream = makeStream(['a', 'b', 'c', 'd', 'e']); 39 | const river = River.riverify(stream); 40 | let str = ''; 41 | river.pump(x => str += x); 42 | return expect(river).to.become(undefined).then(() => { 43 | expect(str).to.equal('abcde'); 44 | expect(stream.dead).to.be.true; 45 | }); 46 | }); 47 | it('should propagate errors emitted by the stream', function () { 48 | const err = new Error('foobar'); 49 | const stream = makeStream(['a', 'b', err, 'd', 'e']); 50 | const river = River.riverify(stream); 51 | let str = ''; 52 | river.pump(x => str += x); 53 | return expect(river).to.be.rejectedWith(err).then(() => { 54 | expect(str).to.equal('ab'); 55 | expect(stream.dead).to.be.true; 56 | }); 57 | }); 58 | it('should return a rejected river if the stream is destroyed prematurely', function () { 59 | const stream = makeStream(['a', 'b', 'c', 'd', 'e']); 60 | const river = River.riverify(stream); 61 | let str = ''; 62 | river.pump((x) => { 63 | str += x; 64 | if (x === 'c') stream.destroy(); 65 | }); 66 | return expect(river).to.be.rejectedWith(River.DataError).then(() => { 67 | expect(str).to.equal('abc'); 68 | expect(stream.dead).to.be.true; 69 | }); 70 | }); 71 | it('should respect the "decouple" option', function () { 72 | const stream = makeStream(['a', 'b', 'c', 'd', 'e']); 73 | const river = River.riverify(stream, { decouple: true }); 74 | let str = ''; 75 | river.pump(x => str += x); 76 | return expect(river).to.become(undefined).then(() => { 77 | expect(str).to.equal('abcde'); 78 | expect(stream.dead).to.be.false; 79 | }); 80 | }); 81 | }); 82 | describe('for async iterable objects', function () { 83 | it('should be tested'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/fast-queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { expect } = require('chai'); 3 | const FastQueue = require('../lib/fast-queue'); 4 | const shared = require('../lib/shared'); 5 | 6 | describe('[shared.push]', function () { 7 | it('should maintain cyclic buffer continuity', function () { 8 | const fq = new FastQueue((resolve) => resolve()) 9 | fq[shared.push](0) 10 | expect(fq[shared.shift]()).to.equal(0) 11 | for (let i = 0; i < 17; i++) { 12 | fq[shared.push](i) 13 | } 14 | for (let i = 0; !fq[shared.isEmpty](); i++) { 15 | expect(fq[shared.shift]()).to.equal(i) 16 | } 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tools/invalid-args.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = () => [ 4 | [], 5 | [undefined], 6 | ['foo'], 7 | [{}], 8 | [123], 9 | [Symbol()], 10 | [4.000001, () => {}], 11 | [-1, () => {}], 12 | [NaN, () => {}], 13 | [Infinity, () => {}], 14 | [0xffffffff + 1, () => {}], 15 | ['foo', () => {}], 16 | ['2px', () => {}], 17 | [{}, () => {}], 18 | [() => 2, () => {}], 19 | [() => {}, 4.000001], 20 | [() => {}, -1], 21 | [() => {}, NaN], 22 | [() => {}, Infinity], 23 | [() => {}, 0xffffffff + 1], 24 | [() => {}, 'foo'], 25 | [() => {}, '2px'], 26 | [() => {}, {}], 27 | [() => {}, () => 2], 28 | [() => {}, Symbol()], 29 | [0, 'foo'], 30 | [0, {}], 31 | [0, 123], 32 | ['foo', 0], 33 | [{}, 0], 34 | [123, 0], 35 | [undefined, undefined] 36 | ]; 37 | 38 | module.exports.callbackOnly = () => module.exports().filter(([x]) => typeof x !== 'function'); 39 | -------------------------------------------------------------------------------- /tools/make-iterable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Given an array or array-like object, this function returns an iterable object 4 | // that iterates directly through the array. The array is not copied or cloned. 5 | module.exports = (arr) => { 6 | const obj = {}; 7 | obj[Symbol.iterator] = () => { 8 | let i = 0; 9 | return { next: () => { 10 | return i < arr.length 11 | ? { done: false, value: arr[i++] } 12 | : (i = NaN, { done: true, value: undefined }); 13 | } }; 14 | }; 15 | return obj; 16 | }; 17 | -------------------------------------------------------------------------------- /tools/make-stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Readable } = require('stream'); 3 | const items = Symbol(); 4 | const finished = Symbol(); 5 | const dead = Symbol(); 6 | 7 | class Stream extends Readable { 8 | constructor(iterable) { 9 | super({ objectMode: true, highWaterMark: 2, autoDestroy: false }); 10 | this[items] = Array.from(iterable); 11 | this[finished] = false; 12 | this[dead] = false; 13 | } 14 | get dead() { 15 | return this[dead]; 16 | } 17 | _read() { 18 | setImmediate(function flow() { 19 | if (this[finished]) return; 20 | if (this[items].length === 0) { 21 | this.push(null); 22 | finish(this); 23 | return; 24 | } 25 | let item = this[items].shift(); 26 | if (item instanceof Error) return void this.destroy(item); 27 | if (item === null) item = {}; 28 | if (this.push(item)) setImmediate(flow.bind(this)); 29 | }.bind(this)); 30 | } 31 | _destroy(err, cb) { 32 | if (this[dead]) return void cb(); 33 | finish(this); 34 | this[dead] = true; 35 | setImmediate(() => { this.emit('close'); }); 36 | cb(err); 37 | } 38 | } 39 | 40 | const finish = (self) => { 41 | self[finished] = true; 42 | self[items] = []; 43 | }; 44 | 45 | // Given an iterable object, this function returns a readable stream that emits 46 | // the items found in the iterable object. 47 | module.exports = (...args) => new Stream(...args); 48 | --------------------------------------------------------------------------------