This repository has been archived by the owner on Nov 3, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 37
/
allback.js
200 lines (189 loc) · 6.42 KB
/
allback.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/**
* Simple coordination logic that might be better handled by promises, although
* we probably have the edge in comprehensibility for now.
**/
define(['exports'], function(exports) {
/**
* Create multiple named callbacks whose results are aggregated and a single
* callback invoked once all the callbacks have returned their result. This
* is intended to provide similar benefit to $Q.all in our non-promise world
* while also possibly being more useful.
*
* Example:
* @js{
* var callbacks = allbackMaker(['foo', 'bar'], function(aggrData) {
* console.log("Foo's result was", aggrData.foo);
* console.log("Bar's result was", aggrData.bar);
* });
* asyncFooFunc(callbacks.foo);
* asyncBarFunc(callbacks.bar);
* }
*
* Protection against a callback being invoked multiple times is provided as
* an anti-foot-shooting measure. Timeout logic and other protection against
* potential memory leaks is not currently provided, but could be.
*/
exports.allbackMaker = function allbackMaker(names, allDoneCallback) {
var aggrData = Object.create(null), callbacks = {},
waitingFor = names.concat();
names.forEach(function(name) {
// (build a consistent shape for aggrData regardless of callback ordering)
aggrData[name] = undefined;
callbacks[name] = function anAllback(callbackResult) {
var i = waitingFor.indexOf(name);
if (i === -1) {
console.error("Callback '" + name + "' fired multiple times!");
throw new Error("Callback '" + name + "' fired multiple times!");
}
waitingFor.splice(i, 1);
if (arguments.length > 1)
aggrData[name] = arguments;
else
aggrData[name] = callbackResult;
if (waitingFor.length === 0 && allDoneCallback)
allDoneCallback(aggrData);
};
});
return callbacks;
};
/**
* A lightweight deferred 'run-all'-like construct for waiting for
* multiple callbacks to finish executing, with a final completion
* callback at the end. Neither promises nor Q provide a construct
* quite like this; Q.all and Promise.all tend to either require all
* promises to be created up front, or they return when the first
* error occurs. This is designed to allow you to wait for an unknown
* number of callbacks, with the knowledge that they're going to
* execute anyway -- no sense trying to abort early.
*
* Results passed to each callback can be passed along to the final
* result by adding a `name` parameter when calling latch.defer().
*
* Example usage:
*
* var latch = allback.latch();
* setTimeout(latch.defer('timeout1'), 200);
* var cb = latch.defer('timeout2');
* cb('foo');
* latch.then(function(results) {
* console.log(results.timeout2[0]); // => 'foo'
* });
*
* The returned latch is an A+ Promises-compatible thennable, so you
* can chain multiple callbacks to the latch.
*
* The promise will never fail; it will always succeed. Each
* `.defer()` call can be passed a `name`; if a name is provided, that
* callback's arguments will be made available as a key on the result
* object.
*
* NOTE: The latch will not actually fire completion until you've
* attached a callback handler. This way, you can create the latch
* before you know how many callbacks you'll need; when you've called
* .defer() as many times as necessary, you can call `then()` to
* actually fire the completion function (when they have all
* completed).
*/
exports.latch = function() {
var ready = false;
var deferred = {};
// Avoid Object.prototype and any for-enumerations getting tripped up
var results = Object.create(null);
var count = 0;
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
function defer(name) {
count++;
var resolved = false;
return function resolve() {
if (resolved) {
var err = new Error("You have already resolved this deferred!");
// Exceptions aren't always readily visible, but this is a
// serious error and needs to be addressed.
console.error(err + '\n' + err.stack);
throw err;
}
resolved = true;
// 'name' might be the integer zero (among other integers) if
// the callee is doing array processing, so we pass anything not
// equalling null and undefined, even the poor falsey zero.
if (name != null) {
results[name] = Array.slice(arguments);
}
if (--count === 0) {
setZeroTimeout(function() {
deferred.resolve(results);
});
}
};
}
var unlatch = defer();
return {
defer: defer,
then: function () {
var ret = deferred.promise.then.apply(deferred.promise, arguments);
if (!ready) {
ready = true;
unlatch();
}
return ret;
}
};
};
/**
* Given the results object from an allback.latch() where named callbacks were
* used (or else we won't save the result!) and the callbacks use the form of
* callback(errIfAny, ...), find and return the first error object, or return
* null if none was found.
*
* Important notes:
* - Use this for callback-based idioms in the node style
* - You MUST use latch.defer(name), not latch.defer()!
* - Because of JS object property ordering, we actually will return the result
* of the first callback that fired with an error value, but you probably
* do not want to be depending on this too much.
*/
exports.extractErrFromCallbackArgs = function(results) {
// If there are any errors, find and propagate.
var anyErr = null;
for (var key in results) {
var args = results[key];
var errIfAny = args[0];
if (errIfAny) {
anyErr = errIfAny;
break;
}
}
return anyErr;
};
exports.latchedWithRejections = function(namedPromises) {
return new Promise(function(resolve, reject) {
// Avoid Object.prototype
var results = Object.create(null);
var pending = 0;
Object.keys(namedPromises).forEach(function(name) {
pending++;
var promise = namedPromises[name];
promise.then(
function(result) {
results[name] = { resolved: true, value: result };
if (--pending === 0) {
resolve(results);
}
},
function(err) {
results[name] = { resolved: false, value: err };
if (--pending === 0) {
resolve(results);
}
});
});
// If we didn't end up scheduling anything
if (!pending) {
resolve(results);
}
});
};
}); // end define