Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Experimental Resource API #58255

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

Closed
wants to merge 3 commits into from
Closed

Conversation

alxhub
Copy link
Member

@alxhub alxhub commented Oct 18, 2024

Implement a new experimental API, called resource(). Resources are asynchronous dependencies that are managed and delivered through the signal graph. Resources are defined by their reactive request function and their asynchronous loader, which retrieves the value of the resource for a given request value. For example, a "current user" resource may retrieve data for the current user, where the request function derives the API call to make from a signal of the current user id.

Resources are represented by the Resource<T> type, which includes signals for the resource's current value as well as its state. WritableResource<T> extends that type to allow for local mutations of the resource through its value signal (which is therefore two-way bindable).

Also, implementations of an rxjs-interop APIs which produce Resources from RxJS Observables. rxResource() is a flavor of resource() which uses a projection to an Observable as its loader (like switchMap).

Optimizes `PendingTasks` slightly to avoid notifying the scheduler if the
disposal function of a pending task is called twice.
@angular-robot angular-robot bot added detected: feature PR contains a feature commit area: core Issues related to the framework runtime labels Oct 18, 2024
@ngbot ngbot bot added this to the Backlog milestone Oct 18, 2024
@michael-small
Copy link
Contributor

michael-small commented Oct 18, 2024

Am I wrong for my layman's take being "oh cool, so basically this would be like an official derivedAsync and beyond"? That's rather reductive as there is a lot more going on with this, but that was my kneejerk take. I feel like this is something a lot of people have been wanting, and even more. I'm all for it.

To walk through my understanding of it, here is what I think the syntax would be in a component from how I am reading your explanation and the tests.

@Component({
  selector: 'app-current-user',
  template: `
      <p>value: {{ userResource.value() }}</p>  
      <p>status: {{  userResource.status() }}</p>  
      <p>error: {{ userResource.error() }}</p>
  `,
})
export class CurrentUserComponent {
    id = input.required<string>();

    //  `inject` some service with a method `fetch` that returns a promise.
    //   Or in the `loader`, perhaps just directly pass in a `fetch`
    backend = //..

    // Reacts to `id` and its initial value
    userResource = resource({
      request: () => ({id: id()}),
      loader: (params) => backend.fetch(params.request),
    });

    doSomething() {
        // `doSomething` could be a method or `computed` or `effect` etc,
        // that can get the `useResource.value()` among other things, 
        // like the user resource's `status()` and `error()` as well
    }
}

And in a different flavor of this

  • There is a writeable version
  • There is some special RxJS interop
  • Can manually refresh

Is that all sound?

