uci-utils-ready/src/ready.js

173 lines
5.7 KiB
JavaScript

// new change
import { from, fromEvent, combineLatest, ReplaySubject } from 'rxjs'
import { switchMap, map as simpleMap, startWith, tap, pairwise, filter, shareReplay} from 'rxjs/operators'
import isObservable from 'is-observable'
import isPromise from 'p-is-promise'
import isPlainObject from 'is-plain-object'
// UCI dependencies
import { createBoolean } from '@uci-utils/to-boolean'
const toBool = createBoolean({undefined:true}) // make default make null event emission cast to TRUE
class Ready extends Map {
constructor(opts) {
super(opts.observables)
// TODO support setting registration in options
this.emitter = typeof opts.emitter.on ==='function' ? opts.emitter : null
this.condition = opts.condition || ( (ev) => toBool(ev) )
this.subscriptions = new Map()
this.combinations = new Map()
this.details = new Map()
this.combineObservers('__all__') // initialize all combination
this.logger = new ReplaySubject()
this.state = [] // holds last state of all observers
this.log = this.logger.next.bind(this.logger)
if (opts.verbose) this.logger.subscribe(console.log)
this.handler = opts.handler || console.log
this._first = true // tracks first emission
}
get observerNames(){return Array.from(this.keys())}
get failure () {
let ret = null
let failed = this.state.some(obs=> {
ret = obs[0]
return obs[1]===false
})
return !failed ? '__none__' : ret
}
getObserver(name) {
return (this.get(name) || this.combinations.get(name||'__all__'))
}
getValue(name) { // NOT recommended. if there is any issue will return false
let obs = this.getObserver(name)
return new Promise(resolve => {
setTimeout(()=>resolve(false),50)
if (isObservable(obs)){
const sub = obs.subscribe(val => {
resolve(val)
})
sub.unsubscribe()
} else resolve(false)
})
}
addObserverDetails(name,details,overwrite) {
if (this.has(name)) {
if (!isPlainObject(details)) return false
// TODO substitue merge anything for Object.assign
this.details.set(name,overwrite ? details : Object.assign(this.details.get(name)||{},details))
return true
}
return false
}
addObserver(name, obs, opts={} ) {
// validation and defaults, obs can be emitter, osbserver, or promise
if (!name || typeof(name)!=='string') return false // name required
if (isPlainObject(obs)) { opts = obs; obs=null }
if (!(obs || this.emitter)) return false // some observable requried
let { condition, event, details } = opts
condition = condition || this.condition
if (typeof (obs ||{}).on ==='function') obs = fromEvent(obs, event || name) // it's an emitter
if (isPromise(obs)) obs = from(obs) // it's a promise
if (obs && !isObservable(obs) && typeof obs==='function' && arguments.length===2) {
condition = obs
// obs = null
}
if (!obs && this.emitter) obs = fromEvent(this.emitter,event || name)
if (!obs || !isObservable(obs)) return false
let xobs = obs.pipe(
startWith(false),
tap(val => this.log(`${name} emitted/resolved the value =>${JSON.stringify(val)}`)),
map(condition),
// tap(val => this.log(`boolean: ${val}`)),
shareReplay(1)
)
this.set(name, xobs)
this.addObserverDetails(name,details,true)
this.combineObservers('__all__') // update total combo
return xobs
}
removeObserver(names) {
if (!names) this.clear
else {
if (!Array.isArray(names)) names = [names]
names.forEach(name => {
this.delete(name)
})
}
this.combineObservers('__all__') // update total combo
}
subscribe(name, handler) {
if (typeof name ==='function') {
handler=name
name = null
}
name = name || '__all__'
if (this.subscriptions.get(name)) this.unsubscribe(name)
let obs = this.getObserver(name)
if (!obs) return false
let subs = obs.subscribe(handler||this.handler)
this.subscriptions.set(name,subs)
return subs
}
unsubscribe(name) {
if (!this.subscriptions.has(name)) return false
this.subscriptions.get(name||'__all__').unsubscribe()
return true
}
combineObservers(name,list) {
if (Array.isArray(name)) {list = name; name = null}
if (name==='__all__') list = Array.from(this.keys()) // get list of all observers
if (!Array.isArray(list)) return false // can't make a combo without a list
if (this.has(name)) return false // can't save a combo with same name as any single oberver
let observers = list.map(name=>this.get(name)||this.getCombination(name))
if (observers.filter(obs=>!isObservable(obs)).length) return false
let combination = combineLatest(observers).pipe(
tap(states => { if (name==='__all__') this.state = list.map((name,index) => [name,states[index]])}),
map(states => states.reduce((res,state) => {return res && state},true)),
startWith(false),
changed(),
shareReplay(1)
)
if (name) this.combinations.set(name, combination) // if name passed then save combo in Map
return combination
}
getCombination(name) {
this.combinations.get(name||'__all__')
}
} // end class
function changed(toBoolean) {
toBoolean = toBoolean || (val => !!val)
return obs$ => obs$.pipe(
pairwise(),
filter( ([p,c]) => {
const chng = !obs$.__not_first__ || !!(toBoolean(p) ^ toBoolean(c))
obs$.__not_first__=true
return chng}),
map( r => r[1] ), //remove previous
)
}
function map(fn) {
return isAsync(fn) ? switchMap.call(this,fn) : simpleMap.call(this,fn)
}
function isAsync(fn) {
return (isPromise(fn) || fn.constructor.name === 'AsyncFunction')
}
export default Ready
export { Ready, changed, map, tap}