@angular/core 21.0.0-next.9 → 21.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/_attribute-chunk.mjs +2 -14
- package/fesm2022/_attribute-chunk.mjs.map +1 -1
- package/fesm2022/_debug_node-chunk.mjs +15265 -28386
- package/fesm2022/_debug_node-chunk.mjs.map +1 -1
- package/fesm2022/_effect-chunk.mjs +322 -504
- package/fesm2022/_effect-chunk.mjs.map +1 -1
- package/fesm2022/_effect-chunk2.mjs +2200 -4068
- package/fesm2022/_effect-chunk2.mjs.map +1 -1
- package/fesm2022/_not_found-chunk.mjs +18 -35
- package/fesm2022/_not_found-chunk.mjs.map +1 -1
- package/fesm2022/_resource-chunk.mjs +312 -391
- package/fesm2022/_resource-chunk.mjs.map +1 -1
- package/fesm2022/_untracked-chunk.mjs +75 -96
- package/fesm2022/_untracked-chunk.mjs.map +1 -1
- package/fesm2022/_weak_ref-chunk.mjs +2 -4
- package/fesm2022/_weak_ref-chunk.mjs.map +1 -1
- package/fesm2022/core.mjs +2463 -4307
- package/fesm2022/core.mjs.map +1 -1
- package/fesm2022/primitives-di.mjs +9 -9
- package/fesm2022/primitives-di.mjs.map +1 -1
- package/fesm2022/primitives-event-dispatch.mjs +626 -1460
- package/fesm2022/primitives-event-dispatch.mjs.map +1 -1
- package/fesm2022/primitives-signals.mjs +154 -188
- package/fesm2022/primitives-signals.mjs.map +1 -1
- package/fesm2022/rxjs-interop.mjs +204 -304
- package/fesm2022/rxjs-interop.mjs.map +1 -1
- package/fesm2022/testing.mjs +2303 -3162
- package/fesm2022/testing.mjs.map +1 -1
- package/package.json +8 -2
- package/resources/best-practices.md +56 -0
- package/schematics/bundles/add-bootstrap-context-to-server-main.cjs +7 -25
- package/schematics/bundles/application-config-core.cjs +8 -19
- package/schematics/bundles/{apply_import_manager-CoeTX_Ob.cjs → apply_import_manager-1Zs_gpB6.cjs} +4 -5
- package/schematics/bundles/bootstrap-options-migration.cjs +93 -132
- package/schematics/bundles/cleanup-unused-imports.cjs +9 -13
- package/schematics/bundles/common-to-standalone-migration.cjs +12 -16
- package/schematics/bundles/{compiler_host-emLDwK2U.cjs → compiler_host-DBwYMlTo.cjs} +10 -11
- package/schematics/bundles/control-flow-migration.cjs +29 -31
- package/schematics/bundles/{imports-DwPXlGFl.cjs → imports-DP72APSx.cjs} +1 -23
- package/schematics/bundles/{index-CLxYZ09c.cjs → index-B7I9sIUx.cjs} +36 -37
- package/schematics/bundles/inject-migration.cjs +9 -26
- package/schematics/bundles/leading_space-D9nQ8UQC.cjs +1 -1
- package/schematics/bundles/{migrate_ts_type_references-CpM5FPGa.cjs → migrate_ts_type_references-UGIUl7En.cjs} +458 -24
- package/schematics/bundles/{ng_component_template-BRbBIAUX.cjs → ng_component_template-Dsuq1Lw7.cjs} +4 -5
- package/schematics/bundles/{ng_decorators-BI0uV7KI.cjs → ng_decorators-DSFlWYQY.cjs} +2 -2
- package/schematics/bundles/ngclass-to-class-migration.cjs +16 -19
- package/schematics/bundles/ngstyle-to-style-migration.cjs +15 -18
- package/schematics/bundles/nodes-B16H9JUd.cjs +1 -1
- package/schematics/bundles/output-migration.cjs +16 -19
- package/schematics/bundles/{parse_html-CPWfkfhR.cjs → parse_html-8VLCL37B.cjs} +5 -5
- package/schematics/bundles/{project_paths-C8H7KDJ3.cjs → project_paths-DvD50ouC.cjs} +14 -247
- package/schematics/bundles/project_tsconfig_paths-CDVxT6Ov.cjs +90 -0
- package/schematics/bundles/property_name-BBwFuqMe.cjs +1 -1
- package/schematics/bundles/route-lazy-loading.cjs +9 -25
- package/schematics/bundles/router-current-navigation.cjs +6 -17
- package/schematics/bundles/router-last-successful-navigation.cjs +6 -17
- package/schematics/bundles/router-testing-module-migration.cjs +7 -18
- package/schematics/bundles/self-closing-tags-migration.cjs +14 -17
- package/schematics/bundles/signal-input-migration.cjs +23 -26
- package/schematics/bundles/signal-queries-migration.cjs +22 -25
- package/schematics/bundles/signals.cjs +10 -13
- package/schematics/bundles/standalone-migration.cjs +22 -56
- package/schematics/bundles/symbol-BObKoqes.cjs +1 -1
- package/types/_api-chunk.d.ts +1 -1
- package/types/_chrome_dev_tools_performance-chunk.d.ts +34 -28
- package/types/_discovery-chunk.d.ts +26 -15
- package/types/_effect-chunk.d.ts +1 -1
- package/types/_event_dispatcher-chunk.d.ts +1 -1
- package/types/_formatter-chunk.d.ts +1 -1
- package/types/_weak_ref-chunk.d.ts +1 -1
- package/types/core.d.ts +18 -88
- package/types/primitives-di.d.ts +1 -1
- package/types/primitives-event-dispatch.d.ts +1 -1
- package/types/primitives-signals.d.ts +1 -1
- package/types/rxjs-interop.d.ts +1 -1
- package/types/testing.d.ts +1 -1
- package/schematics/bundles/index-Dvqnp6JS.cjs +0 -22419
- package/schematics/bundles/project_tsconfig_paths-CiBzGSIa.cjs +0 -51591
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @license Angular v21.0.0-
|
|
2
|
+
* @license Angular v21.0.0-rc.1
|
|
3
3
|
* (c) 2010-2025 Google LLC. https://angular.dev/
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -8,449 +8,370 @@ import { inject, ErrorHandler, DestroyRef, RuntimeError, formatRuntimeError, sig
|
|
|
8
8
|
import { setActiveConsumer, createComputed, SIGNAL } from './_effect-chunk.mjs';
|
|
9
9
|
import { untracked as untracked$1, createLinkedSignal, linkedSignalSetFn, linkedSignalUpdateFn } from './_untracked-chunk.mjs';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* An `OutputEmitterRef` is created by the `output()` function and can be
|
|
13
|
-
* used to emit values to consumers of your directive or component.
|
|
14
|
-
*
|
|
15
|
-
* Consumers of your directive/component can bind to the output and
|
|
16
|
-
* subscribe to changes via the bound event syntax. For example:
|
|
17
|
-
*
|
|
18
|
-
* ```html
|
|
19
|
-
* <my-comp (valueChange)="processNewValue($event)" />
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @publicAPI
|
|
23
|
-
*/
|
|
24
11
|
class OutputEmitterRef {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
12
|
+
destroyed = false;
|
|
13
|
+
listeners = null;
|
|
14
|
+
errorHandler = inject(ErrorHandler, {
|
|
15
|
+
optional: true
|
|
16
|
+
});
|
|
17
|
+
destroyRef = inject(DestroyRef);
|
|
18
|
+
constructor() {
|
|
19
|
+
this.destroyRef.onDestroy(() => {
|
|
20
|
+
this.destroyed = true;
|
|
21
|
+
this.listeners = null;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
subscribe(callback) {
|
|
25
|
+
if (this.destroyed) {
|
|
26
|
+
throw new RuntimeError(953, ngDevMode && 'Unexpected subscription to destroyed `OutputRef`. ' + 'The owning directive/component is destroyed.');
|
|
36
27
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
(this.listeners ??= []).push(callback);
|
|
29
|
+
return {
|
|
30
|
+
unsubscribe: () => {
|
|
31
|
+
const idx = this.listeners?.indexOf(callback);
|
|
32
|
+
if (idx !== undefined && idx !== -1) {
|
|
33
|
+
this.listeners?.splice(idx, 1);
|
|
42
34
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
emit(value) {
|
|
39
|
+
if (this.destroyed) {
|
|
40
|
+
console.warn(formatRuntimeError(953, ngDevMode && 'Unexpected emit for destroyed `OutputRef`. ' + 'The owning directive/component is destroyed.'));
|
|
41
|
+
return;
|
|
52
42
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (this.listeners === null) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const previousConsumer = setActiveConsumer(null);
|
|
43
|
+
if (this.listeners === null) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const previousConsumer = setActiveConsumer(null);
|
|
47
|
+
try {
|
|
48
|
+
for (const listenerFn of this.listeners) {
|
|
65
49
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
this.errorHandler?.handleError(err);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
setActiveConsumer(previousConsumer);
|
|
50
|
+
listenerFn(value);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
this.errorHandler?.handleError(err);
|
|
77
53
|
}
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
setActiveConsumer(previousConsumer);
|
|
78
57
|
}
|
|
58
|
+
}
|
|
79
59
|
}
|
|
80
|
-
/** Gets the owning `DestroyRef` for the given output. */
|
|
81
60
|
function getOutputDestroyRef(ref) {
|
|
82
|
-
|
|
61
|
+
return ref.destroyRef;
|
|
83
62
|
}
|
|
84
63
|
|
|
85
|
-
/**
|
|
86
|
-
* Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function
|
|
87
|
-
* can, optionally, return a value.
|
|
88
|
-
*/
|
|
89
64
|
function untracked(nonReactiveReadsFn) {
|
|
90
|
-
|
|
65
|
+
return untracked$1(nonReactiveReadsFn);
|
|
91
66
|
}
|
|
92
67
|
|
|
93
|
-
/**
|
|
94
|
-
* Create a computed `Signal` which derives a reactive value from an expression.
|
|
95
|
-
*/
|
|
96
68
|
function computed(computation, options) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
69
|
+
const getter = createComputed(computation, options?.equal);
|
|
70
|
+
if (ngDevMode) {
|
|
71
|
+
getter.toString = () => `[Computed: ${getter()}]`;
|
|
72
|
+
getter[SIGNAL].debugName = options?.debugName;
|
|
73
|
+
}
|
|
74
|
+
return getter;
|
|
103
75
|
}
|
|
104
76
|
|
|
105
|
-
const identityFn =
|
|
77
|
+
const identityFn = v => v;
|
|
106
78
|
function linkedSignal(optionsOrComputation, options) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
79
|
+
if (typeof optionsOrComputation === 'function') {
|
|
80
|
+
const getter = createLinkedSignal(optionsOrComputation, identityFn, options?.equal);
|
|
81
|
+
return upgradeLinkedSignalGetter(getter, options?.debugName);
|
|
82
|
+
} else {
|
|
83
|
+
const getter = createLinkedSignal(optionsOrComputation.source, optionsOrComputation.computation, optionsOrComputation.equal);
|
|
84
|
+
return upgradeLinkedSignalGetter(getter, optionsOrComputation.debugName);
|
|
85
|
+
}
|
|
115
86
|
}
|
|
116
87
|
function upgradeLinkedSignalGetter(getter, debugName) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
88
|
+
if (ngDevMode) {
|
|
89
|
+
getter.toString = () => `[LinkedSignal: ${getter()}]`;
|
|
90
|
+
getter[SIGNAL].debugName = debugName;
|
|
91
|
+
}
|
|
92
|
+
const node = getter[SIGNAL];
|
|
93
|
+
const upgradedGetter = getter;
|
|
94
|
+
upgradedGetter.set = newValue => linkedSignalSetFn(node, newValue);
|
|
95
|
+
upgradedGetter.update = updateFn => linkedSignalUpdateFn(node, updateFn);
|
|
96
|
+
upgradedGetter.asReadonly = signalAsReadonlyFn.bind(getter);
|
|
97
|
+
return upgradedGetter;
|
|
127
98
|
}
|
|
128
99
|
|
|
129
|
-
/**
|
|
130
|
-
* Whether a `Resource.value()` should throw an error when the resource is in the error state.
|
|
131
|
-
*
|
|
132
|
-
* This internal flag is being used to gradually roll out this behavior.
|
|
133
|
-
*/
|
|
134
100
|
let RESOURCE_VALUE_THROWS_ERRORS_DEFAULT = true;
|
|
135
101
|
function resource(options) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
102
|
+
if (ngDevMode && !options?.injector) {
|
|
103
|
+
assertInInjectionContext(resource);
|
|
104
|
+
}
|
|
105
|
+
const oldNameForParams = options.request;
|
|
106
|
+
const params = options.params ?? oldNameForParams ?? (() => null);
|
|
107
|
+
return new ResourceImpl(params, getLoader(options), options.defaultValue, options.equal ? wrapEqualityFn(options.equal) : undefined, options.injector ?? inject(Injector), RESOURCE_VALUE_THROWS_ERRORS_DEFAULT);
|
|
142
108
|
}
|
|
143
|
-
/**
|
|
144
|
-
* Base class which implements `.value` as a `WritableSignal` by delegating `.set` and `.update`.
|
|
145
|
-
*/
|
|
146
109
|
class BaseWritableResource {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Check if it's in an error state first to prevent the error from bubbling up.
|
|
163
|
-
if (this.isError()) {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
return this.value() !== undefined;
|
|
167
|
-
});
|
|
168
|
-
hasValue() {
|
|
169
|
-
return this.isValueDefined();
|
|
170
|
-
}
|
|
171
|
-
asReadonly() {
|
|
172
|
-
return this;
|
|
110
|
+
value;
|
|
111
|
+
constructor(value) {
|
|
112
|
+
this.value = value;
|
|
113
|
+
this.value.set = this.set.bind(this);
|
|
114
|
+
this.value.update = this.update.bind(this);
|
|
115
|
+
this.value.asReadonly = signalAsReadonlyFn;
|
|
116
|
+
}
|
|
117
|
+
isError = computed(() => this.status() === 'error');
|
|
118
|
+
update(updateFn) {
|
|
119
|
+
this.set(updateFn(untracked(this.value)));
|
|
120
|
+
}
|
|
121
|
+
isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading');
|
|
122
|
+
isValueDefined = computed(() => {
|
|
123
|
+
if (this.isError()) {
|
|
124
|
+
return false;
|
|
173
125
|
}
|
|
126
|
+
return this.value() !== undefined;
|
|
127
|
+
});
|
|
128
|
+
hasValue() {
|
|
129
|
+
return this.isValueDefined();
|
|
130
|
+
}
|
|
131
|
+
asReadonly() {
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
174
134
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Implementation for `resource()` which uses a `linkedSignal` to manage the resource's state.
|
|
177
|
-
*/
|
|
178
135
|
class ResourceImpl extends BaseWritableResource {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return defaultValue;
|
|
204
|
-
}
|
|
205
|
-
// Prevents `hasValue()` from throwing an error when a reload happened in the error state
|
|
206
|
-
if (this.state().status === 'loading' && this.error()) {
|
|
207
|
-
return defaultValue;
|
|
208
|
-
}
|
|
209
|
-
if (!isResolved(streamValue)) {
|
|
210
|
-
if (throwErrorsFromValue) {
|
|
211
|
-
throw new ResourceValueError(this.error());
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
return defaultValue;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return streamValue.value;
|
|
218
|
-
}, { equal }));
|
|
219
|
-
this.loaderFn = loaderFn;
|
|
220
|
-
this.equal = equal;
|
|
221
|
-
// Extend `request()` to include a writable reload signal.
|
|
222
|
-
this.extRequest = linkedSignal({
|
|
223
|
-
source: request,
|
|
224
|
-
computation: (request) => ({ request, reload: 0 }),
|
|
225
|
-
});
|
|
226
|
-
// The main resource state is managed in a `linkedSignal`, which allows the resource to change
|
|
227
|
-
// state instantaneously when the request signal changes.
|
|
228
|
-
this.state = linkedSignal({
|
|
229
|
-
// Whenever the request changes,
|
|
230
|
-
source: this.extRequest,
|
|
231
|
-
// Compute the state of the resource given a change in status.
|
|
232
|
-
computation: (extRequest, previous) => {
|
|
233
|
-
const status = extRequest.request === undefined ? 'idle' : 'loading';
|
|
234
|
-
if (!previous) {
|
|
235
|
-
return {
|
|
236
|
-
extRequest,
|
|
237
|
-
status,
|
|
238
|
-
previousStatus: 'idle',
|
|
239
|
-
stream: undefined,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
return {
|
|
244
|
-
extRequest,
|
|
245
|
-
status,
|
|
246
|
-
previousStatus: projectStatusOfState(previous.value),
|
|
247
|
-
// If the request hasn't changed, keep the previous stream.
|
|
248
|
-
stream: previous.value.extRequest.request === extRequest.request
|
|
249
|
-
? previous.value.stream
|
|
250
|
-
: undefined,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
this.effectRef = effect(this.loadEffect.bind(this), {
|
|
256
|
-
injector,
|
|
257
|
-
manualCleanup: true,
|
|
258
|
-
});
|
|
259
|
-
this.pendingTasks = injector.get(PendingTasks);
|
|
260
|
-
// Cancel any pending request when the resource itself is destroyed.
|
|
261
|
-
this.unregisterOnDestroy = injector.get(DestroyRef).onDestroy(() => this.destroy());
|
|
262
|
-
}
|
|
263
|
-
status = computed(() => projectStatusOfState(this.state()));
|
|
264
|
-
error = computed(() => {
|
|
265
|
-
const stream = this.state().stream?.();
|
|
266
|
-
return stream && !isResolved(stream) ? stream.error : undefined;
|
|
267
|
-
});
|
|
268
|
-
/**
|
|
269
|
-
* Called either directly via `WritableResource.set` or via `.value.set()`.
|
|
270
|
-
*/
|
|
271
|
-
set(value) {
|
|
272
|
-
if (this.destroyed) {
|
|
273
|
-
return;
|
|
136
|
+
loaderFn;
|
|
137
|
+
equal;
|
|
138
|
+
pendingTasks;
|
|
139
|
+
state;
|
|
140
|
+
extRequest;
|
|
141
|
+
effectRef;
|
|
142
|
+
pendingController;
|
|
143
|
+
resolvePendingTask = undefined;
|
|
144
|
+
destroyed = false;
|
|
145
|
+
unregisterOnDestroy;
|
|
146
|
+
constructor(request, loaderFn, defaultValue, equal, injector, throwErrorsFromValue = RESOURCE_VALUE_THROWS_ERRORS_DEFAULT) {
|
|
147
|
+
super(computed(() => {
|
|
148
|
+
const streamValue = this.state().stream?.();
|
|
149
|
+
if (!streamValue) {
|
|
150
|
+
return defaultValue;
|
|
151
|
+
}
|
|
152
|
+
if (this.state().status === 'loading' && this.error()) {
|
|
153
|
+
return defaultValue;
|
|
154
|
+
}
|
|
155
|
+
if (!isResolved(streamValue)) {
|
|
156
|
+
if (throwErrorsFromValue) {
|
|
157
|
+
throw new ResourceValueError(this.error());
|
|
158
|
+
} else {
|
|
159
|
+
return defaultValue;
|
|
274
160
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
161
|
+
}
|
|
162
|
+
return streamValue.value;
|
|
163
|
+
}, {
|
|
164
|
+
equal
|
|
165
|
+
}));
|
|
166
|
+
this.loaderFn = loaderFn;
|
|
167
|
+
this.equal = equal;
|
|
168
|
+
this.extRequest = linkedSignal({
|
|
169
|
+
source: request,
|
|
170
|
+
computation: request => ({
|
|
171
|
+
request,
|
|
172
|
+
reload: 0
|
|
173
|
+
})
|
|
174
|
+
});
|
|
175
|
+
this.state = linkedSignal({
|
|
176
|
+
source: this.extRequest,
|
|
177
|
+
computation: (extRequest, previous) => {
|
|
178
|
+
const status = extRequest.request === undefined ? 'idle' : 'loading';
|
|
179
|
+
if (!previous) {
|
|
180
|
+
return {
|
|
181
|
+
extRequest,
|
|
182
|
+
status,
|
|
183
|
+
previousStatus: 'idle',
|
|
184
|
+
stream: undefined
|
|
185
|
+
};
|
|
186
|
+
} else {
|
|
187
|
+
return {
|
|
188
|
+
extRequest,
|
|
189
|
+
status,
|
|
190
|
+
previousStatus: projectStatusOfState(previous.value),
|
|
191
|
+
stream: previous.value.extRequest.request === extRequest.request ? previous.value.stream : undefined
|
|
192
|
+
};
|
|
283
193
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
this.effectRef = effect(this.loadEffect.bind(this), {
|
|
197
|
+
injector,
|
|
198
|
+
manualCleanup: true
|
|
199
|
+
});
|
|
200
|
+
this.pendingTasks = injector.get(PendingTasks);
|
|
201
|
+
this.unregisterOnDestroy = injector.get(DestroyRef).onDestroy(() => this.destroy());
|
|
202
|
+
}
|
|
203
|
+
status = computed(() => projectStatusOfState(this.state()));
|
|
204
|
+
error = computed(() => {
|
|
205
|
+
const stream = this.state().stream?.();
|
|
206
|
+
return stream && !isResolved(stream) ? stream.error : undefined;
|
|
207
|
+
});
|
|
208
|
+
set(value) {
|
|
209
|
+
if (this.destroyed) {
|
|
210
|
+
return;
|
|
294
211
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.extRequest.update(({ request, reload }) => ({ request, reload: reload + 1 }));
|
|
303
|
-
return true;
|
|
212
|
+
const error = untracked(this.error);
|
|
213
|
+
const state = untracked(this.state);
|
|
214
|
+
if (!error) {
|
|
215
|
+
const current = untracked(this.value);
|
|
216
|
+
if (state.status === 'local' && (this.equal ? this.equal(current, value) : current === value)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
304
219
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
220
|
+
this.state.set({
|
|
221
|
+
extRequest: state.extRequest,
|
|
222
|
+
status: 'local',
|
|
223
|
+
previousStatus: 'local',
|
|
224
|
+
stream: signal({
|
|
225
|
+
value
|
|
226
|
+
})
|
|
227
|
+
});
|
|
228
|
+
this.abortInProgressLoad();
|
|
229
|
+
}
|
|
230
|
+
reload() {
|
|
231
|
+
const {
|
|
232
|
+
status
|
|
233
|
+
} = untracked(this.state);
|
|
234
|
+
if (status === 'idle' || status === 'loading') {
|
|
235
|
+
return false;
|
|
317
236
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// TODO(alxhub): cleanup after g3 removal of `request` alias.
|
|
353
|
-
request: extRequest.request,
|
|
354
|
-
abortSignal,
|
|
355
|
-
previous: {
|
|
356
|
-
status: previousStatus,
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
// If this request has been aborted, or the current request no longer
|
|
361
|
-
// matches this load, then we should ignore this resolution.
|
|
362
|
-
if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
this.state.set({
|
|
366
|
-
extRequest,
|
|
367
|
-
status: 'resolved',
|
|
368
|
-
previousStatus: 'resolved',
|
|
369
|
-
stream,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
catch (err) {
|
|
373
|
-
if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
this.state.set({
|
|
377
|
-
extRequest,
|
|
378
|
-
status: 'resolved',
|
|
379
|
-
previousStatus: 'error',
|
|
380
|
-
stream: signal({ error: encapsulateResourceError(err) }),
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
finally {
|
|
384
|
-
// Resolve the pending task now that the resource has a value.
|
|
385
|
-
resolvePendingTask?.();
|
|
386
|
-
resolvePendingTask = undefined;
|
|
387
|
-
}
|
|
237
|
+
this.extRequest.update(({
|
|
238
|
+
request,
|
|
239
|
+
reload
|
|
240
|
+
}) => ({
|
|
241
|
+
request,
|
|
242
|
+
reload: reload + 1
|
|
243
|
+
}));
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
destroy() {
|
|
247
|
+
this.destroyed = true;
|
|
248
|
+
this.unregisterOnDestroy();
|
|
249
|
+
this.effectRef.destroy();
|
|
250
|
+
this.abortInProgressLoad();
|
|
251
|
+
this.state.set({
|
|
252
|
+
extRequest: {
|
|
253
|
+
request: undefined,
|
|
254
|
+
reload: 0
|
|
255
|
+
},
|
|
256
|
+
status: 'idle',
|
|
257
|
+
previousStatus: 'idle',
|
|
258
|
+
stream: undefined
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async loadEffect() {
|
|
262
|
+
const extRequest = this.extRequest();
|
|
263
|
+
const {
|
|
264
|
+
status: currentStatus,
|
|
265
|
+
previousStatus
|
|
266
|
+
} = untracked(this.state);
|
|
267
|
+
if (extRequest.request === undefined) {
|
|
268
|
+
return;
|
|
269
|
+
} else if (currentStatus !== 'loading') {
|
|
270
|
+
return;
|
|
388
271
|
}
|
|
389
|
-
abortInProgressLoad()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
272
|
+
this.abortInProgressLoad();
|
|
273
|
+
let resolvePendingTask = this.resolvePendingTask = this.pendingTasks.add();
|
|
274
|
+
const {
|
|
275
|
+
signal: abortSignal
|
|
276
|
+
} = this.pendingController = new AbortController();
|
|
277
|
+
try {
|
|
278
|
+
const stream = await untracked(() => {
|
|
279
|
+
return this.loaderFn({
|
|
280
|
+
params: extRequest.request,
|
|
281
|
+
request: extRequest.request,
|
|
282
|
+
abortSignal,
|
|
283
|
+
previous: {
|
|
284
|
+
status: previousStatus
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.state.set({
|
|
292
|
+
extRequest,
|
|
293
|
+
status: 'resolved',
|
|
294
|
+
previousStatus: 'resolved',
|
|
295
|
+
stream
|
|
296
|
+
});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.state.set({
|
|
302
|
+
extRequest,
|
|
303
|
+
status: 'resolved',
|
|
304
|
+
previousStatus: 'error',
|
|
305
|
+
stream: signal({
|
|
306
|
+
error: encapsulateResourceError(err)
|
|
307
|
+
})
|
|
308
|
+
});
|
|
309
|
+
} finally {
|
|
310
|
+
resolvePendingTask?.();
|
|
311
|
+
resolvePendingTask = undefined;
|
|
395
312
|
}
|
|
313
|
+
}
|
|
314
|
+
abortInProgressLoad() {
|
|
315
|
+
untracked(() => this.pendingController?.abort());
|
|
316
|
+
this.pendingController = undefined;
|
|
317
|
+
this.resolvePendingTask?.();
|
|
318
|
+
this.resolvePendingTask = undefined;
|
|
319
|
+
}
|
|
396
320
|
}
|
|
397
|
-
/**
|
|
398
|
-
* Wraps an equality function to handle either value being `undefined`.
|
|
399
|
-
*/
|
|
400
321
|
function wrapEqualityFn(equal) {
|
|
401
|
-
|
|
322
|
+
return (a, b) => a === undefined || b === undefined ? a === b : equal(a, b);
|
|
402
323
|
}
|
|
403
324
|
function getLoader(options) {
|
|
404
|
-
|
|
405
|
-
|
|
325
|
+
if (isStreamingResourceOptions(options)) {
|
|
326
|
+
return options.stream;
|
|
327
|
+
}
|
|
328
|
+
return async params => {
|
|
329
|
+
try {
|
|
330
|
+
return signal({
|
|
331
|
+
value: await options.loader(params)
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return signal({
|
|
335
|
+
error: encapsulateResourceError(err)
|
|
336
|
+
});
|
|
406
337
|
}
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
return signal({ value: await options.loader(params) });
|
|
410
|
-
}
|
|
411
|
-
catch (err) {
|
|
412
|
-
return signal({ error: encapsulateResourceError(err) });
|
|
413
|
-
}
|
|
414
|
-
};
|
|
338
|
+
};
|
|
415
339
|
}
|
|
416
340
|
function isStreamingResourceOptions(options) {
|
|
417
|
-
|
|
341
|
+
return !!options.stream;
|
|
418
342
|
}
|
|
419
|
-
/**
|
|
420
|
-
* Project from a state with `ResourceInternalStatus` to the user-facing `ResourceStatus`
|
|
421
|
-
*/
|
|
422
343
|
function projectStatusOfState(state) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
344
|
+
switch (state.status) {
|
|
345
|
+
case 'loading':
|
|
346
|
+
return state.extRequest.reload === 0 ? 'loading' : 'reloading';
|
|
347
|
+
case 'resolved':
|
|
348
|
+
return isResolved(state.stream()) ? 'resolved' : 'error';
|
|
349
|
+
default:
|
|
350
|
+
return state.status;
|
|
351
|
+
}
|
|
431
352
|
}
|
|
432
353
|
function isResolved(state) {
|
|
433
|
-
|
|
354
|
+
return state.error === undefined;
|
|
434
355
|
}
|
|
435
356
|
function encapsulateResourceError(error) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
357
|
+
if (error instanceof Error) {
|
|
358
|
+
return error;
|
|
359
|
+
}
|
|
360
|
+
return new ResourceWrappedError(error);
|
|
440
361
|
}
|
|
441
362
|
class ResourceValueError extends Error {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
363
|
+
constructor(error) {
|
|
364
|
+
super(ngDevMode ? `Resource is currently in an error state (see Error.cause for details): ${error.message}` : error.message, {
|
|
365
|
+
cause: error
|
|
366
|
+
});
|
|
367
|
+
}
|
|
447
368
|
}
|
|
448
369
|
class ResourceWrappedError extends Error {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
370
|
+
constructor(error) {
|
|
371
|
+
super(ngDevMode ? `Resource returned an error that's not an Error instance: ${String(error)}. Check this error's .cause for the actual error.` : String(error), {
|
|
372
|
+
cause: error
|
|
373
|
+
});
|
|
374
|
+
}
|
|
454
375
|
}
|
|
455
376
|
|
|
456
377
|
export { OutputEmitterRef, ResourceImpl, computed, encapsulateResourceError, getOutputDestroyRef, linkedSignal, resource, untracked };
|