├── package.json ├── LICENSE ├── test.js ├── lib └── do.js └── README.markdown /package.json: -------------------------------------------------------------------------------- 1 | { "name": "do" 2 | , "version": "0.0.2" 3 | , "author": "Tim Caswell" 4 | , "engines": [ "node" ] 5 | , "main": "./lib/do" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Tim Caswell 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 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // This file is just a mess of examples of how to use the library. 2 | 3 | var Do = require('./lib/do'); 4 | var fs = Do.convert(require('fs'), ["readFile", "stat", "readdir"]); 5 | var http = Do.convert(require('http'), ['cat']); 6 | var sys = require('sys'); 7 | 8 | function debug(message, showHidden) { 9 | sys.error(sys.inspect(message, showHidden)); 10 | } 11 | function showError(trace) { 12 | sys.error("ERROR: " + sys.inspect(trace)); 13 | } 14 | 15 | // A very slow error to make sure that no success message is emitted if there 16 | // is an error anywhere. 17 | function slow_error() { return function (callback, errback) { 18 | setTimeout(function () { 19 | errback(new Error("Yikes!")); 20 | }, 500); 21 | }} 22 | 23 | Do.parallel( 24 | fs.readFile(__filename), 25 | slow_error() 26 | )(function (bad, good) { 27 | sys.puts("Good: " + sys.inspect(arguments)); 28 | }, showError); 29 | 30 | Do.parallel( 31 | fs.readFile(__filename) 32 | )(function (bad, good) { 33 | sys.puts("Good: " + sys.inspect(arguments)); 34 | }, showError); 35 | 36 | Do.parallel( 37 | Do.parallel([ 38 | fs.readFile(__filename), 39 | fs.readFile(__filename) 40 | ]), 41 | fs.readFile(__filename) 42 | )(function () { 43 | sys.puts("Good: " + sys.inspect(arguments)); 44 | }, showError); 45 | 46 | // Filter callback that only let's files through by using stat 47 | function only_files(filename, callback, errback) { 48 | fs.stat(filename)(function (stat) { 49 | callback(stat.isFile()); 50 | }, errback); 51 | } 52 | 53 | // Filter that replaces a filename with the pair of filename and content 54 | function marked_read(filename, callback, errback) { 55 | fs.readFile(filename)(function (data) { 56 | if (data.length < 10) { 57 | errback(new Error(filename + " is too small!")); 58 | } else { 59 | callback([filename, data]); 60 | } 61 | }, errback); 62 | } 63 | 64 | function check_and_load(filename, callback, errback) { 65 | fs.stat(filename)(function (stat) { 66 | if (stat.isFile()) { 67 | marked_read(filename, callback, errback); 68 | } else { 69 | callback(); 70 | } 71 | }, errback); 72 | } 73 | 74 | function loaddir(path) { return function (callback, errback) { 75 | fs.readdir(path)(function (filenames) { 76 | Do.filter(filenames, only_files)(function (filenames) { 77 | Do.map(filenames, marked_read)(callback, errback); 78 | }, errback); 79 | }, errback); 80 | }} 81 | loaddir(__dirname)(debug, showError); 82 | 83 | function fast_loaddir(path) { return function (callback, errback) { 84 | fs.readdir(path)(function (filenames) { 85 | Do.filterMap(filenames, check_and_load)(callback, errback); 86 | }, errback); 87 | }} 88 | fast_loaddir(__dirname)(debug, showError); 89 | 90 | function get_keywords(text) { return function (callback, errback) { 91 | setTimeout(function () { 92 | var last; 93 | var words = text.toLowerCase().replace(/[^a-z ]/g, '').split(' ').sort().filter(function (word) { 94 | if (last === word) { 95 | return false; 96 | } 97 | last = word; 98 | return word.length > 2; 99 | }); 100 | callback(words); 101 | }); 102 | }} 103 | 104 | Do.chain( 105 | fs.readFile(__filename), 106 | get_keywords 107 | )(debug, showError); 108 | 109 | Do.chain( 110 | fs.readdir(__dirname), 111 | function (filenames) { 112 | return Do.filterMap(filenames, check_and_load); 113 | } 114 | )(debug, showError); 115 | 116 | // Use the new continuable style map 117 | var files = ["test.js", "README.markdown"]; 118 | Do.map(files, fs.readFile)(debug, showError); 119 | 120 | function safe_load(filename) { return function (callback, errback) { 121 | fs.stat(filename)(function (stat) { 122 | if (stat.isFile()) { 123 | fs.readFile(filename)(callback, errback) 124 | } else { 125 | callback(); 126 | } 127 | }, errback); 128 | }} 129 | 130 | // Use filterMap with new continuable based filter 131 | fs.readdir(__dirname)(function (list) { 132 | Do.filterMap(list, safe_load)(debug, showError); 133 | }, showError); 134 | -------------------------------------------------------------------------------- /lib/do.js: -------------------------------------------------------------------------------- 1 | // Takes an array of actions and runs them all in parallel. 2 | // You can either pass in an array of actions, or several actions 3 | // as function arguments. 4 | // If you pass in an array, then the output will be an array of all the results 5 | // If you pass in separate arguments, then the output will have several arguments. 6 | exports.parallel = function parallel(actions) { 7 | if (!(actions instanceof Array)) { 8 | actions = Array.prototype.slice.call(arguments); 9 | var direct = true; 10 | } 11 | return function(callback, errback) { 12 | var results = [], 13 | counter = actions.length; 14 | actions.forEach(function (action, i) { 15 | action(function (result) { 16 | results[i] = result; 17 | counter--; 18 | if (counter <= 0) { 19 | if (direct) { 20 | callback.apply(null, results); 21 | } else { 22 | callback.call(null, results); 23 | } 24 | } 25 | }, errback); 26 | }); 27 | } 28 | }; 29 | 30 | // Chains together several actions feeding the output of the first to the 31 | // input of the second and the final output to the callback 32 | exports.chain = function chain(actions) { 33 | if (!(actions instanceof Array)) { 34 | actions = Array.prototype.slice.call(arguments); 35 | } 36 | return function(callback, errback) { 37 | var pos = 0; 38 | var length = actions.length; 39 | function loop(result) { 40 | pos++; 41 | if (pos >= length) { 42 | callback(result); 43 | } else { 44 | actions[pos](result)(loop, errback); 45 | } 46 | } 47 | actions[pos](loop, errback); 48 | } 49 | } 50 | 51 | // Takes an array and does an array map over it using the async callback `fn` 52 | // The signature of `fn` is `function fn(item, callback, errback)` 53 | exports.map = function map(array, fn) { return function (callback, errback) { 54 | var counter = array.length; 55 | var new_array = []; 56 | array.forEach(function (item, index) { 57 | var local_callback = function (result) { 58 | new_array[index] = result; 59 | counter--; 60 | if (counter <= 0) { 61 | new_array.length = array.length 62 | callback(new_array); 63 | } 64 | }; 65 | var cont = fn(item, local_callback, errback); 66 | if (typeof cont === 'function') { 67 | cont(local_callback, errback); 68 | } 69 | }); 70 | }} 71 | 72 | // Takes an array and does an array filter over it using the async callback `fn` 73 | // The signature of `fn` is `function fn(item, callback, errback)` 74 | exports.filter = function filter(array, fn) { return function (callback, errback) { 75 | var counter = array.length; 76 | var valid = {}; 77 | array.forEach(function (item, index) { 78 | var local_callback = function (result) { 79 | valid[index] = result; 80 | counter--; 81 | if (counter <= 0) { 82 | var result = []; 83 | array.forEach(function (item, index) { 84 | if (valid[index]) { 85 | result.push(item); 86 | } 87 | }); 88 | callback(result); 89 | } 90 | }; 91 | var cont = fn(item, local_callback, errback); 92 | if (typeof cont === 'function') { 93 | cont(local_callback, errback); 94 | } 95 | }); 96 | }} 97 | 98 | // Takes an array and does a combined filter and map over it. If the result 99 | // of an item is undefined, then it's filtered out, otherwise it's mapped in. 100 | // The signature of `fn` is `function fn(item, callback, errback)` 101 | exports.filterMap = function filterMap(array, fn) { return function (callback, errback) { 102 | var counter = array.length; 103 | var new_array = []; 104 | array.forEach(function (item, index) { 105 | var local_callback = function (result) { 106 | new_array[index] = result; 107 | counter--; 108 | if (counter <= 0) { 109 | new_array.length = array.length; 110 | callback(new_array.filter(function (item) { 111 | return typeof item !== 'undefined'; 112 | })); 113 | } 114 | }; 115 | var cont = fn(item, local_callback, errback); 116 | if (typeof cont === 'function') { 117 | cont(local_callback, errback); 118 | } 119 | }); 120 | }}; 121 | 122 | // Allows to group several callbacks. 123 | exports.combo = function combo(callback) { 124 | var items = 0, 125 | results = []; 126 | function add() { 127 | var id = items; 128 | items++; 129 | return function () { 130 | check(id, arguments); 131 | }; 132 | } 133 | function check(id, arguments) { 134 | results[id] = Array.prototype.slice.call(arguments); 135 | items--; 136 | if (items == 0) { 137 | callback.apply(this, results); 138 | } 139 | } 140 | return { add: add, check: check }; 141 | } 142 | 143 | 144 | // Takes any async lib that uses callback based signatures and converts 145 | // the specified names to continuable style and returns the new library. 146 | exports.convert = function (lib, names) { 147 | var newlib = {}; 148 | Object.keys(lib).forEach(function (key) { 149 | if (names.indexOf(key) < 0) { 150 | return newlib[key] = lib[key]; 151 | } 152 | newlib[key] = function () { 153 | var args = Array.prototype.slice.call(arguments); 154 | return function (callback, errback) { 155 | args.push(function (err, val) { 156 | if (err) { 157 | errback(err); 158 | } else { 159 | callback(val); 160 | } 161 | }); 162 | lib[key].apply(lib, args) 163 | } 164 | } 165 | }); 166 | return newlib; 167 | } 168 | 169 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # `Do` it! 2 | 3 | `Do` is a library that adds higher level abstraction and continuables. What I mean by a continuable is explained by the following: 4 | 5 | ### Continuables 6 | 7 | function divide(a, b) { return function (callback, errback) { 8 | // Use nextTick to prove that we're working asynchronously 9 | process.nextTick(function () { 10 | if (b === 0) { 11 | errback(new Error("Cannot divide by 0")); 12 | } else { 13 | callback(a / b); 14 | } 15 | }); 16 | }} 17 | 18 | `Do` expects async functions to not require the callback in the initial invocation, but instead return a continuable which can then be called with the `callback` and `errback`. This is done by manually currying the function. The "continuable" is the partially applied version of the function returned by the outer function. The body of the function won't be executed until you finish the application by attaching a callback. 19 | 20 | divide(100, 10)(function (result) { 21 | puts("the result is " + result); 22 | }, function (error) { 23 | throw error; 24 | }); 25 | 26 | This style is extremely simple (doesn't require an external library like promises to use), and is fairly powerful. 27 | 28 | - The initial function can have variable arguments. 29 | - The continuable itself is portable until it's invoked by attaching callbacks. 30 | 31 | ## Higher-level operations 32 | 33 | The `Do` library makes doing higher-level abstractions easy. All of these helpers are themselves continuables so you can attach callbacks by calling the returned, curried function. 34 | 35 | ### Do.parallel(actions) {...} 36 | 37 | Takes an array of actions and runs them all in parallel. You can either pass in an array of actions, or several actions as function arguments. 38 | 39 | - If you pass in an array, then the output will be an array of all the results 40 | - If you pass in separate arguments, then the output will have several arguments. 41 | 42 | **Example:** 43 | 44 | // Multiple arguments 45 | Do.parallel( 46 | Do.read("/etc/passwd"), 47 | Do.read(__filename) 48 | )(function (passwd, self) { 49 | // Do something 50 | }, errorHandler); 51 | 52 | // Single argument 53 | var actions = [ 54 | Do.read("/etc/passwd"), 55 | Do.read("__filename") 56 | ]; 57 | Do.parallel(actions)(function (results) { 58 | // Do something 59 | }, errorHandler); 60 | 61 | ### Do.chain(actions) {...} 62 | 63 | Chains together several actions feeding the output of the first to the input of the second and the final output to the continuables callback. 64 | 65 | **Example:** 66 | 67 | // Multiple arguments 68 | Do.chain( 69 | Do.read(__filename), 70 | function (text) { 71 | return Do.save("newfile", text); 72 | }, 73 | function () { 74 | return Do.stat("newfile"); 75 | } 76 | )(function (stat) { 77 | // Do something 78 | }, errorHandler); 79 | 80 | // Single argument 81 | var actions = [ 82 | Do.read(__filename), 83 | function (text) { 84 | return Do.save("newfile", text); 85 | }, 86 | function () { 87 | return Do.stat("newfile"); 88 | } 89 | ]; 90 | Do.chain(actions)(function (stat) { 91 | // Do something 92 | }, errorHandler); 93 | 94 | ### Do.map(array, fn) {...} 95 | 96 | Takes an array and does an array map over it using the async callback `fn`. The signature of `fn` is `function fn(item, callback, errback)` or any regular continuable. 97 | 98 | **Example:** 99 | 100 | // Direct callback filter 101 | var files = ['users.json', 'pages.json', 'products.json']; 102 | function loadFile(filename, callback, errback) { 103 | fs.read(filename)(function (data) { 104 | callback([filename, data]); 105 | }, errback); 106 | } 107 | Do.map(files, loadFile)(function (contents) { 108 | // Do something 109 | }, errorHandler); 110 | 111 | // continuable based filter 112 | var files = ['users.json', 'pages.json', 'products.json']; 113 | Do.map(files, fs.read)(function (contents) { 114 | // Do something 115 | }, errorHandler); 116 | 117 | ### Do.filter(array, fn) {...} 118 | 119 | Takes an array and does an array filter over it using the async callback `fn`. The signature of `fn` is `function fn(item, callback, errback)` or any regular continuable. 120 | 121 | **Example:** 122 | 123 | // Direct callback filter 124 | var files = ['users.json', 'pages.json', 'products.json']; 125 | function isFile(filename, callback, errback) { 126 | fs.stat(filename)(function (stat) { 127 | callback(stat.isFile()); 128 | }, errback); 129 | } 130 | Do.filter(files, isFile)(function (filtered_files) { 131 | // Do something 132 | }, errorHandler); 133 | 134 | // Continuable based filter 135 | var files = ['users.json', 'pages.json', 'products.json']; 136 | function isFile(filename) { return function (callback, errback) { 137 | fs.stat(filename)(function (stat) { 138 | callback(stat.isFile()); 139 | }, errback); 140 | }} 141 | Do.filter(files, isFile)(function (filtered_files) { 142 | // Do something 143 | }, errorHandler); 144 | 145 | ### Do.filterMap(array, fn) {...} 146 | 147 | Takes an array and does a combined filter and map over it. If the result 148 | of an item is undefined, then it's filtered out, otherwise it's mapped in. 149 | The signature of `fn` is `function fn(item, callback, errback)` or any regular continuable. 150 | 151 | **Example:** 152 | 153 | // Direct callback filter 154 | var files = ['users.json', 'pages.json', 'products.json']; 155 | function check_and_load(filename, callback, errback) { 156 | fs.stat(filename)(function (stat) { 157 | if (stat.isFile()) { 158 | loadFile(filename, callback, errback); 159 | } else { 160 | callback(); 161 | } 162 | }, errback); 163 | } 164 | Do.filterMap(files, check_and_load)(function (filtered_files_with_data) { 165 | // Do something 166 | }, errorHandler); 167 | 168 | // Continuable based filter 169 | var files = ['users.json', 'pages.json', 'products.json']; 170 | function check_and_load(filename) { return function (callback, errback) { 171 | fs.stat(filename)(function (stat) { 172 | if (stat.isFile()) { 173 | loadFile(filename, callback, errback); 174 | } else { 175 | callback(); 176 | } 177 | }, errback); 178 | }} 179 | Do.filterMap(files, check_and_load)(function (filtered_files_with_data) { 180 | // Do something 181 | }, errorHandler); 182 | 183 | ## Using with node libraries 184 | 185 | Do has a super nifty `Do.convert` function that takes a library and converts it to use Do style continuables. For example, if you wanted to use `fs.readFile` and `fs.writeFile`, then you would do this: 186 | 187 | var fs = Do.convert(require('fs'), ['readFile', 'writeFile']); 188 | 189 | Do will give you a copy of `fs` that has `readFile` and `writeFile` converted to Do style. It's that easy! 190 | 191 | ### For library writers 192 | 193 | All async functions in node follow a common interface: 194 | 195 | method(arg1, arg2, arg3, ..., callback) 196 | 197 | Where `callback` is of the form: 198 | 199 | callback(err, result1, result2, ...) 200 | 201 | This is done to keep node simple and to allow for interoperability between the various async abstractions like Do continuables and CommonJS promises. 202 | 203 | If you're writing a library, make sure to export all your async functions following the node interface. Then anyone using your library can know what format to expect. 204 | 205 | ## Future TODOs 206 | 207 | - Make some sort of helper that makes it easy to call any function regardless of it's sync or async status. This is tricky vs. promises since our return value is just a regular function, not an instance of something. 208 | 209 | ## License 210 | 211 | Do is [licensed][] under the [MIT license][]. 212 | 213 | [MIT license]: http://creativecommons.org/licenses/MIT/ 214 | --------------------------------------------------------------------------------