breeze
Version:
Functional async flow control library
378 lines (316 loc) • 9.65 kB
JavaScript
// Constructor
function Breeze (method) {
this.steps = []
if (typeof method === 'function' || isPromise(method)) {
this.then(method)
}
}
// Run next step registered on system step list
Breeze.prototype.run = function () {
var args = this.args || []
var step = this.steps.shift()
var type = step[0]
if (this.skip && typeof this.skip === 'number') {
this.skip--
return this.check(true)
}
try {
this.running = true
if (this[type + 'Handler']) return this[type + 'Handler'](step, args)
return this.check(true)
} catch (err) {
return this.onUncaughtError(err)
}
}
// Set error on system object, invokes available error methods, short-circuit system
Breeze.prototype.onUncaughtError = function (err) {
this.err = err
if (this.onError) this.onError(err)
if (this.onDeferredError) this.onDeferredError(err)
return (this.steps = [] && this.check())
}
// Determine system state and invoke next step when possible
Breeze.prototype.check = function (pop, skip) {
this.steps = this.steps || []
if (pop && this.steps.length) {
return this.run()
}
if (pop && !this.steps.length) {
return (this.running = false)
}
if (this.steps.length && !this.running) {
return this.run()
}
}
// Add iteration step to the system
Breeze.prototype.each = function (iterable, iteratee, next) {
if (this.err) return this
this.steps.push(['each', iterable, iteratee, next])
this.check()
return this
}
// Iterate over each iterable and invoke iteratee for each item in iterables,
// on error break iteration and give error to system
// @private
Breeze.prototype.eachHandler = function (step, args) {
var iterables = step[1]
var iteratee = step[2]
var next = step[3]
var system = this
var index = 0
var count = 0
var lookup
var length
if (typeof iterables === 'function') {
iterables = iterables.apply(this.context, args)
}
if (isPlainObject(iterables)) {
lookup = iterables
iterables = Object.keys(iterables)
}
length = iterables.length
function iterableCallback (error, value) {
if (system.err) return
if (error) return system.onUncaughtError(error)
if (arguments.length > 1) {
if (lookup) {
lookup[iterables[count]] = value
} else {
iterables[count] = value
}
}
if (++count === length) {
if (typeof next === 'function') {
args.unshift(system.createStepCallback())
return next.apply(system.context || [], args)
}
return system.check(true)
}
}
for (; index < length; index++) {
setImmediate(
iteratee,
lookup ? lookup[iterables[index]] : iterables[index],
lookup ? iterables[index] : index,
iterableCallback
)
}
return null
}
// Add conditional step to the system
Breeze.prototype.if = Breeze.prototype.when = Breeze.prototype.maybe = function (arg, next) {
if (this.err) return this
this.steps.push(['when', arg, next])
this.check()
return this
}
// Add step to evaluate truthyness of first argument to invoke callback
Breeze.prototype.some = function (arg, next) {
if (this.err || this.hasNoneHappened) return this
this.hasSome = true
this.steps.push(['some', arg, next])
this.check()
return this
}
// When some has already been invoked short-circuit and continue, when check is truthy
// invoke the resolution method, otherwise continue.
// @private
Breeze.prototype.someHandler = Breeze.prototype.whenHandler = function (step, args) {
var resolution = step[2]
var check = step[1]
var type = step[0]
if (type === 'some' && this.hasSomeHappened) {
return this.check(true)
}
if ((typeof check === 'function' && check.apply(this.context, args)) || (typeof check !== 'function' && check)) {
if (type === 'when') this.hasWhenHappened = true
if (type === 'some') this.hasSomeHappened = true
step = resolution
args.unshift(this.createStepCallback())
return step.apply(this.context, args)
}
return this.check(true)
}
// Add step to handle the case of when no thruthy some statements occured
Breeze.prototype.none = function (next) {
if (this.err || this.hasNoneHappened) return this
if (!this.hasSome) {
throw new Error('Cannot add .none check before adding .some checks')
}
if (this.hasSome) {
this.hasNoneHappened = true
this.steps.push(['none', next])
this.check()
}
return this
}
// Determine whether a some step has passed, when none have passed invoke passed none callback.
// @private
Breeze.prototype.noneHandler = function (step, args) {
if (!this.hasSomeHappened) {
this.hasNoneHappened = true
step = step[1]
args.unshift(this.createStepCallback())
return step.apply(this.context, args)
}
return this.check(true)
}
// Add variable passing step to system to allow quick introduction of variables to system
Breeze.prototype.pass = function () {
if (this.err) return this
var step
step = cloneArguments(arguments)
step.unshift('pass')
this.steps.push(step)
this.check()
return this
}
// Determine step arguments and invoke first argument when it is a function, then concatenate results
// to the system's arguments list.
// @private
Breeze.prototype.passHandler = function (step, args) {
step.shift()
if (step.length === 1 && typeof step === 'function') step[0] = step[0]()
this.args = args
this.args = this.args.concat(step)
return this.check(true)
}
// Add generic thennable step to the system
Breeze.prototype.then = function (next) {
if (this.err) return this
this.steps.push(['then', next])
this.check()
return this
}
// Thennable logic handler
// @private
Breeze.prototype.thenHandler = function (step, args) {
args.unshift(this.createStepCallback())
return step[1].apply(this.context || this, args)
}
// Add error handler to system, invoked immediately when error exists.
Breeze.prototype.catch = function (next) {
if (this.err) {
next(this.err)
if (this.onDeferredError) {
this.onDeferredError(this.err)
}
return this
}
this.onError = next
this.check()
return this
}
// Generate deferred promise object for the current flow system
Breeze.prototype.promise = Breeze.prototype.deferred = function () {
var system = this
var deferred = {
then: function (next) {
if (system.err) return deferred
system.steps.push(['then', function () {
return next.apply(this, cloneArguments(arguments, 1))
}])
system.check()
return deferred
},
catch: function (next) {
if (system.err) return next(system.err)
system.onDeferredError = next
return deferred
}
}
return deferred
}
// Takes the next (optional) callback, and generates a callback to be passed as the first argument.
Breeze.prototype.createStepCallback = function () {
var system = this
function handler (err) {
if (err) return system.onUncaughtError(err)
system.args = cloneArguments(arguments, 1)
system.check(true)
}
function success () {
system.args = system.args.concat(cloneArguments(arguments))
system.args.unshift(null)
handler.apply(system.context, system.args)
}
return function (err) {
if (err === 'skip') {
system.skip = arguments[1] || 1
system.args = system.args ? system.args.slice(1) : []
return system.check(true)
}
system.context = this
system.args = cloneArguments(arguments, 1)
if (isPromise(err)) {
return typeof err.catch === 'function'
? err.then(success).catch(handler)
: err.then(success, handler)
}
return handler.apply(system.context, arguments)
}
}
// Resets system state to an empty state
Breeze.prototype.reset = function () {
this.hasMaybeHappened = null
this.hasNoneHappened = null
this.hasSomeHappened = null
this.hasSome = null
this.onError = null
this.running = null
this.err = null
this.args = null
this.context = null
this.steps = []
return this
}
// Debug garbage collection utility
// @private
Breeze.prototype.gc = function () {
this.reset()
this.steps = null
}
// Export breeze constructor
module.exports = function breeze (method) {
return new Breeze(method)
}
// Shim for setImmediate using setTimeout
if (typeof setImmediate !== 'function') {
function setImmediate () {
var args = cloneArguments(arguments)
args.splice(1, 0, 0)
return setTimeout.apply(null, args)
}
}
// Convert promise signature into callback signature
function promiseToCallback (promise) {
return function convertedPromiseCallbackStructure (callback) {
return typeof promise.catch === 'function'
? promise.then(function (data) {
setImmediate(callback, null, data)
}).catch(function (error) {
setImmediate(callback, error)
})
: promise.then(function (data) {
setImmediate(callback, null, data)
}, function (error) {
setImmediate(callback, error)
})
}
}
// Clone and slice arguments object into a new array without leaking.
function cloneArguments (arg, begin, end) {
var i = 0
var len = arg.length
var args = new Array(len)
for (; i < len; ++i) args[i] = arg[i]
return args = args.slice(begin || 0, end || undefined)
}
// Determine whether passed object is a promise or promise variation
function isPromise (obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
}
// Determine whether passed object is a plain object or not.
function isPlainObject (value) {
return Object.prototype.toString.call(value) === '[object Object]'
}