diff --git a/examples/example.js b/examples/example.js index e476eb6..e9b9e77 100644 --- a/examples/example.js +++ b/examples/example.js @@ -4,13 +4,28 @@ import { EventEmitter } from 'events' let emitter = new EventEmitter() -let verbose = process.env.VERBOSE==='true' +// let verbose = process.env.VERBOSE==='true' let combo = false // handler: (r)=> console.log('result:',r) -let example = new Ready({emitter: emitter, verbose:verbose}) +const late=3000 + +let example = new Ready({emitter: emitter}) + +let subscribe = ()=>{ + console.log('subscribing at',late, 'ms') + example.subscribe(ready => { + console.log(`-----------Subscriber at ${late} ms--------------?`,ready) + console.log('the failed observer:', example.failure, ',details:', example.getObserverDetails(example.failure)) + console.log('the total state', example.state) + console.log('---------------------------------------') + }) +} + +if (!late) subscribe() +else setTimeout(subscribe,late) const tObs = new Observable(subscriber => { subscriber.next('on') @@ -18,20 +33,25 @@ const tObs = new Observable(subscriber => { subscriber.next('enabled') setTimeout(() => { subscriber.next('F') - }, 2000) + }, 7000) setTimeout(() => { subscriber.next('T') - }, 3000) + }, 8000) }) + +example.addObserver('obs',tObs,{details:{desc:'this is a constructed observable from the Observable Class'}}) + const tPromise = new Promise(function(resolve) { - setTimeout(()=>resolve('yes'),1000) + setTimeout(()=>{ + console.log('promise observer is resolving') + resolve('yes') + },1000) }) example.addObserver('e',{details:{desc:'this is the e observer which comes from an emitter'}}) example.addObserver('ce',{condition: (ev)=>ev.ready, event:'ec'}) example.addObserver('pe',emitter) -example.addObserver('obs',tObs) example.addObserver('pr',tPromise) example.addObserver('extnd',example.getObserver('e').pipe( map(state => { @@ -39,12 +59,12 @@ example.addObserver('extnd',example.getObserver('e').pipe( if (state) { let val = Math.floor(Math.random() * Math.floor(10)) // console.log(val) - state = val <10 ? true:false + state = val <11 ? true:false } // console.log('extend after:',state,'\n-------------') return state }) -)) +),{details:'an observer extension of the e observer'}) if (combo) { let combo = example.combineObservers('combo',['e','pr']) @@ -58,23 +78,23 @@ if (combo) { example.subscribe('combo',val => console.log('combo e, pr is:',val)) } } -example.subscribe(val => { - console.log('Full Combination>>>>>>>>READY? example:',val) - console.log('failed observer', example.failure, 'details', example.details.get(example.failure)) -}) // all emitter.emit('ec',{ready:true}) emitter.emit('pe','yup') emitter.emit('e') -console.log('===============done initial emitting=================') +console.log('===============done initial emitting, pe,e,ec/ce=================') setTimeout(async () => { - console.log('============emitting e false===================') + console.log('============emitting e, pe false===================') + example.setObserverDetails('e',{moreinfo:'an additional property of details added later'}) emitter.emit('e',false) + emitter.emit('pe',false) } ,2000) setTimeout(async () => { - console.log('=================emitting e true================') + console.log('=================emitting e true, removing pe which is false ================') emitter.emit('e',true) + example.removeObserver('pe') } ,4000) + setTimeout(async () => {console.log('timeout done')},6000) diff --git a/package.json b/package.json index 88ba4a3..0dfea0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uci-utils/ready", - "version": "0.1.4", + "version": "0.1.5", "description": "A Class to Observe the reduced to boolean combined state of a map of observables", "main": "src/ready.js", "scripts": { diff --git a/src/ready.js b/src/ready.js index 5562ef7..c9669af 100644 --- a/src/ready.js +++ b/src/ready.js @@ -1,28 +1,27 @@ // new change -import { from, fromEvent, combineLatest, ReplaySubject } from 'rxjs' +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' -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 + 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 ReplaySubject() + this.logger = new BehaviorSubject() 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 + 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 } @@ -37,8 +36,25 @@ class Ready extends Map { 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) || this.combinations.get(name||'__all__')) + 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 @@ -54,16 +70,6 @@ class Ready extends Map { }) } - 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 @@ -73,78 +79,87 @@ class Ready extends Map { 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 && !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( - startWith(false), tap(val => this.log(`${name} emitted/resolved the value =>${JSON.stringify(val)}`)), map(condition), - // tap(val => this.log(`boolean: ${val}`)), - shareReplay(1) + tap(val => this.log(`boolean: ${val}`)), + startWith(false), + shareReplay(1), + // multicast(new Subject()) ) - this.set(name, xobs) - this.addObserverDetails(name,details,true) + 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) this.clear + if (!names) names = this.observerNames else { if (!Array.isArray(names)) names = [names] + console.log('names to remove', names) names.forEach(name => { - this.delete(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__' - if (this.subscriptions.get(name)) this.unsubscribe(name) - let obs = this.getObserver(name) + 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 - let subs = obs.subscribe(handler||this.handler) - this.subscriptions.set(name,subs) + 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||'__all__').unsubscribe() + this.subscriptions.get(name).subs.unsubscribe() + this.subscriptions.delete(name) 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 diff --git a/test/ready.test.js b/test/ready.test.js index 321dc3d..36a8623 100644 --- a/test/ready.test.js +++ b/test/ready.test.js @@ -1,15 +1,14 @@ import { expect } from 'chai' import Ready from '../src/ready' -describe('', function () { +let ready = new Ready() - it('Should include custom types', function () { - expect(u.isBuffer(Buffer.from('this is a test'))).to.equal(true) +describe('Ready Testing', function () { + + it('Should worth with four kinds of observers', function () { + // expect(u.isBuffer(Buffer.from('this is a test'))).to.equal(true) }) - it('Should load typechecker', function () { - expect(u.isPlainObject([1])).to.equal(false) - }) })