1
\$\begingroup\$

I'm trying to build a deep proxy to observe all the changes in an object in javascript. Here is what I got so far :

function GetDeepProxy(value, config, path = 'root')
{
    var handler = function(config) {
        return {
            set: function(target, name, value) {
                if(typeof config.beforeSet === 'function')
                {
                    config.beforeSet(target, name, value, this.path);
                }

                target[name] = GetDeepProxyHelper(value, handler(config), this.path + '.' + name);

                if(typeof config.afterSet === 'function')
                {
                    config.afterSet(target, name, value, this.path);
                }
                return true;
            }
        }
    };

    var GetDeepProxyHelper = function(value, handler, path)
    {
        handler.path = path;
        if(typeof value === 'object')
        {
            for(var prop in value)
            {
                if(typeof value[prop] === 'object')
                {
                    value[prop] = GetDeepProxyHelper(value[prop], handler, path + '.' + prop);
                }
            }
            return new Proxy(value, handler);
        }
        else
        {
            return value;
        }
    }

    return GetDeepProxyHelper(value, handler(config), path);
}

You can then use it like that :

var myConfig = {
    afterSet: function(target, name, value, path)
    {
        console.log('set :', path + '.' + name + ' = ', value);
    },
    beforeSet: function(target, name, value, path)
    {
        //console.log('setting :', path + '.' + name + ' = ', value);
    }
}

var proxy = GetDeepProxy({}, myConfig, 'proxy');

proxy.test = [];
proxy.test.push('hi');
proxy.test[0] = 'abc';
proxy.test.push('def');
proxy.test = proxy.test.slice(1);
proxy.deep = {};
proxy.deep.abc = 'abc';
proxy.deep2 = { abc: { test: {} } };
proxy.deep2.abc.test.value = 'test';

console.log(proxy);
console.log(JSON.stringify(proxy));

Which print the following result

(index):160 set : root.test =  []
(index):160 set : root.test.0 =  hi
(index):160 set : root.test.length =  1
(index):160 set : root.test.0 =  abc
(index):160 set : root.test.1 =  def
(index):160 set : root.test.length =  2
(index):160 set : root.test =  ["def"]
(index):160 set : root.deep =  {}
(index):160 set : root.deep.abc =  abc
(index):160 set : root.deep2 =  {abc: Proxy}
(index):160 set : root.deep2.abc.test.value =  test
(index):180 Proxy {test: Proxy, deep: Proxy, deep2: Proxy}
(index):181 {"test":["def"],"deep":{"abc":"abc"},"deep2":{"abc":{"test":{"value":"test"}}}}

Is there any modifications that I won't be able to observe using this approach and should I structure the code differently?

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Missed 2, indirectly blind, know the rules, & chasing the tail.

There are four problems with the functionality. The last one will present you with the biggest challenge, as proxy traps can be CPU costly and adding a check (which involves keeping a reference for each object as you step the path) may well be impractically slow.

The two you missed.

There are two additional method for modifying an object directly

An object can be modified via delete

proxy.test.a = "a";
delete proxy.test.a;  // this modification will be missed

An object can be modified via defineProperty

Object.defineProperty(proxy.test, 'c', {
    writable : false,
    enumerable : false,
    configurable : false,
    value : "definedProp",
});

You will need to use the deleteProperty and defineProperty traps to get them.

The one you don't see.

Also you will not see modification that are indirect.

const testObj = {a : 0, b : 1};
proxy.test[0] = testObj;  // this is trapped
proxy.test[0].c = "c";    // you trap this as well

testObj.c = 10;           // you miss this

The rules.

Though I am not sure if this was the intention but you are also picking up on inherited properties that may have been protected. You need to check that you can write to the property.

Object.defineProperty(Array.prototype, 'foo', {
    writable : false,
    enumerable : false,   // Also not enumerable! should you be looking?
    configurable : false,
    value : "bar",
})
proxy.test = [];
proxy.test.foo = "poo"; // You trap this as if the assignment is ok
// yet
console.log(proxy.test.foo); // outputs "bar"

Chasing the tail.

And the ever present problem that you face when doing deep searches is you have no protection against cyclic references.

It`s a little tricky as i was sure a simple cyclic reference would overflow the call stack, but that did not happen. With a little experimentation I did find a way to make it crash.

const myObj1 = {a : 0};
const myObj2 = {a : myObj1}
proxy.test = [];
proxy.test.push(myObj1)
proxy.test.push(myObj2)
proxy.test[0].a = myObj2;  // all good to this point

// this causes endless iteration and eventually the call stack will throw
// RangeError: Maximum call stack size exceeded
proxy.test[0].b = myObj1;
\$\endgroup\$
4
  • \$\begingroup\$ Thanks for your answer. Is there any way to see indirect modifications? \$\endgroup\$
    – Gudradain
    Commented Oct 7, 2017 at 13:40
  • \$\begingroup\$ @Gudradain To trap the indirect modifications you would have to replace the object's reference/s with a proxy. You could deep scan the global scope for those references, and change them, but references can be hidden inside functions and closures that can not be accessed from outside, This is part of the Javascript security model and there is no workaround. \$\endgroup\$
    – Blindman67
    Commented Oct 7, 2017 at 17:15
  • \$\begingroup\$ Excellent answer. Are the names in the top of the answer part of some common lingo? (e.g. "indirectly blind", "chasing the tail") If so, is there a reference available somewhere for these and related terms/concepts? \$\endgroup\$
    – Kevin Beal
    Commented Dec 7, 2021 at 16:38
  • \$\begingroup\$ @KevinBeal No not part of any lingo I just made them up at the time. \$\endgroup\$
    – Blindman67
    Commented Dec 7, 2021 at 18:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.