@angular/core 21.0.0-next.9 → 21.0.0-rc.0

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.
Files changed (78) hide show
  1. package/fesm2022/_attribute-chunk.mjs +2 -14
  2. package/fesm2022/_attribute-chunk.mjs.map +1 -1
  3. package/fesm2022/_debug_node-chunk.mjs +15164 -28368
  4. package/fesm2022/_debug_node-chunk.mjs.map +1 -1
  5. package/fesm2022/_effect-chunk.mjs +322 -504
  6. package/fesm2022/_effect-chunk.mjs.map +1 -1
  7. package/fesm2022/_effect-chunk2.mjs +2204 -4061
  8. package/fesm2022/_effect-chunk2.mjs.map +1 -1
  9. package/fesm2022/_not_found-chunk.mjs +18 -35
  10. package/fesm2022/_not_found-chunk.mjs.map +1 -1
  11. package/fesm2022/_resource-chunk.mjs +312 -391
  12. package/fesm2022/_resource-chunk.mjs.map +1 -1
  13. package/fesm2022/_untracked-chunk.mjs +75 -96
  14. package/fesm2022/_untracked-chunk.mjs.map +1 -1
  15. package/fesm2022/_weak_ref-chunk.mjs +2 -4
  16. package/fesm2022/_weak_ref-chunk.mjs.map +1 -1
  17. package/fesm2022/core.mjs +2461 -4305
  18. package/fesm2022/core.mjs.map +1 -1
  19. package/fesm2022/primitives-di.mjs +9 -9
  20. package/fesm2022/primitives-di.mjs.map +1 -1
  21. package/fesm2022/primitives-event-dispatch.mjs +626 -1460
  22. package/fesm2022/primitives-event-dispatch.mjs.map +1 -1
  23. package/fesm2022/primitives-signals.mjs +154 -188
  24. package/fesm2022/primitives-signals.mjs.map +1 -1
  25. package/fesm2022/rxjs-interop.mjs +204 -304
  26. package/fesm2022/rxjs-interop.mjs.map +1 -1
  27. package/fesm2022/testing.mjs +2303 -3162
  28. package/fesm2022/testing.mjs.map +1 -1
  29. package/package.json +8 -2
  30. package/resources/best-practices.md +56 -0
  31. package/schematics/bundles/add-bootstrap-context-to-server-main.cjs +7 -25
  32. package/schematics/bundles/application-config-core.cjs +8 -19
  33. package/schematics/bundles/{apply_import_manager-CoeTX_Ob.cjs → apply_import_manager-1Zs_gpB6.cjs} +4 -5
  34. package/schematics/bundles/bootstrap-options-migration.cjs +93 -132
  35. package/schematics/bundles/cleanup-unused-imports.cjs +9 -13
  36. package/schematics/bundles/common-to-standalone-migration.cjs +12 -16
  37. package/schematics/bundles/{compiler_host-emLDwK2U.cjs → compiler_host-DBwYMlTo.cjs} +10 -11
  38. package/schematics/bundles/control-flow-migration.cjs +29 -31
  39. package/schematics/bundles/{imports-DwPXlGFl.cjs → imports-DP72APSx.cjs} +1 -23
  40. package/schematics/bundles/{index-CLxYZ09c.cjs → index-B7I9sIUx.cjs} +36 -37
  41. package/schematics/bundles/inject-migration.cjs +9 -26
  42. package/schematics/bundles/leading_space-D9nQ8UQC.cjs +1 -1
  43. package/schematics/bundles/{migrate_ts_type_references-CpM5FPGa.cjs → migrate_ts_type_references-UGIUl7En.cjs} +458 -24
  44. package/schematics/bundles/{ng_component_template-BRbBIAUX.cjs → ng_component_template-Dsuq1Lw7.cjs} +4 -5
  45. package/schematics/bundles/{ng_decorators-BI0uV7KI.cjs → ng_decorators-DSFlWYQY.cjs} +2 -2
  46. package/schematics/bundles/ngclass-to-class-migration.cjs +16 -19
  47. package/schematics/bundles/ngstyle-to-style-migration.cjs +15 -18
  48. package/schematics/bundles/nodes-B16H9JUd.cjs +1 -1
  49. package/schematics/bundles/output-migration.cjs +16 -19
  50. package/schematics/bundles/{parse_html-CPWfkfhR.cjs → parse_html-8VLCL37B.cjs} +5 -5
  51. package/schematics/bundles/{project_paths-C8H7KDJ3.cjs → project_paths-DvD50ouC.cjs} +14 -247
  52. package/schematics/bundles/project_tsconfig_paths-CDVxT6Ov.cjs +90 -0
  53. package/schematics/bundles/property_name-BBwFuqMe.cjs +1 -1
  54. package/schematics/bundles/route-lazy-loading.cjs +9 -25
  55. package/schematics/bundles/router-current-navigation.cjs +6 -17
  56. package/schematics/bundles/router-last-successful-navigation.cjs +6 -17
  57. package/schematics/bundles/router-testing-module-migration.cjs +7 -18
  58. package/schematics/bundles/self-closing-tags-migration.cjs +14 -17
  59. package/schematics/bundles/signal-input-migration.cjs +23 -26
  60. package/schematics/bundles/signal-queries-migration.cjs +22 -25
  61. package/schematics/bundles/signals.cjs +10 -13
  62. package/schematics/bundles/standalone-migration.cjs +22 -56
  63. package/schematics/bundles/symbol-BObKoqes.cjs +1 -1
  64. package/types/_api-chunk.d.ts +1 -1
  65. package/types/_chrome_dev_tools_performance-chunk.d.ts +1 -1
  66. package/types/_discovery-chunk.d.ts +7 -9
  67. package/types/_effect-chunk.d.ts +1 -1
  68. package/types/_event_dispatcher-chunk.d.ts +1 -1
  69. package/types/_formatter-chunk.d.ts +1 -1
  70. package/types/_weak_ref-chunk.d.ts +1 -1
  71. package/types/core.d.ts +1 -81
  72. package/types/primitives-di.d.ts +1 -1
  73. package/types/primitives-event-dispatch.d.ts +1 -1
  74. package/types/primitives-signals.d.ts +1 -1
  75. package/types/rxjs-interop.d.ts +1 -1
  76. package/types/testing.d.ts +1 -1
  77. package/schematics/bundles/index-Dvqnp6JS.cjs +0 -22419
  78. package/schematics/bundles/project_tsconfig_paths-CiBzGSIa.cjs +0 -51591
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Angular v21.0.0-next.9
2
+ * @license Angular v21.0.0-rc.0
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
- destroyed = false;
26
- listeners = null;
27
- errorHandler = inject(ErrorHandler, { optional: true });
28
- /** @internal */
29
- destroyRef = inject(DestroyRef);
30
- constructor() {
31
- // Clean-up all listeners and mark as destroyed upon destroy.
32
- this.destroyRef.onDestroy(() => {
33
- this.destroyed = true;
34
- this.listeners = null;
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
- subscribe(callback) {
38
- if (this.destroyed) {
39
- throw new RuntimeError(953 /* RuntimeErrorCode.OUTPUT_REF_DESTROYED */, ngDevMode &&
40
- 'Unexpected subscription to destroyed `OutputRef`. ' +
41
- 'The owning directive/component is destroyed.');
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
- (this.listeners ??= []).push(callback);
44
- return {
45
- unsubscribe: () => {
46
- const idx = this.listeners?.indexOf(callback);
47
- if (idx !== undefined && idx !== -1) {
48
- this.listeners?.splice(idx, 1);
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
- /** Emits a new value to the output. */
54
- emit(value) {
55
- if (this.destroyed) {
56
- console.warn(formatRuntimeError(953 /* RuntimeErrorCode.OUTPUT_REF_DESTROYED */, ngDevMode &&
57
- 'Unexpected emit for destroyed `OutputRef`. ' +
58
- 'The owning directive/component is destroyed.'));
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
- for (const listenerFn of this.listeners) {
67
- try {
68
- listenerFn(value);
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
- return ref.destroyRef;
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
- return untracked$1(nonReactiveReadsFn);
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
- const getter = createComputed(computation, options?.equal);
98
- if (ngDevMode) {
99
- getter.toString = () => `[Computed: ${getter()}]`;
100
- getter[SIGNAL].debugName = options?.debugName;
101
- }
102
- return getter;
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 = (v) => v;
77
+ const identityFn = v => v;
106
78
  function linkedSignal(optionsOrComputation, options) {
107
- if (typeof optionsOrComputation === 'function') {
108
- const getter = createLinkedSignal(optionsOrComputation, (identityFn), options?.equal);
109
- return upgradeLinkedSignalGetter(getter, options?.debugName);
110
- }
111
- else {
112
- const getter = createLinkedSignal(optionsOrComputation.source, optionsOrComputation.computation, optionsOrComputation.equal);
113
- return upgradeLinkedSignalGetter(getter, optionsOrComputation.debugName);
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
- if (ngDevMode) {
118
- getter.toString = () => `[LinkedSignal: ${getter()}]`;
119
- getter[SIGNAL].debugName = debugName;
120
- }
121
- const node = getter[SIGNAL];
122
- const upgradedGetter = getter;
123
- upgradedGetter.set = (newValue) => linkedSignalSetFn(node, newValue);
124
- upgradedGetter.update = (updateFn) => linkedSignalUpdateFn(node, updateFn);
125
- upgradedGetter.asReadonly = signalAsReadonlyFn.bind(getter);
126
- return upgradedGetter;
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
- if (ngDevMode && !options?.injector) {
137
- assertInInjectionContext(resource);
138
- }
139
- const oldNameForParams = options.request;
140
- const params = (options.params ?? oldNameForParams ?? (() => null));
141
- return new ResourceImpl(params, getLoader(options), options.defaultValue, options.equal ? wrapEqualityFn(options.equal) : undefined, options.injector ?? inject(Injector), RESOURCE_VALUE_THROWS_ERRORS_DEFAULT);
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
- value;
148
- constructor(value) {
149
- this.value = value;
150
- this.value.set = this.set.bind(this);
151
- this.value.update = this.update.bind(this);
152
- this.value.asReadonly = signalAsReadonlyFn;
153
- }
154
- isError = computed(() => this.status() === 'error');
155
- update(updateFn) {
156
- this.set(updateFn(untracked(this.value)));
157
- }
158
- isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading');
159
- // Use a computed here to avoid triggering reactive consumers if the value changes while staying
160
- // either defined or undefined.
161
- isValueDefined = computed(() => {
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
- loaderFn;
180
- equal;
181
- pendingTasks;
182
- /**
183
- * The current state of the resource. Status, value, and error are derived from this.
184
- */
185
- state;
186
- /**
187
- * Combines the current request with a reload counter which allows the resource to be reloaded on
188
- * imperative command.
189
- */
190
- extRequest;
191
- effectRef;
192
- pendingController;
193
- resolvePendingTask = undefined;
194
- destroyed = false;
195
- unregisterOnDestroy;
196
- constructor(request, loaderFn, defaultValue, equal, injector, throwErrorsFromValue = RESOURCE_VALUE_THROWS_ERRORS_DEFAULT) {
197
- super(
198
- // Feed a computed signal for the value to `BaseWritableResource`, which will upgrade it to a
199
- // `WritableSignal` that delegates to `ResourceImpl.set`.
200
- computed(() => {
201
- const streamValue = this.state().stream?.();
202
- if (!streamValue) {
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
- const error = untracked(this.error);
276
- const state = untracked(this.state);
277
- if (!error) {
278
- const current = untracked(this.value);
279
- if (state.status === 'local' &&
280
- (this.equal ? this.equal(current, value) : current === value)) {
281
- return;
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
- // Enter Local state with the user-defined value.
285
- this.state.set({
286
- extRequest: state.extRequest,
287
- status: 'local',
288
- previousStatus: 'local',
289
- stream: signal({ value }),
290
- });
291
- // We're departing from whatever state the resource was in previously, so cancel any in-progress
292
- // loading operations.
293
- this.abortInProgressLoad();
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
- reload() {
296
- // We don't want to restart in-progress loads.
297
- const { status } = untracked(this.state);
298
- if (status === 'idle' || status === 'loading') {
299
- return false;
300
- }
301
- // Increment the request reload to trigger the `state` linked signal to switch us to `Reload`
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
- destroy() {
306
- this.destroyed = true;
307
- this.unregisterOnDestroy();
308
- this.effectRef.destroy();
309
- this.abortInProgressLoad();
310
- // Destroyed resources enter Idle state.
311
- this.state.set({
312
- extRequest: { request: undefined, reload: 0 },
313
- status: 'idle',
314
- previousStatus: 'idle',
315
- stream: undefined,
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
- async loadEffect() {
319
- const extRequest = this.extRequest();
320
- // Capture the previous status before any state transitions. Note that this is `untracked` since
321
- // we do not want the effect to depend on the state of the resource, only on the request.
322
- const { status: currentStatus, previousStatus } = untracked(this.state);
323
- if (extRequest.request === undefined) {
324
- // Nothing to load (and we should already be in a non-loading state).
325
- return;
326
- }
327
- else if (currentStatus !== 'loading') {
328
- // We're not in a loading or reloading state, so this loading request is stale.
329
- return;
330
- }
331
- // Cancel any previous loading attempts.
332
- this.abortInProgressLoad();
333
- // Capturing _this_ load's pending task in a local variable is important here. We may attempt to
334
- // resolve it twice:
335
- //
336
- // 1. when the loading function promise resolves/rejects
337
- // 2. when cancelling the loading operation
338
- //
339
- // After the loading operation is cancelled, `this.resolvePendingTask` no longer represents this
340
- // particular task, but this `await` may eventually resolve/reject. Thus, when we cancel in
341
- // response to (1) below, we need to cancel the locally saved task.
342
- let resolvePendingTask = (this.resolvePendingTask =
343
- this.pendingTasks.add());
344
- const { signal: abortSignal } = (this.pendingController = new AbortController());
345
- try {
346
- // The actual loading is run through `untracked` - only the request side of `resource` is
347
- // reactive. This avoids any confusion with signals tracking or not tracking depending on
348
- // which side of the `await` they are.
349
- const stream = await untracked(() => {
350
- return this.loaderFn({
351
- params: extRequest.request,
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
- untracked(() => this.pendingController?.abort());
391
- this.pendingController = undefined;
392
- // Once the load is aborted, we no longer want to block stability on its resolution.
393
- this.resolvePendingTask?.();
394
- this.resolvePendingTask = undefined;
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
- return (a, b) => (a === undefined || b === undefined ? a === b : equal(a, b));
322
+ return (a, b) => a === undefined || b === undefined ? a === b : equal(a, b);
402
323
  }
403
324
  function getLoader(options) {
404
- if (isStreamingResourceOptions(options)) {
405
- return options.stream;
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
- return async (params) => {
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
- return !!options.stream;
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
- switch (state.status) {
424
- case 'loading':
425
- return state.extRequest.reload === 0 ? 'loading' : 'reloading';
426
- case 'resolved':
427
- return isResolved(state.stream()) ? 'resolved' : 'error';
428
- default:
429
- return state.status;
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
- return state.error === undefined;
354
+ return state.error === undefined;
434
355
  }
435
356
  function encapsulateResourceError(error) {
436
- if (error instanceof Error) {
437
- return error;
438
- }
439
- return new ResourceWrappedError(error);
357
+ if (error instanceof Error) {
358
+ return error;
359
+ }
360
+ return new ResourceWrappedError(error);
440
361
  }
441
362
  class ResourceValueError extends Error {
442
- constructor(error) {
443
- super(ngDevMode
444
- ? `Resource is currently in an error state (see Error.cause for details): ${error.message}`
445
- : error.message, { cause: error });
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
- constructor(error) {
450
- super(ngDevMode
451
- ? `Resource returned an error that's not an Error instance: ${String(error)}. Check this error's .cause for the actual error.`
452
- : String(error), { cause: error });
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 };