2016-07-20 01:09:27 +03:00
|
|
|
// Yet Another Hack to fight node.js callback hell: generator-based coroutines
|
2016-07-20 13:54:51 +03:00
|
|
|
// Distinctive features:
|
2016-07-23 01:56:08 +03:00
|
|
|
// - simple to use: does not require promisification of existing callback-based code
|
2016-07-21 19:11:05 +03:00
|
|
|
// - safely checks control flow
|
2016-07-20 01:09:27 +03:00
|
|
|
|
2016-06-27 16:03:06 +03:00
|
|
|
module.exports.run = runThread;
|
|
|
|
module.exports.runParallel = runParallel;
|
|
|
|
|
2016-07-23 01:56:08 +03:00
|
|
|
var current;
|
|
|
|
|
|
|
|
module.exports.unsafe = function()
|
|
|
|
{
|
|
|
|
return current;
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports.callback = module.exports.cb = function()
|
|
|
|
{
|
|
|
|
return threadCallback.call(current);
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports.errorfirst = module.exports.ef = function()
|
|
|
|
{
|
|
|
|
return errorFirst.call(current);
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports.throttle = function(count)
|
|
|
|
{
|
|
|
|
return throttleThread.call(current, count);
|
|
|
|
};
|
|
|
|
|
|
|
|
function runThread(generator, onsuccess, onerror)
|
2016-07-20 01:09:27 +03:00
|
|
|
{
|
2016-08-16 15:14:44 +03:00
|
|
|
var thread = function()
|
|
|
|
{
|
|
|
|
thread._current = null;
|
|
|
|
continueThread.apply(thread, arguments);
|
|
|
|
};
|
2016-07-23 01:56:08 +03:00
|
|
|
thread._gen = generator.next ? generator : generator();
|
2016-07-20 01:09:27 +03:00
|
|
|
thread._finishThrottleQueue = finishThrottleQueue.bind(thread);
|
2016-07-23 01:56:08 +03:00
|
|
|
thread._onsuccess = onsuccess;
|
|
|
|
thread._onerror = onerror;
|
2016-08-16 15:14:44 +03:00
|
|
|
thread._running = false;
|
2016-07-25 14:38:04 +03:00
|
|
|
callGen(thread, 'next', []);
|
2016-07-20 01:09:27 +03:00
|
|
|
return thread;
|
|
|
|
}
|
2016-07-17 00:35:35 +03:00
|
|
|
|
2016-07-20 01:09:27 +03:00
|
|
|
function continueThread()
|
2016-07-17 00:35:35 +03:00
|
|
|
{
|
2016-07-20 01:09:27 +03:00
|
|
|
// pass parameters as yield result
|
2016-07-21 19:11:05 +03:00
|
|
|
callGen(this, 'next', Array.prototype.slice.call(arguments, 0));
|
|
|
|
}
|
|
|
|
|
2016-07-25 01:00:07 +03:00
|
|
|
function getStack(fn)
|
|
|
|
{
|
|
|
|
return fn._stack.replace(/Error[\s\S]*at.*(exports\.(cb|ef|errorfirst)|Function\.(errorFirst|threadCallback)).*/, '');
|
|
|
|
}
|
|
|
|
|
2016-07-21 19:11:05 +03:00
|
|
|
function callGen(thread, method, arg)
|
|
|
|
{
|
2016-08-16 15:14:44 +03:00
|
|
|
if (thread._running)
|
|
|
|
{
|
|
|
|
// callback called while generator is already running
|
|
|
|
thread._result = [ method, arg ];
|
|
|
|
return;
|
|
|
|
}
|
2016-07-20 01:09:27 +03:00
|
|
|
var v;
|
2016-08-16 15:14:44 +03:00
|
|
|
thread._running = true;
|
2016-07-23 01:56:08 +03:00
|
|
|
current = thread;
|
2016-07-20 01:09:27 +03:00
|
|
|
try
|
2016-07-17 00:35:35 +03:00
|
|
|
{
|
2016-08-16 15:14:44 +03:00
|
|
|
while (1)
|
|
|
|
{
|
|
|
|
v = thread._gen[method](arg);
|
|
|
|
if (!v.done && thread._result)
|
|
|
|
{
|
|
|
|
method = thread._result[0];
|
|
|
|
arg = thread._result[1];
|
|
|
|
thread._result = null;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
2016-07-20 01:09:27 +03:00
|
|
|
}
|
|
|
|
catch (e)
|
|
|
|
{
|
2016-07-23 01:56:08 +03:00
|
|
|
v = { error: e };
|
2016-07-20 01:09:27 +03:00
|
|
|
}
|
2016-08-16 15:14:44 +03:00
|
|
|
thread._running = false;
|
2016-07-23 01:56:08 +03:00
|
|
|
current = null;
|
|
|
|
if (v.done || v.error)
|
2016-07-20 01:09:27 +03:00
|
|
|
{
|
|
|
|
// generator finished
|
2016-07-21 19:11:05 +03:00
|
|
|
thread._done = true;
|
|
|
|
process.nextTick(thread._finishThrottleQueue);
|
2016-07-17 00:35:35 +03:00
|
|
|
}
|
2016-07-20 01:09:27 +03:00
|
|
|
if (v.error)
|
2016-07-23 01:56:08 +03:00
|
|
|
{
|
|
|
|
if (thread._onerror)
|
|
|
|
thread._onerror(v.error);
|
|
|
|
else
|
|
|
|
throw v.error;
|
|
|
|
}
|
|
|
|
else if (v.done && thread._onsuccess)
|
|
|
|
thread._onsuccess(v.value);
|
|
|
|
else if (typeof v.value == 'object' && v.value.then)
|
2016-07-21 19:11:05 +03:00
|
|
|
{
|
|
|
|
// check if v.value is a Promise
|
2016-07-23 01:56:08 +03:00
|
|
|
var cb = threadCallback.call(current);
|
2016-07-21 19:11:05 +03:00
|
|
|
v.value.then(cb, function(error)
|
|
|
|
{
|
|
|
|
callGen(thread, 'throw', error);
|
|
|
|
});
|
|
|
|
}
|
2016-07-17 00:35:35 +03:00
|
|
|
}
|
|
|
|
|
2016-07-20 01:09:27 +03:00
|
|
|
function threadCallback()
|
2016-06-27 16:03:06 +03:00
|
|
|
{
|
2016-07-20 01:09:27 +03:00
|
|
|
var thread = this;
|
|
|
|
var fn = function()
|
2016-06-27 16:03:06 +03:00
|
|
|
{
|
2016-07-20 01:09:27 +03:00
|
|
|
if (thread._current != fn)
|
2016-07-19 13:38:35 +03:00
|
|
|
{
|
2016-07-20 01:09:27 +03:00
|
|
|
throw new Error('Broken control flow! Callback'+
|
2016-07-25 01:00:07 +03:00
|
|
|
getStack(thread._current)+
|
2016-07-20 01:09:27 +03:00
|
|
|
'\nmust be called to resume thread, but this one is called instead:'+
|
2016-07-25 01:00:07 +03:00
|
|
|
getStack(fn)+'\n--'
|
2016-07-20 01:09:27 +03:00
|
|
|
);
|
2016-07-17 00:35:35 +03:00
|
|
|
}
|
2016-07-22 14:46:44 +03:00
|
|
|
return callGen(thread, 'next', Array.prototype.slice.call(arguments, 0));
|
2016-06-27 16:03:06 +03:00
|
|
|
};
|
2016-07-20 01:09:27 +03:00
|
|
|
fn._stack = new Error().stack;
|
|
|
|
thread._current = fn;
|
|
|
|
return fn;
|
|
|
|
}
|
|
|
|
|
2016-07-21 19:11:05 +03:00
|
|
|
function errorFirst()
|
|
|
|
{
|
|
|
|
var thread = this;
|
|
|
|
var fn = function()
|
|
|
|
{
|
|
|
|
if (thread._current != fn)
|
|
|
|
{
|
|
|
|
throw new Error('Broken control flow! Callback'+
|
2016-07-25 01:00:07 +03:00
|
|
|
getStack(thread._current)+
|
2016-07-21 19:11:05 +03:00
|
|
|
'\nmust be called to resume thread, but this one is called instead:'+
|
2016-07-25 01:00:07 +03:00
|
|
|
getStack(fn._stack)+'\n--'
|
2016-07-21 19:11:05 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
if (arguments[0])
|
2016-07-25 01:00:07 +03:00
|
|
|
{
|
|
|
|
var e = arguments[0];
|
|
|
|
var m = /^([\s\S]*?)((\n\s*at.*)*)$/.exec(e.stack);
|
|
|
|
if (m)
|
|
|
|
e.stack = m[1]+getStack(thread._current)+'\n-- async error thrown at:'+m[2];
|
|
|
|
return callGen(thread, 'throw', e);
|
|
|
|
}
|
2016-07-22 14:46:44 +03:00
|
|
|
return callGen(thread, 'next', Array.prototype.slice.call(arguments, 1));
|
2016-07-21 19:11:05 +03:00
|
|
|
};
|
|
|
|
fn._stack = new Error().stack;
|
|
|
|
thread._current = fn;
|
|
|
|
return fn;
|
|
|
|
}
|
|
|
|
|
2016-07-20 01:09:27 +03:00
|
|
|
function throttleThread(count)
|
|
|
|
{
|
|
|
|
if (!this.throttleData)
|
2016-07-20 13:54:51 +03:00
|
|
|
this.throttleData = this._gen.__proto__._genThreadThrottle = this._gen.__proto__._genThreadThrottle || { queue: [], pending: [] };
|
2016-07-20 01:09:27 +03:00
|
|
|
this._finishThrottleQueue();
|
|
|
|
if (this.throttleData.queue.length < count)
|
2016-07-17 00:35:35 +03:00
|
|
|
{
|
2016-07-20 01:09:27 +03:00
|
|
|
this.throttleData.queue.push(this);
|
2016-07-23 01:56:08 +03:00
|
|
|
process.nextTick(threadCallback.call(this));
|
2016-07-20 01:09:27 +03:00
|
|
|
}
|
|
|
|
else
|
2016-07-23 01:56:08 +03:00
|
|
|
this.throttleData.pending.push([ this, threadCallback.call(this), count ]);
|
2016-07-20 01:09:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function finishThrottleQueue()
|
|
|
|
{
|
|
|
|
if (!this.throttleData)
|
|
|
|
return;
|
|
|
|
for (var i = 0; i < this.throttleData.queue.length; i++)
|
|
|
|
{
|
|
|
|
if (this.throttleData.queue[i]._done)
|
2016-07-17 00:35:35 +03:00
|
|
|
{
|
2016-07-20 01:09:27 +03:00
|
|
|
this.throttleData.queue.splice(i, 1);
|
|
|
|
i--;
|
2016-07-17 00:35:35 +03:00
|
|
|
}
|
2016-07-20 01:09:27 +03:00
|
|
|
}
|
|
|
|
while (this.throttleData.pending.length > 0 && this.throttleData.queue.length < this.throttleData.pending[0][2])
|
2016-07-17 21:30:32 +03:00
|
|
|
{
|
2016-07-20 01:09:27 +03:00
|
|
|
var t = this.throttleData.pending.shift();
|
|
|
|
this.throttleData.queue.push(t[0]);
|
|
|
|
process.nextTick(t[1]);
|
|
|
|
}
|
2016-06-27 16:03:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function runParallel(threads, done)
|
|
|
|
{
|
|
|
|
var results = [];
|
2016-07-23 01:56:08 +03:00
|
|
|
var errors = [];
|
2016-06-27 16:03:06 +03:00
|
|
|
var resultCount = 0;
|
2016-07-23 01:56:08 +03:00
|
|
|
var allDone = function(i, result, error)
|
2016-06-27 16:03:06 +03:00
|
|
|
{
|
2016-07-23 01:56:08 +03:00
|
|
|
if (!results[i] && !errors[i])
|
2016-06-27 16:03:06 +03:00
|
|
|
{
|
2016-07-23 01:56:08 +03:00
|
|
|
if (error)
|
|
|
|
errors[i] = error;
|
|
|
|
else
|
|
|
|
results[i] = result;
|
2016-06-27 16:03:06 +03:00
|
|
|
resultCount++;
|
|
|
|
if (resultCount == threads.length)
|
2016-07-23 01:56:08 +03:00
|
|
|
done(results, errors);
|
2016-06-27 16:03:06 +03:00
|
|
|
}
|
|
|
|
};
|
2016-07-23 01:56:08 +03:00
|
|
|
threads.map((t, i) => runThread(t, function(result) { allDone(i, result); }, function(error) { allDone(i, null, error); }));
|
2016-06-27 16:03:06 +03:00
|
|
|
}
|