A working class, only missing the tested hook and operator support

master
David Kebler 2020-06-07 21:56:54 -07:00
parent f1e4dac3a0
commit 47e4c34ef5
9 changed files with 398 additions and 0 deletions

37
.eslintrc.js Normal file
View File

@ -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"
]
}
}

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules/
/coverage/

5
.npmignore Normal file
View File

@ -0,0 +1,5 @@
tests/
test/
*.test.js
testing/
examples/

63
examples/example.js Normal file
View File

@ -0,0 +1,63 @@
import RxClass from '../src/rx-class.js'
class Example extends RxClass {
constructor(opts={}){
super(opts)
this.test = 10
this.another = { bar:'bust', foo:3}
}
}
const example = new Example({
_rx_ : {
handler: value => {console.log('-----------default subscription handler-------', value)}
}
})
console.log('instance before doing rx on properties\n')
console.log(example)
console.log('--------------------------')
example.on('changed', (prop) => console.log('emitted----a property changed-----',prop))
example.on('test', (val) => console.log('emitted value of test',val))
console.log('making \'test\' reactive')
example.rxAdd('test')
console.log('changing test to 20, 30 directly')
example.test = 20
example.test = 30
example.rxSubscribe('test',(prop)=>console.log('#############late subscription to \'test\' after 30 ############',prop),'custom')
console.log('changing test to 40 via setPath')
example.setPath('test',40)
console.log('making a deeper property \'another.foo\' reactive')
example.rxAdd('another.foo')
example.another.foo = 7
console.log('direct access of another.foo',example.another.foo)
console.log('access via get of another.foo',example.getPath('another.foo'))
console.log('making a deeper property \'some.thing\' reactive that did not exist')
example.rxAdd('some.thing')
// console.log('some test',example.some.test)
example.some.thing=15
console.log('--------------------------')
console.log('instance after adding rx to properties\n')
console.log(example)
console.log('--------------------------')
console.log('now removing reactivity from \'test\'')
example.rxRemove('test')
example.setPath('test',42)
console.log('\'test\' now holds unreactive value')
console.log('now removing reactivity from \'test\' again')
example.rxRemove('test')
console.log('--------------------------')
console.log('instance after removing rx from \'test\'\n')
console.log(example)
console.log('--------------------------')
// console.log('some test',example.some.test)

47
examples/traverse.js Normal file
View File

@ -0,0 +1,47 @@
import RxClass from '../src/rx-class.js'
class Example extends RxClass {
constructor(opts={}){
super(opts)
this.test = {
yesterday: {bar:'bust', foo:3},
today: {bing:5, bong:{who:'first',what:'second'}}
}
}
}
const example = new Example({
_rx_ : {
handler: value => {console.log('-----------default subscription handler-------', value)},
skip: 0
}
})
console.log('instance before doing rx on properties\n')
console.log(example)
console.log('--------------------------')
example.on('changed', (prop) => console.log('emitted----a property changed-----',prop))
example.on('test', (val) => console.log('emitted value of test',val))
console.log('traversing \'test\' and making all leaves reactive')
example.rxAdd('test',{traverse:true})
example.test.today.bong.what = 'third'
example.setPath('test.today.bong.what','forth')
example.setPath('test.today.bong.why','do not know')
console.log('\'test\', values object >', example.getPath('test'))
console.log('\'test\', raw object >', example.getPath('test',true))
console.log('--------------------------')
console.log('instance after adding rx to leaves\n')
console.dir(example.test)
console.dir(example._rx_.props)
console.log('--------------------------')
console.log('now remove all rx on \'test.yesterday\'\n')
example.rxRemove('test.today',{traverse:true})
console.dir(example.test)
console.log('rx props')
console.dir(example._rx_.props)
console.log('--------------------------')
console.dir(example)

3
nodemon.json Normal file
View File

@ -0,0 +1,3 @@
{
"ignore":["examples/*.json"]
}

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "@uci-utils/data-store-rx",
"version": "0.1.3",
"description": "A Extended Class Data Store Module ",
"main": "src/datastore.js",
"scripts": {
"example": "node -r esm examples/example",
"example:dev": "./node_modules/.bin/nodemon -r esm examples/example",
"example:tra": "./node_modules/.bin/nodemon -r esm examples/traverse",
"test": "./node_modules/.bin/mocha -r esm --timeout 30000"
},
"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": {
"data-store": "^4.0.3",
"deep-equal": "^2.0.3",
"get-value": "^3.0.1",
"is-plain-obj": "^2.1.0",
"is-plain-object": "^3.0.0",
"rxjs": "^6.5.5",
"set-value": "^3.0.2",
"traverse": "^0.6.6",
"unset-value": "^1.0.0"
},
"devDependencies": {
"chai": "^4.2.0",
"esm": "^3.2.25",
"mocha": "^7.2.0",
"nodemon": "^2.0.4"
}
}

18
readme.md Normal file
View File

@ -0,0 +1,18 @@
# uCOMmandIt Reactive Class
A Great Class to Extend has built in support for make reactive any/all class properties and their nested leaves.
## Why?
To have reactivity similar to vue and react but in any backend object?
This allow a backend app to maintain app settings and allow "clients" to get updated values when the app changes state. Essentially will create a 'mini' state event bus.
## Features
Will both emit and allow subscriptions to any property 'path' that has been made reactive.
Allows custom registration of unlimited state change event hooks.
Allows additional rxjs operators for the pipe the observer for each property.
See Examples folder

