gen-thread/index.js

152 lines
4.2 KiB
JavaScript
Raw Normal View History

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:
// - simple to use: does not require modifications of existing callback or promise based code
// - 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-20 13:54:51 +03:00
function runThread(generator, arg, finishCallback)
2016-07-20 01:09:27 +03:00
{
var thread = function() { continueThread.apply(thread) };
thread.throttle = throttleThread;
thread.cb = threadCallback.bind(thread);
thread.errorfirst = errorFirst.bind(thread);
2016-07-20 13:54:51 +03:00
thread._gen = generator(thread, arg);
2016-07-20 01:09:27 +03:00
thread._finishThrottleQueue = finishThrottleQueue.bind(thread);
2016-07-20 13:54:51 +03:00
thread._finishCallback = finishCallback;
2016-07-20 01:09:27 +03:00
thread();
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
callGen(this, 'next', Array.prototype.slice.call(arguments, 0));
}
function callGen(thread, method, arg)
{
2016-07-20 01:09:27 +03:00
var v;
try
2016-07-17 00:35:35 +03:00
{
v = thread._gen[method](arg);
2016-07-20 01:09:27 +03:00
}
catch (e)
{
v = { done: 1, error: e };
}
if (v.done)
{
// generator finished
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)
throw v.error;
if (v.done && thread._finishCallback)
thread._finishCallback(v.value);
if (typeof v.value == 'object' && v.value.then)
{
// check if v.value is a Promise
var cb = thread.cb();
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'+
thread._current._stack.replace(/^\s*Error\s*at Function\.thread\.cb\s*\([^)]*\)/, '')+
'\nmust be called to resume thread, but this one is called instead:'+
fn._stack.replace(/^\s*Error\s*at Function\.thread\.cb\s*\([^)]*\)/, '')+'\n--'
);
2016-07-17 00:35:35 +03:00
}
2016-07-20 01:09:27 +03:00
return thread.apply(thread, arguments);
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;
}
function errorFirst()
{
var thread = this;
var fn = function()
{
if (thread._current != fn)
{
throw new Error('Broken control flow! Callback'+
thread._current._stack.replace(/^\s*Error\s*at Function\.thread\.cb\s*\([^)]*\)/, '')+
'\nmust be called to resume thread, but this one is called instead:'+
fn._stack.replace(/^\s*Error\s*at Function\.thread\.cb\s*\([^)]*\)/, '')+'\n--'
);
}
if (arguments[0])
return callGen(thread, 'throw', arguments[0]);
return callGen(thread, Array.prototype.slice.call(arguments, 0));
};
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);
process.nextTick(this.cb());
}
else
this.throttleData.pending.push([ this, this.cb(), count ]);
}
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-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 = [];
var resultCount = 0;
var allDone = function(i, result)
{
if (!results[i])
{
results[i] = result;
resultCount++;
if (resultCount == threads.length)
done(results);
}
};
threads.map((t, i) => runThread(t, null, function(result) { allDone(i, result); }));
}