├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.swp 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | - 5 6 | - 4 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 'Adrian C Shum' 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raco 2 | 3 | Generator based flow-control that supports both callback and promise. 4 | 5 | [![Build Status](https://travis-ci.org/cshum/raco.svg?branch=master)](https://travis-ci.org/cshum/raco) 6 | 7 | ```bash 8 | npm install raco 9 | ``` 10 | 11 | Many existing flow-control libraries such as [co](https://github.com/tj/co), assume promises to be the lowest denominator of async handling. 12 | Callback function requires promisify patch to be compatible, 13 | which creates unnecessary complication. 14 | 15 | In raco, both callbacks and promises are yieldable. 16 | Resulting function can be called by both callbacks and promises. 17 | This enables a powerful control flow while maintaining simplicity. 18 | 19 | ### `raco(fn*, [opts])` 20 | 21 | Resolves a generator function. 22 | This does not return a Promise; uncaught error will be thrown. 23 | 24 | ```js 25 | // import raco 26 | var raco = require('raco') 27 | ... 28 | raco(function * (next) { 29 | // yield promise 30 | console.log(yield Promise.resolve('foo')) // 'foo' 31 | try { 32 | yield Promise.reject(new Error('boom')) 33 | } catch (err) { 34 | console.log(err.message) // 'boom' 35 | } 36 | 37 | // yield callback 38 | yield setTimeout(next, 1000) // delay 1 second 39 | var data = yield fs.readFile('./data', next) 40 | yield mkdirp('/tmp/foo/bar', next) 41 | yield pump( 42 | fs.createReadStream('./foo'), 43 | fs.createWriteStream('./bar'), 44 | next 45 | ) 46 | }) 47 | ``` 48 | 49 | Yieldable callback works by supplying an additional `next` argument. 50 | Yielding non-yieldable value pauses the current generator, 51 | until `next(err, val)` being invoked by callback. 52 | `val` passes back to yielded value, or `throw` if `err` exists. 53 | 54 | ```js 55 | raco(function * (next) { 56 | var res = yield setTimeout(() => { 57 | next(null, 'foo') 58 | }, 100) 59 | console.log(res) // 'foo' 60 | 61 | try { 62 | yield setTimeout(() => { 63 | next(new Error('boom')) 64 | }, 100) 65 | } catch (err) { 66 | console.log(err.message) // 'boom' 67 | } 68 | }) 69 | ``` 70 | 71 | ### `fn = raco.wrap(fn*, [opts])` 72 | 73 | Wraps a generator function into regular function that optionally accepts callback or returns a promise. 74 | 75 | ```js 76 | var fn = raco.wrap(function * (arg1, arg2, next) { 77 | // pass arguments followed by `next` 78 | ... 79 | return arg1 + arg2 80 | }) 81 | 82 | fn(167, 199, (err, val) => { ... }) // Use with callback 83 | 84 | fn(167, 689) // use with promise 85 | .then((val) => { ... }) 86 | .catch((err) => { ... }) 87 | ``` 88 | 89 | ### `raco.wrapAll(obj, [opts])` 90 | 91 | Wraps generator methods of class or object. 92 | 93 | ```js 94 | class App { 95 | * fn (next) { ... } 96 | * fn2 (next) { ... } 97 | } 98 | 99 | // wrap prototype object 100 | raco.wrapAll(App.prototype) 101 | 102 | var app = new App() 103 | 104 | app.fn((err, val) => {...}) 105 | app.fn2().then(...).catch(...) 106 | ``` 107 | 108 | ### Options 109 | 110 | Calling raco with options object makes a factory function with a set of available options: 111 | 112 | ```js 113 | var raco = require('raco')({ 114 | Promise: null, // disable Promise 115 | yieldable: function (val, cb) { 116 | // custom yieldable 117 | }, 118 | prepend: true // prepend or append `next` argument 119 | }) 120 | 121 | ``` 122 | 123 | #### `opts.Promise` 124 | 125 | Raco uses native promise by default. This can be overridden by setting `raco.Promise`. 126 | 127 | ```js 128 | var raco = require('raco')({ Promise: require('bluebird') }) 129 | ``` 130 | 131 | #### `opts.prepend` 132 | 133 | By default, `next(err, val)` function appends to arguments `fn* (args..., next)`. 134 | If `opts.prepend` set to `true`, generator function is called with `fn* (next, args...)`. 135 | This can be useful for functions that accept varying numbers of arguments. 136 | 137 | ```js 138 | var raco = require('raco') 139 | 140 | var fn = raco.wrap(function * (next, a, b) { 141 | return a + b 142 | }, { prpend: true }) 143 | 144 | fn(1, 6, (err, val) => { 145 | console.log(val) // 7 146 | }) 147 | fn(1, 6).then((val) => { 148 | console.log(val) // 7 149 | }) 150 | 151 | ``` 152 | 153 | #### `opts.yieldable` 154 | 155 | By default, the following objects are considered yieldable: 156 | 157 | * Promise 158 | * Generator 159 | * Generator Function 160 | * Thunk 161 | 162 | It is also possible to override the default yieldable mapper. Use with caution: 163 | 164 | * Takes the yielded value, returns `true` to acknowledge yieldable. 165 | * Callback`cb(err, val)` to resolve the yieldable. 166 | 167 | ```js 168 | var raco = require('raco')({ 169 | yieldable: (val, cb) => { 170 | // map array to Promise.all 171 | if (Array.isArray(val)) { 172 | Promise.all(val).then((res) => { 173 | cb(null, res) 174 | }, cb) 175 | return true // acknowledge yieldable 176 | } 177 | // Anything can be mapped! 178 | if (val === 689) { 179 | cb(new Error('DLLM')) 180 | return true // acknowledge yieldable 181 | } 182 | return false // acknowledge non-yieldable 183 | } 184 | }) 185 | 186 | raco(function * () { 187 | console.log(yield [ 188 | Promise.resolve(1), 189 | Promise.resolve(2), 190 | 3 191 | ]) // [1, 2, 3] 192 | 193 | // yield 689 throws error 194 | try { 195 | yield 689 196 | } catch (err) { 197 | console.log(err.message) // 'DLLM' 198 | } 199 | }) 200 | 201 | ``` 202 | 203 | ## License 204 | 205 | MIT 206 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var slice = Array.prototype.slice 4 | 5 | function isFunction (val) { 6 | return val && typeof val === 'function' 7 | } 8 | 9 | function isGenerator (val) { 10 | return val && isFunction(val.next) && isFunction(val.throw) 11 | } 12 | 13 | function isGeneratorFunction (val) { 14 | if (!val || !val.constructor) return false 15 | if (val.constructor.name === 'GeneratorFunction' || val.constructor.displayName === 'GeneratorFunction') return true 16 | return isGenerator(val.constructor.prototype) 17 | } 18 | 19 | function isPromise (val) { 20 | return val && isFunction(val.then) 21 | } 22 | 23 | function noop () {} 24 | 25 | /** 26 | * yieldable resolver 27 | * 28 | * @param {*} val - value to resolve 29 | * @param {function} cb - callback function 30 | * @returns {boolean} denote yieldable 31 | */ 32 | function yieldable (val, cb, opts) { 33 | if (isPromise(val)) { 34 | // Promise 35 | val.then(function (value) { 36 | cb(null, value) 37 | }, function (err) { 38 | cb(err || new Error()) 39 | }) 40 | return true 41 | } else if (isGeneratorFunction(val) || isGenerator(val)) { 42 | // Generator 43 | _raco.call(this, val, null, cb, opts) 44 | return true 45 | } else if (isFunction(val)) { 46 | // Thunk 47 | val(cb) 48 | return true 49 | } else { 50 | // Not yieldable 51 | return false 52 | } 53 | } 54 | 55 | /** 56 | * internal raco resolver 57 | * 58 | * @param {function} genFn - generator function 59 | * @param {array} args - arguments in real array form 60 | * @returns {promise} if no callback provided 61 | */ 62 | function _raco (iter, args, callback, opts) { 63 | var self = this 64 | var trycatch = true 65 | var isYieldable = true 66 | var yielded = false 67 | var nothrow = !!opts.nothrow 68 | /** 69 | * internal callback stepper 70 | * 71 | * @param {object} err - callback error object 72 | * @param {...*} val - callback value(s) 73 | */ 74 | function step (err, val) { 75 | if (iter) { 76 | // generator step 77 | yielded = false 78 | isYieldable = false 79 | var state 80 | if (trycatch) { 81 | try { 82 | if (nothrow) state = iter.next(slice.call(arguments)) 83 | else state = err ? iter.throw(err) : iter.next(val) 84 | } catch (err) { 85 | iter = null // catch err, break iteration 86 | return step(err) 87 | } 88 | } else { 89 | if (nothrow) state = iter.next(slice.call(arguments)) 90 | else state = err ? iter.throw(err) : iter.next(val) 91 | } 92 | if (state && state.done) iter = null 93 | yielded = true 94 | isYieldable = yieldable.call(self, state.value, step, opts) 95 | if (!isYieldable && opts.yieldable) { 96 | isYieldable = opts.yieldable.call(self, state.value, step) 97 | } 98 | // next if generator returned non-yieldable 99 | if (!isYieldable && !iter) next(null, state.value) 100 | } else if (callback) { 101 | callback.apply(self, arguments) 102 | callback = null 103 | } 104 | } 105 | /** 106 | * next, callback stepper with nextTick 107 | * 108 | * @param {object} err - callback error object 109 | * @param {...*} val - callback value(s) 110 | */ 111 | function next () { 112 | var args = slice.call(arguments) 113 | if (!isYieldable) { 114 | // only handle callback if not yieldable 115 | if (iter && yielded) { 116 | // no need defer when yielded 117 | step.apply(self, args) 118 | } else { 119 | // need next tick if not defered 120 | process.nextTick(function () { 121 | step.apply(self, args) 122 | }) 123 | } 124 | } else { 125 | step(new Error('Callback on yieldable is prohibited')) 126 | } 127 | } 128 | 129 | // prepend or append next arg 130 | if (args) opts.prepend ? args.unshift(next) : args.push(next) 131 | else args = [next] 132 | 133 | if (!isGenerator(iter)) iter = iter.apply(self, args) 134 | 135 | if (callback) { 136 | // callback mode 137 | step() 138 | } else if (opts.Promise) { 139 | // return promise if callback not exists 140 | return new opts.Promise(function (resolve, reject) { 141 | callback = function (err, val) { 142 | if (err) reject(err) 143 | else resolve(val) 144 | } 145 | step() 146 | }) 147 | } else { 148 | // callback and promise not exists, 149 | // no try catch wrap 150 | trycatch = false 151 | callback = noop 152 | step() 153 | } 154 | } 155 | 156 | module.exports = (function factory (_opts) { 157 | _opts = Object.assign({ 158 | Promise: Promise, 159 | prepend: false, 160 | nothrow: false, 161 | yieldable: null 162 | }, _opts) 163 | /** 164 | * raco resolver 165 | * returns factory if no arguments 166 | * 167 | * @param {function} genFn - generator function or factory options 168 | * @param {object} [opts] - options object 169 | * @returns {promise} if no callback provided 170 | */ 171 | function raco (genFn, opts) { 172 | if (!isGeneratorFunction(genFn)) { 173 | if (isFunction(genFn)) throw new Error('Generator function required') 174 | else if (!isGenerator(genFn)) return factory(genFn) 175 | } 176 | opts = Object.assign({}, _opts, opts) 177 | opts.Promise = null 178 | return _raco.call(this, genFn, null, null, opts) 179 | } 180 | 181 | /** 182 | * wraps a generator function into regular function that 183 | * optionally accepts callback or returns a promise. 184 | * 185 | * @param {function} genFn - generator function 186 | * @param {object} [opts] - options object 187 | * @returns {function} regular function 188 | */ 189 | raco.wrap = function (genFn, opts) { 190 | if (!isGeneratorFunction(genFn)) throw new Error('Generator function required') 191 | opts = Object.assign({}, _opts, opts) 192 | function fn () { 193 | var args = slice.call(arguments) 194 | var cb = args.length && isFunction(args[args.length - 1]) ? args.pop() : null 195 | return _raco.call(this, genFn, args, cb, opts) 196 | } 197 | switch (genFn.length) { 198 | case 1: return function (a) { return fn.apply(this, arguments) } 199 | case 2: return function (a, b) { return fn.apply(this, arguments) } 200 | case 3: return function (a, b, c) { return fn.apply(this, arguments) } 201 | case 4: return function (a, b, c, d) { return fn.apply(this, arguments) } 202 | case 5: return function (a, b, c, d, e) { return fn.apply(this, arguments) } 203 | default: return fn 204 | } 205 | } 206 | 207 | /** 208 | * wraps generator function properties of object 209 | * 210 | * @param {object} obj - object to raco.wrap 211 | * @param {object} [opts] - options object 212 | * @returns {object} original object 213 | */ 214 | raco.wrapAll = function (obj, opts) { 215 | var props = Object.getOwnPropertyNames(obj) 216 | for (var key of props) { 217 | if (isGeneratorFunction(obj[key])) { 218 | obj[key] = raco.wrap(obj[key], opts) 219 | } 220 | } 221 | return obj 222 | } 223 | 224 | return raco 225 | })() 226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raco", 3 | "version": "3.2.7", 4 | "description": "Generator based flow-control that supports both callback and promise", 5 | "scripts": { 6 | "test": "standard && tape test.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:cshum/raco.git" 11 | }, 12 | "author": "Adrian C Shum ", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "standard": "^8.5.0", 16 | "tape": "^4.6.2" 17 | }, 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var test = require('tape') 3 | var raco = require('./') 4 | 5 | test('arguments and callback return', function (t) { 6 | t.plan(11) 7 | 8 | var fn = raco.wrap(function * (num, str, next) { 9 | t.equal(num, 167, 'arguemnt') 10 | t.equal(str, '167', 'arguemnt') 11 | t.equal(yield next(null, 'foo'), 'foo', 'stepping function') 12 | next(null, 'foo', 'bar') // should return 13 | return 'boom' // should not return 14 | }) 15 | t.equal(fn.length, 3, 'fn arg length') 16 | 17 | // callback 18 | t.notOk(fn(167, '167', function (err, res) { 19 | t.notOk(err, 'no callback error') 20 | t.deepEqual( 21 | Array.prototype.slice.call(arguments), 22 | [null, 'foo', 'bar'], 23 | 'return callback arguments' 24 | ) 25 | }), 'passing callback returns undefined') 26 | 27 | // promise 28 | fn(167, '167').then(function () { 29 | t.deepEqual( 30 | Array.prototype.slice.call(arguments), 31 | ['foo'], 32 | 'return callback value for promise' 33 | ) 34 | }, t.error) 35 | }) 36 | 37 | test('prepend next arg', function (t) { 38 | t.plan(10) 39 | var fn = raco.wrap(function * (next, num, str) { 40 | t.equal(num, 167, 'arguemnt') 41 | t.equal(str, '167', 'arguemnt') 42 | t.equal(yield next(null, 'foo'), 'foo', 'stepping function') 43 | next(null, 'foo', 'bar') // should return 44 | return 'boom' // should not return 45 | }, { prepend: true }) 46 | 47 | // callback 48 | t.notOk(fn(167, '167', function (err, res) { 49 | t.notOk(err, 'no callback error') 50 | t.deepEqual( 51 | Array.prototype.slice.call(arguments), 52 | [null, 'foo', 'bar'], 53 | 'return callback arguments' 54 | ) 55 | }), 'passing callback returns undefined') 56 | 57 | // promise 58 | fn(167, '167').then(function () { 59 | t.deepEqual( 60 | Array.prototype.slice.call(arguments), 61 | ['foo'], 62 | 'return callback value for promise' 63 | ) 64 | }, t.error) 65 | }) 66 | 67 | test('multiple callbacks handling', function (t) { 68 | t.plan(2) 69 | raco.wrap(function * (next) { 70 | next(null, 'foo') 71 | next(null, 'bar') 72 | return 'boom' 73 | })(function (err, val) { 74 | t.error(err) 75 | t.equal(val, 'foo', 'return first callback on return') 76 | }) 77 | }) 78 | 79 | test('scope', function (t) { 80 | t.plan(1) 81 | 82 | var obj = {} 83 | 84 | raco.wrap(function * () { 85 | t.equal(this, obj, 'correct scope') 86 | t.end() 87 | }).call(obj) 88 | }) 89 | 90 | test('explicit throws', function (t) { 91 | t.plan(2) 92 | var r = raco({ Promise: null }) 93 | 94 | t.throws(r.wrap(function * () { 95 | throw new Error('boom') 96 | }), 'boom', 'no callback & promise throws') 97 | 98 | t.throws(function () { 99 | r(function * () { 100 | throw new Error('boom') 101 | }) 102 | }, 'boom', 'no callback & promise throws') 103 | }) 104 | 105 | test('resolve and reject', function (t) { 106 | t.plan(6) 107 | var fn = raco.wrap(function * () { 108 | return yield Promise.resolve(167) 109 | }) 110 | 111 | // callback 112 | fn(function (err, val) { 113 | t.error(err) 114 | t.equal(val, 167, 'callback value') 115 | }) 116 | 117 | // promise 118 | fn().then(function (val) { 119 | t.equal(val, 167, 'promise resolve') 120 | }, t.error) 121 | 122 | raco.wrap(function * () { 123 | throw new Error('167') 124 | })().then(t.error, function (err) { 125 | t.equal(err.message, '167', 'promise reject') 126 | }) 127 | 128 | raco.wrap(function * () { 129 | return Promise.reject() // falsy reject 130 | })(function (err, val) { 131 | t.ok(err instanceof Error, 167, 'promise falsy reject') 132 | t.error(val) 133 | }) 134 | }) 135 | 136 | test('yieldable', function (t) { 137 | t.plan(13) 138 | 139 | function * resolveGen (n) { 140 | return yield Promise.resolve(n) 141 | } 142 | var rejectFn = raco.wrap(function * (n) { 143 | return Promise.reject(n) 144 | }) 145 | var instantCb = function (cb) { 146 | cb(null, 1044) 147 | } 148 | var tryCatch = raco.wrap(function * () { 149 | try { 150 | return yield rejectFn(689) 151 | } catch (err) { 152 | t.equal(err, 689, 'try/catch promise reject') 153 | return yield resolveGen(167) 154 | } 155 | }) 156 | 157 | var tryCatchNext = raco.wrap(function * (next) { 158 | try { 159 | return yield next(689) 160 | } catch (err) { 161 | t.equal(err, 689, 'try/catch next err') 162 | return yield next(null, 167) 163 | } 164 | }) 165 | 166 | raco(function * (next) { 167 | yield setTimeout(next, 0) 168 | t.equal(yield function * (next) { 169 | return yield next(null, 'foo') 170 | }, 'foo', 'yield generator function') 171 | t.equal(yield instantCb(next), 1044, 'yield callback') 172 | t.equal(yield tryCatch(), 167, 'yield gnerator-promise') 173 | t.equal(yield tryCatchNext(), 167, 'yield next val') 174 | }) 175 | raco(function * (next) { 176 | t.deepEqual(yield instantCb(next), [null, 1044], 'yield callback nothrow') 177 | t.deepEqual(yield tryCatch(), [null, 167], 'yield gnerator-promise') 178 | t.deepEqual(yield tryCatchNext(), [null, 167], 'yield next val') 179 | t.deepEqual(yield rejectFn(689), [689], 'nothrow reject args') 180 | t.deepEqual(yield setTimeout(() => next([1], 2, 'c')), [[1], 2, 'c'], 'yield next args') 181 | }, { nothrow: true }) 182 | }) 183 | 184 | test('override yieldable', function (t) { 185 | t.plan(2) 186 | raco(function * () { 187 | t.deepEqual(yield [ 188 | Promise.resolve(1), 189 | Promise.resolve(2), 190 | 3 191 | ], [1, 2, 3], 'yield map array to Promise.all') 192 | 193 | try { 194 | yield 689 195 | } catch (err) { 196 | t.equal(err.message, 'DLLM', 'yield 689 throws error') 197 | } 198 | }, { 199 | yieldable: function (val, cb) { 200 | // yield array 201 | if (Array.isArray(val)) { 202 | Promise.all(val).then(function (res) { 203 | cb(null, res) 204 | }, function (err) { 205 | cb(err || new Error()) 206 | }) 207 | return true 208 | } 209 | // yield 689 throws error 210 | if (val === 689) { 211 | cb(new Error('DLLM')) 212 | return true 213 | } 214 | } 215 | }) 216 | }) 217 | 218 | test('wrapAll', function (t) { 219 | t.plan(7) 220 | 221 | var fn = function () {} 222 | var gen = (function * () {})() 223 | var obj = { 224 | test: 'foo', 225 | fn: fn, 226 | gen: gen, 227 | genFn: function * (next) { 228 | t.equal(typeof next, 'function', 'raco next param') 229 | } 230 | } 231 | class C { 232 | fn () {} 233 | * genFn (next) { 234 | t.equal(typeof next, 'function', 'raco next param') 235 | } 236 | } 237 | raco.wrapAll(C.prototype) 238 | var c = new C() 239 | t.equal(raco.wrapAll(obj), obj, 'mutuable') 240 | t.equal(obj.test, 'foo', 'ignore non raco') 241 | t.equal(obj.fn, fn, 'ignore non raco') 242 | t.notOk(obj.gen === gen, 'wrap generator') 243 | t.notOk(obj.genFn === fn, 'wrap generator function') 244 | c.genFn().catch(t.error) 245 | obj.genFn().catch(t.error) 246 | }) 247 | 248 | test('ignore callback for yieldable', function (t) { 249 | t.plan(1) 250 | function fn (val, cb) { 251 | return new Promise((resolve, reject) => { 252 | process.nextTick(() => { 253 | cb(null, 167) 254 | resolve(val) 255 | }) 256 | }) 257 | } 258 | var n = 5 259 | raco(function * (next) { 260 | try { 261 | for (var i = 0; i < n; i++) yield fn(i, next) 262 | } catch (err) { 263 | t.equal(err.message, 'Callback on yieldable is prohibited') 264 | } 265 | }) 266 | }) 267 | --------------------------------------------------------------------------------