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 observermaster
parent
30f9f273e7
commit
63566d624f
|
@ -4,13 +4,28 @@ import { EventEmitter } from 'events'
|
||||||
|
|
||||||
let emitter = new EventEmitter()
|
let emitter = new EventEmitter()
|
||||||
|
|
||||||
let verbose = process.env.VERBOSE==='true'
|
// let verbose = process.env.VERBOSE==='true'
|
||||||
|
|
||||||
let combo = false
|
let combo = false
|
||||||
|
|
||||||
// handler: (r)=> console.log('result:',r)
|
// 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 => {
|
const tObs = new Observable(subscriber => {
|
||||||
subscriber.next('on')
|
subscriber.next('on')
|
||||||
|
@ -18,20 +33,25 @@ const tObs = new Observable(subscriber => {
|
||||||
subscriber.next('enabled')
|
subscriber.next('enabled')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
subscriber.next('F')
|
subscriber.next('F')
|
||||||
}, 2000)
|
}, 7000)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
subscriber.next('T')
|
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) {
|
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('e',{details:{desc:'this is the e observer which comes from an emitter'}})
|
||||||
example.addObserver('ce',{condition: (ev)=>ev.ready, event:'ec'})
|
example.addObserver('ce',{condition: (ev)=>ev.ready, event:'ec'})
|
||||||
example.addObserver('pe',emitter)
|
example.addObserver('pe',emitter)
|
||||||
example.addObserver('obs',tObs)
|
|
||||||
example.addObserver('pr',tPromise)
|
example.addObserver('pr',tPromise)
|
||||||
example.addObserver('extnd',example.getObserver('e').pipe(
|
example.addObserver('extnd',example.getObserver('e').pipe(
|
||||||
map(state => {
|
map(state => {
|
||||||
|
@ -39,12 +59,12 @@ example.addObserver('extnd',example.getObserver('e').pipe(
|
||||||
if (state) {
|
if (state) {
|
||||||
let val = Math.floor(Math.random() * Math.floor(10))
|
let val = Math.floor(Math.random() * Math.floor(10))
|
||||||
// console.log(val)
|
// console.log(val)
|
||||||
state = val <10 ? true:false
|
state = val <11 ? true:false
|
||||||
}
|
}
|
||||||
// console.log('extend after:',state,'\n-------------')
|
// console.log('extend after:',state,'\n-------------')
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
))
|
),{details:'an observer extension of the e observer'})
|
||||||
|
|
||||||
if (combo) {
|
if (combo) {
|
||||||
let combo = example.combineObservers('combo',['e','pr'])
|
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('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('ec',{ready:true})
|
||||||
emitter.emit('pe','yup')
|
emitter.emit('pe','yup')
|
||||||
emitter.emit('e')
|
emitter.emit('e')
|
||||||
console.log('===============done initial emitting=================')
|
console.log('===============done initial emitting, pe,e,ec/ce=================')
|
||||||
setTimeout(async () => {
|
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('e',false)
|
||||||
|
emitter.emit('pe',false)
|
||||||
}
|
}
|
||||||
,2000)
|
,2000)
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log('=================emitting e true================')
|
console.log('=================emitting e true, removing pe which is false ================')
|
||||||
emitter.emit('e',true)
|
emitter.emit('e',true)
|
||||||
|
example.removeObserver('pe')
|
||||||
}
|
}
|
||||||
,4000)
|
,4000)
|
||||||
|
|
||||||
setTimeout(async () => {console.log('timeout done')},6000)
|
setTimeout(async () => {console.log('timeout done')},6000)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@uci-utils/ready",
|
"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",
|
"description": "A Class to Observe the reduced to boolean combined state of a map of observables",
|
||||||
"main": "src/ready.js",
|
"main": "src/ready.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
125
src/ready.js
125
src/ready.js
|
@ -1,28 +1,27 @@
|
||||||
// new change
|
// 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 { switchMap, map as simpleMap, startWith, tap, pairwise, filter, shareReplay} from 'rxjs/operators'
|
||||||
import isObservable from 'is-observable'
|
import isObservable from 'is-observable'
|
||||||
import isPromise from 'p-is-promise'
|
import isPromise from 'p-is-promise'
|
||||||
import isPlainObject from 'is-plain-object'
|
import isPlainObject from 'is-plain-object'
|
||||||
// UCI dependencies
|
// UCI dependencies
|
||||||
import { createBoolean } from '@uci-utils/to-boolean'
|
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 {
|
class Ready extends Map {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
super(opts.observables)
|
super(opts.observables)
|
||||||
// TODO support setting registration in options
|
|
||||||
this.emitter = typeof opts.emitter.on ==='function' ? opts.emitter : null
|
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.condition = opts.condition || ( (ev) => toBool(ev) )
|
||||||
this.subscriptions = new Map()
|
this.subscriptions = new Map()
|
||||||
this.combinations = new Map()
|
this.combinations = new Map()
|
||||||
this.details = new Map()
|
this.details = new Map()
|
||||||
this.combineObservers('__all__') // initialize all combination
|
this.combineObservers('__all__') // initialize all combination
|
||||||
this.logger = new ReplaySubject()
|
this.logger = new BehaviorSubject()
|
||||||
this.state = [] // holds last state of all observers
|
this.state = [] // holds last state of all observers
|
||||||
this.log = this.logger.next.bind(this.logger)
|
this.log = this.logger.next.bind(this.logger)
|
||||||
if (opts.verbose) this.logger.subscribe(console.log)
|
if (opts.verbose||process.env.UCI_READY_VERBOSE==='true') this.logger.subscribe(console.log)
|
||||||
this.handler = opts.handler || console.log
|
this.handler = opts.handler || ((ready) => {console.log('default handler', ready)})
|
||||||
this._first = true // tracks first emission
|
this._first = true // tracks first emission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,8 +36,25 @@ class Ready extends Map {
|
||||||
return !failed ? '__none__' : ret
|
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) {
|
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
|
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={} ) {
|
addObserver(name, obs, opts={} ) {
|
||||||
// validation and defaults, obs can be emitter, osbserver, or promise
|
// validation and defaults, obs can be emitter, osbserver, or promise
|
||||||
if (!name || typeof(name)!=='string') return false // name required
|
if (!name || typeof(name)!=='string') return false // name required
|
||||||
|
@ -73,78 +79,87 @@ class Ready extends Map {
|
||||||
condition = condition || this.condition
|
condition = condition || this.condition
|
||||||
if (typeof (obs ||{}).on ==='function') obs = fromEvent(obs, event || name) // it's an emitter
|
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 (isPromise(obs)) obs = from(obs) // it's a promise
|
||||||
if (obs && !isObservable(obs) && typeof obs==='function' && arguments.length===2) {
|
if (obs && !isObservable(obs) && typeof obs==='function' && arguments.length===2) condition = obs
|
||||||
condition = obs
|
|
||||||
// obs = null
|
|
||||||
}
|
|
||||||
if (!obs && this.emitter) obs = fromEvent(this.emitter,event || name)
|
if (!obs && this.emitter) obs = fromEvent(this.emitter,event || name)
|
||||||
if (!obs || !isObservable(obs)) return false
|
if (!obs || !isObservable(obs)) return false
|
||||||
let xobs = obs.pipe(
|
let xobs = obs.pipe(
|
||||||
startWith(false),
|
|
||||||
tap(val => this.log(`${name} emitted/resolved the value =>${JSON.stringify(val)}`)),
|
tap(val => this.log(`${name} emitted/resolved the value =>${JSON.stringify(val)}`)),
|
||||||
map(condition),
|
map(condition),
|
||||||
// tap(val => this.log(`boolean: ${val}`)),
|
tap(val => this.log(`boolean: ${val}`)),
|
||||||
shareReplay(1)
|
startWith(false),
|
||||||
|
shareReplay(1),
|
||||||
|
// multicast(new Subject())
|
||||||
)
|
)
|
||||||
this.set(name, xobs)
|
let sub = xobs.subscribe()
|
||||||
this.addObserverDetails(name,details,true)
|
this.set(name, {obs:xobs, sub:sub})
|
||||||
|
this.setObserverDetails(name,details)
|
||||||
this.combineObservers('__all__') // update total combo
|
this.combineObservers('__all__') // update total combo
|
||||||
|
if (this.subscriptions.has('__all__')) this.subscribe() // will resubscribe
|
||||||
return xobs
|
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) {
|
removeObserver(names) {
|
||||||
if (!names) this.clear
|
if (!names) names = this.observerNames
|
||||||
else {
|
else {
|
||||||
if (!Array.isArray(names)) names = [names]
|
if (!Array.isArray(names)) names = [names]
|
||||||
|
console.log('names to remove', names)
|
||||||
names.forEach(name => {
|
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.combineObservers('__all__') // update total combo
|
||||||
|
this.subscribe() // resubscribe to changed combo
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(name, handler) {
|
subscribe(name, handler) {
|
||||||
|
// only one subscription at a time per observer or combination from this method
|
||||||
if (typeof name ==='function') {
|
if (typeof name ==='function') {
|
||||||
handler=name
|
handler=name
|
||||||
name = null
|
name = null
|
||||||
}
|
}
|
||||||
name = name || '__all__'
|
name = name || '__all__'
|
||||||
if (this.subscriptions.get(name)) this.unsubscribe(name)
|
handler = handler || (this.subscriptions.get(name)||{}).handler || this.handler
|
||||||
let obs = this.getObserver(name)
|
let obs = this.getObserver(name) // will attempt to get combo if no simple observer, all if name is null
|
||||||
if (!obs) return false
|
if (!obs) return false
|
||||||
let subs = obs.subscribe(handler||this.handler)
|
if (this.subscriptions.has(name)) this.subscriptions.get(name).subs.unsubscribe()
|
||||||
this.subscriptions.set(name,subs)
|
let subs = obs.subscribe(handler)
|
||||||
|
this.subscriptions.set(name,{subs:subs,handler:handler})
|
||||||
return subs
|
return subs
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(name) {
|
unsubscribe(name) {
|
||||||
|
name = name ||'__all__'
|
||||||
if (!this.subscriptions.has(name)) return false
|
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
|
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
|
} // end class
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import Ready from '../src/ready'
|
import Ready from '../src/ready'
|
||||||
|
|
||||||
describe('', function () {
|
let ready = new Ready()
|
||||||
|
|
||||||
it('Should include custom types', function () {
|
describe('Ready Testing', function () {
|
||||||
expect(u.isBuffer(Buffer.from('this is a test'))).to.equal(true)
|
|
||||||
|
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)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue