Skip to content

Commit 2206efa

Browse files
mmalerbathePunderWoman
authored andcommitted
feat(core): add special return statuses for resource params
Allows throwing from the resource's params function to transition the resource to a status other than resolved. In particular, the following values can be thrown from params: - `ResourceParamsStatus.IDLE` causes the resource to become `idle` (equivalent to returning `undefined`) - `ResourceParamsStatus.LOADING` causes the resource to become `loading` - Any `Error` object causes the resource to become `error` and report the error that was thrown via `.error()` To simplify chaining together resources, this PR also introduces a context object passed into to the `params` functon. This context contains a `chain` function that can be used to get the value of a resource that the params want to depend on, while automatically propagating the idle, loading, and erorr states of the resource forward.
1 parent d2e33e8 commit 2206efa

9 files changed

Lines changed: 437 additions & 101 deletions

File tree

goldens/public-api/common/http/index.api.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Injector } from '@angular/core';
1212
import { ModuleWithProviders } from '@angular/core';
1313
import { Observable } from 'rxjs';
1414
import { Provider } from '@angular/core';
15+
import { ResourceParamsContext } from '@angular/core';
1516
import { ResourceRef } from '@angular/core';
1617
import { Signal } from '@angular/core';
1718
import { ValueEqualityFn } from '@angular/core';
@@ -2980,43 +2981,43 @@ export const httpResource: HttpResourceFn;
29802981

