├── .gitignore ├── package.json ├── lib ├── BreezeHelpers.js ├── BreezeCallable.js └── Breeze.js ├── LICENSE ├── test └── index.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breeze", 3 | "version": "1.2.2", 4 | "description": "Functional async flow control library", 5 | "main": "breeze.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha --colors -R list" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Nijikokun/breeze.git" 12 | }, 13 | "keywords": [ 14 | "flow", 15 | "async", 16 | "ocaml", 17 | "functional", 18 | "haskell", 19 | "waterfall", 20 | "try", 21 | "catch", 22 | "promises", 23 | "thennable", 24 | "bluebird", 25 | "q" 26 | ], 27 | "author": "Nijiko Yonskai", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/Nijikokun/breeze/issues" 31 | }, 32 | "homepage": "https://github.com/Nijikokun/breeze#readme", 33 | "devDependencies": { 34 | "mocha": "^2.2.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/BreezeHelpers.js: -------------------------------------------------------------------------------- 1 | class BreezeHelpers { 2 | static isFunction (obj) { 3 | return typeof obj === 'function' 4 | } 5 | 6 | static isPromise (obj) { 7 | return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' 8 | } 9 | 10 | static toCallback (callable) { 11 | return (args) => Promise.resolve().then(() => this.isPromise(callable) ? callable : callable(args)) 12 | } 13 | 14 | static toConditional (conditional) { 15 | return BreezeHelpers.isFunction(conditional) ? conditional : () => conditional 16 | } 17 | 18 | static toCallbackSignature (callable) { 19 | return values => callable(values) 20 | } 21 | 22 | static benchmark (benchmark) { 23 | let time = Date.now() 24 | 25 | return benchmark ? { 26 | startedAt: benchmark.startedAt, 27 | endedAt: time, 28 | diff: Math.round(time - benchmark.startedAt) 29 | } : { startedAt: time } 30 | } 31 | } 32 | 33 | module.exports = BreezeHelpers 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nijiko Yonskai 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 | -------------------------------------------------------------------------------- /lib/BreezeCallable.js: -------------------------------------------------------------------------------- 1 | const BreezeHelpers = require('./BreezeHelpers') 2 | 3 | class BreezeCallable { 4 | constructor (obj) { 5 | this.id = obj.method ? obj.method.name : false 6 | this.type = obj.type 7 | this.error = false 8 | this.resolution = false 9 | this.benchmark = BreezeHelpers.benchmark() 10 | 11 | this.setMethod(obj.method) 12 | 13 | if (obj.conditional) { 14 | this.setConditional(obj.conditional) 15 | } 16 | } 17 | 18 | setMethod (method) { 19 | this.method = (args) => { 20 | this.benchmark = BreezeHelpers.benchmark(this.benchmark) 21 | 22 | if (this.conditional && !this.conditional(args)) { 23 | this.failed = true 24 | this.benchmark = BreezeHelpers.benchmark(this.benchmark) 25 | return args 26 | } 27 | 28 | return BreezeHelpers.toCallback(method)(args).then(rargs => { 29 | this.args = args 30 | this.resolution = rargs 31 | this.benchmark = BreezeHelpers.benchmark(this.benchmark) 32 | return rargs 33 | }).catch(error => { 34 | this.args = args 35 | this.errored = error 36 | this.benchmark = BreezeHelpers.benchmark(this.benchmark) 37 | throw error 38 | }) 39 | } 40 | } 41 | 42 | setConditional (conditional) { 43 | this.conditional = BreezeHelpers.toConditional(conditional) 44 | } 45 | } 46 | 47 | module.exports = BreezeCallable 48 | -------------------------------------------------------------------------------- /lib/Breeze.js: -------------------------------------------------------------------------------- 1 | const BreezeHelpers = require('./BreezeHelpers') 2 | const BreezeCallable = require('./BreezeCallable') 3 | 4 | class Breeze { 5 | constructor () { 6 | this.tree = [] 7 | this.promise = Promise.resolve() 8 | this.skipAmount = false 9 | this.skipIdentifiers = [] 10 | } 11 | 12 | toSkippableCallable (callable) { 13 | return (args) => { 14 | if (this.skipAmount > 0) { 15 | callable.skipped = true 16 | this.skipAmount -= 1 17 | return args 18 | } 19 | 20 | return callable.method(args) 21 | } 22 | } 23 | 24 | add (callable) { 25 | this.tree.push(callable) 26 | this.promise = this.promise.then(this.toSkippableCallable(callable)) 27 | return this 28 | } 29 | 30 | addCallable (type, method, conditional) { 31 | return this.add(new BreezeCallable({ type: type, method: method, conditional: conditional })) 32 | } 33 | 34 | id (name) { 35 | let entry = this.tree[this.tree.length - 1] 36 | if (entry) entry.id = name 37 | return this 38 | } 39 | 40 | skip (amount) { 41 | this.skipAmount += amount 42 | return this 43 | } 44 | 45 | all (promises) { 46 | return this.addCallable('all', Promise.all(promises)) 47 | } 48 | 49 | get (index) { 50 | return this.addCallable('get', (args) => args[index > 0 ? index : args.length - 1]) 51 | } 52 | 53 | tap (method) { 54 | return this.addCallable('tap', (args) => { method(args); return args }) 55 | } 56 | 57 | when (conditional, method) { 58 | return this.addCallable('when', method, conditional) 59 | } 60 | 61 | then (method) { 62 | return this.addCallable('then', method) 63 | } 64 | 65 | spread (method) { 66 | return this.addCallable('spread', (args) => method.apply(this, args)) 67 | } 68 | 69 | return (value) { 70 | return this.addCallable('return', BreezeHelpers.toConditional(value)) 71 | } 72 | 73 | throw (reason) { 74 | return this.addCallable('throw', BreezeHelpers.toConditional(reason)) 75 | } 76 | 77 | map (promises) { 78 | let map = [] 79 | return this.each(promises, result => map.push(result) && [].concat(map)) 80 | } 81 | 82 | catch () { 83 | let callable = new BreezeCallable({ type: 'catch' }) 84 | 85 | if (!arguments[1]) { 86 | callable.setMethod(arguments[0]) 87 | } else { 88 | callable.setMethod(e => { 89 | if (e instanceof arguments[0]) { 90 | return arguments[1].call(this, e) 91 | } 92 | }) 93 | } 94 | 95 | this.tree.push(callable) 96 | this.promise = this.promise.catch(callable.method) 97 | 98 | return this 99 | } 100 | 101 | each (promises, callable) { 102 | promises.forEach(promise => { 103 | return this 104 | .add(new BreezeCallable({ type: 'each', method: promise })) 105 | .add(new BreezeCallable({ type: 'each-then', method: callable })) 106 | }) 107 | 108 | return this 109 | } 110 | } 111 | 112 | module.exports = Breeze 113 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const Breeze = require('../lib/Breeze') 2 | const assert = require('assert') 3 | 4 | const describe = global.describe 5 | const it = global.it 6 | 7 | describe('Breeze', function () { 8 | it('returns promise chain', function () { 9 | var flow = new Breeze() 10 | 11 | assert(typeof flow.then === 'function') 12 | assert(typeof flow.catch === 'function') 13 | }) 14 | 15 | it('returns promise helpers', function () { 16 | var flow = new Breeze() 17 | 18 | assert(typeof flow.id === 'function') 19 | assert(typeof flow.all === 'function') 20 | assert(typeof flow.map === 'function') 21 | assert(typeof flow.get === 'function') 22 | assert(typeof flow.tap === 'function') 23 | assert(typeof flow.when === 'function') 24 | assert(typeof flow.each === 'function') 25 | assert(typeof flow.spread === 'function') 26 | assert(typeof flow.return === 'function') 27 | assert(typeof flow.throw === 'function') 28 | assert(typeof flow.map === 'function') 29 | }) 30 | 31 | it('passes values through then chain', done => { 32 | var flow = new Breeze() 33 | 34 | flow.then(() => { 35 | return true 36 | }) 37 | 38 | flow.then(result => { 39 | assert(typeof result === 'boolean') 40 | assert(result === true) 41 | done() 42 | }) 43 | 44 | flow.catch(done) 45 | }) 46 | 47 | it('properly handles promises in the chain', done => { 48 | var flow = new Breeze() 49 | 50 | flow.then(new Promise((resolve, reject) => { 51 | assert(typeof resolve === 'function') 52 | assert(typeof reject === 'function') 53 | resolve(true) 54 | })) 55 | 56 | flow.then(result => { 57 | assert(typeof result === 'boolean') 58 | assert(result === true) 59 | done() 60 | }) 61 | 62 | flow.catch(done) 63 | }) 64 | 65 | it('catches errors thrown in chain', done => { 66 | var flow = new Breeze() 67 | 68 | flow.then(() => { 69 | throw new Error('testing') 70 | }) 71 | 72 | flow.then(result => { 73 | done(new Error('This step should not have ran')) 74 | }) 75 | 76 | flow.catch(e => { 77 | assert(e instanceof Error) 78 | assert(e.message === 'testing') 79 | done() 80 | }) 81 | }) 82 | 83 | it('catches errors in rejected promises', done => { 84 | var flow = new Breeze() 85 | 86 | flow.then(new Promise((resolve, reject) => { 87 | reject(new Error('testing')) 88 | })) 89 | 90 | flow.then(result => { 91 | done(new Error('This step should not have ran')) 92 | }) 93 | 94 | flow.catch(e => { 95 | assert(e instanceof Error) 96 | assert(e.message === 'testing') 97 | done() 98 | }) 99 | }) 100 | 101 | it('differentiates custom error objects from normal errors', done => { 102 | var flow = new Breeze() 103 | var CustomError = function (message) { 104 | this.name = 'CustomError' 105 | this.message = message || 'Default Message' 106 | this.stack = (new Error()).stack 107 | } 108 | 109 | CustomError.prototype = Object.create(Error.prototype) 110 | CustomError.prototype.constructor = CustomError 111 | 112 | flow.then(new Promise((resolve, reject) => { 113 | reject(new CustomError('testing')) 114 | })) 115 | 116 | flow.then(result => { 117 | done(new Error('This step should not have ran')) 118 | }) 119 | 120 | flow.catch(CustomError, e => { 121 | assert(e instanceof CustomError) 122 | assert(e.message === 'testing') 123 | done() 124 | }) 125 | 126 | flow.catch(e => { 127 | done(new Error('Improperly caught custom error object')) 128 | }) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Breeze 2 | 3 | Functional async flow control library built on promises. Managing promises and async code has never been easier. 4 | 5 | [![version][npm-version]][npm-url] 6 | [![License][npm-license]][license-url] 7 | [![Downloads][npm-downloads]][npm-url] 8 | [![Dependencies][david-image]][david-url] 9 | 10 | ## Features 11 | 12 | - Small footprint 13 | - Native promise support 14 | - No chaining required 15 | - Benchmarking (yes, even Promises) 16 | - Logging (Chain logs, argument logs, and more...) 17 | 18 | ## Install 19 | 20 | - Download [the latest package][download] 21 | - NPM: `npm install breeze` 22 | 23 | ## Usage 24 | 25 | **Node.js / Browserify / Webpack** 26 | 27 | ```js 28 | const Breeze = require('breeze') 29 | ``` 30 | 31 | ## Documentation 32 | 33 | ### Breeze Flow 34 | 35 | ```js 36 | import Breeze from 'breeze' 37 | 38 | let flow = new Breeze() 39 | ``` 40 | 41 | ### Breeze Flow Instance Methods 42 | 43 | #### then(method: Function|Promise) 44 | 45 | Add step to flow chain. 46 | 47 | ```js 48 | flow 49 | .then(value => 'function with return value') 50 | .then(value => console.log('function says:', value)) 51 | .then(new Promise((resolve, reject) => { 52 | return resolve('Promise resolution') 53 | })) 54 | .then(value => console.log('promise says:', value)) 55 | ``` 56 | 57 | **Note:** You are not required to chain instance methods. 58 | 59 | ```js 60 | flow.then(value => 'function with return value') 61 | flow.then(value => console.log('function says:', value)) 62 | ``` 63 | 64 | #### catch(type?: ErrorType, handler: Function) 65 | 66 | Handle chain rejections. Accepts an optional custom error type to capture specific rejections in the flow chain. 67 | 68 | ```js 69 | flow.then(() => throw new Error('Spoiler Alert')) 70 | 71 | flow.catch(CustomError, err => console.log('Specialized Catch:', err)) 72 | 73 | flow.catch(err => console.log('Generic Catch:', err)) 74 | ``` 75 | 76 | #### id(name: String) 77 | 78 | Identify a step. Useful for benchmarking and logs. 79 | 80 | ```js 81 | // Create a flow step 82 | flow.then(results => client.get('/users')) 83 | 84 | // Identify step for benchmarking and logs 85 | flow.id('fetch-users') 86 | ``` 87 | 88 | #### each(promises: Array, method: Function) 89 | 90 | Invoke method on results of each Promise in the given Array. 91 | 92 | **Todo:** Support previous chain `Array` value. 93 | 94 | #### all(promises: Array) 95 | 96 | Map promise results to an array **in order resolved**. 97 | 98 | #### map(promises: Array) 99 | 100 | Map promise results to an array **in given order**. 101 | 102 | #### skip(steps: Number) 103 | 104 | Skip `n` steps after this point. 105 | 106 | #### get(index: Number) 107 | 108 | Obtain entry in array at given index in next step. 109 | 110 | ```js 111 | flow 112 | .then(() => [1,2,3]) 113 | .get(0) 114 | .then(item => console.log(item)) // Outputs: 1 115 | ``` 116 | 117 | #### when(conditional: Function|Truthy, method: Function) 118 | 119 | Invokes method when the `conditional` argument is `truthy`, otherwise skips to the next step. 120 | 121 | ```js 122 | flow 123 | .then(() => [1, 2, 3]) 124 | .when(result => result[0] === 1, result => console.log(result[0], '=', 1)) 125 | ``` 126 | 127 | This is a basic example to illustrate the small power of how you can make `if` statements 128 | asynchronous. 129 | 130 | #### spread(method: Function) 131 | 132 | Spreads each `argument` from a successful step as individual arguments on the passed `method` 133 | 134 | ```js 135 | flow 136 | .then(() => ['username', 'Steven Seagal']) 137 | .spread((field, value) => console.log(field, '=', value)) // username = Steven Seagal 138 | ``` 139 | 140 | #### tap(method: Function) 141 | 142 | Invoke `method` without modifying the return result of the step, useful for inspection. 143 | 144 | ```js 145 | flow 146 | .then(() => [1, 2, 3]) 147 | .tap(result => console.log(result)) 148 | .then(result => console.log(result)) // [1,2,3] 149 | ``` 150 | 151 | #### return(value: any) 152 | 153 | Convenience method for: 154 | 155 | ```js 156 | .then(() => value) 157 | ``` 158 | 159 | #### throw(reason: any) 160 | 161 | Convenience method for: 162 | 163 | ```js 164 | .then(() => throw error) 165 | ``` 166 | 167 | 168 | ## License 169 | 170 | Licensed under [The MIT License](LICENSE). 171 | 172 | [license-url]: https://github.com/Nijikokun/breeze/blob/master/LICENSE 173 | 174 | [travis-url]: https://travis-ci.org/Nijikokun/breeze 175 | [travis-image]: https://img.shields.io/travis/Nijikokun/breeze.svg?style=flat 176 | 177 | [npm-url]: https://www.npmjs.com/package/breeze 178 | [npm-license]: https://img.shields.io/npm/l/breeze.svg?style=flat 179 | [npm-version]: https://img.shields.io/npm/v/breeze.svg?style=flat 180 | [npm-downloads]: https://img.shields.io/npm/dm/breeze.svg?style=flat 181 | 182 | [codeclimate-url]: https://codeclimate.com/github/Nijikokun/breeze 183 | [codeclimate-quality]: https://img.shields.io/codeclimate/github/Nijikokun/breeze.svg?style=flat 184 | [codeclimate-coverage]: https://img.shields.io/codeclimate/coverage/github/Nijikokun/breeze.svg?style=flat 185 | 186 | [david-url]: https://david-dm.org/Nijikokun/breeze 187 | [david-image]: https://img.shields.io/david/Nijikokun/breeze.svg?style=flat 188 | 189 | [download]: https://github.com/Nijikokun/breeze/archive/v1.2.2.zip 190 | --------------------------------------------------------------------------------