├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── adapters ├── adapters.js ├── express-4.js ├── index.js └── mongoose.js ├── fig.yml ├── index.js ├── lib ├── async.js ├── collection.js ├── context.js ├── require.js └── util.js ├── package.json └── test ├── acceptance ├── file.js ├── glob.js ├── jsdom.js ├── mkdirp.js ├── mongodb.js ├── mongoose.js ├── redis.js ├── request.js └── superagent.js └── unit ├── collection.js └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | TODO 17 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowSpacesInNamedFunctionExpression": { 3 | "beforeOpeningRoundBrace": true 4 | }, 5 | "disallowSpacesInFunctionExpression": { 6 | "beforeOpeningRoundBrace": true 7 | }, 8 | "disallowSpacesInAnonymousFunctionExpression": { 9 | "beforeOpeningRoundBrace": true 10 | }, 11 | "disallowSpacesInFunctionDeclaration": { 12 | "beforeOpeningRoundBrace": true 13 | }, 14 | "disallowSpacesInsideParentheses": true, 15 | "disallowQuotedKeysInObjects": true, 16 | "disallowSpaceAfterObjectKeys": true, 17 | "disallowSpaceAfterPrefixUnaryOperators": true, 18 | "disallowSpaceBeforePostfixUnaryOperators": true, 19 | "disallowSpaceBeforeBinaryOperators": [ 20 | "," 21 | ], 22 | "disallowMixedSpacesAndTabs": true, 23 | "disallowTrailingWhitespace": true, 24 | "disallowTrailingComma": true, 25 | "disallowYodaConditions": true, 26 | "disallowKeywords": [ "with" ], 27 | "disallowMultipleLineBreaks": true, 28 | "requireSpaceBeforeBlockStatements": true, 29 | "requireParenthesesAroundIIFE": true, 30 | "requireSpacesInConditionalExpression": true, 31 | "disallowMultipleVarDecl": true, 32 | "requireBlocksOnNewline": 1, 33 | "requireCommaBeforeLineBreak": true, 34 | "requireSpaceBeforeBinaryOperators": true, 35 | "requireSpaceAfterBinaryOperators": true, 36 | "requireLineFeedAtFileEnd": true, 37 | "requireCapitalizedConstructors": true, 38 | "requireDotNotation": true, 39 | "requireSpacesInForStatement": true, 40 | "requireCurlyBraces": [ 41 | "do" 42 | ], 43 | "requireSpaceAfterKeywords": [ 44 | "if", 45 | "else", 46 | "for", 47 | "while", 48 | "do", 49 | "switch", 50 | "case", 51 | "return", 52 | "try", 53 | "catch", 54 | "typeof" 55 | ], 56 | "safeContextKeyword": "self", 57 | "validateLineBreaks": "LF", 58 | "validateIndentation": 2 59 | } 60 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "undef": true, 4 | "unused": true, 5 | "lastsemic": true, 6 | "-W058": false, /* don't require parentheses for no-arg constructors */ 7 | "-W054": false, /* use Function constructor responsibly */ 8 | "-W033": false, /* let jscs deal with semicolons */ 9 | "-W038": false /* don't try and fool ourself about block vs function scope */ 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4" 5 | env: 6 | - CXX=g++-4.8 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4-onbuild 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Chester 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dchester/generator-async.png?branch=master)](https://travis-ci.org/dchester/generator-async) 2 | 3 | # generator-async 4 | 5 | Flexible generator-based asynchronous control flow for Node.js. 6 | 7 | ## Introduction 8 | 9 | With `generator-async` use both callback-based and promise-based libraries all together with a clean, consistent interface. Use the `yield` keyword before an asynchronous function call that would normally take a callback or return a promise, and skip the callback or call to `then()`. 10 | 11 | ```js 12 | var async = require('generator-async'); 13 | 14 | // load `fs` with async's `require` 15 | var fs = async.require('fs'); 16 | 17 | async.run(function*() { 18 | 19 | // read a file 20 | var passwd = yield fs.readFile('/etc/passwd'); 21 | 22 | // then read another file 23 | var group = yield fs.readFile('/etc/group'); 24 | 25 | // then spit out the contents of both files 26 | console.log(passwd, group); 27 | }); 28 | ``` 29 | 30 | In this example, the contents of `/etc/passwd` are read from disk and assigned to `passwd`. Program execution waits on the I/O and assignment, while the process event loop keeps on moving. That's in contrast to `fs.readFileSync` for example, where the entire process waits. 31 | 32 | Notice `fs` was loaded through `async.require`, which wraps async methods to be compatible. If you'd rather not wrap, you can load modules directly like normal, and just refer to `async.cb` wherever a standard callback would be expected. See the [Alternative Explicit Callback Interface](#alternative-explicit-callback-interface) section below for more. 33 | 34 | 35 | ## Running in Parallel 36 | 37 | Run functions concurrently with `async.parallel` and then yield in the same order. 38 | 39 | ```js 40 | async.run(function*() { 41 | 42 | // kick off read operations with async.parallel 43 | async.parallel( fs.readFile('/etc/passwd') ); 44 | async.parallel( fs.readFile('/etc/group') ); 45 | 46 | // yield in order 47 | var passwd = yield async.yield; 48 | var group = yield async.yield; 49 | }); 50 | ``` 51 | 52 | ## Examples 53 | 54 | ### Working with Request 55 | 56 | ```js 57 | var request = async.require('request'); 58 | 59 | async.run(function*() { 60 | 61 | var data = yield request('http://www.google.com'); 62 | console.log(data); 63 | }); 64 | ``` 65 | 66 | ### Working with Redis 67 | 68 | ```js 69 | var redis = async.require('redis'); 70 | 71 | async.run(function*() { 72 | 73 | var client = redis.createClient(6379, '172.17.42.1', {}); 74 | 75 | yield client.hset("hash key", "hashtest 1", "some value"); 76 | yield client.hset("hash key", "hashtest 2", "some other value"); 77 | 78 | var data = yield client.hkeys("hash key"); 79 | console.log(data); 80 | }); 81 | ``` 82 | 83 | ### Working with Express 84 | 85 | Use generator functions directly as route handlers. 86 | 87 | ```js 88 | var express = async.require('express'); 89 | var fs = async.require('fs'); 90 | 91 | var app = express(); 92 | 93 | app.get('/', function*(req, res) { 94 | 95 | var data = yield fs.readFile('/etc/passwd'); 96 | res.end(data); 97 | }); 98 | ``` 99 | 100 | ### Working with the file system 101 | 102 | ```js 103 | var fs = async.require('fs'); 104 | var mkdirp = async.require('mkdirp'); 105 | var rimraf = async.require('rimraf'); 106 | var path = require('path'); 107 | var assert = require('assert'); 108 | 109 | async.run(function*() { 110 | 111 | // come up with a random-ish nested dir name and see that it doesn't exist yet 112 | var name = path.join('mkdirp-' + parseInt(Math.random() * 10000), 'nested', 'dir'); 113 | assert.equal(yield fs.exists(name), false); 114 | 115 | // create the new directory and see that it exists 116 | var err = yield mkdirp(name); 117 | assert.equal(yield fs.exists(name), true); 118 | 119 | // remove our nascent dir and see that it no longer exists 120 | yield rimraf(name); 121 | assert.equal(yield fs.exists(name), false); 122 | }); 123 | ``` 124 | 125 | ## Wrapping Classes 126 | 127 | Wrapped classes that define methods as generator functions will automatically have those generator functions wrapped in a generator-async context, meaning they'll be invoked and executed when called by consumers. 128 | 129 | Consider a Class representing a file: 130 | 131 | ```js 132 | var File = function() { 133 | this.initialize.apply(this, arguments); 134 | }; 135 | 136 | File.prototype = { 137 | 138 | initialize: function(filename) { 139 | this.filename = filename; 140 | }, 141 | read: function*() { 142 | var contents = yield fs.readFile(this.filename); 143 | return contents; 144 | }, 145 | size: function*() { 146 | var stat = yield fs.stat(this.filename); 147 | return stat.size; 148 | } 149 | }; 150 | 151 | File = async(File); 152 | ``` 153 | 154 | Consume this functionality in an `async.run` context: 155 | 156 | ```js 157 | async.run(function*() { 158 | var file = new File('/etc/passwd'); 159 | var size = yield file.size(); 160 | console.log(size); 161 | }); 162 | ``` 163 | 164 | Or, consume this functionality as-is just like normal with conventional callbacks. 165 | 166 | ```js 167 | var file = new File('/etc/passwd'); 168 | file.size(function(err, size) { 169 | console.log(size); 170 | }; 171 | ``` 172 | 173 | ## Alternative Explicit Callback Interface 174 | 175 | Sometimes it's not feasible to wrap a module or method to be yieldable since it may have a non-standard callback scheme. Or you may prefer the more verbose interface in order to use the module directly and shed some of the intermediary magic. In that case, node's `require` like normal, and refer to `async.cb` where a callback is expected: 176 | 177 | ```js 178 | // require `fs` directly 179 | var fs = require('fs'); 180 | 181 | async.run(function*() { 182 | // refer to async.cb where the callback would go 183 | var contents = yield fs.readFile('/etc/passwd', async.cb); 184 | console.log(contents); 185 | }); 186 | ``` 187 | 188 | Use `async.cb` where a standard node callback would be expected (a function that accepts `err` and `data` parameters). For non-standard callbacks refer to `async.raw` to get back all the values (and handle errors yourself). 189 | 190 | 191 | ## API 192 | 193 | #### async(input) 194 | 195 | Implementation depends on the type of input. Given a function, or generator, returns a function that is both yieldable in an `async.run` context, and also compatible with a standard callback interface. Given an object or class, returns the input with each of its methods wrapped to be yieldable. Aliased as `async.wrap`. 196 | 197 | #### async.require(module, [hints]) 198 | 199 | Imports a module Like node's native `require`, but also wraps that module so that its methods are yieldable in an `async.run` context with no callbacks necessary. 200 | 201 | That's the goal at least. Wrapping involves making a heuristic best guess about which methods are asynchronous and what is their callback signature etc. So while it often works well, you may sometimes need to give hints about the makeup of the module. See [module-async-map](https://github.com/dchester/module-async-map) for more. 202 | 203 | If you'd rather, feel free to use node's native `require` instead, and refer to `async.cb` where a callback is expected. 204 | 205 | #### async.run(fn\*) 206 | 207 | Invokes and executes the supplied generator function. 208 | 209 | #### async.proxy(fn) 210 | 211 | Returns a wrapped version of the supplied function compatible to be run either in an `async.run` context or standard node callback style. 212 | 213 | #### async.fn(fn\*) 214 | 215 | Returns a function that when called will invoke and execute the supplied generator function. 216 | 217 | 218 | ### Collection Methods 219 | 220 | Since the `yield` keyword is only valid directly inside of generator functions, we can't `yield` inside of stock `Array` methods, which might be exactly what you want to do sometimes. Instead, use these collection methods, which accept generator functions as iterator functions, so you can `yield` from within them. Underlying implementations courtesy of the fantastic [async](https://github.com/caolan/async) library, which has more documentation. 221 | 222 | #### async.forEach(arr, fn\*) 223 | 224 | Applies `fn*` as an iterator to each item in `arr`, in parallel. Aliased as `async.each`. 225 | 226 | ```js 227 | var fs = async.require('fs'); 228 | 229 | async.run(function*() { 230 | 231 | var filenames = [...]; 232 | var totalBytes = 0; 233 | 234 | yield async.forEach(filenames, function*(filename) { 235 | var stat = yield fs.stat(filename); 236 | totalBytes += stat.size; 237 | console.log(filename, stat.size); 238 | }); 239 | 240 | console.log("Total bytes:", totalBytes); 241 | }); 242 | ``` 243 | 244 | #### async.eachLimit(arr, limit, fn\*) 245 | 246 | Same as `async.forEach` except that only `limit` iterators will be simultaneously running at any time. 247 | 248 | #### async.map(arr, fn\*) 249 | 250 | Produces a new array of values by mapping each value in arr through the generator function. 251 | 252 | ```js 253 | async.run(function*() { 254 | 255 | var filenames = [...]; 256 | 257 | var existences = yield async.map(filenames, function*(filename) { 258 | return yield fs.exists(filename); 259 | }); 260 | 261 | console.log(existences); // => [ true, true, false, true, ... ] 262 | }); 263 | 264 | ``` 265 | #### async.mapLimit(arr, limit, fn\*) 266 | 267 | Same as `async.map` except that only limit iterators will be simultaneously running at any time. 268 | 269 | #### async.filter(arr, fn\*) 270 | 271 | Returns a new array of all the values in arr which pass an async truth test. 272 | 273 | ```js 274 | async.run(function*() { 275 | 276 | var filenames = [...]; 277 | 278 | var bigFiles = yield async.filter(filenames, function*(filename) { 279 | var stat = yield fs.stat(filename); 280 | return stat.size > 1024; 281 | }); 282 | 283 | console.log("Big files:", bigFiles); 284 | }); 285 | ``` 286 | 287 | #### async.reject(arr, fn\*) 288 | 289 | The opposite of `async.filter`. Removes values that pass an async truth test. 290 | 291 | #### async.reduce(arr, memo, fn\*) 292 | 293 | Reduces `arr` into a single value using an async iterator to return each successive step. `memo` is the initial state of the reduction. Runs in series. 294 | 295 | #### async.reduceRight(arr, memo, fn\*) 296 | 297 | Same as `async.reduce`, only operates on arr in reverse order. 298 | 299 | #### async.detect(arr, fn\*) 300 | 301 | Returns the first value in `arr` that passes an async truth test. The generator function is applied in parallel, meaning the first iterator to return true will itself be returned. 302 | 303 | #### async.sortBy(arr, fn\*) 304 | 305 | Sorts a list by the results of running each arr value through an async generator function. 306 | 307 | ```js 308 | async.run(function*() { 309 | 310 | var filenames = [...]; 311 | 312 | var sortedFiles = yield async.filter(filenames, function*(filename) { 313 | var stat = yield fs.stat(filename); 314 | return stat.size; 315 | }); 316 | 317 | console.log("Sorted files:", sorted); 318 | }); 319 | ``` 320 | 321 | #### async.some(arr, fn\*) 322 | 323 | Returns true if at least one element in the arr satisfies an async test. 324 | 325 | #### async.every(arr, fn\*) 326 | 327 | Returns true if every element in arr satisfies an async test. 328 | 329 | #### async.concat(arr, fn\*) 330 | 331 | Applies iterator to each item in arr, concatenating the results. Returns the concatenated list. 332 | 333 | 334 | ## History and Inspiration 335 | 336 | This is an evolution of [gx](https://github.com/dchester/gx), with inspiration from other generator-based control flow libraries such as [co](https://github.com/visionmedia/co), [genny](https://github.com/spion/genny), [galaxy](https://github.com/bjouhier/galaxy), and [suspend](https://github.com/jmar777/suspend). 337 | 338 | 339 | ## License 340 | 341 | Copyright (c) 2015 David Chester 342 | 343 | Permission is hereby granted, free of charge, to any person obtaining a copy of 344 | this software and associated documentation files (the "Software"), to deal in 345 | the Software without restriction, including without limitation the rights to 346 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 347 | the Software, and to permit persons to whom the Software is furnished to do so, 348 | subject to the following conditions: 349 | 350 | The above copyright notice and this permission notice shall be included in all 351 | copies or substantial portions of the Software. 352 | 353 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 354 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 355 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 356 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 357 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 358 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 359 | -------------------------------------------------------------------------------- /adapters/adapters.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'mongoose@3.8.x': require('./mongoose'), 3 | 'express@4': require('./express-4') 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /adapters/express-4.js: -------------------------------------------------------------------------------- 1 | var _methods = require('methods'); 2 | 3 | module.exports = function(express) { 4 | 5 | var async = require('..'); 6 | var methods = [].concat(_methods, 'all'); 7 | 8 | methods.forEach(function(method) { 9 | 10 | var _orig = express.Route.prototype[method]; 11 | 12 | express.Route.prototype[method] = function() { 13 | 14 | var callbacks = flatten([].slice.call(arguments)); 15 | 16 | callbacks.forEach(function(fn, index) { 17 | if (fn.constructor.name == 'GeneratorFunction') { 18 | callbacks[index] = async.fn(fn); 19 | } 20 | }); 21 | 22 | _orig.apply(this, callbacks); 23 | }; 24 | }); 25 | }; 26 | 27 | function flatten(arr, ret) { 28 | ret = ret || []; 29 | var len = arr.length; 30 | for (var i = 0; i < len; ++i) { 31 | if (Array.isArray(arr[i])) { 32 | flatten(arr[i], ret); 33 | } else { 34 | ret.push(arr[i]); 35 | } 36 | } 37 | return ret; 38 | }; 39 | 40 | 41 | -------------------------------------------------------------------------------- /adapters/index.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver'); 2 | 3 | module.exports = { 4 | 5 | resolve: function(name, version) { 6 | 7 | var adapter; 8 | 9 | Object.keys(this.adapters).forEach(function(key) { 10 | var comps = key.split('@'); 11 | var _name = comps[0]; 12 | var _version = comps[1] || '*'; 13 | if (name == _name && (version == '*' || semver.satisfies(version, _version))) { 14 | adapter = this.adapters[key]; 15 | } 16 | }.bind(this)); 17 | 18 | return adapter; 19 | }, 20 | 21 | adapters: require('./adapters') 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /adapters/mongoose.js: -------------------------------------------------------------------------------- 1 | module.exports = function(mongoose) { 2 | var async = require('..'); 3 | var _hook = mongoose.constructor.prototype.Document.prototype.hook; 4 | mongoose.constructor.prototype.Document.prototype.hook = function(name) { 5 | _hook.apply(this, arguments); 6 | this[name] = async.proxy(this[name]); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /fig.yml: -------------------------------------------------------------------------------- 1 | test: 2 | build: . 3 | command: node --harmony /usr/src/app/node_modules/.bin/mocha --harmony -u tdd test/acceptance 4 | volumes: 5 | - .:/usr/src/app 6 | environment: 7 | - REDIS_TEST_HOST 8 | - REDIS_TEST_PORT 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/async'); 2 | -------------------------------------------------------------------------------- /lib/async.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Context = require('./context'); 3 | var util = require('./util'); 4 | var mapper = require('module-async-mapper'); 5 | var jp = require('jsonpath'); 6 | 7 | var async = function(input, args) { 8 | 9 | args = Object(args); 10 | 11 | var method = 12 | util.is_generator(input) ? 'proxy' : 13 | input instanceof Function && Object.keys(input.prototype).length ? 'traverse' : 14 | input instanceof Function && Object.keys(input).length ? 'traverse' : 15 | input instanceof Function ? 'proxy' : 16 | input instanceof Object ? 'traverse' : null; 17 | 18 | if (!method) return input; 19 | 20 | var obj = async[method].apply(this, arguments); 21 | 22 | return obj; 23 | }; 24 | 25 | async.wrap = async; 26 | 27 | async.traverse = function(obj, args) { 28 | 29 | args = Object(args); 30 | 31 | var traverseAll = !args.hints; 32 | var map = mapper.map(obj, args.hints, { traverseAll: traverseAll }); 33 | 34 | var transformKey = map.$; 35 | var transform = async.callbackTransforms[transformKey]; 36 | var root = transform ? async.proxy(obj, transform) : obj; 37 | 38 | Object.keys(map).forEach(function(path) { 39 | if (path == '$') return; 40 | var transformKey = map[path]; 41 | var transform = async.callbackTransforms[transformKey]; 42 | if (transform) { 43 | jp.apply(obj, path, function(fn) { 44 | return async.proxy(fn, transform) 45 | }); 46 | } 47 | }); 48 | 49 | util.extend(root, obj); 50 | return root; 51 | }; 52 | 53 | async.proxy = function(fn, transform) { 54 | 55 | if (fn._async_proxy) return fn; 56 | 57 | if (util.is_generator(fn)) fn = async.fn(fn); 58 | 59 | /* jshint unused:false */ 60 | var mediator = function(done) { 61 | 62 | var args = arguments; 63 | 64 | var generatorCaller = util.is_generator_caller(mediator); 65 | 66 | if (!generatorCaller) { 67 | return fn.apply(this, arguments); 68 | } 69 | 70 | var proxy = function(callback) { 71 | callback._proxy_caller = true; 72 | args[args.length++] = callback; 73 | fn.apply(this, args); 74 | 75 | }.bind(this); 76 | 77 | if (transform) { 78 | proxy.async = { transform: transform }; 79 | } 80 | 81 | return proxy; 82 | }; 83 | 84 | mediator._async_proxy = true; 85 | util.clone_properties(fn, mediator); 86 | 87 | return mediator; 88 | }; 89 | 90 | async.parallel = function(val) { 91 | 92 | if (!val) return; 93 | 94 | if (val.then && util.is_function(val.then)) { 95 | return val.then(async.simple); 96 | } 97 | 98 | if (util.is_function(val)) { 99 | return val(async.cb); 100 | } 101 | }; 102 | 103 | async.fire = function(val) { 104 | if (!val) return; 105 | 106 | if (val.then && util.is_function(val.then)) { 107 | return val.then(function() {}); 108 | } 109 | 110 | if (util.is_function(val)) { 111 | return val(function() {}); 112 | } 113 | }; 114 | 115 | async.yield = '_async_yield'; 116 | 117 | async.fn = function(generator) { 118 | 119 | if (!generator) throw new Error("async.fn needs a generator"); 120 | if (util.is_function(generator)) return generator; 121 | 122 | /* jshint unused:false */ 123 | return function(_arguments) { 124 | 125 | // questionably take _arguments since some libs like mocha inspect 126 | 127 | var args = []; 128 | 129 | for (var i = 0; i < arguments.length; i++) { 130 | args.push(arguments[i]); 131 | } 132 | 133 | var callback = arguments[arguments.length - 1]; 134 | if (!util.is_function(callback)) callback = function() {}; 135 | 136 | var context = new Context(generator, args, this); 137 | context.run(callback); 138 | }; 139 | }; 140 | 141 | async.run = function(generator, callback) { 142 | 143 | callback = callback || function() {}; 144 | 145 | var context = new Context(generator); 146 | context.run(callback); 147 | }; 148 | 149 | async.extend = function(methods) { 150 | var _methods = methods(async); 151 | Object.keys(_methods).forEach(function(method) { 152 | async[method] = _methods[method]; 153 | }); 154 | } 155 | 156 | async._id = '__async'; 157 | 158 | async.extend(require('./collection')); 159 | async.extend(require('./require')); 160 | 161 | Object.defineProperty(async, 'cb', { 162 | get: function() { 163 | var context = Context.stack.active(); 164 | return context.cb(); 165 | } 166 | }); 167 | 168 | Object.defineProperty(async, 'simple', { 169 | get: function() { 170 | var context = Context.stack.active(); 171 | return context.cb(async.callbackTransforms.simple); 172 | } 173 | }); 174 | 175 | Object.defineProperty(async, 'raw', { 176 | get: function() { 177 | var context = Context.stack.active(); 178 | return context.cb(async.callbackTransforms.raw); 179 | } 180 | }); 181 | 182 | module.exports = async; 183 | -------------------------------------------------------------------------------- /lib/collection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var _async = require('async'); 3 | 4 | module.exports = function(async) { 5 | 6 | var source = { 7 | simple: [ 8 | 'filter', 9 | 'filterSeries', 10 | 'reject', 11 | 'rejectSeries', 12 | 'detect', 13 | 'detectSeries', 14 | 'some', 15 | 'every' 16 | ], 17 | standard: [ 18 | 'forEach', 19 | 'forEachSeries', 20 | 'each', 21 | 'eachSeries', 22 | 'eachLimit', 23 | 'map', 24 | 'mapSeries', 25 | 'mapLimit', 26 | 'reduce', 27 | 'reduceRight', 28 | 'sortBy', 29 | 'concat', 30 | 'concatSeries' 31 | ] 32 | } 33 | 34 | var properties = {}; 35 | 36 | source.standard.forEach(function(method) { 37 | properties[method] = async.proxy(function() { 38 | 39 | // poor man's spread to pull out function args, generator, and callback 40 | var args = [].slice.call(arguments, 0, arguments.length - 2); 41 | var generator = arguments[arguments.length - 2]; 42 | var cb = arguments[arguments.length - 1]; 43 | 44 | if (typeof cb != 'function') { 45 | throw new Error('missing callback function'); 46 | } 47 | 48 | if (typeof generator != 'function') { 49 | throw new Error('last argument should be a generator function'); 50 | } 51 | 52 | var _args = [] 53 | .concat(args) 54 | .concat(async.fn(generator)) 55 | .concat(cb); 56 | 57 | _async[method].apply(null, _args); 58 | 59 | }); 60 | }); 61 | 62 | source.simple.forEach(function(method) { 63 | properties[method] = async.proxy(function(arr, generator, cb) { 64 | var _iterator = async.fn(generator); 65 | var iterator = function(item, _cb) { 66 | _iterator(item, function(err, val) { _cb(val) }); 67 | }; 68 | _async[method](arr, iterator, function(val) { 69 | cb(null, val); 70 | }); 71 | }); 72 | }); 73 | 74 | return properties; 75 | }; 76 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | var Context = function(generator, args, _this) { 2 | 3 | if (!generator) throw new Error("Context needs a generator function"); 4 | 5 | this.queue = []; 6 | this.callback = function() {}; 7 | this.pendingCount = 0; 8 | this.iterator = generator.apply(_this, args); 9 | }; 10 | 11 | Context.prototype = { 12 | 13 | run: function(callback) { 14 | this.callback = callback || this.callback; 15 | this.continue(); 16 | }, 17 | 18 | continue: function(payload, err) { 19 | 20 | try { 21 | Context.stack.set(this); 22 | 23 | var ret = err ? 24 | this.iterator.throw(err) : 25 | this.iterator.next(payload); 26 | 27 | var value = ret.value; 28 | 29 | if (ret.done) { 30 | this.callback(null, value); 31 | this._done = true; 32 | } else if (value && value instanceof Object && value._id == '__async') { 33 | // do nothing 34 | } else if (is_promise(value)) { 35 | var callback = this.cb(); 36 | value.then(function(resolution) { 37 | callback(null, resolution); 38 | }).catch(callback); 39 | } else if (value instanceof Function) { 40 | var transform = value.async ? value.async.transform : undefined; 41 | value.call(null, this.cb(transform)); 42 | } else if (this._cbinit) { 43 | // do nothing 44 | } else { 45 | throw new Error("Attempted to yield bad value: " + value); 46 | } 47 | 48 | Context.stack.clear(); 49 | 50 | } catch (e) { 51 | throw e && e.stack ? e.stack : e; 52 | } 53 | }, 54 | 55 | cb: function(transform) { 56 | 57 | this._cbinit = true; 58 | if (this._done) return; 59 | 60 | var placeholder = {}; 61 | 62 | this.pendingCount++; 63 | this.queue.push(placeholder); 64 | 65 | transform = transform || function(args) { return args }; 66 | 67 | return function() { 68 | 69 | var results = transform(arguments); 70 | var err = results[0]; 71 | var data = results[1]; 72 | 73 | if (err) { 74 | console.warn("WARN", err); 75 | err = new Error(err); 76 | } 77 | 78 | placeholder.value = data; 79 | if (--this.pendingCount !== 0) return; 80 | 81 | var len = this.queue.length; 82 | 83 | while (len--) { 84 | // push a dummy operation onto the next frame of the event loop 85 | setImmediate(function() {}); 86 | 87 | var d = this.queue.shift(); 88 | this.continue(d.value, err); 89 | } 90 | 91 | }.bind(this); 92 | } 93 | }; 94 | 95 | Context.stack = { 96 | 97 | stack: [], 98 | 99 | active: function() { 100 | return this.stack[this.stack.length - 1] 101 | }, 102 | 103 | set: function(context) { 104 | this.stack.push(context); 105 | }, 106 | 107 | clear: function() { 108 | this.stack.pop(); 109 | } 110 | }; 111 | 112 | function is_promise(fn) { 113 | return fn && typeof fn == "object" && typeof fn.then == "function"; 114 | } 115 | 116 | module.exports = Context; 117 | -------------------------------------------------------------------------------- /lib/require.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var packagePath = require('package-path'); 3 | var callsite = require('callsite'); 4 | var mapper = require('module-async-mapper'); 5 | 6 | module.exports = function(async) { 7 | 8 | var cache = {}; 9 | var adapters = require('../adapters'); 10 | 11 | return { 12 | 13 | require: function(file, hints) { 14 | 15 | var path = require('path'); 16 | 17 | // handle relative paths 18 | if (file.match(/^(\.\/|\/|\.\.($|\/))/)) { 19 | var caller = callsite()[2].getFilename(); 20 | var base = path.dirname(caller); 21 | file = path.resolve(base, file); 22 | } 23 | 24 | var key = require.resolve(file); 25 | if (cache[key]) return cache[key]; 26 | 27 | var nonLocal = path.dirname(key) == '.'; 28 | 29 | try { 30 | // don't traverse up we only have a filename 31 | if (!nonLocal) { 32 | var _path = packagePath.sync(key); 33 | var pkg = require(path.join(packagePath.sync(key), 'package.json')); 34 | /* jshint unused:false */ 35 | var version = pkg.version; 36 | } 37 | } catch (e) {} 38 | 39 | version = version || '*'; 40 | 41 | // momentarily delete from require cache proper 42 | var _obj = require.cache[key]; 43 | delete require.cache[key]; 44 | 45 | var obj = require(file); 46 | 47 | // restore the cache proper 48 | require.cache[key] = _obj; 49 | 50 | var _hints = nonLocal ? {} : mapper.loadHints(file, version) || {}; 51 | for (var k in hints) _hints[k] = hints[k]; 52 | obj = async.wrap(obj, { hints: _hints }); 53 | cache[key] = obj; 54 | 55 | var adapter = adapters.resolve(file, version); 56 | if (adapter) obj = adapter(obj) || obj; 57 | 58 | return obj; 59 | }, 60 | 61 | cache: cache, 62 | 63 | callbackTransforms: { 64 | simple: function(args) { return [ null, args[0] ] }, 65 | standard: function(args) { return args }, 66 | generator: function(args) { return args }, 67 | raw: function(args) { return [ null, args ] } 68 | } 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var callsite = require('callsite'); 3 | 4 | module.exports = { 5 | 6 | keys: function(obj) { 7 | 8 | var blacklist = ['callee', 'caller', 'arguments']; 9 | 10 | var keys = Object.getOwnPropertyNames(obj); 11 | 12 | for (var k in obj) { 13 | if (keys.indexOf(k) == -1) { 14 | keys.push(k); 15 | } 16 | } 17 | 18 | keys = keys.filter(function(k) { return blacklist.indexOf(k) == -1 }); 19 | 20 | return keys; 21 | }, 22 | 23 | is_function: function(fn) { 24 | 25 | if (typeof fn != "function") return; 26 | if (fn.constructor.name != "Function") return; 27 | 28 | return true; 29 | }, 30 | 31 | is_generator: function(fn) { 32 | 33 | if (typeof fn != "function") return; 34 | if (fn.constructor.name != "GeneratorFunction") return; 35 | 36 | return true; 37 | }, 38 | 39 | clone_properties: function(src, dst) { 40 | 41 | Object.getOwnPropertyNames(src).forEach(function(name) { 42 | var prop = Object.getOwnPropertyDescriptor(src, name); 43 | if (prop.configurable) { 44 | Object.defineProperty(dst, name, prop); 45 | } 46 | }); 47 | }, 48 | 49 | extend: function(obj, source) { 50 | 51 | var props = this.keys(obj); 52 | 53 | props.forEach(function(prop) { 54 | if (obj[prop] != source[prop]) { 55 | try { 56 | obj[prop] = source[prop]; 57 | } catch (e) {} 58 | } 59 | }); 60 | }, 61 | 62 | is_generator_caller: function() { 63 | var typeName = callsite()[3].getTypeName(); 64 | // accommodate nodes 4 and 0.12 65 | return typeName == '[object Generator]' || typeName == 'GeneratorFunctionPrototype'; 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-async", 3 | "description": "Generator-based asynchronous control flow for Node.js", 4 | "version": "0.1.6", 5 | "scripts": { 6 | "test": "mocha --harmony -u tdd test/unit && jscs lib && jshint lib" 7 | }, 8 | "devDependencies": { 9 | "es6-promise": "*", 10 | "glob": "4.3.5", 11 | "jscs": "1.10.0", 12 | "jsdom": "3.1.0", 13 | "jshint": "^2.6.0", 14 | "mkdirp": "0.5.0", 15 | "mocha": "2.1.0", 16 | "mongodb": "1.4.29", 17 | "mongoose": "3.8.22", 18 | "redis": "0.12.1", 19 | "request": "2.51.0", 20 | "rimraf": "2.2.8", 21 | "superagent": "0.21.0", 22 | "mocha": "2.4.5" 23 | }, 24 | "dependencies": { 25 | "esprima": "2.7.2", 26 | "jsonpath": "0.2.2", 27 | "module-async-mapper": "0.1.3", 28 | "methods": "1.1.1", 29 | "async": "0.9.0", 30 | "package-path": "https://github.com/azer/package-path/archive/d1a59dcb5ce33ea9891a4a5dbf6e2397b0c39df7.tar.gz", 31 | "semver": "4.2.0", 32 | "callsite": "1.0.0" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/dchester/generator-async" 37 | }, 38 | "keywords": [ 39 | "async", 40 | "flow-control", 41 | "generator", 42 | "parallel", 43 | "sync", 44 | "asynchronous" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /test/acceptance/file.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var fs = async.require('fs'); 4 | 5 | suite('node fs module', function() { 6 | 7 | test('fs.readFile', async(function*() { 8 | 9 | var passwd = yield fs.readFile('/etc/passwd', 'utf-8'); 10 | assert.ok(passwd.match(/root/)); 11 | 12 | var group = yield fs.readFile('/etc/group', 'utf-8'); 13 | assert.ok(group.match(/root/)); 14 | 15 | })); 16 | 17 | test('fs.exists', async(function*() { 18 | 19 | var exists = yield fs.exists('/etc/passwd'); 20 | assert.equal(exists, true); 21 | 22 | var bogus = yield fs.exists('/nonsense-sf98u23'); 23 | assert.equal(bogus, false); 24 | 25 | })); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/acceptance/glob.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var glob = async.require('glob'); 4 | 5 | suite('glob module', function() { 6 | 7 | test('sync and async glob files match', async(function*() { 8 | var syncFiles = glob.sync("*"); 9 | var asyncFiles = yield glob("*"); 10 | assert.deepEqual(asyncFiles, syncFiles); 11 | })); 12 | 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /test/acceptance/jsdom.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var jsdom = async.require('jsdom'); 4 | 5 | suite('jsdom module', function() { 6 | 7 | test('we get a sane DOM from markup', async(function*() { 8 | 9 | var window = yield jsdom.env('Home'); 10 | var anchors = window.document.getElementsByTagName('a'); 11 | assert.equal(anchors.length, 1); 12 | 13 | })); 14 | }); 15 | 16 | -------------------------------------------------------------------------------- /test/acceptance/mkdirp.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | var async = require('../..'); 4 | var fs = async.require('fs'); 5 | var mkdirp = async.require('mkdirp'); 6 | var rimraf = async.require('rimraf'); 7 | 8 | suite('mkdirp and rimraf modules', function() { 9 | 10 | test('create and remove nested temp dirs', async(function*() { 11 | 12 | var root = 'mkdirp-' + parseInt(Math.random() * 10000); 13 | var nested = path.join(root, 'nested', 'dir'); 14 | 15 | assert.equal(yield fs.exists(root), false); 16 | 17 | yield mkdirp(nested); 18 | assert.equal(yield fs.exists(nested), true); 19 | 20 | yield rimraf(root); 21 | assert.equal(yield fs.exists(root), false); 22 | 23 | })); 24 | }); 25 | -------------------------------------------------------------------------------- /test/acceptance/mongodb.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var mongodb = async.require('mongodb'); 4 | var client = mongodb.MongoClient; 5 | 6 | suite('mongodb module', function() { 7 | 8 | test('insert and read docs from mongo', async(function*() { 9 | 10 | var db = yield client.connect('mongodb://172.17.42.1:27017/test'); 11 | assert.ok(db); 12 | 13 | var collection = db.collection('documents'); 14 | 15 | var result = yield collection.insert([ {a : 1}, {a : 2}, {a : 3} ]); 16 | assert.equal(result.length, 3); 17 | 18 | yield collection.ensureIndex({ a: 1 }); 19 | 20 | var results = yield collection.find().toArray(); 21 | assert.ok(results instanceof Array); 22 | assert.ok(results.length); 23 | 24 | db.close(); 25 | 26 | })); 27 | }); 28 | 29 | process.on('SIGINT', function() { 30 | process.exit(); 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /test/acceptance/mongoose.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var mongoose = async.require('mongoose'); 4 | 5 | mongoose.connect('mongodb://172.17.42.1:27017/mongoose_test'); 6 | 7 | suite('mongoose module', function() { 8 | 9 | test('mongoose can read and write', async(function*() { 10 | 11 | var Post = mongoose.model('Post', { 12 | title: String, 13 | body: String, 14 | date: Date 15 | }); 16 | 17 | var post = new Post({ 18 | title: "jimmerz", 19 | body: "is the best", 20 | date: new Date 21 | }); 22 | 23 | var result = yield post.save(); 24 | assert.ok('_id' in result); 25 | 26 | var posts = yield Post.find(); 27 | 28 | assert.ok(posts instanceof Array); 29 | assert.ok(posts.length); 30 | 31 | mongoose.connection.close() 32 | 33 | })); 34 | }); 35 | 36 | process.on('SIGINT', function() { 37 | process.exit(); 38 | }); 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/acceptance/redis.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var redis = async.require('redis'); 4 | 5 | var host = process.env['REDIS_TEST_HOST'] || '172.17.42.1'; 6 | var port = process.env['REDIS_TEST_PORT'] || 6379; 7 | 8 | suite('redis module', function() { 9 | 10 | test('redis hash keys get set okay', async(function*() { 11 | 12 | client = redis.createClient(port, host, {}); 13 | client.on("error", console.warn); 14 | 15 | var reply = yield client.set("string key", "string val"); 16 | assert.equal(reply, "OK"); 17 | 18 | reply = yield client.hset("hash key", "hashtest 1", "some value"); 19 | assert.equal(reply, 1); 20 | 21 | reply = yield client.hset(["hash key", "hashtest 2", "some other value"]); 22 | assert.equal(reply, 1); 23 | 24 | var keys = yield client.hkeys("hash key"); 25 | assert.deepEqual(keys, ['hashtest 1', 'hashtest 2']) 26 | 27 | yield client.quit(); 28 | 29 | })); 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /test/acceptance/request.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var request = async.require('request'); 4 | 5 | suite('request module', function() { 6 | 7 | test('requests to google have a sane response', async(function*() { 8 | 9 | var response = yield request('http://www.google.com'); 10 | assert.ok(String(response.body).match(/google/i)); 11 | 12 | var response = yield request.get('http://www.google.com'); 13 | assert.ok(String(response.body).match(/google/i)); 14 | 15 | })) 16 | }); 17 | -------------------------------------------------------------------------------- /test/acceptance/superagent.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var async = require('../..'); 3 | var request = async.require('superagent'); 4 | 5 | suite('superagent module', function() { 6 | 7 | test('requests to google have a sane response', async(function*() { 8 | 9 | var response = yield request('http://www.google.com'); 10 | assert.ok(String(response.text).match(/google/i)); 11 | 12 | var response = yield request.get('http://www.google.com').end(); 13 | assert.ok(String(response.text).match(/google/i)); 14 | 15 | })); 16 | }); 17 | -------------------------------------------------------------------------------- /test/unit/collection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var assert = require('assert'); 3 | var async = require('../..'); 4 | 5 | var numbers = [1, 2, 3, 4, 5]; 6 | 7 | suite('collection', function() { 8 | 9 | test('forEach', function(done) { 10 | 11 | async.run(function*() { 12 | var items = []; 13 | yield async.forEach(numbers, function*(x) { items.push(x) }); 14 | assert.deepEqual(items, [1, 2, 3, 4, 5]); 15 | done(); 16 | }); 17 | }); 18 | 19 | test('eachLimit', function(done) { 20 | 21 | async.run(function*() { 22 | var items = []; 23 | yield async.eachLimit(numbers, 2, function*(x) { items.push(x) }); 24 | assert.deepEqual(items, [1, 2, 3, 4, 5]); 25 | done(); 26 | }); 27 | }); 28 | 29 | test('map', function(done) { 30 | 31 | async.run(function*() { 32 | var doubles = yield async.map(numbers, function*(x) { return x * 2 }); 33 | assert.deepEqual(doubles, [2, 4, 6, 8, 10]); 34 | done(); 35 | }); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var assert = require('assert'); 3 | var fs = require('fs'); 4 | var async = require('../..'); 5 | var Promise = require('es6-promise').Promise; 6 | 7 | suite('main', function() { 8 | 9 | test('fn', function(done) { 10 | 11 | var generator = function*(a, b) { 12 | return a + b; 13 | }; 14 | 15 | var add = async.fn(generator); 16 | 17 | add(7, 11, function(err, value) { 18 | assert.equal(value, 18); 19 | done(); 20 | }); 21 | 22 | }); 23 | 24 | test('fnYield', function(done) { 25 | 26 | var callback = new NodeCallback; 27 | 28 | var generator = function*(a, b) { 29 | callback(async.cb); 30 | yield async; 31 | return a + b; 32 | }; 33 | 34 | var add = async.fn(generator); 35 | 36 | add(7, 11, function(err, value) { 37 | assert.equal(value, 18); 38 | done(); 39 | }); 40 | 41 | }); 42 | 43 | test('explicitCallback', function(done) { 44 | 45 | var pacify = new NodeCallback; 46 | 47 | async.run(function*() { 48 | var state = yield pacify(async.cb); 49 | assert.equal(state, "OK"); 50 | done() 51 | }); 52 | }); 53 | 54 | test('proxyClassic', function(done) { 55 | 56 | var callback = new NodeCallback; 57 | var proxy = async.proxy(callback); 58 | 59 | assert.ok(proxy instanceof Function); 60 | 61 | proxy(function(err, data) { 62 | assert.equal(data, "OK"); 63 | done(); 64 | }); 65 | 66 | }); 67 | 68 | test('proxyGenerator', function(done) { 69 | 70 | var callback = new NodeCallback; 71 | var proxy = async.proxy(callback); 72 | 73 | assert.ok(proxy instanceof Function); 74 | 75 | async.run(function*() { 76 | var data = yield proxy(); 77 | assert.equal(data, "OK"); 78 | done(); 79 | }); 80 | 81 | }); 82 | 83 | test('proxyGeneratorMulti', function(done) { 84 | 85 | var identity = new Identity; 86 | var proxy = async.proxy(identity); 87 | 88 | assert.ok(proxy instanceof Function); 89 | 90 | async.run(function*() { 91 | 92 | var data1 = yield proxy("OK 1"); 93 | assert.equal(data1, "OK 1"); 94 | 95 | var data2 = yield proxy("OK 2"); 96 | assert.equal(data2, "OK 2"); 97 | 98 | done(); 99 | }); 100 | 101 | }); 102 | 103 | test('promise', function(done) { 104 | 105 | var accountant = new AccountantGuarantor; 106 | 107 | async.run(function*() { 108 | var sum = yield accountant.add(7, 5); 109 | assert.equal(sum, 12); 110 | done(); 111 | }); 112 | 113 | }); 114 | 115 | test('promiseParallel', function(done) { 116 | 117 | var accountant = new AccountantGuarantor; 118 | 119 | async.run(function*() { 120 | 121 | async.parallel( accountant.add(7, 2) ); 122 | async.parallel( accountant.add(3, 5) ); 123 | 124 | var nine = yield async; 125 | var eight = yield async; 126 | 127 | assert.equal(nine, 9); 128 | assert.equal(eight, 8); 129 | 130 | done(); 131 | }); 132 | 133 | }); 134 | 135 | test('promiseReject', function(done) { 136 | 137 | var accountant = new AccountantGuarantor; 138 | 139 | async.run(function*() { 140 | try { 141 | var sum = yield accountant.add(null); 142 | } catch(e) { 143 | assert.ok(!!e.message.match(/bad params/)); 144 | done(); 145 | } 146 | }); 147 | 148 | }); 149 | 150 | test('parallel', function(done) { 151 | 152 | var identity = async.wrap(new Identity); 153 | var delay = async.wrap(new Delay); 154 | 155 | async.run(function*() { 156 | 157 | async.parallel(delay(3)); 158 | async.parallel(identity(7)); 159 | 160 | var three = yield async; 161 | var seven = yield async; 162 | 163 | assert.equal(three, 3); 164 | assert.equal(seven, 7); 165 | done(); 166 | }); 167 | 168 | }); 169 | 170 | test('parallelCallback', function(done) { 171 | 172 | var identity = new Identity; 173 | var delay = new Delay; 174 | 175 | async.run(function*() { 176 | 177 | async.parallel(delay(3, async.cb)); 178 | async.parallel(identity(7, async.cb)); 179 | 180 | var three = yield async; 181 | var seven = yield async; 182 | 183 | assert.equal(three, 3); 184 | assert.equal(seven, 7); 185 | done(); 186 | }); 187 | 188 | }); 189 | 190 | test('parallelMixed', function(done) { 191 | 192 | var identity = new Identity; 193 | var delay = async.wrap(new Delay); 194 | 195 | async.run(function*() { 196 | 197 | async.parallel(delay(3)); 198 | async.parallel(identity(7, async.cb)); 199 | 200 | var three = yield async; 201 | var seven = yield async; 202 | 203 | assert.equal(three, 3); 204 | assert.equal(seven, 7); 205 | done(); 206 | }); 207 | 208 | }); 209 | 210 | test('defer', function(done) { 211 | 212 | var identity = async.wrap(new Identity); 213 | 214 | async.run(function*() { 215 | 216 | async.parallel(identity(27)); 217 | var twentySeven = yield async; 218 | 219 | assert.equal(twentySeven, 27); 220 | done(); 221 | }); 222 | 223 | }); 224 | 225 | test('deferMulti', function(done) { 226 | 227 | var identity = async.wrap(new Identity); 228 | 229 | async.run(function*() { 230 | 231 | async.parallel(identity(28)); 232 | async.parallel(identity(29)); 233 | 234 | var twentyEight = yield async; 235 | var twentyNine = yield async; 236 | 237 | assert.equal(twentyEight, 28); 238 | assert.equal(twentyNine, 29); 239 | 240 | done(); 241 | }); 242 | 243 | }); 244 | 245 | test('generatorMethod', function(done) { 246 | 247 | var Rectangle = new RectangleClass; 248 | var areaMethod = async.proxy(async.fn(Rectangle.prototype.area)); 249 | 250 | async.run(function*() { 251 | var rect = new Rectangle(11, 3); 252 | var area = yield areaMethod.apply(rect); 253 | assert.equal(area, 33); 254 | done(); 255 | }); 256 | 257 | }); 258 | 259 | test('generatorClass', function(done) { 260 | 261 | var Rectangle = new RectangleClass; 262 | Rectangle = async.wrap(Rectangle); 263 | 264 | async.run(function*() { 265 | var rect = new Rectangle(11, 3); 266 | var area = yield rect.area(); 267 | assert.equal(area, 33); 268 | done(); 269 | }); 270 | 271 | }); 272 | 273 | test('error', function(done) { 274 | 275 | var err = new Err; 276 | var gentry = async.wrap(err); 277 | 278 | async.run(function*() { 279 | try { 280 | var data = yield gentry(); 281 | } catch(e) { 282 | assert.ok(!!e.message.match(/oh noes/)); 283 | done(); 284 | } 285 | }); 286 | 287 | }); 288 | 289 | test('keys', function(done) { 290 | 291 | var maths = async.wrap(new Maths); 292 | 293 | async.run(function*() { 294 | var sum = yield maths.add(5, 7); 295 | assert.equal(sum, 12); 296 | done(); 297 | }); 298 | 299 | }); 300 | 301 | test('asyncConstructorClassic', function(done) { 302 | 303 | var Rect = function() { 304 | this.initialize.apply(this, arguments); 305 | }; 306 | 307 | Rect.prototype.initialize = function(w, h, cb) { 308 | this.w = w; 309 | this.h = h; 310 | cb(null, this); 311 | }; 312 | 313 | async.run(function*() { 314 | var rect = new Rect(2, 7, function(err, rect) { 315 | assert.equal(rect.w, 2); 316 | assert.equal(rect.h, 7); 317 | done(); 318 | }); 319 | }); 320 | 321 | }); 322 | 323 | test('asyncConstructor', function(done) { 324 | 325 | var Rect = function(w, h, cb) { 326 | this.w = w; 327 | this.h = h; 328 | cb(null, this); 329 | }; 330 | 331 | Rect = async.wrap(Rect); 332 | 333 | async.run(function*() { 334 | var rect = yield new Rect(2, 7); 335 | assert.equal(rect.w, 2); 336 | assert.equal(rect.h, 7); 337 | done(); 338 | }); 339 | 340 | }); 341 | 342 | test('asyncInitialize', function(done) { 343 | 344 | var Rect = function(w, h, cb) { 345 | this.initialize(w, h, cb); 346 | }; 347 | 348 | Rect.prototype.initialize = function*(w, h) { 349 | this.w = w; 350 | this.h = h; 351 | return this; 352 | }; 353 | 354 | Rect.prototype.area = function*() { 355 | return this.w * this.h; 356 | }; 357 | 358 | Rect = async.wrap(Rect); 359 | 360 | async.run(function*() { 361 | var rect = yield new Rect(2, 7); 362 | assert.equal(rect.w, 2); 363 | assert.equal(rect.h, 7); 364 | done(); 365 | }); 366 | 367 | }); 368 | 369 | test('fire', function(done) { 370 | 371 | var x = 0; 372 | 373 | var dl = async.proxy(function(value, callback) { 374 | setTimeout(function() { 375 | callback(null, value); 376 | }, 250); 377 | }); 378 | 379 | var incrementer = async(function*() { 380 | x += 1; 381 | }); 382 | 383 | async.run(function*() { 384 | async.fire(incrementer()); 385 | }); 386 | 387 | setTimeout(function() { 388 | assert.equal(x, 1); 389 | done(); 390 | }, 100); 391 | 392 | }); 393 | 394 | }); 395 | 396 | function Identity() { 397 | return function(value, callback) { 398 | setImmediate(function() { 399 | callback(null, value); 400 | }); 401 | }; 402 | }; 403 | 404 | function Delay() { 405 | return function(value, callback) { 406 | setTimeout(function() { 407 | callback(null, value); 408 | }, 250); 409 | } 410 | }; 411 | 412 | function Err() { 413 | return function(callback) { 414 | setImmediate(function() { 415 | callback("oh noes!"); 416 | }); 417 | }; 418 | }; 419 | 420 | function NodeCallback() { 421 | return function(callback) { 422 | setImmediate(function() { 423 | callback(null, "OK"); 424 | }); 425 | } 426 | }; 427 | 428 | function Maths() { 429 | return { 430 | add: function(a, b, callback) { 431 | callback(null, a + b); 432 | }, 433 | multiply: function(a, b, callback) { 434 | callback 435 | } 436 | }; 437 | }; 438 | 439 | function AccountantGuarantor() { 440 | return { 441 | add: function(a, b) { 442 | return new Promise(function(resolve, reject) { 443 | if (Number(a) !== a) reject("bad params"); 444 | setImmediate(function() { resolve(a + b) }); 445 | }) 446 | } 447 | }; 448 | }; 449 | 450 | function RectangleClass() { 451 | 452 | var Rectangle = function() { 453 | this.initialize.apply(this, arguments); 454 | }; 455 | 456 | Rectangle.prototype = { 457 | 458 | initialize: function(width, height) { 459 | this.width = width; 460 | this.height = height; 461 | }, 462 | area: function*() { 463 | return this.width * this.height; 464 | } 465 | }; 466 | 467 | return Rectangle; 468 | }; 469 | 470 | --------------------------------------------------------------------------------