29812982
// @public
29822983
export interface HttpResourceFn {
2983-
<TResult = unknown>(url: () => string | undefined, options: HttpResourceOptions<TResult, unknown> & {
2984+
<TResult = unknown>(url: (ctx: ResourceParamsContext) => string | undefined, options: HttpResourceOptions<TResult, unknown> & {
29842985
defaultValue: NoInfer<TResult>;
29852986
}): HttpResourceRef<TResult>;
2986-
<TResult = unknown>(url: () => string | undefined, options?: HttpResourceOptions<TResult, unknown>): HttpResourceRef<TResult | undefined>;
2987-
<TResult = unknown>(request: () => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, unknown> & {
2987+
<TResult = unknown>(url: (ctx: ResourceParamsContext) => string | undefined, options?: HttpResourceOptions<TResult, unknown>): HttpResourceRef<TResult | undefined>;
2988+
<TResult = unknown>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, unknown> & {
29882989
defaultValue: NoInfer<TResult>;
29892990
}): HttpResourceRef<TResult>;
2990-
<TResult = unknown>(request: () => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, unknown>): HttpResourceRef<TResult | undefined>;
2991+
<TResult = unknown>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, unknown>): HttpResourceRef<TResult | undefined>;
29912992
arrayBuffer: {
2992-
<TResult = ArrayBuffer>(url: () => string | undefined, options: HttpResourceOptions<TResult, ArrayBuffer> & {
2993+
<TResult = ArrayBuffer>(url: (ctx: ResourceParamsContext) => string | undefined, options: HttpResourceOptions<TResult, ArrayBuffer> & {
29932994
defaultValue: NoInfer<TResult>;
29942995
}): HttpResourceRef<TResult>;
2995-
<TResult = ArrayBuffer>(url: () => string | undefined, options?: HttpResourceOptions<TResult, ArrayBuffer>): HttpResourceRef<TResult | undefined>;
2996-
<TResult = ArrayBuffer>(request: () => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, ArrayBuffer> & {
2996+
<TResult = ArrayBuffer>(url: (ctx: ResourceParamsContext) => string | undefined, options?: HttpResourceOptions<TResult, ArrayBuffer>): HttpResourceRef<TResult | undefined>;
2997+
<TResult = ArrayBuffer>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, ArrayBuffer> & {
29972998
defaultValue: NoInfer<TResult>;
29982999
}): HttpResourceRef<TResult>;
2999-
<TResult = ArrayBuffer>(request: () => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, ArrayBuffer>): HttpResourceRef<TResult | undefined>;
3000+
<TResult = ArrayBuffer>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, ArrayBuffer>): HttpResourceRef<TResult | undefined>;
30003001
};
30013002
blob: {
3002-
<TResult = Blob>(url: () => string | undefined, options: HttpResourceOptions<TResult, Blob> & {
3003+
<TResult = Blob>(url: (ctx: ResourceParamsContext) => string | undefined, options: HttpResourceOptions<TResult, Blob> & {
30033004
defaultValue: NoInfer<TResult>;
30043005
}): HttpResourceRef<TResult>;
3005-
<TResult = Blob>(url: () => string | undefined, options?: HttpResourceOptions<TResult, Blob>): HttpResourceRef<TResult | undefined>;
3006-
<TResult = Blob>(request: () => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, Blob> & {
3006+
<TResult = Blob>(url: (ctx: ResourceParamsContext) => string | undefined, options?: HttpResourceOptions<TResult, Blob>): HttpResourceRef<TResult | undefined>;
3007+
<TResult = Blob>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, Blob> & {
30073008
defaultValue: NoInfer<TResult>;
30083009
}): HttpResourceRef<TResult>;
3009-
<TResult = Blob>(request: () => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, Blob>): HttpResourceRef<TResult | undefined>;
3010+
<TResult = Blob>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, Blob>): HttpResourceRef<TResult | undefined>;
30103011
};
30113012
text: {
3012-
<TResult = string>(url: () => string | undefined, options: HttpResourceOptions<TResult, string> & {
3013+
<TResult = string>(url: (ctx: ResourceParamsContext) => string | undefined, options: HttpResourceOptions<TResult, string> & {
30133014
defaultValue: NoInfer<TResult>;
30143015
}): HttpResourceRef<TResult>;
3015-
<TResult = string>(url: () => string | undefined, options?: HttpResourceOptions<TResult, string>): HttpResourceRef<TResult | undefined>;
3016-
<TResult = string>(request: () => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, string> & {
3016+
<TResult = string>(url: (ctx: ResourceParamsContext) => string | undefined, options?: HttpResourceOptions<TResult, string>): HttpResourceRef<TResult | undefined>;
3017+
<TResult = string>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options: HttpResourceOptions<TResult, string> & {
30173018
defaultValue: NoInfer<TResult>;
30183019
}): HttpResourceRef<TResult>;
3019-
<TResult = string>(request: () => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, string>): HttpResourceRef<TResult | undefined>;
3020+
<TResult = string>(request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined, options?: HttpResourceOptions<TResult, string>): HttpResourceRef<TResult | undefined>;
30203021
};
30213022
}
30223023

goldens/public-api/core/index.api.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export interface BaseResourceOptions<T, R> {
185185
defaultValue?: NoInfer<T>;
186186
equal?: ValueEqualityFn<T>;
187187
injector?: Injector;
188-
params?: () => R;
188+
params?: (ctx: ResourceParamsContext) => R;
189189
}
190190

191191
// @public
@@ -1657,6 +1657,12 @@ export function resource<T, R>(options: ResourceOptions<T, R> & {
16571657
// @public
16581658
export function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T | undefined>;
16591659

1660+
// @public
1661+
export class ResourceDependencyError extends Error {
1662+
constructor(dependency: Resource<unknown>);
1663+
readonly dependency: Resource<unknown>;
1664+
}
1665+
16601666
// @public
16611667
export function resourceFromSnapshots<T>(source: () => ResourceSnapshot<T>): Resource<T>;
16621668

@@ -1680,6 +1686,17 @@ export type ResourceOptions<T, R> = (PromiseResourceOptions<T, R> | StreamingRes
16801686
debugName?: string;
16811687
};
16821688

1689+
// @public
1690+
export interface ResourceParamsContext {
1691+
readonly chain: <T>(resource: Resource<T>) => T;
1692+
}
1693+
1694+
// @public
1695+
export class ResourceParamsStatus extends Error {
1696+
static readonly IDLE: ResourceParamsStatus;
1697+
static readonly LOADING: ResourceParamsStatus;
1698+
}
1699+
16831700
// @public
16841701
export interface ResourceRef<T> extends WritableResource<T> {
16851702
destroy(): void;

packages/common/http/src/resource.ts

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,30 @@
77
*/
88

99
import {
10-
Injector,
11-
Signal,
12-
ɵResourceImpl as ResourceImpl,
13-
inject,
14-
linkedSignal,
1510
assertInInjectionContext,
16-
signal,
1711
computed,
12+
ɵencapsulateResourceError as encapsulateResourceError,
13+
inject,
14+
Injector,
15+
linkedSignal,
16+
ɵResourceImpl as ResourceImpl,
17+
type ResourceParamsContext,
1818
ResourceStreamItem,
19+
Signal,
20+
signal,
21+
TransferState,
1922
type ValueEqualityFn,
2023
ɵRuntimeError,
2124
ɵRuntimeErrorCode,
22-
ɵencapsulateResourceError as encapsulateResourceError,
23-
TransferState,
24-
untracked,
2525
} from '@angular/core';
2626
import type {Subscription} from 'rxjs';
2727

28-
import {HttpRequest} from './request';
2928
import {HttpClient} from './client';
30-
import {HttpErrorResponse, HttpEventType, HttpProgressEvent} from './response';
3129
import {HttpHeaders} from './headers';
3230
import {HttpParams} from './params';
33-
import {HttpResourceRef, HttpResourceOptions, HttpResourceRequest} from './resource_api';
31+
import {HttpRequest} from './request';
32+
import {HttpResourceOptions, HttpResourceRef, HttpResourceRequest} from './resource_api';
33+
import {HttpErrorResponse, HttpEventType, HttpProgressEvent} from './response';
3434
import {
3535
CACHE_OPTIONS,
3636
HTTP_TRANSFER_CACHE_ORIGIN_MAP,
@@ -57,7 +57,7 @@ export interface HttpResourceFn {
5757
* @experimental 19.2
5858
*/
5959
<TResult = unknown>(
60-
url: () => string | undefined,
60+
url: (ctx: ResourceParamsContext) => string | undefined,
6161
options: HttpResourceOptions<TResult, unknown> & {defaultValue: NoInfer<TResult>},
6262
): HttpResourceRef<TResult>;
6363

@@ -73,7 +73,7 @@ export interface HttpResourceFn {
7373
* @experimental 19.2
7474
*/
7575
<TResult = unknown>(
76-
url: () => string | undefined,
76+
url: (ctx: ResourceParamsContext) => string | undefined,
7777
options?: HttpResourceOptions<TResult, unknown>,
7878
): HttpResourceRef<TResult | undefined>;
7979

@@ -89,7 +89,7 @@ export interface HttpResourceFn {
8989
* @experimental 19.2
9090
*/
9191
<TResult = unknown>(
92-
request: () => HttpResourceRequest | undefined,
92+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
9393
options: HttpResourceOptions<TResult, unknown> & {defaultValue: NoInfer<TResult>},
9494
): HttpResourceRef<TResult>;
9595

@@ -105,7 +105,7 @@ export interface HttpResourceFn {
105105
* @experimental 19.2
106106
*/
107107
<TResult = unknown>(
108-
request: () => HttpResourceRequest | undefined,
108+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
109109
options?: HttpResourceOptions<TResult, unknown>,
110110
): HttpResourceRef<TResult | undefined>;
111111

@@ -121,22 +121,22 @@ export interface HttpResourceFn {
121121
*/
122122
arrayBuffer: {
123123
<TResult = ArrayBuffer>(
124-
url: () => string | undefined,
124+
url: (ctx: ResourceParamsContext) => string | undefined,
125125
options: HttpResourceOptions<TResult, ArrayBuffer> & {defaultValue: NoInfer<TResult>},
126126
): HttpResourceRef<TResult>;
127127

128128
<TResult = ArrayBuffer>(
129-
url: () => string | undefined,
129+
url: (ctx: ResourceParamsContext) => string | undefined,
130130
options?: HttpResourceOptions<TResult, ArrayBuffer>,
131131
): HttpResourceRef<TResult | undefined>;
132132

133133
<TResult = ArrayBuffer>(
134-
request: () => HttpResourceRequest | undefined,
134+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
135135
options: HttpResourceOptions<TResult, ArrayBuffer> & {defaultValue: NoInfer<TResult>},
136136
): HttpResourceRef<TResult>;
137137

138138
<TResult = ArrayBuffer>(
139-
request: () => HttpResourceRequest | undefined,
139+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
140140
options?: HttpResourceOptions<TResult, ArrayBuffer>,
141141
): HttpResourceRef<TResult | undefined>;
142142
};
@@ -153,22 +153,22 @@ export interface HttpResourceFn {
153153
*/
154154
blob: {
155155
<TResult = Blob>(
156-
url: () => string | undefined,
156+
url: (ctx: ResourceParamsContext) => string | undefined,
157157
options: HttpResourceOptions<TResult, Blob> & {defaultValue: NoInfer<TResult>},
158158
): HttpResourceRef<TResult>;
159159

160160
<TResult = Blob>(
161-
url: () => string | undefined,
161+
url: (ctx: ResourceParamsContext) => string | undefined,
162162
options?: HttpResourceOptions<TResult, Blob>,
163163
): HttpResourceRef<TResult | undefined>;
164164

165165
<TResult = Blob>(
166-
request: () => HttpResourceRequest | undefined,
166+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
167167
options: HttpResourceOptions<TResult, Blob> & {defaultValue: NoInfer<TResult>},
168168
): HttpResourceRef<TResult>;
169169

170170
<TResult = Blob>(
171-
request: () => HttpResourceRequest | undefined,
171+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
172172
options?: HttpResourceOptions<TResult, Blob>,
173173
): HttpResourceRef<TResult | undefined>;
174174
};
@@ -185,22 +185,22 @@ export interface HttpResourceFn {
185185
*/
186186
text: {
187187
<TResult = string>(
188-
url: () => string | undefined,
188+
url: (ctx: ResourceParamsContext) => string | undefined,
189189
options: HttpResourceOptions<TResult, string> & {defaultValue: NoInfer<TResult>},
190190
): HttpResourceRef<TResult>;
191191

192192
<TResult = string>(
193-
url: () => string | undefined,
193+
url: (ctx: ResourceParamsContext) => string | undefined,
194194
options?: HttpResourceOptions<TResult, string>,
195195
): HttpResourceRef<TResult | undefined>;
196196

197197
<TResult = string>(
198-
request: () => HttpResourceRequest | undefined,
198+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
199199
options: HttpResourceOptions<TResult, string> & {defaultValue: NoInfer<TResult>},
200200
): HttpResourceRef<TResult>;
201201

202202
<TResult = string>(
203-
request: () => HttpResourceRequest | undefined,
203+
request: (ctx: ResourceParamsContext) => HttpResourceRequest | undefined,
204204
options?: HttpResourceOptions<TResult, string>,
205205
): HttpResourceRef<TResult | undefined>;
206206
};
@@ -230,7 +230,9 @@ export const httpResource: HttpResourceFn = (() => {
230230
* the requestee.
231231
*/
232232
type ResponseType = 'arraybuffer' | 'blob' | 'json' | 'text';
233-
type RawRequestType = (() => string | undefined) | (() => HttpResourceRequest | undefined);
233+
type RawRequestType =
234+
| ((ctx: ResourceParamsContext) => string | undefined)
235+
| ((ctx: ResourceParamsContext) => HttpResourceRequest | undefined);
234236

235237
function makeHttpResourceFn<TRaw>(responseType: ResponseType) {
236238
return function httpResource<TResult = TRaw>(
@@ -270,8 +272,8 @@ function makeHttpResourceFn<TRaw>(responseType: ResponseType) {
270272

271273
return new HttpResourceImpl(
272274
injector,
273-
() => normalizeRequest(request, responseType),
274-
options?.defaultValue as TResult,
275+
(ctx: ResourceParamsContext) => normalizeRequest(ctx, request, responseType),
276+
options?.defaultValue,
275277
options?.debugName,
276278
options?.parse as (value: unknown) => TResult,
277279
options?.equal as ValueEqualityFn<unknown>,
@@ -281,10 +283,11 @@ function makeHttpResourceFn<TRaw>(responseType: ResponseType) {
281283
}
282284

283285
function normalizeRequest(
286+
ctx: ResourceParamsContext,
284287
request: RawRequestType,
285288
responseType: ResponseType,
286289
): HttpRequest<unknown> | undefined {
287-
let unwrappedRequest = typeof request === 'function' ? request() : request;
290+
let unwrappedRequest = typeof request === 'function' ? request(ctx) : request;
288291
if (unwrappedRequest === undefined) {
289292
return undefined;
290293
} else if (typeof unwrappedRequest === 'string') {
@@ -356,7 +359,7 @@ class HttpResourceImpl<T>
356359

357360
constructor(
358361
injector: Injector,
359-
request: () => HttpRequest<unknown> | undefined,
362+
request: (ctx: ResourceParamsContext) => HttpRequest<T> | undefined,
360363
defaultValue: T,
361364
debugName?: string,
362365
parse?: (value: unknown) => T,

packages/common/http/test/resource_spec.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {isNode} from '@angular/private/testing';
10-
import {ApplicationRef, Injector, signal} from '@angular/core';
9+
import {ApplicationRef, Injector, resourceFromSnapshots, signal} from '@angular/core';
1110
import {TestBed} from '@angular/core/testing';
11+
import {isNode} from '@angular/private/testing';
1212
import {
13-
HttpEventType,
14-
provideHttpClient,
15-
httpResource,
1613
HttpContext,
1714
HttpContextToken,
15+
HttpEventType,
16+
httpResource,
1817
HttpResourceRef,
18+
provideHttpClient,
1919
} from '../index';
2020
import {HttpTestingController, provideHttpClientTesting} from '../testing';
2121
import {withHttpTransferCache} from '../src/transfer_cache';
@@ -358,6 +358,17 @@ describe('httpResource', () => {
358358
expect(res.statusCode()).toBe(undefined);
359359
});
360360

361+
it('should support chain', async () => {
362+
const backend = TestBed.inject(HttpTestingController);
363+
const endpoint = resourceFromSnapshots(signal({status: 'resolved', value: '/data'}));
364+
const res = httpResource(({chain}) => chain(endpoint), {injector: TestBed.inject(Injector)});
365+
TestBed.tick();
366+
const req = backend.expectOne('/data');
367+
req.flush([]);
368+
await TestBed.inject(ApplicationRef).whenStable();
369+
expect(res.value()).toEqual([]);
370+
});
371+
361372
describe('types', () => {
362373
it('should narrow hasValue() when the value can be undefined', () => {
363374
const result: HttpResourceRef<number | undefined> = httpResource(() => '/data', {

0 commit comments

Comments
 (0)