├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── package.json ├── rollup.config.js ├── src ├── config.js ├── gurgle.js ├── helpers │ └── isClosed.js ├── operators │ ├── bufferWithCount.js │ ├── combineLatest.js │ ├── debounce.js │ ├── delay.js │ ├── distinctUntilChanged.js │ ├── filter.js │ ├── flatMap.js │ ├── flatMapLatest.js │ ├── map.js │ ├── merge.js │ ├── pairwise.js │ ├── scan.js │ ├── skipCurrent.js │ ├── take.js │ ├── tap.js │ ├── throttle.js │ └── until.js ├── sources │ ├── fromAjax.js │ ├── fromEvent.js │ ├── fromGeolocation.js │ ├── fromPromise.js │ ├── of.js │ └── requestAnimationFrame.js └── stream.js └── test └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 2, "tab", { "SwitchCase": 1 } ], 4 | "quotes": [ 2, "single" ], 5 | "linebreak-style": [ 2, "unix" ], 6 | "semi": [ 2, "always" ], 7 | "keyword-spacing": [ 2, { "before": true, "after": true } ], 8 | "space-before-blocks": [ 2, "always" ], 9 | "space-before-function-paren": [ 2, "always" ], 10 | "no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ], 11 | "no-cond-assign": [ 0 ] 12 | }, 13 | "env": { 14 | "es6": true, 15 | "browser": true, 16 | "mocha": true, 17 | "node": true 18 | }, 19 | "extends": "eslint:recommended", 20 | "parserOptions": { 21 | "ecmaVersion": 6, 22 | "sourceType": "module" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # gurgle changelog 2 | 3 | ## 0.2.0 4 | 5 | * Streams close on error 6 | * Add `fromAjax` and `of` sources 7 | * Call callbacks with initial value, if stream has started 8 | * Various fixes 9 | 10 | ## 0.1.9 11 | 12 | * Fix build 13 | 14 | ## 0.1.7-8 15 | 16 | * Oops 17 | 18 | ## 0.1.6 19 | 20 | * Add `interval` option to `fromGeolocation` 21 | 22 | ## 0.1.5 23 | 24 | * Add `fromGeolocation` 25 | 26 | ## 0.1.1-4 27 | 28 | * various 29 | 30 | ## 0.1.0 31 | 32 | * first version 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gurgle 2 | 3 | Like [RxJS](https://github.com/Reactive-Extensions/RxJS/), but for normal people. 4 | 5 | 6 | ## Really? Why? 7 | 8 | The Reactive Extensions for JavaScript, or RxJS, give us a new way to think about how data flows through your application. Rather than gaffer taping your application together with listeners that respond to discrete *events*, Rx allows us to think about *streams* of values. The power of this approach becomes obvious when you manipulate and combine streams – throttling keypresses in a search field and discarding obsolete AJAX responses, for example. 9 | 10 | On a recent project, I decided to use RxJS to avoid what would otherwise have quickly become a spaghetti mess of imperative code. But I'm not used to thinking in streams, and found myself fighting the library at every turn. The Reactive mantra that **anything can be stream** (from this [excellent introduction](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)) soon gave way to **everything must be a stream**. Figuring out how to use Reactive concepts alongside more familiar techniques was beyond me in the limited time I had available before my deadline. 11 | 12 | So I did the only sensible thing and wrote my own library, with the following goals: 13 | 14 | * **tiny** – if you're using a modern module bundler such as Rollup or Webpack 2, any unused operators will be discarded. Like a tightly-optimised custom build, except with zero configuration 15 | * **readable** – you should be able to read the source code and understand exactly what is going on 16 | * **easy** – ideological purity may be sacrificed in the name of getting stuff done 17 | * **familiar** – operators are modelled after their RxJS counterparts. If you're familiar with `flatMapLatest` and `distinctUntilChanged` and friends, you'll know exactly what is going on 18 | 19 | ### You should use Gurgle if... 20 | 21 | * You want to dabble with streams but can't afford to take several frustrating weeks to rewire your brain 22 | * You want to introduce streams into an existing codebase gradually 23 | * You don't want to add a hefty dependency to your app 24 | 25 | ### You shouldn't use Gurgle if... 26 | 27 | * You need the full functionality of RxJS (right now, Gurgle is missing lots of operators and sources. You should contribute!) 28 | * Your app is extremely performance-sensitive (RxJS has been battle-tested at places like Netflix, whereas this... well, I wrote it in a couple of afternoons. It doesn't (yet) have concepts like schedulers and backpressure) 29 | * You enjoy ideological bunfights about the true nature of FRP and suchlike 30 | 31 | 32 | ## Installation 33 | 34 | ```bash 35 | npm install --save gurgle 36 | ``` 37 | 38 | ## Usage 39 | 40 | You can import individual functions into your app using this ES6 syntax... 41 | 42 | ```js 43 | import { stream, map, filter } from 'gurgle'; 44 | ``` 45 | 46 | ...but for the sake of convenience, we'll do it like this in all of the examples: 47 | 48 | ```js 49 | import * as g from 'gurgle'; 50 | ``` 51 | 52 | This is equivalent to `var g = require('gurgle')` if you're old school. (It comes with a UMD build, so you can also use it with an AMD loader or plain ol' script tags.) 53 | 54 | 55 | ### Your first stream 56 | 57 | You can create a stream of arbitrary values like so: 58 | 59 | ```js 60 | const stream = g.stream( () => { 61 | // this callback is optional – if provided, will be 62 | // called when the stream is closed. Use it for 63 | // detaching event handlers etc 64 | console.log( 'cleaning up' ); 65 | }); 66 | 67 | // All streams have a `push` method, which returns `this` 68 | stream.push( 'a' ); 69 | stream.value; // 'a' – the most recently pushed value 70 | 71 | // the second and third arguments to `stream.subscribe` 72 | // are optional. If a value has been pushed to the stream 73 | // the stream is considered to have 'started', and the 74 | // callback will be called with the current stream value 75 | stream.subscribe( 76 | value => console.log( `the value is ${value}` ), 77 | error => console.log( `oh noes! ${error.message}` ), 78 | () => console.log( `closing the stream` ) 79 | ); 80 | // -> 'the value is a' 81 | 82 | stream.push( 'b' ); 83 | // -> 'the value is b' 84 | 85 | stream.push( 'c' ).push( 'd' ).close(); 86 | // -> 'the value is c' 87 | // -> 'the value is d' 88 | // -> 'cleaning up' 89 | // -> 'closing the stream' 90 | ``` 91 | 92 | Once a stream is closed, you cannot push more values to it, and the subscriber functions will no longer be called. 93 | 94 | 95 | ### Error handling 96 | 97 | Calling `stream.error(...)` will cause any error handlers to be invoked, **and will close the stream**: 98 | 99 | ```js 100 | const anotherStream = g.stream( () => { 101 | console.log( 'cleaning up' ); 102 | }); 103 | 104 | anotherStream.subscribe( 105 | value => console.log( `the value is ${value}` ), 106 | error => console.log( `oh noes! ${error.message}` ), 107 | () => console.log( `closing the stream` ) 108 | ); 109 | 110 | anotherStream.error( new Error( 'something went wrong' ) ); 111 | // -> 'oh noes! something went wrong' 112 | // -> 'closing the stream' 113 | ``` 114 | 115 | 116 | ### Stream sources 117 | 118 | Gurgle provides convenient ways to create common streams: 119 | 120 | ```js 121 | // fromAjax – accepts a URL and an options object. Self-closing 122 | const ajaxStream = g.fromAjax( 'http://example.com/data.json', { 123 | responseType: 'json' 124 | }); 125 | 126 | // fromEvent – accepts a DOM node (or `window`) and 127 | // an event type. Removes event listener when stream 128 | // is closed 129 | const eventStream = g.fromEvent( window, 'mousemove' ); 130 | 131 | // fromPromise – creates a stream from a Promise, which 132 | // auto-closes once the promise resolves 133 | const promiseStream = g.fromPromise( doSomethingAsync() ); 134 | 135 | // fromGeolocation – watches user's location at a specified interval 136 | // or using watchPosition 137 | const geolocationStream = g.fromGeolocation({ 138 | interval: 10 * 1e3, 139 | onerror ( err ) { 140 | // without this, geolocation errors will close the stream 141 | } 142 | }); 143 | 144 | // requestAnimationFrame – creates a stream that updates every frame 145 | // with a tick value 146 | const frame = g.requestAnimationFrame(); 147 | 148 | // more to come... 149 | ``` 150 | 151 | ### Operators 152 | 153 | In Gurgle, operators on streams are standalone functions that return streams: 154 | 155 | ```js 156 | const input = g.stream(); 157 | const mapped = g.map( input, x => ... ); 158 | const filtered = g.filter( mapped, x => ... ); 159 | ``` 160 | 161 | This works well and is inherently highly *composable*, but if you prefer chaining, you can do so with the `pipe` method: 162 | 163 | ```js 164 | const input = g.stream(); 165 | const filtered = input 166 | .pipe( g.map, x => ... ) 167 | .pipe( g.filter, x => ... ); 168 | ``` 169 | 170 | Consult the wiki for details about individual operators. 171 | 172 | ## License 173 | 174 | MIT 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gurgle", 3 | "version": "0.2.0", 4 | "author": "Rich Harris", 5 | "repository": "https://github.com/Rich-Harris/gurgle", 6 | "license": "MIT", 7 | "main": "dist/gurgle.umd.js", 8 | "jsnext:main": "dist/gurgle.es.js", 9 | "files": [ 10 | "dist", 11 | "README.md" 12 | ], 13 | "devDependencies": { 14 | "buble": "^0.14.0", 15 | "eslint": "^3.0.1", 16 | "mocha": "^3.1.0", 17 | "rollup": "^0.36.1", 18 | "rollup-plugin-buble": "^0.14.0", 19 | "source-map-support": "^0.4.2" 20 | }, 21 | "scripts": { 22 | "build": "rollup -c", 23 | "dev": "rollup -c -w", 24 | "prepublish": "npm run lint && npm run test", 25 | "test": "mocha test/test.js --compilers js:buble/register", 26 | "pretest": "npm run build", 27 | "lint": "eslint src" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble'; 2 | 3 | export default { 4 | entry: 'src/gurgle.js', 5 | plugins: [ buble() ], 6 | moduleName: 'gurgle', 7 | targets: [ 8 | { format: 'es', dest: 'dist/gurgle.es.js' }, 9 | { format: 'umd', dest: 'dist/gurgle.umd.js' } 10 | ], 11 | sourceMap: true 12 | }; 13 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | let P = typeof Promise !== 'undefined' ? Promise : null; 2 | 3 | export { P as Promise }; 4 | 5 | export function usePromise ( Promise ) { 6 | P = Promise; 7 | } 8 | -------------------------------------------------------------------------------- /src/gurgle.js: -------------------------------------------------------------------------------- 1 | export { default as stream } from './stream.js'; 2 | 3 | // sources 4 | export { default as fromAjax } from './sources/fromAjax.js'; 5 | export { default as fromEvent } from './sources/fromEvent.js'; 6 | export { default as fromGeolocation } from './sources/fromGeolocation.js'; 7 | export { default as fromPromise } from './sources/fromPromise.js'; 8 | export { default as of } from './sources/of.js'; 9 | export { default as requestAnimationFrame } from './sources/requestAnimationFrame.js'; 10 | 11 | // operators 12 | export { default as bufferWithCount } from './operators/bufferWithCount.js'; 13 | export { default as combineLatest } from './operators/combineLatest.js'; 14 | export { default as debounce } from './operators/debounce.js'; 15 | export { default as delay } from './operators/delay.js'; 16 | export { default as distinctUntilChanged } from './operators/distinctUntilChanged.js'; 17 | export { default as filter } from './operators/filter.js'; 18 | export { default as flatMap } from './operators/flatMap.js'; 19 | export { default as flatMapLatest } from './operators/flatMapLatest.js'; 20 | export { default as map } from './operators/map.js'; 21 | export { default as merge } from './operators/merge.js'; 22 | export { default as pairwise } from './operators/pairwise.js'; 23 | export { default as scan } from './operators/scan.js'; 24 | export { default as skipCurrent } from './operators/skipCurrent.js'; 25 | export { default as take } from './operators/take.js'; 26 | export { default as tap } from './operators/tap.js'; 27 | export { default as throttle } from './operators/throttle.js'; 28 | export { default as until } from './operators/until.js'; 29 | 30 | // misc 31 | export { usePromise } from './config.js'; 32 | -------------------------------------------------------------------------------- /src/helpers/isClosed.js: -------------------------------------------------------------------------------- 1 | export default function isClosed ( stream ) { 2 | return stream.closed; 3 | } 4 | -------------------------------------------------------------------------------- /src/operators/bufferWithCount.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function bufferWithCount ( source, count, skip = count ) { 4 | const destination = stream(); 5 | 6 | let itemsUntilNewBuffer = 1; 7 | let buffers = []; 8 | 9 | source.subscribe( 10 | value => { 11 | if ( --itemsUntilNewBuffer === 0 ) { 12 | buffers.push([]); 13 | itemsUntilNewBuffer = skip; 14 | } 15 | 16 | buffers.forEach( buffer => buffer.push( value ) ); 17 | if ( buffers[0].length === count ) destination.push( buffers.shift() ); 18 | }, 19 | destination.error, 20 | () => { 21 | buffers.forEach( buffer => destination.push( buffer ) ); 22 | destination.close(); 23 | } 24 | ); 25 | 26 | return destination; 27 | } 28 | -------------------------------------------------------------------------------- /src/operators/combineLatest.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | import isClosed from '../helpers/isClosed.js'; 3 | 4 | function getValue ( stream ) { 5 | return stream.value; 6 | } 7 | 8 | export default function combineLatest () { 9 | const numArguments = arguments.length; 10 | 11 | const lastArgument = arguments[ numArguments - 1 ]; 12 | const fn = typeof lastArgument === 'function' && lastArgument; 13 | const end = fn ? numArguments - 1 : numArguments; 14 | 15 | let sources = []; 16 | for ( let i = 0; i < end; i += 1 ) { 17 | sources.push( arguments[i] ); 18 | } 19 | 20 | const destination = stream(); 21 | 22 | function push () { 23 | const values = sources.map( getValue ); 24 | destination.push( fn ? fn.apply( null, values ) : values ); 25 | } 26 | 27 | function error ( err ) { 28 | destination.error( err ); 29 | } 30 | 31 | function close () { 32 | if ( sources.every( isClosed ) ) destination.close(); 33 | } 34 | 35 | sources.forEach( source => { 36 | source.subscribe( push, error, close ); 37 | }); 38 | 39 | return destination; 40 | } 41 | -------------------------------------------------------------------------------- /src/operators/debounce.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function debounce ( source, ms = 250 ) { 4 | const destination = stream(); 5 | 6 | let latestValue; 7 | let timeout; 8 | 9 | source.subscribe( 10 | value => { 11 | latestValue = value; 12 | clearTimeout( timeout ); 13 | 14 | timeout = setTimeout( () => { 15 | destination.push( latestValue ); 16 | }, ms ); 17 | }, 18 | destination.error, 19 | destination.close 20 | ); 21 | 22 | return destination; 23 | } 24 | -------------------------------------------------------------------------------- /src/operators/delay.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function delay ( source, ms ) { 4 | const destination = stream(); 5 | 6 | source.subscribe( 7 | value => { 8 | setTimeout( () => { 9 | if ( !destination.closed ) destination.push( value ); 10 | }, ms ); 11 | }, 12 | destination.error, 13 | () => setTimeout( destination.close, ms ) 14 | ); 15 | 16 | return destination; 17 | } 18 | -------------------------------------------------------------------------------- /src/operators/distinctUntilChanged.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function distinctUntilChanged ( source ) { 4 | const destination = stream(); 5 | 6 | let latestValue; 7 | 8 | source.subscribe( 9 | value => { 10 | if ( value === latestValue ) return; 11 | latestValue = value; 12 | 13 | destination.push( value ); 14 | }, 15 | destination.error, 16 | destination.close 17 | ); 18 | 19 | return destination; 20 | } 21 | -------------------------------------------------------------------------------- /src/operators/filter.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function filter ( source, fn ) { 4 | const destination = stream(); 5 | 6 | source.subscribe( 7 | value => { 8 | if ( fn( value ) ) destination.push( value ); 9 | }, 10 | destination.error, 11 | destination.close 12 | ); 13 | 14 | return destination; 15 | } 16 | -------------------------------------------------------------------------------- /src/operators/flatMap.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function flatMap ( source, streamGenerator ) { 4 | const destination = stream(); 5 | 6 | let childSources = []; 7 | let closed = false; 8 | 9 | source.subscribe( 10 | value => { 11 | const child = streamGenerator( value ); 12 | childSources.push( child ); 13 | 14 | child.subscribe( 15 | value => { 16 | if ( !closed ) destination.push( value ); 17 | }, 18 | destination.error, 19 | () => { 20 | const index = childSources.indexOf( child ); 21 | if ( ~index ) childSources.splice( index, 1 ); 22 | } 23 | ); 24 | }, 25 | destination.error, 26 | () => { 27 | closed = true; 28 | 29 | childSources.forEach( child => child.close() ); 30 | childSources = []; 31 | 32 | destination.close(); 33 | } 34 | ); 35 | 36 | return destination; 37 | } 38 | -------------------------------------------------------------------------------- /src/operators/flatMapLatest.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function flatMap ( source, streamGenerator ) { 4 | const destination = stream(); 5 | 6 | let latest = 0; 7 | 8 | source.subscribe( 9 | value => { 10 | latest += 1; 11 | 12 | const token = latest; 13 | const next = streamGenerator( value ); 14 | 15 | next.subscribe( value => { 16 | if ( token === latest ) destination.push( value ); 17 | }); 18 | }, 19 | destination.error, 20 | destination.close 21 | ); 22 | 23 | return destination; 24 | } 25 | -------------------------------------------------------------------------------- /src/operators/map.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function map ( source, fn ) { 4 | const destination = stream(); 5 | 6 | source.subscribe( 7 | value => { 8 | destination.push( fn( value ) ); 9 | }, 10 | destination.error, 11 | destination.close 12 | ); 13 | 14 | return destination; 15 | } 16 | -------------------------------------------------------------------------------- /src/operators/merge.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | import isClosed from '../helpers/isClosed.js'; 3 | 4 | export default function merge ( ...sources ) { 5 | const destination = stream(); 6 | 7 | function close () { 8 | if ( sources.every( isClosed ) ) destination.close(); 9 | } 10 | 11 | sources.forEach( source => { 12 | source.subscribe( 13 | value => { 14 | destination.push( value ); 15 | }, 16 | destination.error, 17 | close 18 | ); 19 | }); 20 | 21 | return destination; 22 | } 23 | -------------------------------------------------------------------------------- /src/operators/pairwise.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function pairwise ( source ) { 4 | const destination = stream(); 5 | 6 | let last = null; 7 | let hasStarted = false; 8 | 9 | source.subscribe( 10 | value => { 11 | if ( hasStarted ) destination.push( [ last, value ] ); 12 | hasStarted = true; 13 | 14 | last = value; 15 | }, 16 | destination.error, 17 | destination.close 18 | ); 19 | 20 | return destination; 21 | } 22 | -------------------------------------------------------------------------------- /src/operators/scan.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function scan ( source, fn, acc ) { 4 | const destination = stream(); 5 | 6 | destination.push( acc ); 7 | 8 | source.subscribe( 9 | value => { 10 | acc = fn( destination.value, value ); 11 | destination.push( acc ); 12 | }, 13 | destination.error, 14 | destination.close 15 | ); 16 | 17 | return destination; 18 | } 19 | -------------------------------------------------------------------------------- /src/operators/skipCurrent.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function skipCurrent ( source ) { 4 | if ( !source.started ) return source; 5 | 6 | const destination = stream(); 7 | let started = false; 8 | 9 | source.subscribe( 10 | value => { 11 | if ( started ) destination.push( value ); 12 | }, 13 | destination.error, 14 | destination.close 15 | ); 16 | 17 | started = true; 18 | return destination; 19 | } 20 | -------------------------------------------------------------------------------- /src/operators/take.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function take ( source, count ) { 4 | const destination = stream(); 5 | 6 | source.subscribe( 7 | value => { 8 | if ( destination.closed ) return; 9 | 10 | destination.push( value ); 11 | count -= 1; 12 | 13 | if ( count === 0 ) destination.close(); 14 | }, 15 | 16 | destination.error, 17 | destination.close 18 | ); 19 | 20 | return destination; 21 | } 22 | -------------------------------------------------------------------------------- /src/operators/tap.js: -------------------------------------------------------------------------------- 1 | export default function tap ( source, fn ) { 2 | source.subscribe( fn ); 3 | return source; 4 | } 5 | -------------------------------------------------------------------------------- /src/operators/throttle.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function throttle ( source, ms = 250 ) { 4 | const destination = stream(); 5 | 6 | let blocked = false; 7 | 8 | source.subscribe( 9 | value => { 10 | if ( blocked ) return; 11 | blocked = true; 12 | 13 | destination.push( value ); 14 | setTimeout( () => blocked = false, ms ); 15 | }, 16 | destination.error, 17 | destination.close 18 | ); 19 | 20 | return destination; 21 | } 22 | -------------------------------------------------------------------------------- /src/operators/until.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function until ( source, signal ) { 4 | const destination = stream(); 5 | 6 | const sourceSubscriber = source.subscribe( 7 | value => { 8 | destination.push( value ); 9 | }, 10 | destination.error, 11 | destination.close 12 | ); 13 | 14 | let initial = true; 15 | 16 | const signalSubscriber = signal.subscribe( 17 | () => { 18 | if ( initial ) return; 19 | 20 | sourceSubscriber.cancel(); 21 | signalSubscriber.cancel(); 22 | destination.close(); 23 | }, 24 | destination.error 25 | ); 26 | 27 | initial = false; 28 | return destination; 29 | } 30 | -------------------------------------------------------------------------------- /src/sources/fromAjax.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function fromAjax ( url, options = {} ) { 4 | let closed = false; 5 | const source = stream( () => closed = true ); 6 | 7 | const xhr = new XMLHttpRequest(); 8 | xhr.open( options.method || 'GET', url ); 9 | 10 | xhr.responseType = options.responseType || 'text'; 11 | 12 | xhr.onerror = source.error; 13 | 14 | xhr.onload = () => { 15 | if ( closed ) return; 16 | 17 | source.push( xhr.response ); 18 | source.close(); 19 | }; 20 | 21 | xhr.send(); 22 | 23 | return source; 24 | } 25 | -------------------------------------------------------------------------------- /src/sources/fromEvent.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function fromEvent ( node, type ) { 4 | const source = stream( () => { 5 | node.removeEventListener( type, source.push, false ); 6 | }); 7 | 8 | node.addEventListener( type, source.push, false ); 9 | 10 | return source; 11 | } 12 | -------------------------------------------------------------------------------- /src/sources/fromGeolocation.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function fromGeolocation ( options = {} ) { 4 | const interval = options.interval; 5 | let closed = false; 6 | let watchId; 7 | 8 | const source = stream( () => { 9 | if ( interval ) { 10 | closed = true; 11 | } else { 12 | navigator.geolocation.clearWatch( watchId ); 13 | } 14 | }); 15 | 16 | if ( interval ) { 17 | const onsuccess = position => { 18 | if ( closed ) return; 19 | 20 | source.push( position ); 21 | setTimeout( check, interval ); 22 | }; 23 | 24 | const onerror = error => { 25 | if ( options.onerror ) { 26 | options.onerror( error ); 27 | setTimeout( check, interval ); 28 | } else { 29 | source.error( error ); 30 | } 31 | }; 32 | 33 | const check = () => { 34 | if ( closed ) return; 35 | navigator.geolocation.getCurrentPosition( onsuccess, onerror, options ); 36 | }; 37 | 38 | check(); 39 | } else { 40 | const onerror = error => { 41 | if ( options.onerror ) { 42 | options.onerror( error ); 43 | } else { 44 | source.error( error ); 45 | } 46 | }; 47 | 48 | watchId = navigator.geolocation.watchPosition( source.push, onerror, options ); 49 | } 50 | 51 | return source; 52 | } 53 | -------------------------------------------------------------------------------- /src/sources/fromPromise.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function fromPromise ( promise ) { 4 | let closed = false; 5 | const source = stream( () => closed = true ); 6 | 7 | promise 8 | .then( value => !closed && source.push( value ) ) 9 | .catch( err => !closed && source.error( err ) ) 10 | .then( () => source.close() ); 11 | 12 | return source; 13 | } 14 | -------------------------------------------------------------------------------- /src/sources/of.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function of ( value ) { 4 | const source = stream().push( value ); 5 | source.close(); 6 | 7 | return source; 8 | } 9 | -------------------------------------------------------------------------------- /src/sources/requestAnimationFrame.js: -------------------------------------------------------------------------------- 1 | import stream from '../stream.js'; 2 | 3 | export default function rAF () { 4 | const loop = value => { 5 | if ( !source.closed ) { 6 | source.push( value ); 7 | requestAnimationFrame( loop ); 8 | } 9 | }; 10 | 11 | const source = stream(); 12 | loop(); 13 | 14 | return source; 15 | } 16 | -------------------------------------------------------------------------------- /src/stream.js: -------------------------------------------------------------------------------- 1 | import { Promise } from './config.js'; 2 | 3 | const noop = () => {}; 4 | 5 | export default function stream ( onclose = noop ) { 6 | let subscribers = []; 7 | 8 | // `stream.done` resolves once the stream is closed. 9 | // Handy for testing etc 10 | let fulfil; 11 | let reject; 12 | 13 | let errored = false; 14 | let error = null; 15 | 16 | const done = new Promise( ( f, r ) => { 17 | fulfil = f; 18 | reject = r; 19 | }); 20 | 21 | const s = { 22 | __gurgle: true, 23 | 24 | // properties 25 | closed: false, 26 | done, 27 | started: false, 28 | value: undefined, 29 | 30 | // methods 31 | close () { 32 | if ( s.closed ) return; 33 | 34 | onclose(); 35 | 36 | Object.defineProperty( s, 'closed', { 37 | value: true, 38 | writable: false, 39 | enumerable: true 40 | }); 41 | 42 | if ( errored ) { 43 | reject( error ); 44 | } else { 45 | fulfil( s.value ); 46 | } 47 | 48 | subscribers.forEach( subscriber => { 49 | if ( subscriber.onclose ) subscriber.onclose(); 50 | }); 51 | 52 | subscribers = null; 53 | }, 54 | 55 | debug ( label = 'gurgle' ) { 56 | label = `[${label}]`; 57 | 58 | return s.subscribe( value => { 59 | console.log( label, value ); // eslint-disable-line no-console 60 | }, err => { 61 | console.error( label, err ); // eslint-disable-line no-console 62 | }, () => { 63 | console.log( label, 'closed stream' ); // eslint-disable-line no-console 64 | }); 65 | }, 66 | 67 | error ( err ) { 68 | let caught = false; 69 | subscribers.forEach( subscriber => { 70 | if ( subscriber.onerror ) { 71 | caught = true; 72 | subscriber.onerror( err ); 73 | } 74 | }); 75 | 76 | error = err; 77 | s.close(); 78 | 79 | if ( !caught ) throw err; 80 | }, 81 | 82 | pipe ( fn, ...args ) { 83 | return fn( s, ...args ); 84 | }, 85 | 86 | push ( value ) { 87 | if ( s.closed ) throw new Error( 'Cannot push to a closed stream' ); 88 | 89 | const previousValue = s.value; 90 | s.value = value; 91 | 92 | subscribers.forEach( subscriber => { 93 | subscriber.onvalue( value, previousValue ); 94 | }); 95 | 96 | s.started = true; 97 | 98 | return s; 99 | }, 100 | 101 | subscribe ( onvalue, onerror, onclose ) { 102 | const callbacks = { onvalue, onerror, onclose }; 103 | 104 | if ( errored ) { 105 | if ( onerror ) onerror( error ); 106 | } else { 107 | if ( s.started ) onvalue( s.value ); 108 | 109 | if ( s.closed ) { 110 | if ( onclose ) onclose(); 111 | } else { 112 | subscribers.push( callbacks ); 113 | } 114 | } 115 | 116 | return { 117 | cancel () { 118 | if ( s.closed ) return; 119 | const index = subscribers.indexOf( callbacks ); 120 | if ( ~index ) subscribers.splice( index, 1 ); 121 | } 122 | }; 123 | } 124 | }; 125 | 126 | return s; 127 | } 128 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global require, describe, it */ 2 | 3 | const assert = require( 'assert' ); 4 | const g = require( '../' ); 5 | 6 | require( 'source-map-support' ).install(); 7 | 8 | // fake requestAnimationFrame for node 9 | let time = 1; 10 | global.requestAnimationFrame = function ( cb ) { 11 | process.nextTick( () => cb( time++ ) ); 12 | }; 13 | 14 | describe( 'gurgle', () => { 15 | describe( 'stream', () => { 16 | it( 'calls a callback on close', () => { 17 | let closed = false; 18 | const stream = g.stream( () => closed = true ); 19 | stream.close(); 20 | assert.ok( closed ); 21 | }); 22 | 23 | describe( 'push', () => { 24 | it( 'updates stream.value', () => { 25 | const stream = g.stream(); 26 | 27 | stream.push( 'a' ); 28 | assert.equal( stream.value, 'a' ); 29 | stream.close(); 30 | }); 31 | 32 | it( 'is chainable', () => { 33 | const stream = g.stream(); 34 | 35 | stream.push( 'a' ).push( 'b' ); 36 | assert.equal( stream.value, 'b' ); 37 | stream.close(); 38 | }); 39 | }); 40 | 41 | describe( 'close', () => { 42 | it( 'closes a stream', () => { 43 | const stream = g.stream(); 44 | 45 | assert.ok( !stream.closed ); 46 | stream.close(); 47 | assert.ok( stream.closed ); 48 | 49 | assert.throws( function () { 50 | stream.push( 'x' ); 51 | }, /Cannot push to a closed stream/ ); 52 | }); 53 | }); 54 | 55 | describe( 'subscribe', () => { 56 | it( 'subscribes to a stream', () => { 57 | const stream = g.stream(); 58 | 59 | let value, error, closed; 60 | 61 | stream.subscribe( 62 | v => value = v, 63 | e => error = e, 64 | () => closed = true 65 | ); 66 | 67 | stream.push( 42 ); 68 | stream.error( new Error( 'oh noes!' ) ); 69 | stream.close(); 70 | 71 | assert.equal( value, 42 ); 72 | assert.equal( error.message, 'oh noes!' ); 73 | assert.ok( closed ); 74 | }); 75 | 76 | it( 'returns an object with a `cancel` method', () => { 77 | const stream = g.stream(); 78 | 79 | let value, closed; 80 | 81 | const subscriber = stream.subscribe( 82 | v => value = v, 83 | () => {}, 84 | () => closed = true 85 | ); 86 | 87 | stream.push( 42 ); 88 | 89 | subscriber.cancel(); 90 | 91 | stream.push( 99 ); 92 | stream.close(); 93 | 94 | assert.equal( value, 42 ); 95 | assert.ok( !closed ); 96 | 97 | subscriber.cancel(); // second time should be a noop 98 | }); 99 | 100 | it( 'immediately calls value handler with value, but only if stream has started', () => { 101 | const stream1 = g.stream().push( 1 ); 102 | const stream2 = g.stream(); 103 | 104 | let called1 = false; 105 | let called2 = false; 106 | 107 | stream1.subscribe( () => called1 = true ); 108 | stream2.subscribe( () => called2 = true ); 109 | 110 | assert.ok( called1 ); 111 | assert.ok( !called2 ); 112 | }); 113 | 114 | it( 'calls error handler if stream is in error state', () => { 115 | const stream = g.stream(); 116 | 117 | let called = false; 118 | stream.subscribe( 119 | () => {}, 120 | err => assert.equal( ( called = true, err.message ), 'oops' ) 121 | ); 122 | 123 | stream.error( new Error( 'oops' ) ); 124 | assert.ok( called ); 125 | }); 126 | 127 | it( 'calls value and close handler if stream has closed', () => { 128 | const stream = g.stream().push( 42 ); 129 | stream.close(); 130 | 131 | let value; 132 | let closed; 133 | 134 | stream.subscribe( 135 | v => { 136 | assert.ok( !closed ); // value handler fired first 137 | value = v; 138 | }, 139 | () => {}, 140 | () => closed = true 141 | ); 142 | 143 | assert.equal( value, 42 ); 144 | assert.ok( closed ); 145 | }); 146 | }); 147 | 148 | describe( 'error', () => { 149 | it( 'closes a stream', () => { 150 | const stream = g.stream(); 151 | 152 | let error = null; 153 | let closed = false; 154 | 155 | stream.subscribe( 156 | () => {}, 157 | e => error = e, 158 | () => closed = true 159 | ); 160 | 161 | stream.error( new Error( 'womp womp' ) ); 162 | 163 | assert.equal( error.message, 'womp womp' ); 164 | assert.ok( closed ); 165 | assert.ok( stream.closed ); 166 | }); 167 | }); 168 | }); 169 | 170 | describe( 'sources', () => { 171 | describe( 'fromEvent', () => { 172 | it( 'creates a stream of DOM events', () => { 173 | const fakeDomNode = { 174 | listeners: {}, 175 | addEventListener: ( type, listener ) => { 176 | ( fakeDomNode.listeners[ type ] || ( fakeDomNode.listeners[ type ] = [] ) ).push( listener ); 177 | }, 178 | removeEventListener: ( type, listener ) => { 179 | const group = fakeDomNode.listeners[ type ]; 180 | if ( !group ) return; 181 | const index = group.indexOf( listener ); 182 | if ( ~index ) group.splice( index, 1 ); 183 | }, 184 | trigger: ( type, event ) => { 185 | const group = fakeDomNode.listeners[ type ]; 186 | if ( !group ) return; 187 | group.forEach( fn => fn( event ) ); 188 | } 189 | }; 190 | 191 | const stream = g.fromEvent( fakeDomNode, 'mousemove' ); 192 | stream.subscribe( event => { 193 | assert.equal( event.type, 'mousemove' ); 194 | assert.equal( event.clientX, 1 ); 195 | }); 196 | 197 | fakeDomNode.trigger( 'mousemove', { type: 'mousemove', clientX: 1 }); 198 | stream.close(); 199 | fakeDomNode.trigger( 'mousemove', { type: 'mousemove', clientX: 2 }); 200 | assert.equal( fakeDomNode.listeners.mousemove.length, 0 ); 201 | }); 202 | }); 203 | 204 | describe( 'fromPromise', () => { 205 | it( 'creates a stream from a promise', () => { 206 | let fulfil; 207 | const promise = new Promise( f => fulfil = f ); 208 | const stream = g.fromPromise( promise ); 209 | 210 | let value; 211 | stream.subscribe( v => value = v ); 212 | 213 | fulfil( 42 ); 214 | 215 | return stream.done.then( () => { 216 | assert.equal( value, 42 ); 217 | }); 218 | }); 219 | 220 | it( 'handles rejections', () => { 221 | let reject; 222 | const promise = new Promise( ( f, r ) => reject = r ); 223 | const stream = g.fromPromise( promise ); 224 | 225 | stream.subscribe( () => {}, () => {} ); 226 | 227 | reject( new Error( 'something went wrong' ) ); 228 | 229 | return stream.done.catch( err => { 230 | assert.equal( err.message, 'something went wrong' ); 231 | }); 232 | }); 233 | 234 | it( 'ignores resolutions after the stream is closed', () => { 235 | let fulfil; 236 | const promise = new Promise( f => fulfil = f ); 237 | const stream = g.fromPromise( promise ); 238 | 239 | let value; 240 | stream.subscribe( v => value = v ); 241 | 242 | stream.close(); 243 | fulfil( 42 ); 244 | 245 | return promise.then( () => { 246 | assert.equal( value, undefined ); 247 | }); 248 | }); 249 | }); 250 | 251 | describe( 'of', () => { 252 | it( 'creates a stream with a single value', () => { 253 | const stream = g.of( 42 ); 254 | assert.equal( stream.value, 42 ); 255 | }); 256 | 257 | it( 'closes immediately', () => { 258 | const stream = g.of( 42 ); 259 | assert.ok( stream.closed ); 260 | }); 261 | }); 262 | 263 | describe( 'requestAnimationFrame', () => { 264 | const stream = g.requestAnimationFrame(); 265 | 266 | assert.equal( stream.value, null ); 267 | 268 | let values = []; 269 | stream.subscribe( value => { 270 | values.push( value ); 271 | if ( values.length >= 3 ) stream.close(); 272 | }); 273 | 274 | return stream.done.then( () => { 275 | assert.equal( values.length, 3 ); 276 | assert.ok( typeof values[0] === 'number' ); 277 | assert.ok( typeof values[1] === 'number' ); 278 | assert.ok( typeof values[2] === 'number' ); 279 | assert.ok( values[2] > values[1] ); 280 | assert.ok( values[1] > values[0] ); 281 | }); 282 | }); 283 | }); 284 | 285 | describe( 'operators', () => { 286 | describe( 'bufferWithCount', () => { 287 | it( 'chunks input stream up into buffers of the specified size', () => { 288 | const source = g.stream(); 289 | const dest = g.bufferWithCount( source, 3 ); 290 | 291 | let results = []; 292 | dest.subscribe( value => results.push( value ) ); 293 | 294 | for ( let i = 0; i < 9; i += 1 ) { 295 | source.push( i ); 296 | } 297 | 298 | source.close(); 299 | assert.deepEqual( results, [ 300 | [ 0, 1, 2 ], 301 | [ 3, 4, 5 ], 302 | [ 6, 7, 8 ] 303 | ]); 304 | }); 305 | 306 | it( 'chunks input stream up into buffers of the specified size and with a specified offset', () => { 307 | const source = g.stream(); 308 | const dest = g.bufferWithCount( source, 3, 1 ); 309 | 310 | let results = []; 311 | dest.subscribe( value => results.push( value ) ); 312 | 313 | for ( let i = 0; i < 9; i += 1 ) { 314 | source.push( i ); 315 | } 316 | 317 | source.close(); 318 | assert.deepEqual( results, [ 319 | [ 0, 1, 2 ], 320 | [ 1, 2, 3 ], 321 | [ 2, 3, 4 ], 322 | [ 3, 4, 5 ], 323 | [ 4, 5, 6 ], 324 | [ 5, 6, 7 ], 325 | [ 6, 7, 8 ], 326 | [ 7, 8 ], 327 | [ 8 ] 328 | ]); 329 | }); 330 | }); 331 | 332 | describe( 'combineLatest', () => { 333 | it( 'combines latest values', () => { 334 | const a = g.stream(); 335 | const b = g.stream(); 336 | 337 | const combined = a.pipe( g.combineLatest, b, ( a, b ) => a + b ); 338 | 339 | a.push( 'w' ); 340 | b.push( 1 ); 341 | 342 | a.push( 'x' ); 343 | b.push( 2 ); 344 | 345 | let results = []; 346 | combined.subscribe( value => results.push( value ) ); 347 | 348 | b.push( 3 ); 349 | a.push( 'y' ).push( 'z' ); 350 | b.push( 4 ); 351 | 352 | a.close(); 353 | b.close(); 354 | 355 | assert.deepEqual( results, [ 'x2', 'x3', 'y3', 'z3', 'z4' ]); 356 | }); 357 | }); 358 | 359 | describe( 'debounce', () => { 360 | it( 'waits until specified period of inactivity', () => { 361 | const source = g.stream(); 362 | const dest = g.debounce( source, 1 ); 363 | 364 | let results = []; 365 | dest.subscribe( value => results.push( value ) ); 366 | 367 | source.push( 'a' ).push( 'b' ).push( 'c' ).close(); 368 | 369 | dest.done.then( () => { 370 | assert.deepEqual( results, [ 'c' ]); 371 | }); 372 | }); 373 | }); 374 | 375 | describe( 'delay', () => { 376 | it( 'delays events', () => { 377 | const setTimeout = global.setTimeout; 378 | 379 | let task; 380 | let delay; 381 | global.setTimeout = ( t, d ) => { 382 | task = t; 383 | delay = d; 384 | }; 385 | 386 | const input = g.stream(); 387 | const output = g.delay( input, 250 ); 388 | 389 | input.push( 'x' ); 390 | 391 | assert.equal( delay, 250 ); 392 | assert.ok( !output.value ); 393 | 394 | task(); 395 | assert.equal( output.value, 'x' ); 396 | 397 | input.close(); 398 | assert.ok( !output.closed ); 399 | task(); 400 | assert.ok( output.closed ); 401 | 402 | global.setTimeout = setTimeout; 403 | }); 404 | }); 405 | 406 | describe( 'distinctUntilChanged', () => { 407 | it( 'ignores values that are identical to the previous one', () => { 408 | const source = g.stream(); 409 | const dest = g.distinctUntilChanged( source ); 410 | 411 | let results = []; 412 | dest.subscribe( value => results.push( value ) ); 413 | 414 | source 415 | .push( 1 ) 416 | .push( 2 ) 417 | .push( 3 ) 418 | .push( 3 ) 419 | .push( 2 ) 420 | .push( 3 ) 421 | .push( 2 ) 422 | .push( 2 ) 423 | .push( 1 ) 424 | .push( 1 ) 425 | .push( 1 ) 426 | .push( 4 ) 427 | .close(); 428 | 429 | assert.deepEqual( results, [ 1, 2, 3, 2, 3, 2, 1, 4 ]); 430 | }); 431 | }); 432 | 433 | describe( 'filter', () => { 434 | it( 'filters out values', () => { 435 | const source = g.stream(); 436 | const dest = g.filter( source, x => x % 2 ); 437 | 438 | let results = []; 439 | dest.subscribe( value => results.push( value ) ); 440 | 441 | source 442 | .push( 1 ) 443 | .push( 2 ) 444 | .push( 3 ) 445 | .push( 4 ) 446 | .push( 5 ) 447 | .push( 6 ) 448 | .push( 7 ) 449 | .push( 8 ) 450 | .push( 9 ) 451 | .close(); 452 | 453 | assert.deepEqual( results, [ 1, 3, 5, 7, 9 ]); 454 | }); 455 | }); 456 | 457 | describe( 'flatMap', () => { 458 | it( 'flattens a stream of streams into a single stream', () => { 459 | const input = g.stream(); 460 | 461 | let temp = []; 462 | const output = g.flatMap( input, value => { 463 | const stream = g.stream(); 464 | temp.push({ stream, value }); 465 | 466 | return stream; 467 | }); 468 | 469 | let results = []; 470 | output.subscribe( value => results.push( value ) ); 471 | 472 | input.push( 'a' ); 473 | temp[0].stream.push( temp[0].value.toUpperCase() ); 474 | 475 | input.push( 'b' ); 476 | input.push( 'c' ); 477 | temp[2].stream.push( temp[2].value.toUpperCase() ); // out of order 478 | temp[1].stream.push( temp[1].value.toUpperCase() ); 479 | 480 | input.close(); 481 | 482 | return output.done.then( () => { 483 | assert.deepEqual( results, [ 'A', 'C', 'B' ] ); 484 | }); 485 | }); 486 | 487 | it( 'disregards values from child streams after source stream has closed', () => { 488 | const input = g.stream(); 489 | 490 | let temp = []; 491 | const output = g.flatMap( input, value => { 492 | let closed = false; 493 | const stream = g.stream( () => { 494 | closed = true; 495 | }); 496 | 497 | temp.push({ 498 | push ( value ) { 499 | if ( !closed ) stream.push( value ); 500 | }, 501 | value 502 | }); 503 | 504 | return stream; 505 | }); 506 | 507 | let results = []; 508 | output.subscribe( value => results.push( value ) ); 509 | 510 | input.push( 'a' ); 511 | input.push( 'b' ); 512 | input.push( 'c' ); 513 | 514 | temp[0].push( temp[0].value.toUpperCase() ); 515 | 516 | input.close(); 517 | 518 | temp[1].push( temp[1].value.toUpperCase() ); 519 | temp[2].push( temp[2].value.toUpperCase() ); 520 | 521 | return output.done.then( () => { 522 | assert.deepEqual( results, [ 'A' ] ); 523 | }); 524 | }); 525 | }); 526 | 527 | describe( 'flatMapLatest', () => { 528 | it( 'ignores responses that arrive after later requests', () => { 529 | let temp = []; 530 | 531 | const input = g.stream(); 532 | const output = input.pipe( g.flatMapLatest, value => { 533 | const stream = g.stream(); 534 | temp.push({ stream, value }); 535 | 536 | return stream; 537 | }); 538 | 539 | let results = []; 540 | output.subscribe( value => results.push( value ) ); 541 | 542 | input.push( 'a' ); 543 | temp[0].stream.push( temp[0].value.toUpperCase() ); 544 | 545 | input.push( 'b' ); 546 | input.push( 'c' ); 547 | temp[2].stream.push( temp[2].value.toUpperCase() ); // out of order 548 | temp[1].stream.push( temp[1].value.toUpperCase() ); 549 | 550 | input.close(); 551 | 552 | return output.done.then( () => { 553 | assert.deepEqual( results, [ 'A', 'C' ] ); 554 | }); 555 | }); 556 | }); 557 | 558 | describe( 'map', () => { 559 | it( 'maps a stream', () => { 560 | const stream = g.stream(); 561 | const mapped = stream.pipe( g.map, x => x * x ); 562 | 563 | let results = []; 564 | mapped.subscribe( value => results.push( value ) ); 565 | 566 | stream.push( 1 ); 567 | stream.push( 2 ); 568 | stream.push( 3 ); 569 | 570 | stream.close(); 571 | 572 | assert.deepEqual( results, [ 1, 4, 9 ]); 573 | }); 574 | }); 575 | 576 | describe( 'merge', () => { 577 | it( 'merges streams', () => { 578 | const a = g.stream(); 579 | const b = g.stream(); 580 | 581 | const merged = g.merge( a, b ); 582 | 583 | a.push( 1 ); 584 | assert.equal( merged.value, 1 ); 585 | 586 | b.push( 2 ); 587 | assert.equal( merged.value, 2 ); 588 | 589 | a.push( 3 ); 590 | assert.equal( merged.value, 3 ); 591 | 592 | a.close(); 593 | b.close(); 594 | }); 595 | 596 | it( 'closes the merged stream when all inputs are closed', () => { 597 | const a = g.stream(); 598 | const b = g.stream(); 599 | 600 | const merged = g.merge( a, b ); 601 | assert.ok( !merged.closed ); 602 | 603 | a.close(); 604 | assert.ok( !merged.closed ); 605 | 606 | b.close(); 607 | assert.ok( merged.closed ); 608 | }); 609 | }); 610 | 611 | describe( 'pairwise', () => { 612 | it( 'combines sequential values into a sequence of arrays', () => { 613 | const a = g.stream(); 614 | 615 | const pairwise = g.pairwise( a ); 616 | 617 | assert.equal( pairwise.value, null ); 618 | a.push( 'a' ); 619 | assert.equal( pairwise.value, null ); 620 | a.push( 'b' ); 621 | assert.deepEqual( pairwise.value, [ 'a', 'b' ] ); 622 | a.push( 'c' ); 623 | assert.deepEqual( pairwise.value, [ 'b', 'c' ] ); 624 | 625 | a.close(); 626 | assert.ok( pairwise.closed ); 627 | }); 628 | }); 629 | 630 | describe( 'scan', () => { 631 | it( 'accumulates values', () => { 632 | const input = g.stream(); 633 | const output = input.pipe( g.scan, ( prev, next ) => prev + next, 0 ); 634 | 635 | assert.equal( output.value, 0 ); 636 | 637 | input.push( 1 ); 638 | assert.equal( output.value, 1 ); 639 | 640 | input.push( 2 ); 641 | assert.equal( output.value, 3 ); 642 | 643 | input.push( 3 ); 644 | assert.equal( output.value, 6 ); 645 | 646 | input.close(); 647 | }); 648 | }); 649 | 650 | describe( 'skipCurrent', () => { 651 | it( 'skips any existing value', () => { 652 | const input = g.stream().push( 1 ); 653 | const output = g.skipCurrent( input ); 654 | 655 | assert.ok( !output.value ); 656 | input.push( 2 ); 657 | assert.equal( output.value, 2 ); 658 | }); 659 | }); 660 | 661 | describe( 'take', () => { 662 | it( 'takes the specified number of values', () => { 663 | const input = g.stream(); 664 | const output = g.take( input, 3 ); 665 | 666 | input.push( 1 ).push( 2 ).push( 3 ).push( 4 ).push( 5 ); 667 | 668 | assert.equal( output.value, 3 ); 669 | assert.ok( output.closed ); 670 | }); 671 | }); 672 | 673 | describe( 'tap', () => { 674 | it( 'calls specified function on values', () => { 675 | const input = g.stream(); 676 | const list = []; 677 | const output = g.tap( input, value => list.push( value ) ); 678 | 679 | input.push( 1 ).push( 2 ).push( 3 ); 680 | 681 | assert.deepEqual( list, [ 1, 2, 3 ]); 682 | assert.strictEqual( input, output ); 683 | }); 684 | }); 685 | 686 | describe( 'throttle', () => { 687 | it( 'throttles a stream', () => { 688 | const input = g.stream(); 689 | const output = g.throttle( input, 10 ); 690 | 691 | input.push( 1 ); 692 | input.push( 2 ); 693 | input.push( 3 ); 694 | assert.equal( output.value, 1 ); 695 | 696 | setTimeout( () => { 697 | assert.equal( output.value, 1 ); 698 | input.push( 4 ); 699 | assert.equal( output.value, 4 ); 700 | 701 | input.close(); 702 | }, 20 ); 703 | 704 | return output.done; 705 | }); 706 | }); 707 | 708 | describe( 'until', () => { 709 | it( 'continues a stream until a signal ends it', () => { 710 | const input = g.stream(); 711 | const signal = g.stream(); 712 | const output = g.until( input, signal ); 713 | 714 | const list = []; 715 | 716 | output.subscribe( value => list.push( value ) ); 717 | 718 | input.push( 1 ).push( 2 ).push( 3 ); 719 | signal.push(); 720 | input.push( 4 ).push( 5 ).push( 6 ); 721 | 722 | assert.deepEqual( list, [ 1, 2, 3 ] ); 723 | assert.ok( output.closed ); 724 | }); 725 | }); 726 | }); 727 | }); 728 | --------------------------------------------------------------------------------