nx-js / observer-util

Transparent reactivity with 100% language coverage. Made with ❤️ and ES6 Proxies.
MIT License
1.2k stars 94 forks source link

Is it possible to receive the type of change when observing a value? #34

Closed Dai696 closed 5 years ago

Dai696 commented 5 years ago

I'm trying to observe an array of objects and see what type of change was made.

If the array had stuff removed, I wanna call a specific function to respond to that.

If the array had stuff added, I wanna call a different function.

In mobX this was rather straight forward...observe would just return an object that could tell u a list of if things changed, added, or removed...

I was curious if you had an easy way to observe a type of change.

Edit*( Not only just observe the type of change, but maybe return the values changed?)

Like if 2 objects were pushed into the array, it return added: [object1, object 2]

I think mobx had something where it give u a list of objects that changed...

So if I added 2 objects, I'd get an array of added with both objects

If I had removed 2 objects, I'd get an array of removed of both objects)

Worst case scenerio... I maybe try to cache a second copy of the array, and compare a changed version to the old version using a function to determine the type of change...

Maybe thats the intended way to do it.

Again, this library is amazing. Thank you so much for how easy it is to use!

solkimicreb commented 5 years ago

I missed this issue, sorry. Answer coming soon for this one and your other one.

solkimicreb commented 5 years ago

You can check the debugger option. This lib can react to a lot of different mutations and the debugger is called with some contextual data about each mutation. I think you should try it and see if it is what you need. If not, we can continue the discussion.

Implementing something very similar to mobx's observe could be done like this:

const obj = observable({})

// this calls `console.log` with some contextual data on every change at any depth of obj (because of JSON.stringify)
/// you can replace `console.log` with your own function to do something with the data
// or replace `JSON.stringify` for a more detailed observation.
observe(() => JSON.stringify(obj), { debugger: console.log })
Dai696 commented 5 years ago

Oki great! I got the scheduler working but now I'm onto the debugger. I see that the debugger has a "add" and a "delete" that shows me which thing got added/deleted

The issue I have now is using that info in the actual reaction.

I don't think their is anyway to feed the debugger data into the original reaction.

My goal is to combine the debugger with the scheduler

to analyize the changed array data. Scheduler works fine now with just using critical priority which is great! but i wanna be able to use the debugger data within the schedueled reaction.

Do I just cache the command I need and then use that inside the reaction function?


My current test code...

const scheduler = new Queue(priorities.CRITICAL);
        this.observeArrayTest = observe(()=>Manager.mapInstances.light.length
            , {scheduler: scheduler, debugger: console.log});

If I can get this solved this will remove my need for the hacked caching stuff I use right now for my observable.