181
src/rx-class.js Normal file
View File

@ -0,0 +1,181 @@
import { EventEmitter } from 'events'
import { BehaviorSubject, from } from 'rxjs'
import { distinctUntilChanged, skip } from 'rxjs/operators'
// ** nested object access **
import _get from 'get-value'
import _set from 'set-value'
import _del from 'unset-value'
// *********************
import equal from 'deep-equal'
import traverse from 'traverse'
import isPlainObject from 'is-plain-object'
export default class RxClass extends EventEmitter {
constructor(opts){
// console.log('constructor', opts)
super(opts)
this._rx_ = {
event: opts._rx_.event || 'changed',
handler: opts._rx_.handler,
props: {},
hooks: [],
operators:[],
skip: opts._rx_.skip == null ? 1 : opts._rx_.skip
}
}
// if property exists it moves value to _ version and creates getter and setter and behavior subject
rxAdd(opath,opts={}) {
const path = this._rxFormatPath(opath)
if (opts.traverse) {
const self = this
traverse(_get(this,path)).map(function () {
if (this.isLeaf) {
const lpath = this.path
lpath.unshift(path)
// console.log('leaf',lpath,this.node)
self.rxAdd(lpath)
// console.log(this.get())
}
}
)
return
} // end traverse
const value = _get(this,path)
if (value === undefined) {
console.log ('no current property for', path)
_set(this,path,null)
}
const {parent,name} = this.rxGetObj(path,'__parent__')
const rx = this.setPath(['_rx_.props',path],{})
rx.value = value
rx.obs = from(new BehaviorSubject({value:value, path:path}).pipe(
// rx.obs = from(new ReplaySubject(1).pipe(
skip(this._rx_.skip),
distinctUntilChanged()
// ,
// takeUntil(_get(this._deleted,path))
))
rx.subs = {}
this.rxSubscribe(rx,this._rx_.handler,'_default')
this.rxSubscribe(rx,(opts.subscribe ||{}).handler,(opts.subscribe || {}).name)
const self = this
Object.defineProperty(parent, name, {
configurable: true,
enumerable: true,
get () {return rx.value},
set (value) {
rx.value = value
rx.obs.next({value:value, path:path})
self.emit(opts.event || self._rx_.event,{value:value, path:path})
self.emit(path,value)
self._rx_.hooks.forEach(hook=>hook.call(self,{value:value, path:path}))
}
})
}
// if name is only passed remove
rxHook(func,name) {
this._rx_.hooks[name]=func
}
// if name is only passed remove
rxOperator(func,name) {
this._rx_.operators[name]=func
}
// pass '__new__' when getting props for new rx object
rxGetObj(path,prop) {
path = this._rxFormatPath(path)
path = path.split('.')
const name = path.pop()
path = path.join('.')
let parent = path ? _get(this,path) : this
// console.log(name,path,parent)
if (parent === null) parent = {}
if (prop==='__parent__') return {name:name, parent:parent, parentPath:path}
const rx = this.getPath(['_rx_.props',path,name],true)
// console.log(path,name,'reactive object',rx)
return prop ? (rx || {})[prop] : rx
}
// removes obser, subscriptions, and getter and setter and make back into plain value
rxRemove(path,opts={}) {
if (opts.traverse) {
const self = this
traverse(_get(this,path)).map(function () {
if (this.isLeaf) {
const lpath = this.path
lpath.unshift(path)
self.rxRemove(lpath)
}
}
)
this.delPath(['_rx_.props',path],true)
return
}
let { parent, name, parentPath } = this.rxGetObj(path,'__parent__')
const rxparent = this.getPath(['_rx_.props',parentPath],true)
const rx = rxparent[name]
if (rx && Object.getOwnPropertyDescriptor(parent, name)['get']) {
Object.values(rx.subs).forEach(sub=>sub.unsubscribe())
delete parent[name]
parent[name] = rx.value
delete rxparent[name]
} // else console.warn(`property at '${path}' was not reative, remove aborted`)
}
// pass rx as object or path
rxSubscribe(rxObj,handler,name){
// console.log('subscription',rxObj)
if (typeof name !== 'string') return
rxObj = (typeof rxObj === 'string') ? this.rxGetObj(rxObj) : rxObj
if (typeof handler==='function') rxObj.subs[name] = rxObj.obs.subscribe(handler)
}
_rxFormatPath(path) {
if (path==null) return path
if (Array.isArray(path)) {
path = path.filter(Boolean).join('.')
} else path = path.replace(/\/:/g, '.')
return path
}
setObjPath () {return _set(...arguments)}
getObjPath () {return _get(...arguments)}
delObjPath () { return _del(...arguments)}
delPath(path,confirm) {
path = this._rxFormatPath(path)
if (confirm) _del(this,path)
else console.warn('must confirm to delete path',path)
}
getPath(path,raw){
path = this._rxFormatPath(path)
const value = _get(this,path)
if (!isPlainObject(value) || raw) return value
let obj = {}
traverse(_get(this,path)).map(function () {
if (this.isLeaf) {
// let leaf = this.path // this path is immutable
// console.log(this.path,this.node)
// leaf = leaf.pop()
_set(obj,this.path.join('.'),this.node)
}
}
)
return obj
}
setPath(path,value){
path = this._rxFormatPath(path)
const curValue = _get(this,path)
if (!equal(curValue,value) || !value) {
_set(this,path,value)
}
return _get(this,path)
}
} // end class