Skip to content

Commit 18d8d44

Browse files
alxhubAndrewKushnir
authored andcommitted
feat(core): experimental resource() API for async dependencies (#58255)
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
1 parent cd59e5d commit 18d8d44

File tree

8 files changed

+961
-0
lines changed

8 files changed

+961
-0
lines changed

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,59 @@ export interface RendererType2 {
15451545
// @public
15461546
export function resolveForwardRef<T>(type: T): T;
15471547

1548+
// @public
1549+
export interface Resource<T> {
1550+
readonly error: Signal<unknown>;
1551+
hasValue(): this is Resource<T> & {
1552+
value: Signal<T>;
1553+
};
1554+
readonly isLoading: Signal<boolean>;
1555+
reload(): boolean;
1556+
readonly status: Signal<ResourceStatus>;
1557+
readonly value: Signal<T | undefined>;
1558+
}
1559+
1560+
// @public
1561+
export function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T>;
1562+
1563+
// @public
1564+
export type ResourceLoader<T, R> = (param: ResourceLoaderParams<R>) => PromiseLike<T>;
1565+
1566+
// @public
1567+
export interface ResourceLoaderParams<R> {
1568+
// (undocumented)
1569+
abortSignal: AbortSignal;
1570+
// (undocumented)
1571+
previous: {
1572+
status: ResourceStatus;
1573+
};
1574+
// (undocumented)
1575+
request: Exclude<NoInfer<R>, undefined>;
1576+
}
1577+
1578+
// @public
1579+
export interface ResourceOptions<T, R> {
1580+
equal?: ValueEqualityFn<T>;
1581+
injector?: Injector;
1582+
loader: ResourceLoader<T, R>;
1583+
request?: () => R;
1584+
}
1585+
1586+
// @public
1587+
export interface ResourceRef<T> extends WritableResource<T> {
1588+
destroy(): void;
1589+
}
1590+
1591+
// @public
1592+
export enum ResourceStatus {
1593+
Error = 1,
1594+
Idle = 0,
1595+
Loading = 2,
1596+
Local = 5,
1597+
Reloading = 3,
1598+
Resolved = 4
1599+
}
1600+
15481601
// @public
15491602
export function runInInjectionContext<ReturnT>(injector: Injector, fn: () => ReturnT): ReturnT;
15501603

@@ -1877,6 +1930,20 @@ export abstract class ViewRef extends ChangeDetectorRef {
18771930
abstract onDestroy(callback: Function): void;
18781931
}
18791932

1933+
// @public
1934+
export interface WritableResource<T> extends Resource<T> {
1935+
// (undocumented)
1936+
asReadonly(): Resource<T>;
1937+
// (undocumented)
1938+
hasValue(): this is WritableResource<T> & {
1939+
value: WritableSignal<T>;
1940+
};
1941+
set(value: T | undefined): void;
1942+
update(updater: (value: T | undefined) => T | undefined): void;
1943+
// (undocumented)
1944+
readonly value: WritableSignal<T | undefined>;
1945+
}
1946+
18801947
// @public
18811948
export interface WritableSignal<T> extends Signal<T> {
18821949
// (undocumented)

packages/core/src/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export {ErrorHandler} from './error_handler';
8989
export * from './core_private_export';
9090
export * from './core_render3_private_export';
9191
export * from './core_reactivity_export';
92+
export * from './resource';
9293
export {SecurityContext} from './sanitization/security';
9394
export {Sanitizer} from './sanitization/sanitizer';
9495
export {

packages/core/src/resource/api.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Injector} from '../di/injector';
10+
import {Signal, ValueEqualityFn} from '../render3/reactivity/api';
11+
import {WritableSignal} from '../render3/reactivity/signal';
12+
13+
/**
14+
* Status of a `Resource`.
15+
*
16+
* @experimental
17+
*/
18+
export enum ResourceStatus {
19+
/**
20+
* The resource has no valid request and will not perform any loading.
21+
*
22+
* `value()` will be `undefined`.
23+
*/
24+
Idle,
25+
26+
/**
27+
* Loading failed with an error.
28+
*
29+
* `value()` will be `undefined`.
30+
*/
31+
Error,
32+
33+
/**
34+
* The resource is currently loading a new value as a result of a change in its `request`.
35+
*
36+
* `value()` will be `undefined`.
37+
*/
38+
Loading,
39+
40+
/**
41+
* The resource is currently reloading a fresh value for the same request.
42+
*
43+
* `value()` will continue to return the previously fetched value during the reloading operation.
44+
*/
45+
Reloading,
46+
47+
/**
48+
* Loading has completed and the resource has the value returned from the loader.
49+
*/
50+
Resolved,
51+
52+
/**
53+
* The resource's value was set locally via `.set()` or `.update()`.
54+
*/
55+
Local,
56+
}
57+
58+
/**
59+
* A Resource is an asynchronous dependency (for example, the results of an API call) that is
60+
* managed and delivered through signals.
61+
*
62+
* The usual way of creating a `Resource` is through the `resource` function, but various other APIs
63+
* may present `Resource` instances to describe their own concepts.
64+
*
65+
* @experimental
66+
*/
67+
export interface Resource<T> {
68+
/**
69+
* The current value of the `Resource`, or `undefined` if there is no current value.
70+
*/
71+
readonly value: Signal<T | undefined>;
72+
73+
/**
74+
* The current status of the `Resource`, which describes what the resource is currently doing and
75+
* what can be expected of its `value`.
76+
*/
77+
readonly status: Signal<ResourceStatus>;
78+
79+
/**
80+
* When in the `error` state, this returns the last known error from the `Resource`.
81+
*/
82+
readonly error: Signal<unknown>;
83+
84+
/**
85+
* Whether this resource is loading a new value (or reloading the existing one).
86+
*/
87+
readonly isLoading: Signal<boolean>;
88+
89+
/**
90+
* Whether this resource has a valid current value.
91+
*
92+
* This function is reactive.
93+
*/
94+
hasValue(): this is Resource<T> & {value: Signal<T>};
95+
96+
/**
97+
* Instructs the resource to re-load any asynchronous dependency it may have.
98+
*
99+
* Note that the resource will not enter its reloading state until the actual backend request is
100+
* made.
101+
*
102+
* @returns true if a reload was initiated, false if a reload was unnecessary or unsupported
103+
*/
104+
reload(): boolean;
105+
}
106+
107+
/**
108+
* A `Resource` with a mutable value.
109+
*
110+
* Overwriting the value of a resource sets it to the 'local' state.
111+
*
112+
* @experimental
113+
*/
114+
export interface WritableResource<T> extends Resource<T> {
115+
readonly value: WritableSignal<T | undefined>;
116+
hasValue(): this is WritableResource<T> & {value: WritableSignal<T>};
117+
118+
/**
119+
* Convenience wrapper for `value.set`.
120+
*/
121+
set(value: T | undefined): void;
122+
123+
/**
124+
* Convenience wrapper for `value.update`.
125+
*/
126+
update(updater: (value: T | undefined) => T | undefined): void;
127+
asReadonly(): Resource<T>;
128+
}
129+
130+
/**
131+
* A `WritableResource` created through the `resource` function.
132+
*
133+
* @experimental
134+
*/
135+
export interface ResourceRef<T> extends WritableResource<T> {
136+
/**
137+
* Manually destroy the resource, which cancels pending requests and returns it to `idle` state.
138+
*/
139+
destroy(): void;
140+
}
141+
142+
/**
143+
* Parameter to a `ResourceLoader` which gives the request and other options for the current loading
144+
* operation.
145+
*
146+
* @experimental
147+
*/
148+
export interface ResourceLoaderParams<R> {
149+
request: Exclude<NoInfer<R>, undefined>;
150+
abortSignal: AbortSignal;
151+
previous: {
152+
status: ResourceStatus;
153+
};
154+
}
155+
156+
/**
157+
* Loading function for a `Resource`.
158+
*
159+
* @experimental
160+
*/
161+
export type ResourceLoader<T, R> = (param: ResourceLoaderParams<R>) => PromiseLike<T>;
162+
163+
/**
164+
* Options to the `resource` function, for creating a resource.
165+
*
166+
* @experimental
167+
*/
168+
export interface ResourceOptions<T, R> {
169+
/**
170+
* A reactive function which determines the request to be made. Whenever the request changes, the
171+
* loader will be triggered to fetch a new value for the resource.
172+
*
173+
* If a request function isn't provided, the loader won't rerun unless the resource is reloaded.
174+
*/
175+
request?: () => R;
176+
177+
/**
178+
* Loading function which returns a `Promise` of the resource's value for a given request.
179+
*/
180+
loader: ResourceLoader<T, R>;
181+
182+
/**
183+
* Equality function used to compare the return value of the loader.
184+
*/
185+
equal?: ValueEqualityFn<T>;
186+
187+
/**
188+
* Overrides the `Injector` used by `resource`.
189+
*/
190+
injector?: Injector;
191+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export * from './api';
10+
export {resource} from './resource';

0 commit comments

Comments
 (0)