Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@cacheFor decorator to read last-cached value on a @cached decorator - RFC #656

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions text/0656-read-cached-last-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
- Start Date: 2020-08-18
- Relevant Team(s): Ember.js
- RFC PR: https://github.com/emberjs/rfcs/pull/656
- Tracking:

# Read Previous @cached value with new @cacheFor Decorator

## Summary

Introduce the `@cacheFor(propName)` decorator to provide
a conventional way for RFC 566's `@cached` getters to retrieve
the last-returned value from within the next execution of the getter,
enabling computation load reduction for complex results that are
incremental nature. Consider this example of an online auction where
participants arrive slowly and each can see the bids sourced from the
others (showing the proposed `@cacheFor` feature):

```js
import { tracked } from '@glimmer/tracking';
import { cached, cacheFor } from '...';

class OnlineItemAuction {
/**
Bidders who have joined this particular item auction - this
array is set from elsewhere and grows item by item over time.
*/
@tracked bidders = [];

/**
This (read only) property returns the most recently-returned
result of the bidSources getter below, or an initial value
if the getter has not been called yet. This property is not
useful to be bound elsewhere - it is 'volatile' in that it is
calculated on demand when called by simply reading the last
value in the cache for the desired property (or the initial
value provided).
*/
@cacheFor('bidSources') _bidSources = [];

/**
Each bidder becomes a source of bids for this auction
with history that is displayed locally for the duration
of the auction and which initially comes from the server
which is expensive, and so this array result should
not fully regenerate, but incrementally regenerate instead
as new bidders arrive.
*/
@cached
get bidSources() {
// Read the previously returned set of bid sources
// so that only new ones need be generated for newly
// arrived bidders
const _prevs = this._bidSources;
const bidders = this.bidders;

// Generate a BidSource from each bidder, but avoid doing
// it more than once per bidder.
return bidders.map(bidder => {
return _prevs.find(prev => prev.bidder === bidder) ||
// expensive as it downloads initial state from server
new BidSource(bidder);
});
}
}
```

## Motivation

This is not an uncommon use case: in particular, incremental arrays
of complex-to-create instances are very common, especially where
the feeder array (bidders) increments over time like above.

Despite this appearing at first glance to be a more advanced
use case, I have found empirically over the past 7 years
that this is one of a (shrinking) list of cases that arises quite
often and creates frustration among solid-JS but new-to-Ember
developers.
In this case, their experience causes recognition of the need to
memoize, but there is no solution by convention. This seems
counterintuitive (to me as well) as frameworks like Ember are all
about optimizing computation and rendering of UI-bound data.

Perhaps more problematically, junior developers come up with some
pretty varied and hard-to-debug versions of this pattern and
that can really slow adoption and confidence (not to mention
code reviews and QA).

All levels would be helped tremendously by an out-of-the-box
conventional solution, and furthermore, this sits very well on top
of the @cached decorator concept (in fact, the implementation if
@cached is in place is a few extra lines of code if done as above).

## Detailed design

A getter that is marked for memoization using `@cached` can access
its last-returned value by reading a property on its own instance
identified with the proposed `@cacheFor(propName)` decorator.

The `@cacheFor(propName)` decorator is itself a volatile getter
that simply returns the last value be reading the cache. This is
done on demand and no state is stored in the getter itself. If the
cache is not found to exist yet for the related cached property,
then the initial value (if provided) is returned instead.

> To Do: Put in sample implementation code later today

### Resource Leaks

This is a very clean and lightweight solution - the cache exists
already (or not, if never created), but in either case, this
decorator does not create additional resource load or references.

## How we teach this

An extension of the `@cached` documentation is the natural place to
put this, using a strong and broadly-relatable anecdote
(like bidding in the example).
Building up an example of such a use-case from scratch would
certainly make the need for this feature very evident.

Assuming a knowledge of `@cached` (or any experience with it), it
doesn't take long to end up looking for the proposed functionality as
`@cached` itself is designed to limit re-computation. Combine that
with an array-type property and you have a very likely demand
for the pattern addressed by `@cacheFor(propName)`.

Existing users are apt to pursue memoization topics if they have had
anything more than a trivial exposure to Ember.
Across the 20+ production-grade apps we've built on Ember, I can't
think of a single one where this wasn't important for performance
in at least some circumstance of the application.
Unfortunately, implementation of solutions for this issue can be
highly varied as Ember (currently) leaves incremental state as
a matter for the end-developer to address.

No additional reorganization is needed for the guides.

## Drawbacks

Use of this as a crutch for an absence of better coding practices
may arise, but this approach is not really beneficial to other cases.
As proposed, it only applies if `@cached` is in use, limiting the
domain of potential misuse.

## Alternatives

- Let the developer continue to manage this on a case-by-case
basis
- Provide a base class that implements this 'automagically',
however that's exactly the opposite direction Ember and those
like it are moving in.

## Unresolved questions

- Is this the best ergonomic implementation?
- Is there an issue with the possibility that one getter can access
another getter's prior value (since the prior value can
be read anywhere - is this actually a beneficial
side-effect?)