(Which is some really awful code, I'm hoping to simplfy this process by reading the added/deleted from the debugger...instead of checking myself)


My actual code right now. Excuse how messy it is.


const scheduler = new Queue(priorities.CRITICAL);
this.observerMarque = observe(()=> this.forceObjectMapChange(), {scheduler: scheduler})

$.forceObjectMapChange = function() {

        const cache = Object.assign([],raw(this.mapStoreCache));
        const current = Manager.mapInstances.light;
        const added = [];
        const removed = [];
        for (const i in current) {
            if (!cache[i] || current[i].ID !== cache[i].ID)
                added.push(current[i]);

        }
        for (const i in cache) {
            if (!current[i] || cache[i].ID !== current[i].ID) {
                removed.push(cache[i]);
            }
        }

        added.forEach((obj) => {
            this.addObject(obj);
        });

        removed.forEach((obj) => {
            this.removeObject(obj);
        });

        this.mapStoreCache = JSON.parse(JSON.stringify(raw(Manager.mapInstances))).light

    };

Thanks for ur amazing support btw!

Dai696 commented 5 years ago

One other thing, I noticed some strange behavior with observable arrays (or an object) and setting a none observable to it.

I have a undo/redo store which I use to hold a stack record of changes.

I experimented setting my observable map instance array to equal a none observable version of itself from the history stack when undoing.

I'd add an object to my scene... (this pushes an observable object into the map instance store.

Then I'd save a snapshot (which is just a regular object version of the instance store saved in the history store stack)

Then I'd delete the object I placed in the map.

Then call undo, setting the observable instance store to the regular instance store saved in the history stack .

It'd run the observe that reads the instance store array and update the map based on added/remove d objects (undoing the deletion of the object)

(The observable is the last function in my previous post before this one)


However... that same observe I set doesn't work properly anymore after that. It won't fire if I add new items to the array. So I thought somehow the observe connection was lost due to me setting the observable array to a regular array.

Strangely if I delete stuff the observe works... and it works if I add more snapshots to the history... and undo with that new snapshot...

For example if I add a bunch of objects (the observe wont fire) but if I save a snapshot... add more objects (observe still doesn't fire) then redo... (the observe suddenly fires)

When I checked if the observable array was still a observable, it said it was....

I'm really confused at this behavior... It wont allow the observable to update when pushing to it... but it will update when removing from it... (so splicing)

I'd really love if u could help me figure this one out...

This is really odd behavior to me... even if I did a lot of things wrong... I'm not sure it should be acting this way


Below i posted most relevant code... I apologize for being spammy.

I'm so close to fixing this and I feel like you could help me out.

Thank you again so much for ur help so far!


Ex of my history code that creates this behavior

 addSnapshot(store = this.mapInstances, config) {
            this.historyStore.stack.push(JSON.parse(JSON.stringify(store)))
            this.historyStore.index = this.historyStore.stack.length-1
        }

        get stack() {
            return this.historyStore.stack;
        }

        undo() {
            this.mapInstances.light = this.historyStore.stack[this.historyStore.index].light
        }

this.mapInstances is just a getter to the map stores current map data

     get mapInstances() {
            if (this.mapStore.currentMap === -1) return [];
            return this.mapStore.map[this.mapStore.currentMap]

        }

my map store


import uniqid from 'uniqid'
import {store} from 'react-easy-state'
import { observable, observe, isObservable } from '@nx-js/observer-util';
//console.log(observable)

const mapStore = store({
    currentMap: observable(0),
    map: observable([
        observable({
            light: observable([]),
            sprites: observable([]),
            collisions: observable([]),
            shadowCasters: observable([]),
            particles: observable([]),
            events: observable([])
        })
    ]),
    selectedMapInstances: observable([])

});

//console.log(isObservable(mapStore.map[0].light))
//observe(()=> console.log(mapStore.map[0].light.length))
export default mapStore

my history store


import {store} from 'react-easy-state'

const historyStore = store({
    index: -1,
    stack: []
});

export default historyStore

my add and remove objects code

       addMapObject(mode, obj) {
            this.mapInstances[mode].push(obj); // add object
        }

        removeMapObject(obj) {
            const argumentsLength = arguments.length;
            if (argumentsLength > 1) {

                for (let i = 0; i < argumentsLength; ++i) {
                    this.removeMapObject(arguments[0]);

                }
            } else {
                const {type} = obj;//gets object type

                const index = this.mapInstances[type].map(function (x) {
                    return x.ID;
                }).indexOf(obj.ID);
                this.mapInstances[type].splice(index, 1);
            }
        }
Dai696 commented 5 years ago

Oki I got most of what I needed working, however I was curious if their was a way to combine the debugger with the scheduler.

Only way I found out how so far was to cache the debugger data, and then access that inside the observe or queue functions. Is this the intended way?

Also it a bit strange that the first function in the observe checks if the observable should be fired, but the scheduler doesn't. However it does have function support if you use a queue.

Is the intended behavior to just have the first observable function check if the observe should even happen, then use the queue schedulers functions to fire on observe?

Or just use the first function to observe and fire reactions?

Or is either fine... its just context that matters.

solkimicreb commented 5 years ago

I think I mislead you a bit with the debugger tip. It is really not meant to do what you are doing, it's just a utility to debug complex reaction chains. A mobx observe like functionality can be implemented fairly simple on top of observer-util. (This is why it is not part of the lib, I try to keep it low level.)

import { observe, unobserve, observable } from '@nx-js/observer-util'

// you can add a scheduler too, if you would like one
function myObserve (obj, prop, cb ) {
  let oldValue = obj[prop]
  return observe(() => {
     const newValue = obj[prop]
     cb(obj, prop, newValue, oldValue)
     oldValue = newValue
  })
}

const person = observable({
  name: 'Bob',
  age: 100
})

// this will be called when person.name changes
const reaction = myObserve(person, 'name', (obj, prop, value, oldValue) => {
  // do something based on old and new value here
})

// you can unobserve it any time
unobserve(reaction)

Arrays are a bit more complex since they have complex compound mutations. You could do domething like:

import { observe, unobserve, observable } from '@nx-js/observer-util'

// in case of arrays I definitely recommend a scheduler
// otherwise the observed function could fire a lot of times for compound mutations (like splice)
function observeArray (array, cb) {
  let oldArray = Array.from(array)
  return observe(() => {
     // maybe create some more digestiable data from array and oldArray
     cb(array, oldArray)
     oldArray = Array.from(array)
  }, { scheduler: yourPreferredScheduler })
}

const nums = observable([1, 2, 3, 4])

// this will be called when person.name changes
const reaction = myObserve(nums, (nums, oldNums) => {
  // do something based on the old and new array
})

// you can unobserve it any time
unobserve(reaction)

If you need a more digestible diff format you could add some array diffing to the observeArray function. There plenty of packages out there for that, maybe check this one.

Does this help? Are you looking for a more complex observe functionality? Are you interested in the scheduler addition?

Dai696 commented 5 years ago

Thank you so much for the detailed response! I think this is what I'm aiming for!

As long as I can monitor the array changes and not have to compare entire arrays with new ones I'm happy.

Just being able to observe pushed data and sliced data is what I'm looking for in the array.

That way if I add items to the map, I can react to those added. If I removed I can also react.

How would you go about just observing if something was added or removed to the array (without needing to check an old one and compare the values?)

Edit*

I was reading this article on observable arrays https://stackoverflow.com/questions/5100376/how-to-watch-for-array-changes

The 3rd method talks about using proxies, so I was thinking it may be possible already natively to observe specific method changes

I mostly just wanna see if the array slice or pushes, but pop, shift, unshift, ect... would be nice too.

Is this easily possible?

Dai696 commented 5 years ago

Oh wow I'm slow, I didn't notice you provided info for differing arrays.

Ill take a look and see what I can do... Thank you!

Dai696 commented 5 years ago

Awesome I got this working with fast-array-diff!

Thank you so much for your help!!