synchronize
Version:
Turns asynchronous function into synchronous
353 lines (317 loc) • 11.7 kB
JavaScript
/*jshint node: true, indent:2, loopfunc: true, asi: true, undef:true*/
var Fiber = require('fibers')
// Takes function and returns its synchronized version, it's still backward compatible and
// can be used as asynchronous `syncFn = sync(asyncFn)`.
//
// Or You can provide object and it will synchronize its functions `sync(obj, fname1, fname2, ...)`.
//
// New synchronized version of function is backward compatible and You may call it as usual with
// explicit callback or inside of `fiber` with `aware` and `defer` keywords.
var sync = module.exports = function(){
if(arguments.length > 1){
// Synchronizing functions of object.
var obj = arguments[0]
for(var i = 1; i < arguments.length; i++){
var fname = arguments[i]
var fn = obj[fname]
if(!fn) throw new Error("object doesn't have '" + fname + "' function!")
obj[fname] = sync(fn)
}
}else{
return sync.syncFn(arguments[0])
}
}
var nextTick = global.setImmediate || process.nextTick || function(cb) { setTimeout(cb, 0) }
// Sometimes `Fiber` needed outside of `sync`.
sync.Fiber = Fiber
// Takes function and returns its synchronized version, it's still backward compatible and can
// be used as asynchronous.
sync.syncFn = function(fn){
// Preventing function to be synchronized twice.
if(fn._synchronized) return fn
var syncFn = function(){
// Using fibers only if there's active fiber and callback not provided explicitly.
if(Fiber.current && (typeof arguments[arguments.length-1] !== 'function')){
// Calling asynchronous function with our special fiber-aware callback.
Array.prototype.push.call(arguments, sync.defer())
fn.apply(this, arguments)
// Waiting for asynchronous result.
return sync.await()
}else{
// If there's no active fiber or callback provided explicitly we call original version.
return fn.apply(this, arguments)
}
}
// Marking function as synchronized.
syncFn._synchronized = true
return syncFn
}
// Use it to wait for asynchronous callback.
sync.await = Fiber.yield
// Creates fiber-aware asynchronous callback resuming current fiber when it will be finished.
sync.defer = function(){
if(!Fiber.current) throw new Error("no current Fiber, defer can't be used without Fiber!")
if(Fiber.current._syncParallel) return sync.deferParallel()
else return sync.deferSerial()
}
// Exactly the same as defer, but additionally it triggers an error if there's no response
// on time.
sync.deferWithTimeout = function(timeout, message){
if(!Fiber.current) throw new Error("no current Fiber, deferWithTimeout can't be used without Fiber!")
if(!timeout) throw new Error("no timeout provided!")
if(Fiber.current._syncParallel) throw new Error("deferWithTimeout can't be used in parallel!")
var defer = this.defer()
var error = new Error(message || "defer timed out!")
var called = false
var d = setTimeout(function(){
if(called) return
called = true
defer(error)
}, timeout)
return function(){
if(called) return
called = true
clearTimeout(d)
return defer.apply(this, arguments)
}
}
//
sync.deferSerial = function(){
var fiber = Fiber.current
if(!fiber) throw new Error("no current Fiber, defer can't be used without Fiber!")
// if(fiber._defered) throw new Error("invalid usage, should be clear previous defer!")
// fiber._defered = true
// Prevent recursive call
var called = false
// Returning asynchronous callback.
return function(err, result){
if (called) throw new Error("defer can't be used twice!")
called = true
// Wrapping in nextTick as a safe measure against not asynchronous usage.
nextTick(function(){
// fiber._defered = false
if(fiber._syncIsTerminated) return
if(err){
// Resuming fiber and throwing error.
fiber.throwInto(err)
}else{
// Resuming fiber and returning result.
fiber.run(result)
}
})
}
}
sync.deferParallel = function(){
var fiber = Fiber.current
if(!fiber) throw new Error("no current Fiber, defer can't be used without Fiber!")
if(!fiber._syncParallel) throw new Error("invalid usage, should be called in parallel mode!")
var data = fiber._syncParallel
// Counting amount of `defer` calls.
data.called += 1
var resultIndex = data.called - 1
// Returning asynchronous callback.
return function(err, result){
// Wrapping in nextTick as a safe measure against not asynchronous usage.
nextTick(function(){
if(fiber._syncIsTerminated) return
// Error in any of parallel call will result in aborting all of the calls.
if(data.errorHasBeenThrown) return
if(err){
data.errorHasBeenThrown = true
// Resuming fiber and throwing error.
fiber.throwInto(err)
}else{
data.returned += 1
data.results[resultIndex] = result
// Resuming fiber and returning result when all callbacks finished.
if(data.returned == data.called) fiber.run(data.results)
}
})
}
}
sync.defers = function(){
if(!Fiber.current) throw new Error("no current Fiber, defer can't be used without Fiber!")
if(Fiber.current._syncParallel) return sync.defersParallel.apply(sync, arguments)
else return sync.defersSerial.apply(sync, arguments)
}
sync.defersSerial = function(){
var fiber = Fiber.current;
if(!fiber) throw new Error("no current Fiber, defer can't be used without Fiber!")
// if(fiber._defered) throw new Error("invalid usage, should be clear previous defer!")
// fiber._defered = true
var kwds = Array.prototype.slice.call(arguments)
// Prevent recursive call
var called = false
// Returning asynchronous callback.
return function(err) {
if (called) throw new Error("defer can't be used twice!")
called = true
// Wrapping in nextTick as a safe measure against not asynchronous usage.
var args = Array.prototype.slice.call(arguments, 1)
nextTick(function(){
// fiber._defered = false
if(fiber._syncIsTerminated) return
if (err) {
// Resuming fiber and throwing error.
fiber.throwInto(err)
} else {
var results;
if(!kwds.length){
results = args
} else {
results = {}
kwds.forEach(function(kwd, i){
results[kwd]=args[i]
})
}
fiber.run(results)
}
})
}
}
sync.defersParallel = function(){
var fiber = Fiber.current
if(!fiber) throw new Error("no current Fiber, defer can't be used without Fiber!")
if(!fiber._syncParallel) throw new Error("invalid usage, should be called in parallel mode!")
var data = fiber._syncParallel
// Counting amount of `defer` calls.
data.called += 1
var resultIndex = data.called - 1
var kwds = Array.prototype.slice.call(arguments)
// Returning asynchronous callback.
return function(err) {
// Wrapping in nextTick as a safe measure against not asynchronous usage.
var args = Array.prototype.slice.call(arguments, 1)
nextTick(function(){
if(fiber._syncIsTerminated) return
// Error in any of parallel call will result in aborting all of the calls.
if(data.errorHasBeenThrown) return
if (err) {
data.errorHasBeenThrown = true
// Resuming fiber and throwing error.
fiber.throwInto(err)
}
var results;
if(!kwds.length){
results = args
} else {
results = {}
kwds.forEach(function(kwd, i){
results[kwd]=args[i]
})
}
data.returned += 1
data.results[resultIndex] = results
if(data.returned == data.called) fiber.run(data.results)
})
}
}
// Support for parallel calls, all `defer` calls within callback will be
// performed in parallel.
sync.parallel = function(cb){
var fiber = Fiber.current
if(!fiber) throw new Error("no current Fiber, defer can't be used without Fiber!")
// Enabling `defer` calls to be performed in parallel.
// There's an important note - error in any parallel call will result in aborting
// all of the parallel calls.
fiber._syncParallel = {called: 0, returned: 0, results: [], errorHasBeenThrown: false}
try{
cb.call(this)
}finally{
// If neither `defer` nor `defers` were called within the callback, we need
// to run the fiber ourselves or else the fiber will unwind when `await` is called.
// This might happen if the user intended to enumerate an array within the
// callback, calling `defer` once per array item, but the array was empty.
if (!fiber._syncParallel.called) {
nextTick(function(){
// Return an empty array to represent that there were no results.
if(!fiber._syncIsTerminated) fiber.run([])
})
}
delete fiber._syncParallel
}
}
// Executes `cb` within `Fiber`, when it finish it will call `done` callback.
// If error will be thrown during execution, this error will be catched and passed to `done`,
// if `done` not provided it will be just rethrown.
sync.fiber = function(cb, done){
var that = this
var fiber = Fiber(function(){
// Prevent restart fiber
if (Fiber.current._started) return
if (done) {
var result
try {
result = cb.call(that)
Fiber.current._syncIsTerminated = true
} catch (error){
return done(error)
}
done(null, result)
} else {
// Don't catch errors if done not provided!
cb.call(that)
Fiber.current._syncIsTerminated = true
}
})
fiber.run()
fiber._started = true
}
// Asynchronous wrapper for mocha.js tests.
//
// async = sync.asyncIt
// it('should pass', async(function(){
// ...
// }))
//
sync.asyncIt = function(cb){
if(!cb) throw "no callback for async spec helper!"
return function(done){sync.fiber(cb.bind(this), done)}
}
// Same as `sync` but with verbose logging for every method invocation.
// Ignore this method, it shouldn't be used unless you want to track down
// tricky and complex bugs and need full information abouth how and when all
// this async stuff has been called.
var fiberIdCounter = 1
sync.syncWithDebug = function(){
if(arguments.length > 1){
// Synchronizing functions of object.
var obj = arguments[0]
for(var i = 1; i < arguments.length; i++){
(function(fname){
var fn = obj[fname]
if(!fn) throw new Error("object doesn't have '" + fname + "' function!")
var syncedFn = sync(fn)
obj[fname] = function(){
if(Fiber.current && Fiber.current._fiberId === undefined){
Fiber.current._fiberId = fiberIdCounter
fiberIdCounter = fiberIdCounter + 1
Fiber.current._callbackLevel = 0
}
var fiberId = '-'
if(Fiber.current){
fiberId = Fiber.current._fiberId
Fiber.current._callbackLevel = Fiber.current._callbackLevel + 1
}
var indent = ' '
if(Fiber.current)
for(var j = 0; j < Fiber.current._callbackLevel; j++) indent = indent + ' '
console.log(fiberId + indent + this.constructor.name + '.' +
fname + " called", JSON.stringify(arguments))
var result
try{
result = syncedFn.apply(this, arguments)
}finally{
console.log(fiberId + indent + this.constructor.name + '.' +
fname + " finished")
if(Fiber.current)
Fiber.current._callbackLevel = Fiber.current._callbackLevel - 1
}
return result
}
})(arguments[i])
}
}else{
return sync.syncFn(arguments[0])
}
}