loader: ({request, abortSignal}) => {
const cancelled = new Subject<void>();
abortSignal.addEventListener('abort', () => cancelled.next());
return firstValueFrom(opts.loader(request).pipe(takeUntil(cancelled)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be more clearly documented that this only handles the first emission of an observable stream?
This will work well for handling most HTTPClient calls, which is probably the most significant target for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be more clearly documented that this only handles the first emission of an observable stream?

This was the biggest takeaway of the source code for me. Same with your second point. I suppose the refresh would cover those other HttpClient cases, but imperatively. Hence the clear documentation would be nice.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is still a draft PR 😊
More docs are coming!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mind elaborating on the reasoning behind this decision to use firstValueFrom? It's not necessarily intuitive from an RxJS perspective and also limits the usage and flexibility to some extent.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mind elaborating on the reasoning behind this decision to use firstValueFrom?

A stream of T values doesn't have enough information to communicate start loading / resolved. So this is geared towards HTTPClient-like usages.

An alternative API would have a stream of value + state but this rather far from the initial API idea.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO a status of "loading" only makes sense before the first value returns. After that whenever a new value is emitted from the observable, it's just "there". I work a lot with websockets and the server is pushing values whenever it needs (I connect observables in the backend via websockets to observables in the frontend to propagate changes to all interested clients).

Regarding the name "loading": for me this means it's for fetching data. But couldn't we use it for posting data, too? In my similar implementations I use "busy" as the description of the status.

I just got a quick look at the code, it's definitely something I'm very interested in! Great work!

@pkozlowski-opensource pkozlowski-opensource added the target: minor This PR is targeted for the next minor release label Oct 18, 2024
@ducin
Copy link
Contributor

ducin commented Oct 18, 2024

To me it looks as a conceptual micro-implementation of what a query is in Tanstack Query, but:

  • different API/details ofc (resource vs injectQuery)
  • no responsibility for sharing (up to devs to decide)
  • highly integrated with the framework
  • under full control (own impl)
  • compatible with promises/rxjs/signals - whatever people need

IMO an absolutely great move 💪

Question @alxhub: what's your take on potential esource invalidation, whenever the client knows that the resource is already outdated? E.g.

  • resource fetches a list
  • another request somewhere deletes the element from the list
  • resource should get refetched
    Q: whose responsibility is it? Any declarative approach for request invalidation (a.k.a. mutation)?

@pkozlowski-opensource
Copy link
Member

Question @alxhub: what's your take on potential resource invalidation, whenever the client knows that the resource is already outdated?

Maybe the refresh() method on the resource is what you are looking for? 15db202#diff-44ce2a734cb36b459d59d047d7f6d31bce24030489ee3e22a97c8eb2a191e08eR166-R193

@kyjus25
Copy link

kyjus25 commented Oct 18, 2024

My only concern is that the server caching for SSR won't work for the fetch flavor. You'd need to use firstValueFrom(HttpClient) to still get the benefits of HttpClient or use the rxResource version.

It's a gotcha, but perhaps a confusing one for new devs.

@alxhub alxhub force-pushed the experimental/resource/impl branch from c3b6a75 to b56f9ce Compare October 18, 2024 15:07
@alxhub alxhub marked this pull request as ready for review October 18, 2024 15:08
@angular-robot angular-robot bot added area: core Issues related to the framework runtime and removed area: core Issues related to the framework runtime labels Oct 18, 2024
Copy link
Member

@pkozlowski-opensource pkozlowski-opensource left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Reviewed-for: public-api
Reviewed-for: fw-core

@alxhub alxhub force-pushed the experimental/resource/impl branch from b56f9ce to 03cf65d Compare October 18, 2024 15:17
@angular-robot angular-robot bot added area: core Issues related to the framework runtime and removed area: core Issues related to the framework runtime labels Oct 18, 2024
@alxhub alxhub force-pushed the experimental/resource/impl branch from 03cf65d to d36db90 Compare October 18, 2024 16:18
@angular-robot angular-robot bot removed the area: core Issues related to the framework runtime label Oct 18, 2024
@angular-robot angular-robot bot added the area: core Issues related to the framework runtime label Oct 21, 2024
@ngbot ngbot bot modified the milestone: Backlog Oct 21, 2024
@alxhub alxhub added action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note and removed action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews labels Oct 21, 2024
@alxhub
Copy link
Member Author

alxhub commented Oct 21, 2024

Caretaker: TAP is failing only due to the batched migration changes (which require a BUILD update) - this PR is passing.

@AndrewKushnir
Copy link
Contributor

This PR was merged into the repository by commit 9762b24.

The changes were merged into the following branches: main

AndrewKushnir pushed a commit that referenced this pull request Oct 21, 2024
)

Implement a new experimental API, called `resource()`. Resources are
asynchronous dependencies that are managed and delivered through the signal
graph. Resources are defined by their reactive request function and their
asynchronous loader, which retrieves the value of the resource for a given
request value. For example, a "current user" resource may retrieve data for
the current user, where the request function derives the API call to make
from a signal of the current user id.

Resources are represented by the `Resource<T>` type, which includes signals
for the resource's current value as well as its state. `WritableResource<T>`
extends that type to allow for local mutations of the resource through its
`value` signal (which is therefore two-way bindable).

PR Close #58255
AndrewKushnir pushed a commit that referenced this pull request Oct 21, 2024
Implementations of two rxjs-interop APIs which produce `Resource`s from
RxJS Observables. `rxResource()` is a flavor of `resource()` which uses a
projection to an `Observable` as its loader (like `switchMap`).

PR Close #58255
@wartab
Copy link
Contributor

wartab commented Oct 23, 2024

Not sure if this is still being read, but as someone who had previously worked with resources with SolidJS and having implemented a version of Resources a while ago in Angular userland (I was actually working on a PR before leaving for holidays, because I didn't expect resources at all, so I'm really happy you guys did it), I have a few requests/suggestions regarding the API.

Resources are nice, but being able to handle error-tolerant "paginated" resource lists without going through hoops is nicer. So in my opinion, either a sort of ListResource needs to exists along Resource or something would need to be changed with how resources are implemented.

Quickly going through the code without testing, from what I understand, once the state of the resource changes, the current value is lost. I don't think that is a good thing. In my opinion, the value should still be there while loading and while having errors.

For this purpose, I would also suggest passing the current value while executing the resource loader.

On top of that (and I know this is something that sucks with Promises and rxjs), I'd love if there was a way to type hint errors.

Also a personal preference I had was to be able to use the variable that contains the resource as a signal for shorter and more consistent syntax rather than doing myResource.value().

@flensrocker
Copy link

@wartab That my be solvable with the new linkedSignal, which provides the previous value. But I haven't actually tried it.

@pkozlowski-opensource
Copy link
Member

@wartab cool to hear the you were toying with the ideas similar to the resource! Appreciate your feedback - this adds to several other considerations around this API. We do plan to host a RFC to discuss and potentially refine this design (we are releasing it as experimental in v19).

@tomastrajan
Copy link
Contributor

tomastrajan commented Oct 23, 2024

Hey hey folks!

Based on the examples I have seen floating in the wild and my own experimentation I would like to ask about some choices that have been made for rxResource

// what we will get (from what i understand)
  todos = rxResource({
    request: () => this.query(),
    loader: ({ request }) => {
      // not really "rx" (similar to how Angular interceptors work
      // each call is creating a new stream instance in isolation
      return of(request).pipe(
        debounceTime(300), // will only ever be called once hence no debounce
        switchMap((r) => // seen in examples in the wild, will only ever be called once hence no switch
          this.#http.get<Todo[]>(`${API_URL}/todos?title_like=${r}`),
        )
      );

      // or as seen in the wild
      return timer(300).pipe(switchMap(...)) // again , will only be called once at most hence pointless?
     // from what I understand the "switch" happens on the rxResource level, this stream just get's unsubbed
    },
  });

  // actual RX would be something like following (similar to rxMethod)
  todos = rxResource<Todo[], string>({
    request: () => this.query(),
    loader: ({ request: Observable<string> }) => this.request.pipe( // request is "streamified" 
      debounceTime(300), 
      switchMap((r) =>  // we could pick operator which makes sense, then again most likely switchMap
        this.#http.get<Todo[]>(`${API_URL}/todos?title_like=${r}`),
      ),
    ),
  })   

Now , what I assume is that switch us baked into rxResource itself as that's a resonable default for loading of data.

My main worry is that this confuses people about RxJs even more as the current way it's used is close to something like

getTodos(query: tring) {
    this.http().subscribe() 
}

because the loader() is called for each change of query and therefore creates multiple stream instances in isolation, just that it also unsubs previous one if there was a new call to loader...

Am I missing something?

EDIT:

Another thing which comes to mind, the resource

could have just accepted both Promise and Observable and keep the isolated loader call behavior, I would guess it would not be to crazy to make that work at a cost of slightly larger bundle size, but I guess that's all about making RxJs optional?

If that's the case, I would still say it would be more fortunate to provide something like a rxLoader factory for jsut that loader

and keep the rxResource more in line with what RxJs does usually, as in:

  • declarative
  • single stream instance
  • fully defined from start inducing all sources of change

@manfredsteyer
Copy link
Contributor

@tomastrajan I have the impression, rxRessource is currently rather optimized for HTTP requests that exactly return one result. I'm not sure if there are plans to make it more powerful, but currently, I would directly use RxJS for more complex data flows.

Regarding combining rxResource and resource: IMHO the reason for this is to separate the RxJS-interop by putting it into a secondary entry point of its own to prepare for a time when RxJS becomes optional (but stays important for more complex data flows).

@tomastrajan
Copy link
Contributor

@manfredsteyer this makes sense, then again, it looks like main use case is to react to the change in the request which means there will be multiple requests, debouncing will most likely be common feature required in such scenarios.

It feels like this is solved in a very non-idiomatic / confusing ways with the currently proposed approach?

@msmallest
Copy link
Contributor

msmallest commented Oct 23, 2024

@tomastrajan

debouncing will most likely be common feature required in such scenarios.

A typeahead with a debounce was the first thing that I tried with an rxResource. I feel like your "actual RX would be something like following (similar to rxMethod)" suggestion is a lot closer to what I was expecting. The alternative to using a resource to have a signal driven typeahead requires some of the toSignal(toObservable(this.typeaheadStringSignal).pipe(debounceTime(...))) maneuvering that I imagined rxResource would eliminate the need for

each call is creating a new stream instance in isolation
return of(request).pipe(

This is also the scenario I ran into when trying to initiate a stream with the request value, rather than passing it directly into something like a .get(...). Since starting a stream with an of(request) loses a lot of rx benefits like you outline, I would probably just use the promise based resource by writing my HTTP methods to return promises or wrap observable based methods in firstValueFrom, which in a roundabout leads to what rxResource does in the end. Your suggestion about rxMethod-like functionality would be much better, and give rxResource a more distinct identity from resource.

PS to the team: Overall very excited about this API - looking forward to expanding on the rx observations during the RFC.

@e-oz
Copy link

e-oz commented Oct 23, 2024

My 5 cents: exhaustMap() behavior of the refresh() is a mistake

@OliverGrack
Copy link

First of all, nice to see this being included in Angular!

I have 2 questions:

First, you decided against using getters on the resource. I.e. one calls 'myResource.value()', as opposed to 'myResource.value'. In Svelte and SolidJs, often such stores / bundles of signals, are represented using getters. I am not sure, if this would work with the current typeguard on hasValue, that might require Resource to be defined as a union. Anyways, what were the decision for one over the other here?

And secondly, I am sure you also looked into Suspense and similar concepts of other frameworks. In react and solidjs, reading data before its loaded throws, to allow propagating loading states automatically. In the case of solidjs, automatically propagating loading states through their signal graph. Is this something that is still being explored, or did you decide against such a mechanism already?

@alxhub
Copy link
Member Author

alxhub commented Oct 23, 2024

Hey all! Wow, this PR has 74 comments 😲 I'm happy to join in the discussion here, but I do want to note:

There will be a Resource RFC

This has been the plan, but we wanted to get the API into people's hands beforehand, so that we can have more concrete conversations around the API and its semantics. Your feedback / opinions are extremely valuable to us ❤️

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Nov 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime detected: feature PR contains a feature commit merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.