Skip to content

Commit 1b258fb

Browse files
committed
Merge pull request caolan#769 from caolan/ensure_async
ensureAsync
2 parents f71193a + 7c7326b commit 1b258fb

File tree

5 files changed

+198
-22
lines changed

5 files changed

+198
-22
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ Usage:
174174

175175
* [`memoize`](#memoize)
176176
* [`unmemoize`](#unmemoize)
177+
* [`ensureAsync`](#ensureAsync)
177178
* [`log`](#log)
178179
* [`dir`](#dir)
179180
* [`noConflict`](#noConflict)
@@ -1657,6 +1658,41 @@ __Arguments__
16571658

16581659
* `fn` - the memoized function
16591660

1661+
---------------------------------------
1662+
1663+
<a name="ensureAsync" />
1664+
### ensureAsync(fn)
1665+
1666+
Wrap an async function and ensure it calls its callback on a later tick of the event loop. If the function already calls its callback on a next tick, no extra deferral is added. This is useful for preventing stack overflows (`RangeError: Maximum call stack size exceeded`) and generally keeping [Zalgo](http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony) contained.
1667+
1668+
__Arguments__
1669+
1670+
* `fn` - an async function, one that expects a node-style callback as its last argument
1671+
1672+
Returns a wrapped function with the exact same call signature as the function passed in.
1673+
1674+
__Example__
1675+
1676+
```js
1677+
function sometimesAsync(arg, callback) {
1678+
if (cache[arg]) {
1679+
return callback(null, cache[arg]); // this would be synchronous!!
1680+
} else {
1681+
doSomeIO(arg, callback); // this IO would be asynchronous
1682+
}
1683+
}
1684+
1685+
// this has a risk of stack overflows if many results are cached in a row
1686+
async.mapSeries(args, sometimesAsync, done);
1687+
1688+
// this will defer sometimesAsync's callback if necessary,
1689+
// preventing stack overflows
1690+
async.mapSeries(args, async.ensureAsync(sometimesAsync), done);
1691+
1692+
```
1693+
1694+
---------------------------------------
1695+
16601696
<a name="log" />
16611697
### log(function, arguments)
16621698

lib/async.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,18 +1255,39 @@
12551255
async.applyEachSeries = doSeries(_applyEach);
12561256

12571257
async.forever = function (fn, callback) {
1258+
var done = only_once(callback || noop);
1259+
var task = ensureAsync(fn);
12581260
function next(err) {
12591261
if (err) {
1260-
if (callback) {
1261-
return callback(err);
1262-
}
1263-
throw err;
1262+
return done(err);
12641263
}
1265-
fn(next);
1264+
task(next);
12661265
}
12671266
next();
12681267
};
12691268

1269+
function ensureAsync(fn) {
1270+
return function (/*...args, callback*/) {
1271+
var args = _baseSlice(arguments);
1272+
var callback = args.pop();
1273+
args.push(function () {
1274+
var innerArgs = arguments;
1275+
if (sync) {
1276+
async.setImmediate(function () {
1277+
callback.apply(null, innerArgs);
1278+
});
1279+
} else {
1280+
callback.apply(null, innerArgs);
1281+
}
1282+
});
1283+
var sync = true;
1284+
fn.apply(this, args);
1285+
sync = false;
1286+
};
1287+
}
1288+
1289+
async.ensureAsync = ensureAsync;
1290+
12701291
// Node.js
12711292
if (typeof module !== 'undefined' && module.exports) {
12721293
module.exports = async;

perf/benchmark.js

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
var _ = require("lodash");
44
var Benchmark = require("benchmark");
5-
var benchOptions = {defer: true, minSamples: 1, maxTime: 2};
65
var exec = require("child_process").exec;
76
var fs = require("fs");
87
var path = require("path");
@@ -16,8 +15,11 @@ var args = require("yargs")
1615
.alias("g", "grep")
1716
.default("g", ".*")
1817
.describe("i", "skip benchmarks whose names match this regex")
19-
.alias("g", "reject")
18+
.alias("i", "reject")
2019
.default("i", "^$")
20+
.describe("l", "maximum running time per test (in seconds)")
21+
.alias("l", "limit")
22+
.default("l", 2)
2123
.help('h')
2224
.alias('h', 'help')
2325
.example('$0 0.9.2 0.9.0', 'Compare v0.9.2 with v0.9.0')
@@ -33,6 +35,7 @@ var reject = new RegExp(args.i, "i");
3335
var version0 = args._[0] || require("../package.json").version;
3436
var version1 = args._[1] || "current";
3537
var versionNames = [version0, version1];
38+
var benchOptions = {defer: true, minSamples: 1, maxTime: +args.l};
3639
var versions;
3740
var wins = {};
3841
var totalTime = {};
@@ -120,16 +123,30 @@ function doesNotMatch(suiteConfig) {
120123
function createSuite(suiteConfig) {
121124
var suite = new Benchmark.Suite();
122125
var args = suiteConfig.args;
126+
var errored = false;
123127

124128
function addBench(version, versionName) {
125129
var name = suiteConfig.name + " " + versionName;
130+
131+
try {
132+
suiteConfig.setup(1);
133+
suiteConfig.fn(version, function () {});
134+
} catch (e) {
135+
console.error(name + " Errored");
136+
errored = true;
137+
return;
138+
}
139+
126140
suite.add(name, function (deferred) {
127141
suiteConfig.fn(version, function () {
128142
deferred.resolve();
129143
});
130144
}, _.extend({
131145
versionName: versionName,
132-
setup: _.partial.apply(null, [suiteConfig.setup].concat(args))
146+
setup: _.partial.apply(null, [suiteConfig.setup].concat(args)),
147+
onError: function (err) {
148+
console.log(err.stack);
149+
}
133150
}, benchOptions));
134151
}
135152

@@ -139,18 +156,22 @@ function createSuite(suiteConfig) {
139156

140157
return suite.on('cycle', function(event) {
141158
var mean = event.target.stats.mean * 1000;
142-
console.log(event.target + ", " + (+mean.toPrecision(2)) + "ms per run");
159+
console.log(event.target + ", " + (+mean.toPrecision(3)) + "ms per run");
143160
var version = event.target.options.versionName;
161+
if (errored) return;
144162
totalTime[version] += mean;
145163
})
164+
.on('error', function (err) { console.error(err); })
146165
.on('complete', function() {
147-
var fastest = this.filter('fastest');
148-
if (fastest.length === 2) {
149-
console.log("Tie");
150-
} else {
151-
var winner = fastest[0].options.versionName;
152-
console.log(winner + ' is faster');
153-
wins[winner]++;
166+
if (!errored) {
167+
var fastest = this.filter('fastest');
168+
if (fastest.length === 2) {
169+
console.log("Tie");
170+
} else {
171+
var winner = fastest[0].options.versionName;
172+
console.log(winner + ' is faster');
173+
wins[winner]++;
174+
}
154175
}
155176
console.log("--------------------------------------");
156177
});

perf/suites.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,30 @@ module.exports = [
204204
fn: function (async, done) {
205205
setTimeout(done, 0);
206206
}
207+
},
208+
{
209+
name: "ensureAsync sync",
210+
fn: function (async, done) {
211+
async.ensureAsync(function (cb) {
212+
cb();
213+
})(done);
214+
}
215+
},
216+
{
217+
name: "ensureAsync async",
218+
fn: function (async, done) {
219+
async.ensureAsync(function (cb) {
220+
setImmediate(cb);
221+
})(done);
222+
}
223+
},
224+
{
225+
name: "ensureAsync async noWrap",
226+
fn: function (async, done) {
227+
(function (cb) {
228+
setImmediate(cb);
229+
}(done));
230+
}
207231
}
208232
];
209233

test/test-async.js

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@ function getFunctionsObject(call_order) {
8080
};
8181
}
8282

83-
exports['forever'] = function (test) {
84-
test.expect(1);
83+
exports['forever'] = {
84+
85+
'async': function (test) {
86+
test.expect(2);
8587
var counter = 0;
8688
function addOne(callback) {
8789
counter++;
@@ -94,8 +96,28 @@ exports['forever'] = function (test) {
9496
}
9597
async.forever(addOne, function (err) {
9698
test.equal(err, 'too big!');
99+
test.equal(counter, 50);
100+
test.done();
101+
});
102+
},
103+
104+
'sync': function (test) {
105+
test.expect(2);
106+
var counter = 0;
107+
function addOne(callback) {
108+
counter++;
109+
if (counter === 50000) {
110+
return callback('too big!');
111+
}
112+
callback();
113+
}
114+
async.forever(addOne, function (err) {
115+
test.equal(err, 'too big!');
116+
test.equal(counter, 50000);
97117
test.done();
98118
});
119+
}
120+
99121
};
100122

101123
exports['applyEach'] = function (test) {
@@ -1030,12 +1052,12 @@ exports['parallel does not continue replenishing after error'] = function (test)
10301052
}
10311053
setTimeout(function(){
10321054
callback();
1033-
}, delay);
1055+
}, delay);
10341056
}
10351057

10361058
async.parallelLimit(arr, limit, function(x, callback) {
10371059

1038-
}, function(err){});
1060+
}, function(err){});
10391061

10401062
setTimeout(function(){
10411063
test.equal(started, 3);
@@ -1438,7 +1460,7 @@ exports['eachLimit does not continue replenishing after error'] = function (test
14381460
setTimeout(function(){
14391461
callback();
14401462
}, delay);
1441-
}, function(err){});
1463+
}, function(err){});
14421464

14431465
setTimeout(function(){
14441466
test.equal(started, 3);
@@ -1743,7 +1765,7 @@ exports['mapLimit does not continue replenishing after error'] = function (test)
17431765
setTimeout(function(){
17441766
callback();
17451767
}, delay);
1746-
}, function(err){});
1768+
}, function(err){});
17471769

17481770
setTimeout(function(){
17491771
test.equal(started, 3);
@@ -3561,3 +3583,55 @@ exports['queue started'] = function(test) {
35613583

35623584
};
35633585

3586+
exports['ensureAsync'] = {
3587+
'defer sync functions': function (test) {
3588+
var sync = true;
3589+
async.ensureAsync(function (arg1, arg2, cb) {
3590+
test.equal(arg1, 1);
3591+
test.equal(arg2, 2);
3592+
cb(null, 4, 5);
3593+
})(1, 2, function (err, arg4, arg5) {
3594+
test.equal(err, null);
3595+
test.equal(arg4, 4);
3596+
test.equal(arg5, 5);
3597+
test.ok(!sync, 'callback called on same tick');
3598+
test.done();
3599+
});
3600+
sync = false;
3601+
},
3602+
3603+
'do not defer async functions': function (test) {
3604+
var sync = false;
3605+
async.ensureAsync(function (arg1, arg2, cb) {
3606+
test.equal(arg1, 1);
3607+
test.equal(arg2, 2);
3608+
async.setImmediate(function () {
3609+
sync = true;
3610+
cb(null, 4, 5);
3611+
sync = false;
3612+
});
3613+
})(1, 2, function (err, arg4, arg5) {
3614+
test.equal(err, null);
3615+
test.equal(arg4, 4);
3616+
test.equal(arg5, 5);
3617+
test.ok(sync, 'callback called on next tick');
3618+
test.done();
3619+
});
3620+
},
3621+
3622+
'double wrapping': function (test) {
3623+
var sync = true;
3624+
async.ensureAsync(async.ensureAsync(function (arg1, arg2, cb) {
3625+
test.equal(arg1, 1);
3626+
test.equal(arg2, 2);
3627+
cb(null, 4, 5);
3628+
}))(1, 2, function (err, arg4, arg5) {
3629+
test.equal(err, null);
3630+
test.equal(arg4, 4);
3631+
test.equal(arg5, 5);
3632+
test.ok(!sync, 'callback called on same tick');
3633+
test.done();
3634+
});
3635+
sync = false;
3636+
}
3637+
};

0 commit comments

Comments
 (0)