callbacks.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /** @license MIT License (c) copyright 2013-2014 original author or authors */
  2. /**
  3. * Collection of helper functions for interacting with 'traditional',
  4. * callback-taking functions using a promise interface.
  5. *
  6. * @author Renato Zannon
  7. * @contributor Brian Cavalier
  8. */
  9. (function(define) {
  10. define(function(require) {
  11. var when = require('./when');
  12. var Promise = when.Promise;
  13. var _liftAll = require('./lib/liftAll');
  14. var slice = Array.prototype.slice;
  15. var makeApply = require('./lib/apply');
  16. var _apply = makeApply(Promise, dispatch);
  17. return {
  18. lift: lift,
  19. liftAll: liftAll,
  20. apply: apply,
  21. call: call,
  22. promisify: promisify
  23. };
  24. /**
  25. * Takes a `traditional` callback-taking function and returns a promise for its
  26. * result, accepting an optional array of arguments (that might be values or
  27. * promises). It assumes that the function takes its callback and errback as
  28. * the last two arguments. The resolution of the promise depends on whether the
  29. * function will call its callback or its errback.
  30. *
  31. * @example
  32. * var domIsLoaded = callbacks.apply($);
  33. * domIsLoaded.then(function() {
  34. * doMyDomStuff();
  35. * });
  36. *
  37. * @example
  38. * function existingAjaxyFunction(url, callback, errback) {
  39. * // Complex logic you'd rather not change
  40. * }
  41. *
  42. * var promise = callbacks.apply(existingAjaxyFunction, ["/movies.json"]);
  43. *
  44. * promise.then(function(movies) {
  45. * // Work with movies
  46. * }, function(reason) {
  47. * // Handle error
  48. * });
  49. *
  50. * @param {function} asyncFunction function to be called
  51. * @param {Array} [extraAsyncArgs] array of arguments to asyncFunction
  52. * @returns {Promise} promise for the callback value of asyncFunction
  53. */
  54. function apply(asyncFunction, extraAsyncArgs) {
  55. return _apply(asyncFunction, this, extraAsyncArgs || []);
  56. }
  57. /**
  58. * Apply helper that allows specifying thisArg
  59. * @private
  60. */
  61. function dispatch(f, thisArg, args, h) {
  62. args.push(alwaysUnary(h.resolve, h), alwaysUnary(h.reject, h));
  63. tryCatchResolve(f, thisArg, args, h);
  64. }
  65. function tryCatchResolve(f, thisArg, args, resolver) {
  66. try {
  67. f.apply(thisArg, args);
  68. } catch(e) {
  69. resolver.reject(e);
  70. }
  71. }
  72. /**
  73. * Works as `callbacks.apply` does, with the difference that the arguments to
  74. * the function are passed individually, instead of as an array.
  75. *
  76. * @example
  77. * function sumInFiveSeconds(a, b, callback) {
  78. * setTimeout(function() {
  79. * callback(a + b);
  80. * }, 5000);
  81. * }
  82. *
  83. * var sumPromise = callbacks.call(sumInFiveSeconds, 5, 10);
  84. *
  85. * // Logs '15' 5 seconds later
  86. * sumPromise.then(console.log);
  87. *
  88. * @param {function} asyncFunction function to be called
  89. * @param {...*} args arguments that will be forwarded to the function
  90. * @returns {Promise} promise for the callback value of asyncFunction
  91. */
  92. function call(asyncFunction/*, arg1, arg2...*/) {
  93. return _apply(asyncFunction, this, slice.call(arguments, 1));
  94. }
  95. /**
  96. * Takes a 'traditional' callback/errback-taking function and returns a function
  97. * that returns a promise instead. The resolution/rejection of the promise
  98. * depends on whether the original function will call its callback or its
  99. * errback.
  100. *
  101. * If additional arguments are passed to the `lift` call, they will be prepended
  102. * on the calls to the original function, much like `Function.prototype.bind`.
  103. *
  104. * The resulting function is also "promise-aware", in the sense that, if given
  105. * promises as arguments, it will wait for their resolution before executing.
  106. *
  107. * @example
  108. * function traditionalAjax(method, url, callback, errback) {
  109. * var xhr = new XMLHttpRequest();
  110. * xhr.open(method, url);
  111. *
  112. * xhr.onload = callback;
  113. * xhr.onerror = errback;
  114. *
  115. * xhr.send();
  116. * }
  117. *
  118. * var promiseAjax = callbacks.lift(traditionalAjax);
  119. * promiseAjax("GET", "/movies.json").then(console.log, console.error);
  120. *
  121. * var promiseAjaxGet = callbacks.lift(traditionalAjax, "GET");
  122. * promiseAjaxGet("/movies.json").then(console.log, console.error);
  123. *
  124. * @param {Function} f traditional async function to be decorated
  125. * @param {...*} [args] arguments to be prepended for the new function @deprecated
  126. * @returns {Function} a promise-returning function
  127. */
  128. function lift(f/*, args...*/) {
  129. var args = arguments.length > 1 ? slice.call(arguments, 1) : [];
  130. return function() {
  131. return _apply(f, this, args.concat(slice.call(arguments)));
  132. };
  133. }
  134. /**
  135. * Lift all the functions/methods on src
  136. * @param {object|function} src source whose functions will be lifted
  137. * @param {function?} combine optional function for customizing the lifting
  138. * process. It is passed dst, the lifted function, and the property name of
  139. * the original function on src.
  140. * @param {(object|function)?} dst option destination host onto which to place lifted
  141. * functions. If not provided, liftAll returns a new object.
  142. * @returns {*} If dst is provided, returns dst with lifted functions as
  143. * properties. If dst not provided, returns a new object with lifted functions.
  144. */
  145. function liftAll(src, combine, dst) {
  146. return _liftAll(lift, combine, dst, src);
  147. }
  148. /**
  149. * `promisify` is a version of `lift` that allows fine-grained control over the
  150. * arguments that passed to the underlying function. It is intended to handle
  151. * functions that don't follow the common callback and errback positions.
  152. *
  153. * The control is done by passing an object whose 'callback' and/or 'errback'
  154. * keys, whose values are the corresponding 0-based indexes of the arguments on
  155. * the function. Negative values are interpreted as being relative to the end
  156. * of the arguments array.
  157. *
  158. * If arguments are given on the call to the 'promisified' function, they are
  159. * intermingled with the callback and errback. If a promise is given among them,
  160. * the execution of the function will only occur after its resolution.
  161. *
  162. * @example
  163. * var delay = callbacks.promisify(setTimeout, {
  164. * callback: 0
  165. * });
  166. *
  167. * delay(100).then(function() {
  168. * console.log("This happens 100ms afterwards");
  169. * });
  170. *
  171. * @example
  172. * function callbackAsLast(errback, followsStandards, callback) {
  173. * if(followsStandards) {
  174. * callback("well done!");
  175. * } else {
  176. * errback("some programmers just want to watch the world burn");
  177. * }
  178. * }
  179. *
  180. * var promisified = callbacks.promisify(callbackAsLast, {
  181. * callback: -1,
  182. * errback: 0,
  183. * });
  184. *
  185. * promisified(true).then(console.log, console.error);
  186. * promisified(false).then(console.log, console.error);
  187. *
  188. * @param {Function} asyncFunction traditional function to be decorated
  189. * @param {object} positions
  190. * @param {number} [positions.callback] index at which asyncFunction expects to
  191. * receive a success callback
  192. * @param {number} [positions.errback] index at which asyncFunction expects to
  193. * receive an error callback
  194. * @returns {function} promisified function that accepts
  195. *
  196. * @deprecated
  197. */
  198. function promisify(asyncFunction, positions) {
  199. return function() {
  200. var thisArg = this;
  201. return Promise.all(arguments).then(function(args) {
  202. var p = Promise._defer();
  203. var callbackPos, errbackPos;
  204. if(typeof positions.callback === 'number') {
  205. callbackPos = normalizePosition(args, positions.callback);
  206. }
  207. if(typeof positions.errback === 'number') {
  208. errbackPos = normalizePosition(args, positions.errback);
  209. }
  210. if(errbackPos < callbackPos) {
  211. insertCallback(args, errbackPos, p._handler.reject, p._handler);
  212. insertCallback(args, callbackPos, p._handler.resolve, p._handler);
  213. } else {
  214. insertCallback(args, callbackPos, p._handler.resolve, p._handler);
  215. insertCallback(args, errbackPos, p._handler.reject, p._handler);
  216. }
  217. asyncFunction.apply(thisArg, args);
  218. return p;
  219. });
  220. };
  221. }
  222. function normalizePosition(args, pos) {
  223. return pos < 0 ? (args.length + pos + 2) : pos;
  224. }
  225. function insertCallback(args, pos, callback, thisArg) {
  226. if(typeof pos === 'number') {
  227. args.splice(pos, 0, alwaysUnary(callback, thisArg));
  228. }
  229. }
  230. function alwaysUnary(fn, thisArg) {
  231. return function() {
  232. if (arguments.length > 1) {
  233. fn.call(thisArg, slice.call(arguments));
  234. } else {
  235. fn.apply(thisArg, arguments);
  236. }
  237. };
  238. }
  239. });
  240. })(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); });