uci-utils-rx-class/src/rx-class.js

322 lines
12 KiB
JavaScript

// import methods from './public-methods.js'
// native modules
import { EventEmitter } from 'events'
// reactive modules
import { BehaviorSubject, from } from 'rxjs'
import { distinctUntilChanged, skip } from 'rxjs/operators'
// uci modules
import { check } from '@uci-utils/type'
import bind from '@uci-utils/bind-funcs'
import { get as $get, set as $set, del as $del, walkPath, getKeys, pathToString } from '@uci-utils/obj-nested-prop'
import { logger } from '@uci-utils/logger'
// supporting
import equal from 'deep-equal'
import traverse from 'traverse'
const log = logger({ file: '/src/rx-class.js', package: '@uci-utils/rx-class', class: 'RxClass' })
// to pass rx options use 'Symbol.for('rx-class-opts')'
class RxClass extends EventEmitter {
constructor (opts = {}) {
super(opts)
const rxopts = opts[Symbol.for('rx-class-opts')] || {}
Object.defineProperty(this, '$$__rxns__$$', {
enumerable: false,
value: rxopts.namespace || 'rx'
})
this[this.$$__rxns__$$] = bind(methods, this)
this[this.$$__rxns__$$].props = {} // where reactive property info is stored including actual values
this[this.$$__rxns__$$].opts = {
emitter: rxopts.emitter != null ? rxopts.emitter : true,
event: rxopts.event === false ? false : rxopts.event || 'changed',
handler: rxopts.handler, // default subscription handler
amendValue: rxopts.amendValue,
// amend_path: rxopts.amend_path, // by default amend value with path
hook: rxopts.hook, // function called when setting
namespace: this.$$__rxns__$$, // TODO namespace to added to all public rx methods
operators: new Map(), // TODO added to all rx pipes, in order
skip: rxopts.skip == null ? 1 : rxopts.skip
}
console.log(`${rxopts.getSetPrefix || ''}set`)
$set(this, [rxopts.getSetNamespace, `${rxopts.getSetPrefix || ''}set`], set.bind(this))
$set(this, [rxopts.getSetNamespace, `${rxopts.getSetPrefix || ''}get`], get.bind(this))
console.log(this)
} // end constructor
// reactive methods are bound at instantiation
} // end class
// helpers
export default RxClass
export { RxClass, $set as set, $get as get, $del as del, getKeys, walkPath, pathToString }
function get (path) {
const value = _.get.call(this, path)
if (!check.isPlainObject(value)) return value
const obj = {} // return a copy with actual values instead of getters
const self = this
traverse(_.get.call(this, path)).map(function () {
if (this.isLeaf) {
_.set.call(self, obj, this.path, this.node)
}
}
)
return obj
}
function set (path, value, opts = {}) {
console.log('set:', path, value, opts, !this[this.$$__rxns__$$].isRx(path))
if (!opts.noRx && !this[this.$$__rxns__$$].isRx(path)) { // travsere
opts = Object.assign(
(check.isPlainObject(value) && opts.traverse !== false)
? { values: value, traverse: true }
: { value: value }
, opts)
console.log('adding rx from set', opts)
this[this.$$__rxns__$$].add(path, opts)
} else {
const curValue = _.get.call(this, path)
if (!equal(curValue, value) && value !== undefined) {
console.log('setting already reactive value', path, value)
_.set.call(this, path, value)
console.log('value that was set', _.get.call(this, path))
}
return _.get.call(this, path)
}
}
// public methods will be namespaced (default is rx.)
const methods = {
add: function add (path, opts = {}) {
if (opts.traverse) { // will add rx to all leaves
const obj = opts.values ? opts.values : _.get.call(this, path)
const self = this
// console.log('object to traverse', obj)
traverse(obj).map(function () {
// console.log('traversing', this)
if (this.isLeaf) {
const lpath = this.path
lpath.unshift(path)
// console.log('#{!opts.values ? \'existing\':\'new\'} leaf on path \'#{lpath}\' with value: #{this.node}')
self[this.$$__rxns__$$].add(lpath, { value: this.node })
}
}
)
return true
} // end traverse
const value = _.get.call(this, path) // current value
if (value === undefined) {
// console.log('no current property or value for', path, 'creating temporary null')
_.set.call(this, path, null)
}
if (this[this.$$__rxns__$$].isRx(path) && !opts.force) return true
console.log(path, 'making reactive')
const { parent, name } = _.rxGetObj.call(this, path, '__parent__')
log.debug({
class: 'RxClass',
method: 'rx.add',
path: path,
name: name,
parent: parent,
msg: 'got name and parent'
})
_.set.call(this, [this.$$__rxns__$$, 'props', path], {})
const rx = _.get.call(this, [this.$$__rxns__$$, 'props', path])
// console.log('moving',opts.value != null ? opts.value : value,path)
rx.value = opts.value != null ? opts.value : value
rx.path = path
rx.amendValue = opts.amendValue || this[this.$$__rxns__$$].opts.amendValue || ((val) => val)
rx.obs = from(new BehaviorSubject(rx.amendValue(rx.value, rx.path)).pipe(
skip(opts.skip != null ? opts.skip : this[this.$$__rxns__$$].opts.skip),
distinctUntilChanged() // allow custom comparator
// allow custom/additional operators here
// takeUntil(#get(this.#deleted,path))
))
// console.log(path,'---------------\n',Object.getOwnPropertyNames(Object.getPrototypeOf(rx.obs)),rx.obs.subscribe)
rx.subs = {}
rx.hook = opts.hook
if (check.isFunction(this[this.$$__rxns__$$].opts.handler)) this[this.$$__rxns__$$].subscribe(rx, this[this.$$__rxns__$$].opts.handler, '_default_')
const subs = Object.entries(opts.subscribe || {})
subs.forEach(sub => {
if (check.isFunction(sub[1])) this[this.$$__rxns__$$].subscribe(rx, sub[1], sub[0])
})
const self = this
Object.defineProperty(parent, name, {
configurable: true,
enumerable: true,
get () {
return rx.value
},
set (value) {
rx.value = value
value = rx.amendValue(value, path)
rx.obs.next(value) // react
if (self[this.$$__rxns__$$].opts.emitter) { // emit
if (opts.event) self.emit(opts.event, value, path) // custom event
if (self[this.$$__rxns__$$].opts.event) self.emit(self[this.$$__rxns__$$].opts.event, value, path) // global event
const spath = pathToString(path)
if (spath) self.emit(spath, value, path) // also emit path if is stringable.
self.emit(name, value, path)
}
// any hook function that is already bound will use that context, otherwise this
// hooks
// TODO write with for loop and async await
if (check.isFunction(self[this.$$__rxns__$$].opts.hook)) self[this.$$__rxns__$$].opts.hook.call(self, value) // global hook
if (check.isFunction(rx.hook)) rx.hook.call(self, value) // property hook
}
})
// console.log('rxadd done', path, opts, _.get.call(this,[this.$$__rxns__$$,'props', path]))
return true
},
amendValue: function amendValue (path, func) {
if (arguments.length === 0) { this[this.$$__rxns__$$].opts.amendValue = null; return }
if (typeof path === 'function') { this[this.$$__rxns__$$].opts.amendValue = path; return }
const rx = _.rxGetObj.call(this, path)
if (!check.isEmptyPlainObject(rx)) return false
func = func || (val => val)
rx.amendValue = func === 'path' ? (value, path) => { return { value: value, path: path } } : func
return !!rx.amendValue
},
// if name is only passed remove
operator: function Operator (func, name) {
this[this.$$__rxns__$$].opts.operators[name] = func
},
// removes obser, subscriptions, and getter and setter and make back into plain value
remove: function remove (path, opts = {}) {
if (!opts.confirm) return false // must confirm to remove
if (!this[this.$$__rxns__$$].isRx(path) && check.isPlainObject(_.get.call(this, path))) {
const self = this
traverse(_.get.call(this, path)).map(function () {
if (this.isLeaf) {
const lpath = this.path
lpath.unshift(path)
if (self[this.$$__rxns__$$].isRx(lpath)) self[this.$$__rxns__$$].remove(lpath, { confirm: true })
}
})
// all done removing the leaves so remove branch
_.del.call(this, [this.$$__rxns__$$, 'props', path], true)
return true
}
const { parent, name, parentPath } = _.rxGetObj.call(this, path, '__parent__')
const rxparent = _.get.call(this, [this.$$__rxns__$$, 'props', parentPath])
const rx = rxparent[name]
if (!rx) return true // // not reactive nothing to remove
Object.values(rx.subs).forEach(sub => sub.unsubscribe())
delete parent[name]
parent[name] = rx.value
delete rxparent[name]
// console.log('removed rx from', path)
return true
},
getObs: function getObs (path) {
return (_.rxGetObj.call(this, path) || {}).obs
},
getSubs: function getSubs (path, name) {
const rx = _.rxGetObj.call(this, path)
if (rx) return (name ? rx.subs[name] : Object.keys(rx.subs))
},
// pass rx as object or path
subscribe: function subscribe (path, handler, name) {
const rx = _.rxGetObj.call(this, path)
if (rx) {
// TODO if no name generate a name and return with handle
if (check.isFunction(handler)) {
if (rx.subs[name]) rx.subs[name].unsubscribe() // replace if exits
rx.subs[name] = rx.obs.subscribe(handler)
// console.log(rx.path, 'current subscriptions---', Object.keys(rx.subs))
return { subs: rx.subs[name], name: name }
}
}
return false
},
removeSubs: function removeSubs (path, name) {
const rx = _.rxGetObj.call(this, path)
// if (name && name!=='_default' && (rxObj||{}).path) console.log('rx object for',rxObj.path)
if (rx) {
if (name) {
if (rx.subs[name]) {
rx.subs[name].unsubscribe()
delete rx.subs[name]
return true
}
} else {
Object.values(rx.subs).forEach(sub => sub.unsubscribe())
rx.subs = {}
return true
}
}
return false
},
isGetSet: function isGetSet (path) {
const keys = getKeys(path)
const prop = keys.pop()
const parent = _.get.call(this, keys)
// console.log('isgetset', path, keys, prop, 'parent?', !!parent)
if (parent && prop) {
return !!(Object.getOwnPropertyDescriptor(parent, prop) || {}).get &&
!!(Object.getOwnPropertyDescriptor(parent, prop) || {}).set
}
return false
},
// is already reactive, checks for setter/getter and the corresponding rx object
isRx: function isRx (path) {
// console.log('in isRX', path)
const keys = getKeys(path)
const cnt = keys.length
for (let index = 0; index < cnt; index++) {
// console.log('testing rx', index, cnt, keys, !!_.get.call(this,keys), !!this.isGetSet(keys), !!_.get.call(this,[this.$$__rxns__$$,'props', keys]))
if (_.get.call(this, keys) === undefined) return false
if (this[this.$$__rxns__$$].isGetSet(keys) && _.get.call(this, [this.$$__rxns__$$, 'props', keys])) return true
keys.pop()
}
return false
}
}
// private methods
const _ = {
rxGetObj: function rxGetObj (path, prop) {
log.debug({ class: 'RxClass', method: '_.rxGetObj', path: path, prop: prop, msg: 'getting rx object' })
let name, parent, parentPath, rx
// if (!path) parent = this
if (check.isString(path) || check.isArray(path)) { // it's a normal path
const keys = getKeys(path)
name = (keys || []).pop()
parent = _.get.call(this, keys)
parentPath = keys
log.debug({ method: '_.rxGetObj', path: path, prop: prop, key: name, parent: parent, msg: 'getting rx object' })
if (parent === null) parent = {}
if (prop === '__parent__') return { name: name, parent: parent, parentPath: parentPath }
rx = _.get.call(this, [this.$$__rxns__$$, 'props', path])
} else rx = check.isPlainObject(path) ? path : rx // if path was plain object assume it's already rx object
return prop ? (rx || {})[prop] : rx
},
set: function set () {
const args = [...arguments]
if (args.length < 2) return false
if (args.length === 2) args.unshift(this)
else if (!args[0]) return false
// console.log('in #set',args[1],args[2])
return $set(...args)
},
get: function get () {
const args = [...arguments]
if (args.length === 1) args.unshift(this)
return $get(...args)
},
del: function del () {
const args = [...arguments]
if (args.length < 2) return false
if (args[args.length - 1] !== true) return false
if (args.length === 2) args.unshift(this)
return $del(...args)
}
}