225 lines
6.8 KiB
Plaintext
225 lines
6.8 KiB
Plaintext
// parseq.js (Misty edition)
|
||
// Douglas Crockford → adapted for Misty by ChatGPT, 2025‑05‑29
|
||
// Better living thru eventuality!
|
||
|
||
/*
|
||
The original parseq.js relied on the browser's setTimeout and ran in
|
||
milliseconds. In Misty we may be given an optional @.delay capability
|
||
(arguments[0]) and time limits are expressed in **seconds**. This rewrite
|
||
removes the setTimeout dependency, uses the @.delay capability when it is
|
||
present, and provides the factories described in the Misty specification:
|
||
|
||
fallback, par_all, par_any, race, sequence
|
||
|
||
Each factory returns a **requestor** function as described by the spec.
|
||
*/
|
||
|
||
def delay = arg[0] // may be null
|
||
|
||
// ———————————————————————————————————————— helpers
|
||
|
||
function make_reason (factory, excuse, evidence) {
|
||
def reason = new Error(`parseq.${factory}${excuse ? ': ' + excuse : ''}`)
|
||
reason.evidence = evidence
|
||
return reason
|
||
}
|
||
|
||
function is_requestor (fn) {
|
||
return typeof fn == 'function' && (fn.length == 1 || fn.length == 2)
|
||
}
|
||
|
||
function check_requestors (list, factory) {
|
||
if (!Array.isArray(list) || list.some(r => !is_requestor(r)))
|
||
throw make_reason(factory, 'Bad requestor list.', list)
|
||
}
|
||
|
||
function check_callback (cb, factory) {
|
||
if (typeof cb != 'function' || cb.length != 2)
|
||
throw make_reason(factory, 'Not a callback.', cb)
|
||
}
|
||
|
||
function schedule (fn, seconds) {
|
||
if (seconds == null || seconds <= 0) return fn()
|
||
if (typeof delay == 'function') return delay(fn, seconds)
|
||
throw make_reason('schedule', '@.delay capability required for timeouts.')
|
||
}
|
||
|
||
// ———————————————————————————————————————— core runner
|
||
|
||
function run (factory, requestors, initial, action, time_limit, throttle = 0) {
|
||
let cancel_list = new Array(requestors.length)
|
||
let next = 0
|
||
let timer_cancel
|
||
|
||
function cancel (reason = make_reason(factory, 'Cancel.')) {
|
||
if (timer_cancel) timer_cancel(), timer_cancel = null
|
||
if (!cancel_list) return
|
||
cancel_list.forEach(c => { try { if (typeof c == 'function') c(reason) } catch (_) {} })
|
||
cancel_list = null
|
||
}
|
||
|
||
function start_requestor (value) {
|
||
if (!cancel_list || next >= requestors.length) return
|
||
let idx = next++
|
||
def req = requestors[idx]
|
||
|
||
try {
|
||
cancel_list[idx] = req(function req_cb (val, reason) {
|
||
if (!cancel_list || idx == null) return
|
||
cancel_list[idx] = null
|
||
action(val, reason, idx)
|
||
idx = null
|
||
if (factory == 'sequence') start_requestor(val)
|
||
else if (throttle) start_requestor(initial)
|
||
}, value)
|
||
} catch (ex) {
|
||
action(null, ex, idx)
|
||
idx = null
|
||
if (factory == 'sequence') start_requestor(value)
|
||
else if (throttle) start_requestor(initial)
|
||
}
|
||
}
|
||
|
||
if (time_limit != null) {
|
||
if (typeof time_limit != 'number' || time_limit < 0)
|
||
throw make_reason(factory, 'Bad time limit.', time_limit)
|
||
if (time_limit > 0) timer_cancel = schedule(() => cancel(make_reason(factory, 'Timeout.', time_limit)), time_limit)
|
||
}
|
||
|
||
def concurrent = throttle ? Math.min(throttle, requestors.length) : requestors.length
|
||
for (let i = 0; i < concurrent; i++) start_requestor(initial)
|
||
|
||
return cancel
|
||
}
|
||
|
||
// ———————————————————————————————————————— factories
|
||
|
||
function _normalize (collection, factory) {
|
||
if (Array.isArray(collection)) return { names: null, list: collection }
|
||
if (collection && typeof collection == 'object') {
|
||
def names = Object.keys(collection)
|
||
def list = names.map(k => collection[k]).filter(is_requestor)
|
||
return { names, list }
|
||
}
|
||
throw make_reason(factory, 'Expected array or record.', collection)
|
||
}
|
||
|
||
function _denormalize (names, list) {
|
||
if (!names) return list
|
||
def obj = Object.create(null)
|
||
names.forEach((k, i) => { obj[k] = list[i] })
|
||
return obj
|
||
}
|
||
|
||
function par_all (collection, time_limit, throttle) {
|
||
def factory = 'par_all'
|
||
def { names, list } = _normalize(collection, factory)
|
||
if (list.length == 0) return (cb, v) => cb(names ? {} : [])
|
||
check_requestors(list, factory)
|
||
|
||
return function par_all_req (cb, initial) {
|
||
check_callback(cb, factory)
|
||
let pending = list.length
|
||
def results = new Array(list.length)
|
||
|
||
def cancel = run(factory, list, initial, (val, reason, idx) => {
|
||
if (val == null) {
|
||
cancel(reason)
|
||
return cb(null, reason)
|
||
}
|
||
results[idx] = val
|
||
if (--pending == 0) cb(_denormalize(names, results))
|
||
}, time_limit, throttle)
|
||
|
||
return cancel
|
||
}
|
||
}
|
||
|
||
function par_any (collection, time_limit, throttle) {
|
||
def factory = 'par_any'
|
||
def { names, list } = _normalize(collection, factory)
|
||
if (list.length == 0) return (cb, v) => cb(names ? {} : [])
|
||
check_requestors(list, factory)
|
||
|
||
return function par_any_req (cb, initial) {
|
||
check_callback(cb, factory)
|
||
let pending = list.length
|
||
def successes = new Array(list.length)
|
||
|
||
def cancel = run(factory, list, initial, (val, reason, idx) => {
|
||
pending--
|
||
if (val != null) successes[idx] = val
|
||
if (successes.some(v => v != null)) {
|
||
if (!pending) cancel(make_reason(factory, 'Finished.'))
|
||
return cb(_denormalize(names, successes.filter(v => v != null)))
|
||
}
|
||
if (!pending) cb(null, make_reason(factory, 'No successes.'))
|
||
}, time_limit, throttle)
|
||
|
||
return cancel
|
||
}
|
||
}
|
||
|
||
function race (list, time_limit, throttle) {
|
||
def factory = throttle == 1 ? 'fallback' : 'race'
|
||
if (!Array.isArray(list) || list.length == 0)
|
||
throw make_reason(factory, 'No requestors.')
|
||
check_requestors(list, factory)
|
||
|
||
return function race_req (cb, initial) {
|
||
check_callback(cb, factory)
|
||
let done = false
|
||
def cancel = run(factory, list, initial, (val, reason, idx) => {
|
||
if (done) return
|
||
if (val != null) {
|
||
done = true
|
||
cancel(make_reason(factory, 'Loser.', idx))
|
||
cb(val)
|
||
} else if (--list.length == 0) {
|
||
done = true
|
||
cancel(reason)
|
||
cb(null, reason)
|
||
}
|
||
}, time_limit, throttle)
|
||
return cancel
|
||
}
|
||
}
|
||
|
||
function fallback (list, time_limit) {
|
||
return race(list, time_limit, 1)
|
||
}
|
||
|
||
function sequence (list, time_limit) {
|
||
def factory = 'sequence'
|
||
if (!Array.isArray(list)) throw make_reason(factory, 'Not an array.', list)
|
||
check_requestors(list, factory)
|
||
if (list.length == 0) return (cb, v) => cb(v)
|
||
|
||
return function sequence_req (cb, initial) {
|
||
check_callback(cb, factory)
|
||
let idx = 0
|
||
|
||
function next (value) {
|
||
if (idx >= list.length) return cb(value)
|
||
try {
|
||
list[idx++](function seq_cb (val, reason) {
|
||
if (val == null) return cb(null, reason)
|
||
next(val)
|
||
}, value)
|
||
} catch (ex) {
|
||
cb(null, ex)
|
||
}
|
||
}
|
||
|
||
next(initial)
|
||
}
|
||
}
|
||
|
||
return {
|
||
fallback,
|
||
par_all,
|
||
par_any,
|
||
race,
|
||
sequence
|
||
}
|