4

I have an ES6 Proxy which contains other deeply nested Proxies (generated in the get trap of the root proxy). All of them use the same trap handler.

When I try to get the value of a deeply nested object in this collection, the getter is called for every deeply nested object encountered. It is imperative that a record of this so-called 'path' through the object is known after the sequence of gets is complete. For example, calling:

console.log(proxy.shallow.deep);

is a 'chain' of two get operations, on shallow and deep in sequence, both of which are pushed to an array acting as a breadcrumb trail of properties accessed in the chain, like:

console.log(path); // ['shallow', 'deep']

There's a problem with this, though. In any sequence of gets, it's not possible to know what the final get in the chain is - every get to the proxy is treated as an atomic action so there is no intrinsic notion of what property is the last to be called. This means in subsequent 'get chains', the path array already has values associated with it. I've written some code below to illustrate the structure of my proxy and the problem I'm encountering:

let nestedObject = { shallow: { deep: 0 }};
let path = [];
let proxy = new Proxy(nestedObject, {
    get(target, prop) {
        path.push(prop);
        let out = target[prop];
        // this ensures each get to the proxy (whenever the value at property 
        // is an object), returns that object as a new proxy, 
        // else return the value of the property
        // this means the get/set traps are called for each deeply nested object
        return typeof out === 'object' ? new Proxy(out, this) : out;
    },
    set(target, prop, val) {
        path.push(prop);
        target[prop] = val;
        path.length = 0;
    }
});

// getting these two pushes both properties to the path array
// but there's no way of knowing that the get to 'deep' was the final 
// get in the chain - therefore it cannot be reset for the next get chain
proxy.shallow.deep;
// path: ['shallow', 'deep']
console.log(path);

// because of this, I can't figure out how the code should know that the path
// array must be reset - so subsequent gets just add more values to the path array,
// and there are erroneous elements in the array when the next chain starts
proxy.shallow.deep
// path: ['shallow', 'deep', 'shallow', 'deep']
console.log(path);

// setting a value is fine however, since a set action will always be the last action
// it pushes 'shallow' to the array in the getter and 'deep' in setter. 
// The setter can then clear the path array.
proxy.shallow.deep = 1;
// path: []
console.log(path);

So what I'm wondering is: Is it possible to know what the last 'get' is in the chain somehow, and/or if there's a way to perhaps call a function whenever a chain of gets has completed?

Thanks for the help!

jonny
  • 3,022
  • 1
  • 17
  • 30
  • If getters may be random then you need to require that last item in the chain to be a call, like _proxy.shallow.deep()_. If you have some vocabulary, then you can react to specific phrases. – Yurii Feb 12 '18 at 21:02
  • You could probably create a shared collection for all the proxies, and create a `Promise` that resolves when the returned value is invoked, or when a property on the value is accessed, or when a property on a _different_ value is accessed, which would indicate the end of the current chain has been reached. However, this forces the values at the end of the chain to be invoked asynchronously, which may not be what you want. – Patrick Roberts Feb 12 '18 at 21:07

2 Answers2

1

I think I've found a reasonable solution for me and it involves very few changes to the code. It involves attaching a path variable to each successive trap handler, the value of which is the accumulated values of each previous handler's path value. Like so:

let nestedObject = { shallow: { deep: {deepest: 0 }}};
let path = [];
let proxy = new Proxy(nestedObject, {
    get(target, prop) {
        let out = target[prop];
        // make a copy of this handler for the nested proxy to use
        let copy = Object.assign({}, this);
        // if this handler contains no path variable, add it
        // otherwise concatenate it with the previous handler's path 
        !this.path ?
            Object.assign(copy, {path: [prop]}) :
            Object.assign(copy, {path: this.path.concat(prop)});
        path = copy.path;
        return typeof out === 'object' ?
            new Proxy(out, copy) :
            out;
    },
    set(target, prop, val) {
        path = this.path.concat(prop);
        target[prop] = val;
    }
});

proxy.shallow;
console.log(path); // ['shallow']
proxy.shallow.deep;
console.log(path); // ['shallow', 'deep']
proxy.shallow.deep.deepest;
console.log(path); // ['shallow', 'deep', 'deepest']
proxy.shallow.deep.deepest = 1;
console.log(path); // ['shallow', 'deep', 'deepest']

I'm not sure if this is the optimum solution, but it works. Though I'd still love to see any alternative ways of implementing this.

Thanks for the help guys.

jonny
  • 3,022
  • 1
  • 17
  • 30
  • 1
    Using an immutable path value for each proxy is definitely the way to go, however of course there is no way to detect an "end" other than by inspecting the path at the right time (usually not done with a global variable, but rather using the result of the path expression). – Bergi Feb 12 '18 at 21:40
  • 1
    @Bergi actually I only used a global for illustrative purposes, in production I emit an event from within the function itself – jonny Feb 13 '18 at 01:41
  • 1
    That's still basically a global, static channel, regardless whether it exposes anything in the global variable scope or not. And the subscriber(s) of the event are still decoupled from the actual path expression. You still haven't mentioned your use case, so it might be sufficient, but probably it's not. – Bergi Feb 13 '18 at 01:44
  • @Bergi and jonny I am also trying to do the same thing, if its the final call I want to return X, but if its not I want to keep chaining. Thanks for sharing, will keep on lookout if you guys come up with any new ways. Bergi if you can share a generic solution that would be awesome, your answer qualities are amazing. – Noitidart Feb 22 '19 at 00:27
  • This is something I found that is trying to solve similar - https://github.com/mozilla/webextension-polyfill/blob/master/src/browser-polyfill.js – Noitidart Feb 22 '19 at 00:30
  • 1
    @Noitidart As I said, the proxy never can know that something is the final call, unless you are explicitly telling it somehow. – Bergi Feb 22 '19 at 10:36
  • @Bergi this was my use case, I posted a solution to another topic - https://stackoverflow.com/a/54830620/1828637 – Noitidart Feb 22 '19 at 15:47
0

I found this solution to be quite interesting and simple, it uses recursion to accumulate a path.

function path(acc = () => []) {
  return new Proxy(
    acc,
    {
      get(target, prop) {
        return path(() => [...acc(), prop])
      },
    },
  )
}

const config = path()

config.path.to.prop()
// ['path', 'to', 'prop']

config.path.to.another.prop()
// ['path', 'to', 'another', 'prop']
LIMPIX64
  • 201
  • 1
  • 3
  • 12