// 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) } }