diff --git a/adev/shared-docs/services/search.service.ts b/adev/shared-docs/services/search.service.ts index c4be8addb48..73d454bd039 100644 --- a/adev/shared-docs/services/search.service.ts +++ b/adev/shared-docs/services/search.service.ts @@ -55,8 +55,8 @@ export class Search { private readonly client = inject(ALGOLIA_CLIENT); searchResults = resource({ - request: () => this.searchQuery() || undefined, // coerces empty string to undefined - loader: async ({request: query, abortSignal}) => { + params: () => this.searchQuery() || undefined, // coerces empty string to undefined + loader: async ({params: query, abortSignal}) => { // Until we have a better alternative we debounce by awaiting for a short delay. await wait(SEARCH_DELAY, abortSignal); diff --git a/adev/src/content/guide/signals/resource.md b/adev/src/content/guide/signals/resource.md index d0b70ac6d0f..a5ccdf58005 100644 --- a/adev/src/content/guide/signals/resource.md +++ b/adev/src/content/guide/signals/resource.md @@ -14,24 +14,24 @@ import {resource, Signal} from '@angular/core'; const userId: Signal = getUserId(); const userResource = resource({ - // Define a reactive request computation. - // The request value recomputes whenever any read signals change. - request: () => ({id: userId()}), + // Define a reactive computation. + // The params value recomputes whenever any read signals change. + params: () => ({id: userId()}), // Define an async loader that retrieves data. - // The resource calls this function every time the `request` value changes. - loader: ({request}) => fetchUser(request), + // The resource calls this function every time the `params` value changes. + loader: ({params}) => fetchUser(params), }); // Create a computed signal based on the result of the resource's loader function. const firstName = computed(() => userResource.value().firstName); ``` -The `resource` function accepts a `ResourceOptions` object with two main properties: `request` and `loader`. +The `resource` function accepts a `ResourceOptions` object with two main properties: `params` and `loader`. -The `request` property defines a reactive computation that produce a request value. Whenever signals read in this computation change, the resource produces a new request value, similar to `computed`. +The `params` property defines a reactive computation that produces a parameter value. Whenever signals read in this computation change, the resource produces a new parameter value, similar to `computed`. -The `loader` property defines a `ResourceLoader`— an async function that retrieves some state. The resource calls the loader every time the `request` computation produces a new value, passing that value to the loader. See [Resource loaders](#resource-loaders) below for more details. +The `loader` property defines a `ResourceLoader`— an async function that retrieves some state. The resource calls the loader every time the `params` computation produces a new value, passing that value to the loader. See [Resource loaders](#resource-loaders) below for more details. `Resource` has a `value` signal that contains the results of the loader. @@ -39,19 +39,19 @@ The `loader` property defines a `ResourceLoader`— an async function that retri When creating a resource, you specify a `ResourceLoader`. This loader is an async function that accepts a single parameter— a `ResourceLoaderParams` object— and returns a value. -The `ResourceLoaderParams` object contains three properties: `request`, `previous`, and `abortSignal`. +The `ResourceLoaderParams` object contains three properties: `params`, `previous`, and `abortSignal`. | Property | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| `request` | The value of the resource's `request` computation. | +| `params` | The value of the resource's `params` computation. | | `previous` | An object with a `status` property, containing the previous `ResourceStatus`. | | `abortSignal` | An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). See [Aborting requests](#aborting-requests) below for details. | -If the `request` computation returns `undefined`, the loader function does not run and the resource status becomes `'idle'`. +If the `params` computation returns `undefined`, the loader function does not run and the resource status becomes `'idle'`. ### Aborting requests -A resource aborts an outstanding request if the `request` computation changes while the resource is loading. +A resource aborts an outstanding loading operation if the `params` computation changes while the resource is loading. You can use the `abortSignal` in `ResourceLoaderParams` to respond to aborted requests. For example, the native `fetch` function accepts an `AbortSignal`: @@ -59,7 +59,7 @@ You can use the `abortSignal` in `ResourceLoaderParams` to respond to aborted re const userId: Signal = getUserId(); const userResource = resource({ - request: () => ({id: userId()}), + params: () => ({id: userId()}), loader: ({request, abortSignal}): Promise => { // fetch cancels any outstanding HTTP requests when the given `AbortSignal` // indicates that the request has been aborted. @@ -78,8 +78,8 @@ You can programmatically trigger a resource's `loader` by calling the `reload` m const userId: Signal = getUserId(); const userResource = resource({ - request: () => ({id: userId()}), - loader: ({request}) => fetchUser(request), + params: () => ({id: userId()}), + loader: ({params}) => fetchUser(params), }); // ... diff --git a/goldens/public-api/core/index.api.md b/goldens/public-api/core/index.api.md index 86e7c4f89a2..0c6783e57b4 100644 --- a/goldens/public-api/core/index.api.md +++ b/goldens/public-api/core/index.api.md @@ -179,7 +179,7 @@ export interface BaseResourceOptions { defaultValue?: NoInfer; equal?: ValueEqualityFn; injector?: Injector; - request?: () => R; + params?: () => R; } // @public @@ -1620,11 +1620,11 @@ export interface ResourceLoaderParams { // (undocumented) abortSignal: AbortSignal; // (undocumented) + params: NoInfer>; + // (undocumented) previous: { status: ResourceStatus; }; - // (undocumented) - request: Exclude, undefined>; } // @public (undocumented) diff --git a/packages/common/http/src/resource.ts b/packages/common/http/src/resource.ts index 7b10d6bb35d..a0c18e2f5ab 100644 --- a/packages/common/http/src/resource.ts +++ b/packages/common/http/src/resource.ts @@ -315,7 +315,7 @@ class HttpResourceImpl ) { super( request, - ({request, abortSignal}) => { + ({params: request, abortSignal}) => { let sub: Subscription; // Track the abort listener so it can be removed if the Observable completes (as a memory diff --git a/packages/core/rxjs-interop/test/rx_resource_spec.ts b/packages/core/rxjs-interop/test/rx_resource_spec.ts index b49aaf6835c..b798b7c1153 100644 --- a/packages/core/rxjs-interop/test/rx_resource_spec.ts +++ b/packages/core/rxjs-interop/test/rx_resource_spec.ts @@ -30,8 +30,8 @@ describe('rxResource()', () => { let unsub = false; let lastSeenRequest: number = 0; rxResource({ - request, - loader: ({request}) => { + params: request, + loader: ({params: request}) => { lastSeenRequest = request; return new Observable((sub) => { if (request === 2) { diff --git a/packages/core/src/resource/api.ts b/packages/core/src/resource/api.ts index ebe1f7107be..38cb3d597a2 100644 --- a/packages/core/src/resource/api.ts +++ b/packages/core/src/resource/api.ts @@ -128,7 +128,7 @@ export interface ResourceRef extends WritableResource { * @experimental */ export interface ResourceLoaderParams { - request: Exclude, undefined>; + params: NoInfer>; abortSignal: AbortSignal; previous: { status: ResourceStatus; @@ -163,7 +163,7 @@ export interface BaseResourceOptions { * * If a request function isn't provided, the loader won't rerun unless the resource is reloaded. */ - request?: () => R; + params?: () => R; /** * The value which will be returned from the resource when a server value is unavailable, such as diff --git a/packages/core/src/resource/resource.ts b/packages/core/src/resource/resource.ts index b269e9dd55f..6e94843803b 100644 --- a/packages/core/src/resource/resource.ts +++ b/packages/core/src/resource/resource.ts @@ -20,6 +20,7 @@ import { ResourceStreamingLoader, StreamingResourceOptions, ResourceStreamItem, + ResourceLoaderParams, } from './api'; import {ValueEqualityFn} from '../../primitives/signals'; @@ -58,9 +59,12 @@ export function resource( export function resource(options: ResourceOptions): ResourceRef; export function resource(options: ResourceOptions): ResourceRef { options?.injector || assertInInjectionContext(resource); - const request = (options.request ?? (() => null)) as () => R; + const oldNameForParams = ( + options as ResourceOptions & {request: ResourceOptions['params']} + ).request; + const params = (options.params ?? oldNameForParams ?? (() => null)) as () => R; return new ResourceImpl( - request, + params, getLoader(options), options.defaultValue, options.equal ? wrapEqualityFn(options.equal) : undefined, @@ -308,12 +312,14 @@ export class ResourceImpl extends BaseWritableResource implements Resou // which side of the `await` they are. const stream = await untracked(() => { return this.loaderFn({ + params: extRequest.request as Exclude, + // TODO(alxhub): cleanup after g3 removal of `request` alias. request: extRequest.request as Exclude, abortSignal, previous: { status: previousStatus, }, - }); + } as ResourceLoaderParams); }); // If this request has been aborted, or the current request no longer diff --git a/packages/core/test/resource/resource_spec.ts b/packages/core/test/resource/resource_spec.ts index ee90d9ff687..67312a92c63 100644 --- a/packages/core/test/resource/resource_spec.ts +++ b/packages/core/test/resource/resource_spec.ts @@ -78,8 +78,8 @@ describe('resource', () => { const counter = signal(0); const backend = new MockEchoBackend(); const echoResource = resource({ - request: () => ({counter: counter()}), - loader: (params) => backend.fetch(params.request), + params: () => ({counter: counter()}), + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); @@ -130,8 +130,8 @@ describe('resource', () => { const backend = new MockEchoBackend(); const requestParam = {}; const echoResource = resource({ - request: () => requestParam, - loader: (params) => backend.fetch(params.request), + params: () => requestParam, + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); @@ -149,9 +149,9 @@ describe('resource', () => { const backend = new MockEchoBackend(); const counter = signal(0); const echoResource = resource({ - request: () => ({counter: counter()}), + params: () => ({counter: counter()}), loader: (params) => { - if (params.request.counter % 2 === 0) { + if (params.params.counter % 2 === 0) { return Promise.resolve('ok'); } else { throw new Error('KO'); @@ -186,10 +186,10 @@ describe('resource', () => { const request = signal(0); let resolve: Array<() => void> = []; const res = resource({ - request, - loader: async ({request}) => { + params: request, + loader: async ({params}) => { const p = Promise.withResolvers(); - resolve.push(() => p.resolve(request)); + resolve.push(() => p.resolve(params)); return p.promise; }, injector: TestBed.inject(Injector), @@ -228,9 +228,9 @@ describe('resource', () => { const DEFAULT: string[] = []; const request = signal(0); const res = resource({ - request, - loader: async ({request}) => { - if (request === 2) { + params: request, + loader: async ({params}) => { + if (params === 2) { throw new Error('err'); } return ['data']; @@ -256,8 +256,8 @@ describe('resource', () => { const counter = signal(0); const backend = new MockEchoBackend(); const echoResource = resource({ - request: () => (counter() > 5 ? {counter: counter()} : undefined), - loader: (params) => backend.fetch(params.request), + params: () => (counter() > 5 ? {counter: counter()} : undefined), + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); @@ -275,12 +275,12 @@ describe('resource', () => { const backend = new MockEchoBackend<{counter: number}>(); const aborted: {counter: number}[] = []; const echoResource = resource<{counter: number}, {counter: number}>({ - request: () => ({counter: counter()}), - loader: ({request, abortSignal}) => { - abortSignal.addEventListener('abort', () => backend.abort(request)); - return backend.fetch(request).catch((reason) => { + params: () => ({counter: counter()}), + loader: ({params, abortSignal}) => { + abortSignal.addEventListener('abort', () => backend.abort(params)); + return backend.fetch(params).catch((reason) => { if (reason === 'aborted') { - aborted.push(request); + aborted.push(params); } throw new Error(reason); }); @@ -309,12 +309,12 @@ describe('resource', () => { const aborted: {counter: number}[] = []; const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector)); const echoResource = resource<{counter: number}, {counter: number}>({ - request: () => ({counter: counter()}), - loader: ({request, abortSignal}) => { - abortSignal.addEventListener('abort', () => backend.abort(request)); - return backend.fetch(request).catch((reason) => { + params: () => ({counter: counter()}), + loader: ({params, abortSignal}) => { + abortSignal.addEventListener('abort', () => backend.abort(params)); + return backend.fetch(params).catch((reason) => { if (reason === 'aborted') { - aborted.push(request); + aborted.push(params); } throw new Error(reason); }); @@ -341,12 +341,12 @@ describe('resource', () => { const aborted: {counter: number}[] = []; const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector)); const echoResource = resource<{counter: number}, {counter: number}>({ - request: () => ({counter: counter()}), - loader: ({request, abortSignal}) => { - abortSignal.addEventListener('abort', () => backend.abort(request)); - return backend.fetch(request).catch((reason) => { + params: () => ({counter: counter()}), + loader: ({params, abortSignal}) => { + abortSignal.addEventListener('abort', () => backend.abort(params)); + return backend.fetch(params).catch((reason) => { if (reason === 'aborted') { - aborted.push(request); + aborted.push(params); } throw new Error(reason); }); @@ -372,11 +372,11 @@ describe('resource', () => { const unrelated = signal('a'); const backend = new MockResponseCountingBackend(); const res = resource({ - request: () => 0, + params: () => 0, loader: (params) => { // read reactive state and assure it is _not_ tracked unrelated(); - return backend.fetch(params.request); + return backend.fetch(params.params); }, injector: TestBed.inject(Injector), }); @@ -398,8 +398,8 @@ describe('resource', () => { const counter = signal(0); const backend = new MockEchoBackend(); const echoResource = resource({ - request: () => ({counter: counter()}), - loader: (params) => backend.fetch(params.request), + params: () => ({counter: counter()}), + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); @@ -435,8 +435,8 @@ describe('resource', () => { it('should allow re-fetching data', async () => { const backend = new MockResponseCountingBackend(); const res = resource({ - request: () => 0, - loader: (params) => backend.fetch(params.request), + params: () => 0, + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); @@ -504,8 +504,8 @@ describe('resource', () => { const request = signal(undefined); const backend = new MockEchoBackend(); const echoResource = resource({ - request, - loader: (params) => backend.fetch(params.request), + params: request, + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); // Idle to start. @@ -532,8 +532,8 @@ describe('resource', () => { const request = signal(1); const backend = new MockEchoBackend(); const echoResource = resource({ - request, - loader: (params) => backend.fetch(params.request), + params: request, + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); const appRef = TestBed.inject(ApplicationRef); @@ -563,8 +563,8 @@ describe('resource', () => { const request = signal(1); const backend = new MockEchoBackend(); const echoResource = resource({ - request, - loader: (params) => backend.fetch(params.request), + params: request, + loader: (params) => backend.fetch(params.params), injector: TestBed.inject(Injector), }); const appRef = TestBed.inject(ApplicationRef); @@ -642,9 +642,9 @@ describe('resource', () => { const stream = signal<{value: number} | {error: unknown}>({value: 0}); const request = signal(1); const res = resource({ - request, - stream: async ({request}) => { - if (request === 1) { + params: request, + stream: async ({params}) => { + if (params === 1) { return stream; } else { return signal({value: 0}); @@ -672,12 +672,12 @@ describe('resource', () => { const backend = new MockEchoBackend<{counter: number} | null>(); const aborted: ({counter: number} | null)[] = []; const echoResource = resource<{counter: number} | null, {counter: number} | null>({ - request: () => ({counter: counter()}), - loader: ({request, abortSignal}) => { - abortSignal.addEventListener('abort', () => backend.abort(request)); - return backend.fetch(request).catch((reason) => { + params: () => ({counter: counter()}), + loader: ({params, abortSignal}) => { + abortSignal.addEventListener('abort', () => backend.abort(params)); + return backend.fetch(params).catch((reason) => { if (reason === 'aborted') { - aborted.push(request); + aborted.push(params); } throw new Error(reason); });