├── LICENSE-MIT ├── README.md ├── dist ├── ba-foreach.js └── ba-foreach.min.js ├── grunt.js ├── lib └── foreach.js ├── package.json └── test └── foreach_test.js /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 "Cowboy" Ben Alman 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Sync/Async forEach 2 | 3 | An optionally-asynchronous forEach with an interesting interface. 4 | 5 | ## Getting Started 6 | 7 | This code should work just fine in Node.js: 8 | 9 | First, install the module with: `npm install async-foreach` 10 | 11 | ```javascript 12 | var forEach = require('async-foreach').forEach; 13 | forEach(["a", "b", "c"], function(item, index, arr) { 14 | console.log("each", item, index, arr); 15 | }); 16 | // logs: 17 | // each a 0 ["a", "b", "c"] 18 | // each b 1 ["a", "b", "c"] 19 | // each c 2 ["a", "b", "c"] 20 | ``` 21 | 22 | Or in the browser: 23 | 24 | ```html 25 | 26 | 35 | ``` 36 | 37 | In the browser, you can attach the forEach method to any object. 38 | 39 | ```html 40 | 43 | 44 | 53 | ``` 54 | 55 | ## The General Idea (Why I thought this was worth sharing) 56 | 57 | The idea is to allow the callback to decide _at runtime_ whether the loop will be synchronous or asynchronous. By using `this` in a creative way (in situations where that value isn't already spoken for), an entire control API can be offered without over-complicating function signatures. 58 | 59 | ```javascript 60 | forEach(arr, function(item, index) { 61 | // Synchronous. 62 | }); 63 | 64 | forEach(arr, function(item, index) { 65 | // Only when `this.async` is called does iteration becomes asynchronous. The 66 | // loop won't be continued until the `done` function is executed. 67 | var done = this.async(); 68 | // Continue in one second. 69 | setTimeout(done, 1000); 70 | }); 71 | 72 | forEach(arr, function(item, index) { 73 | // Break out of synchronous iteration early by returning false. 74 | return index !== 1; 75 | }); 76 | 77 | forEach(arr, function(item, index) { 78 | // Break out of asynchronous iteration early... 79 | var done = this.async(); 80 | // ...by passing false to the done function. 81 | setTimeout(function() { 82 | done(index !== 1); 83 | }); 84 | }); 85 | ``` 86 | 87 | ## Examples 88 | See the unit tests for more examples. 89 | 90 | ```javascript 91 | // Generic "done" callback. 92 | function allDone(notAborted, arr) { 93 | console.log("done", notAborted, arr); 94 | } 95 | 96 | // Synchronous. 97 | forEach(["a", "b", "c"], function(item, index, arr) { 98 | console.log("each", item, index, arr); 99 | }, allDone); 100 | // logs: 101 | // each a 0 ["a", "b", "c"] 102 | // each b 1 ["a", "b", "c"] 103 | // each c 2 ["a", "b", "c"] 104 | // done true ["a", "b", "c"] 105 | 106 | // Synchronous with early abort. 107 | forEach(["a", "b", "c"], function(item, index, arr) { 108 | console.log("each", item, index, arr); 109 | if (item === "b") { return false; } 110 | }, allDone); 111 | // logs: 112 | // each a 0 ["a", "b", "c"] 113 | // each b 1 ["a", "b", "c"] 114 | // done false ["a", "b", "c"] 115 | 116 | // Asynchronous. 117 | forEach(["a", "b", "c"], function(item, index, arr) { 118 | console.log("each", item, index, arr); 119 | var done = this.async(); 120 | setTimeout(function() { 121 | done(); 122 | }, 500); 123 | }, allDone); 124 | // logs: 125 | // each a 0 ["a", "b", "c"] 126 | // each b 1 ["a", "b", "c"] 127 | // each c 2 ["a", "b", "c"] 128 | // done true ["a", "b", "c"] 129 | 130 | // Asynchronous with early abort. 131 | forEach(["a", "b", "c"], function(item, index, arr) { 132 | console.log("each", item, index, arr); 133 | var done = this.async(); 134 | setTimeout(function() { 135 | done(item !== "b"); 136 | }, 500); 137 | }, allDone); 138 | // logs: 139 | // each a 0 ["a", "b", "c"] 140 | // each b 1 ["a", "b", "c"] 141 | // done false ["a", "b", "c"] 142 | 143 | // Not actually asynchronous. 144 | forEach(["a", "b", "c"], function(item, index, arr) { 145 | console.log("each", item, index, arr); 146 | var done = this.async() 147 | done(); 148 | }, allDone); 149 | // logs: 150 | // each a 0 ["a", "b", "c"] 151 | // each b 1 ["a", "b", "c"] 152 | // each c 2 ["a", "b", "c"] 153 | // done true ["a", "b", "c"] 154 | 155 | // Not actually asynchronous with early abort. 156 | forEach(["a", "b", "c"], function(item, index, arr) { 157 | console.log("each", item, index, arr); 158 | var done = this.async(); 159 | done(item !== "b"); 160 | }, allDone); 161 | // logs: 162 | // each a 0 ["a", "b", "c"] 163 | // each b 1 ["a", "b", "c"] 164 | // done false ["a", "b", "c"] 165 | ``` 166 | 167 | ## Contributing 168 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [grunt](https://github.com/cowboy/grunt). 169 | 170 | _Also, please don't edit files in the "dist" subdirectory as they are generated via grunt. You'll find source code in the "lib" subdirectory!_ 171 | 172 | ## Release History 173 | 174 | 04/29/2013 175 | v0.1.3 176 | Removed hard Node.js version dependency. 177 | 178 | 11/17/2011 179 | v0.1.2 180 | Adding sparse array support. 181 | Invalid length properties are now sanitized. 182 | This closes issue #1 (like a boss). 183 | 184 | 11/11/2011 185 | v0.1.1 186 | Refactored code to be much simpler. Yay for unit tests! 187 | 188 | 11/11/2011 189 | v0.1.0 190 | Initial Release. 191 | 192 | ## License 193 | Copyright (c) 2012 "Cowboy" Ben Alman 194 | Licensed under the MIT license. 195 | 196 | -------------------------------------------------------------------------------- /dist/ba-foreach.js: -------------------------------------------------------------------------------- 1 | /* JavaScript Sync/Async forEach - v0.1.2 - 1/10/2012 2 | * http://github.com/cowboy/javascript-sync-async-foreach 3 | * Copyright (c) 2012 "Cowboy" Ben Alman; Licensed MIT */ 4 | 5 | (function(exports) { 6 | 7 | // Iterate synchronously or asynchronously. 8 | exports.forEach = function(arr, eachFn, doneFn) { 9 | var i = -1; 10 | // Resolve array length to a valid (ToUint32) number. 11 | var len = arr.length >>> 0; 12 | 13 | // This IIFE is called once now, and then again, by name, for each loop 14 | // iteration. 15 | (function next(result) { 16 | // This flag will be set to true if `this.async` is called inside the 17 | // eachFn` callback. 18 | var async; 19 | // Was false returned from the `eachFn` callback or passed to the 20 | // `this.async` done function? 21 | var abort = result === false; 22 | 23 | // Increment counter variable and skip any indices that don't exist. This 24 | // allows sparse arrays to be iterated. 25 | do { ++i; } while (!(i in arr) && i !== len); 26 | 27 | // Exit if result passed to `this.async` done function or returned from 28 | // the `eachFn` callback was false, or when done iterating. 29 | if (abort || i === len) { 30 | // If a `doneFn` callback was specified, invoke that now. Pass in a 31 | // boolean value representing "not aborted" state along with the array. 32 | if (doneFn) { 33 | doneFn(!abort, arr); 34 | } 35 | return; 36 | } 37 | 38 | // Invoke the `eachFn` callback, setting `this` inside the callback to a 39 | // custom object that contains one method, and passing in the array item, 40 | // index, and the array. 41 | result = eachFn.call({ 42 | // If `this.async` is called inside the `eachFn` callback, set the async 43 | // flag and return a function that can be used to continue iterating. 44 | async: function() { 45 | async = true; 46 | return next; 47 | } 48 | }, arr[i], i, arr); 49 | 50 | // If the async flag wasn't set, continue by calling `next` synchronously, 51 | // passing in the result of the `eachFn` callback. 52 | if (!async) { 53 | next(result); 54 | } 55 | }()); 56 | }; 57 | 58 | }(typeof exports === "object" && exports || this)); -------------------------------------------------------------------------------- /dist/ba-foreach.min.js: -------------------------------------------------------------------------------- 1 | /* JavaScript Sync/Async forEach - v0.1.2 - 1/10/2012 2 | * http://github.com/cowboy/javascript-sync-async-foreach 3 | * Copyright (c) 2012 "Cowboy" Ben Alman; Licensed MIT */ 4 | (function(a){a.forEach=function(a,b,c){var d=-1,e=a.length>>>0;(function f(g){var h,j=g===!1;do++d;while(!(d in a)&&d!==e);if(j||d===e){c&&c(!j,a);return}g=b.call({async:function(){return h=!0,f}},a[d],d,a),h||f(g)})()}})(typeof exports=="object"&&exports||this) -------------------------------------------------------------------------------- /grunt.js: -------------------------------------------------------------------------------- 1 | /*global config:true, task:true*/ 2 | config.init({ 3 | pkg: '', 4 | meta: { 5 | title: 'JavaScript Sync/Async forEach', 6 | license: ['MIT'], 7 | copyright: 'Copyright (c) 2012 "Cowboy" Ben Alman', 8 | banner: '/* {{meta.title}} - v{{pkg.version}} - {{today "m/d/yyyy"}}\n' + 9 | ' * {{pkg.homepage}}\n' + 10 | ' * {{{meta.copyright}}}; Licensed {{join meta.license}} */' 11 | }, 12 | concat: { 13 | 'dist/ba-foreach.js': ['', ''] 14 | }, 15 | min: { 16 | 'dist/ba-foreach.min.js': ['', 'dist/ba-foreach.js'] 17 | }, 18 | test: { 19 | files: ['test/**/*.js'] 20 | }, 21 | lint: { 22 | files: ['grunt.js', 'lib/**/*.js', 'test/**/*.js'] 23 | }, 24 | watch: { 25 | files: '', 26 | tasks: 'lint:files test:files' 27 | }, 28 | jshint: { 29 | options: { 30 | curly: true, 31 | eqeqeq: true, 32 | immed: true, 33 | latedef: true, 34 | newcap: true, 35 | noarg: true, 36 | sub: true, 37 | undef: true, 38 | eqnull: true 39 | }, 40 | globals: { 41 | exports: true 42 | } 43 | }, 44 | uglify: {} 45 | }); 46 | 47 | // Default task. 48 | task.registerTask('default', 'lint:files test:files concat min'); 49 | -------------------------------------------------------------------------------- /lib/foreach.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Sync/Async forEach 3 | * https://github.com/cowboy/javascript-sync-async-foreach 4 | * 5 | * Copyright (c) 2012 "Cowboy" Ben Alman 6 | * Licensed under the MIT license. 7 | * http://benalman.com/about/license/ 8 | */ 9 | 10 | (function(exports) { 11 | 12 | // Iterate synchronously or asynchronously. 13 | exports.forEach = function(arr, eachFn, doneFn) { 14 | var i = -1; 15 | // Resolve array length to a valid (ToUint32) number. 16 | var len = arr.length >>> 0; 17 | 18 | // This IIFE is called once now, and then again, by name, for each loop 19 | // iteration. 20 | (function next(result) { 21 | // This flag will be set to true if `this.async` is called inside the 22 | // eachFn` callback. 23 | var async; 24 | // Was false returned from the `eachFn` callback or passed to the 25 | // `this.async` done function? 26 | var abort = result === false; 27 | 28 | // Increment counter variable and skip any indices that don't exist. This 29 | // allows sparse arrays to be iterated. 30 | do { ++i; } while (!(i in arr) && i !== len); 31 | 32 | // Exit if result passed to `this.async` done function or returned from 33 | // the `eachFn` callback was false, or when done iterating. 34 | if (abort || i === len) { 35 | // If a `doneFn` callback was specified, invoke that now. Pass in a 36 | // boolean value representing "not aborted" state along with the array. 37 | if (doneFn) { 38 | doneFn(!abort, arr); 39 | } 40 | return; 41 | } 42 | 43 | // Invoke the `eachFn` callback, setting `this` inside the callback to a 44 | // custom object that contains one method, and passing in the array item, 45 | // index, and the array. 46 | result = eachFn.call({ 47 | // If `this.async` is called inside the `eachFn` callback, set the async 48 | // flag and return a function that can be used to continue iterating. 49 | async: function() { 50 | async = true; 51 | return next; 52 | } 53 | }, arr[i], i, arr); 54 | 55 | // If the async flag wasn't set, continue by calling `next` synchronously, 56 | // passing in the result of the `eachFn` callback. 57 | if (!async) { 58 | next(result); 59 | } 60 | }()); 61 | }; 62 | 63 | }(typeof exports === "object" && exports || this)); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "\"Cowboy\" Ben Alman (http://benalman.com/)", 3 | "name": "async-foreach", 4 | "description": "An optionally-asynchronous forEach with an interesting interface.", 5 | "version": "0.1.3", 6 | "homepage": "http://github.com/cowboy/javascript-sync-async-foreach", 7 | "bugs": "https://github.com/cowboy/javascript-sync-async-foreach/issues", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/cowboy/javascript-sync-async-foreach.git" 11 | }, 12 | "main": "lib/foreach", 13 | "engines": { 14 | "node": "*" 15 | }, 16 | "keywords": [ 17 | "array", 18 | "loop", 19 | "sync", 20 | "async", 21 | "foreach" 22 | ], 23 | "dependencies": {}, 24 | "devDependencies": {} 25 | } 26 | -------------------------------------------------------------------------------- /test/foreach_test.js: -------------------------------------------------------------------------------- 1 | /*global require:true, setTimeout:true */ 2 | var forEach = require('../lib/foreach').forEach; 3 | 4 | exports['foreach'] = { 5 | setUp: function(done) { 6 | this.order = []; 7 | this.track = function() { 8 | [].push.apply(this.order, arguments); 9 | }; 10 | done(); 11 | }, 12 | 'Synchronous': function(test) { 13 | test.expect(1); 14 | var that = this; 15 | 16 | var arr = ["a", "b", "c"]; 17 | forEach(arr, function(item, index, arr) { 18 | that.track("each", item, index, arr); 19 | }); 20 | 21 | test.deepEqual(that.order, [ 22 | "each", "a", 0, arr, 23 | "each", "b", 1, arr, 24 | "each", "c", 2, arr 25 | ], "should call eachFn for each array item, in order."); 26 | test.done(); 27 | }, 28 | 'Synchronous, done': function(test) { 29 | test.expect(1); 30 | var that = this; 31 | 32 | var arr = ["a", "b", "c"]; 33 | forEach(arr, function(item, index, arr) { 34 | that.track("each", item, index, arr); 35 | }, function(notAborted, arr) { 36 | that.track("done", notAborted, arr); 37 | }); 38 | 39 | test.deepEqual(that.order, [ 40 | "each", "a", 0, arr, 41 | "each", "b", 1, arr, 42 | "each", "c", 2, arr, 43 | "done", true, arr 44 | ], "should call eachFn for each array item, in order, followed by doneFn."); 45 | test.done(); 46 | }, 47 | 'Synchronous, early abort': function(test) { 48 | test.expect(1); 49 | var that = this; 50 | 51 | var arr = ["a", "b", "c"]; 52 | forEach(arr, function(item, index, arr) { 53 | that.track("each", item, index, arr); 54 | if (item === "b") { return false; } 55 | }, function(notAborted, arr) { 56 | that.track("done", notAborted, arr); 57 | }); 58 | 59 | test.deepEqual(that.order, [ 60 | "each", "a", 0, arr, 61 | "each", "b", 1, arr, 62 | "done", false, arr 63 | ], "should call eachFn for each array item, in order, followed by doneFn."); 64 | test.done(); 65 | }, 66 | 'Asynchronous': function(test) { 67 | test.expect(1); 68 | var that = this; 69 | 70 | var arr = ["a", "b", "c"]; 71 | forEach(arr, function(item, index, arr) { 72 | that.track("each", item, index, arr); 73 | var done = this.async(); 74 | setTimeout(done, 10); 75 | }); 76 | 77 | setTimeout(function() { 78 | test.deepEqual(that.order, [ 79 | "each", "a", 0, arr, 80 | "each", "b", 1, arr, 81 | "each", "c", 2, arr 82 | ], "should call eachFn for each array item, in order."); 83 | test.done(); 84 | }, 100); 85 | }, 86 | 'Asynchronous, done': function(test) { 87 | test.expect(1); 88 | var that = this; 89 | 90 | var arr = ["a", "b", "c"]; 91 | forEach(arr, function(item, index, arr) { 92 | that.track("each", item, index, arr); 93 | var done = this.async(); 94 | setTimeout(done, 10); 95 | }, function(notAborted, arr) { 96 | that.track("done", notAborted, arr); 97 | test.deepEqual(that.order, [ 98 | "each", "a", 0, arr, 99 | "each", "b", 1, arr, 100 | "each", "c", 2, arr, 101 | "done", true, arr 102 | ], "should call eachFn for each array item, in order, followed by doneFn."); 103 | test.done(); 104 | }); 105 | }, 106 | 'Asynchronous, early abort': function(test) { 107 | test.expect(1); 108 | var that = this; 109 | 110 | var arr = ["a", "b", "c"]; 111 | forEach(arr, function(item, index, arr) { 112 | that.track("each", item, index, arr); 113 | var done = this.async(); 114 | setTimeout(function() { 115 | done(item !== "b"); 116 | }, 10); 117 | }, function(notAborted, arr) { 118 | that.track("done", notAborted, arr); 119 | test.deepEqual(that.order, [ 120 | "each", "a", 0, arr, 121 | "each", "b", 1, arr, 122 | "done", false, arr 123 | ], "should call eachFn for each array item, in order, followed by doneFn."); 124 | test.done(); 125 | }); 126 | }, 127 | 'Not actually asynchronous': function(test) { 128 | test.expect(1); 129 | var that = this; 130 | 131 | var arr = ["a", "b", "c"]; 132 | forEach(arr, function(item, index, arr) { 133 | that.track("each", item, index, arr); 134 | var done = this.async(); 135 | done(); 136 | }, function(notAborted, arr) { 137 | that.track("done", notAborted, arr); 138 | test.deepEqual(that.order, [ 139 | "each", "a", 0, arr, 140 | "each", "b", 1, arr, 141 | "each", "c", 2, arr, 142 | "done", true, arr 143 | ], "should call eachFn for each array item, in order, followed by doneFn."); 144 | test.done(); 145 | }); 146 | }, 147 | 'Not actually asynchronous, early abort': function(test) { 148 | test.expect(1); 149 | var that = this; 150 | 151 | var arr = ["a", "b", "c"]; 152 | forEach(arr, function(item, index, arr) { 153 | that.track("each", item, index, arr); 154 | var done = this.async(); 155 | done(item !== "b"); 156 | }, function(notAborted, arr) { 157 | that.track("done", notAborted, arr); 158 | test.deepEqual(that.order, [ 159 | "each", "a", 0, arr, 160 | "each", "b", 1, arr, 161 | "done", false, arr 162 | ], "should call eachFn for each array item, in order, followed by doneFn."); 163 | test.done(); 164 | }); 165 | }, 166 | 'Sparse array support': function(test) { 167 | test.expect(1); 168 | var that = this; 169 | 170 | var arr = []; 171 | arr[0] = "a"; 172 | arr[9] = "z"; 173 | 174 | forEach(arr, function(item, index, arr) { 175 | that.track("each", item, index, arr); 176 | }); 177 | 178 | test.deepEqual(that.order, [ 179 | "each", "a", 0, arr, 180 | "each", "z", 9, arr 181 | ], "should skip nonexistent array items."); 182 | test.done(); 183 | }, 184 | 'Invalid length sanitization': function(test) { 185 | test.expect(1); 186 | var that = this; 187 | 188 | var obj = {length: 4294967299, 0: "a", 2: "b", 3: "c" }; 189 | 190 | forEach(obj, function(item, index, arr) { 191 | that.track("each", item, index, arr); 192 | }); 193 | 194 | test.deepEqual(that.order, [ 195 | "each", "a", 0, obj, 196 | "each", "b", 2, obj 197 | ], "should sanitize length property (ToUint32)."); 198 | test.done(); 199 | } 200 | }; 201 | --------------------------------------------------------------------------------