From c59701704fa67c6d873713a0c7ef035c230a64cd Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:00:05 -0500 Subject: [PATCH 01/16] tracked promise --- text/0000-tracked-promise.md | 190 +++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 text/0000-tracked-promise.md diff --git a/text/0000-tracked-promise.md b/text/0000-tracked-promise.md new file mode 100644 index 0000000000..48d8f09379 --- /dev/null +++ b/text/0000-tracked-promise.md @@ -0,0 +1,190 @@ +--- +stage: accepted +start-date: 2024-12-19T00:00:00.000Z +release-date: # In format YYYY-MM-DDT00:00:00.000Z +release-versions: +teams: # delete teams that aren't relevant + - data + - framework + - learning +prs: + accepted: # Fill this in with the URL for the Proposal RFC PR +project-link: +suite: +--- + + + +# Built in tracking utility for promises + +## Summary + +This RFC defines a new utility to add by default to the framework, based on all the prior community implementations of promise wrapping, which is byte-wise opt-in in a brand new package (`@ember/reactive`). + +## Motivation + +Promises are ubiquitous, yet we don't have a _default_ way to use them _reactively_ in our templates. +Meanwhile, we have several _competing and duplicate_ implementations: +- `TrackedAsyncData` from [ember-async-data][gh-tracked-async-data] +- `PromiseState` from [@warp-drive/ember][gh-ember-data-promise-state] +- `State` from [reactiveweb's remote-data][gh-reactiveweb-state] +- `Request` from [ember-data-resources][gh-ember-data-resource-request] +- `Query` from [ember-await][gh-ember-await-query] + +[gh-reactiveweb-state]: https://github.com/universal-ember/reactiveweb/blob/main/reactiveweb/src/remote-data.ts#L13 +[gh-tracked-async-data]: https://github.com/tracked-tools/ember-async-data/blob/main/ember-async-data/src/tracked-async-data.ts +[gh-ember-data-promise-state]: https://github.com/emberjs/data/blob/main/packages/ember/src/-private/promise-state.ts +[gh-ember-data-resource-request]: https://github.com/NullVoxPopuli/ember-data-resources/blob/main/ember-data-resources/src/-private/resources/request.ts +[gh-ember-await-query]: https://github.com/EmberExperts/ember-await/blob/master/addon/utils/query.js#L8 + +There are probably more, but the point of this RFC is to provide a common utility, that we can both unify all of the implementations and polyfill to earlier versions of ember as well. + + +## Detailed design + +For loadaing UIs' there are concerns that this proposed utility will not be concerned with directly, but also the utility still needs to enable usage of those UIs, so in _How we teach this_, there will be examples of various loading patterns and re-implementations of existing utilities (such as those mentioned in the _Motivation_ section above) using the new tracking utility for promises. + + +The import: +```js +import { TrackedPromise, trackPromise } from '@ember/reactive'; +``` + +### `trackPromise` + +This utility wraps and instruments any promise with reactive state, `TrackedPromise`. + + +Sample type declaration +```ts +export function trackPromise(existingPromise: Promise): TrackedPromise { + /* ... */ +} +``` + +### `TrackedPromise` + +This utility is analgous to `new Promise((resolve) => /* ... */)`, but includes the tracking of the underlying promise. +Additionally `TrackedPromise` must be able to receive an existing promise via `new TrackedPromise(existingPromise);`. + +Sample type declaration +```ts +export class TrackedPromise implements PromiseLike { + constructor(existingPromise: Promise); + constructor(callback: ConstructorParameters>[0]); + constructor(promiseOrCallback: /* ... */) { /* ... */ } + + // private, tracked, all state is derived from this, + // since promises are not allowed to have more than one state. + #state: + | ['PENDING'] + | ['REJECTED', error: unknown] + | ['RESOLVED', value: Value]; + + // private, the underlying promise that we're instrumenting + #promise: Promise; + + // upon success, Value, otherwise null. + value: Value | null; + + // upon error, unknown, otherwise null. + error: unknown; + + // state helpers + isPending: boolean; + isResolved: boolean; + isRejected: boolean; + + // allows TrackedPromises to be awaited + then(/* ... */): PromiseLike +} +``` + +Unlike [TrackedAsyncData][gh-tracked-async-data], all properties are accessible at throughout the the lifecycle of the promise. This is to reduce the pit-of-frustration and eliminate extraneous errors when working with promise-data. While ember-async-data can argue that it doesn't make sense to _even try_ accessing `value` until a promise is resolved, the constraint completely prevents UI patterns where you want value and loading state to be separate, and not part of a single whollistic if/else-if set of blocks. + +For simplicity, these new utilities will not be using `@dependentKeyCompat` to support the `@computed` era of reactivity. pre-`@tracked` is before ember-source @ 3.13, which is from over 5 years ago, at the time of writing. For the broadest, most supporting libraries we have, 3.28+ is the supported range, and for speed of implementation, these tracked promise utilities can strive for the similar compatibility. + +An extra feature that none of the previously mentioned implementations have is the ability to `await` directly. This is made easy by only implementing a `then` method -- and allows a good ergonomic bridge between reactive and non-reactive usages. + + +The implementation of `TrackedPromise` is intentionally limited, as we want to encourage reliance on [_The Platform_][mdn-Promise] whenever it makes sense, and is ergonomic to do so. For example, using `race` would still be done native, and can be wrapped for reactivity: +```js +/** @type {TrackedPromise} */ +let trackedPromise + = trackPromise(Promise.race([promise1, promise2])); +``` + +[mdn-Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise + +### `@ember/reactive` + +The process of making libraries support wide-ranges of `ember-source` is known. `ember-source` has recently been adapting its release process to use [release-plan][gh-release-plan], so that the [ember.js][gh-emberjs] repo can publish multiple packages seemslessly, rather than always bundle everything under one package. + + +With those new release capabilities within the [ember.js][gh-emberjs] repo, Instead of a polyfill for older versions of ember, `@ember/reactive`, the package (at the time of this RFC, does not exist, but would have the two exported utilities from it), would be pulished as its own package _and_ included with ember-source, as to not add more dependencies to the package.json going forward. + +[gh-release-plan]: https://github.com/embroider-build/release-plan +[gh-emberjs]: https://github.com/emberjs/ember.js/ + + +> This is the bulk of the RFC. + +> Explain the design in enough detail for somebody +familiar with the framework to understand, and for somebody familiar with the +implementation to implement. This should get into specifics and corner-cases, +and include examples of how the feature is used. Any new terminology should be +defined here. + +> Please keep in mind any implications within the Ember ecosystem, such as: +> - Lint rules (ember-template-lint, eslint-plugin-ember) that should be added, modified or removed +> - Features that are replaced or made obsolete by this feature and should eventually be deprecated +> - Ember Inspector and debuggability +> - Server-side Rendering +> - Ember Engines +> - The Addon Ecosystem +> - IDE Support +> - Blueprints that should be added or modified + +## How we teach this + +> What names and terminology work best for these concepts and why? How is this +idea best presented? As a continuation of existing Ember patterns, or as a +wholly new one? + +> Would the acceptance of this proposal mean the Ember guides must be +re-organized or altered? Does it change how Ember is taught to new users +at any level? + +> How should this feature be introduced and taught to existing Ember +users? + +> Keep in mind the variety of learning materials: API docs, guides, blog posts, tutorials, etc. + +## Drawbacks + +> Why should we *not* do this? Please consider the impact on teaching Ember, +on the integration of this feature with other existing and planned features, +on the impact of the API churn on existing apps, etc. + +> There are tradeoffs to choosing any path, please attempt to identify them here. + +## Alternatives + +- reclaim the `ember` package and export undedr `ember/reactive`, add `ember` to the package.json. + +## Unresolved questions + +> Optional, but suggested for first drafts. What parts of the design are still +TBD? From 99b6a4140137fd04f17f16ca5abbf6e8cf124a87 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:01:19 -0500 Subject: [PATCH 02/16] Update meta --- text/{0000-tracked-promise.md => 1060-tracked-promise.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename text/{0000-tracked-promise.md => 1060-tracked-promise.md} (99%) diff --git a/text/0000-tracked-promise.md b/text/1060-tracked-promise.md similarity index 99% rename from text/0000-tracked-promise.md rename to text/1060-tracked-promise.md index 48d8f09379..e181058f8e 100644 --- a/text/0000-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -8,7 +8,7 @@ teams: # delete teams that aren't relevant - framework - learning prs: - accepted: # Fill this in with the URL for the Proposal RFC PR + accepted: https://github.com/emberjs/rfcs/pull/1060 project-link: suite: --- From 21a62ce124b3622116fda1768b8863126f5723e7 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:05:33 -0500 Subject: [PATCH 03/16] More details --- text/1060-tracked-promise.md | 57 ++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index e181058f8e..652da1ac34 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -132,30 +132,40 @@ let trackedPromise The process of making libraries support wide-ranges of `ember-source` is known. `ember-source` has recently been adapting its release process to use [release-plan][gh-release-plan], so that the [ember.js][gh-emberjs] repo can publish multiple packages seemslessly, rather than always bundle everything under one package. - -With those new release capabilities within the [ember.js][gh-emberjs] repo, Instead of a polyfill for older versions of ember, `@ember/reactive`, the package (at the time of this RFC, does not exist, but would have the two exported utilities from it), would be pulished as its own package _and_ included with ember-source, as to not add more dependencies to the package.json going forward. +With those new release capabilities within the [ember.js][gh-emberjs] repo, Instead of a polyfill for older versions of ember, `@ember/reactive`, the package (at the time of this RFC, does not exist, but would have the two exported utilities from it), would be pulished as its own `type=module` package _and_ included with ember-source, as to not add more dependencies to the package.json going forward. [gh-release-plan]: https://github.com/embroider-build/release-plan [gh-emberjs]: https://github.com/emberjs/ember.js/ +Why `type=module`? + +This is a requirement for some optimization features of packages (webpack / vite), such as _proper_ treeshaking -- without `type=module`, the best optimization we can get is "pay for only what you import". For large projects this isn't so much of a problem, but for small projects (or highly optimized projects), the impact to network transfer/parse/eval is measurable. This RFC is also proposing that `@ember/reactive` be _the_ place for all our ecosystem's reactivity utilities will end up once they've been proven out, tested, and desire for standardation is seen. + +For example, other future exports from `@ember/reactive` (in future RFCs), may include: +- TrackedObject +- TrackedArray +- TrackedMap +- TrackedSet +- TrackedWeakSet +- TrackedWeakMap +- localCopy +- certain [window properties](https://svelte.dev/docs/svelte/svelte-reactivity-window) +- ...and more -> This is the bulk of the RFC. +without the static analysis guarantees of `type=module`, every consumer of `@ember/reactive` would always have all of these exports in their build. +For some utilities, we can place them under sub-path-exports, such as `@ember/reactive/window`, for window-specific reactive properties, but the exact specifics of each of these can be hashed out in their individual RFCs. -> Explain the design in enough detail for somebody -familiar with the framework to understand, and for somebody familiar with the -implementation to implement. This should get into specifics and corner-cases, -and include examples of how the feature is used. Any new terminology should be -defined here. +When a project wants to use `@ember/reactive`, they would then only need to install the package separately / add it to their `package.json`. -> Please keep in mind any implications within the Ember ecosystem, such as: -> - Lint rules (ember-template-lint, eslint-plugin-ember) that should be added, modified or removed -> - Features that are replaced or made obsolete by this feature and should eventually be deprecated -> - Ember Inspector and debuggability -> - Server-side Rendering -> - Ember Engines -> - The Addon Ecosystem -> - IDE Support -> - Blueprints that should be added or modified +The proposed list of compatibilyt here is only meant as an example -- if implementation proves that more can be supported easier, with less work, that should be pursued, and this part is kind of implementation detail. + +But for demonstration: +- apps pre [version available], would add `@ember/reactive` to their `devDependencies` or `dependencies` + - importing `@ember/reactive` would be handled by ember-auto-import/embroider (as is the case with all v2 addons) +- v1 addons would not be supported +- v2 addons, for maximum compatibility, would need to add `@ember/reactive` to their `dependencies` + - in consuming apps post [version available], this would be optimized away if the version declared in dependencies satisfies the range provided by the consuming app (an optimization that packagers already do, and nothing we need to worry about) +- apps post [version available], would not need to add `@ember/reactive` to their `devDependencies` or `dependencies`, as we can rely on the `ember-addon#renamed-modules` config in ember-source's `package.json`. ## How we teach this @@ -174,17 +184,14 @@ users? ## Drawbacks -> Why should we *not* do this? Please consider the impact on teaching Ember, -on the integration of this feature with other existing and planned features, -on the impact of the API churn on existing apps, etc. - -> There are tradeoffs to choosing any path, please attempt to identify them here. +I think not doing this has more drawbacks than doing it. A common problem we have is that we have too many packages and too many ways to do things. Our users long for "the ember way" to do things, and a comprehensive reactive library full of vibrant, shared utilities is one such way to bring back some of what folks are longing for. ## Alternatives -- reclaim the `ember` package and export undedr `ember/reactive`, add `ember` to the package.json. +- reclaim the `ember` package and export under `ember/reactive`, add `ember` to the package.json. + - doing this _would_ require a polyfill, as `ember` is already available in all versions of projects, but it does not have sub-path-exports that folks use. +- use `/reactivity` instead of `/reactive` ## Unresolved questions -> Optional, but suggested for first drafts. What parts of the design are still -TBD? +none (yet) \ No newline at end of file From 7bd49dd1dcc09794d7b91fd15a4473f554f262ff Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:18:11 -0500 Subject: [PATCH 04/16] Some how we teach this --- text/1060-tracked-promise.md | 181 +++++++++++++++++++++++++++++++++-- 1 file changed, 172 insertions(+), 9 deletions(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index 652da1ac34..ba845289f1 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -155,6 +155,9 @@ For example, other future exports from `@ember/reactive` (in future RFCs), may i without the static analysis guarantees of `type=module`, every consumer of `@ember/reactive` would always have all of these exports in their build. For some utilities, we can place them under sub-path-exports, such as `@ember/reactive/window`, for window-specific reactive properties, but the exact specifics of each of these can be hashed out in their individual RFCs. + +### Consumption + When a project wants to use `@ember/reactive`, they would then only need to install the package separately / add it to their `package.json`. The proposed list of compatibilyt here is only meant as an example -- if implementation proves that more can be supported easier, with less work, that should be pursued, and this part is kind of implementation detail. @@ -169,18 +172,175 @@ But for demonstration: ## How we teach this -> What names and terminology work best for these concepts and why? How is this -idea best presented? As a continuation of existing Ember patterns, or as a -wholly new one? +### API Docs + +#### `trackPromise` + +```js +import { trackPromise } from '@ember/reactive'; +``` + +The returned value is an instance of `TrackedPromise`, and is for instrumenting promise state with reactive properties, so that UI can update as the state of a promise changes over time. + +This is a shorthand utility for passing an existing promise to `TrackedPromise`. + +Example in a template-only component +```gjs +import { trackPromise } from '@ember/reactive'; + +function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + + +``` + +Example in a class component: +```gjs +import Component from '@glimmer/component'; +import { cached } from '@glimmer/tracking'; +import { trackPromise } from '@ember/reactive'; + +export default class Demo extends Component { + @cached + get state() { + // promise resolves after 400ms + let promise = new Promise((resolve) => { + setTimeout(resolve, 400); + }); + + return trackPromise(promise); + } + + +} +``` + + +#### `TrackedPromise` -> Would the acceptance of this proposal mean the Ember guides must be -re-organized or altered? Does it change how Ember is taught to new users -at any level? +```js +import { TrackedPromise } from '@ember/reactive'; +``` -> How should this feature be introduced and taught to existing Ember -users? +C + +### Guides + +#### use with `fetch` + +With `@cached`, we can make any getter have stable state and referential integrity, which is essential for having multiple accesses to the getter return the same object -- in this case, the return value from `trackPromise`: + +```gjs +import Component from '@glimmer/component'; +import { cached } from '@glimmer/tracking'; +import { trackPromise } from '@ember/reactive'; + +export default class Demo extends Component { + @cached + get requestState() { + let id = this.args.personId; + let fetchPromise = + fetch(`https://swapi.tech/api/people/${id}`) + .then(response => response.json()); + + return trackPromise(fetchPromise); + } + + // Properties can be aliased like any other tracked data + get isLoading() { + return this.requestState.isPending; + } + + +} +``` +In this example, we only ever show one of the states at a time, loading _or_ the value _or_ the error. +We can separate each of these, if desired, like this: +```gjs + +``` +Doing so would allow more separation of loading / error UI, such as portaling loading / error notifications to somewhere central in your applications. + +#### creating reactive promises + + +```gjs +import Component from '@glimmer/component'; +import { cached } from '@glimmer/tracking'; +import { TrackedPromise } from '@ember/reactive'; + +export default class Demo extends Component { + @cached + get state() { + return new TrackedPromise((resolve => { + + })); + let id = this.args.personId; + let fetchPromise = + fetch(`https://swapi.tech/api/people/${id}`) + .then(response => response.json()); + + return trackPromise(fetchPromise); + } + + // Properties can be aliased like any other tracked data + get isLoading() { + return this.requestState.isPending; + } + + +} +``` -> Keep in mind the variety of learning materials: API docs, guides, blog posts, tutorials, etc. ## Drawbacks @@ -191,6 +351,9 @@ I think not doing this has more drawbacks than doing it. A common problem we hav - reclaim the `ember` package and export under `ember/reactive`, add `ember` to the package.json. - doing this _would_ require a polyfill, as `ember` is already available in all versions of projects, but it does not have sub-path-exports that folks use. - use `/reactivity` instead of `/reactive` +- re-use `@glimmer/tracking` + - would require that `@glimmer/tracking` move in to the `ember-source` repo + - would also require a polyfill, as prior versions of `@glimmer/tracking` would not have the new behaviors ## Unresolved questions From c35f5ba7671bf3627d93d9052eb92dab83232685 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:28:11 -0500 Subject: [PATCH 05/16] Some how we teach this --- text/1060-tracked-promise.md | 46 +++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index ba845289f1..a4c09edf06 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -237,7 +237,51 @@ export default class Demo extends Component { import { TrackedPromise } from '@ember/reactive'; ``` -C +Creates a tracked `Promise`, with `tracked` properties for implementing UI that updates based on the state of a promise. + +Creating a tracked promise from a non-async API: +```gjs +import { TrackedPromise } from '@ember/reactive'; + +function wait(ms) { + return new TrackedPromise((resolve) => setTimeout(resolve, ms)); +} + + +``` + +Creating a tracked promise from an existing promise: + +```gjs +import Component from '@glimmer/component'; +import { cached } from '@glimmer/tracking'; +import { TrackedPromise } from '@ember/reactive'; + +export default class Demo extends Component { + @cached + get state() { + let id = this.args.personId; + let fetchPromise = + fetch(`https://swapi.tech/api/people/${id}`) + .then(response => response.json()); + + return new TrackedPromise(fetchPromise); + } + + +} +``` ### Guides From 978507845531393fa3d727025ba1cd60fa6eb423 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:35:34 -0500 Subject: [PATCH 06/16] Opps, hostile design --- text/1060-tracked-promise.md | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index a4c09edf06..a6e8369061 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -346,6 +346,7 @@ Doing so would allow more separation of loading / error UI, such as portaling lo #### creating reactive promises +We can use `TrackedPromise` to turn not-async APIs into reactive + async behaviors -- for example, if we want to make a promise out of `setTimeout`, and cause an artificial delay / timer behavior: ```gjs import Component from '@glimmer/component'; @@ -354,38 +355,30 @@ import { TrackedPromise } from '@ember/reactive'; export default class Demo extends Component { @cached - get state() { + get () { return new TrackedPromise((resolve => { - + setTimeout(() => { + resolve(); + }, 5_000 /* 5 seconds */); })); - let id = this.args.personId; - let fetchPromise = - fetch(`https://swapi.tech/api/people/${id}`) - .then(response => response.json()); - - return trackPromise(fetchPromise); } - // Properties can be aliased like any other tracked data - get isLoading() { - return this.requestState.isPending; + get showSubscribeModal() { + return this.requestState.isResolved; } } ``` - ## Drawbacks I think not doing this has more drawbacks than doing it. A common problem we have is that we have too many packages and too many ways to do things. Our users long for "the ember way" to do things, and a comprehensive reactive library full of vibrant, shared utilities is one such way to bring back some of what folks are longing for. From d9494f0d956ab90edc0cd69a4d18fba78da67af5 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:00:51 -0500 Subject: [PATCH 07/16] Copy logic from https://github.com/universal-ember/reactiveweb/blob/main/reactiveweb/src/keep-latest.ts --- text/1060-tracked-promise.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index a6e8369061..709db15843 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -344,6 +344,10 @@ We can separate each of these, if desired, like this: ``` Doing so would allow more separation of loading / error UI, such as portaling loading / error notifications to somewhere central in your applications. +NOTE: using `@cached` with promises does not enable cancellation, as there is no _lifetime_ to attach to at the getter/property level of granularity.[^resources] + +[^resources]: This is where _resources_ can help (topic for later) -- otherwise you need to use a full component just for destruction (or `invokeHelper` on a class-based helper, which is very un-ergonomic). + #### creating reactive promises We can use `TrackedPromise` to turn not-async APIs into reactive + async behaviors -- for example, if we want to make a promise out of `setTimeout`, and cause an artificial delay / timer behavior: @@ -379,6 +383,12 @@ export default class Demo extends Component { } ``` +#### keeping the latest value while new content loads + +This pattern is useful for dropdown searches, tables (filtering), and other UIs which could otherwise cause large layout shifts / excessive repaints. + +We still want a loading UI on _initial data load_, but want to have a more subtle subsequent loading indicator (yet still prominently visible) + ## Drawbacks I think not doing this has more drawbacks than doing it. A common problem we have is that we have too many packages and too many ways to do things. Our users long for "the ember way" to do things, and a comprehensive reactive library full of vibrant, shared utilities is one such way to bring back some of what folks are longing for. From e5a25f2414008a112f7679382f737209927a0374 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:03:27 -0500 Subject: [PATCH 08/16] Copy logic from https://github.com/universal-ember/reactiveweb/blob/main/reactiveweb/src/keep-latest.ts --- text/1060-tracked-promise.md | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index 709db15843..f264530e79 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -389,6 +389,79 @@ This pattern is useful for dropdown searches, tables (filtering), and other UIs We still want a loading UI on _initial data load_, but want to have a more subtle subsequent loading indicator (yet still prominently visible) +```gjs +import Component from '@glimmer/component'; +import { tracked, cached } from '@glimmer/tracking'; +import { trackPromise } from '@ember/reactive'; +import { isEmpty } from '@ember/utils'; + +export default class Demo extends Component { + @tracked id = 51; + updateId = (event) => this.id = event.target.value; + + @cached + get request() { + let promise = fetch(`https://swapi.tech/api/peopile/${this.id}`) + .then(response => respones.json()); + + return trackPromise(promise); + } + + #previous; + #initial = true; + + @cached + get latest() { + let { value, isPending } = this.request; + + if (isPending) { + if (this.#previous === undefined && this.#initial) { + this.#initial = false; + + return value; + } + + return (this.#previous = isEmpty(value) ? this.#previous : value); + } + + return this.#previous = value; + } + + +} +``` + ## Drawbacks I think not doing this has more drawbacks than doing it. A common problem we have is that we have too many packages and too many ways to do things. Our users long for "the ember way" to do things, and a comprehensive reactive library full of vibrant, shared utilities is one such way to bring back some of what folks are longing for. From 3ebadf88bc9113014d1a520f9951d337a77ff62d Mon Sep 17 00:00:00 2001 From: johanrd Date: Fri, 20 Dec 2024 20:57:41 +0100 Subject: [PATCH 09/16] typo in 1060-tracked-promise.md --- text/1060-tracked-promise.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index f264530e79..b9e32f9f86 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -54,7 +54,7 @@ There are probably more, but the point of this RFC is to provide a common utilit ## Detailed design -For loadaing UIs' there are concerns that this proposed utility will not be concerned with directly, but also the utility still needs to enable usage of those UIs, so in _How we teach this_, there will be examples of various loading patterns and re-implementations of existing utilities (such as those mentioned in the _Motivation_ section above) using the new tracking utility for promises. +For loading UIs, there are concerns that this proposed utility will not be concerned with directly, but also the utility still needs to enable usage of those UIs, so in _How we teach this_, there will be examples of various loading patterns and re-implementations of existing utilities (such as those mentioned in the _Motivation_ section above) using the new tracking utility for promises. The import: @@ -477,4 +477,4 @@ I think not doing this has more drawbacks than doing it. A common problem we hav ## Unresolved questions -none (yet) \ No newline at end of file +none (yet) From b1f534bdd45d81c30bfce88413285d63d13ca553 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:51:20 -0500 Subject: [PATCH 10/16] Specify that we want to handle plain values as well --- text/1060-tracked-promise.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index b9e32f9f86..f85b27140a 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -69,7 +69,9 @@ This utility wraps and instruments any promise with reactive state, `TrackedProm Sample type declaration ```ts -export function trackPromise(existingPromise: Promise): TrackedPromise { +export function trackPromise( + existingPromise: Promise | Value +): TrackedPromise { /* ... */ } ``` @@ -182,8 +184,11 @@ import { trackPromise } from '@ember/reactive'; The returned value is an instance of `TrackedPromise`, and is for instrumenting promise state with reactive properties, so that UI can update as the state of a promise changes over time. +When a non-promise is passed, as one may do for a default value, it'll behave as if it were a resolved promise, i.e.: `Promise.resolve(passedValue)`. + This is a shorthand utility for passing an existing promise to `TrackedPromise`. + Example in a template-only component ```gjs import { trackPromise } from '@ember/reactive'; @@ -239,6 +244,8 @@ import { TrackedPromise } from '@ember/reactive'; Creates a tracked `Promise`, with `tracked` properties for implementing UI that updates based on the state of a promise. +When a non-promise is passed, as one may do for a default value, it'll behave as if it were a resolved promise, i.e.: `Promise.resolve(passedValue)`. + Creating a tracked promise from a non-async API: ```gjs import { TrackedPromise } from '@ember/reactive'; From 74bfea8a7ca544d3de5d86a863cab19de1dc10ce Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:20:42 -0500 Subject: [PATCH 11/16] Add key behaviors, noting that we don't want to cause extra renders --- text/1060-tracked-promise.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index f85b27140a..bf858e71fb 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -62,6 +62,14 @@ The import: import { TrackedPromise, trackPromise } from '@ember/reactive'; ``` +> [!NOTE] +> Key behaviors: +> - if the passed promise is resolved, or a non-promise, we do not await, this allows +> values to not block render (or cause a re-render if they don't need to) +> - no `@dependentKeyCompat` +> - promise states are all mutually exclusive +> - the returned value is also promise-like + ### `trackPromise` This utility wraps and instruments any promise with reactive state, `TrackedPromise`. From eec338724c5c4ed7cf64e3a00870ba4161256d0b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:24:06 -0500 Subject: [PATCH 12/16] not relevant for this section of the RFC --- text/1060-tracked-promise.md | 1 - 1 file changed, 1 deletion(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index bf858e71fb..665d34bfa8 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -68,7 +68,6 @@ import { TrackedPromise, trackPromise } from '@ember/reactive'; > values to not block render (or cause a re-render if they don't need to) > - no `@dependentKeyCompat` > - promise states are all mutually exclusive -> - the returned value is also promise-like ### `trackPromise` From 1463765a19b323d9cff83e8bc90bd175abfbb93a Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:01:33 -0500 Subject: [PATCH 13/16] Add some subtle examples about timing and hinting at a promise cache (tho that is an implementation detail) --- text/1060-tracked-promise.md | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index 665d34bfa8..d472054d0d 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -137,6 +137,50 @@ let trackedPromise [mdn-Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise + +#### Subtle Notes + +If a promise is passed to `trackPromise` or `TrackedPromise` multiple times, we don't want to _re-do_ any computations. + +Examples: + +```gjs +let a = Promise.resolve(2); // "fulfilled" + + +``` + +This component renders only once, and _both_ occurances of of `trackPromise` immediately resolve and _never_ enter the pending states. + + + +```gjs +let a = Promise.resolve(2); // "fulfilled" +let b = Promise.resolve(2); // "fulfilled" + + +``` + +In this component, it _also_ only renders once as both promises are resolved, and we can adapt the initial state returned by `trackPromise` to reflect that. + + + ### `@ember/reactive` The process of making libraries support wide-ranges of `ember-source` is known. `ember-source` has recently been adapting its release process to use [release-plan][gh-release-plan], so that the [ember.js][gh-emberjs] repo can publish multiple packages seemslessly, rather than always bundle everything under one package. From fb07d8c442d63959d40477dccca35c3e3fd401b9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:03:09 -0500 Subject: [PATCH 14/16] Another alternative --- text/1060-tracked-promise.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index d472054d0d..f3dc4e1bc9 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -532,6 +532,7 @@ I think not doing this has more drawbacks than doing it. A common problem we hav - re-use `@glimmer/tracking` - would require that `@glimmer/tracking` move in to the `ember-source` repo - would also require a polyfill, as prior versions of `@glimmer/tracking` would not have the new behaviors + - whole-sale pull in parts of `@warp-drive/ember` (though, much of this RFC is already heavily influenced by their `getPromiseState`, which is backed by a promise cache -- all of which is sort of an implementation detail as far this RFC is concerned) ## Unresolved questions From afc13feeb990a2684d74d5d4f257bcb55c3dfe0c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:03:16 -0500 Subject: [PATCH 15/16] Another alternative --- text/1060-tracked-promise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index f3dc4e1bc9..d5a8ddb4a9 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -532,7 +532,7 @@ I think not doing this has more drawbacks than doing it. A common problem we hav - re-use `@glimmer/tracking` - would require that `@glimmer/tracking` move in to the `ember-source` repo - would also require a polyfill, as prior versions of `@glimmer/tracking` would not have the new behaviors - - whole-sale pull in parts of `@warp-drive/ember` (though, much of this RFC is already heavily influenced by their `getPromiseState`, which is backed by a promise cache -- all of which is sort of an implementation detail as far this RFC is concerned) +- whole-sale pull in parts of `@warp-drive/ember` (though, much of this RFC is already heavily influenced by their `getPromiseState`, which is backed by a promise cache -- all of which is sort of an implementation detail as far this RFC is concerned) ## Unresolved questions From b4378159ce5ac7761683e2929f4f8a5ff7711a95 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:46:11 -0500 Subject: [PATCH 16/16] Rename trackProomise to trackedPromise (noun, not action) --- text/1060-tracked-promise.md | 82 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/text/1060-tracked-promise.md b/text/1060-tracked-promise.md index d5a8ddb4a9..dcdc30c2f9 100644 --- a/text/1060-tracked-promise.md +++ b/text/1060-tracked-promise.md @@ -59,7 +59,7 @@ For loading UIs, there are concerns that this proposed utility will not be conce The import: ```js -import { TrackedPromise, trackPromise } from '@ember/reactive'; +import { TrackedAsyncState, trackedPromise } from '@ember/reactive'; ``` > [!NOTE] @@ -69,28 +69,28 @@ import { TrackedPromise, trackPromise } from '@ember/reactive'; > - no `@dependentKeyCompat` > - promise states are all mutually exclusive -### `trackPromise` +### `trackedPromise` -This utility wraps and instruments any promise with reactive state, `TrackedPromise`. +This utility wraps and instruments any promise with reactive state, `TrackedAsyncState`. Sample type declaration ```ts -export function trackPromise( +export function trackedPromise( existingPromise: Promise | Value -): TrackedPromise { +): TrackedAsyncState { /* ... */ } ``` -### `TrackedPromise` +### `TrackedAsyncState` This utility is analgous to `new Promise((resolve) => /* ... */)`, but includes the tracking of the underlying promise. -Additionally `TrackedPromise` must be able to receive an existing promise via `new TrackedPromise(existingPromise);`. +Additionally `TrackedAsyncState` must be able to receive an existing promise via `new TrackedAsyncState(existingPromise);`. Sample type declaration ```ts -export class TrackedPromise implements PromiseLike { +export class TrackedAsyncState implements PromiseLike { constructor(existingPromise: Promise); constructor(callback: ConstructorParameters>[0]); constructor(promiseOrCallback: /* ... */) { /* ... */ } @@ -116,7 +116,7 @@ export class TrackedPromise implements PromiseLike { isResolved: boolean; isRejected: boolean; - // allows TrackedPromises to be awaited + // allows TrackedAsyncStates to be awaited then(/* ... */): PromiseLike } ``` @@ -128,11 +128,11 @@ For simplicity, these new utilities will not be using `@dependentKeyCompat` to s An extra feature that none of the previously mentioned implementations have is the ability to `await` directly. This is made easy by only implementing a `then` method -- and allows a good ergonomic bridge between reactive and non-reactive usages. -The implementation of `TrackedPromise` is intentionally limited, as we want to encourage reliance on [_The Platform_][mdn-Promise] whenever it makes sense, and is ergonomic to do so. For example, using `race` would still be done native, and can be wrapped for reactivity: +The implementation of `TrackedAsyncState` is intentionally limited, as we want to encourage reliance on [_The Platform_][mdn-Promise] whenever it makes sense, and is ergonomic to do so. For example, using `race` would still be done native, and can be wrapped for reactivity: ```js -/** @type {TrackedPromise} */ +/** @type {TrackedAsyncState} */ let trackedPromise - = trackPromise(Promise.race([promise1, promise2])); + = trackedPromise(Promise.race([promise1, promise2])); ``` [mdn-Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise @@ -140,7 +140,7 @@ let trackedPromise #### Subtle Notes -If a promise is passed to `trackPromise` or `TrackedPromise` multiple times, we don't want to _re-do_ any computations. +If a promise is passed to `trackedPromise` or `TrackedAsyncState` multiple times, we don't want to _re-do_ any computations. Examples: @@ -148,17 +148,17 @@ Examples: let a = Promise.resolve(2); // "fulfilled" ``` -This component renders only once, and _both_ occurances of of `trackPromise` immediately resolve and _never_ enter the pending states. +This component renders only once, and _both_ occurances of of `trackedPromise` immediately resolve and _never_ enter the pending states. @@ -167,17 +167,17 @@ let a = Promise.resolve(2); // "fulfilled" let b = Promise.resolve(2); // "fulfilled" ``` -In this component, it _also_ only renders once as both promises are resolved, and we can adapt the initial state returned by `trackPromise` to reflect that. +In this component, it _also_ only renders once as both promises are resolved, and we can adapt the initial state returned by `trackedPromise` to reflect that. @@ -227,29 +227,29 @@ But for demonstration: ### API Docs -#### `trackPromise` +#### `trackedPromise` ```js -import { trackPromise } from '@ember/reactive'; +import { trackedPromise } from '@ember/reactive'; ``` -The returned value is an instance of `TrackedPromise`, and is for instrumenting promise state with reactive properties, so that UI can update as the state of a promise changes over time. +The returned value is an instance of `TrackedAsyncState`, and is for instrumenting promise state with reactive properties, so that UI can update as the state of a promise changes over time. When a non-promise is passed, as one may do for a default value, it'll behave as if it were a resolved promise, i.e.: `Promise.resolve(passedValue)`. -This is a shorthand utility for passing an existing promise to `TrackedPromise`. +This is a shorthand utility for passing an existing promise to `TrackedAsyncState`. Example in a template-only component ```gjs -import { trackPromise } from '@ember/reactive'; +import { trackedPromise } from '@ember/reactive'; function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }