├── .gitignore ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── lib └── promise.js ├── package.json └── test ├── promise.domain.test.js ├── promise.test.js └── promises.Aplus.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | node_modules/ 3 | .DS_Store 4 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.10 5 | - 0.11 6 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.5.1 / 2014-01-20 2 | ================== 3 | 4 | * fixed; `end` is much more consistent (especially for `then` chains) 5 | 6 | 0.4.4 / 2014-01-20 7 | ================== 8 | 9 | * fixed; `end` is much more consistent (especially for `then` chains) 10 | 11 | 0.4.3 / 2013-12-17 12 | ================== 13 | 14 | * fixed; non-A+ behavior on fulfill and reject [lbeschastny](https://github.com/lbeschastny) 15 | * tests; simplified harness + compatible with travis + compatible with windows 16 | 17 | 0.5.0 / 2013-12-14 18 | ================== 19 | 20 | * fixed; non-A+ behavior on fulfill and reject [lbeschastny](https://github.com/lbeschastny) 21 | * tests; simplified harness + compatible with travis + compatible with windows 22 | 23 | 0.4.2 / 2013-11-26 24 | ================== 25 | 26 | * fixed; enter the domain only if not the present domain 27 | * added; `end` returns the promise 28 | 29 | 0.4.1 / 2013-10-26 30 | ================== 31 | 32 | * Add `all` 33 | * Longjohn for easier debugging 34 | * can end a promise chain with an error handler 35 | * Add ```chain``` 36 | 37 | 0.4.0 / 2013-10-24 38 | ================== 39 | 40 | * fixed; now plays nice with domains #3 [refack](https://github.com/refack) 41 | * updated; compatibility for Promises A+ 2.0.0 [refack](https://github.com/refack) 42 | * updated; guard against invalid arguments [refack](https://github.com/refack) 43 | 44 | 0.3.0 / 2013-07-25 45 | ================== 46 | 47 | * updated; sliced to 0.0.5 48 | * fixed; then is passed all fulfillment values 49 | * use setImmediate if available 50 | * conform to Promises A+ 1.1 51 | 52 | 0.2.1 / 2013-02-09 53 | ================== 54 | 55 | * fixed; conformancy with A+ 1.2 56 | 57 | 0.2.0 / 2013-01-09 58 | ================== 59 | 60 | * added; .end() 61 | * fixed; only catch handler executions 62 | 63 | 0.1.0 / 2013-01-08 64 | ================== 65 | 66 | * cleaned up API 67 | * customizable event names 68 | * docs 69 | 70 | 0.0.1 / 2013-01-07 71 | ================== 72 | 73 | * original release 74 | 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 [Aaron Heckmann](aaron.heckmann+github@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | 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 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpromise 2 | ========== 3 | 4 | [![Build Status](https://travis-ci.org/aheckmann/mpromise.png)](https://travis-ci.org/aheckmann/mpromise) 5 | 6 | A [promises/A+](https://github.com/promises-aplus/promises-spec) conformant implementation, written for [mongoose](http://mongoosejs.com). 7 | 8 | ## installation 9 | 10 | ``` 11 | $ npm install mpromise 12 | ``` 13 | 14 | ## docs 15 | 16 | An `mpromise` can be in any of three states, pending, fulfilled (success), or rejected (error). Once it is either fulfilled or rejected it's state can no longer be changed. 17 | 18 | The exports object is the Promise constructor. 19 | 20 | ```js 21 | var Promise = require('mpromise'); 22 | ``` 23 | 24 | The constructor accepts an optional function which is executed when the promise is first resolved (either fulfilled or rejected). 25 | 26 | ```js 27 | var promise = new Promise(fn); 28 | ``` 29 | 30 | This is the same as passing the `fn` to `onResolve` directly. 31 | 32 | ```js 33 | var promise = new Promise; 34 | promise.onResolve(function (err, args..) { 35 | ... 36 | }); 37 | ``` 38 | 39 | ### Methods 40 | 41 | #### fulfill 42 | 43 | Fulfilling a promise with values: 44 | 45 | ```js 46 | var promise = new Promise; 47 | promise.fulfill(args...); 48 | ``` 49 | 50 | If the promise has already been fulfilled or rejected, no action is taken. 51 | 52 | #### reject 53 | 54 | Rejecting a promise with a reason: 55 | 56 | ```js 57 | var promise = new Promise; 58 | promise.reject(reason); 59 | ``` 60 | 61 | If the promise has already been fulfilled or rejected, no action is taken. 62 | 63 | #### resolve 64 | 65 | Node.js callback style promise resolution `(err, args...)`: 66 | 67 | ```js 68 | var promise = new Promise; 69 | promise.resolve([reason], [arg1, arg2, ...]); 70 | ``` 71 | 72 | If the promise has already been fulfilled or rejected, no action is taken. 73 | 74 | #### onFulfill 75 | 76 | To register a function for execution when the promise is fulfilled, pass it to `onFulfill`. When executed it will receive the arguments passed to `fulfill()`. 77 | 78 | ```js 79 | var promise = new Promise; 80 | promise.onFulfill(function (a, b) { 81 | assert.equal(3, a + b); 82 | }); 83 | promise.fulfill(1, 2); 84 | ``` 85 | 86 | The function will only be called once when the promise is fulfilled, never when rejected. 87 | 88 | Registering a function with `onFulfill` after the promise has already been fulfilled results in the immediate execution of the function with the original arguments used to fulfill the promise. 89 | 90 | ```js 91 | var promise = new Promise; 92 | promise.fulfill(" :D "); 93 | promise.onFulfill(function (arg) { 94 | console.log(arg); // logs " :D " 95 | }) 96 | ``` 97 | 98 | #### onReject 99 | 100 | To register a function for execution when the promise is rejected, pass it to `onReject`. When executed it will receive the argument passed to `reject()`. 101 | 102 | ```js 103 | var promise = new Promise; 104 | promise.onReject(function (reason) { 105 | assert.equal('sad', reason); 106 | }); 107 | promise.reject('sad'); 108 | ``` 109 | 110 | The function will only be called once when the promise is rejected, never when fulfilled. 111 | 112 | Registering a function with `onReject` after the promise has already been rejected results in the immediate execution of the function with the original argument used to reject the promise. 113 | 114 | ```js 115 | var promise = new Promise; 116 | promise.reject(" :( "); 117 | promise.onReject(function (reason) { 118 | console.log(reason); // logs " :( " 119 | }) 120 | ``` 121 | 122 | #### onResolve 123 | 124 | Allows registration of node.js style callbacks `(err, args..)` to handle either promise resolution type (fulfill or reject). 125 | 126 | ```js 127 | // fulfillment 128 | var promise = new Promise; 129 | promise.onResolve(function (err, a, b) { 130 | console.log(a + b); // logs 3 131 | }); 132 | promise.fulfill(1, 2); 133 | 134 | // rejection 135 | var promise = new Promise; 136 | promise.onResolve(function (err) { 137 | if (err) { 138 | console.log(err.message); // logs "failed" 139 | } 140 | }); 141 | promise.reject(new Error('failed')); 142 | ``` 143 | 144 | #### then 145 | 146 | Creates a new promise and returns it. If `onFulfill` or `onReject` are passed, they are added as SUCCESS/ERROR callbacks to this promise after the nextTick. 147 | 148 | Conforms to [promises/A+](https://github.com/promises-aplus/promises-spec) specification and passes its [tests](https://github.com/promises-aplus/promises-tests). 149 | 150 | ```js 151 | // promise.then(onFulfill, onReject); 152 | 153 | var p = new Promise; 154 | 155 | p.then(function (arg) { 156 | return arg + 1; 157 | }).then(function (arg) { 158 | throw new Error(arg + ' is an error!'); 159 | }).then(null, function (err) { 160 | assert.ok(err instanceof Error); 161 | assert.equal('2 is an error!', err.message); 162 | }); 163 | p.fulfill(1); 164 | ``` 165 | 166 | #### end 167 | 168 | Signifies that this promise was the last in a chain of `then()s`: if a handler passed to the call to `then` which produced this promise throws, the exception be rethrown. 169 | You can pass an OnReject handler to `end` so that exceptions will be handled (like a final catch clause); 170 | This method returns it's promise for easy use with `return`. 171 | 172 | ```js 173 | var p = new Promise; 174 | p.then(function(){ throw new Error('shucks') }); 175 | setTimeout(function () { 176 | p.fulfill(); 177 | // error was caught and swallowed by the promise returned from 178 | // p.then(). we either have to always register handlers on 179 | // the returned promises or we can do the following... 180 | }, 10); 181 | 182 | // this time we use .end() which prevents catching thrown errors 183 | var p = new Promise; 184 | setTimeout(function () { 185 | p.fulfill(); // throws "shucks" 186 | }, 10); 187 | return p.then(function(){ throw new Error('shucks') }).end(); // <-- 188 | ``` 189 | 190 | 191 | ### chain 192 | 193 | Allows direct promise to promise chaining (especially useful by a outside aggregating function). It doesn't use the asynchronous `resolve` algorithm and so excepts only another Promise as it's argument. 194 | 195 | ```js 196 | function makeMeAPromise(i) { 197 | var p = new Promise; 198 | p.fulfill(i); 199 | return p; 200 | } 201 | 202 | var returnPromise = initialPromise = new Promise; 203 | for (i=0; i<10; ++i) 204 | returnPromise = returnPromise.chain(makeMeAPromise(i)); 205 | 206 | initialPromise.fulfill(); 207 | return returnPromise; 208 | ``` 209 | 210 | ### Event names 211 | 212 | If you'd like to alter this implementations event names used to signify success and failure you may do so by setting `Promise.SUCCESS` or `Promise.FAILURE` respectively. 213 | 214 | ```js 215 | Promise.SUCCESS = 'complete'; 216 | Promise.FAILURE = 'err'; 217 | ``` 218 | 219 | ### Luke, use the Source 220 | For more ideas read the [source](https://github.com/aheckmann/mpromise/blob/master/lib), [tests](https://github.com/aheckmann/mpromise/blob/master/test), or the [mongoose implementation](https://github.com/LearnBoost/mongoose/blob/3.6x/lib/promise.js). 221 | 222 | ## license 223 | 224 | [MIT](https://github.com/aheckmann/mpromise/blob/master/LICENSE) 225 | -------------------------------------------------------------------------------- /lib/promise.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var EventEmitter = require('events').EventEmitter; 4 | function toArray(arr, start, end) { 5 | return Array.prototype.slice.call(arr, start, end) 6 | } 7 | function strongUnshift(x, arrLike) { 8 | var arr = toArray(arrLike); 9 | arr.unshift(x); 10 | return arr; 11 | } 12 | 13 | 14 | /** 15 | * Promise constructor. 16 | * 17 | * _NOTE: The success and failure event names can be overridden by setting `Promise.SUCCESS` and `Promise.FAILURE` respectively._ 18 | * 19 | * @param {Function} back a function that accepts `fn(err, ...){}` as signature 20 | * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter 21 | * @event `reject`: Emits when the promise is rejected (event name may be overridden) 22 | * @event `fulfill`: Emits when the promise is fulfilled (event name may be overridden) 23 | * @api public 24 | */ 25 | function Promise(back) { 26 | this.emitter = new EventEmitter(); 27 | this.emitted = {}; 28 | this.ended = false; 29 | if ('function' == typeof back) 30 | this.onResolve(back); 31 | } 32 | 33 | 34 | /* 35 | * Module exports. 36 | */ 37 | module.exports = Promise; 38 | 39 | 40 | /*! 41 | * event names 42 | */ 43 | Promise.SUCCESS = 'fulfill'; 44 | Promise.FAILURE = 'reject'; 45 | 46 | 47 | /** 48 | * Adds `listener` to the `event`. 49 | * 50 | * If `event` is either the success or failure event and the event has already been emitted, the`listener` is called immediately and passed the results of the original emitted event. 51 | * 52 | * @param {String} event 53 | * @param {Function} callback 54 | * @return {Promise} this 55 | * @api private 56 | */ 57 | Promise.prototype.on = function (event, callback) { 58 | if (this.emitted[event]) 59 | callback.apply(undefined, this.emitted[event]); 60 | else 61 | this.emitter.on(event, callback); 62 | 63 | return this; 64 | }; 65 | 66 | 67 | /** 68 | * Keeps track of emitted events to run them on `on`. 69 | * 70 | * @api private 71 | */ 72 | Promise.prototype.safeEmit = function (event) { 73 | // ensures a promise can't be fulfill() or reject() more than once 74 | if (event == Promise.SUCCESS || event == Promise.FAILURE) { 75 | if (this.emitted[Promise.SUCCESS] || this.emitted[Promise.FAILURE]) { 76 | return this; 77 | } 78 | this.emitted[event] = toArray(arguments, 1); 79 | } 80 | 81 | this.emitter.emit.apply(this.emitter, arguments); 82 | return this; 83 | }; 84 | 85 | 86 | /** 87 | * Fulfills this promise with passed arguments. 88 | * 89 | * If this promise has already been fulfilled or rejected, no action is taken. 90 | * 91 | * @api public 92 | */ 93 | Promise.prototype.fulfill = function () { 94 | return this.safeEmit.apply(this, strongUnshift(Promise.SUCCESS, arguments)); 95 | }; 96 | 97 | 98 | /** 99 | * Rejects this promise with `reason`. 100 | * 101 | * If this promise has already been fulfilled or rejected, no action is taken. 102 | * 103 | * @api public 104 | * @param {Object|String} reason 105 | * @return {Promise} this 106 | */ 107 | Promise.prototype.reject = function (reason) { 108 | if (this.ended && !this.hasRejectListeners()) throw reason; 109 | return this.safeEmit(Promise.FAILURE, reason); 110 | }; 111 | 112 | 113 | /** 114 | * Resolves this promise to a rejected state if `err` is passed or 115 | * fulfilled state if no `err` is passed. 116 | * 117 | * @param {Error} [err] error or null 118 | * @param {Object} [val] value to fulfill the promise with 119 | * @api public 120 | */ 121 | Promise.prototype.resolve = function (err) { 122 | if (err) return this.reject(err); 123 | return this.fulfill.apply(this, toArray(arguments, 1)); 124 | }; 125 | 126 | /** 127 | * Adds a listener to the SUCCESS event. 128 | * 129 | * @return {Promise} this 130 | * @api public 131 | */ 132 | Promise.prototype.onFulfill = function (fn) { 133 | if (!fn) return this; 134 | if ('function' != typeof fn) throw new TypeError("fn should be a function"); 135 | return this.on(Promise.SUCCESS, fn); 136 | }; 137 | 138 | 139 | Promise.prototype.hasRejectListeners = function () { 140 | return this.emitter.listeners(Promise.FAILURE).length > 0; 141 | }; 142 | 143 | 144 | /** 145 | * Adds a listener to the FAILURE event. 146 | * 147 | * @return {Promise} this 148 | * @api public 149 | */ 150 | Promise.prototype.onReject = function (fn) { 151 | if (!fn) return this; 152 | if ('function' != typeof fn) throw new TypeError("fn should be a function"); 153 | return this.on(Promise.FAILURE, fn); 154 | }; 155 | 156 | 157 | /** 158 | * Adds a single function as a listener to both SUCCESS and FAILURE. 159 | * 160 | * It will be executed with traditional node.js argument position: 161 | * function (err, args...) {} 162 | * 163 | * Also marks the promise as `end`ed, since it's the common use-case, and yet has no 164 | * side effects unless `fn` is undefined or null. 165 | * 166 | * @param {Function} fn 167 | * @return {Promise} this 168 | */ 169 | Promise.prototype.onResolve = function (fn) { 170 | this.end(); 171 | if (!fn) return this; 172 | if ('function' != typeof fn) throw new TypeError("fn should be a function"); 173 | this.on(Promise.FAILURE, function (err) { fn.call(this, err); }); 174 | this.on(Promise.SUCCESS, function () { fn.apply(this, strongUnshift(null, arguments)); }); 175 | return this; 176 | }; 177 | 178 | 179 | /** 180 | * Creates a new promise and returns it. If `onFulfill` or 181 | * `onReject` are passed, they are added as SUCCESS/ERROR callbacks 182 | * to this promise after the next tick. 183 | * 184 | * Conforms to [promises/A+](https://github.com/promises-aplus/promises-spec) specification. Read for more detail how to use this method. 185 | * 186 | * ####Example: 187 | * 188 | * var p = new Promise; 189 | * p.then(function (arg) { 190 | * return arg + 1; 191 | * }).then(function (arg) { 192 | * throw new Error(arg + ' is an error!'); 193 | * }).then(null, function (err) { 194 | * assert.ok(err instanceof Error); 195 | * assert.equal('2 is an error', err.message); 196 | * }); 197 | * p.complete(1); 198 | * 199 | * @see promises-A+ https://github.com/promises-aplus/promises-spec 200 | * @param {Function} onFulfill 201 | * @param {Function} [onReject] 202 | * @return {Promise} newPromise 203 | */ 204 | Promise.prototype.then = function (onFulfill, onReject) { 205 | var newPromise = new Promise; 206 | 207 | if ('function' == typeof onFulfill) { 208 | this.onFulfill(handler(newPromise, onFulfill)); 209 | } else { 210 | this.onFulfill(newPromise.fulfill.bind(newPromise)); 211 | } 212 | 213 | if ('function' == typeof onReject) { 214 | this.onReject(handler(newPromise, onReject)); 215 | } else { 216 | this.onReject(newPromise.reject.bind(newPromise)); 217 | } 218 | 219 | return newPromise; 220 | }; 221 | 222 | 223 | function handler(promise, fn) { 224 | function newTickHandler() { 225 | var pDomain = promise.emitter.domain; 226 | if (pDomain && pDomain !== process.domain) pDomain.enter(); 227 | try { 228 | var x = fn.apply(undefined, boundHandler.args); 229 | } catch (err) { 230 | promise.reject(err); 231 | return; 232 | } 233 | resolve(promise, x); 234 | } 235 | function boundHandler() { 236 | boundHandler.args = arguments; 237 | process.nextTick(newTickHandler); 238 | } 239 | return boundHandler; 240 | } 241 | 242 | 243 | function resolve(promise, x) { 244 | function fulfillOnce() { 245 | if (done++) return; 246 | resolve.apply(undefined, strongUnshift(promise, arguments)); 247 | } 248 | function rejectOnce(reason) { 249 | if (done++) return; 250 | promise.reject(reason); 251 | } 252 | 253 | if (promise === x) { 254 | promise.reject(new TypeError("promise and x are the same")); 255 | return; 256 | } 257 | var rest = toArray(arguments, 1); 258 | var type = typeof x; 259 | if ('undefined' == type || null == x || !('object' == type || 'function' == type)) { 260 | promise.fulfill.apply(promise, rest); 261 | return; 262 | } 263 | 264 | try { 265 | var theThen = x.then; 266 | } catch (err) { 267 | promise.reject(err); 268 | return; 269 | } 270 | 271 | if ('function' != typeof theThen) { 272 | promise.fulfill.apply(promise, rest); 273 | return; 274 | } 275 | 276 | var done = 0; 277 | try { 278 | var ret = theThen.call(x, fulfillOnce, rejectOnce); 279 | return ret; 280 | } catch (err) { 281 | if (done++) return; 282 | promise.reject(err); 283 | } 284 | } 285 | 286 | 287 | /** 288 | * Signifies that this promise was the last in a chain of `then()s`: if a handler passed to the call to `then` which produced this promise throws, the exception will go uncaught. 289 | * 290 | * ####Example: 291 | * 292 | * var p = new Promise; 293 | * p.then(function(){ throw new Error('shucks') }); 294 | * setTimeout(function () { 295 | * p.fulfill(); 296 | * // error was caught and swallowed by the promise returned from 297 | * // p.then(). we either have to always register handlers on 298 | * // the returned promises or we can do the following... 299 | * }, 10); 300 | * 301 | * // this time we use .end() which prevents catching thrown errors 302 | * var p = new Promise; 303 | * var p2 = p.then(function(){ throw new Error('shucks') }).end(); // <-- 304 | * setTimeout(function () { 305 | * p.fulfill(); // throws "shucks" 306 | * }, 10); 307 | * 308 | * @api public 309 | * @param {Function} [onReject] 310 | * @return {Promise} this 311 | */ 312 | Promise.prototype.end = function (onReject) { 313 | this.onReject(onReject); 314 | this.ended = true; 315 | return this; 316 | }; 317 | 318 | 319 | /** 320 | * A debug utility function that adds handlers to a promise that will log some output to the `console` 321 | * 322 | * ####Example: 323 | * 324 | * var p = new Promise; 325 | * p.then(function(){ throw new Error('shucks') }); 326 | * setTimeout(function () { 327 | * p.fulfill(); 328 | * // error was caught and swallowed by the promise returned from 329 | * // p.then(). we either have to always register handlers on 330 | * // the returned promises or we can do the following... 331 | * }, 10); 332 | * 333 | * // this time we use .end() which prevents catching thrown errors 334 | * var p = new Promise; 335 | * var p2 = p.then(function(){ throw new Error('shucks') }).end(); // <-- 336 | * setTimeout(function () { 337 | * p.fulfill(); // throws "shucks" 338 | * }, 10); 339 | * 340 | * @api public 341 | * @param {Promise} p 342 | * @param {String} name 343 | * @return {Promise} this 344 | */ 345 | Promise.trace = function (p, name) { 346 | p.then( 347 | function () { 348 | console.log("%s fulfill %j", name, toArray(arguments)); 349 | }, 350 | function () { 351 | console.log("%s reject %j", name, toArray(arguments)); 352 | } 353 | ) 354 | }; 355 | 356 | 357 | Promise.prototype.chain = function (p2) { 358 | var p1 = this; 359 | p1.onFulfill(p2.fulfill.bind(p2)); 360 | p1.onReject(p2.reject.bind(p2)); 361 | return p2; 362 | }; 363 | 364 | 365 | Promise.prototype.all = function (promiseOfArr) { 366 | var pRet = new Promise; 367 | this.then(promiseOfArr).then( 368 | function (promiseArr) { 369 | var count = 0; 370 | var ret = []; 371 | var errSentinel; 372 | if (!promiseArr.length) pRet.resolve(); 373 | promiseArr.forEach(function (promise, index) { 374 | if (errSentinel) return; 375 | count++; 376 | promise.then( 377 | function (val) { 378 | if (errSentinel) return; 379 | ret[index] = val; 380 | --count; 381 | if (count == 0) pRet.fulfill(ret); 382 | }, 383 | function (err) { 384 | if (errSentinel) return; 385 | errSentinel = err; 386 | pRet.reject(err); 387 | } 388 | ); 389 | }); 390 | return pRet; 391 | } 392 | , pRet.reject.bind(pRet) 393 | ); 394 | return pRet; 395 | }; 396 | 397 | 398 | Promise.hook = function (arr) { 399 | var p1 = new Promise; 400 | var pFinal = new Promise; 401 | var signalP = function () { 402 | --count; 403 | if (count == 0) 404 | pFinal.fulfill(); 405 | return pFinal; 406 | }; 407 | var count = 1; 408 | var ps = p1; 409 | arr.forEach(function (hook) { 410 | ps = ps.then( 411 | function () { 412 | var p = new Promise; 413 | count++; 414 | hook(p.resolve.bind(p), signalP); 415 | return p; 416 | } 417 | ) 418 | }); 419 | ps = ps.then(signalP); 420 | p1.resolve(); 421 | return ps; 422 | }; 423 | 424 | 425 | /* This is for the A+ tests, but it's very useful as well */ 426 | Promise.fulfilled = function fulfilled() { var p = new Promise; p.fulfill.apply(p, arguments); return p; }; 427 | Promise.rejected = function rejected(reason) { return new Promise().reject(reason); }; 428 | Promise.deferred = function deferred() { 429 | var p = new Promise; 430 | return { 431 | promise: p, 432 | reject: p.reject.bind(p), 433 | resolve: p.fulfill.bind(p), 434 | callback: p.resolve.bind(p) 435 | } 436 | }; 437 | /* End A+ tests adapter bit */ 438 | 439 | 440 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpromise", 3 | "version": "0.5.4", 4 | "publishConfig": { 5 | "tag": "beta" 6 | }, 7 | "description": "Promises A+ conformant implementation", 8 | "main": "lib/promise.js", 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "devDependencies": { 13 | "promises-aplus-tests": "2.0.3", 14 | "mocha": "1.17.1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/aheckmann/mpromise" 19 | }, 20 | "keywords": [ 21 | "promise", 22 | "mongoose", 23 | "aplus", 24 | "a+", 25 | "plus" 26 | ], 27 | "author": "Aaron Heckmann ", 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /test/promise.domain.test.js: -------------------------------------------------------------------------------- 1 | var Promise = require('../') 2 | , Domain = require('domain').Domain 3 | , assert = require('assert'); 4 | 5 | 6 | describe("domains", function () { 7 | it("exceptions should not breakout of domain boundaries", function (done) { 8 | if (process.version.indexOf('v0.10') != 0) return done(); 9 | var d = new Domain; 10 | d.on('error', function (err) { 11 | assert.equal(err.message, 'gaga'); 12 | done() 13 | }); 14 | 15 | var p = new Promise(); 16 | d.run(function () { 17 | p.then( 18 | function () {} 19 | ).then( 20 | function () { throw new Error('gaga'); } 21 | ).end(); 22 | }); 23 | 24 | process.nextTick(function () { 25 | p.fulfill(); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/promise.test.js: -------------------------------------------------------------------------------- 1 | /*global describe,it */ 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var assert = require('assert'); 7 | var Promise = require('../'); 8 | 9 | /** 10 | * Test. 11 | */ 12 | 13 | describe('promise', function () { 14 | it('events fire right after fulfill()', function (done) { 15 | var promise = new Promise() 16 | , called = 0; 17 | 18 | promise.on('fulfill', function (a, b) { 19 | assert.equal(a, '1'); 20 | assert.equal(b, '2'); 21 | called++; 22 | }); 23 | 24 | promise.fulfill('1', '2'); 25 | 26 | promise.on('fulfill', function (a, b) { 27 | assert.equal(a, '1'); 28 | assert.equal(b, '2'); 29 | called++; 30 | }); 31 | 32 | assert.equal(2, called); 33 | done(); 34 | }); 35 | 36 | it('events fire right after reject()', function (done) { 37 | var promise = new Promise() 38 | , called = 0; 39 | 40 | promise.on('reject', function (err) { 41 | assert.ok(err instanceof Error); 42 | called++; 43 | }); 44 | 45 | promise.reject(new Error('booyah')); 46 | 47 | promise.on('reject', function (err) { 48 | assert.ok(err instanceof Error); 49 | called++; 50 | }); 51 | 52 | assert.equal(2, called); 53 | done() 54 | }); 55 | 56 | describe('onResolve()', function () { 57 | it('from constructor works', function (done) { 58 | var called = 0; 59 | 60 | var promise = new Promise(function (err) { 61 | assert.ok(err instanceof Error); 62 | called++; 63 | }); 64 | 65 | promise.reject(new Error('dawg')); 66 | 67 | assert.equal(1, called); 68 | done(); 69 | }); 70 | 71 | it('after fulfill()', function (done) { 72 | var promise = new Promise() 73 | , called = 0; 74 | 75 | promise.fulfill('woot'); 76 | 77 | promise.onResolve(function (err, data) { 78 | assert.equal(data, 'woot'); 79 | called++; 80 | }); 81 | 82 | promise.onResolve(function (err) { 83 | assert.strictEqual(err, null); 84 | called++; 85 | }); 86 | 87 | assert.equal(2, called); 88 | done(); 89 | }) 90 | }); 91 | 92 | describe('onFulfill shortcut', function () { 93 | it('works', function (done) { 94 | var promise = new Promise() 95 | , called = 0; 96 | 97 | promise.onFulfill(function (woot) { 98 | assert.strictEqual(woot, undefined); 99 | called++; 100 | }); 101 | 102 | promise.fulfill(); 103 | 104 | assert.equal(1, called); 105 | done(); 106 | }); 107 | }); 108 | 109 | describe('onReject shortcut', function () { 110 | it('works', function (done) { 111 | var promise = new Promise() 112 | , called = 0; 113 | 114 | promise.onReject(function (err) { 115 | assert.ok(err instanceof Error); 116 | called++; 117 | }); 118 | 119 | promise.reject(new Error); 120 | assert.equal(1, called); 121 | done(); 122 | }) 123 | }); 124 | 125 | describe('return values', function () { 126 | it('on()', function (done) { 127 | var promise = new Promise(); 128 | assert.ok(promise.on('jump', function () {}) instanceof Promise); 129 | done() 130 | }); 131 | 132 | it('onFulfill()', function (done) { 133 | var promise = new Promise(); 134 | assert.ok(promise.onFulfill(function () {}) instanceof Promise); 135 | done(); 136 | }); 137 | it('onReject()', function (done) { 138 | var promise = new Promise(); 139 | assert.ok(promise.onReject(function () {}) instanceof Promise); 140 | done(); 141 | }); 142 | it('onResolve()', function (done) { 143 | var promise = new Promise(); 144 | assert.ok(promise.onResolve(function () {}) instanceof Promise); 145 | done(); 146 | }) 147 | }); 148 | 149 | describe('casting errors', function () { 150 | describe('reject()', function () { 151 | it('does not cast arguments to Error', function (done) { 152 | var p = new Promise(function (err) { 153 | assert.equal(3, err); 154 | done(); 155 | }); 156 | 157 | p.reject(3); 158 | }) 159 | }) 160 | }); 161 | 162 | describe('then', function () { 163 | describe('catching', function () { 164 | it('should not catch returned promise fulfillments', function (done) { 165 | var errorSentinal 166 | , p = new Promise; 167 | p.then(function () { throw errorSentinal = new Error("boo!") }); 168 | 169 | p.fulfill(); 170 | done(); 171 | }); 172 | 173 | 174 | it('should not catch returned promise fulfillments even async', function (done) { 175 | var errorSentinal 176 | , p = new Promise; 177 | p.then(function () { throw errorSentinal = new Error("boo!") }); 178 | 179 | setTimeout(function () { 180 | p.fulfill(); 181 | done(); 182 | }, 10); 183 | }); 184 | 185 | 186 | it('can be disabled using .end()', function (done) { 187 | if (process.version.indexOf('v0.8') == 0) return done(); 188 | var errorSentinal 189 | , overTimeout 190 | , domain = require('domain').create(); 191 | 192 | domain.once('error', function (err) { 193 | assert(err, errorSentinal); 194 | clearTimeout(overTimeout); 195 | done() 196 | }); 197 | 198 | domain.run(function () { 199 | var p = new Promise; 200 | var p2 = p.then(function () { 201 | throw errorSentinal = new Error('shucks') 202 | }); 203 | p2.end(); 204 | 205 | p.fulfill(); 206 | }); 207 | overTimeout = setTimeout(function () { done(new Error('error was swallowed')); }, 10); 208 | }); 209 | 210 | 211 | it('can be disabled using .end() even when async', function (done) { 212 | if (process.version.indexOf('v0.10') != 0) return done(); 213 | var errorSentinal 214 | , overTimeout 215 | , domain = require('domain').create(); 216 | 217 | domain.on('error', function (err) { 218 | assert(err, errorSentinal); 219 | clearTimeout(overTimeout); 220 | done() 221 | }); 222 | 223 | domain.run(function () { 224 | var p = new Promise; 225 | var p2 = p.then(function () { 226 | throw errorSentinal = new Error("boo!") 227 | }); 228 | p2.end(); 229 | 230 | setTimeout(function () {p.fulfill();}, 10); 231 | }); 232 | overTimeout = setTimeout(function () { done(new Error('error was swallowed')); }, 20); 233 | }); 234 | 235 | 236 | it('can be handled using .end() so no throwing', function (done) { 237 | var errorSentinal 238 | , overTimeout 239 | , domain = require('domain').create(); 240 | 241 | domain.run(function () { 242 | var p = new Promise; 243 | var p2 = p.then(function () { 244 | throw errorSentinal = new Error("boo!") 245 | }); 246 | p2.end(function (err) { 247 | assert.equal(err, errorSentinal); 248 | clearTimeout(overTimeout); 249 | done() 250 | }); 251 | 252 | setTimeout(function () {p.fulfill();}, 10); 253 | }); 254 | overTimeout = setTimeout(function () { done(new Error('error was swallowed')); }, 20); 255 | }); 256 | 257 | }); 258 | 259 | it('persistent', function (done) { 260 | var p = new Promise; 261 | v = null; 262 | 263 | function ensure(val) { 264 | v = v || val; 265 | assert.equal(v, val); 266 | } 267 | 268 | function guard() { 269 | throw new Error('onReject should not be called'); 270 | } 271 | 272 | p.then(ensure, guard).end(); 273 | 274 | p.fulfill('foo'); 275 | p.fulfill('bar'); 276 | p.reject(new Error('baz')); 277 | 278 | p.then(ensure, guard).end(); 279 | 280 | setTimeout(done, 0); 281 | }); 282 | 283 | 284 | it('accepts multiple completion values', function (done) { 285 | var p = new Promise; 286 | 287 | p.then(function (a, b) { 288 | assert.equal(2, arguments.length); 289 | assert.equal('hi', a); 290 | assert.equal(4, b); 291 | done(); 292 | }, done).end(); 293 | 294 | p.fulfill('hi', 4); 295 | }) 296 | }); 297 | 298 | describe('fulfill values and splats', function () { 299 | it('should handle multiple values', function (done) { 300 | var p = new Promise; 301 | p.onFulfill(function (a, b, c) { 302 | assert.equal('a', a); 303 | assert.equal('b', b); 304 | assert.equal('c', c); 305 | done(); 306 | }); 307 | p.fulfill('a', 'b', 'c'); 308 | }); 309 | 310 | it('should handle multiple values from a then', function (done) { 311 | Promise.fulfilled().then( 312 | function () { 313 | return Promise.fulfilled().then( 314 | function () { 315 | var p = new Promise; 316 | p.fulfill('a', 'b', 'c'); 317 | return p; 318 | } 319 | ); 320 | } 321 | ).onFulfill( 322 | function (a, b, c) { 323 | assert.equal('a', a); 324 | assert.equal('b', b); 325 | assert.equal('c', c); 326 | done(); 327 | } 328 | ).end() 329 | }); 330 | 331 | it('should work with `fulfilled` convenience method', function (done) { 332 | Promise.fulfilled('a', 'b', 'c').then(function (a, b, c) { 333 | assert.equal('a', a); 334 | assert.equal('b', b); 335 | assert.equal('c', c); 336 | done(); 337 | }) 338 | }); 339 | }); 340 | 341 | 342 | describe('end', function () { 343 | it("should return the promise", function (done) { 344 | var p = new Promise; 345 | var p1 = p.end(); 346 | assert.equal(p, p1); 347 | done(); 348 | }); 349 | 350 | 351 | it("should throw for chain", function (done) { 352 | var p = new Promise; 353 | p.then().then().then().then().end(); 354 | try { 355 | p.reject('bad'); 356 | } catch (e) { 357 | done(); 358 | } 359 | }); 360 | 361 | 362 | it("should not throw for chain with reject handler", function (done) { 363 | var p = new Promise; 364 | p.then().then().then().then().end(function () { 365 | done(); 366 | }); 367 | try { 368 | p.reject('bad'); 369 | } catch (e) { 370 | done(e); 371 | } 372 | }); 373 | }); 374 | 375 | 376 | describe('chain', function () { 377 | it('should propagate fulfillment', function (done) { 378 | var varSentinel = {a: 'a'}; 379 | var p1 = new Promise; 380 | p1.chain(new Promise(function (err, doc) { 381 | assert.equal(doc, varSentinel); 382 | done(); 383 | })); 384 | p1.fulfill(varSentinel); 385 | }); 386 | 387 | 388 | it('should propagate rejection', function (done) { 389 | var e = new Error("gaga"); 390 | var p1 = new Promise; 391 | p1.chain(new Promise(function (err) { 392 | assert.equal(err, e); 393 | done(); 394 | })); 395 | p1.reject(e); 396 | }); 397 | 398 | 399 | it('should propagate resolution err', function (done) { 400 | var e = new Error("gaga"); 401 | var p1 = new Promise; 402 | p1.chain(new Promise(function (err) { 403 | assert.equal(err, e); 404 | done(); 405 | })); 406 | p1.resolve(e); 407 | }); 408 | 409 | 410 | it('should propagate resolution val', function (done) { 411 | var varSentinel = {a: 'a'}; 412 | var p1 = new Promise; 413 | p1.chain(new Promise(function (err, val) { 414 | assert.equal(val, varSentinel); 415 | done(); 416 | })); 417 | p1.resolve(null, varSentinel); 418 | }); 419 | 420 | 421 | it('should propagate multiple resolution vals', function(done) { 422 | var val1 = 'eggs'; 423 | var val2 = 'bacon'; 424 | var p = new Promise; 425 | p.chain(new Promise(function (err, v1, v2) { 426 | assert.equal(v1, val1); 427 | assert.equal(v2, val2); 428 | done(); 429 | })); 430 | p.resolve(null, val1, val2); 431 | }); 432 | }); 433 | 434 | 435 | describe("all", function () { 436 | it("works", function (done) { 437 | var count = 0; 438 | var p = new Promise; 439 | var p2 = p.all(function () { 440 | return [ 441 | (function () { 442 | var p = new Promise(); 443 | count++; 444 | p.resolve(); 445 | return p; 446 | })() 447 | , (function () { 448 | var p = new Promise(); 449 | count++; 450 | p.resolve(); 451 | return p; 452 | })() 453 | ]; 454 | }); 455 | p2.then(function () { 456 | assert.equal(count, 2); 457 | done(); 458 | }); 459 | p.resolve(); 460 | }); 461 | 462 | 463 | it("handles rejects", function (done) { 464 | var count = 0; 465 | var p = new Promise; 466 | var p2 = p.all(function () { 467 | return [ 468 | (function () { 469 | var p = new Promise(); 470 | count++; 471 | p.resolve(); 472 | return p; 473 | })() 474 | , (function () { 475 | count++; 476 | throw new Error("gaga"); 477 | })() 478 | ]; 479 | }); 480 | p2.onReject(function (err) { 481 | assert(err.message, "gaga"); 482 | assert.equal(count, 2); 483 | done(); 484 | }); 485 | p.resolve(); 486 | }); 487 | }); 488 | 489 | 490 | describe("deferred", function () { 491 | it("works", function (done) { 492 | var d = Promise.deferred(); 493 | assert.ok(d.promise instanceof Promise); 494 | assert.ok(d.reject instanceof Function); 495 | assert.ok(d.resolve instanceof Function); 496 | assert.ok(d.callback instanceof Function); 497 | done(); 498 | }); 499 | }); 500 | 501 | 502 | describe("hook", function () { 503 | it("works", function (done) { 504 | var run = 0; 505 | var l1 = function (ser, par) { 506 | run++; 507 | ser(); 508 | par(); 509 | }; 510 | Promise.hook([l1, l1, l1]).then(function () { 511 | assert(run, 3); 512 | done(); 513 | }) 514 | 515 | }); 516 | 517 | 518 | it("works with async serial hooks", function (done) { 519 | this.timeout(800); 520 | var run = 0; 521 | var l1 = function (ser, par) { 522 | run++; 523 | setTimeout(function () {ser();}, 200); 524 | par(); 525 | }; 526 | Promise.hook([l1, l1, l1]).then(function () { 527 | assert(run, 3); 528 | done(); 529 | }) 530 | }); 531 | 532 | 533 | it("works with async parallel hooks", function (done) { 534 | this.timeout(400); 535 | var run = 0; 536 | var l1 = function (ser, par) { 537 | run++; 538 | ser(); 539 | setTimeout(function () {par();}, 200); 540 | }; 541 | Promise.hook([l1, l1, l1]).then(function () { 542 | assert(run, 3); 543 | done(); 544 | }) 545 | }); 546 | 547 | 548 | it("catches errors in hook logic", function (done) { 549 | var run = 0; 550 | var l1 = function (ser, par) { 551 | run++; 552 | ser(); 553 | par(); 554 | }; 555 | var l2 = function (ser, par) { 556 | run++; 557 | ser(); 558 | par(); 559 | throw new Error("err") 560 | }; 561 | Promise.hook([l1, l2, l1]).end(function (err) { 562 | assert(run, 2); 563 | done(); 564 | }); 565 | }); 566 | }); 567 | }); 568 | -------------------------------------------------------------------------------- /test/promises.Aplus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Promise = require('../lib/promise'); 5 | var aplus = require('promises-aplus-tests'); 6 | 7 | // tests 8 | describe("run A+ suite", function () { 9 | aplus.mocha({ 10 | fulfilled: Promise.fulfilled, 11 | rejected: Promise.rejected, 12 | deferred: Promise.deferred 13 | }); 14 | }); 15 | 16 | --------------------------------------------------------------------------------