Skip to content

Commit 8085922

Browse files
committed
Make map and filter more concurrent
Makes Promise.map, Promise.filter, .filter and .map more concurrent. Previously they waited for the entire input array to be fulfilled before starting to process it. The callback order is still the same as before, e.g. if 4th promise in the array fulfills while 2nd one is still pending, the callback for 4th value in the array is not called until 2nd and 3rd are fulfilled and had their callbacks executed.
1 parent e2c3412 commit 8085922

4 files changed

Lines changed: 174 additions & 86 deletions

File tree

Gruntfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ module.exports = function( grunt ) {
5353
"call_get.js": ['Promise'],
5454
"filter.js": ['Promise', 'Promise$_CreatePromiseArray', 'PromiseArray', 'apiRejection'],
5555
"generators.js": ['Promise', 'apiRejection', 'INTERNAL'],
56-
"map.js": ['Promise', 'Promise$_CreatePromiseArray', 'PromiseArray', 'apiRejection'],
56+
"map.js": ['Promise', 'PromiseArray', 'INTERNAL', 'apiRejection'],
5757
"nodeify.js": ['Promise'],
5858
"promisify.js": ['Promise', 'INTERNAL'],
5959
"props.js": ['Promise', 'PromiseArray'],

src/filter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = function(Promise) {
44
var isArray = require("./util.js").isArray;
55

66
function Promise$_filter(booleans) {
7-
var values = this._settledValue;
7+
var values = this instanceof Promise ? this._settledValue : this;
88
ASSERT(isArray(values));
99
ASSERT(isArray(booleans));
1010
ASSERT(values.length === booleans.length);

src/map.js

Lines changed: 131 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,151 @@
11
"use strict";
2-
module.exports = function(
3-
Promise, Promise$_CreatePromiseArray, PromiseArray, apiRejection) {
2+
module.exports = function(Promise, PromiseArray, INTERNAL, apiRejection) {
43

54
var ASSERT = require("./assert.js");
5+
var all = Promise.all;
6+
var util = require("./util.js");
7+
var canAttach = require("./errors.js").canAttach;
8+
var isArray = util.isArray;
9+
var _cast = Promise._cast;
610

7-
function Promise$_mapper(values) {
8-
var fn = this;
9-
var receiver = void 0;
10-
11-
if (typeof fn !== "function") {
12-
receiver = fn.receiver;
13-
fn = fn.fn;
14-
}
15-
ASSERT(typeof fn === "function");
16-
var shouldDefer = false;
17-
18-
var ret = new Array(values.length);
19-
20-
if (receiver === void 0) {
21-
for (var i = 0, len = values.length; i < len; ++i) {
22-
var value = fn(values[i], i, len);
23-
if (!shouldDefer) {
24-
var maybePromise = Promise._cast(value,
25-
Promise$_mapper, void 0);
26-
if (maybePromise instanceof Promise) {
27-
if (maybePromise.isFulfilled()) {
28-
ret[i] = maybePromise._settledValue;
29-
continue;
30-
}
31-
else {
32-
shouldDefer = true;
33-
}
34-
value = maybePromise;
35-
}
36-
}
37-
ret[i] = value;
38-
}
39-
}
40-
else {
41-
for (var i = 0, len = values.length; i < len; ++i) {
42-
var value = fn.call(receiver, values[i], i, len);
43-
if (!shouldDefer) {
44-
var maybePromise = Promise._cast(value,
45-
Promise$_mapper, void 0);
46-
if (maybePromise instanceof Promise) {
47-
if (maybePromise.isFulfilled()) {
48-
ret[i] = maybePromise._settledValue;
49-
continue;
50-
}
51-
else {
52-
shouldDefer = true;
53-
}
54-
value = maybePromise;
55-
}
56-
}
57-
ret[i] = value;
58-
}
59-
}
60-
return shouldDefer
61-
? Promise$_CreatePromiseArray(ret, PromiseArray,
62-
Promise$_mapper, void 0).promise()
63-
: ret;
11+
function unpack(values) {
12+
ASSERT(this.length === 4);
13+
return Promise$_Map(values, this[0], this[1], this[2], this[3]);
6414
}
6515

6616
function Promise$_Map(promises, fn, useBound, caller, ref) {
6717
if (typeof fn !== "function") {
6818
return apiRejection(NOT_FUNCTION_ERROR);
6919
}
7020

71-
if (useBound === USE_BOUND && promises._isBound()) {
72-
fn = {
73-
fn: fn,
74-
receiver: promises._boundTo
75-
};
21+
var receiver = void 0;
22+
if (useBound === USE_BOUND) {
23+
if (promises._isBound()) {
24+
receiver = promises._boundTo;
25+
}
26+
}
27+
else if (useBound !== DONT_USE_BOUND) {
28+
receiver = useBound;
7629
}
7730

78-
var ret = Promise$_CreatePromiseArray(
79-
promises,
80-
PromiseArray,
81-
caller,
82-
useBound === USE_BOUND && promises._isBound()
83-
? promises._boundTo
84-
: void 0
85-
).promise();
86-
87-
if (ref !== void 0) {
88-
ref.ref = ret;
31+
var shouldUnwrapItems = ref !== void 0;
32+
if (shouldUnwrapItems) ref.ref = promises;
33+
34+
if (promises instanceof Promise) {
35+
return promises._then(unpack, void 0, void 0,
36+
[fn, receiver, caller, ref], void 0, Promise$_Map);
37+
}
38+
else if (!isArray(promises)) {
39+
return apiRejection(COLLECTION_ERROR);
8940
}
9041

91-
return ret._then(
92-
Promise$_mapper,
93-
void 0,
94-
void 0,
95-
fn,
96-
void 0,
97-
caller
98-
);
42+
var promise = new Promise(INTERNAL);
43+
if (receiver !== void 0) promise._setBoundTo(receiver);
44+
promise._setTrace(caller, void 0);
45+
46+
var mapping = new Mapping(promise,
47+
fn,
48+
promises,
49+
receiver,
50+
shouldUnwrapItems);
51+
mapping.init();
52+
return promise;
9953
}
10054

55+
var pending = {};
56+
function Mapping(promise, callback, items, receiver, shouldUnwrapItems) {
57+
this.shouldUnwrapItems = shouldUnwrapItems;
58+
this.index = 0;
59+
this.items = items;
60+
this.callback = callback;
61+
this.receiver = receiver;
62+
this.promise = promise;
63+
this.result = new Array(items.length);
64+
}
65+
util.inherits(Mapping, PromiseArray);
66+
67+
Mapping.prototype.init = function Mapping$init() {
68+
var items = this.items;
69+
var len = items.length;
70+
var result = this.result;
71+
var isRejected = false;
72+
for (var i = 0; i < len; ++i) {
73+
var maybePromise = _cast(items[i], void 0, void 0);
74+
if (maybePromise instanceof Promise) {
75+
if (maybePromise.isPending()) {
76+
result[i] = pending;
77+
maybePromise._proxyPromiseArray(this, i);
78+
}
79+
else if (maybePromise.isFulfilled()) {
80+
result[i] = maybePromise.value();
81+
}
82+
else {
83+
maybePromise._unsetRejectionIsUnhandled();
84+
if (!isRejected) {
85+
this.reject(maybePromise.reason());
86+
isRejected = true;
87+
}
88+
}
89+
}
90+
else {
91+
result[i] = maybePromise;
92+
}
93+
}
94+
if (!isRejected) this.iterate();
95+
};
96+
97+
Mapping.prototype.isResolved = function Mapping$isResolved() {
98+
return this.promise === null;
99+
};
100+
101+
Mapping.prototype._promiseProgressed =
102+
function Mapping$_promiseProgressed(value) {
103+
if (this.isResolved()) return;
104+
this.promise._progress(value);
105+
};
106+
107+
Mapping.prototype._promiseFulfilled =
108+
function Mapping$_promiseFulfilled(value, index) {
109+
if (this.isResolved()) return;
110+
this.result[index] = value;
111+
if (this.shouldUnwrapItems) this.items[index] = value;
112+
if (this.index === index) this.iterate();
113+
};
114+
115+
Mapping.prototype._promiseRejected =
116+
function Mapping$_promiseRejected(reason) {
117+
this.reject(reason);
118+
};
119+
120+
Mapping.prototype.reject = function Mapping$reject(reason) {
121+
if (this.isResolved()) return;
122+
var trace = canAttach(reason) ? reason : new Error(reason + "");
123+
this.promise._attachExtraTrace(trace);
124+
this.promise._reject(reason, trace);
125+
};
126+
127+
Mapping.prototype.iterate = function Mapping$iterate() {
128+
var i = this.index;
129+
var items = this.items;
130+
var result = this.result;
131+
var len = items.length;
132+
var result = this.result;
133+
var receiver = this.receiver;
134+
var callback = this.callback;
135+
136+
for (; i < len; ++i) {
137+
var value = result[i];
138+
if (value === pending) {
139+
this.index = i;
140+
return;
141+
}
142+
try { result[i] = callback.call(receiver, value, i, len); }
143+
catch (e) { return this.reject(e); }
144+
}
145+
this.promise._follow(all(result));
146+
this.items = this.result = this.callback = this.promise = null;
147+
};
148+
101149
Promise.prototype.map = function Promise$map(fn, ref) {
102150
return Promise$_Map(this, fn, USE_BOUND, this.map, ref);
103151
};

test/mocha/when_map.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ describe("when.map-test", function () {
209209
}
210210
//This test didn't contain the mapper argument so I assume
211211
//when.js uses identity mapper in such cases.
212-
213212
//In bluebird it's illegal to call Promise.map without mapper function
214213
return when.map(input, identity).then(function () {
215214
assert(ncall === 6);
@@ -240,4 +239,45 @@ describe("when.map-test", function () {
240239

241240
});
242241

242+
function delay(val, ms) {
243+
return new when(function(resolve) {
244+
setTimeout(function() {
245+
resolve(val);
246+
}, ms);
247+
});
248+
}
249+
250+
specify("should be concurrent but process in-order", function(done) {
251+
var firstProcessed = false;
252+
var secondProcessed = false;
253+
var thirdProcessed = false;
254+
255+
var first = delay(1, 20);
256+
var second = delay(2, 100).then(function(){secondProcessed = true});
257+
var third = delay(3, 100).then(function(){thirdProcessed = true});
258+
259+
when.map([first, second, third], function(integer) {
260+
if (integer === 1) {
261+
return delay(0, 10).then(function() {
262+
assert(!secondProcessed);
263+
assert(!thirdProcessed);
264+
return delay(0, 10);
265+
}).then(function() {
266+
assert(!secondProcessed);
267+
assert(!thirdProcessed);
268+
return delay(0, 10);
269+
}).then(function(){
270+
firstProcessed = true;
271+
});
272+
}
273+
else {
274+
assert(firstProcessed);
275+
}
276+
}).then(function() {
277+
assert(firstProcessed);
278+
assert(secondProcessed);
279+
assert(thirdProcessed);
280+
done();
281+
});
282+
});
243283
});

0 commit comments

Comments
 (0)