From 3eef450669d1480c3b1e6c164216a8e642690372 Mon Sep 17 00:00:00 2001 From: David Kebler Date: Wed, 1 Jan 2020 18:36:48 -0800 Subject: [PATCH] 0.1.1 --- .eslintrc.js | 37 ++++++++++++++++ .gitignore | 2 + .npmignore | 4 ++ examples/example.js | 34 ++++++++++++++ package.json | 39 ++++++++++++++++ readme.md | 3 ++ src/ready.js | 105 ++++++++++++++++++++++++++++++++++++++++++++ test/ready.test.js | 15 +++++++ 8 files changed, 239 insertions(+) create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 examples/example.js create mode 100644 package.json create mode 100644 readme.md create mode 100644 src/ready.js create mode 100644 test/ready.test.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..49bac18 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,37 @@ +module.exports = { + "ecmaFeatures": { + "modules": true, + "spread" : true, + "restParams" : true + }, + // "plugins": [ + // "unicorn" + // ], + "env": { + "es6": true, + "node": true, + "mocha": true + }, + "parserOptions": { + "ecmaVersion": 2017, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + // "unicorn/no-array-instanceof": "error", + "no-console": 0, + "semi": ["error", "never"], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61051f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +/coverage/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f16fc41 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +tests/ +test/ +*.test.js +testing/ diff --git a/examples/example.js b/examples/example.js new file mode 100644 index 0000000..0e0eb09 --- /dev/null +++ b/examples/example.js @@ -0,0 +1,34 @@ +import { Observable } from 'rxjs' +import Ready from '../src/ready' +import { EventEmitter } from 'events' + +let emitter = new EventEmitter() + +let process = new Ready({emitter: emitter}) + +const tObs = new Observable(subscriber => { + subscriber.next('on') + subscriber.next('off') + subscriber.next('enabled') + setTimeout(() => { + subscriber.next('F') + subscriber.next('T') + }, 2000) +}) + +const tPromise = new Promise(function(resolve) { + setTimeout(()=>resolve('yes'),3000) +}) + +process.addObserver('e') +process.addObserver('ec',(ev)=>ev.test) +process.addObserver('pe',emitter) +process.addObserver('obs',tObs) +process.addObserver('pr',tPromise) + +console.log(process.size) +process.subscribe() + +emitter.emit('ec',{test:true}) +emitter.emit('e','sure') +emitter.emit('pe') diff --git a/package.json b/package.json new file mode 100644 index 0000000..3b71926 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "@uci-utils/ready", + "version": "0.1.1", + "description": "A Class to Observe the reduced to boolean combined state of a map of observables", + "main": "src/ready.js", + "scripts": { + "example": "node --r esm examples/example", + "example:dev": "UCI_ENV=dev ./node_modules/.bin/nodemon -r esm examples/example", + "test": "./node_modules/.bin/mocha -r esm --timeout 30000", + "testd": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000' || exit 0", + "testdd": "UCI_LOG_LEVEL='trace' npm run testd", + "testde": "UCI_LOG_LEVEL='warn' npm run testd", + "testl": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log 0 npm run test || exit 0" + }, + "author": "David Kebler", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/uCOMmandIt/.git" + }, + "keywords": [ + "node.js" + ], + "bugs": { + "url": "https://github.com/uCOMmandIt/uci-utils/issues" + }, + "homepage": "https://github.com/uCOMmandIt/uci-utils#readme", + "dependencies": { + "is-observable": "^2.0.0", + "p-is-promise": "^3.0.0", + "rxjs": "^6.5.4" + }, + "devDependencies": { + "chai": "^4.2.0", + "esm": "^3.2.25", + "mocha": "^6.2.2", + "nodemon": "^1.19.4" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5f38a03 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +### uCOMmandIt Ready Class + +Watches a list of events and/or promises that resolve to a boolean. When all a met it emits true otherwise at each event it emits false diff --git a/src/ready.js b/src/ready.js new file mode 100644 index 0000000..3a15ea6 --- /dev/null +++ b/src/ready.js @@ -0,0 +1,105 @@ +import { from, fromEvent, combineLatest } from 'rxjs' +import { map, startWith, tap } from 'rxjs/operators' +import isObservable from 'is-observable' +import isPromise from 'p-is-promise' + +import { createBoolean } from '../../to-boolean' + +const toBool = createBoolean({undefined: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 + this.condition = opts.condition || ( (ev) => toBool(ev) ) + this.subscriptions = new Map() + this._state = {} + this._updateObserversList() + this.handler = opts.handler || ( ([r,s]) => {console.log('ready:',r,'states:',s,'\n---------------------------')}) + } + + get state() {return this._state} + + addObserver(name, obs, condition ) { + if (!name) return false // name required + if (!(obs || this.emitter)) return false // some observable requried + if (typeof (obs ||{}).on ==='function') obs = fromEvent(obs,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 && this.emitter) obs = fromEvent(this.emitter,name) + if (!obs || !isObservable(obs)) return false + this.set(name, obs + .pipe( + tap((ev) => console.log(name,'emitted/resolved=>',ev)), + map(condition||this.condition), + tap((b) => console.log('boolean:',b)), + startWith(false), + ) + ) + this._updateObserversList() + return true + } + + removeObserver(names) { + if (!names) this.clear + else { + if (!Array.isArray(names)) names = [names] + names.forEach(name => { + this.delete(name) + }) + } + this._updateObserversList() + } + + subscribe(name, handler) { + if (typeof name ==='function') { + handler=name + name = null + } + this.subscriptions.set(name||'_primary', (name ? this.get(name):this._state).subscribe(handler||this.handler)) + } + + unsubscribe(name) { + this.subscriptions.get(name||'_primary').unsubscribe() + } + + _updateObserversList() { + this._state = combineLatest(Array.from(this.values())).pipe( + // tap((states)=>console.log('states',states)), + map(states=> { + return [states.reduce((res,state) => {return res && state},true), Array.from(this.keys()).map((name,index) => [name,states[index]])] + }) + ) + } +} + +// const toBoolean = (cond) => +// pipe( +// map(cond), +// startWith(false), +// ) + +const rTrue=['t','true','y','yes','on','positive','up','enabled','affirmative','yea'] +const rFalse=['f','false','n','no','off','negative','down','disabled','nope'] + +function castBoolean (opts={}) { + return (value => { + if (value===undefined) return opts.undefined + if (value===null) return opts.null + if (!isNaN(Number(value))) return Number(value) <1 ? false :true + if (typeof value==='string') { + value = value.trim() + value = value.toLowerCase() + if ((opts.rTrue || rTrue).includes(value)) return true + if ((opts.rFalse || rFalse).includes(value)) return false + } + return !!value + }) +} + +export default Ready +export { Ready } diff --git a/test/ready.test.js b/test/ready.test.js new file mode 100644 index 0000000..321dc3d --- /dev/null +++ b/test/ready.test.js @@ -0,0 +1,15 @@ +import { expect } from 'chai' +import Ready from '../src/ready' + +describe('', function () { + + it('Should include custom types', 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) + + }) + +})