// 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}