emberjs / data

A lightweight reactive data library for web applications. Designed over composable primitives.
https://api.emberjs.com/ember-data/release
Other
3.03k stars 1.33k forks source link

EmberData | 5.0 Roadmap #8086

Closed runspired closed 1 year ago

runspired commented 2 years ago

5.0 Roadmap

Target Date: Nov/Dec 2022

Note Tasks marked [-] have been moved to the 6.0 Roadmap #8460

Overview

The EmberData Team intends to release a major version of the library around/during the fall of 2022. This issue tracks the work to be done to prepare for that major. It's a non-comprehensive list and some things will likely be added to it as unknowns are discovered over time.

5.0 will ship with an ember-data optimized for Embroider out of the box. This should significantly improve build times for all apps (even traditional ember-cli builds), allow ember-data to be chunked, and bring all of the asset compilation optimizations that ember-cli users currently receive to embroider users. Today embroider users ship 2-3x more compressed ember-data code to production than users of traditional ember-cli. This effort is tracked in #8103

This major version will focus on four critical points of evolving the data story for Ember applications: many of these will be items that will need to be individually RFC'd.

  1. removal of all support for Ember classic
  2. removal of all promise proxies
  3. introduction of a new story for managing network requests
  4. introduction of a model-optional default story for presenting data in applications

There's a lot to unpack in those four work items. Let’s dig in:

1. removal of all support for Ember classic

Some features can only work in classic mode if we build them using classic APIs. This includes features like decorators which must be built over computed, and base classes (such as Model) which must be built upon EmberObject. Building these APIs in this way introduces unintentional complications that limit what EmberData can do and prevent us from offering better solutions. For instance, the nature of reopen and reopenClass and Mixins are such that even if we were to deprecate classic class syntax, any custom decorators we write for schema information may fall victim to what these methods can do to a Model.

Historically, significant portions of internal code, typically the densest and most complex portions, were developed to defend against and manage the consequences of observers, computed chains and "mandatory setter" wreaking havoc by accessing store state before it was finalized. This did not become simpler with Octane, but harder, because EmberData now has to juggle two fundamentally opposed forms of change notification. Bug reports around change notification are the most common reports we receive. Removing support for observers and chains will significantly simplify some portions of the codebase allowing us to offer more advanced feature sets at lower performance costs and with more reliable change notification semantics. It would even allow for some portions of the library to be reimagined to use tracked properties directly, something we have to this point been unable to do due to the consequences of observers. While the observer problem was made significantly better with the deprecation and removal of sync-observers in ember 4.x, there's still much more for us to achieve.

While Ember is intending to deprecate support for classic, we feel EmberData should make the first move here due to the simple fact that we cannot support classic without building with classic APIs, and were we to not remove this support in advance then the resulting Ember deprecations would be unresolvable for our users.

What does this mean in practice?

2. removal of all promise proxies

Several RFCs have already begun this work. All promise proxies would be eliminated in favor of awaiting the async value before usage. Promise state flags would be provided via a utility such as the awesome ember-promise-helpers library by @fivetanley. We feel such a utility should be given a home core to the ecosystem in a way that all libraries could utilize it, and would likely bring it in as an official package. Promise proxies currently make it nearly impossible to use async await within the ember-data codebase when also using typescript and properly typing a method. While we could eventually decide to sometimes provide a custom async value, simply returning promises as the default for async is better for tooling, typescript, and expectations.

3. introduction of a new story for managing network requests

Adapters and Serializers got us far, but simpler approaches with greater flexibility, maintainability, and customizability have been proved out. Watch for a RequestManager RFC. 5.0 would not itself remove support for adapters and serializers, as we can easily support them as a custom plugin for the new system for some time, but it would move their usage into a "legacy" mode and remove them from the default story.

4. introduction of a model-optional default story for presenting data in applications

ember-m3, ember-data-model-fragments, ember-data-changetracker, ember-data-storefront and so many other libraries along the way have each tried to solve how to model more complex data and interactions than what @ember-data/model has provided out of the box. ember-m3 was a proving ground that we can do better by default, and it's learnings have led to internal refactoring, new public APIs, and new RFCs to allow us to present data in a whole new approach that fits a larger number of APIs more naturally, is more adept at adapting to new situations, and scales more naturally as applications and teams grow. Critically, these new APIs offer us the chance to offer GraphQL as a default option for users of EmberData. While we are not pivoting away from a strong story for JSON:API users, we want both stories to be equally strong (and even encourage mixing the two, they complement each other well).

