diff --git a/README.md b/README.md index 27e45f870d..3a68fcc0b9 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,8 @@ that RFC. RFCs in [Final Comment Periods](#final-comment-periods-fcp) are labeled with [Final Comment Period](https://github.com/emberjs/rfcs/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3A%22Final+Comment+Period%22) -and are announced on the [Ember twitter account](https://twitter.com/emberjs). +and are announced in `#news-and-announcements` on [Ember Discord](https://discord.gg/emberjs) and +on the [Ember twitter account](https://twitter.com/emberjs). [Quick links](#quick-links-for-pull-requests-for-proposed-and-advancing-rfcs) are provided at the top of the README to help you review what you are interested in. @@ -450,9 +451,9 @@ further guidance. For certain stage advancements, a _final comment period_ (FCP) is required. This is a period lasting 7 days. The beginning of this period will be signaled with a -comment and tag on the RFC's pull request. Furthermore, -[Ember's official Twitter account](https://twitter.com/emberjs) will post a -tweet about the RFC to attract the community's attention. +comment and tag on the RFC's pull request. Furthermore, a message will be posted in +`#news-and-announcements` on [Ember Discord](https://discord.gg/emberjs) and on the +[official Ember Twitter account](https://twitter.com/emberjs) to attract the community's attention. An RFC can be modified based upon feedback from the [core teams] and community during the final comment period. Significant modifications may trigger a new diff --git a/text/0236-deprecation-ember-string.md b/text/0236-deprecation-ember-string.md index fde9040c75..944e4f40fb 100644 --- a/text/0236-deprecation-ember-string.md +++ b/text/0236-deprecation-ember-string.md @@ -1,5 +1,5 @@ --- -stage: released +stage: recommended start-date: 2017-07-14T00:00:00.000Z release-date: 2023-01-12T00:00:00.000Z release-versions: @@ -11,6 +11,7 @@ prs: accepted: 'https://github.com/emberjs/rfcs/pull/236' ready-for-release: 'https://github.com/emberjs/rfcs/pull/892' released: 'https://github.com/emberjs/rfcs/pull/897' + recommended: 'https://github.com/emberjs/rfcs/pull/898' project-link: meta: tracking: 'https://github.com/emberjs/ember.js/issues/20340' diff --git a/text/0331-deprecate-globals-resolver.md b/text/0331-deprecate-globals-resolver.md index 7ef94a0f4b..581d495760 100644 --- a/text/0331-deprecate-globals-resolver.md +++ b/text/0331-deprecate-globals-resolver.md @@ -1,5 +1,5 @@ --- -stage: released +stage: recommended start-date: 2018-05-08T00:00:00.000Z release-date: 2020-01-20T00:00:00.000Z release-versions: diff --git a/text/0487-custom-model-classes.md b/text/0487-custom-model-classes.md index dc3a55ab31..fc14168352 100644 --- a/text/0487-custom-model-classes.md +++ b/text/0487-custom-model-classes.md @@ -1,9 +1,9 @@ --- stage: released # FIXME: This may be recommended start-date: 2019-05-09T00:00:00.000Z -release-date: 2019-09-19T00:00:00.000Z +release-date: 2021-08-20T00:00:00.000Z release-versions: - ember-source: v3.13.0 + ember-data: v3.28.0 teams: - data diff --git a/text/0496-handlebars-strict-mode.md b/text/0496-handlebars-strict-mode.md index 0f8f1f3280..083d672fd2 100644 --- a/text/0496-handlebars-strict-mode.md +++ b/text/0496-handlebars-strict-mode.md @@ -317,7 +317,7 @@ template string and returning a ready-to-be-consumed template object (the output of `createTemplateFactory`), instead of the wire format. This is mostly used for compiling templates at runtime, which is pretty rare. -We propose to introduce a new `strict` option to the `precompile` and `compile` +We propose to introduce a new `strictMode` option to the `precompile` and `compile` functions to enable strict mode compilation: ```js @@ -325,7 +325,7 @@ import { precompileTemplate } from '@ember/template-compilation'; precompileTemplate('Hello, {{name}}!', { moduleName: 'hello.hbs', - strict: true + strictMode: true }); ``` @@ -363,12 +363,13 @@ precompileTemplate(`{{#let this.session.currentUser as |user|}} {{/let}}`, { moduleName: 'index.hbs', - strict: true + strictMode: true }); /* => `{ "id": "ANJ73B7b", "block": "{\"statements\":[\"...\"]}", "meta": { "moduleName": "index.hbs" }, - "scope": () => [BlogPost, titleize] + "scope": () => [BlogPost, titleize], + "isStrictMode": true }` */ ``` @@ -411,7 +412,8 @@ export default createTemplateFactory({ "id": "ANJ73B7b", "block": "{\"statements\":[\"...\"]}", "meta": { "moduleName": "index.hbs" }, - "scope": () => [BlogPost, titleize] + "scope": () => [BlogPost, titleize], + "isStrictMode": true }); ``` @@ -434,7 +436,8 @@ precompileTemplate(`{{#let this.session.currentUser as |user|}} {{/let}}`, { moduleName: 'index.hbs', strict: true, - scope: ['BlogPost', 'titleize'] + scope: ['BlogPost', 'titleize'], + isStrictMode: true }); ``` @@ -540,7 +543,7 @@ strict mode semantics at this stage. Instead, the guides should be updated to feature template imports or single-file components when they become available. As for the low-level APIs, we should update the API documentation to cover the -new flags (`strict` and `scope`). The documentation should cover the details of +new flags (`strictMode` and `scope`). The documentation should cover the details of the "ambient scope" feature discussed in this RFC, and emphasize that it is intended for linking static values such as helpers and components. @@ -566,7 +569,7 @@ discussed in the contextual helpers RFC. By adopting these piecemeal, we will also have to define the interaction and combined semantics for any possible combinations of these flags, and tooling - will be unable to take advantage of the improved static guarentees without + will be unable to take advantage of the improved static guarantees without doing a lot of work to account for all these possibilities. 2. Instead of proposing a standalone strict mode, we could just bundle these @@ -578,7 +581,7 @@ discussed in the contextual helpers RFC. 3. Switch to HTML attributes by default in strict mode. - Today, Glimmer uses a complicated set of huristics to decide if a bound HTML + Today, Glimmer uses a complicated set of heuristics to decide if a bound HTML "attribute" syntax should indeed be set using `setAttribute` or set as a JavaScript property using `element[...] = ...;`. This does not always work well in practice, and it causes a lot of confusion and complexity. diff --git a/text/0580-destroyables.md b/text/0580-destroyables.md index c57cd20d35..a1717fc1d9 100644 --- a/text/0580-destroyables.md +++ b/text/0580-destroyables.md @@ -1,14 +1,14 @@ --- -stage: released # FIXME: This may be recommended +stage: recommended start-date: 2020-01-10T00:00:00.000Z release-date: 2020-10-05T00:00:00.000Z release-versions: ember-source: v3.22.0 - teams: - framework prs: - accepted: https://github.com/emberjs/rfcs/pull/580 + accepted: 'https://github.com/emberjs/rfcs/pull/580' + recommended: 'https://github.com/emberjs/rfcs/pull/915' project-link: --- diff --git a/text/0625-helper-managers.md b/text/0625-helper-managers.md index e22c5d751a..4d72834ee5 100644 --- a/text/0625-helper-managers.md +++ b/text/0625-helper-managers.md @@ -1,5 +1,5 @@ --- -stage: released # FIXME: This may be a further stage +stage: recommended start-date: 2020-04-28T00:00:00.000Z release-date: 2020-11-16T00:00:00.000Z release-versions: diff --git a/text/0637-customizable-test-setups.md b/text/0637-customizable-test-setups.md index c959491452..fe458a7d54 100644 --- a/text/0637-customizable-test-setups.md +++ b/text/0637-customizable-test-setups.md @@ -4,7 +4,7 @@ start-date: 2020-06-01T00:00:00.000Z release-date: 2020-05-02T00:00:00.000Z release-versions: ember-source: v4.4.0 - # ember-cli: FIXME + ember-cli: v4.3.0 teams: - framework @@ -50,7 +50,7 @@ For example: ```js import { module, test } from 'qunit'; - import setupTest from 'ember-qunit'; + import { setupTest } from 'ember-qunit'; module('Unit | Service | tomster', function(hooks) { setupTest(hooks); @@ -64,7 +64,7 @@ manually be changed to: ```js import { module, test } from 'qunit'; - import setupTest from 'my-app/tests/helpers'; + import { setupTest } from 'my-app/tests/helpers'; module('Unit | Service | tomster', function(hooks) { setupTest(hooks); @@ -77,8 +77,8 @@ or where the setup functions are to be composed, this must be added: ```js import { module, test } from 'qunit'; - import setupTest from 'ember-qunit'; - import setupMyTest from 'my-app/tests/helpers'; + import { setupTest } from 'ember-qunit'; + import { setupMyTest } from 'my-app/tests/helpers'; module('Unit | Service | tomster', function(hooks) { setupTest(hooks); diff --git a/text/0739-ember-data-deprecate-non-strict-relationships.md b/text/0739-ember-data-deprecate-non-strict-relationships.md index 1e6b6c840b..e5e3e01215 100644 --- a/text/0739-ember-data-deprecate-non-strict-relationships.md +++ b/text/0739-ember-data-deprecate-non-strict-relationships.md @@ -1,12 +1,15 @@ --- -stage: accepted # FIXME: This may be a further stage +stage: recommended start-date: 2021-04-23T00:00:00.000Z -release-date: +release-date: 2022-09-06T00:00:00.000Z release-versions: + ember-data: v4.7.0 teams: - data prs: - accepted: https://github.com/emberjs/rfcs/pull/739 + accepted: 'https://github.com/emberjs/rfcs/pull/739' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/909' + released: 'https://github.com/emberjs/rfcs/pull/924' project-link: --- diff --git a/text/0756-helper-default-manager.md b/text/0756-helper-default-manager.md index f60738c618..7baf687224 100644 --- a/text/0756-helper-default-manager.md +++ b/text/0756-helper-default-manager.md @@ -67,13 +67,15 @@ of the three constructs: Helpers, Modifiers, and Components._ The desired usage of a plain function in a template should be: - convenient - reduce boilerplate - - be easily portable to JS for developers' mental model of how template and JS interact. + - be easily portable to JS for developers' mental model of how template and JS interact + - support normal JavaScript idioms and existing JavaScript functions (from lodash, etc). Which results in: - default to positional parameters - all named arguments are grouped into an "options object" as the last parameter. this happens to align with the _syntax_ of helper invocation where named arguments may not appear before the last positional argument. + - if no named arguments are passed in the template, no "options object" is passed to the JS call. #### Example with mixed params @@ -112,6 +114,26 @@ class A { } ``` +#### Example with consuming tracked data defined outside of the helper + +This works with the `helper` and `Helper` from `@ember/component/helper`, as well as plain functions. + +```hbs +{{this.multiply 4}} + + +``` +```js +class A { + @tracked multiplicand = 5; + + multiply = (passed) => passed * this.multiplicand; + + increment = () => this.multiplicand++; +} +``` +When the button is clicked, the text in `` will update, even though the `multiplicand` is not passed to the helper. + #### Example Default Helper Implementation The implementation for the this function-handling helper-manager could look like this: @@ -136,8 +158,6 @@ class FunctionHelperManager { if (Object.keys(args.named).length > 0) { argsForFn.push(args.named); - } else { - argsForFn.push({}); } return fn(...argsForFn); @@ -153,12 +173,61 @@ const DEFAULT_HELPER_MANAGER = new FunctionHelperManager(); // side-effect -- this file needs to be imported for the helper manager to be installed setHelperManager(() => DEFAULT_HELPER_MANAGER, Function.prototype); ``` - - when the "helper" is created, the function is not invoked - - when `getValue` is invoked, - - the function is invoked with the named arguments all grouped into the last arg - - if no named arguments are given, an empty object is used instead to allow less nullish checking in userland - - to register this helper manager, it should occur during app boot so developers do not need to import anything to - trigger the `setHelperManager` call + +- when the "helper" is created, the function is not invoked +- when `getValue` is invoked, + - the function is invoked with the named arguments all grouped into an object in the last arg ("options object") + - ~~if no named arguments are given, an empty object is used instead to allow less nullish checking in userland~~ (see notes below) + - if no named are passed, the "options object" argument is omitted +- to register this helper manager, it should occur during app boot so developers do not need to import anything to + trigger the `setHelperManager` call + +##### Notes regarding the "options object" argument + +An earlier version of this RFC initially proposed an "options object" with always be passed +as the argument, even when no-named arguments are passed. During [implementation](https://github.com/glimmerjs/glimmer-vm/pull/1348) +this was observed to be problematic. + +For instance, given the following JavaScript function: + +```js +function sum(...values) { + let total = 0; + + for (let value of values) { + total += value; + } + + return total; +} +``` + +In the original proposal, an invocation like `{{sum 1 2 3}}` would result in the JS call +`sum(1, 2, 3, {})`, which would yield surprising and incorrect result. + +Another case where this matters is with default arguments: + +```js +function formatDate(date, formatString = "DD MM YYYY hh:mm:ss") { + return ...; +} +``` + +In the original proposal, an invocation like `{{formatDate this.now}}` would result in the +JS call `formatDate(this.now, {})`, which has the effect of overriding the default argument +`formatString` with an empty object, also leading to surprising and incorrect behavior. + +Given the goal of the RFC is to support normal JavaScript idioms and the ability to use a +large variety of existing JavaScript functions (from packages like lodash) directly in the +template, the proposal is updated to only pass the "options object" when necessary. + +This has the drawback of an arguably less consistent signature. However, in practice, this +did not appear to be a issue. Base on the semantics and idioms of JavaScript, it is quite +rare for functions to mix-and-match variable positional arguments or defaulting of positional +arguments together with "named arguments" ("option object") in a way that would conflict with +what the updated proposal. Notably, JavaScript does not support `myFunc(...args, options)` in +the syntax, and functions with these kind of signatures already needs to manually introspect +the arguments carefully, in ways that should be compatible with the current proposal. ### Updating highlevel manager choosing algorithm diff --git a/text/0811-element-modifiers.md b/text/0811-element-modifiers.md index 5d46f60a77..045eed0d5b 100644 --- a/text/0811-element-modifiers.md +++ b/text/0811-element-modifiers.md @@ -1,13 +1,16 @@ --- -stage: accepted +stage: released start-date: 2022-03-29T00:00:00.000Z -release-date: +release-date: 2023-03-03T00:00:00.000Z release-versions: -teams: # delete teams that aren't relevant + ember-cli: v4.11.0 +teams: - cli - learning prs: - accepted: https://github.com/emberjs/rfcs/pull/811 + accepted: 'https://github.com/emberjs/rfcs/pull/811' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/885' + released: 'https://github.com/emberjs/rfcs/pull/928' project-link: --- diff --git a/text/0812-tracked-built-ins.md b/text/0812-tracked-built-ins.md index 7eb8653471..5c16e46eba 100644 --- a/text/0812-tracked-built-ins.md +++ b/text/0812-tracked-built-ins.md @@ -1,13 +1,14 @@ --- -stage: accepted +stage: ready-for-release start-date: 2022-03-29T00:00:00.000Z release-date: release-versions: -teams: # delete teams that aren't relevant +teams: - cli - learning prs: - accepted: https://github.com/emberjs/rfcs/pull/812 + accepted: 'https://github.com/emberjs/rfcs/pull/812' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/886' project-link: --- diff --git a/text/0854-ember-data-cache-v2.1.md b/text/0854-ember-data-cache-v2.1.md new file mode 100644 index 0000000000..f3c1cbfda4 --- /dev/null +++ b/text/0854-ember-data-cache-v2.1.md @@ -0,0 +1,1515 @@ +--- +stage: recommended +start-date: 2022-08-27T00:00:00.000Z +release-date: 2023-04-08T00:00:00.000Z +release-versions: + ember-data: v4.12.0 +teams: + - data +prs: + accepted: 'https://github.com/emberjs/rfcs/pull/854' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/923' + recommended: 'https://github.com/emberjs/rfcs/pull/926' +--- + + + +# EmberData Cache V2.1 + +## Summary + +A rename of the Cache (RecordData) API with new restrictions and additional amendments +to the accepted #461 RecordData V2 spec. These alterations will further simplify EmberData +while increasing its flexibility and extensibility. In turn, this increased flexibility +provides the backbone for further design and exploration of new capabilities such as +QueryCache, GraphQL and an enhanced SSR and Rehydration Mode. + +## Motivation + +We originally designed RecordData as a public formalization of one of the core +responsibilities of `InternalModel`. This allowed applications that had needed to reach +into private API to achieve criticial feature support a public avenue for doing so. However, +even then we recognized that this was merely a bridge for supporting users while we +underwent heavier refactoring to rationalize the internals and better explore what was +needed in a long-term cache solution. + +Between the efforts in ember-m3, orbit.js, our own implementation of RecordData V1 and V2, +and through the process of eliminating InternalModel we were able to come to a better +understanding of what capabilities the cache lacks today. Some of these learnings resulted +in [RFC #461](https://rfcs.emberjs.com/id/0461-ember-data-singleton-record-data). Others +were floated as internal proposals for some time, but needed more cleanup to happen +internally for us to be confident in their direction. + +These changes look to either solve (or tee up) multiple key issues for our users. The below +discussion is not comprehensive nor exhaustive, but representative of the kinds of problems +we're looking to solve by effecting these changes. + +For example: currently, if users want to avoid refetching a query they must design and place +their own cache in front of the store's query method. With the proposed changes, the cache +becomes capable of managing this responsibility. This turns out to be ideal for handling +paginated collections and GraphQL responses as well. Near Future RFCs will directly utilize +the changes in this RFC to bring these capabilities to users of EmberData. + +Similarly, the query context and response documents contain information critical for the +correct handling of individual resources it contains. For instance, when fetching a related +collection via a link it is not a requirement of the `JSON:API` spec that the records in the +response have a relationship back to the requesting record of their own. Access to the original +response document is necessary to know both the membership and the ordering of the returned +relationship data. Today, EmberData iterates payloads and fills in the gaps of this missing +information by creating artificial records, but that ties us unnecessarily to the JSON:API spec +and results in unnecessarily high overhead for processing a response. A Cache which received +the full request context and document could properly update this relationship without any +additional overhead. + +**Its the URL. Its always the URL** + +Until now, EmberData has focused on being a Resource cache. While many of the changes here do +affect the Resource Caching APIs, **the introduction of Document caching is perhaps the most +substantive change in EmberData's long history.** + +For many years discussion of a Document Cache focused on finding a solution to caching the +response of `Store.query` such that repeat calls did not require hitting network if that was +unnecessary. Typically these discussions got hung up on cache lifetimes and what to use as +cache-keys. + +Then in late 2017 we had the insight that relationships also suffered from this lacking +capability, and began work on what was then [the collections RFC](https://github.com/runspired/rfcs/blob/ember-data-collections/text/0000-ember-data-collections.md). + +Unfortunately, that RFC was premature, not for its proposed design but because it turned out to +require other Cache improvements it did not specify, was too coupled to JSON:API at a time when +we had already realized we wanted to support GraphQL, and required [Identifiers](https://github.com/emberjs/rfcs/pull/403) +to be fully realized. + +As we continued to work through GraphQL support built over our R&D efforts in [ember-m3](https://github.com/hjdivad/ember-m3), +it became increasingly clear that if we were to truly make the data format opaque to the Store, +then the Cache must also be responsible for determining what in a response Document constituted +a Resource. + +And so, with time, we embraced the lesson that Web Engineers have had to learn to embrace time and +time again: `use the URL`. + +Caching by URL gives EmberData immediate access to a host of potential capabilities. + +- `URL` is a unique identifier, including encoding information about pagination, sorting, + filtering, and order +- `headers` associated with a URL typically contain cache directives a Cache implementation + could utilize for data lifetimes +- `body` of a response to a URL can be in any format, not just JSON, and so caching blobs for + images, xml documents like SVG, video streams, etc. becomes feasible (these are capabilities + `Identifiers` previously allowed but only at a `Resource` level which made it not all that + useful) + +Caching by URL gives EmberData enhanced abilities when the response Document and Cache +implementation are aligned in format. For instance, with JSON:API the top level of a response +will often contain + +- `links` (URLs!) for additional pages related to the current query (`next`, `prev` and so on) +- `meta` that may contain information around total count still to be loaded +- `order` implicitly the order of the `data` array in a collection response *is* valuable + information for sorting +- `included` information about what was and was not loaded for a particular request, which might + be used to construct a more robust secondary fetch. For instance, instead of loading `user` + and then later loading individual resources or types of resources associated to that user, + you might be able to use this information to construct a request for exactly the set of + missing data to fill out the graph once it becomes needed. + +A Document centric cache is also essential to the GraphQL story where while the Document as +a whole is a composition of Resources the entry point is the query itself. + +Our vision with EmberData is one of composable primitives flexible enough to handle organizing +and coordinating desired capabilities. It is clear to us now that to embrace that vision requires +embracing the URL. + +But this new Cache brings so much more! In a move atypical for EmberData, we +are proposing new public APIs here that are not simply a codification of private +APIs we've had years to explore. Our reasoning is straightforward: to encourage +exploration within constraints. + +**Streaming the Future** + +The addition of Documents to the cache provides the ability to cache all information +necessary for reconstruction of what was delivered to the UI. This affords a number +of interesting opportunities around cache dump and cache restore, including among other +things for offline support, rehydration, and background workers. + +We don't want to stand in the way of such exploration while waiting to dream of the +perfect API, but we would like to provide a framework for it that helps cache's know +what to implement to be maximally compatible with solutions the community may dream up. + +To this end, we are introducing with minimal constraints and guidance streaming APIs +for dumping, restoring and patching the cache. The format is almost entirely opaque, +because typically the optimal serialization form will be as-close as possible to the +form the cache stores this information in regularly. Where it is not opaque is merely +the constraint that the format needs to be chunkable. Of course a Cache could choose +to treat the entire state as one chunk, but hopefully by encouraging chunking from the +beginning Cache implementations will position themselves to take maximal advantage of +the very good native streaming and buffer APIs. + +**Forking the Past** + +The final change to this version of the Cache is that it will support forking. Cache +forking is a technique we are ~~stealing~~ (uh) ~~borrowing~~ (err) *learning* from [Orbit.js](https://github.com/orbitjs/orbit/blob/2df79c5d67f2311c3bdc84941a18cc6896668f25/website/docs/memory-sources.md#forking-memory-sources) + +On the surface, forking has been described to us by some folks as a solution without a +problem. We disagree. Forking is the right sort of boring solution that has the power +to be disruptive in just how simple and boring it is. Why is that? Instead of solving +one problem, it is a general solve to a large set of problems. + +Need to load a lot of modeled relational data for a single route or component, but for +memory concerns want to unload it when you exit that route or dismount that component? +Use a fork! The fork will allow you load additional data related to data you have already +loaded previously, then safely and quickly toss it away later when you no longer need it. + +Need to edit a lot of relational data and save it in one go? Use a fork! If you later +want to discard the edits, instead of trying to keep track of what changed, just toss +away the fork. Or, if you want to persist it, save all changes to the fork in one single +go. + +Want to have some data loaded for all routes, but have other routes or engines manage their +own data but still correctly be able to reference the global data? That's right, use forks! + +Want to be able to edit a whole lot of data and serialize the changes for local storage for +a save/restore feature for a form, but don't want to pollute those records for where they +appear elsewhere in your UI? Use a fork! + +Is it a bird, is it a plane? It might sound like a knife and feed you as easily as a spoon... +but its a fork! + +### The Grand Vision (Abbreviated) + +This RFC codifies the direction and vision that the EmberData team has been building +towards in our long-term strategy since early in my tenure on the team. To anyone who +has been in these discussions the ideas presented here are not new, and we already know +how they fit neatly into that vision; but for the community at large many of these +changes may still be a surprise. + +This RFC is not "the grand vision", but it is an integral piece. Without it, changes we +have planned for the presentation, network and store primitives will not be possible. + +Above, I have laid out several key use-cases and motivations for the changes herein, +but the observant will recognize that this RFC does not expose APIs or specify how +the store, network, or current model primitives might make use of these cache APIs. We +leave each of these to their own RFCs – coming very soon – but for the sake of adequate +discussion herein we outline the direction we expect those RFCs to take. + +**The APIs shown in this section are not a proposed design but representative of the overall vision for EmberData in the near future** + +**`From Fetch to Fully Distributed Replica Database`** + +For the network layer, we see the introduction of `FetchManager`, a managed flow +for making requests that allows registration of middleware to handle fulfillment +or decoration of the request or response. This service would thereby make headers +and auth management easy to handle, letting apps that previously had multiple +unorganized services or ad-hoc fetch handlers to unify over one conventional flow. + +This managed `Fetch` flow would (by default) not be integrated with the Store, thereby +skipping the cache and providing access to raw responses. + +```ts +class FetchManager { + registerMiddlewares(wares: Middleware[]); + + async request(req: RequestOptions): RequestFuture; +} +``` + +For the store, we see the addition of a new API – `Store.request` – overwhich all +existing store methods will be rebuilt as "convenience macros". This delegates to +the above `FetchManager`, but passes the Store's cache (which is potentially a fork) +as a reserved arg, and which manages passing the result to the Cache and instantiating +any records or other presentation classes required in the return value. + +```ts +class Store { + async request(req): RequestFuture; +} +``` + +```ts +import { gql } from '@ember-data/graphql'; + +const USER_QUERY = gql`query singleUser($id: Id) { + user(id: $id) { + name + friends { + name + } + } +}`; + +class extends Route { + @service store; + + async model({ id }) { + const user = await this.store.request(USER_QUERY({ id }, { cache: { backgroundReload: true } })); + + return user.data; + } +} +``` + +Similarly, we see request-builders (like the graphql example above) becoming the primary mechanism +by which folks query for data. This ensures URLs are available for the cache, illuminates the mental +model of `fetch => fully distributed replica Database`, simplifies the mental model of EmberData in +general by removing the mystery of "how" and "when" data is fetched, and encourages a pattern that +can benefit from static analysis and build-time optimizations (such as pre-built GraphQL queries). + +```ts +import { buildUrlForFindRecord } from '@ember-data/request-builders'; + +class extends Route { + @service store; + + async model({ id }) { + const user = await this.store.request({ + url: buildUrlForFindRecord({ + type: 'user', + id, + incude: 'friends,company,parents,children,pets' + }) + }); + + return user.data; + } +} +``` + +The Store will also gain APIs for forking and for peeking documents. + +For modeling data, we see a story that is immutable-by-default. All edits run through forks, +which occurs by calling `Store.fork` which would similarly setup its cache via `Cache.fork`. + +A major benefit of this approach is that it gives application developers the ability to choose +the specifics of the optimistic or pessimistic timing of when the "remote" state is updated. + +An app might eagerly update the primary store on every edit, or it might update it optimistically +once a save has initiated, or it may wait only until the server has confirmed the update to do so. + +In this way, global state always reflects the developer's wishes for the specific application: +whether that is the persisted state or the mutated state. + +Of course, because these APIs are opaque and the Store is merely playing the role of coordinator, +libraries can choose to create all sorts of pairing of capabilities and patterns between cache +and presentation, and we will be able to maintain the existing `@ember-data/model` story for some +time as a "legacy" approach that users can install if they are not yet ready to upgrade to these +newer features and patterns. + +**Reimagining the Install and Learning Experience** + +The sum total of these changes (this RFC and planned RFCs) leads to a new default story for +EmberData. For this reason I have at times in jest referred to it as "The Grand EmberData +Modules Disunification RFC". + +For some time now the inclusion of EmberData's `ember-data` package in the `ember-cli` blueprint +has made increasingly less sense: not because we don't want EmberData to be the default, but +because we don't see there existing just one "EmberData way". + +An ideal install experience would likely focus on capabilities and requirements, a guided install +where running `npx ember-data` walks you through choices like `JSON:API` vs `GraphQL`, "just fetch" +or all the way to "distributed replica" along the way showing what packages you will need and in +the end installing them and wiring them together to get you started. + +An ideal learning experience would likely take a similar path; documentation that guides the user +incrementally from fetch to full-story, and a tutorial app that shows using EmberData for just +fetch+ and then as needs in the tutorial app expand adding in additional packages and capabilities +to match. + +And so armed with this context, let's dive in. + +## Detailed design + +This RFC proposes five substantive changes. + +### 1. `RecordData` becomes `Cache` + +First, the cache interface is renamed from `RecordData` to `Cache`. This is to reflect its upgraded responsibilities from handling only Resource data to also handling the caching of Documents. Additionally, `Cache` implementations MUST be Singletons. + +The following APIs are affected by this change. + + - `StoreWrapper.recordDataFor` is deprecated without replacement. Since the cache is a Singleton it will already contain a reference to itself. + + - On `Store` + - `Store.instantiateRecord` will receive only two args: `identifier` and `createArgs` + - the removed args are `recordDataFor` and `notificationManager` which are now available as the store's `cache` and `notifications` properties + - The notification manager (already public via instantiateRecord) is made accessible via `Store.notifications` + - `Store.createRecordDataFor` is deprecated in favor of `Store.createCache` + - `Store.createCache` and `Store.cache` are added with the following signatures + + ```ts + class Store { + /** + * This hook allows for supplying a custom cache + * implementation following the Cache spec. + * This hook will only be called once by the Store + * the first time cache access is needed. + * + * For interopability, there are multiple avenues + * for composing caches together to support a range + * of capabilities. + * + * When extending a store with an alternative cache + * you may super this meethod. Alternatively you may + * extend another public Cache implementation, or + * manually instantiate and wrap another cache implentation as a delegate. + * + * You should not call this method yourself. + * + * @method createCache (hook) + * @public + * @returns {Cache} a new Cache instance + */ + createCache(store: StoreWrapper): Cache; + + /** + * The store's Cache instance. Instantiates it + * if needed. + * + * @property {Cache} cache + * @public + */ + cache: Cache; + + /** + * Provides access to the NotificationManager instance + * for this store. + * + * The NotificationManager can be used to subscribe to changes + * for any identifier. + * + * @property {NotificationManager} notifications + * @public + */ + notifications: NotificationManager; + + + /** + * A hook which an app or addon may implement. Called when + * the Store is attempting to create a Record Instance for + * a resource. + * + * This hook can be used to select or instantiate any desired + * mechanism of presentating cache data to the ui for access + * mutation, and interaction. + * + * @method instantiateRecord (hook) + * @param identifier + * @param createRecordArgs + * @param recordDataFor + * @param notifications + * @returns A record instance + * @public + */ + instantiateRecord( + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown }, + ): RecordInstance; + } + ``` + + - From a Package Perspective + - `@ember-data/record-data` will be rebranded as `@ember-data/json-api` and the Cache + implementation will be publically available as `import Cache from '@ember-data/json-api';` + This means consumers are free to extend this implementation if desired, though this is not recommended. + - A new package, `@ember-data/graph`, will be introduced, it will currently contain no public + API exports. This is done so that EmberData may experiment with providing additional + official cache implementations that also make use of the primitives we are designing for + managing relationships between resources. This abstract utility will become public after + some additional iteration. + +### 2. Simplification of Resource Cache API + +These changes are *instead of* the changes in the original RecordData V2 RFC. + +> **Note** for a transitionary period `Store.createRecordDataFor` will still be invoked if present and +> it should return the singleton cache instance if the RecordData has been upgraded to Cache 2.1. +> This should ease the transition from V1 to V2 and provide interop between. + +Below is the finalized "v2.1" API for Resources. All existing methods and signatures not +contained herein are deprecated. + + + + + + + + + + + +
Cache APIsAssociated Types
+ +```ts +export interface Cache { + /** + * The Cache Version that + * this implementation implements. Defaults + * to '1' if not defined. + * + * @type {'1'|'2'} + * @property version + */ + version: '2'; + + // Resource Cache APIs + // =================== + + /** + * Push resource data from a remote source into the cache for this identifier + * + * @method upsert + * @public + * @param identifier + * @param data + * @param hasRecord + * @returns {void | string[]} if `hasRecord` is true then calculated key changes should be returned + */ + upsert( + identifier: StableRecordIdentifier, + data: ResourceBlob, + hasRecord?: boolean + ): void | string[]; + + /** + * [LIFECYCLE] Signal to the cache that a new record has been instantiated on the client + * + * It returns properties from options that should be set on the record during the create + * process. This return value behavior is deprecated. + * + * @method clientDidCreate + * @public + * @param identifier + * @param createArgs + */ + clientDidCreate( + identifier: StableRecordIdentifier, + createArgs?: Record + ): Record; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * will be part of a save transaction. + * + * @method willCommit + * @public + * @param identifier + */ + willCommit( + identifier: StableRecordIdentifier + ): void; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was successfully updated as part of a save transaction. + * + * @method didCommit + * @public + * @param identifier + * @param data + */ + didCommit( + identifier: StableRecordIdentifier, + data: StructuredDataDocument + ): void; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was update via a save transaction failed. + * + * @method commitWasRejected + * @public + * @param identifier + * @param errors + */ + commitWasRejected( + identifier: StableRecordIdentifier, + errors?: StructuredErrorDocument + ): void; + + /** + * [LIFECYCLE] Signals to the cache that all data for a resource + * should be cleared. + * + * This method is a candidate to become a mutation + * + * @method unloadRecord + * @public + * @param identifier + */ + unloadRecord( + identifier: StableRecordIdentifier + ): void; + + // Flexible Resource APIs + // ====================== + // These APIs have additional + // signatures detailed in + // other sections + + /** + * Perform an operation on the cache to update the remote state. + * + * Note: currently the only valid operation is a MergeOperation + * which occurs when a collision of identifiers is detected. + * + * @method patch + * @public + * @param op the operation to perform + * @returns {void} + */ + patch( + op: Operation + ): void; + + /** + * Update resource data with a local mutation. + * Currently supports operations on relationships + * only. + * + * @method mutate + * @public + * @param operation + */ + mutate( + mutation: Mutation + ): void; + + /** + * Peek resource data from the Cache. + * + * In development, if the return value + * is JSON the return value + * will be deep-cloned and deep-frozen + * to prevent mutation thereby enforcing cache + * Immutability. + * + * This form of peek is useful for implementations + * that want to feed raw-data from cache to the UI + * or which want to interact with a blob of data + * directly from the presentation cache. + * + * An implementation might want to do this because + * de-referencing records which read from their own + * blob is generally safer because the record does + * not require retainining connections to the Store + * and Cache to present data on a per-field basis. + * + * This generally takes the place of `getAttr` as + * an API and may even take the place of `getRelationship` + * depending on implementation specifics, though this + * latter usage is less recommended due to the advantages + * of the Graph handling necessary entanglements and + * notifications for relational data. + * + * @method peek + * @public + * @param identifier + * @returns {ResourceBlob | null} the known resource data + */ + peek( + identifier: StableRecordIdentifier + ): ResourceBlob | null; + + // Granular Resource Data APIs + // =========================== + + /** + * Retrieve the data for an attribute from the cache + * + * @method getAttr + * @public + * @param identifier + * @param field + * @returns {unknown} + */ + getAttr( + identifier: StableRecordIdentifier, + field: string + ): unknown; + + /** + * Mutate the data for an attribute in the cache + * + * This method is a candidate to become a mutation + * + * @method setAttr + * @public + * @param identifier + * @param field + * @param value + */ + setAttr( + identifier: StableRecordIdentifier, + field: string, + value: unknown + ): void; + + /** + * Query the cache for the changed attributes of a resource. + * + * @method changedAttrs + * @public + * @deprecated + * @param identifier + * @returns { : [, ] } + */ + changedAttrs( + identifier: StableRecordIdentifier + ): Record; + + /** + * Query the cache for whether any mutated attributes exist + * + * @method hasChangedAttrs + * @public + * @param identifier + * @returns {boolean} + */ + hasChangedAttrs( + identifier: StableRecordIdentifier + ): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to attributes + * + * This method is a candidate to become a mutation + * + * @method rollbackAttrs + * @public + * @param identifier + * @returns {string[]} the names of fields that were restored + */ + rollbackAttrs( + identifier: StableRecordIdentifier + ): string[]; + + /** + * Query the cache for the current state of a relationship property + * + * @method getRelationship + * @public + * @param identifier + * @param field + * @returns resource relationship object + */ + getRelationship( + identifier: StableRecordIdentifier, + field: string + ): Relationship; + + + // Resource State + // =============== + + /** + * Update the cache state for the given resource to be marked + * as locally deleted, or remove such a mark. + * + * This method is a candidate to become a mutation + * + * @method setIsDeleted + * @public + * @param identifier + * @param isDeleted {boolean} + */ + setIsDeleted( + identifier: StableRecordIdentifier, + isDeleted: boolean + ): void; + + /** + * Query the cache for any validation errors applicable to the given resource. + * + * @method getErrors + * @public + * @param identifier + * @returns {ValidationError[]} + */ + getErrors( + identifier: StableRecordIdentifier + ): ValidationError[]; + + /** + * Query the cache for whether a given resource has any available data + * + * @method isEmpty + * @public + * @param identifier + * @returns {boolean} + */ + isEmpty( + identifier: StableRecordIdentifier + ): boolean; + + /** + * Query the cache for whether a given resource was created locally and not + * yet persisted. + * + * @method isNew + * @public + * @param identifier + * @returns {boolean} + */ + isNew( + identifier: StableRecordIdentifier + ): boolean; + + /** + * Query the cache for whether a given resource is marked as deleted (but not + * necessarily persisted yet). + * + * @method isDeleted + * @public + * @param identifier + * @returns {boolean} + */ + isDeleted( + identifier: StableRecordIdentifier + ): boolean; + + /** + * Query the cache for whether a given resource has been deleted and that deletion + * has also been persisted. + * + * @method isDeletionCommitted + * @public + * @param identifier + * @returns {boolean} + */ + isDeletionCommitted( + identifier: StableRecordIdentifier + ): boolean; + +} +``` + + + +
+ Types + + + +```ts +// The ResourceBlob is an opaque type that must +// satisfy two constraints. +// (1) it should be possible for the IdentifierCache +// to be able to generate a RecordIdentifier for it +// whether by default or due to configuration. +// (2) it should be in a format expected by the Cache. +// This format is Cache declared. +// +// this Opaqueness allows arbitrary storage of any +// serializable / transferable state including such things +// as Buffers and Strings. +type ResourceBlob = unknown; + +// a "Stable" RecordIdentifier means +// that the object reference is known to +// the IdentifierCache, and as such +// referential integrity may be used +// to key by reference if desired. +interface StableRecordIdentifier { + type: string; + lid: string; + id: string | null; +} + +// An error relating to a Resource +// Received when attempting to persist +// changes to that resource. +// +// considered "opaque" to the Store itself. +// +// Currently we restrict Errors to being +// shaped in JSON:API format; however, +// this is a restriction we will willingly +// recede if desired. So long as the +// presentation layer and the cache and the +// network layer are in agreement about the +// schema of these Errors, then EmberData +// has no reason to enforce this shape. +interface ValidationError { + title: string; + detail: string; + source: { + pointer: string; + }; +} + +interface Op { + op: string; +} + +// Occasionally the IdentifierCache +// discovers that two previously thought +// to be distinct Identifiers refer to +// the same ResourceBlob. This Operation +// will be performed giving the Cache the +// change to cleanup and merge internal +// state as desired when this discovery +// is made. +interface MergeOperation extends Op { + op: 'mergeIdentifiers'; + // existing + record: StableRecordIdentifier; + // new + value: StableRecordIdentifier; +} + +// An Operation is an action that updates +// the remote state of the Cache in some +// manner. Additional Operations will be +// added in the future. +type Operation = MergeOperation; + +// A Mutation is an action that updates +// the local state of the Cache in some +// manner. +// Most Mutations are in theory also +// Operations; with the difference being +// that the change should be applied as +// "local" or "dirty" state instead of +// as "remote" or "clean" state. +// +// Note: this RFC does not publicly surface +// any of the mutations listed here as +// "operations", though the (private) Graph +// already expects and utilizes these. +// and we look forward to an RFC that makes +// the Graph a fully public API. +type Mutation = + | ReplaceRelatedRecordsMutation + | ReplaceRelatedRecordMutation + | RemoveFromRelatedRecordsMutation + | AddToRelatedRecordsMutation + | SortRelatedRecordsMutation; + + +// Note: in v1 data could be a ResourceIdentifier, now +// we request that it be in the stable form already. +interface ResourceRelationship { + data?: StableRecordIdentifier | null; + meta?: Dict; + links?: Links; +} + +// Note: in v1 data could be a ResourceIdentifier, now +// we request that it be in the stable form already. +interface CollectionRelationship { + data?: StableRecordIdentifier[]; + meta?: Dict; + links?: PaginationLinks; +} + +type Relationship = ResourceRelationship | CollectionRelationship; + +``` + +
+ +
+ Mutations + + + +```ts + +interface AddToRelatedRecordsMutation { + op: 'addToRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier | StableRecordIdentifier[]; + index?: number; +} + +interface RemoveFromRelatedRecordsMutation { + op: 'removeFromRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier | StableRecordIdentifier[]; + index?: number; +} + +interface ReplaceRelatedRecordMutation { + op: 'replaceRelatedRecord'; + record: StableRecordIdentifier; + field: string; + // never null if field is a collection + value: StableRecordIdentifier | null; + // if field is a collection, + // the value we are swapping with + prior?: StableRecordIdentifier; + index?: number; +} + +interface ReplaceRelatedRecordsMutation { + op: 'replaceRelatedRecords'; + record: StableRecordIdentifier; + field: string; + // the records to add. If no prior/index + // specified all existing should be removed + value: StableRecordIdentifier[]; + // if this is a "splice" the + // records we expect to be removed + prior?: StableRecordIdentifier[]; + // if this is a "splice" the + // index to start from + index?: number; +} + +interface SortRelatedRecordsMutation { + op: 'sortRelatedRecords'; + record: StableRecordIdentifier; + field: string; + value: StableRecordIdentifier[]; +} + +``` + +
+ +
+ +A key takeaway from these changes should be that generally the Resource API is evolving away +from hyper-granular methods for operating on the cache towards general-purpose methods +that allow for customized granularity and can be extended via additional sigantures (operations) +instead of by adding new methods. + +This is partly-due to the cache evolving to handle more than just resources, but it is also due +to us desiring to eliminate non-opaque-schema from the core APIs. EmberData should not need to +care whether some field is an Attribute or a Relationship. It needs to manage the flow of mutations +and queries, but it need not define their specificity nor shoe-horn the mechanics into artificial +constraints. + +We expect further evolution in this area in the future, mostly in the direction of removing +single-purpose methods towards operational flows. + +### 3. Introduction of Document Cache API + +The design for caching documents focuses on solving three key constraints. + +1) It should be possible to serialize the cache, and so the data handed to cache should + be in a serialized form. +2) It should be possible to rebuild/retrieve the raw response via peek so that if + desired the Cache could be used as little more than (for instance) an in-memory + JSON store. +3) The cache should have access to serializable information about the request and response + that may be required for proper cache storage, management or desired for later access. + + + + + + + + + + + +
Cache APIsAssociated Types
+ +```ts +class Cache { + /** + * Cache the response to a request + * + * Unlike `store.push` which has UPSERT + * semantics, `put` has `replace` semantics similar to + * the `http` method `PUT` + * + * the individually cacheable resource data it may contain + * should upsert, but the document data surrounding it should + * fully replace any existing information + * + * Note that in order to support inserting arbitrary data + * to the cache that did not originate from a request `put` + * should expect to sometimes encounter a document with only + * a `data` member and therefor must not assume the existence + * of `request` and `response` on the document. + * + * @method put + * @param {StructuredDocument} doc + * @returns {ResourceDocument} + * @public + */ + put(doc: StructuredDocument): ResourceDocument; + + /** + * Update the "remote" or "canonical" (persisted) state of the Cache + * by merging new information into the existing state. + * + * @method patch + * @param {Operation} op + * @returns {void} + * @public + */ + patch( + op: Operation + ): void; + + /** + * Update the "local" or "current" (unpersisted) state of the Cache + * + * @method mutate + * @param {Mutation} mutation + * @returns {void} + * @public + */ + mutate( + mutation: Mutation + ): void; + + /** + * Peek the Cache for the existing data associated with + * a StructuredDocument + * + * @method peek + * @param {StableDocumentIdentifier} + * @returns {ResourceDocument | null} + * @public + */ + peek( + identifier: StableDocumentIdentifier + ): ResourceDocument | null; + + /** + * Peek the Cache for the existing request data associated with + * a cacheable request + * + * @method peekRequest + * @param {StableDocumentIdentifier} + * @returns {StableDocumentIdentifier | null} + * @public + */ + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null; + + didCommit( + identifier: StableRecordIdentifier, + data: StructuredDataDocument + ): void; + + commitWasRejected( + identifier: StableRecordIdentifier, + errors?: StructuredErrorDocument + ): void; +} +``` + + + + +```ts + +interface RequestInfo extends Request { + disableTestWaiter?: boolean; + /* + * data that a handler should convert into + * the query (GET) or body (POST) + */ + data?: Record; + /* + * options specifically intended for handlers + * to utilize to process the request + */ + options?: Record; +} + +interface ResponseInfo { + readonly headers: ImmutableHeaders; // to do, maybe not this? + readonly ok: boolean; + readonly redirected: boolean; + readonly status: number; + readonly statusText: string; + readonly type: string; + readonly url: string; +} + +interface StructuredDataDocument { + request: RequestInfo; + response: Response | ResponseInfo | null; + content: T; +} +interface StructuredErrorDocument extends Error { + request: RequestInfo; + response: Response | ResponseInfo | null; + error: Error; + content?: unknown; +} + +type StructuredDocument = StructuredDataDocument | StructuredErrorDocument; + +// must have one of meta, data or error +interface ResourceDocument { + // the url or cache-key associated with the structured document + lid: string; + links?: Links; + meta?: Meta; + data?: StableRecordIdentifier | StableRecordIdentifier[] | null; + error?: Error; +} + +``` + +
+ +#### Notice about RequestStateService + +Currently the `RequestStateService` which provides access to promise information about +save/fetch requests for individual resources will not be altered. However, the response +cache it provides will now differ in structure from the `StructuredDocument` provided to the +cache, and relying on it for more than querying the status of a request will prove brittle +as we expect this to undergo further design work to align with `StructuredDocument` in the +near future as we introduce a new design for managing requests. + + +#### Changes to `store.push` + +Historically, `store.push` received a `JSON:API` document from which it decomposed resources +from within `data` and `included` which it then individually inserted into the cache. As well, +`store.push` was the primary mechanism by which data would load into the cache following any +request. + +Beginning with this new cache implementation, this role will be significantly reduced. Instead, +responses from requests will be passed as `StructuredDocuments` directly to the cache for the +cache to decompose as it sees fit. + +`store.push` will remain, however it too will see two significant changes. + 1) It will no longer decompose the data it receives, instead pushing that + data (`T`) into the cache via `cache.put({ data: T })` + 2) It will no longer be required that the data given to `push` be in `JSON:API` format, + the new requirement will be that it be in the format expected by the configured `Cache`. + +### 4. Introduction of Streaming Cache API + +Cache implementations should implement two methods to support streaming +SSR and AOT Hydration. + +`cache.dump` should return a stream of the cache's contents that can be +provided to the same cache's `hydrate` method. The only requirement is +that a `Cache` should output a stream from `cache.dump` that it can also import +via `cache.hydrate`. The opaque nature of this contract allows cache implementations +the flexibility to experiment with the best formats for fastest restore. + +`cache.dump` returns a promise resolving to this stream to allow the cache the +opportunity to handle any necessary async operations before beginning the stream. + +`cache.hydrate` should accept a stream of content to add to the cache, and return +a `Promise` the resolves when reading the stream has completed or rejects if for +some reason `hydrate` has failed. Currently there is no defined behavior for +recovery when `hydrate` fails, and caches may handle a failure however they see fit. + +A key consideration implementations of `cache.hydrate` must make is that `hydrate` +should expect that it may be called in two different modes: both during initial +hydration and to hydrate additional state into an already booted application. + + + +```ts +class Cache { + /** + * Serialize the entire contents of the Cache into a Stream + * which may be fed back into a new instance of the same Cache + * via `cache.hydrate`. + * + * @method dump + * @returns {Promise} + * @public + */ + async dump(): Promise>; + + /** + * hydrate a Cache from a Stream with content previously serialized + * from another instance of the same Cache, resolving when hydration + * is complete. + * + * This method should expect to be called both in the context of restoring + * the Cache during application rehydration after SSR **AND** at unknown + * times during the lifetime of an already booted application when it is + * desired to bulk-load additional information into the cache. This latter + * behavior supports optimizing pre/fetching of data for route transitions + * via data-only SSR modes. + * + * @method hydrate + * @param {ReadableStream} stream + * @returns {Promise} + * @public + */ + async hydrate(stream: ReadableStream): void; +} +``` + + +### 5. Introduction of Cache Forking + +Cache implementations should implement three methods to support + store forking. While the mechanics of how a Cache chooses to + fork are left to it, forks should expect to live-up to the following + constraints. + +1. A parent should never retain a reference to a child. +2. A child should never mutate/update the state of a parent. + +> Note: when saving state on a `Store`, the `store` is provided as the +> first argument to Adapter and Serializer methods, thereby allowing +> these class instances to access information about the current `Store` +> which may not be the same instance as the singleton `Store` +> injectible as a service. This is in keeping with the same existing design +> we have today for these classes to support multiple unique top-level stores. + +```ts +class Store { + /** + * Create a fork of the Store starting + * from the current state. + * + * @method fork + * @public + * @returns Promise + */ + async fork(): Promise; + + /** + * Merge a fork back into a parent Store + * + * @method merge + * @param {Store} store + * @public + * @returns Promise + */ + async merge(store: Store): void; +} +``` + +```ts +class Cache { + /** + * Create a fork of the cache from the current state. + * + * Applications should typically not call this method themselves, + * preferring instead to fork at the Store level, which will + * utilize this method to fork the cache. + * + * @method fork + * @public + * @returns Promise + */ + async fork(): Promise; + + /** + * Merge a fork back into a parent Cache. + * + * Applications should typically not call this method themselves, + * preferring instead to merge at the Store level, which will + * utilize this method to merge the caches. + * + * @method merge + * @param {Cache} cache + * @public + * @returns Promise + */ + async merge(cache: Cache): void; + + /** + * Generate the list of changes applied to all + * record in the store. + * + * Each individual resource or document that has + * been mutated should be described as an individual + * `Change` entry in the returned array. + * + * A `Change` is described by an object containing up to + * three properties: (1) the `identifier` of the entity that + * changed; (2) the `op` code of that change being one of + * `upsert` or `remove`, and if the op is `upsert` a `patch` + * containing the data to merge into the cache for the given + * entity. + * + * This `patch` is opaque to the Store but should be understood + * by the Cache and may expect to be utilized by an Adapter + * when generating data during a `save` operation. + * + * It is generally recommended that the `patch` contain only + * the updated state, ignoring fields that are unchanged + * + * ```ts + * interface Change { + * identifier: StableRecordIdentifier | StableDocumentIdentifier; + * op: 'upsert' | 'remove'; + * patch?: unknown; + * } + * ``` + * + */ + async diff(): Promise; +} +``` + +## The complete v2.1 Cache Interface + +```ts +interface Cache { + version: '2'; + + // Cache Management + // ================ + + put(doc: StructuredDocument): ResourceDocument; + patch(op: Operation): void; + mutate(mutation: Mutation): void; + + peek(identifier: StableRecordIdentifier): ResourceBlob | null; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null; + + upsert( + identifier: StableRecordIdentifier, + data: ResourceBlob, + hasRecord: boolean + ): void | string[]; + + // Cache Forking Support + // ===================== + + async fork(): Promise; + async merge(cache: Cache): void; + async diff(): Promise; + + // SSR Support + // =========== + + async dump(): Promise>; + async hydrate(stream: ReadableStream): void; + + // Resource Support + // ================ + + willCommit( + identifier: StableRecordIdentifier + ): void; + + didCommit( + identifier: StableRecordIdentifier, + data: StructuredDataDocument + ): void; + + commitWasRejected( + identifier: StableRecordIdentifier, + errors?: StructuredErrorDocument + ): void; + + getErrors( + identifier: StableRecordIdentifier + ): ValidationError[]; + + getAttr( + identifier: StableRecordIdentifier, + field: string + ): unknown; + + setAttr( + identifier: StableRecordIdentifier, + field: string, + value: unknown + ): void; + + changedAttrs( + identifier: StableRecordIdentifier + ): Record; + + hasChangedAttrs( + identifier: StableRecordIdentifier + ): boolean; + + rollbackAttrs( + identifier: StableRecordIdentifier + ): string[]; + + getRelationship( + identifier: StableRecordIdentifier, + field: string + ): Relationship; + + unloadRecord( + identifier: StableRecordIdentifier + ): void; + + isEmpty( + identifier: StableRecordIdentifier + ): boolean; + + clientDidCreate( + identifier: StableRecordIdentifier, + createArgs?: Record + ): Record; + + isNew( + identifier: StableRecordIdentifier + ): boolean; + + setIsDeleted( + identifier: StableRecordIdentifier, + isDeleted: boolean + ): void; + + isDeleted( + identifier: StableRecordIdentifier + ): boolean; + + isDeletionCommitted( + identifier: StableRecordIdentifier + ): boolean; +} +``` + +## Typescript Support + +All implementable interfaces involved in this RFC will be made available via a new package +`@ember-data/experimental-preview-types`. These types should be considered unstable. When +we no longer consider these types experimental we will mark their stability by migrating +them to `@ember-data/types`. + +Documentation for these types is likely to ship + publicly before the types themselves become installable, and will do so using the final + package name (`@ember-data/type`) so that the interfaces are easily explorable on + `api.emberjs.com` even before they are mature enough for consumption. + +The specific reason for this instability is the need to flesh out and implement an official +pattern for *registries* for record types and their fields. For instance, we expect to change +from `type: string` to the more narrowable and restricted `keyof ModelRegistry & string` when that occurs. + +## How we teach this + +- updated learning URLs +- updated learning materials (see [emberjs/data#8394](https://github.com/emberjs/data/issues/8394)) +- while this adds to cache, it does not add + the APIs needed for network/store etc. + future RFCs for exposing these capabilities + will better define the learning story for the + average user. Basic integration was defined by [RFC#860](https://github.com/emberjs/rfcs/pull/860) + +## Drawbacks + +- we haven't explored with implementations on some of these + ideas; however, it is difficult to explore sans-accepted-rfc + due to useful features requiring some minimal handshake agreement + with other layers of the system. +- but singleton + versioning + manager should keep us safely constrained to allow this + exploration while still staying within bounds of steerable patterns. + +## Alternatives + +- a separate document cache +- ephemeral records / buffered proxies +- multiple stores fully isolated (no cache inheritance) +- SSR support at the "source" (Adapter) level only, similar to Shoebox. This approach has a large number of negative performance ramifications. diff --git a/text/0860-ember-data-request-service.md b/text/0860-ember-data-request-service.md index 47765512ab..c27da151ca 100644 --- a/text/0860-ember-data-request-service.md +++ b/text/0860-ember-data-request-service.md @@ -1,12 +1,15 @@ --- -stage: accepted -start-date: 2023-11-10 -release-date: +stage: released +start-date: 2023-11-10T00:00:00.000Z +release-date: release-versions: + ember-data: v4.12.0 teams: - data prs: - accepted: https://github.com/emberjs/rfcs/pull/860 + accepted: 'https://github.com/emberjs/rfcs/pull/860' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/879' + released: 'https://github.com/emberjs/rfcs/pull/925' project-link: --- @@ -63,8 +66,8 @@ interface RequestManager { For example: ```ts -import { RequestManager } from '@ember-data/request'; -import { Fetch } from '@ember/data/request/fetch'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember/data/request/fetch'; import Auth from 'ember-simple-auth/ember-data-handler'; import Config from './config'; @@ -93,27 +96,57 @@ for streaming data; however, when doing so the future should not resolve until the response stream is fully read. ```ts +/** + * @class Future + * @public + */ interface Future extends Promise> { + /** + * Cancel this request by firing the AbortController's signal. + * + * @method abort + * @public + * @returns {void} + */ abort(): void; - async getStream(): ReadableStream | null; + /** + * Get the response stream, if any, once made available. + * + * @method getStream + * @public + * @returns {Promise} + */ + getStream(): Promise; + + /** + * Run a callback when this request completes. Use sparingly. + * + * @method onFinalize + * @param cb the callback to run + * @public + * @returns {void} + */ + onFinalize(cb: () => void): void; } ``` The `StructuredDocument` interface is the same as is proposed in emberjs/rfcs#854 but is shown here in richer detail. ```ts -interface RequestInfo { - /** +interface RequestInfo extends Request { + disableTestWaiter?: boolean; + /* * data that a handler should convert into * the query (GET) or body (POST) */ data?: Record; - /** + /* * options specifically intended for handlers * to utilize to process the request */ options?: Record; + /** * Allows supplying a custom AbortController for * the request, if none is supplied one is generated @@ -125,12 +158,7 @@ interface RequestInfo { * request on the context supplied to handlers. */ controller?: AbortController; - - // the below options perfectly mirror the - // native Request interface - cache?: RequestCache; - credentials?: RequestCredentials; - destination?: RequestDestination; + /** * Once a request has been made it becomes immutable, this * includes Headers. To modify headers you may copy existing @@ -140,21 +168,15 @@ interface RequestInfo { * to allow this to be done swiftly. */ headers?: Headers; - integrity?: string; - keepalive?: boolean; - method?: string; - mode?: RequestMode; - redirect?: RequestRedirect; - referrer?: string; - referrerPolicy?: ReferrerPolicy; + /** * Typically you should not set this, though you may choose to curry * a received signal if calling next. signal will automatically be set * to the associated controller's signal if none is supplied. */ signal?: AbortSignal; - url?: string; } + interface ResponseInfo { headers: Headers; ok: boolean; @@ -167,13 +189,14 @@ interface ResponseInfo { interface StructuredDataDocument { request: RequestInfo; - response: ResponseInfo; - data: T; + response: Response | ResponseInfo | null; + content: T; } interface StructuredErrorDocument extends Error { request: RequestInfo; - response: ResponseInfo; - error: string | object; + response: Response | ResponseInfo | null; + error: Error; + content?: unknown; } type StructuredDocument = StructuredDataDocument | StructuredErrorDocument; ``` @@ -193,7 +216,7 @@ that it can then compose how it sees fit with its own response. type NextFn =

(req: RequestInfo) => Future

; interface Handler { - request(context: RequestContext, next: NextFn): T; + request(context: RequestContext, next: NextFn): Promise | Future; } ``` @@ -326,8 +349,8 @@ applications by exporting the manager as an Ember service. *services/request.ts* ```ts -import { RequestManager } from '@ember-data/request'; -import { Fetch } from '@ember/data/request/fetch'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember/data/request/fetch'; import Auth from 'ember-simple-auth/ember-data-handler'; export default class extends RequestManager { @@ -356,8 +379,8 @@ Alternatively to have a request service unique to the store: ```ts import Store from '@ember-data/store'; -import { RequestManager } from '@ember-data/request'; -import { Fetch } from '@ember/data/request/fetch'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember/data/request/fetch'; export default class extends Store { requestManager = new RequestManager(); @@ -376,8 +399,8 @@ like the above would need to be done by the consuming application in order to ma ```ts import Store from '@ember-data/store'; -import { RequestManager } from '@ember-data/request'; -import { LegacyHandler } from '@ember-data/legacy-network-handler'; +import RequestManager from '@ember-data/request'; +import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; export default class extends Store { requestManager = new RequestManager(); @@ -454,6 +477,41 @@ be useful for both application code and the Ember Inspector. If you are interest such support, we would accept an RFC. With the greatly improved flow this RFC brings we expect that the overall design of the RequestStateService ought to be revisited. +### Registering a CacheHandler + +While any handler could make use of a cache, there is a handler granted specialized +status which effectively functions as the very first handler in the handler chain +(some additional special priviledges may be afforded around timing). + +Only one such handler may exist, and an error will be thrown if more than one +is attempted to be registered. + +This method should only be used by a consuming application when the RequestManager +instance is not the same instance used by the `Store`. If using `@ember-data/store`, +`@ember-data/store` configures a `CacheHandler` which utilizes the `Cache`, the `LifetimesService` +and `cacheOptions` to gate whether the request continues down the handler chain. + +This same handler is what is responsible for updating the `Cache` via `Cache.put` once +the request completes. + +```ts +class RequestManager { + /** + * Register a handler to use for primary cache intercept. + * + * Only one such handler may exist. If using the same + * RequestManager as the Store instance the Store + * registers itself as a Cache handler. + * + * @method useCache + * @public + * @param {Handler[]} cacheHandler + * @returns {void} + */ + useCache(cacheHandler: Handler): void; +} +``` + ### Cache Lifetimes In the past, cache lifetimes for single resources were controlled by either @@ -464,13 +522,16 @@ for `shouldReloadRecord`, `shouldReloadAll`, `shouldBackgroundReloadRecord` and This behavior will now be controlled by the combination of either supplying `cacheOptions` on the associated `RequestInfo` or by supplying a `lifetimes` service to the `Store`. +Explicit `cacheOptions` will always take precedence over the `lifetimes` service. + ```ts class Store { lifetimes: LifetimesService; } interface LifetimesService { - isExpired(url: string, method: HTTPMethod) {} + isHardExpired(identifier: StableDocumentIdentifier): boolean; + isSoftExpired(identifier: StableDocumentIdentifier): boolean; } ``` @@ -489,6 +550,11 @@ Adapter and Serializer methods. If no adapter exists for the type (including no handler would call `next`. In this manner an app can incrementally migrate request-handling to this new paradigm on a per-type basis as desired. +The legacy handler would only attempt to handle requests with an `op` and no `url`. Requests with a `url` +would be forwarded on via `next`. In this way, individual requests can be migrated away from legacy by +either directly invoking `store.request` with the correct args or by utilizing a request builder which +assigns the url to the request object. + The package `ember-data` would automatically configure this handler. If not using `ember-data` this configuration would need to be done explicitly. @@ -649,4 +715,5 @@ instead encouraging data-transformation to be done within the Adapter. In fact, is fully possible today, we could just better document it and do nothing more. However, this approach does not solve the need for more general request management, nor does it interact well with common development paradigms such as GraphQL query building, nor does it allow us -to introduce pagination-by-default. \ No newline at end of file +to introduce pagination-by-default, and finally it does very little to advance the goal of being +a document centric cache. \ No newline at end of file diff --git a/text/0907-pnpm-support.md b/text/0907-pnpm-support.md new file mode 100644 index 0000000000..98600de6d2 --- /dev/null +++ b/text/0907-pnpm-support.md @@ -0,0 +1,167 @@ +--- +stage: accepted +start-date: 2023-03-06T14:09:00.000Z +release-date: # In format YYYY-MM-DDT00:00:00.000Z +release-versions: +teams: # delete teams that aren't relevant + - cli + - learning +prs: + accepted: https://github.com/emberjs/rfcs/pull/907 +project-link: +suite: +--- + + + +# `pnpm` support + +## Summary + +Enable Ember CLI users to opt into using `pnpm` for package management. + +## Motivation + +[`pnpm`](https://pnpm.io/) is a popular alternative to both `npm` and `yarn` that prioritizes correctness, especially for `peerDependencies` and monorepos. + +`pnpm` has very active maintainance, support, and financial funding. +Their [website](https://pnpm.io/) states that `pnpm` is: + - Fast: `pnpm` is up to 2x faster than the alternatives + - Efficient: Files inside `node_modules` are cloned or hard linked from a single content-addressable storage + - Supports monorepos: `pnpm` has built-in support for multiple packages in a repository + - Strict: `pnpm` creates a non-flat `node_modules` by default, so code has no access to arbitrary packages. + +For folks with lots of projects on their computers, `pnpm` is _extremely_ space-efficient. +Where `npm` and `yarn` would duplicate `node_modules` per-project, `pnpm` will only download a package (at a specific version) once across your whole machine. + +Additionally, `npm` and `yarn` repeatedly have demonstrated that their strategies with `peerDependencies` are not correct, and it is vitally important we use and support a tool that can guide folks towards correctly creating addons. For example, `@embroider/macros` relies on node's resolution algorithm, so having `peerDependencies` resolved correctly is important for `dependencySatisfies` to work as expected in monorepos. + +Ember CLI currently only supports `npm` (default) and `yarn` for project initialization as well as various commands. At present, projects work with `pnpm`, but the tooling is unaware. + + + +## Detailed design + +Enabling `pnpm` is designed as opt-in to prevent disruptions to developer's current workflow, much the same as `yarn`. + +There are a few integration points that we must support for `pnpm` (and any package manager): + - `ember install` + - `ember init`, `ember new`, `ember addon` + - `ember try:one`, `ember try:each` + - generated C.I. configs + - Documenation + +### `ember install` + +There are two mechanisms through which to opt-in. +The first one is the presence of a `pnpm-lock.yaml` file in the project root. + +The `pnpm-lock.yaml` file is generated by `pnpm` when you run `pnpm install` (or the shorter `pnpm i`), +so we assume that its presence means the developer intends to use `pnpm` to manage their dependencies. + +Alternatively you, you can force Ember CLI to use `pnpm` with the `--pnpm` flag, and symmetrically, +you can force Ember CLI to not use `pnpm` with the `--no-pnpm` flag. + +To recap: + +- `ember install ember-resources` with `pnpm-lock.yaml` present will use `pnpm` +- `ember install ember-resources` without `pnpm-lock.yaml` present will use npm +- `ember install ember-resources --pnpm` will use `pnpm` +- `ember install ember-resources --no-pnpm` will use npm + +### `ember init`, `ember new`, `ember addon` + +Since this triad of commands is generally ran before a project is set up, there is no `pnpm-lock.yaml` file presence to check. +This means we are left with the `--pnpm`/`--no-pnpm` pair of flags, that will also be added to these commands: + +- `ember new my-app` will use npm +- `ember new my-app --pnpm` will use `pnpm` + +The above also applies to `ember addon` and `ember init`, noting that `ember init` doesn't receive any arguments. + +#### `--skip-npm` + +The `--skip-npm` flag _actually means_ "skip installation of dependencies" when using `ember addon` and `ember new`, +including skipping installation with both `npm` and `yarn`. +This will need to be extended to also skip installation of dependencies when `pnpm` is used. + +For example: +```bash +ember new my-app --pnpm --skip-npm +``` + + +### `ember try:one`, `ember try:each` + +At the time of writing this RFC, `ember-try` already supports `pnpm`, but it is undocumented in [the README](https://github.com/ember-cli/ember-try). +Documentation will need to be added to the README, +as well as the relevant `ember-cli` blueprints will need to correctly configure `usePnpm: true` in the `ember-try.js` config file when the `pnpm` flag is present. + + +### generated C.I. configs + +At the time of writing this RFC, `ember-cli` supports two C.I. environments: Travis and GitHub Actions. + +Both the `.travis.yml` and `.github/workflows/ci.yml` config files for relevant blueprints will need to support the `pnpm` option such that C.I. passes on new projects using `pnpm`. + +### for addons + +Addons should be stricter than defaults, so that they are good stewards of the ecosystem and don't leak accidental uncertainty to their consumers. + +In addon projects, the `.npmrc` will need the following defaults: +``` +# all peer dependencies must be declared or forwarded to the consumer +auto-install-peers=false + +# we want true isolation in addons -- if a dependency is not declared, we want an error +reslove-peers-from-workspace-root=false +``` + +### Documentation + +These pages presently mention `npm` / `yarn` and will need to be updated to include `pnpm` + - https://cli.emberjs.com/release/basic-use/assets-and-dependencies/ + - https://guides.emberjs.com/release/addons-and-dependencies/ + +## How we teach this + +The Ember Guides should be updated to reflect the new flags, where applicable, +as well as the new behavior of `ember install` in the presence of a `pnpm-lock.yaml` -- though most of the guides use `ember` as the CLI tool for managing packages. + +We may want to consider updating the [tutorial](https://guides.emberjs.com/release/tutorial/part-1/orientation/) (and its automation) to use `pnpm`, as `npm` is very slow. + + +In addition, the built-in instructions for `ember help` should be updated to reflect the new flags. + + +## Drawbacks + +- `pnpm` is very strict about peers and what dependencies are allowed in a project and will error if a project's package.json is incorrect for a given scenario. `pnpm` is very clear about these errors and what to do for action items, but it means that we'll need to make sure that `pnpm` is tested in `ember-cli` when generating new projects so that we can be certain that folks' "first time experience" is smooth +- There may be other package managers in the future, but we cannot see the future. There have been talks about making ember-cli somehow generically handle package-managers, but it is unknown how that would work, and is unneeded for now. + + +## Alternatives + +- Figure out a way to generically handle any package manager +- Continue with the partial pnpm support we have today + +## Unresolved questions + +- Are there any other references in the guides to `npm` or `yarn`? + The only place I could find that _mentioned_ `yarn` is here: https://guides.emberjs.com/release/addons-and-dependencies/ +- Is there a `--package-manager` flag / option in `ember-cli`? for blueprint authors, that may be useful. +- Should `--skip-npm` be aliased to `--skip-install`? + + diff --git a/text/0931-template-compiler-api.md b/text/0931-template-compiler-api.md new file mode 100644 index 0000000000..8796f9eff8 --- /dev/null +++ b/text/0931-template-compiler-api.md @@ -0,0 +1,436 @@ +--- +stage: accepted +start-date: 2023-05-31 +release-date: # In format YYYY-MM-DDT00:00:00.000Z +release-versions: +teams: # delete teams that aren't relevant + - cli + - data + - framework + - learning + - steering + - typescript +prs: + accepted: https://github.com/emberjs/rfcs/pull/931 +project-link: +suite: +--- + + + +# JS Representation of Template Tag + +## Summary + +Formalize a Javascript-spec-compliant representation of template tag. + +## Motivation + +The goal of this RFC is to improve the plain-Javascript representation of the Template Tag (aka "first-class component templates") feature in order to: + +- reduce the number and complexity of API calls required to represent a component +- efficiently coordinate between different layers of build-time and static-analysis tooling that need to use preprocessing to understand our GJS syntax extensions. +- avoid exposing a "bare template" as a user-visible value +- provide a declarative way to opt-in to run-time (as opposed to build-time) template compilation. +- enable support for class private fields in components + +As an illustrative example, currently this template tag expression: + +```js +let x = ; +``` + +Has the plain javascript representation: + +```js +import { precompileTemplate } from "@ember/component"; +import templateOnlyComponent from "@ember/component/template-only"; +import { setComponentTemplate } from "@ember/component"; +let x = setComponentTemplate( + precompileTemplate("Hello {{message}}", { + strictMode: true, + scope: () => ({ message }), + }), + templateOnlyComponent() +); +``` + +This RFC proposes simplifying the above case to: + +```js +import { template } from "@ember/template-compiler"; +let x = template("Hello {{message}}", { + scope: () => ({ message }), +}); +``` + +As a second illustrative example, currently this class member template: + +```js +class Example extends Component { + +} +``` + +Has the plain javascript representation: + +```js +import { precompileTemplate } from "@ember/component"; +import { setComponentTemplate } from "@ember/component"; +class Example extends Component {} +setComponentTemplate( + precompileTemplate("Hello {{message}}", { + strictMode: true, + scope: () => ({ message }), + }), + Example +); +``` + +This RFC proposes simplifying the above case to: + +```js +import { template } from "@ember/template-compiler"; +class Example extends Component { + static { + template( + "Hello {{message}}", + { + component: this, + scope: () => ({ message }), + }, + ); + } +} +``` + +## Detailed design + +This RFC introduces two new importable APIs: + +```js +// The ahead-of-time template compiler: +import { template } from "@ember/template-compiler"; + +// The runtime template compiler: +import { template } from "@ember/template-compiler/runtime"; +``` + +They are intended to be drop-in replacements for each other _except_ for the differences summarized in this table: + +| | Template Contents | Scope Param | Syntax Errors | Payload | +| ------------- | ---------------------- | -------------------------------------------- | ------------------------ | ---------------- | +| Ahead-of-Time | Restricted to literals | Restricted to a few explicitly-allowed forms | Stops your build | Smaller & Faster | +| Runtime | Unrestricted | Unrestricted | Can by caught at runtime | Larger & Slower | + +By putting these two implementations in different importable modules, the problem of "how do you opt-in to including the template compiler in your app?" goes away. If you import it, you will have it, if you don't, you won't. + +The remainder of this design only uses examples with the ahead-of-time template compiler, because everything about the runtime template compiler's API is the same. + +### Scope Access + +To give templates access to the relevant Javascript scope, we offer **two different forms** for two different use cases. A critical feature of this design is that *both forms* have valid Javascript syntax **and** semantics. That means they can actually run when you want them to, with no further processing. And they are fully understandable by all spec-compliant Javascript tools. This is in contrast with intermediate forms like `[__GLIMMER_TEMPLATE("")]` in the current ember-template-imports implementation or the proposed: + +```js +template`` +``` + +from [RFC 813](https://github.com/emberjs/rfcs/pull/813), which both lack any mechanism to access the surrounding scope, and therefore need "magic" beyond Javascript to make them run. + +#### Explicit Form + +The "Explicit Form" makes all data flow statically visible. It's the appropriate form to publish to NPM. To produce Explicit Form, build tools need to do a full parse of the template and a full lexical analysis of Javascript and Handlebars scopes. + +Examples of Explicit Form: + +```js +import { template } from '@ember/template-compiler'; + +// when nothing is needed from scope, no scope params are required: +const Headline = template("

{{yield}}

"); + +// local variable access works the same as in current precompileTemplate +const Section = template( + "{{@title}}", + { + scope: () => ({ Headline}) + } +); + +// in class member position, we can also put private fields in scope +class extends Component { + static { + template( + "
", + { + component: this, + scope: (instance) => ({ + Section, + "#secret": instance.#secret + }), + }, + ) + } +} +``` + +> This RFC is focused on making sure the scope accessors can do everything javascript can do, which is why we're including private fields. However, additional work beyond this RFC is required to make the template compiler correctly parse expressions like `{{this.#secret}}`. + +#### Implicit Form + +The "Implicit Form" is cheaper and easier to produce because it doesn't need to do any lexical analysis and doesn't need to parse the handlebars at all. + +The downside is that data flow is not all statically visible, because it relies on `eval`. + +Implicit Form has two key use cases: + - as an intermediate format between a preprocessor stage (which can eliminate all GJS special syntax and semantics) and the rest of a standard Javascript toolchain. + - as the implementation format in sandbox-like environments where dynamic code execution is the whole point. + +Examples of Implicit Form: + +```js +import { template } from '@ember/template-compiler'; + +// Notice that all of these have the exact same +// `params` argument. It's always the same. +// That's why it's easy to produce. + +const Headline = template( + "

{{yield}}

", + { + eval() { return eval(arguments[0]); } + } +); + +const Section = template( + "{{@title}}", + { + eval() { return eval(arguments[0]); } + } +); + +class extends Component { + static { + template( + "
", + { + component: this, + // this handles private fields just fine, + // see Appendix A. + eval() { return eval(arguments[0]); } + }, + ) + } +} +``` + +> _eval seems bad, what about Content Security Policy?_
+> Typical apps never needs to actually _run_ the eval. This is a communication format between layers of build tooling. You _can_ run it, if you're making something like an interactive development sandbox. But that is a case that already requires `eval` anyway. + +> _Why `arguments[0]` instead of an explicit argument?_
+> If we picked a local name to use for the argument, we would shadow that name in the surrounding scope. Whereas `arguments` is already a keyword that exists for this purpose, and can never collide with other local bindings. + + +### Type Signature + +```ts +import { ComponentLike } from '@glint/template'; + +declare function template( + templateContent: string, + params?: Params +): ComponentLike; + +declare function template>( + templateContent: string, + params: Params +): C; + +interface Params { + component?: ComponentClass; + strict?: boolean; + moduleName?: string; + eval?: () => Record; + scope?: ( + instance: ComponentClass extends ComponentLike + ? InstanceType + : never + ) => Record; +} + +``` + +### Strict defaults to true + +Unlike `precompileTemplate`, our `strict` param defaults to true instead of false if it's not provided. This is aligned with the expectation that our main programming model is moving everyone toward handlebars strict mode by default. + +This also addresses the naming confusing between earlier RFCs (which used "strict") and the implementations in the ecosystem (which used "strictMode"). + +### Always Returns a Component + +A key difference between `precompileTemplate` and our new `template` is that its return value is always a _component_, never a "bare template". In this sense, the implementation combines the jobs of `precompileTemplate` and `setComponentTemplate`. + +Bare templates are a historical concept that we'd like to move away from, in order to have fewer things to teach. + +When the `component` argument is passed, the return value is that backing class, with the template associated, just like `setComponentTemplate`. When the `component` argument is not provided, it creates and returns a new template-only component. + +> _Aren't route templates "bare templates"? What about them?
_ +> Yes, this RFC deliberately doesn't say anything about route templates. We expect a future routing RFC to use components to express what today's route templates express. This RFC also doesn't deprecate `precompileTemplate` yet -- although that will clearly be a good goal _after_ a new routing design addresses route templates. + +### Syntactic Restrictions + +The runtime template compiler has no syntactic restrictions. + +The ahead-of-time template compiler has syntactic restrictions on `templateContents`, `params.scope`, and `params.eval`. + +`templateContents` must be one of: + +- a string literal +- a template literal with no expressions + +If provided, `params.scope` must be: + +- an arrow function expression or function expression + - that accepts zero or one arguments + - with body containing either + - an expression + - or a block statement that contains exactly one return statement + - where the return value is an object literal + - whose properties are all non-computed + - whose values are all either + - identifiers + - or private field member expressions on our argument identifier + +If provided, `params.eval` must be: + - an object method + - whose body contains exactly one return statment. + - where the return value must be exactly `eval(arguments[0])`. + + +### Older things that are intentionally dropped + +`precompileTemplate` and its predecessors like `import { hbs } from 'ember-cli-htmlbars'` accepted some additional params that we are choosing not to keep. + + - `insertRuntimeErrors`: instead you should use `@ember/template-compiler/runtime` and catch the exception thrown by `template()`. + +## How we teach this + +Mentioning these APIs is appropriate when introducing the template tag feature in the guides. We can explain template tag as a convenience over these lower-level APIs and show side-by-side how a given template tag "really means" a call to `template()`. These examples should probably use the Explicit Form. + +We can also mention that fully runtime template compilation is possible using `@ember/template-compiler/runtime` and show this example in Implicit Form, since that pairs well with a dynamic, REPL-like environment. + +## Drawbacks + + + +## Alternatives + +This RFC builds off the proposal in +https://github.com/emberjs/rfcs/pull/813. + +The main difference is that RFC 813 offered a form: + +``` +template`` +``` + +that converts `