├── .gitignore ├── README ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | AsyncArray 2 | ========== 3 | 4 | Yet another control flow library after getting fustrated with previous ones. 5 | 6 | ## Usage 7 | 8 | The `next` callback takes the arguments (error, data) 9 | 10 | var AsyncArray = require('async-array') 11 | 12 | var array = new AsyncArray([1, 2, 3, 4]) 13 | 14 | array 15 | .map(function (item, i, next) { 16 | db.query('SELECT * FROM x WHERE id = ?', [item], next) 17 | }) 18 | .done(function (error, results) { 19 | console.log("Got me database listings partner!") 20 | }) 21 | .forEach(function (db_result, i, next) { 22 | doSomethingAsync(db_result, next) 23 | }) 24 | .exec() 25 | 26 | As you can see, you can chain stuff and the result is passed along from the previous operation. If you don't call `exec` immediately you can store the operation to be executed at some later time. 27 | 28 | `AsyncArray` inherits from `Array` with the following methods added: 29 | 30 | - map 31 | - mapSerial 32 | - filter 33 | - filterSerial 34 | - forEach 35 | - forEachSerial 36 | 37 | Serial methods do things one after another instead of in parallel. 38 | 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // The MIT License 2 | // 3 | // Copyright (c) 2011 Tim Smart 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a 6 | // copy of this software and associated documentation files 7 | // (the "Software"), to deal in the Software without restriction, 8 | // including without limitation the rights to use, copy, modify, merge, 9 | // publish, distribute, sublicense, and/or sell copies of the Software, and 10 | // to permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included 14 | // in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | // 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 | 24 | /** 25 | * Array for async operations 26 | * 27 | * @constructor 28 | * @extends {Array} 29 | * @param @optional {Array} arr : Base elements. 30 | */ 31 | function AsyncArray (arr) { 32 | arr || (arr = []) 33 | arr.__proto__ = this.__proto__ 34 | return arr 35 | } 36 | 37 | AsyncArray.prototype.__proto__ = Array.prototype 38 | 39 | // Export 40 | module.exports = function async (array) { 41 | array || (array = []) 42 | array.__proto__ = AsyncArray.prototype 43 | return array 44 | } 45 | 46 | // Backward compatability 47 | module.exports.async = module.exports 48 | 49 | /** 50 | * Convert back to a normal array. 51 | * 52 | * @return {Array} 53 | */ 54 | AsyncArray.prototype.array = function () { 55 | this.__proto__ = Array.prototype 56 | return this 57 | } 58 | 59 | // Proxy methods to operations. 60 | AsyncArray.prototype.forEach = function (callback) { 61 | return new Operation(this).forEach(callback) 62 | } 63 | AsyncArray.prototype.forEachSerial = function (callback) { 64 | return new Operation(this).forEachSerial(callback) 65 | } 66 | AsyncArray.prototype.map = function (callback) { 67 | return new Operation(this).map(callback) 68 | } 69 | AsyncArray.prototype.mapSerial = function (callback) { 70 | return new Operation(this).mapSerial(callback) 71 | } 72 | AsyncArray.prototype.filter = function (callback) { 73 | return new Operation(this).filter(callback) 74 | } 75 | AsyncArray.prototype.filterSerial = function (callback) { 76 | return new Operation(this).filterSerial(callback) 77 | } 78 | 79 | // -------------------- 80 | 81 | /** 82 | * Represents a async operation 83 | * 84 | * @constructor 85 | * @param {AsyncArray} array 86 | */ 87 | function Operation (array) { 88 | this.array = array 89 | this.steps = [] 90 | } 91 | 92 | // Export op 93 | module.exports.Operation = Operation 94 | 95 | /** 96 | * Get the last added step 97 | * 98 | * @return {Step} 99 | */ 100 | Operation.prototype.lastStep = function () { 101 | return this.steps[this.steps.length - 1] 102 | } 103 | 104 | /** 105 | * Run the next step 106 | * 107 | * @param {OperState} state 108 | * @param {Error} error 109 | * @param {AsyncArray} result 110 | */ 111 | Operation.prototype._next = function (state, error, result) { 112 | if (error) { 113 | return 114 | } 115 | 116 | ++state.index 117 | if (this.steps[state.index]) { 118 | this.steps[state.index].run(state, result) 119 | } 120 | } 121 | 122 | /** 123 | * Add's a callback for the last step item 124 | * 125 | * @param {Function} callback 126 | */ 127 | Operation.prototype.done = function (callback) { 128 | this.lastStep().callbacks.push(callback) 129 | return this 130 | } 131 | 132 | /** 133 | * Iterates over the array. 134 | * 135 | * @param {Function} callback 136 | */ 137 | Operation.prototype.forEach = function (callback) { 138 | this.steps.push(new ForEach(this, callback)) 139 | return this 140 | } 141 | 142 | /** 143 | * Iterates over the array. Serial. 144 | * 145 | * @param {Function} callback 146 | */ 147 | Operation.prototype.forEachSerial = function (callback) { 148 | this.steps.push(new ForEach(this, callback, true)) 149 | return this 150 | } 151 | 152 | /** 153 | * Creates a new array from the results 154 | * 155 | * @param {Function} callback 156 | */ 157 | Operation.prototype.map = function (callback) { 158 | this.steps.push(new Map(this, callback)) 159 | return this 160 | } 161 | 162 | /** 163 | * Creates a new array from the results. Serial. 164 | * 165 | * @param {Function} callback 166 | */ 167 | Operation.prototype.mapSerial = function (callback) { 168 | this.steps.push(new Map(this, callback, true)) 169 | return this 170 | } 171 | 172 | /** 173 | * Filters an array 174 | * 175 | * @param {Function} callback 176 | */ 177 | Operation.prototype.filter = function (callback) { 178 | this.steps.push(new Filter(this, callback)) 179 | return this 180 | } 181 | 182 | /** 183 | * Filters an array. Serial 184 | * 185 | * @param {Function} callback 186 | */ 187 | Operation.prototype.filterSerial = function (callback) { 188 | this.steps.push(new Filter(this, callback, true)) 189 | return this 190 | } 191 | 192 | /** 193 | * Starts the operation 194 | */ 195 | Operation.prototype.exec = function (callback) { 196 | if (callback) { 197 | this.done(callback) 198 | } 199 | var state = new OperState(this) 200 | this.steps[0].run(state, this.array) 201 | return this 202 | } 203 | 204 | /** 205 | * Save an operation into a function. 206 | * 207 | * The function created will work like: myoper([1, 2, 3], callback) 208 | */ 209 | Operation.prototype.save = function save () { 210 | var steps = this.steps 211 | 212 | return function asyncop (arr, done) { 213 | arr = module.exports(arr) 214 | var oper = new Operation(arr) 215 | oper.steps = steps 216 | return oper.exec(done) 217 | } 218 | } 219 | 220 | // -------------------- 221 | 222 | /** 223 | * Keep track of things. 224 | * 225 | * @constructor 226 | * @param {Step} step 227 | * @param {AsyncArray} array 228 | * @param {AsyncArray} result 229 | */ 230 | function OperState (oper) { 231 | this.oper = oper 232 | this.index = 0 233 | } 234 | 235 | // -------------------- 236 | 237 | /** 238 | * Reprensents a step in a operation 239 | * 240 | * @constructor 241 | * @param {Operation} oper 242 | * @param {Function} callback 243 | */ 244 | function Step (oper, callback, serial) { 245 | var step = this 246 | this.oper = oper 247 | this.callbacks = [] 248 | this.callback = callback 249 | this.serial = serial || false 250 | } 251 | 252 | /** 253 | * Called when a iteration is done. 254 | * 255 | * @param {Error} error 256 | * @param @optional {Mixed} result 257 | */ 258 | Step.prototype.next = function (state, i, error, data) { 259 | if (state.done) { 260 | return 261 | } 262 | 263 | if (error) { 264 | state.done = true 265 | return this.done(error, state) 266 | } 267 | 268 | ++state.count 269 | 270 | if (this.serial) { 271 | if (state.count >= state.array.length) { 272 | state.done = true 273 | return this.done(null, state) 274 | } 275 | 276 | return this.callback(state.array[state.count], state.count, state.serialfn) 277 | } 278 | 279 | if (state.count >= state.array.length) { 280 | state.done = true 281 | this.done(null, state) 282 | } 283 | } 284 | 285 | /** 286 | * Called when the step is done. 287 | * 288 | * @param {Error} error 289 | * @param {StepState} state 290 | */ 291 | Step.prototype.done = function (error, state) { 292 | module.exports(state.result) 293 | 294 | for (var i = 0, il = this.callbacks.length; i < il; i++) { 295 | this.callbacks[i].call(this.oper, error, state.result) 296 | } 297 | 298 | this.oper._next(state.oper_state, error, state.result) 299 | } 300 | 301 | /** 302 | * Get the party started 303 | * 304 | * @param {AsyncArray} array 305 | */ 306 | Step.prototype.run = function (oper_state, array) { 307 | var step = this 308 | , state = new StepState(this, oper_state, array, []) 309 | 310 | if (array.length === 0) { 311 | return this.done(null, state) 312 | } 313 | 314 | if (this.serial) { 315 | state.serialfn = function (error, data) { 316 | step.next(state, state.count, error, data) 317 | } 318 | return this.callback(array[0], 0, state.serialfn) 319 | } 320 | 321 | Array.prototype.forEach.call(array, function (item, i) { 322 | step.callback(item, i, function (error, data) { 323 | step.next(state, i, error, data) 324 | }) 325 | }) 326 | } 327 | 328 | // -------------------- 329 | 330 | /** 331 | * Keep track of things. 332 | * 333 | * @constructor 334 | * @param {Step} step 335 | * @param {AsyncArray} array 336 | * @param {AsyncArray} result 337 | */ 338 | function StepState (step, oper_state, array, result) { 339 | this.step = step 340 | this.oper_state = oper_state 341 | this.array = array 342 | this.result = result || array 343 | this.count = 0 344 | this.done = false 345 | this.serialfn = null 346 | } 347 | 348 | // -------------------- 349 | 350 | /** 351 | * A forEach step 352 | * 353 | * @constructor 354 | * @extends {Step} 355 | * @param {Operation} oper 356 | * @param {Function} callback 357 | */ 358 | function ForEach (oper, callback, serial) { 359 | Step.call(this, oper, callback, serial) 360 | } 361 | 362 | // Inherit Step 363 | ForEach.prototype.__proto__ = Step.prototype 364 | 365 | /** 366 | * Called when a iteration is done. 367 | * 368 | * @param {Error} error 369 | * @param @optional {Mixed} result 370 | */ 371 | ForEach.prototype.next = function (state, i, error, data) { 372 | if ('undefined' !== typeof data) { 373 | state.result.push(data) 374 | } 375 | 376 | Step.prototype.next.call(this, state, i, error, data) 377 | } 378 | 379 | /** 380 | * When the step is done 381 | * 382 | * @param {Error} error 383 | * @param {StepState} state 384 | */ 385 | ForEach.prototype.done = function (error, state) { 386 | if (0 === state.result.length) { 387 | state.result = state.array 388 | } 389 | 390 | Step.prototype.done.call(this, error, state) 391 | } 392 | 393 | // -------------------- 394 | 395 | /** 396 | * A map step 397 | * 398 | * @constructor 399 | * @extends {Step} 400 | * @param {Operation} oper 401 | * @param {Function} callback 402 | */ 403 | function Map (oper, callback, serial) { 404 | Step.call(this, oper, callback, serial) 405 | } 406 | 407 | // Inherit Step 408 | Map.prototype.__proto__ = Step.prototype 409 | 410 | /** 411 | * Called when a iteration is done. 412 | * 413 | * @param {Error} error 414 | * @param @optional {Mixed} result 415 | */ 416 | Map.prototype.next = function (state, i, error, data) { 417 | state.result[i] = data 418 | Step.prototype.next.call(this, state, i, error, data) 419 | } 420 | 421 | // -------------------- 422 | 423 | /** 424 | * A filter step 425 | * 426 | * @constructor 427 | * @extends {Step} 428 | * @param {Operation} oper 429 | * @param {Function} callback 430 | */ 431 | function Filter (oper, callback, serial) { 432 | Step.call(this, oper, callback, serial) 433 | } 434 | 435 | // Inherit Step 436 | Filter.prototype.__proto__ = Step.prototype 437 | 438 | /** 439 | * Sort function for filtered results 440 | * 441 | * @param {Number} a 442 | * @param {Number} b 443 | */ 444 | Filter.SORTFN = function (a, b) { 445 | return a - b 446 | } 447 | 448 | /** 449 | * Called when a iteration is done. 450 | * 451 | * @param {Error} error 452 | * @param @optional {Mixed} result 453 | */ 454 | Filter.prototype.next = function (state, i, error, data) { 455 | if (data === true) { 456 | state.result.push(i) 457 | } 458 | 459 | Step.prototype.next.call(this, state, i, error, data) 460 | } 461 | 462 | /** 463 | * When the step is done 464 | * 465 | * @param {Error} error 466 | * @param {StepState} state 467 | */ 468 | Filter.prototype.done = function (error, state) { 469 | if (!error && !this.serial) { 470 | var result = state.result.sort(Filter.SORTFN) 471 | state.result = [] 472 | 473 | for (var i = 0, il = result.length; i < il; i++) { 474 | state.result.push(state.array[result[i]]) 475 | } 476 | } 477 | 478 | Step.prototype.done.call(this, error, state) 479 | } 480 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "async-array" 2 | , "description" : "A sane control flow library" 3 | , "keywords" : ["control", "flow", "async", "array"] 4 | , "version" : "0.2.0" 5 | , "author" : "Tim Smart" 6 | , "repository" : 7 | { "type" : "git" 8 | , "url" : "git://github.com/Tim-Smart/async-array.git" 9 | } 10 | , "main" : "./" 11 | } 12 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var AsyncArray = require('./') 2 | 3 | var items = new AsyncArray([1, 2, 'three', 4]) 4 | 5 | items 6 | .map(function (item, i, next) { 7 | setTimeout(next, 1000, null, 'did ' + i) 8 | }) 9 | .done(function (error, results) { 10 | console.log('map', results) 11 | }) 12 | .exec() 13 | 14 | items 15 | .mapSerial(function (item, i, next) { 16 | setTimeout(next, 1000, null, 'did ' + i) 17 | }) 18 | .done(function (error, results) { 19 | console.log('mapSerial', results, results.length) 20 | }) 21 | .exec() 22 | 23 | items 24 | .filter(function (item, i, next) { 25 | setTimeout(function () { 26 | if ('string' === typeof item) { 27 | return next() 28 | } 29 | next(null, true) 30 | }, 1000) 31 | }) 32 | .exec(function (error, results) { 33 | console.log('filter', results) 34 | }) 35 | 36 | items 37 | .forEachSerial(function (item, i, next) { 38 | setTimeout(function () { 39 | console.log('forEachSerial', i, item) 40 | next() 41 | }, 1000) 42 | }) 43 | .exec() 44 | 45 | items 46 | .map(function (item, i, next) { 47 | if ('string' === typeof item) { 48 | return next(new Error('stupid')) 49 | } 50 | next(null, item) 51 | }) 52 | .exec(function (error) { 53 | console.log('error', error) 54 | }) 55 | 56 | items 57 | .filter(function (item, i, next) { 58 | if ('string' === typeof item) { 59 | return next() 60 | } 61 | next(null, true) 62 | }) 63 | .done(function (error, results) { 64 | console.log('chain filter', results) 65 | }) 66 | .map(function (item, i, next) { 67 | next(null, 'item ' + i + ': ' + item) 68 | }) 69 | .exec(function (error, results) { 70 | console.log('chain map', results) 71 | results.array() 72 | console.log('normal array', 'undefined' === typeof results.array) 73 | }) 74 | 75 | var op = new AsyncArray.Operation; 76 | op.forEachSerial(function foreach (item, i, next) { 77 | console.log('forEachSaved', i) 78 | next() 79 | }) 80 | op = op.save() 81 | 82 | op([1, 2, 3], function (err) { 83 | console.log('forEachSaved', 'done') 84 | }) 85 | --------------------------------------------------------------------------------