// pronto.cm // Extremism in the pursuit of parallelism is no vice. // Based on Douglas Crockford's parseq, adapted for Cell. // Time is in seconds. function safe_call(fn, arg) { fn(arg) } disruption {} function make_reason(factory, excuse, evidence) { def reason = Error(`pronto.${factory}${excuse ? ': ' + excuse : ''}`) reason.evidence = evidence return reason } function is_requestor(fn) { return is_function(fn) && (length(fn) == 1 || length(fn) == 2) } function check_requestors(list, factory) { if (!is_array(list) || some(list, r => !is_requestor(r))) disrupt } function check_callback(cb, factory) { if (!is_function(cb) || length(cb) != 2) disrupt } // fallback(requestor_array) // Tries each requestor in order until one succeeds. function fallback(requestor_array) { def factory = 'fallback' if (!is_array(requestor_array) || length(requestor_array) == 0) disrupt check_requestors(requestor_array, factory) return function fallback_requestor(callback, value) { check_callback(callback, factory) var index = 0 var current_cancel = null var cancelled = false function cancel(reason) { cancelled = true if (current_cancel) { safe_call(current_cancel, reason) current_cancel = null } } function try_next() { if (cancelled) return if (index >= length(requestor_array)) { callback(null, make_reason(factory, 'All requestors failed.')) return } def requestor = requestor_array[index] index += 1 var requestor_disrupted = false var start_requestor = function() { current_cancel = requestor(function(val, reason) { if (cancelled) return current_cancel = null if (val != null) { callback(val) } else { try_next() } }, value) } disruption { requestor_disrupted = true } start_requestor() if (requestor_disrupted) { try_next() } } try_next() return cancel } } // parallel(requestor_array, throttle, need) // Runs requestors in parallel, collecting all results. function parallel(requestor_array, throttle, need) { def factory = 'parallel' if (!is_array(requestor_array)) disrupt check_requestors(requestor_array, factory) def length = length(requestor_array) if (length == 0) return function(callback, value) { callback([]) } if (need == null) need = length if (!is_number(need) || need < 0 || need > length) disrupt if (throttle != null && (!is_number(throttle) || throttle < 1)) disrupt return function parallel_requestor(callback, value) { check_callback(callback, factory) def results = array(length) def cancel_list = array(length) var next_index = 0 var successes = 0 var failures = 0 var finished = false function cancel(reason) { if (finished) return finished = true arrfor(cancel_list, c => { var do_cancel = function() { if (is_function(c)) c(reason) } disruption {} do_cancel() }) } function start_one() { if (finished || next_index >= length) return def idx = next_index next_index += 1 def requestor = requestor_array[idx] var requestor_disrupted = false var requestor_ex = null var run_requestor = function() { cancel_list[idx] = requestor(function(val, reason) { if (finished) return cancel_list[idx] = null if (val != null) { results[idx] = val successes += 1 if (successes >= need) { finished = true cancel(make_reason(factory, 'Finished.')) callback(results) return } } else { failures += 1 if (failures > length - need) { cancel(reason) callback(null, reason || make_reason(factory, 'Too many failures.')) return } } start_one() }, value) } disruption { requestor_disrupted = true } run_requestor() if (requestor_disrupted) { failures += 1 if (failures > length - need) { cancel(requestor_ex) callback(null, requestor_ex) return } start_one() } } def concurrent = throttle ? min(throttle, length) : length for (var i = 0; i < concurrent; i++) start_one() return cancel } } // race(requestor_array, throttle, need) // Runs requestors in parallel, returns first success(es). function race(requestor_array, throttle, need) { def factory = 'race' if (!is_array(requestor_array) || length(requestor_array) == 0) disrupt check_requestors(requestor_array, factory) def length = length(requestor_array) if (need == null) need = 1 if (!is_number(need) || need < 1 || need > length) disrupt if (throttle != null && (!is_number(throttle) || throttle < 1)) disrupt return function race_requestor(callback, value) { check_callback(callback, factory) def results = array(length) def cancel_list = array(length) var next_index = 0 var successes = 0 var failures = 0 var finished = false function cancel(reason) { if (finished) return finished = true arrfor(cancel_list, c => { var do_cancel = function() { if (is_function(c)) c(reason) } disruption {} do_cancel() }) } function start_one() { if (finished || next_index >= length) return def idx = next_index next_index += 1 def requestor = requestor_array[idx] var requestor_disrupted = false var requestor_ex = null var run_requestor = function() { cancel_list[idx] = requestor(function(val, reason) { if (finished) return cancel_list[idx] = null if (val != null) { results[idx] = val successes += 1 if (successes >= need) { cancel(make_reason(factory, 'Winner.')) if (need == 1) { callback(val) } else { callback(results) } return } } else { failures += 1 if (failures > length - need) { cancel(reason) callback(null, reason || make_reason(factory, 'All failed.')) return } } start_one() }, value) } disruption { requestor_disrupted = true } run_requestor() if (requestor_disrupted) { failures += 1 if (failures > length - need) { cancel(requestor_ex) callback(null, requestor_ex) return } start_one() } } def concurrent = throttle ? min(throttle, length) : length for (var i = 0; i < concurrent; i++) start_one() return cancel } } // sequence(requestor_array) // Runs requestors one at a time, passing result to next. function sequence(requestor_array) { def factory = 'sequence' if (!is_array(requestor_array)) disrupt check_requestors(requestor_array, factory) if (length(requestor_array) == 0) return function(callback, value) { callback(value) } return function sequence_requestor(callback, value) { check_callback(callback, factory) var index = 0 var current_cancel = null var cancelled = false function cancel(reason) { cancelled = true if (current_cancel) { safe_call(current_cancel, reason) current_cancel = null } } function run_next(val) { if (cancelled) return if (index >= length(requestor_array)) { callback(val) return } def requestor = requestor_array[index] index += 1 var requestor_disrupted = false var requestor_ex = null var run_requestor = function() { current_cancel = requestor(function(result, reason) { if (cancelled) return current_cancel = null if (result == null) { callback(null, reason) } else { run_next(result) } }, val) } disruption { requestor_disrupted = true } run_requestor() if (requestor_disrupted) { callback(null, requestor_ex) } } run_next(value) return cancel } } // requestorize(unary) // Converts a unary function into a requestor. function requestorize(unary) { def factory = 'requestorize' if (!is_function(unary)) disrupt return function requestorized(callback, value) { check_callback(callback, factory) var call_disrupted = false var call_ex = null var do_call = function() { def result = unary(value) callback(result == null ? true : result) } disruption { call_disrupted = true } do_call() if (call_disrupted) { callback(null, call_ex) } } } return { fallback, parallel, race, sequence, requestorize, is_requestor, check_callback }