From baad9a9ae353778a4839abb75e0909ad044bb508 Mon Sep 17 00:00:00 2001 From: runspired Date: Fri, 2 Dec 2022 20:40:22 +0000 Subject: [PATCH 01/57] Advance RFC to Stage ready-for-release --- text/0860-ember-data-request-service.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0860-ember-data-request-service.md b/text/0860-ember-data-request-service.md index 47765512ab..b3c414d21a 100644 --- a/text/0860-ember-data-request-service.md +++ b/text/0860-ember-data-request-service.md @@ -1,12 +1,12 @@ --- -stage: accepted -start-date: 2023-11-10 -release-date: +stage: ready-for-release +start-date: 2023-11-10T00:00:00.000Z +release-date: release-versions: teams: - data prs: - accepted: https://github.com/emberjs/rfcs/pull/860 + accepted: 'https://github.com/emberjs/rfcs/pull/860' project-link: --- From d94b2008c1df57d34206e56e779fb4c6fe686589 Mon Sep 17 00:00:00 2001 From: "Ember.js RFCS CI" Date: Fri, 2 Dec 2022 20:40:26 +0000 Subject: [PATCH 02/57] Update RFC 0860 ready-for-release PR URL --- text/0860-ember-data-request-service.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0860-ember-data-request-service.md b/text/0860-ember-data-request-service.md index b3c414d21a..86fcc26bca 100644 --- a/text/0860-ember-data-request-service.md +++ b/text/0860-ember-data-request-service.md @@ -7,6 +7,7 @@ teams: - data prs: accepted: 'https://github.com/emberjs/rfcs/pull/860' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/879' project-link: --- From a095355c71c6b9dad5e33b29bc37f49b97898220 Mon Sep 17 00:00:00 2001 From: wagenet Date: Tue, 13 Dec 2022 00:38:16 +0000 Subject: [PATCH 03/57] Advance RFC to Stage ready-for-release --- text/0811-element-modifiers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0811-element-modifiers.md b/text/0811-element-modifiers.md index 5d46f60a77..7eed70c2d7 100644 --- a/text/0811-element-modifiers.md +++ b/text/0811-element-modifiers.md @@ -1,13 +1,13 @@ --- -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/811 + accepted: 'https://github.com/emberjs/rfcs/pull/811' project-link: --- From 7d1942767ce172b430abedcea10dfb47548a3664 Mon Sep 17 00:00:00 2001 From: "Ember.js RFCS CI" Date: Tue, 13 Dec 2022 00:38:19 +0000 Subject: [PATCH 04/57] Update RFC 0811 ready-for-release PR URL --- text/0811-element-modifiers.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0811-element-modifiers.md b/text/0811-element-modifiers.md index 7eed70c2d7..cdb54caf6e 100644 --- a/text/0811-element-modifiers.md +++ b/text/0811-element-modifiers.md @@ -8,6 +8,7 @@ teams: - learning prs: accepted: 'https://github.com/emberjs/rfcs/pull/811' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/885' project-link: --- From c0785e9f3e8e3b1531ec5084713d26f0bc11ffbc Mon Sep 17 00:00:00 2001 From: wagenet Date: Sat, 14 Jan 2023 00:02:38 +0000 Subject: [PATCH 05/57] Advance RFC to Stage recommended --- text/0236-deprecation-ember-string.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0236-deprecation-ember-string.md b/text/0236-deprecation-ember-string.md index fde9040c75..2a072a85b2 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: From 4d09bdfb904ad5d55783ea971bbb4b2be9554909 Mon Sep 17 00:00:00 2001 From: "Ember.js RFCS CI" Date: Sat, 14 Jan 2023 00:02:41 +0000 Subject: [PATCH 06/57] Update RFC 0236 recommended PR URL --- text/0236-deprecation-ember-string.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0236-deprecation-ember-string.md b/text/0236-deprecation-ember-string.md index 2a072a85b2..944e4f40fb 100644 --- a/text/0236-deprecation-ember-string.md +++ b/text/0236-deprecation-ember-string.md @@ -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' From a76159f0eb17ea7058fe5f70f9d5d94d0f4f1686 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 6 Mar 2023 14:58:04 -0500 Subject: [PATCH 07/57] Propose pnpm support --- text/0907-pnpm-support.md | 153 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 text/0907-pnpm-support.md diff --git a/text/0907-pnpm-support.md b/text/0907-pnpm-support.md new file mode 100644 index 0000000000..f8b83d5e76 --- /dev/null +++ b/text/0907-pnpm-support.md @@ -0,0 +1,153 @@ +--- +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`. + +### 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`? + + From a93366e5079ea48fa4fd95bfc581d0aa991cccdf Mon Sep 17 00:00:00 2001 From: wagenet Date: Mon, 13 Mar 2023 16:31:48 +0000 Subject: [PATCH 08/57] Advance RFC {{ inputs.rfc-number }} to Stage ready-for-release --- text/0739-ember-data-deprecate-non-strict-relationships.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0739-ember-data-deprecate-non-strict-relationships.md b/text/0739-ember-data-deprecate-non-strict-relationships.md index 1e6b6c840b..0371cc483e 100644 --- a/text/0739-ember-data-deprecate-non-strict-relationships.md +++ b/text/0739-ember-data-deprecate-non-strict-relationships.md @@ -1,12 +1,12 @@ --- -stage: accepted # FIXME: This may be a further stage +stage: ready-for-release start-date: 2021-04-23T00:00:00.000Z release-date: release-versions: teams: - data prs: - accepted: https://github.com/emberjs/rfcs/pull/739 + accepted: 'https://github.com/emberjs/rfcs/pull/739' project-link: --- From fb69e734f7e9bcead64c73d8b2eec0eb76d0186f Mon Sep 17 00:00:00 2001 From: "Ember.js RFCS CI" Date: Mon, 13 Mar 2023 16:31:54 +0000 Subject: [PATCH 09/57] Update RFC 0739 ready-for-release PR URL --- text/0739-ember-data-deprecate-non-strict-relationships.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0739-ember-data-deprecate-non-strict-relationships.md b/text/0739-ember-data-deprecate-non-strict-relationships.md index 0371cc483e..f368ac99c2 100644 --- a/text/0739-ember-data-deprecate-non-strict-relationships.md +++ b/text/0739-ember-data-deprecate-non-strict-relationships.md @@ -7,6 +7,7 @@ teams: - data prs: accepted: 'https://github.com/emberjs/rfcs/pull/739' + ready-for-release: 'https://github.com/emberjs/rfcs/pull/909' project-link: --- From 5452180f0cd80bc0a905fc72bcad898a14e25878 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Mon, 13 Mar 2023 14:18:38 -0700 Subject: [PATCH 10/57] Move #0625 helper-managers to recommended --- text/0625-helper-managers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From c7829076b0c5364a6dce3974de13f11560ae2956 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Mon, 13 Mar 2023 14:29:01 -0700 Subject: [PATCH 11/57] Correct metadata for #0487 custom model classes --- text/0487-custom-model-classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From ecaed512f9bc516c15c1ddf20bfeeed3dab9b2c2 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Mon, 13 Mar 2023 14:29:55 -0700 Subject: [PATCH 12/57] Move #0331 deprecate-globals-resolver to recommended --- text/0331-deprecate-globals-resolver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 476b65ff8aa833cf5da6201641a9f7899ae9cef6 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 27 Mar 2023 00:17:17 -0700 Subject: [PATCH 13/57] chore: update RequestService url with finalized design details --- text/0860-ember-data-request-service.md | 125 +++++++++++++++++------- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/text/0860-ember-data-request-service.md b/text/0860-ember-data-request-service.md index 47765512ab..b9fcf9ddeb 100644 --- a/text/0860-ember-data-request-service.md +++ b/text/0860-ember-data-request-service.md @@ -63,8 +63,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 +93,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 +155,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 +165,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 +186,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 +213,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 +346,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 +376,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 +396,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 +474,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 +519,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(key: string, url: string, method: HTTPMethod): boolean; + isSoftExpired(key: string, url: string, method: HTTPMethod): boolean; } ``` @@ -649,4 +707,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 From 1c0e010cb5481f5781dc463cc83e5c391e527bae Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Mon, 27 Mar 2023 00:20:30 -0700 Subject: [PATCH 14/57] add note --- text/0860-ember-data-request-service.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/text/0860-ember-data-request-service.md b/text/0860-ember-data-request-service.md index b9fcf9ddeb..e215e2ed84 100644 --- a/text/0860-ember-data-request-service.md +++ b/text/0860-ember-data-request-service.md @@ -547,6 +547,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. From b6cb4eb7ef3ec2500260eaf5d96ca48b78dbcf41 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 28 Mar 2023 14:16:56 -0400 Subject: [PATCH 15/57] Update RFC 496, typos, correct field name --- text/0496-handlebars-strict-mode.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) 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. From 9f2b1b42491f3b9e3650ebe731ef8a8b307014d1 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 31 Mar 2023 19:07:49 -0700 Subject: [PATCH 16/57] Update text/0860-ember-data-request-service.md Co-authored-by: MrChocolatine <47531779+MrChocolatine@users.noreply.github.com> --- text/0860-ember-data-request-service.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0860-ember-data-request-service.md b/text/0860-ember-data-request-service.md index e215e2ed84..2f8fdca067 100644 --- a/text/0860-ember-data-request-service.md +++ b/text/0860-ember-data-request-service.md @@ -122,7 +122,7 @@ interface Future extends Promise> { * @method onFinalize * @param cb the callback to run * @public - * @returns void + * @returns {void} */ onFinalize(cb: () => void): void; } From caf22abf1b24634c4c86b3e3e2da6ec8e44d5e28 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Tue, 4 Apr 2023 20:39:44 -0700 Subject: [PATCH 17/57] finalize lifetimes --- text/0860-ember-data-request-service.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0860-ember-data-request-service.md b/text/0860-ember-data-request-service.md index 2f8fdca067..3b354422ed 100644 --- a/text/0860-ember-data-request-service.md +++ b/text/0860-ember-data-request-service.md @@ -527,8 +527,8 @@ class Store { } interface LifetimesService { - isHardExpired(key: string, url: string, method: HTTPMethod): boolean; - isSoftExpired(key: string, url: string, method: HTTPMethod): boolean; + isHardExpired(identifier: StableDocumentIdentifier): boolean; + isSoftExpired(identifier: StableDocumentIdentifier): boolean; } ``` From 5db6c681d98c182545b9453a54a462cf7d9f7aab Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Tue, 4 Apr 2023 20:44:03 -0700 Subject: [PATCH 18/57] feat: EmberData Cache v2.1 --- text/0854-ember-data-cache-v2.1.md | 1515 ++++++++++++++++++++++++++++ 1 file changed, 1515 insertions(+) create mode 100644 text/0854-ember-data-cache-v2.1.md 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..4a9666caf8 --- /dev/null +++ b/text/0854-ember-data-cache-v2.1.md @@ -0,0 +1,1515 @@ +--- +stage: accepted +start-date: 2022-08-27 +release-date: Unreleased +release-versions: + ember-source: vX.Y.Z + ember-data: vX.Y.Z +teams: + - data +prs: + accepted: https://github.com/emberjs/rfcs/pull/854 + +--- + + + +# 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. From a6e547eb81f9f9c8eaddedd01f4bb54d8dbb54ee Mon Sep 17 00:00:00 2001 From: runspired Date: Wed, 5 Apr 2023 04:13:35 +0000 Subject: [PATCH 19/57] Advance RFC {{ inputs.rfc-number }} to Stage ready-for-release --- text/0854-ember-data-cache-v2.1.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/text/0854-ember-data-cache-v2.1.md b/text/0854-ember-data-cache-v2.1.md index 4a9666caf8..e4308c317a 100644 --- a/text/0854-ember-data-cache-v2.1.md +++ b/text/0854-ember-data-cache-v2.1.md @@ -1,6 +1,6 @@ --- -stage: accepted -start-date: 2022-08-27 +stage: ready-for-release +start-date: 2022-08-27T00:00:00.000Z release-date: Unreleased release-versions: ember-source: vX.Y.Z @@ -8,8 +8,7 @@ release-versions: teams: - data prs: - accepted: https://github.com/emberjs/rfcs/pull/854 - + accepted: 'https://github.com/emberjs/rfcs/pull/854' --- + +# JS Representation of Template Tag + +## Summary + +Formalize a Javascript-spec-compliant representation of template tag. + +## Motivation + +The goal of this RFC is to simplify 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. + +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}}", + { + scope: () => ({ message }), + }, + this + ); + } +} +``` + +## 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. + +### Type Signature + +```ts +function template( + templateContent: string, + params?: Params, + backingClass?: object +): TheComponent; + +// This is the actual invokable component. Needs discussion with typescript team to formalize the correct type here and make sure the important inferrence cases work. +type TheComponent = TODO; + +interface Params { + strict?: boolean; + scope?: Scope + moduleName?: string; +} + +type Scope = + | (() => Record) + | ((local: string, instance: any) => any); + +``` + +### 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 optional `backingClass` argument is passed, the return value is that backing class, with the template associated, just like `setComponentTemplate`. When the `backingClass` 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. + +### Scope Parameter + +The scope parameter exists for the same reason as is exists in `precompileTemplate`: it gives the template access to things from the surrounding Javascript scope. + +We accept two different forms, distinguished by function arity: + +1. `() => Record` is exactly like the existing `scope` in precompileTemplate. The values in the returned object are the lexically scoped local variables available to the template. +2. `(local: string, instance: any) => any` is an alternative form that does per-local-name lookup. + +The first form is the preferred choice for addons to publish to NPM and any other long-term stable source code. It makes the data flow statically apparent to all Javascript-aware tooling. To produce this form, build tooling needs to do a full parse of the template to discover exactly what upvars are discovered in the `` tag, and intersect them with the set of bindings in Javascript scope. + +The second form is the preferred format to pass from a GJS preprocessor to other Javascript toolchains that don't understand GJS (like Babel or SWC). The typical usage would look like: + +```js +template("Hello {{message}}", { scope(c8bb1dc1e509aa6b, fca6cbb3e151d53f) => eval(c8bb1dc1e509aa6b) }}); +``` + +> *Why the weird local variable names*?
+> In order to let the `eval` access anything in scope, we need to avoid shadowing any existing names. Since this is supposed to be the cheap transformation that doesn't require lexical analysis, we'd rather avoid the collision by picking well-known randomized identifiers. + + +This is cheap to produce from `