├── index.js ├── Makefile ├── .gitignore ├── package.json ├── LICENSE ├── vows ├── DeferredList.js └── Deferred.js ├── lib ├── DeferredURLRequest.js └── deferred.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/deferred') 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VOWS=vows/{Deferred,DeferredList}.js 2 | 3 | spec: 4 | vows --spec $(VOWS) 5 | 6 | test: 7 | vows $(VOWS) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # osx noise 2 | .DS_Store 3 | profile 4 | 5 | # xcode noise 6 | build/* 7 | *.mode1 8 | *.mode1v3 9 | *.mode2v3 10 | *.perspective 11 | *.perspectivev3 12 | *.pbxuser 13 | *.xcworkspace 14 | xcuserdata 15 | 16 | # svn & cvs 17 | .svn 18 | CVS 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "deferred" 2 | , "description" : "An implementation of Twisted's Deferred in JavaScript" 3 | , "version" : "1.0" 4 | , "homepage" : "http://heavylifters.com/deferred/js" 5 | , "author" : "HeavyLifters Network Ltd. " 6 | , "repository" : 7 | { "type" : "git" 8 | , "url" : "https://github.com/heavylifters/deferred-js" 9 | } 10 | , "bugs" : 11 | { "mail" : "dev@heavylifters.com" 12 | , "web" : "http://github.com/heavylifters/deferred-js/issues" 13 | } 14 | , "directories" : { "lib" : "./lib" } 15 | , "main" : "./lib/deferred" 16 | , "engines" : { "node" : ">=0.2.0" } 17 | , "licenses" : 18 | [ { "type" : "MIT" 19 | , "url" : "http://github.com/heavylifters/deferred-js/raw/master/LICENSE" 20 | } 21 | ] 22 | , "devDependencies" : { "vows" : "0.5.x" } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 HeavyLifters Network Ltd. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /vows/DeferredList.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , deferred = require('../lib/deferred') 4 | , Deferred = deferred.Deferred 5 | , Failure = deferred.Failure 6 | , DeferredList = deferred.DeferredList 7 | , N = 5 8 | 9 | vows.describe('DeferredList').addBatch({ 10 | 11 | 'A resolved list of Deferreds': { 12 | topic: function() { 13 | var n = N 14 | , ds = [] 15 | while (n--) { 16 | ds.push(deferred.wrapResult(n)) 17 | } 18 | return deferred.all(ds) 19 | }, 20 | 'is resolved when all its children are resolved': function(d) { 21 | d.then(function(results) { 22 | assert.ok(true) 23 | return results 24 | }) 25 | }, 26 | 'has the same number of results as children': function(d) { 27 | d.then(function(results) { 28 | assert.equal(results.length, N) 29 | return results 30 | }) 31 | }, 32 | 'has its results in the correct order': function(d) { 33 | d.then(function(results) { 34 | var n = N 35 | results.forEach(function(r) { 36 | assert.equal(--n, r) 37 | }) 38 | return results 39 | }) 40 | } 41 | }, 42 | 43 | 'An empty DeferredList': { 44 | topic: null, 45 | 'resolves immediately if fireOnFirstResult is false': function() { 46 | var dl = new DeferredList([]) 47 | dl.then(function(results) { 48 | assert.ok(true) 49 | assert.equal(results.length, 0) 50 | }) 51 | }, 52 | 'never resolves if fireOnFirstResult is true': function() { 53 | var dl = new DeferredList([], {fireOnFirstResult: true}) 54 | dl.both(function() { 55 | assert.ok(false) 56 | }) 57 | } 58 | }, 59 | 60 | 'A DeferredList that fires after one result': { 61 | topic: null, 62 | 'is resolved after any of its Deferreds is resolved': function() { 63 | var d = new Deferred() 64 | , ds = [d, new Deferred()] 65 | , dl = new DeferredList(ds, {fireOnFirstResult: true}) 66 | d.resolve(42) 67 | dl.then(function(answer) { 68 | assert.ok(true) 69 | assert.equal(answer, 42) 70 | }) 71 | }, 72 | 'ignores results after the first result': function() { 73 | var d = new Deferred() 74 | , ds = [d, deferred.wrapResult(42)] 75 | , dl = new DeferredList(ds, {fireOnFirstResult: true}) 76 | dl.then(function(answer) { 77 | assert.ok(true) 78 | assert.equal(answer, 42) 79 | }) 80 | assert.doesNotThrow(function() { 81 | d.resolve(-42) 82 | }) 83 | }, 84 | 'does not fire after an error': function() { 85 | var ds = [deferred.wrapFailure('broken'), new Deferred()] 86 | , dl = deferred.all(ds, {fireOnFirstResult: true}) 87 | dl.then(function() { 88 | assert.ok(false) 89 | }) 90 | assert.ok(true) 91 | } 92 | }, 93 | 94 | 'A DeferredList that fires after one error': { 95 | topic: null, 96 | 'is rejected after any of its Deferreds is rejected': function() { 97 | var d = new Deferred() 98 | , ds = [d, new Deferred()] 99 | , dl = new DeferredList(ds, {fireOnFirstError: true}) 100 | d.reject('broken') 101 | dl.fail(function(f) { 102 | assert.ok(true) 103 | assert.equal(f.value, 'broken') 104 | }) 105 | }, 106 | 'ignores results after the first error': function() { 107 | var d = new Deferred() 108 | , ds = [d, deferred.wrapFailure('broken')] 109 | , dl = new DeferredList(ds, {fireOnFirstError: true}) 110 | dl.fail(function(f) { 111 | assert.ok(true) 112 | assert.equal(f.value, 'broken') 113 | }) 114 | assert.doesNotThrow(function() { 115 | d.reject('broken again') 116 | }) 117 | }, 118 | 'does not fire after a result': function() { 119 | var ds = [deferred.wrapResult(42), new Deferred()] 120 | , dl = deferred.all(ds, {fireOnFirstError: true}) 121 | dl.then(function() { 122 | assert.ok(false) 123 | }) 124 | assert.ok(true) 125 | } 126 | }, 127 | 128 | 'A DeferredList that consumes errors': { 129 | topic: null, 130 | 'always leaves its Deferreds executing the callback chain': function() { 131 | var d = deferred.wrapFailure('broken') 132 | , ds = [d, deferred.wrapResult(42)] 133 | , dl = new DeferredList(ds, {consumeErrors: true}) 134 | dl.then(function() { 135 | assert.ok(true) 136 | }) 137 | d.then(function(result) { 138 | assert.ok(true) 139 | }) 140 | }, 141 | 'returns null instead of any Failures encountered': function() { 142 | var ds = [deferred.wrapFailure('broken'), deferred.wrapResult(42)] 143 | , dl = new DeferredList(ds, {consumeErrors: true}) 144 | dl.then(function(results) { 145 | assert.equal(results[0], null) 146 | assert.equal(results[1], 42) 147 | }) 148 | } 149 | }, 150 | 151 | 'A DeferredList with cancelDeferredsWhenCancelled set': { 152 | topic: null, 153 | 'cancels its Deferreds when cancelled': function() { 154 | var canceller = function() { 155 | assert.ok(true) 156 | } 157 | , d = new Deferred(canceller) 158 | , dl = new DeferredList([d], {cancelDeferredsWhenCancelled: true}) 159 | d.fail(function(f) { 160 | assert.ok(true) 161 | assert.equal(f.value, 'cancelled') 162 | }) 163 | dl.cancel() 164 | } 165 | } 166 | 167 | }).export(module) 168 | -------------------------------------------------------------------------------- /lib/DeferredURLRequest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DeferredURLRequest.js 3 | * 4 | * Copyright 2011, HeavyLifters Network Ltd. All rights reserved. 5 | */ 6 | 7 | var deferred = require('./deferred') 8 | , Deferred = deferred.Deferred 9 | , util = require('util') 10 | , fs = require('fs') 11 | , http = require('http') 12 | 13 | exports.DeferredURLRequest = DeferredURLRequest 14 | exports.DeferredJSONRequest = DeferredJSONRequest 15 | exports.DeferredDownload = DeferredDownload 16 | 17 | function DeferredURLRequest(context) { 18 | this.context = context 19 | this.client = this._createClient() 20 | this.request = null 21 | this.response = null 22 | this.responseData = null 23 | this.deferred = new Deferred() 24 | } 25 | 26 | DeferredURLRequest.prototype._createClient = function() { 27 | var port = this.context.port || 80 28 | , host = this.context.host 29 | return http.createClient(port, host) 30 | } 31 | 32 | DeferredURLRequest.prototype._createRequest = function() { 33 | var method = this.context.method || 'GET' 34 | , host = this.context.host 35 | , endpoint = this.context.endpoint || '/' 36 | return this.client.request(method, endpoint, {'host': host}) 37 | } 38 | 39 | DeferredURLRequest.prototype._logRequestFailure = function(err) { 40 | var port = this.context.port || 80 41 | , host = this.context.host 42 | , method = this.context.method || 'GET' 43 | , endpoint = this.context.endpoint || '/' 44 | // FIXME: use log.error? 45 | console.log('DeferredURLRequest request failed, request: ' + method + ' http://' + host + ':' + port + endpoint) 46 | console.log('DeferredURLRequest request failed, error: ' + err) 47 | } 48 | 49 | DeferredURLRequest.prototype._logResponseFailure = function(err) { 50 | var port = this.context.port || 80 51 | , host = this.context.host 52 | , method = this.context.method || 'GET' 53 | , endpoint = this.context.endpoint || '/' 54 | // FIXME: use log.error? 55 | console.log('DeferredURLRequest response failed, request: ' + method + ' http://' + host + ':' + port + endpoint) 56 | console.log('DeferredURLRequest response failed, error: ' + err) 57 | } 58 | 59 | DeferredURLRequest.prototype._responseBegan = function() { 60 | // console.log('_responseBegan') 61 | if (this.response.statusCode === 200) { 62 | this.responseData = '' 63 | // console.log('_responseBegan true') 64 | return true 65 | } else { 66 | // console.log('_responseBegan statusCode: ' + this.response.statusCode) 67 | // console.log('_responseBegan false') 68 | this.error = {code: this.response.statusCode} 69 | this._responseFailed() 70 | return false 71 | } 72 | } 73 | 74 | DeferredURLRequest.prototype._responseReceivedData = function(data) { 75 | // console.log('_responseReceivedData') 76 | if (this.responseData !== null) this.responseData += data 77 | } 78 | 79 | DeferredURLRequest.prototype._responseFinished = function() { 80 | // console.log('_responseFinished') 81 | this.deferred.resolve(this.responseData) 82 | } 83 | 84 | DeferredURLRequest.prototype._responseFailed = function() { 85 | // console.log('_responseFailed') 86 | this._logResponseFailure(this.error) 87 | this.deferred.reject(this.error) 88 | } 89 | 90 | DeferredURLRequest.prototype.requestStart = function() { 91 | var self = this 92 | 93 | this.request = this._createRequest() 94 | this.request.on('response', function(res) { 95 | self.response = res 96 | if (self._responseBegan()) { 97 | res.on('data', function(chunk) { self._responseReceivedData(chunk) }) 98 | res.on('end', function() { self._responseFinished() }) 99 | res.on('error', function(err) { 100 | self._logResponseFailure(err) 101 | if (self.error === undefined) { 102 | self.error = err 103 | self._responseFailed() 104 | } 105 | }) 106 | } 107 | }) 108 | this.request.on('error', function(err) { 109 | self._logRequestFailure(err) 110 | if (self.error === undefined) { 111 | self.error = err 112 | self.deferred.reject(err) 113 | } 114 | }) 115 | this.request.end() 116 | return this.deferred 117 | } 118 | 119 | function DeferredJSONRequest(context) { 120 | DeferredURLRequest.call(this, context) 121 | } 122 | 123 | util.inherits(DeferredJSONRequest, DeferredURLRequest) 124 | 125 | DeferredJSONRequest.prototype._responseFinished = function() { 126 | // console.log('_responseFinished') 127 | try { 128 | this.deferred.resolve(JSON.parse(this.responseData)) 129 | } catch (e) { 130 | this.deferred.reject({message: 'failed to parse JSON response', data: this.responseData}) 131 | } 132 | } 133 | 134 | function DeferredDownload(context, destpath) { 135 | DeferredURLRequest.call(this, context) 136 | this.destpath = destpath 137 | } 138 | 139 | util.inherits(DeferredDownload, DeferredURLRequest) 140 | 141 | DeferredDownload.prototype._requestStart = function() { 142 | try { 143 | fs.statSync(this.destpath) 144 | this.deferred.resolve(this.destpath) 145 | } catch (e) { 146 | DeferredURLRequest.prototype._requestStart.call(this) 147 | } 148 | } 149 | 150 | DeferredDownload.prototype._responseBegan = function() { 151 | if (DeferredURLRequest.prototype._responseBegan.call(this)) { 152 | this.file = fs.createWriteStream(this.destpath) 153 | return true 154 | } 155 | return false 156 | } 157 | 158 | DeferredDownload.prototype._responseReceivedData = function(data) { 159 | if (this.file) { 160 | this.file.write(data, 'utf8') 161 | return true 162 | } 163 | return false 164 | } 165 | 166 | DeferredDownload.prototype._responseFinished = function() { 167 | var self = this 168 | if (this.file) { 169 | this.file.on('close', function(had_error) { 170 | if (had_error) { 171 | self.error = 'failed to close file: ' + self.destpath 172 | self._responseFailed() 173 | } else { 174 | self.deferred.resolve(self.destpath) 175 | } 176 | }) 177 | this.file.end() 178 | } else { 179 | this.error = "download failed" 180 | this._responseFailed() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/deferred.js: -------------------------------------------------------------------------------- 1 | /* 2 | * deferred.js 3 | * 4 | * Copyright 2011, HeavyLifters Network Ltd. All rights reserved. 5 | */ 6 | 7 | ;(function() { 8 | 9 | var DeferredAPI = { 10 | deferred: deferred, 11 | all: all, 12 | Deferred: Deferred, 13 | DeferredList: DeferredList, 14 | wrapResult: wrapResult, 15 | wrapFailure: wrapFailure, 16 | Failure: Failure 17 | } 18 | 19 | // CommonJS module support 20 | if (typeof module !== 'undefined') { 21 | var DeferredURLRequest = require('./DeferredURLRequest') 22 | for (var k in DeferredURLRequest) { 23 | DeferredAPI[k] = DeferredURLRequest[k] 24 | } 25 | module.exports = DeferredAPI 26 | } 27 | 28 | // Browser API 29 | else if (typeof window !== 'undefined') { 30 | window.deferred = DeferredAPI 31 | } 32 | 33 | // Fake out console if necessary 34 | if (typeof console === 'undefined') { 35 | var global = function() { return this || (1,eval)('this') } 36 | ;(function() { 37 | var noop = function(){} 38 | global().console = { log: noop, warn: noop, error: noop, dir: noop } 39 | }()) 40 | } 41 | 42 | function wrapResult(result) { 43 | return new Deferred().resolve(result) 44 | } 45 | 46 | function wrapFailure(error) { 47 | return new Deferred().reject(error) 48 | } 49 | 50 | function Failure(v) { this.value = v } 51 | 52 | // Crockford style constructor 53 | function deferred(t) { return new Deferred(t) } 54 | 55 | function Deferred(canceller) { 56 | this.called = false 57 | this.running = false 58 | this.result = null 59 | this.pauseCount = 0 60 | this.callbacks = [] 61 | this.verbose = false 62 | this._canceller = canceller 63 | 64 | // If this Deferred is cancelled and the creator of this Deferred 65 | // didn't cancel it, then they may not know about the cancellation and 66 | // try to resolve or reject it as well. This flag causes the 67 | // "already called" error that resolve() or reject() normally throws 68 | // to be suppressed once. 69 | this._suppressAlreadyCalled = false 70 | } 71 | 72 | if (typeof Object.defineProperty === 'function') { 73 | var _consumeThrownExceptions = true 74 | Object.defineProperty(Deferred, 'consumeThrownExceptions', { 75 | enumerable: false, 76 | set: function(v) { _consumeThrownExceptions = v }, 77 | get: function() { return _consumeThrownExceptions } 78 | }) 79 | } else { 80 | Deferred.consumeThrownExceptions = true 81 | } 82 | 83 | Deferred.prototype.cancel = function() { 84 | if (!this.called) { 85 | if (typeof this._canceller === 'function') { 86 | this._canceller(this) 87 | } else { 88 | this._suppressAlreadyCalled = true 89 | } 90 | if (!this.called) { 91 | this.reject('cancelled') 92 | } 93 | } else if (this.result instanceof Deferred) { 94 | this.result.cancel() 95 | } 96 | } 97 | 98 | Deferred.prototype.then = function(callback, errback) { 99 | this.callbacks.push({callback: callback, errback: errback}) 100 | if (this.called) _run(this) 101 | return this 102 | } 103 | 104 | Deferred.prototype.fail = function(errback) { 105 | this.callbacks.push({callback: null, errback: errback}) 106 | if (this.called) _run(this) 107 | return this 108 | } 109 | 110 | Deferred.prototype.both = function(callback) { 111 | return this.then(callback, callback) 112 | } 113 | 114 | Deferred.prototype.resolve = function(result) { 115 | _startRun(this, result) 116 | return this 117 | } 118 | 119 | Deferred.prototype.reject = function(err) { 120 | if (!(err instanceof Failure)) { 121 | err = new Failure(err) 122 | } 123 | _startRun(this, err) 124 | return this 125 | } 126 | 127 | Deferred.prototype.pause = function() { 128 | this.pauseCount += 1 129 | if (this.extra) { 130 | console.log('Deferred.pause ' + this.pauseCount + ': ' + this.extra) 131 | } 132 | return this 133 | } 134 | 135 | Deferred.prototype.unpause = function() { 136 | this.pauseCount -= 1 137 | if (this.extra) { 138 | console.log('Deferred.unpause ' + this.pauseCount + ': ' + this.extra) 139 | } 140 | if (this.pauseCount <= 0 && this.called) { 141 | _run(this) 142 | } 143 | return this 144 | } 145 | 146 | // For debugging 147 | Deferred.prototype.inspect = function(extra, cb) { 148 | this.extra = extra 149 | var self = this 150 | return this.then(function(r) { 151 | console.log('Deferred.inspect resolved: ' + self.extra) 152 | console.dir(r) 153 | return r 154 | }, function(e) { 155 | console.log('Deferred.inspect rejected: ' + self.extra) 156 | console.dir(e) 157 | return e 158 | }) 159 | } 160 | 161 | /// A couple of sugary methods 162 | 163 | Deferred.prototype.thenReturn = function(result) { 164 | return this.then(function(_) { return result }) 165 | } 166 | 167 | Deferred.prototype.thenCall = function(f) { 168 | return this.then(function(result) { 169 | f(result) 170 | return result 171 | }) 172 | } 173 | 174 | Deferred.prototype.failReturn = function(result) { 175 | return this.fail(function(_) { return result }) 176 | } 177 | 178 | Deferred.prototype.failCall = function(f) { 179 | return this.fail(function(result) { 180 | f(result) 181 | return result 182 | }) 183 | } 184 | 185 | function _continue(d, newResult) { 186 | d.result = newResult 187 | d.unpause() 188 | return d.result 189 | } 190 | 191 | function _nest(outer) { 192 | outer.result.both(function(newResult) { 193 | return _continue(outer, newResult) 194 | }) 195 | } 196 | 197 | function _startRun(d, result) { 198 | if (d.called) { 199 | if (d._suppressAlreadyCalled) { 200 | d._suppressAlreadyCalled = false 201 | return 202 | } 203 | throw new Error("Already resolved Deferred: " + d) 204 | } 205 | d.called = true 206 | d.result = result 207 | if (d.result instanceof Deferred) { 208 | d.pause() 209 | _nest(d) 210 | return 211 | } 212 | _run(d) 213 | } 214 | 215 | function _run(d) { 216 | if (d.running) return 217 | var link, status, fn 218 | if (d.pauseCount > 0) return 219 | while (d.callbacks.length > 0) { 220 | link = d.callbacks.shift() 221 | status = (d.result instanceof Failure) ? 'errback' : 'callback' 222 | fn = link[status] 223 | if (typeof fn !== 'function') continue 224 | try { 225 | d.running = true 226 | d.result = fn(d.result) 227 | d.running = false 228 | if (d.result instanceof Deferred) { 229 | d.pause() 230 | _nest(d) 231 | return 232 | } 233 | } catch (e) { 234 | if (Deferred.consumeThrownExceptions) { 235 | d.running = false 236 | var f = new Failure(e) 237 | f.source = f.source || status 238 | d.result = f 239 | if (d.verbose) { 240 | console.warn('uncaught error in deferred ' + status + ': ' + e.message) 241 | console.warn('Stack: ' + e.stack) 242 | } 243 | } else { 244 | throw e 245 | } 246 | } 247 | } 248 | } 249 | 250 | 251 | /// DeferredList / all 252 | 253 | function all(ds, opts) { return new DeferredList(ds, opts) } 254 | 255 | function DeferredList(ds, opts) { 256 | opts = opts || {} 257 | Deferred.call(this) 258 | this._deferreds = ds 259 | this._finished = 0 260 | this._length = ds.length 261 | this._results = [] 262 | this._fireOnFirstResult = opts.fireOnFirstResult 263 | this._fireOnFirstError = opts.fireOnFirstError 264 | this._consumeErrors = opts.consumeErrors 265 | this._cancelDeferredsWhenCancelled = opts.cancelDeferredsWhenCancelled 266 | 267 | if (this._length === 0 && !this._fireOnFirstResult) { 268 | this.resolve(this._results) 269 | } 270 | 271 | for (var i = 0, n = this._length; i < n; ++i) { 272 | ds[i].both(deferredListCallback(this, i)) 273 | } 274 | } 275 | 276 | if (typeof Object.create === 'function') { 277 | DeferredList.prototype = Object.create(Deferred.prototype, { 278 | constructor: { value: DeferredList, enumerable: false } 279 | }) 280 | } else { 281 | DeferredList.prototype = new Deferred() 282 | DeferredList.prototype.constructor = DeferredList 283 | } 284 | 285 | DeferredList.prototype.cancelDeferredsWhenCancelled = function() { 286 | this._cancelDeferredsWhenCancelled = true 287 | } 288 | 289 | var _deferredCancel = Deferred.prototype.cancel 290 | DeferredList.prototype.cancel = function() { 291 | _deferredCancel.call(this) 292 | if (this._cancelDeferredsWhenCancelled) { 293 | for (var i = 0; i < this._length; ++i) { 294 | this._deferreds[i].cancel() 295 | } 296 | } 297 | } 298 | 299 | function deferredListCallback(d, i) { 300 | return function(result) { 301 | var isErr = result instanceof Failure 302 | , myResult = (isErr && d._consumeErrors) ? null : result 303 | // Support nesting 304 | if (result instanceof Deferred) { 305 | result.both(deferredListCallback(d, i)) 306 | return 307 | } 308 | d._results[i] = myResult 309 | d._finished += 1 310 | if (!d.called) { 311 | if (d._fireOnFirstResult && !isErr) { 312 | d.resolve(result) 313 | } else if (d._fireOnFirstError && isErr) { 314 | d.reject(result) 315 | } else if (d._finished === d._length) { 316 | d.resolve(d._results) 317 | } 318 | } 319 | return myResult 320 | } 321 | } 322 | 323 | }()) -------------------------------------------------------------------------------- /vows/Deferred.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows') 2 | , assert = require('assert') 3 | , deferred = require('../lib/deferred') 4 | , Deferred = deferred.Deferred 5 | , Failure = deferred.Failure 6 | 7 | Deferred.consumeThrownExceptions = false 8 | 9 | vows.describe('Deferred').addBatch({ 10 | 11 | 'A new Deferred': { 12 | topic: new Deferred(), 13 | 'has not been called': function(d) { 14 | assert.ok(!d.called) 15 | }, 16 | 'is not running': function(d) { 17 | assert.ok(!d.running) 18 | }, 19 | 'has no callbacks': function(d) { 20 | assert.equal(d.callbacks.length, 0) 21 | }, 22 | 'has no result': function(d) { 23 | assert.equal(d.result, null) 24 | }, 25 | 'is not paused': function(d) { 26 | assert.equal(d.pauseCount, 0) 27 | }, 28 | 'can have callbacks added to it': function(d) { 29 | d.then(console.log) 30 | assert.equal(d.callbacks.length, 1) 31 | assert.equal(d.callbacks[0].callback, console.log) 32 | assert.ok(!d.callbacks[0].errback) 33 | 34 | // cheat, clear the callback chain 35 | d.callbacks = [] 36 | }, 37 | 'can have errbacks added to it': function(d) { 38 | d.fail(console.log) 39 | assert.equal(d.callbacks.length, 1) 40 | assert.equal(d.callbacks[0].errback, console.log) 41 | assert.ok(!d.callbacks[0].callback) 42 | 43 | // cheat, clear the callback chain 44 | d.callbacks = [] 45 | }, 46 | 'can have a callback added for both success and failure': function(d) { 47 | d.both(console.log) 48 | assert.equal(d.callbacks.length, 1) 49 | assert.equal(d.callbacks[0].callback, console.log) 50 | assert.equal(d.callbacks[0].errback, console.log) 51 | 52 | // cheat, clear the callback chain 53 | d.callbacks = [] 54 | } 55 | }, 56 | 57 | 'A resolved Deferred': { 58 | topic: deferred.wrapResult(42), 59 | 'has been called': function(d) { 60 | assert.ok(d.called) 61 | }, 62 | 'has the correct result': function(d) { 63 | assert.equal(d.result, 42) 64 | }, 65 | 'fires new callbacks immediately (synchronously)': function(d) { 66 | d.thenCall(assert.ok) 67 | }, 68 | 'cannot be resolved again': function(d) { 69 | assert.throws(function() { 70 | d.resolve() 71 | }) 72 | }, 73 | 'cannot be rejected': function(d) { 74 | assert.throws(function() { 75 | d.reject() 76 | }) 77 | }, 78 | 'sets its result to the value returned by its callbacks': function(d) { 79 | d.then(function(x) { 80 | return 'something else' 81 | }).then(function(x) { 82 | assert.equal(x, 'something else') 83 | return x 84 | }) 85 | } 86 | }, 87 | 88 | 'Another resolved Deferred': { 89 | topic: deferred.wrapResult(42), 90 | 'switches to the fail branch when a callback returns a Failure': function(d) { 91 | d.then(function(x) { 92 | return new Failure('broken') 93 | }).fail(function(e) { 94 | assert.equal(e.value, 'broken') 95 | return e 96 | }) 97 | } 98 | }, 99 | 100 | 'Yet another resolved Deferred': { 101 | topic: deferred.wrapResult(42), 102 | 'switches to the fail branch when a callback throws an Error': function(d) { 103 | Deferred.consumeThrownExceptions = true 104 | d.then(function() { 105 | throw new Error('broken') 106 | }).fail(function(e) { 107 | assert.ok(e) 108 | assert.instanceOf(e.value, Failure) 109 | assert.equal(e.value.message, 'broken') 110 | return e 111 | }) 112 | Deferred.consumeThrownExceptions = false 113 | } 114 | }, 115 | 116 | 'Yet one more resolved Deferred': { 117 | topic: deferred.wrapFailure('broken'), 118 | 'switches to the success branch when an errback returns a non-Failure': function(d) { 119 | d.fail(function() { 120 | return 42 121 | }).then(function(x) { 122 | assert.equal(x, 42) 123 | return x 124 | }) 125 | } 126 | }, 127 | 128 | 'A rejected Deferred': { 129 | topic: deferred.wrapFailure('broken'), 130 | 'has been called': function(d) { 131 | assert.ok(d.called) 132 | }, 133 | 'has the correct result': function(d) { 134 | assert.equal(d.result.value, 'broken') 135 | }, 136 | 'fires new callbacks immediately (synchronously)': function(d) { 137 | d.fail(function(e) { 138 | assert.ok(e) 139 | return e 140 | }) 141 | }, 142 | 'cannot be rejected again': function(d) { 143 | assert.throws(function() { 144 | d.reject() 145 | }) 146 | }, 147 | 'cannot be resolved': function(d) { 148 | assert.throws(function() { 149 | d.resolve() 150 | }) 151 | } 152 | }, 153 | 154 | 'A Deferred resolved with a nested Deferred': { 155 | topic: deferred.wrapResult(deferred.wrapResult(42)), 156 | 'waits for and assumes the result of the nested Deferred': function(d) { 157 | d.then(function(result) { 158 | assert.equal(result, 42) 159 | return result 160 | }) 161 | } 162 | }, 163 | 164 | 'A Deferred with a nested Deferred returned in a callback': { 165 | topic: new Deferred(), 166 | 'waits for and assumes the result of the nested Deferred': function(d) { 167 | var innerD = new Deferred() 168 | d.then(function() { 169 | return innerD 170 | }) 171 | d.resolve() 172 | assert.ok(d.pauseCount > 0) 173 | innerD.resolve(42) 174 | d.then(function(result) { 175 | assert.equal(result, 42) 176 | return result 177 | }) 178 | } 179 | }, 180 | 181 | 'When nesting, the outter Deferred': { 182 | topic: deferred.wrapResult().then(function() { 183 | return new Deferred() 184 | }), 185 | 'is paused while waiting for the inner Deferred': function(d) { 186 | assert.equal(d.pauseCount, 1) 187 | assert.ok(d.called) 188 | assert.ok(!d.result.called) 189 | }, 190 | 'is unpaused when the inner Deferred is resolved': function(d) { 191 | d.result.resolve(42) 192 | assert.equal(d.pauseCount, 0) 193 | assert.equal(d.result, 42) 194 | } 195 | }, 196 | 197 | 'The result of a Deferred': { 198 | topic: deferred.wrapResult(42), 199 | 'can be set without a callback using thenReturn': function(d) { 200 | d.thenReturn('new result').then(function(result) { 201 | assert.equal(result, 'new result') 202 | return result 203 | }) 204 | }, 205 | 'can be preserved regardless of a callback\'s return value using thenCall': function(d) { 206 | function noop(x) { /* discard result, could have side effects */ } 207 | d.thenReturn(42).thenCall(noop).then(function(result) { 208 | assert.equal(result, 42) 209 | }) 210 | } 211 | }, 212 | 213 | 'The result of a failed Deferred': { 214 | topic: null, 215 | 'can be set without a callback using failReturn': function() { 216 | var d = deferred.wrapFailure(42) 217 | d.failReturn(new Failure('new result')).fail(function(failure) { 218 | assert.equal(failure.value, 'new result') 219 | return failure 220 | }) 221 | }, 222 | 'can be preserved regardless of a callback\'s return value using failCall': function() { 223 | var d = deferred.wrapFailure(42) 224 | function noop(x) { /* discard result, could have side effects */ } 225 | d.failCall(noop).fail(function(failure) { 226 | assert.equal(failure.value, 42) 227 | }) 228 | } 229 | }, 230 | 231 | // TODO: cancel (including _suppressAlreadyCalled) 232 | // nested deferred gets cancel() called 233 | 'A Deferred with a canceller': { 234 | topic: null, 235 | 'calls the canceller when cancelled': function() { 236 | var canceller = function() { assert.ok(true) } 237 | , d = new Deferred(canceller) 238 | d.cancel() 239 | }, 240 | 'fires the errback chain if cancelled before being called': function() { 241 | var d = new Deferred() 242 | d.fail(function(e) { 243 | assert.ok(true) 244 | assert.equal(e.value, 'cancelled') 245 | }) 246 | d.cancel() 247 | } 248 | }, 249 | 250 | 'A Deferred without a canceller': { 251 | topic: null, 252 | 'supresses "already called" error once if resolved after cancellation': function() { 253 | var d = new Deferred() 254 | d.cancel() 255 | assert.doesNotThrow(function() { 256 | d.resolve() 257 | }) 258 | assert.throws(function() { 259 | d.resolve() 260 | }) 261 | }, 262 | 'supresses "already called" error once if rejected after cancellation': function() { 263 | var d = new Deferred() 264 | d.cancel() 265 | assert.doesNotThrow(function() { 266 | d.reject() 267 | }) 268 | assert.throws(function() { 269 | d.reject() 270 | }) 271 | } 272 | }, 273 | 274 | 'A called Deferred': { 275 | topic: null, 276 | 'cancels a nested Deferred when cancelled': function() { 277 | var canceller = function(d) { 278 | assert.ok(true) 279 | assert.equal(d, nestedD) 280 | } 281 | , nestedD = new Deferred(canceller) 282 | , outerD = deferred.wrapResult(nestedD) 283 | outerD.cancel() 284 | } 285 | } 286 | 287 | }).export(module) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HeavyLifters Deferred Library 2 | ============================= 3 | 4 | Asynchronous programming is the norm in JavaScript, where callbacks are commonly used to manage async processes. Unfortunately, no consistently-followed convention for organizing flow control with callbacks has emerged. With the rise of [Node.js](http://nodejs.org/) and client-side web app frameworks, programmers with little experience with asynchronous programming are struggling to understand and organize their code. 5 | 6 | Why should I use this? 7 | ---------------------- 8 | 9 | We've [seen](http://metaduck.com/post/2675027550/asynchronous-iteration-patterns-in-node-js) [many](http://stackoverflow.com/questions/5308514/prescriptive-nodejs) [developers](http://zef.me/3715/three-routes-to-spaghetti-free-javascript) complain about the difficulty of organizing asynchronous code: for example: calling a function when N async processes finish their work. 10 | 11 | This is not a new problem, and solutions have existed for years. Our favourite is Twisted's `Deferred` class; our library brings it to JavaScript. HeavyLifters provides and uses implementations in Objective-C, Objective-J, JavaScript, and Java. 12 | 13 | The biggest benefit you get by using [`Deferred`][D] objects in JavaScript is a consistent pattern for organizing callbacks and easily obtaining future values. 14 | 15 | [`Deferred`][D] objects also make it easy to have async processes dependent on each other, including waiting for a list of async processes to finish, with optional notifications for the first completion or the first error. This simplifies flow control and makes your code much easier to reason about. 16 | 17 | Requirements 18 | ------------ 19 | 20 | This library is designed to work in modern browsers and with Node.js, and should run in just about any ES3+ environment but no promises. 21 | 22 | If it doesn't work in your preferred JavaScript environment, [submit a bug report](https://github.com/heavylifters/deferred-js/issues)! 23 | 24 | Running Unit Tests 25 | ------------------ 26 | 27 | Install [Node.js](http://nodejs.org/) and [npm](http://github.com/isaacs/npm#readme), then: 28 | 29 | npm install vows 30 | cd deferred-js 31 | make test 32 | # for more detail, make spec 33 | 34 | How it works 35 | ------------ 36 | 37 | You can create a [`Deferred`][D] object directly, but you typically request one from a **data source**. A data source is a function that returns a [`Deferred`][D] object. 38 | 39 | The [`Deferred`][D] object provides access to the data source's data by allowing you to attach **callbacks** and/or **errbacks** to its **callback chain**. 40 | 41 | When the data source has the result, it calls the `resolve(result)` method on the [`Deferred`][D] object, or `reject(failure)` (in the case of failure). This causes the [`Deferred`][D] object's callback chain to be **fired** - meaning each link in the chain (a callback or errback) is called in turn. The result is the input to the first callback, and its output is the input to the next callback (and so on). 42 | 43 | If a callback (or errback) returns a [`Failure`][F] object, the next errback is called; otherwise the next callback is called. 44 | 45 | ![Deferred-process](https://github.com/heavylifters/HLDeferred-objc/raw/master/Documentation/images/twisted/deferred-process.png) 46 | 47 | The Most Basic Example 48 | ---------------------- 49 | 50 | function demonstration() { 51 | var d = new Deferred() 52 | console.log('created Deferred object') 53 | 54 | d.then(function(result) { 55 | console.log('Hello ' + result) 56 | return result 57 | }) 58 | console.log('added a callback to the Deferred's callback chain') 59 | 60 | // resolve the Deferred, which fires the callback chain 61 | d.resolve('World') 62 | console.log('You should see Hello, World above this line in the console') 63 | 64 | // Note: use d.reject('DISASTER!') to indicate failure 65 | } 66 | 67 | Adding callbacks and errbacks 68 | ----------------------------- 69 | 70 | Each **link** in a callback chain is a pair of functions, representing a **callback** and an **errback**. Firing the chain executes the callback **OR** errback of **each link, in sequence**. For each link, its callback is executed if its input is a result; the errback is executed if its input is a failure (failures are represented by [`Failure`][F] objects). 71 | 72 | ### Adding (just) a callback ### 73 | 74 | To append a link with a callback to an [`Deferred`][D] object, call the `then(cb)` method, passing in a callback function. Example: 75 | 76 | d.then(function(result) { 77 | // do something useful with the result 78 | return result 79 | }) 80 | 81 | [`Deferred`][D] adds a link to its chain with your callback and a "passthrough" errback. The passthrough errback simply returns its exception parameter. 82 | 83 | ### Adding (just) an errback ### 84 | 85 | To append a link with an errback to an [`Deferred`][D] object, call the `fail(eb)` method, passing in an errback function. Example: 86 | 87 | d.fail(function(failure) { 88 | // optionally do something useful with failure.value() 89 | return failure 90 | }); 91 | 92 | [`Deferred`][D] adds a link to its chain with your errback and a "passthrough" callback. The passthrough callback simply returns its result parameter. 93 | 94 | ### Adding a callback and an errback ### 95 | 96 | To add a link with a callback *and* errback to an [`Deferred`][D] object, call the `-then(cb, eb)` method or the `both(cb)` method. 97 | 98 | Use `then(cb, eb)` when you want different behaviour in the case of success or failure: 99 | 100 | d.then(function(result) { 101 | // do something useful with the result 102 | return result 103 | }, function(failure) { 104 | // optionally do something useful with failure.value() 105 | return failure 106 | }) 107 | 108 | Use `both(cb)` when you intend to do the same thing in either case: 109 | 110 | d.both(function(result) { 111 | // in the case of failure, result is a Failure 112 | // do something in either case 113 | return result 114 | }) 115 | 116 | Deferred in practice 117 | ---------------------- 118 | 119 | By convention, names of methods returning an [`Deferred`][D] object are prefixed with "request", such as: 120 | 121 | // result is a MyThing object 122 | function requestDistantInformation() 123 | 124 | We recommend you should document that information somewhere. This convention helps indicate that your function is asynchronous and returns a Deferred object. 125 | 126 | A function that fetches something asynchronously can return a [`Deferred`][D] instead of accepting callback parameters. We call these functions **data sources**. When a data source has the requested value it `resolve`s the [`Deferred`][D] object, which fires the callback chain. If it encounters an error it will `reject` the deferred, which fires the errback chain. 127 | 128 | Here's an example of reading a file using Node.js: 129 | 130 | // Reading a file in Node 131 | fs.readFile('/etc/passwd', function(err, data) { 132 | if (err) throw err 133 | console.log(data) 134 | }) 135 | 136 | Here's how you might wrap Node.js's readFile API to use the [`Deferred`][D] pattern: 137 | 138 | dfs.requestReadFile = function(name) { 139 | var d = new Deferred() 140 | fs.readFile(name, function(err, data) { 141 | if (err) d.reject(err) 142 | else d.resolve(data) 143 | }) 144 | return d 145 | } 146 | 147 | Now you can read a file and give the [`Deferred`][D] object out to any party interested in the contents: 148 | 149 | // Reading a file with Deferred (assuming we have a Deferred filesystem module) 150 | var contentsDeferred = dfs.requestReadFile('/etc/passwd') 151 | 152 | // anything interested in the contents of that file 153 | // can attach callbacks to contentsDeferred 154 | 155 | contentsDeferred.then(function(data) { console.log(data) }) 156 | 157 | This is not a flattering example. In the simplest of cases `Deferred` is more verbose, but it more complex scenarios it's easier to reason about what's going on. 158 | 159 | Waiting on many asynchronous data sources 160 | ----------------------------------------- 161 | 162 | You can wait for all the values in a list of [`Deferred`][D] objects, or start a bunch of `Deferred` operations and run the callback chain when the first one has succeeded or failed. 163 | 164 | // Note: dfs.requestReadFile is defined above 165 | 166 | var dPasswd = dfs.requestReadFile('/etc/passwd') 167 | , dShadow = dfs.requestReadFile('/etc/shadow') 168 | , dGroup = dfs.requestReadFile('/etc/group') 169 | 170 | // convert ['a', 'b', 'c'] to 'abc' 171 | function join(things) { 172 | return things.reduce(function(m, t) { return m + t }) 173 | } 174 | 175 | // after all values are received, deferred.all runs the callback chain 176 | deferred.all([dPasswd, dShadow, dGroup]).then(join).thenCall(console.log) 177 | 178 | // or, after the first value is received, deferred.all runs the callback chain 179 | deferred.all([dPasswd, dShadow, dGroup], {fireOnFirstResult: true}).thenCall(console.log) 180 | 181 | // or, if any error occurs, deferred.all runs the errback chain 182 | var dAll = deferred.all([dPasswd, dShadow, dGroup], {fireOnFirstError: true}) 183 | 184 | // callbacks and errbacks are supposed to return a value 185 | // the value is the input to the next link in the chain 186 | // thenCall and failCall are conveniences that call the 187 | // supplied function with the input and then return the input 188 | // this way you don't have to wrap functions that don't return 189 | // a value 190 | dAll.failCall(console.error) 191 | dAll.then(join).thenCall(console.log) 192 | 193 | Composing Deferred objects arbitrarily 194 | -------------------------------------- 195 | 196 | The return value of a callback is passed to the next callback. When a callback returns a [`Deferred`][D] object, the original `Deferred` will transparently wait for the other to receive its value and then run its own callback chain using that value. We call this nesting. 197 | 198 | var d = dfs.readFile('/etc/passwd').then(function(passwdData) { 199 | return dfs.readFile('/etc/group').then(function(groupData) { 200 | return passwdData + groupData 201 | }) 202 | }).then(function(data) { 203 | console.log(data) 204 | return data 205 | }) 206 | 207 | Now you can do something with `d`, return it, pass it to another function, etc. Subsequent callbacks registered on `d` will receive the value returned from the innermost callback: `passwdData + groupData`. 208 | 209 | `Deferred` shines when you have higher level constructs built on top of it, such as work queues and data sources for databases and filesystem access. We have more stuff coming out for Node.js soon! 210 | 211 | Links 212 | ----- 213 | - [Docs](https://github.com/heavylifters/deferred-js/wiki) (forthcoming) 214 | - [Issue Tracker](https://github.com/heavylifters/deferred-js/issues) (please report bugs and feature requests!) 215 | 216 | How to contribute 217 | ----------------- 218 | - Fork [deferred-js on GitHub](https://github.com/heavylifters/deferred-js), send a pull request 219 | 220 | Contributors 221 | ------------ 222 | - [samsonjs](https://github.com/samsonjs) of [HeavyLifters](https://github.com/heavylifters) 223 | - [JimRoepcke](https://github.com/JimRoepcke) of [HeavyLifters](https://github.com/heavylifters) 224 | 225 | Alternatives 226 | ------------ 227 | 228 | - [node-promise](https://github.com/kriszyp/node-promise) by [Kris Zyp](https://twitter.com/kriszyp) 229 | - [q](https://github.com/kriskowal/q) by [Kris Kowal](https://twitter.com/kriskowal) 230 | - [jQuery Deferred Object](http://api.jquery.com/category/deferred-object/) 231 | - There are many more (forthcoming) 232 | - Write your own! Control flow libraries are the new web frameworks 233 | 234 | Credits 235 | ------- 236 | - Based on [Twisted's Deferred](http://twistedmatrix.com/trac/browser/tags/releases/twisted-10.2.0/twisted/internet/defer.py) classes 237 | - Sponsored by [HeavyLifters Network Ltd.](http://heavylifters.com/) 238 | 239 | License 240 | ------- 241 | 242 | Copyright 2011 [HeavyLifters Network Ltd.](http://heavylifters.com/) Licensed under the terms of the MIT license. See included [LICENSE](https://github.com/heavylifters/deferred-js/raw/master/LICENSE) file. 243 | 244 | [D]: https://github.com/heavylifters/deferred-js/wiki/Deferred 245 | [F]: https://github.com/heavylifters/deferred-js/wiki/Failure 246 | --------------------------------------------------------------------------------