@angular/core 21.0.0-next.8 → 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 (84) 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 +15214 -28375
  4. package/fesm2022/_debug_node-chunk.mjs.map +1 -1
  5. package/fesm2022/_effect-chunk.mjs +402 -120
  6. package/fesm2022/_effect-chunk.mjs.map +1 -1
  7. package/fesm2022/_effect-chunk2.mjs +2951 -0
  8. package/fesm2022/_effect-chunk2.mjs.map +1 -0
  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 +316 -563
  12. package/fesm2022/_resource-chunk.mjs.map +1 -1
  13. package/fesm2022/_untracked-chunk.mjs +96 -0
  14. package/fesm2022/_untracked-chunk.mjs.map +1 -0
  15. package/fesm2022/_weak_ref-chunk.mjs +2 -4
  16. package/fesm2022/_weak_ref-chunk.mjs.map +1 -1
  17. package/fesm2022/core.mjs +2466 -4309
  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 +157 -191
  24. package/fesm2022/primitives-signals.mjs.map +1 -1
  25. package/fesm2022/rxjs-interop.mjs +208 -308
  26. package/fesm2022/rxjs-interop.mjs.map +1 -1
  27. package/fesm2022/testing.mjs +2305 -3164
  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-CBLmogDD.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 +381 -0
  37. package/schematics/bundles/{compiler_host-T6xncpiw.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-DWSaRJdz.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-Cu-FR4L5.cjs → migrate_ts_type_references-UGIUl7En.cjs} +458 -24
  44. package/schematics/bundles/{ng_component_template-BkWiUuGG.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-C97tKKp3.cjs → parse_html-8VLCL37B.cjs} +5 -5
  51. package/schematics/bundles/{project_paths-C6g3lqjX.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/schematics/collection.json +6 -0
  65. package/schematics/migrations/common-to-standalone-migration/schema.json +14 -0
  66. package/types/_api-chunk.d.ts +1 -1
  67. package/types/_chrome_dev_tools_performance-chunk.d.ts +20 -12
  68. package/types/_discovery-chunk.d.ts +18 -14
  69. package/types/_effect-chunk.d.ts +1 -1
  70. package/types/_event_dispatcher-chunk.d.ts +1 -1
  71. package/types/_formatter-chunk.d.ts +4 -3
  72. package/types/_weak_ref-chunk.d.ts +1 -1
  73. package/types/core.d.ts +49 -100
  74. package/types/primitives-di.d.ts +1 -1
  75. package/types/primitives-event-dispatch.d.ts +1 -1
  76. package/types/primitives-signals.d.ts +2 -2
  77. package/types/rxjs-interop.d.ts +1 -1
  78. package/types/testing.d.ts +1 -1
  79. package/fesm2022/_root_effect_scheduler-chunk.mjs +0 -4630
  80. package/fesm2022/_root_effect_scheduler-chunk.mjs.map +0 -1
  81. package/fesm2022/_signal-chunk.mjs +0 -581
  82. package/fesm2022/_signal-chunk.mjs.map +0 -1
  83. package/schematics/bundles/index-BnmACOsq.cjs +0 -22319
  84. package/schematics/bundles/project_tsconfig_paths-CdhVNYMk.cjs +0 -51583
@@ -1,625 +1,378 @@
1
1
  /**
2
- * @license Angular v21.0.0-next.8
2
+ * @license Angular v21.0.0-rc.0
3
3
  * (c) 2010-2025 Google LLC. https://angular.dev/
4
4
  * License: MIT
5
5
  */
6
6
 
7
- import { inject, ErrorHandler, DestroyRef, RuntimeError, formatRuntimeError, assertNotInReactiveContext, assertInInjectionContext, Injector, ViewContext, ChangeDetectionScheduler, EffectScheduler, setInjectorProfilerContext, emitEffectCreatedEvent, EFFECTS, NodeInjectorDestroyRef, FLAGS, markAncestorsForTraversal, noop, setIsRefreshingViews, signalAsReadonlyFn, PendingTasks, signal } from './_root_effect_scheduler-chunk.mjs';
8
- import { setActiveConsumer, createComputed, SIGNAL, consumerDestroy, isInNotificationPhase } from './_signal-chunk.mjs';
9
- import { untracked as untracked$1, BASE_EFFECT_NODE, runEffect, createLinkedSignal, linkedSignalSetFn, linkedSignalUpdateFn } from './_effect-chunk.mjs';
7
+ import { inject, ErrorHandler, DestroyRef, RuntimeError, formatRuntimeError, signalAsReadonlyFn, assertInInjectionContext, Injector, effect, PendingTasks, signal } from './_effect-chunk2.mjs';
8
+ import { setActiveConsumer, createComputed, SIGNAL } from './_effect-chunk.mjs';
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
- class EffectRefImpl {
106
- [SIGNAL];
107
- constructor(node) {
108
- this[SIGNAL] = node;
109
- }
110
- destroy() {
111
- this[SIGNAL].destroy();
112
- }
113
- }
114
- /**
115
- * Registers an "effect" that will be scheduled & executed whenever the signals that it reads
116
- * changes.
117
- *
118
- * Angular has two different kinds of effect: component effects and root effects. Component effects
119
- * are created when `effect()` is called from a component, directive, or within a service of a
120
- * component/directive. Root effects are created when `effect()` is called from outside the
121
- * component tree, such as in a root service.
122
- *
123
- * The two effect types differ in their timing. Component effects run as a component lifecycle
124
- * event during Angular's synchronization (change detection) process, and can safely read input
125
- * signals or create/destroy views that depend on component state. Root effects run as microtasks
126
- * and have no connection to the component tree or change detection.
127
- *
128
- * `effect()` must be run in injection context, unless the `injector` option is manually specified.
129
- *
130
- * @publicApi 20.0
131
- */
132
- function effect(effectFn, options) {
133
- ngDevMode &&
134
- assertNotInReactiveContext(effect, 'Call `effect` outside of a reactive context. For example, schedule the ' +
135
- 'effect inside the component constructor.');
136
- if (ngDevMode && !options?.injector) {
137
- assertInInjectionContext(effect);
138
- }
139
- if (ngDevMode && options?.allowSignalWrites !== undefined) {
140
- console.warn(`The 'allowSignalWrites' flag is deprecated and no longer impacts effect() (writes are always allowed)`);
141
- }
142
- const injector = options?.injector ?? inject(Injector);
143
- let destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null;
144
- let node;
145
- const viewContext = injector.get(ViewContext, null, { optional: true });
146
- const notifier = injector.get(ChangeDetectionScheduler);
147
- if (viewContext !== null) {
148
- // This effect was created in the context of a view, and will be associated with the view.
149
- node = createViewEffect(viewContext.view, notifier, effectFn);
150
- if (destroyRef instanceof NodeInjectorDestroyRef && destroyRef._lView === viewContext.view) {
151
- // The effect is being created in the same view as the `DestroyRef` references, so it will be
152
- // automatically destroyed without the need for an explicit `DestroyRef` registration.
153
- destroyRef = null;
154
- }
155
- }
156
- else {
157
- // This effect was created outside the context of a view, and will be scheduled independently.
158
- node = createRootEffect(effectFn, injector.get(EffectScheduler), notifier);
159
- }
160
- node.injector = injector;
161
- if (destroyRef !== null) {
162
- // If we need to register for cleanup, do that here.
163
- node.onDestroyFn = destroyRef.onDestroy(() => node.destroy());
164
- }
165
- const effectRef = new EffectRefImpl(node);
166
- if (ngDevMode) {
167
- node.debugName = options?.debugName ?? '';
168
- const prevInjectorProfilerContext = setInjectorProfilerContext({ injector, token: null });
169
- try {
170
- emitEffectCreatedEvent(effectRef);
171
- }
172
- finally {
173
- setInjectorProfilerContext(prevInjectorProfilerContext);
174
- }
175
- }
176
- return effectRef;
177
- }
178
- const EFFECT_NODE =
179
- /* @__PURE__ */ (() => ({
180
- ...BASE_EFFECT_NODE,
181
- cleanupFns: undefined,
182
- zone: null,
183
- onDestroyFn: noop,
184
- run() {
185
- if (ngDevMode && isInNotificationPhase()) {
186
- throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`);
187
- }
188
- // We clear `setIsRefreshingViews` so that `markForCheck()` within the body of an effect will
189
- // cause CD to reach the component in question.
190
- const prevRefreshingViews = setIsRefreshingViews(false);
191
- try {
192
- runEffect(this);
193
- }
194
- finally {
195
- setIsRefreshingViews(prevRefreshingViews);
196
- }
197
- },
198
- cleanup() {
199
- if (!this.cleanupFns?.length) {
200
- return;
201
- }
202
- const prevConsumer = setActiveConsumer(null);
203
- try {
204
- // Attempt to run the cleanup functions. Regardless of failure or success, we consider
205
- // cleanup "completed" and clear the list for the next run of the effect. Note that an error
206
- // from the cleanup function will still crash the current run of the effect.
207
- while (this.cleanupFns.length) {
208
- this.cleanupFns.pop()();
209
- }
210
- }
211
- finally {
212
- this.cleanupFns = [];
213
- setActiveConsumer(prevConsumer);
214
- }
215
- },
216
- }))();
217
- const ROOT_EFFECT_NODE =
218
- /* @__PURE__ */ (() => ({
219
- ...EFFECT_NODE,
220
- consumerMarkedDirty() {
221
- this.scheduler.schedule(this);
222
- this.notifier.notify(12 /* NotificationSource.RootEffect */);
223
- },
224
- destroy() {
225
- consumerDestroy(this);
226
- this.onDestroyFn();
227
- this.cleanup();
228
- this.scheduler.remove(this);
229
- },
230
- }))();
231
- const VIEW_EFFECT_NODE =
232
- /* @__PURE__ */ (() => ({
233
- ...EFFECT_NODE,
234
- consumerMarkedDirty() {
235
- this.view[FLAGS] |= 8192 /* LViewFlags.HasChildViewsToRefresh */;
236
- markAncestorsForTraversal(this.view);
237
- this.notifier.notify(13 /* NotificationSource.ViewEffect */);
238
- },
239
- destroy() {
240
- consumerDestroy(this);
241
- this.onDestroyFn();
242
- this.cleanup();
243
- this.view[EFFECTS]?.delete(this);
244
- },
245
- }))();
246
- function createViewEffect(view, notifier, fn) {
247
- const node = Object.create(VIEW_EFFECT_NODE);
248
- node.view = view;
249
- node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
250
- node.notifier = notifier;
251
- node.fn = createEffectFn(node, fn);
252
- view[EFFECTS] ??= new Set();
253
- view[EFFECTS].add(node);
254
- node.consumerMarkedDirty(node);
255
- return node;
256
- }
257
- function createRootEffect(fn, scheduler, notifier) {
258
- const node = Object.create(ROOT_EFFECT_NODE);
259
- node.fn = createEffectFn(node, fn);
260
- node.scheduler = scheduler;
261
- node.notifier = notifier;
262
- node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
263
- node.scheduler.add(node);
264
- node.notifier.notify(12 /* NotificationSource.RootEffect */);
265
- return node;
266
- }
267
- function createEffectFn(node, fn) {
268
- return () => {
269
- fn((cleanupFn) => (node.cleanupFns ??= []).push(cleanupFn));
270
- };
271
- }
272
-
273
- const identityFn = (v) => v;
77
+ const identityFn = v => v;
274
78
  function linkedSignal(optionsOrComputation, options) {
275
- if (typeof optionsOrComputation === 'function') {
276
- const getter = createLinkedSignal(optionsOrComputation, (identityFn), options?.equal);
277
- return upgradeLinkedSignalGetter(getter, options?.debugName);
278
- }
279
- else {
280
- const getter = createLinkedSignal(optionsOrComputation.source, optionsOrComputation.computation, optionsOrComputation.equal);
281
- return upgradeLinkedSignalGetter(getter, optionsOrComputation.debugName);
282
- }
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
+ }
283
86
  }
284
87
  function upgradeLinkedSignalGetter(getter, debugName) {
285
- if (ngDevMode) {
286
- getter.toString = () => `[LinkedSignal: ${getter()}]`;
287
- getter[SIGNAL].debugName = debugName;
288
- }
289
- const node = getter[SIGNAL];
290
- const upgradedGetter = getter;
291
- upgradedGetter.set = (newValue) => linkedSignalSetFn(node, newValue);
292
- upgradedGetter.update = (updateFn) => linkedSignalUpdateFn(node, updateFn);
293
- upgradedGetter.asReadonly = signalAsReadonlyFn.bind(getter);
294
- 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;
295
98
  }
296
99
 
297
- /**
298
- * Whether a `Resource.value()` should throw an error when the resource is in the error state.
299
- *
300
- * This internal flag is being used to gradually roll out this behavior.
301
- */
302
100
  let RESOURCE_VALUE_THROWS_ERRORS_DEFAULT = true;
303
101
  function resource(options) {
304
- if (ngDevMode && !options?.injector) {
305
- assertInInjectionContext(resource);
306
- }
307
- const oldNameForParams = options.request;
308
- const params = (options.params ?? oldNameForParams ?? (() => null));
309
- 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);
310
108
  }
311
- /**
312
- * Base class which implements `.value` as a `WritableSignal` by delegating `.set` and `.update`.
313
- */
314
109
  class BaseWritableResource {
315
- value;
316
- constructor(value) {
317
- this.value = value;
318
- this.value.set = this.set.bind(this);
319
- this.value.update = this.update.bind(this);
320
- this.value.asReadonly = signalAsReadonlyFn;
321
- }
322
- isError = computed(() => this.status() === 'error');
323
- update(updateFn) {
324
- this.set(updateFn(untracked(this.value)));
325
- }
326
- isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading');
327
- // Use a computed here to avoid triggering reactive consumers if the value changes while staying
328
- // either defined or undefined.
329
- isValueDefined = computed(() => {
330
- // Check if it's in an error state first to prevent the error from bubbling up.
331
- if (this.isError()) {
332
- return false;
333
- }
334
- return this.value() !== undefined;
335
- });
336
- hasValue() {
337
- return this.isValueDefined();
338
- }
339
- asReadonly() {
340
- 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;
341
125
  }
126
+ return this.value() !== undefined;
127
+ });
128
+ hasValue() {
129
+ return this.isValueDefined();
130
+ }
131
+ asReadonly() {
132
+ return this;
133
+ }
342
134
  }
343
- /**
344
- * Implementation for `resource()` which uses a `linkedSignal` to manage the resource's state.
345
- */
346
135
  class ResourceImpl extends BaseWritableResource {
347
- loaderFn;
348
- equal;
349
- pendingTasks;
350
- /**
351
- * The current state of the resource. Status, value, and error are derived from this.
352
- */
353
- state;
354
- /**
355
- * Combines the current request with a reload counter which allows the resource to be reloaded on
356
- * imperative command.
357
- */
358
- extRequest;
359
- effectRef;
360
- pendingController;
361
- resolvePendingTask = undefined;
362
- destroyed = false;
363
- unregisterOnDestroy;
364
- constructor(request, loaderFn, defaultValue, equal, injector, throwErrorsFromValue = RESOURCE_VALUE_THROWS_ERRORS_DEFAULT) {
365
- super(
366
- // Feed a computed signal for the value to `BaseWritableResource`, which will upgrade it to a
367
- // `WritableSignal` that delegates to `ResourceImpl.set`.
368
- computed(() => {
369
- const streamValue = this.state().stream?.();
370
- if (!streamValue) {
371
- return defaultValue;
372
- }
373
- // Prevents `hasValue()` from throwing an error when a reload happened in the error state
374
- if (this.state().status === 'loading' && this.error()) {
375
- return defaultValue;
376
- }
377
- if (!isResolved(streamValue)) {
378
- if (throwErrorsFromValue) {
379
- throw new ResourceValueError(this.error());
380
- }
381
- else {
382
- return defaultValue;
383
- }
384
- }
385
- return streamValue.value;
386
- }, { equal }));
387
- this.loaderFn = loaderFn;
388
- this.equal = equal;
389
- // Extend `request()` to include a writable reload signal.
390
- this.extRequest = linkedSignal({
391
- source: request,
392
- computation: (request) => ({ request, reload: 0 }),
393
- });
394
- // The main resource state is managed in a `linkedSignal`, which allows the resource to change
395
- // state instantaneously when the request signal changes.
396
- this.state = linkedSignal({
397
- // Whenever the request changes,
398
- source: this.extRequest,
399
- // Compute the state of the resource given a change in status.
400
- computation: (extRequest, previous) => {
401
- const status = extRequest.request === undefined ? 'idle' : 'loading';
402
- if (!previous) {
403
- return {
404
- extRequest,
405
- status,
406
- previousStatus: 'idle',
407
- stream: undefined,
408
- };
409
- }
410
- else {
411
- return {
412
- extRequest,
413
- status,
414
- previousStatus: projectStatusOfState(previous.value),
415
- // If the request hasn't changed, keep the previous stream.
416
- stream: previous.value.extRequest.request === extRequest.request
417
- ? previous.value.stream
418
- : undefined,
419
- };
420
- }
421
- },
422
- });
423
- this.effectRef = effect(this.loadEffect.bind(this), {
424
- injector,
425
- manualCleanup: true,
426
- });
427
- this.pendingTasks = injector.get(PendingTasks);
428
- // Cancel any pending request when the resource itself is destroyed.
429
- this.unregisterOnDestroy = injector.get(DestroyRef).onDestroy(() => this.destroy());
430
- }
431
- status = computed(() => projectStatusOfState(this.state()));
432
- error = computed(() => {
433
- const stream = this.state().stream?.();
434
- return stream && !isResolved(stream) ? stream.error : undefined;
435
- });
436
- /**
437
- * Called either directly via `WritableResource.set` or via `.value.set()`.
438
- */
439
- set(value) {
440
- if (this.destroyed) {
441
- 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;
442
160
  }
443
- const error = untracked(this.error);
444
- const state = untracked(this.state);
445
- if (!error) {
446
- const current = untracked(this.value);
447
- if (state.status === 'local' &&
448
- (this.equal ? this.equal(current, value) : current === value)) {
449
- return;
450
- }
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
+ };
451
193
  }
452
- // Enter Local state with the user-defined value.
453
- this.state.set({
454
- extRequest: state.extRequest,
455
- status: 'local',
456
- previousStatus: 'local',
457
- stream: signal({ value }),
458
- });
459
- // We're departing from whatever state the resource was in previously, so cancel any in-progress
460
- // loading operations.
461
- 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;
462
211
  }
463
- reload() {
464
- // We don't want to restart in-progress loads.
465
- const { status } = untracked(this.state);
466
- if (status === 'idle' || status === 'loading') {
467
- return false;
468
- }
469
- // Increment the request reload to trigger the `state` linked signal to switch us to `Reload`
470
- this.extRequest.update(({ request, reload }) => ({ request, reload: reload + 1 }));
471
- 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
+ }
472
219
  }
473
- destroy() {
474
- this.destroyed = true;
475
- this.unregisterOnDestroy();
476
- this.effectRef.destroy();
477
- this.abortInProgressLoad();
478
- // Destroyed resources enter Idle state.
479
- this.state.set({
480
- extRequest: { request: undefined, reload: 0 },
481
- status: 'idle',
482
- previousStatus: 'idle',
483
- stream: undefined,
484
- });
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;
485
236
  }
486
- async loadEffect() {
487
- const extRequest = this.extRequest();
488
- // Capture the previous status before any state transitions. Note that this is `untracked` since
489
- // we do not want the effect to depend on the state of the resource, only on the request.
490
- const { status: currentStatus, previousStatus } = untracked(this.state);
491
- if (extRequest.request === undefined) {
492
- // Nothing to load (and we should already be in a non-loading state).
493
- return;
494
- }
495
- else if (currentStatus !== 'loading') {
496
- // We're not in a loading or reloading state, so this loading request is stale.
497
- return;
498
- }
499
- // Cancel any previous loading attempts.
500
- this.abortInProgressLoad();
501
- // Capturing _this_ load's pending task in a local variable is important here. We may attempt to
502
- // resolve it twice:
503
- //
504
- // 1. when the loading function promise resolves/rejects
505
- // 2. when cancelling the loading operation
506
- //
507
- // After the loading operation is cancelled, `this.resolvePendingTask` no longer represents this
508
- // particular task, but this `await` may eventually resolve/reject. Thus, when we cancel in
509
- // response to (1) below, we need to cancel the locally saved task.
510
- let resolvePendingTask = (this.resolvePendingTask =
511
- this.pendingTasks.add());
512
- const { signal: abortSignal } = (this.pendingController = new AbortController());
513
- try {
514
- // The actual loading is run through `untracked` - only the request side of `resource` is
515
- // reactive. This avoids any confusion with signals tracking or not tracking depending on
516
- // which side of the `await` they are.
517
- const stream = await untracked(() => {
518
- return this.loaderFn({
519
- params: extRequest.request,
520
- // TODO(alxhub): cleanup after g3 removal of `request` alias.
521
- request: extRequest.request,
522
- abortSignal,
523
- previous: {
524
- status: previousStatus,
525
- },
526
- });
527
- });
528
- // If this request has been aborted, or the current request no longer
529
- // matches this load, then we should ignore this resolution.
530
- if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
531
- return;
532
- }
533
- this.state.set({
534
- extRequest,
535
- status: 'resolved',
536
- previousStatus: 'resolved',
537
- stream,
538
- });
539
- }
540
- catch (err) {
541
- if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
542
- return;
543
- }
544
- this.state.set({
545
- extRequest,
546
- status: 'resolved',
547
- previousStatus: 'error',
548
- stream: signal({ error: encapsulateResourceError(err) }),
549
- });
550
- }
551
- finally {
552
- // Resolve the pending task now that the resource has a value.
553
- resolvePendingTask?.();
554
- resolvePendingTask = undefined;
555
- }
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;
556
271
  }
557
- abortInProgressLoad() {
558
- untracked(() => this.pendingController?.abort());
559
- this.pendingController = undefined;
560
- // Once the load is aborted, we no longer want to block stability on its resolution.
561
- this.resolvePendingTask?.();
562
- 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;
563
312
  }
313
+ }
314
+ abortInProgressLoad() {
315
+ untracked(() => this.pendingController?.abort());
316
+ this.pendingController = undefined;
317
+ this.resolvePendingTask?.();
318
+ this.resolvePendingTask = undefined;
319
+ }
564
320
  }
565
- /**
566
- * Wraps an equality function to handle either value being `undefined`.
567
- */
568
321
  function wrapEqualityFn(equal) {
569
- 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);
570
323
  }
571
324
  function getLoader(options) {
572
- if (isStreamingResourceOptions(options)) {
573
- 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
+ });
574
337
  }
575
- return async (params) => {
576
- try {
577
- return signal({ value: await options.loader(params) });
578
- }
579
- catch (err) {
580
- return signal({ error: encapsulateResourceError(err) });
581
- }
582
- };
338
+ };
583
339
  }
584
340
  function isStreamingResourceOptions(options) {
585
- return !!options.stream;
341
+ return !!options.stream;
586
342
  }
587
- /**
588
- * Project from a state with `ResourceInternalStatus` to the user-facing `ResourceStatus`
589
- */
590
343
  function projectStatusOfState(state) {
591
- switch (state.status) {
592
- case 'loading':
593
- return state.extRequest.reload === 0 ? 'loading' : 'reloading';
594
- case 'resolved':
595
- return isResolved(state.stream()) ? 'resolved' : 'error';
596
- default:
597
- return state.status;
598
- }
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
+ }
599
352
  }
600
353
  function isResolved(state) {
601
- return state.error === undefined;
354
+ return state.error === undefined;
602
355
  }
603
356
  function encapsulateResourceError(error) {
604
- if (error instanceof Error) {
605
- return error;
606
- }
607
- return new ResourceWrappedError(error);
357
+ if (error instanceof Error) {
358
+ return error;
359
+ }
360
+ return new ResourceWrappedError(error);
608
361
  }
609
362
  class ResourceValueError extends Error {
610
- constructor(error) {
611
- super(ngDevMode
612
- ? `Resource is currently in an error state (see Error.cause for details): ${error.message}`
613
- : error.message, { cause: error });
614
- }
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
+ }
615
368
  }
616
369
  class ResourceWrappedError extends Error {
617
- constructor(error) {
618
- super(ngDevMode
619
- ? `Resource returned an error that's not an Error instance: ${String(error)}. Check this error's .cause for the actual error.`
620
- : String(error), { cause: error });
621
- }
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
+ }
622
375
  }
623
376
 
624
- export { OutputEmitterRef, ResourceImpl, computed, effect, encapsulateResourceError, getOutputDestroyRef, linkedSignal, resource, untracked };
377
+ export { OutputEmitterRef, ResourceImpl, computed, encapsulateResourceError, getOutputDestroyRef, linkedSignal, resource, untracked };
625
378
  //# sourceMappingURL=_resource-chunk.mjs.map