GithubHelp home page GithubHelp logo

Comments (8)

runspired avatar runspired commented on June 1, 2024

Roughly speaking these are the system expectations:

  • defaultValues are not considered dirty state (e.g. are not localAttrs)
  • dirty comparisons are made by referential equality checks
  • defaultValues can be included in payloads sent to the server
  • the cache contains only serialized state

Either one of these first two trips up the idea of a defaultValue being used to populate state you want to later send back to the API. Due to the former, it won't take part in any patch that gets generated, and due to the latter it also won't take part in any patch even if you've mutated it so long as the reference is the same.

These have been constraints going back really really really far in this library, so you are probably wondering, what changed?

We no longer pull the value from the record instance (a mistake of the earlier architecture was that it did). This means we ask the cache for the attr value and the cache does not (and imo should not) retain these values since they are intended to be transient.

Since classic computed properties in ember memoize their value, when we pulled the value from the record instance it would appear stable even though it was not.

So what to do about this? Generally, if the intent is to use defaultValue to initialize missing state during createRecord, probably you should not. Currently, its better to pass those values in to createRecord or have your API return defaults or add defaults during serialization.

Which gets us into a second problem: where does defaultValue live?

This was a second mistake of the early architecture. Instead of defaultValue being a concern of a transform, it is instead a concern of a record. Meanwhile, transforms also have a mistake in the early architecture in that instead of being a concern of a field they are a concern of a serializer.

Both of these mistakes are rectified by schema-record:

  • transforms for schema-record are a field concern instead of a serializer concern
  • defaultValue is a transform concern not a record concern and produces a serialized value instead of a stateful value
export type Transform<T extends Value = string, PT = unknown> = {
  serialize(value: PT, options: Record<string, unknown> | null, record: SchemaRecord): T;
  hydrate(value: T | undefined, options: Record<string, unknown> | null, record: SchemaRecord): PT;
  defaultValue?(options: Record<string, unknown> | null, identifier: StableRecordIdentifier): T;
};

This change allows us to provide the record instance to the transform and to defaultValue correctly where we couldn't really do so before, and ensures that defaultValues are serializable (currently they usually aren't).

On its own though, this still wouldn't fix your issue because defaultValues still won't be considered dirty. However, in schema-record, mutating the value returned by your defaultValue value would result in the value becoming part of the dirty state of the cache since schema-record is capable of deep-tracking.

In the meantime, I'm open to a hacky fix to stabilize the value, its not totally clear though what the ideal form of that hack would be because ultimately we probably don't want to keep a cache of populated defaultValues and it would be a mistake for defaultValues to cause records to become dirty since there's a pretty high probability then that every record ever loaded from the API is immediately in a dirty state.

from data.

christophersansone avatar christophersansone commented on June 1, 2024

@runspired Thanks so much for the thorough explanation and willingness to consider a solution here, even if on the hacky side of things.

The easy solution in user land is to just set the values during store.createRecord. That's what I did to get unblocked, and it gets the job done.

Considering an internal solution, based on the system expectations you outlined, this issue exhibits when defaultValue is a function that returns inconsistent output. If defaultValue is simply defined as a primitive, it works as is: whether you reach for the value from the model instance or from defaultValue over and over, you will get the same output. If a fix was isolated to defaultValues as functions, it would isolate the change to a much smaller problem space, alleviating the concern about a high probability that every record ever loaded from the API is immediately in a dirty state.

That said, to me it seems reasonable to assert that if defaultValue is defined as a function, it is because it needs to output a unique value... and because that value is unique, that value is set on the record instance and the cache, just as if it were manually set... which therefore results in a dirty state.

If that were the case, then the fix would be to simply store these default value function results within localAttrs.

If it were really a concern that we are considering the record dirty as a result of this, then I guess I would consider defining a separate defaultAttrs variable to store these values. This way, the value can be retrieved as needed without affecting localAttrs and therefore the dirty state. It better adheres to the system expectations you outlined above (i.e. "defaultValues are not considered dirty state"), but it seems like more effort, and it feels like an exception can be made in this case, given that this functionality is already somewhat deprecated.

FWIW, I would say that in my experience, the general rule is that default values as functions only run on createRecord. Once the data is persisted, the expectation is that the server returns the persisted value, even if it's still the default. In this case, the record is dirty upon creation but then pristine once fetched. Occasionally I probably create a new attribute for existing records that requires a default value function but have not pre-populated that value on the persisted records, but that is a much smaller use case than the run of the mill new record creation. So even if a default value function causes a fetched record to be dirty, it seems like it would be a rare occurrence for those that use this pattern, and I don't know that I would notice.

from data.

runspired avatar runspired commented on June 1, 2024

So even if a default value function causes a fetched record to be dirty, it seems like it would be a rare occurrence for those that use this pattern, and I don't know that I would notice.

You might not, but we accidentally made it dirty state once before and tons of apps noticed 🙈

It's unfortunately common for folks to use it either for fields the API doesn't always send, doesn't send yet, or to trim down on what gets sent (intentionally leaving off fields from payloads that would match default values)

from data.

christophersansone avatar christophersansone commented on June 1, 2024

Ha, yeah you seemed more concerned than I did about tracking them as dirty... now I know why! 😄

Would a separate defaultAttrs key be worthwhile, allowing the default values to be accessible by the cache after the fact, while not storing them as if they were dirty? Could we simply augment getAttr as follows?

getAttr(...) {
  ...
  else if (cached.defaultAttrs && attr in cached.defaultAttrs) {
    return cached.defaultAttrs[attr];
  } else {
      ...
      const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
      if (typeof attrSchema?.options?.defaultValue === 'function') {
        cached.defaultAttrs = cached.defaultAttrs || {};
        cached.defaultAttrs[attr] = defaultValue;
      }
      return defaultValue;
    }

It actually seems more elegant than I would have expected. It's nice and isolated, only instantiates defaultAttrs when necessary, and only stores the value when the default value is a function. I imagine it would also need to get cleared on rollback and unload as well, though.

from data.

runspired avatar runspired commented on June 1, 2024

@christophersansone I think thats allowable and we can revisit that and remove it once schema-record allows us to avoid the problem in the first place

from data.

runspired avatar runspired commented on June 1, 2024

If we do this, we should probably delete the attr from defaultAttrs if a remoteAttr or localAttr is ever set

from data.

christophersansone avatar christophersansone commented on June 1, 2024

Sounds good. 👍 Want me to take a crack at a PR? I can give it a whirl but may take a day or two to make it happen. I'll make sure current tests pass... but should I write additional tests for this?

We can delete the attr for the sake of cleanliness... but I don't think the value would ever be read if attr in remoteAttrs or attr in localAttrs.

from data.

runspired avatar runspired commented on June 1, 2024

@christophersansone go for it! additional tests would be great, the dx of working in the monorepo though is pretty meh. If you can't figure it out from the various package.json files etc feel free to setup some time to pair.

TL;DR if you make changes to packages, run pnpm install and restart test server.

For this, tests will go in the tests in tests/main. Unsure if there's a great module in there for this already but creating a new module for defaultValue tests seems ok.

We can delete the attr for the sake of cleanliness... but I don't think the value would ever be read if attr in remoteAttrs or attr in localAttrs.

Because defaultValue might get mutated (just like you are doing) you want to be sure that if you later get an update from the server or do a rollback that you don't reuse the same value.

from data.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.