Action Items

This is a non-comprehensive list that we expect to grow over the next several months as we determine the exact mechanics of achieving the above.

Deprecations

Refactoring

Documentation

Features

Deprecation Guides

Other Notes:

Moved to 6.0

runspired commented 2 years ago

A potential thing here is to deprecate unstable identifier API signatures. As we've worked through actual implementations this overhead has turned out to be largely unnecessary, at times it even introduces potential error situations (caches doing operations in an unsupported order), and the ergonomics of working with the stable identifier are far better for both caches and the store anyway: especially with typescript.

sandstrom commented 2 years ago

Great roadmap! 🏅

Removing EmberObject, observers, etc. sound like a very good idea.

Snapshots and/or forks of Ember Data models

I don't want to pile on additional things to the roadmap. Just mentioning this as more of a background feedback and general thought, rather than something that I think should necessarily be in 5.0. But having this use-case in mind early in the planning, may make it easier to add them in the future, even if it isn't in 5.0.

The ability to make snapshots (or forks) of model state (attributes + relationships) and then roll back is something that would make working with forms easier. Basically, a lot of forms have an edit/cancel button, and if you attach a model to the form, make some changes and then press cancel it's nice to have an easy way of rolling it all back.

I know there are snapshots internally, exposed to serializers. But they aren't accessible "from the outside".

There are also some prior addons:

Today we are using Ember Changeset to handle forms, and are using a bunch of workarounds to buffer changes to relationships. I've also discussed this with @snewcomer in https://github.com/poteto/ember-changeset/issues/551, as something that could also potentially be handled by that addon.

Would the ideal place for this be Ember Data or Ember Changeset? I don't know. But maybe it's a primitive that should reside in Ember Data, so I'm mentioning it here.

Happy to clarify and explain more about our use-case. There is also a small code example in the aforementioned issue.

runspired commented 2 years ago

@sandstrom internally it's actually possible at this point to rollback relationships. This capability is not exposed to users because historically rollback did not include that.

With the V2 Cache, store-forking becomes something we can (relatively easily) add. Store forking is our intended route for handling all the various encapsulation scenarios where you want to be able to group a set of changes and/or quickly discard a set of records and/or a set of changes.

sandstrom commented 2 years ago

Sounds awesome @runspired!

runspired commented 2 years ago

Added deprecate accessing record props after destroy. This behavior was already difficult to support and partially broken by changes in 4.4 and again 4.5. The singleton cache exposed this problem again in a sever way in #8122, the result was adding a secret second cache to make access during teardown "mostly safe" for one browser lifecycle tick.

unloadRecord(identifier: StableRecordIdentifier): void {
    const cached = this.#peek(identifier);
    const storeWrapper = this.#storeWrapper;
    graphFor(storeWrapper).unload(identifier);

    // effectively clearing these is ensuring that
    // we report as `isEmpty` during teardown.
    cached.localAttrs = null;
    cached.remoteAttrs = null;
    cached.inflightAttrs = null;

    let relatedIdentifiers = _allRelatedRecordDatas(storeWrapper, identifier);
    if (areAllModelsUnloaded(storeWrapper, relatedIdentifiers)) {
      for (let i = 0; i < relatedIdentifiers.length; ++i) {
        let identifier = relatedIdentifiers[i];
        storeWrapper.disconnectRecord(identifier);
      }
    }

    this.#cache.delete(identifier);
    this.#destroyedCache.set(identifier, cached);

    /*
     * The destroy cache is a hack to prevent applications
     * from blowing up during teardown. Accessing state
     * on a destroyed record is not safe, but historically
     * was possible due to a combination of teardown timing
     * and retention of a RecordData instance directly on the
     * record itself.
     *
     * Once we have deprecated accessing state on a destroyed
     * instance we may remove this.
     */
    if (this.#destroyedCache.size === 1) {
      schedule('destroy', () => {
        setTimeout(() => {
          this.#destroyedCache.clear();
        });
      });
    }
  }
runspired commented 1 year ago

And we're done 🥳

sandstrom commented 1 year ago

@runspired Awesome! 💯

Happy Easter! 🐰🥚