aurelia-deep-computed icon indicating copy to clipboard operation
aurelia-deep-computed copied to clipboard

deepComputedFrom breaks when object contains a reference loop

Open avrahamcool opened this issue 4 years ago • 8 comments

consider the following Node model, that represent a node in a Doubly linked list. the combined getter should list all the values starting the root, to the last child.

export interface Node
{
  parent?: Node;
  child?: Node;
  value?: string;
}

export class App
{
  root: Node;
  constructor()
  {
    this.root = {};

    this.root.child = {
      parent: this.root
      }
  }

  @deepComputedFrom("root")
  get combined(): string
  {
    let root = this.root;
    let value = "";
    while(root)
    {
      value += `${root.value} =>`;
      root = root.child;
    }
    return value;
  }
}

but: we get an error - because there is a reference loop between the parent and the child.

Uncaught (in promise) RangeError: Maximum call stack size exceeded
    at Function.[Symbol.hasInstance] (<anonymous>)
    at getDependency (index.js?3ae2:309)
    at ObjectPropertyDependency.collect (index.js?3ae2:91)
    at eval (index.js?3ae2:59)
    at Array.forEach (<anonymous>)
    at ObjectDependency.collect (index.js?3ae2:56)
    at ObjectPropertyDependency.collect (index.js?3ae2:96)
    at eval (index.js?3ae2:59)
    at Array.forEach (<anonymous>)
    at ObjectDependency.collect (index.js?3ae2:56)

this can be fixed by marking the visited objects after they have been "patched". or saving a Set with all "patched" objects. that way: the process of deep traversing will not run into endless loops.

avrahamcool avatar Mar 18 '21 16:03 avrahamcool

I'm trying to create a PR for this, but it turn-out to be more difficult than I initially thought.

at first I thought it's enough to fix the ObjectDependency.collect, but now I realize we need to fix all collect scenarios. an object can be a part of the loop in many ways.

  • the obvious way - (x => y => x) like in the issue above
  • as part of an arbitrary length loop (a => b => c => ... => a)
  • as part of an internal collection of some sort (a => b[7] => a) etc.

because I don't want to add a flag directly on the current observed object. (maybe we can use metadata API?), I need to keep track of all "items" that are already collected. - regardless of where they are in the actual object.

you already have an owner object that gets passed down the hirearchy. can I add a Set there? to keep track of every visited item?

its sound simple, but I don't have the full picture of how things works in mind. would I need to sync this set when new objects gets added after initial collection? would I need to remove them from the set when they are disconnected from the original object? etc.

I'd like your input on that.

or even better - if you are already working on a fix for that. :) that will be great.

avrahamcool avatar Mar 19 '21 09:03 avrahamcool

you already have an owner object that gets passed down the hirearchy.

This is what im thinking too. Can use a weakmap to associate the owner object with a set of tracked object.

And no, i havent worked on a PR yet, anyncontribution will be really welcomed 😃

bigopon avatar Mar 19 '21 09:03 bigopon

From what you commented, I thi k you already got an idea of how it all shouls work together 👍

bigopon avatar Mar 19 '21 09:03 bigopon

well, I've tried to implement that in a fork. it's seems to work only if I had all of the property that I was intending to change present in my VM beforehand.

for example: I had to explicitly set the value property:

constructor() {
  this.root = {value: undefined};
  this.root.child = {parent: this.root, value: undefined};
}

I think it it was always like that, and not because of my changes. (am I right?)

if I tried like that - the changes would not fire:

constructor() {
  this.root = {};
  this.root.child = {parent: this.root};
}

in a real case scenario - when you have bindings from the view to that non-existing property - aurelia will create a property for it, so this will be unnoticeable.

overall, introducing the WeakMap did not break any other test.. except: it lowered the amount of times values were being re-evaluted. I didn't fix those tests - because I wanted your opinion on the all solution beforehand.

image

before inserrting things into the WeakMap, I had to make sure they are objects. I think that could have been done nicely with a helper function, instead of repeating the same code over and over. I'll do in the offical PR that if this a valid solution.

avrahamcool avatar Mar 20 '21 23:03 avrahamcool

@avrahamcool that sounds cool, let's create a PR 🚀 For the predefined property issue, it's kind of a requirement, as it's impossible to do without it, unless we use proxy. I think it's ok to not involve proxy at all in v1, so that we can keep it compat to older JS environments.

bigopon avatar Mar 22 '21 22:03 bigopon

the reop contains dist files, what is the policy about the PR? should it update the dist? or just src?

avrahamcool avatar Mar 24 '21 05:03 avrahamcool

Just the src, I'll do the publish after the merge.

bigopon avatar Mar 24 '21 06:03 bigopon

I was wrong. my simple implementation doe's break some test. (I was able to see that only after I cleaned the json counts errors). although its sounds good, and should work in theory.

I'll try to dive in more deeply in the upcoming.

avrahamcool avatar Mar 26 '21 09:03 avrahamcool