UNPKG

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