188 lines
6.8 KiB
JavaScript
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}
|