Conversation
|
Would it be worthwhile to think through what a testing utility might look like for Resources? It might be helpful to have a way to test them outside of creating a dummy component that uses it and then interacting with the faux component. I’m imagining something sort of similar to what import { use } from ‘@ember/test-helpers’;
test(‘reacting to a state change’, function(assert) {
this.foo = ‘some state’;
const resource = use(Resource.create(this.foo));
assert.equal(resource.state, ‘whatever’);
});I’m imagining that the result of One question that comes to mind is whether there needs to be a way to mark the testing properties on the test context as being tracked, so that updating them in the test context behaves in such a way that it can be updated and the Resource reacts to the change, as it does when creating a Resource with component state/args as arguments. |
|
I think the major issue with naming, which was partially touched on in the RFC, is that a "resource" typically refers to nouns, à la REST, yet a primary use case here is to support declarative side-effects, like the That being said, one alternative to get around the above confusion could be to follow suit with React and separate the side-effect API from the state API? Instead of exposing one base class, expose two separate interfaces called something like |
Based on my research for this RFC, I actually believe that REST is the exception, not the rule. Most other software uses of the term "resource" actually refer to something that has a managed lifecycle, as in the Resource-Acquisition-Is-Initialization pattern. This is discussed briefly in the RFC in the Name Choice section. I do agree though that within the context of the web, "resource" has another distinct and not-uncommon meaning. We did consider various combinations of the words |
Definitely agree that EDIT: Also I guess what I meant by "resource in the context of other software" was more along the lines of it usually mapping to the concept of a "material entity", e.g. a section of memory, a lock/mutex, etc. So the observation I was attempting to make is that a side-effect doesn't seem to map to the same concept of a material entity in my mind. |
|
Could you discuss the motivation for having |
text/0567-use-and-resources.md
Outdated
| ``` | ||
|
|
||
| This helper will create and invoke the resource dynamically, with the same | ||
| semantics as `@use`. Since helpers are always consumed, resources used with |
There was a problem hiding this comment.
How would the helper args map to the arguments passed to start()/ update()? I.e. will only positional params work, or also named?
There was a problem hiding this comment.
Correct, we should state this explicitly, but the goal is to have as close to a 1-1 API as possible, so named arguments wouldn't make sense.
| resources to be setup based solely on the template. | ||
|
|
||
| ```hbs | ||
| {{use SyncDocumentTitle @title}} |
There was a problem hiding this comment.
I guess to be able to work with state in the template, basically every use of the helper would have to look more like this (unless you are only interested in the side effect, as in this example, and not in the returned state):
I think that's fine, to keep the APIs orthogonal, but it tends to get a bit verbose though...
There was a problem hiding this comment.
So that example would actually be more like:
Since the use will return the state property of the resource directly, like the decorated property. Simple cases would probably work pretty well, but more complex cases would definitely get verbose. I think the "let for the rest of the template" idea could help here, potentially.
| fundamentally related to the existing state flow patterns that were established. | ||
| They should be introduced in the `Components` section of the guides, after | ||
| modifiers. The docs should include useful examples for how to accomplish basic | ||
| tasks, such as writing a `RemoteData` resource. |
There was a problem hiding this comment.
As this RFC not only proposes the low-level manager API, but also a ready to use Resource class, what does this imply for Ember CLI?
- will
ember g resourceandember g resource-testbe introduced? - will there be an "official" new app folder
/resourceswhere resource classes live by default?
There was a problem hiding this comment.
That's a great question. I think generating the resource and resource test would be good, though I'm not sure where they should go.
I don't think we want to add a new top level /resources folder - my aim was to not add resources as a top-level resolvable, and to encourage defining them inline or nearby to the place they are used, more like utilities. There will definitely be some common resources that are used in many places, but there will also likely be some resources that are one-offs, only used in one place in the app.
Maybe they could go under /app/utils/resources? This really starts to get into how generators will work in general in a more import-driven world.
There was a problem hiding this comment.
Disagree with putting this in a Components section of the guides. This seems complex enough and useful enough to deserve its own section in the guides. IMO this is a top level construct similar to a React Context.
There was a problem hiding this comment.
That's a great question. I think generating the resource and resource test would be good, though I'm not sure where they should go.
I don't think we want to add a new top level
/resourcesfolder - my aim was to not add resources as a top-level resolvable, and to encourage defining them inline or nearby to the place they are used, more like utilities. There will definitely be some common resources that are used in many places, but there will also likely be some resources that are one-offs, only used in one place in the app.Maybe they could go under
/app/utils/resources? This really starts to get into how generators will work in general in a more import-driven world.
I agree resources should not be resolvable by the resolver. They should be imported as described here. However, I think for organizational purposes it should be recommended that a top level resources folder be created and used for resources.
IMO this is a top level construct similar to a React Context.
|
@jgwhite absolutely, there are a few reasons:
Edit: Found another reason - Because we're tearing down and creating new instances of the Resource by default, it prevents users from accidentally passing around stale versions of the resource. I think this is pretty important in the end, since ending up with a stale version could definitely cause issues. |
|
@pzuraq thanks for the fantastic explanation. That all makes a lot of sense. Do you think there’s any risk that @use products = RemoteData.for(this.args.searchUrl)
@use products = RemoteData.from(this.args.searchUrl)
@use products = RemoteData.compute(this.args.searchUrl)On a related note, why not |
|
I agree with @jgwhite – I feel that @use products = RemoteData.for(this.args.searchUrl) |
|
ember-concurrency does many things that resources do not do; but I think can complement resources if and when task cancellation is required. We should add a sentence like: "Rather than replacing ember-concurrency, resources can be used to trigger ember-concurrency tasks while ember-concurrency continues to handle async needs when the ability to cancel a task is required or desired." I'd love to also see a full example of using ember-concurrency in a resource. |
|
@Gaurav0 the language discussing replacing/rewriting libraries like
If the concern is that tasks are also used for action-driven, rather than consumption-driven, purposes, then I do think the Resource API as proposed is flexible enough to handle this. You could imagine a future task API where modifiers are used to signal if a task should run automatically, or if it should wait to be triggered by an action or external event: class MyComponent extends Component {
@use loadDataTask = task(function*() {
// ...
}).auto();
@use saveDataTask = task(function*(arg1, arg2) {
// ...
});
@action
saveData() {
this.saveDataTask.perform()
}
}If there are other specific concerns that you have that make you think Resources would not be a good fit for a primitive to rebuild |
|
@jgwhite @tchak agreed that the naming conflict is not ideal. We did consider other names, specifically @use counter = Counter.for();
@use counter = Counter.from();
@use counter = Counter.with();
@use counter = Counter.compute();I think
This actually has two purposes, one being the type work around, but the other is a bit more nuanced and should probably be spelled out more explicitly in the RFC. The initializer is technically a thunk that we are using to accomplish two tasks:
When we want to update a Resource, we need to be able to rerun this thunk in order to get the new values of those arguments, but without creating a new instance of the Resource. Using static methods, we could do this like so: class Resource {
static create(...args) {
let { isCreating } = getUseInfo();
setUseArgs(...args);
if (!isCreating) {
return new this();
}
}
}This way, the first time we run the thunk, it will create the resource and save off the arg values. The second time, it will just save off the arg values. The dual nature of this thunk is definitely not clear cut, and we've received feedback already from a few folks that it's a bit counterintuitive that a class RemoteData {
@tracked data = null;
@tracked isLoading = false;
get state() {
return {
isLoading: this.isLoading,
data: this.data,
};
}
async start(url) {
this.isLoading = true;
let response = await fetch(url);
let data = await response.json();
if (!this.isDestroyed) {
this.isLoading = false;
this.data = data;
}
}
}
export const remoteData = resource(RemoteData);// usage
@use data = remoteData(this.args.url);This API is type-able, and doesn't introduce the same issues with the dual purpose of It also would potentially solve an impending problem with class-based helpers/modifiers and template imports, if it were mirrored over to them. Currently, both class-based and functional helpers are normalized to snake-case when used, but with template imports that'll change to use the direct identifier. This means that we'll be invoking class-based helpers/modifiers with capital case: If we followed this pattern of wrapping classes, it'd allow us to normalize all the identifiers to camelCase: I think that this should be decided in a separate RFC, ultimately, but it's worth considering how our choices for this API could affect the future there. |
|
@pzuraq the
|
|
Will |
|
@tchak I think it’ll work on all framework-managed things. Per the RFC:
|
|
I love the direction here; this is definitely something missing in the Octane programming model.
|
|
@josemarluedke yes, Resources will have access to the Personally I think this is probably fine, and the |
|
|
||
| // usage | ||
| class MyComponent extends Component { | ||
| @use counter = counter(this.args.interval); |
There was a problem hiding this comment.
I'm finding this example a bit confusing because of the use of counter a few different ways. In the example above, you use different words on either side of the equals which I find easier to follow (products = remoteData). So, in a similar way, what if this was something like:
| @use counter = counter(this.args.interval); | |
| @use counter = counterResource(this.args.interval); |
This would also mean changing the variable on 243 to const counterResource = resource(Counter)
|
|
||
| ```js | ||
| export default class SearchResults extends Component { | ||
| @use products = remoteData(this.args.searchUrl); |
There was a problem hiding this comment.
Going along with my comment below, I think could be even more clear by renaming it to:
| @use products = remoteData(this.args.searchUrl); | |
| @use products = remoteDataResource(this.args.searchUrl); |
49fef9c to
a89b7a1
Compare
| type ResourceDefinition = { resource: ManagedResource }; | ||
| type ResourceDefinitionThunk = () => ResourceDefinition; | ||
|
|
||
| interface ResourceInstance { |
There was a problem hiding this comment.
ZOMG LETS NOT INCLUDE INSTANCE IN AN INTERFACE / CLASS NAME AGAIN PLZ?!?!?!!?!
😸 ❤️
There was a problem hiding this comment.
We should come up with a name not as apt to troll you for sure. 😆
I’d suggest we invert this: it should be interface Resource and if we need to name the type of the resource constructor, differentiate that as interface ResourceConstructor. We may need to make that distinction here because of how TS conceptualizes its types, i.e. that you do have to distinguish between the type and it’s constructor. However, as this is designed currently, I don’t think we need that: we can just specify that they type for the argument to resource is a Resource, full stop?
(I’ll have more comments on the types proposed here later this week. @pzuraq and @dfreeman and I have played with a bunch of variants in the design space for the types, some of them better than others; this is part of how we identified the constraints around static methods described in the RFC. However, the types as they are in the proposal here represent a fairly rough draft: I just threw together the quickest thing that could possible work when we started discussing this version of the proposal, to see if it was viable at all. As such, we’d like to polish them a fair bit, and now rather than post-RFC, so that what the Typed Ember crowd ends up doing on DefinitelyTyped can actually match the design here. Otherwise we’re asking for confusion in the community, and nobody wants that! 😂)
| constantly in a repetitive manner. The core of the API consists of: | ||
|
|
||
| - The `@use` decorator (and the complementary `{{use}}` helper) | ||
| - The resource base class (implemented via a `ResourceManager`, similar to |
There was a problem hiding this comment.
Am I right in thinking the base class is no longer part of this proposal?
|
It seems like the proposed timing for For example, one might be tempted to refactor the class MyComponent extends Component {
- constructor() {
- useResource(this, () => interval(this.sayHello, this.args.interval));
- }
+ @use interval = interval(this.sayHello, this.args.interval);
sayHello() {
console.log('Hello!');
}
}At a glance these two declarations look equivalent, but based on the detailed design of |
|
@jgwhite that is a consideration that we've been mulling over, and it is true this could be a bit confusing. There were two reasons driving the change:
One alternative design would be to decorate getter functions instead: @use
get interval() {
return interval(this.args.handler, this.args.interval);
}While this would imply laziness, it introduces another problem in that we really don't want or expect this getter function to be dynamic. For instance, we would want to avoid something like this: @use
get interval() {
if (this.args.interval) {
return interval(this.sayHello, this.args.interval);
} else if (this.args.timeout) {
return timeout(this.sayHello, this.args.timeout);
}
}While the same kind of dynamism is definitely possible in a class field, it is much harder to do, and I think this will discourage it effectively. Personally, I would prefer accepting laziness from the decorator if it means we get to keep the class field syntax. |
|
@pzuraq i'm not sure how: is any less likely than and I'm not sure why avoiding a |
|
@jrjohnson the later is perfectly valid though, because the |
Perhaps I’m misunderstanding this point. Wouldn’t a resource begin emitting |
|
@jgwhite the point is that it's not possible to initialize them in the exact same order. Consider: class Foo {
bar1 = console.log(1);
bar2 = console.log(2);
bar3 = console.log(3);
}
new Foo(); // 1, 2, 3Now, let's say we make class Foo {
bar1 = console.log(1);
@use bar2 = logResource(2);
bar3 = console.log(3);
constructor() {
// bar1 and bar3 have been initialized by this point
scheduleUseStart(this);
}
}
new Foo(); // 1, 3, 2No matter what we do here, the difference in timing will be observable. We can also observably change these timing semantics by accessing the field, so for instance: class Foo {
bar1 = console.log(1);
@use bar2 = logResource(2);
bar2Starter = this.bar2;
bar3 = console.log(3);
constructor() {
scheduleUseStart(this);
}
}
new Foo(); // 1, 2, 3Which seems odd. Always being consumption based makes this much simpler overall, it means you need to learn that |
|
@pzuraq ah, that’s a wonderful illustration, thank you. So are we saying that users would have to learn that the |
|
@jgwhite correct, I think what I'm saying is no matter what, if we want to replace the field with a getter, it will change some things. We can try to minimize that gap and maybe if it's small enough, we won't need to teach it, but my intuition is that it'll at least need to be in an "advanced" section of some kind for edge cases like these. One alternative would be for And no worries, this is the perfect time to hash these things out 😄 this API is admittedly trying to solve a lot of concerns within a very narrow design space. |
|
@pzuraq given that |
|
As a little data point in favor of this design, I was looking at an Ember component in our app today, which is 57 lines of JavaScript (excluding comments and whitespace) and no template and thinking about how I'd refactor it into a Glimmer component, and especially thinking about whether it could be a template-only component. The existence of template-space And those 57 lines of JavaScript would be reduced to a whopping 9 lines of template code (formatted generously! It's 6 formatted the way Prettier would format it) and 28 lines of JS (including the imports). Better than the reduction in lines of code, though, this is a dramatic improvement on the separation of concerns and therefore the maintainability and testability of the code. Each piece of it is much more self-contained and only knows what it has to. |
|
I don't know if this is dollar short day late but the implementation of this is pretty much the actor pattern; considering that |
|
@sukima that link doesn't currently seem to go anywhere (just back to the top of this PR), so this may be a case of similar terminology being used in different ways, but Resources as characterized here don't quite align with my understanding of the actor model. My mental model of actors as they're implemented in libraries like Akka and as VM primitives for languages like Erlang/Elixir is as fundamental units of concurrency, mediated by message passing. That of course brings with it some similar concerns to the ones addressed here, like lifecycle entanglement (if Actor X spawns Actor Y, what happens to Y when X terminates and vice versa?) but both the overall problem space and the API form factor presented in this RFC seem distinct from the actor model as I understand it. |
There was a problem hiding this comment.
What if you want to make a keyboard shortcut, where you don't care about the return value?
Example : adopted-ember-addons/ember-keyboard#135 (comment)
@use _copy = keyboardShortcut('Ctrl+C'', this.handleCopy);
// _copy would be unused|
Great proposal! Very much in favor! 🏅 As another data point, I've looked through our code base for |
|
This proposal has evolved a lot since it was first made, and we have released a number of smaller proposals which made it possible to release something like it in user space, which I just did 😄 see this blog post for details: https://www.pzuraq.com/introducing-use/ I'm still planning on continuing this RFC in the future, but it will likely change a bit in the meantime and I don't want to leave this up. I'm planning on submitting a new version once it's clearer what the final API will look like. |
Rendered