Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do you tell a getter to only invalidate if the _value_ changes, rather than the tracked references accessed invalidate (for any reason) #20632

Open
NullVoxPopuli opened this issue Jan 27, 2024 · 1 comment

Comments

@NullVoxPopuli
Copy link
Sponsor Contributor

Something that comes close to this is @dedupeTracked from tracked-toolbox: https://github.com/tracked-tools/tracked-toolbox/tree/master#dedupetracked

but it only works by intercepting the setting of a what-would-be-@tracked property.

The situation I have is related to the URL, but the source of the data could be anything.

get localData() {
  // currentURL will invalidate for *any* URL change, 
  // including QP-only transitions
  // but I want a way to discard the invalidations where the returned value
  // would be the same.
  return this.routerService.currentURL.split('?')[0];
}

or generically,

get localData() {
  let [dataYouCareAbout, dataYouDont] = this.upstreamDataYouDontControl;

  return dataYouCareAbout;
}

for rendering performance reasons (like, maybe something downstream, depending on localData causes other things to happen, you want those other things to execute as few times as possible -- in this case, based on value-equality rather than reference-dirtiness.

@cached is a reference-based cache, and the references do change, so it doesn't work.

What we need is something like @dedupeTracked, but for getters, and absorbing upstream changes.

@dedupeTracked  // @cached + value equality checking
get localData() {
  // ...
}

I don't think ember has a way to handle this.

Starbeam does, tho. So maybe the answer is to wait for better reactivity primitives.
https://www.starbeamjs.com/guides/advanced/equivalence.html

@KoKuToru
Copy link

I do this with custom a decorator and a custom tag like this:

but i have no idea if this is public api or whatever ..
or if the code is using the api correctly ...
but it works for my use cases

import { COMPUTE, TYPE, DIRYTABLE_TAG_ID, consumeTag, beginTrackFrame, endTrackFrame, beginUntrackFrame, endUntrackFrame, INITIAL } from '@glimmer/validator';

export default function cached(_target, _key, desc) {
  const CACHE = new WeakMap();
  return {
    get() {
      let tag = CACHE.get(this);
      if (!tag) {
        tag = new CachedTag(desc.get.bind(this));
        CACHE.set(this, tag);
      }
      return tag.value;
    }
  };
}

class CachedTag {
  revision = INITIAL;
  cached_revision = INITIAL;
  cached_value;
  subtag;
  getter;

  constructor(getter) {
    this.getter = getter;
  }

  [TYPE] = DIRYTABLE_TAG_ID;

  [COMPUTE]() {
    const subtag = this.subtag;
    let revision = this.revision;
    if (Array.isArray(subtag)) {
      for (const tag of subtag) {
        const value = tag[COMPUTE]();
        revision = Math.max(value, revision);
      }
    } else {
      const tag = subtag;
      const value = tag[COMPUTE]();
      revision = Math.max(value, revision);
    }
    if (this.cached_revision !== revision) {
      this.cached_revision = revision;
      beginUntrackFrame();
      try {
        if (this.cached_value !== this.value) {
          this.revision = this.cached_revision;
        }
      } finally {
        endUntrackFrame();
      }
    }
    return this.revision;
  }

  get value() {
    beginTrackFrame();
    try {
      this.cached_value = this.getter();
    } finally {
      this.subtag = endTrackFrame();
      consumeTag(this);
    }
    return this.cached_value;
  }
}

Usage:

import cached from '........./cached-decorator';

export default class TestComponent extends Component {
  @tracked clock = new Date();

  constructor(...args) {
    super(...args);
    setInterval(() => this.clock = new Date(), 125);
  }

  @cached get test() {
    return this.clock.getSeconds();
  }
}

if you put in a template

{{log this.test}}

with my custom decorator / tag it will output into console:

47
48
49
50
51
....

without or with orginal cached:

47
47
47
47
47
47
47
47
47
48
48
48
48
48
48
48
...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants