uci-utils-ready/src/ready.js

188 lines
6.8 KiB
JavaScript

// new change
import { from, fromEvent, combineLatest, BehaviorSubject } 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'
class Ready extends Map {
constructor(opts) {
super(opts.observables)
this.emitter = typeof opts.emitter.on ==='function' ? opts.emitter : null
const toBool = createBoolean(opts.boolean) // e.g. {undefined:true}
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 BehaviorSubject()
this.state = [] // holds last state of all observers
this.log = this.logger.next.bind(this.logger)
if (opts.verbose||process.env.UCI_READY_VERBOSE==='true') this.logger.subscribe(console.log)
this.handler = opts.handler || ((ready) => {console.log('default handler', ready)})
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
}
getObserverDetails(name) { return (this.get(name)||{}).details}
setObserverDetails(name,details,overwrite) {
if (this.has(name)) {
if (details==null) return false
if (!isPlainObject(details)) details= {desc:details}
// TODO substitue merge anything for Object.assign
this.get(name).details = overwrite ? details : Object.assign(this.get(name).details || {},details)
return true
}
return false
}
getObserver(name) {
return ((this.get(name) || {}).obs || this.combinations.get(name||'__all__'))
}
getCombination(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)
})
}
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
if (!obs && this.emitter) obs = fromEvent(this.emitter,event || name)
if (!obs || !isObservable(obs)) return false
let xobs = obs.pipe(
tap(val => this.log(`${name} emitted/resolved the value =>${JSON.stringify(val)}`)),
map(condition),
tap(val => this.log(`boolean: ${val}`)),
startWith(false),
shareReplay(1),
// multicast(new Subject())
)
let sub = xobs.subscribe()
this.set(name, {obs:xobs, sub:sub})
this.setObserverDetails(name,details)
this.combineObservers('__all__') // update total combo
if (this.subscriptions.has('__all__')) this.subscribe() // will resubscribe
return xobs
}
combineObservers(name,list) {
if (Array.isArray(name)) {list = name; name = null}
name = name || '__all__'
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.getObserver(name)) // will get combo if exists
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]])}),
tap(states => { this.log(list.map((name,index) => [name,states[index]]))}),
map(states => states.reduce((res,state) => {return res && state},true)),
changed(), //filters out emission if it is unchanged from last
startWith(false),
shareReplay(1)
)
this.combinations.set(name, combination) // if name passed then save combo in Map
return combination
}
// will remove combination as well
removeObserver(names) {
if (!names) names = this.observerNames
else {
if (!Array.isArray(names)) names = [names]
console.log('names to remove', names)
names.forEach(name => {
const sub = (this.get(name)||{}).sub
if (sub) sub.unsubscribe() // remove attached subscription
this.unsubscribe(name) // remove any manual subscription
this.delete(name) || this.combinations.delete(name)
})
}
console.log(this.observerNames)
this.combineObservers('__all__') // update total combo
this.subscribe() // resubscribe to changed combo
}
subscribe(name, handler) {
// only one subscription at a time per observer or combination from this method
if (typeof name ==='function') {
handler=name
name = null
}
name = name || '__all__'
handler = handler || (this.subscriptions.get(name)||{}).handler || this.handler
let obs = this.getObserver(name) // will attempt to get combo if no simple observer, all if name is null
if (!obs) return false
if (this.subscriptions.has(name)) this.subscriptions.get(name).subs.unsubscribe()
let subs = obs.subscribe(handler)
this.subscriptions.set(name,{subs:subs,handler:handler})
return subs
}
unsubscribe(name) {
name = name ||'__all__'
if (!this.subscriptions.has(name)) return false
this.subscriptions.get(name).subs.unsubscribe()
this.subscriptions.delete(name)
return true
}
} // 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}