From 63566d624ffbd61d4ea291d76acb61cab6be1a6f Mon Sep 17 00:00:00 2001 From: David Kebler Date: Tue, 14 Jan 2020 13:26:40 -0800 Subject: [PATCH] 0.1.5 add boolean option to allow pass through of toBoolean module options refactor main map to hold observer, details and corresponding 'hidden' subscription together unsubscribes from that subscription when removing observer new getter/setter for observer details, can also pass as option when adding observer --- examples/example.js | 50 ++++++++++++------ package.json | 2 +- src/ready.js | 125 +++++++++++++++++++++++++------------------- test/ready.test.js | 11 ++-- 4 files changed, 111 insertions(+), 77 deletions(-) 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) - }) })