@angular/core 21.0.0-next.0 → 21.0.0-next.10
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 +12 -0
- package/fesm2022/_attribute-chunk.mjs.map +1 -0
- package/fesm2022/_debug_node-chunk.mjs +18469 -0
- package/fesm2022/_debug_node-chunk.mjs.map +1 -0
- package/fesm2022/_effect-chunk.mjs +423 -0
- package/fesm2022/_effect-chunk.mjs.map +1 -0
- package/fesm2022/_effect-chunk2.mjs +2951 -0
- package/fesm2022/_effect-chunk2.mjs.map +1 -0
- package/fesm2022/_not_found-chunk.mjs +39 -0
- package/fesm2022/_not_found-chunk.mjs.map +1 -0
- package/fesm2022/_resource-chunk.mjs +378 -0
- package/fesm2022/_resource-chunk.mjs.map +1 -0
- package/fesm2022/_untracked-chunk.mjs +96 -0
- package/fesm2022/_untracked-chunk.mjs.map +1 -0
- package/fesm2022/_weak_ref-chunk.mjs +10 -0
- package/fesm2022/_weak_ref-chunk.mjs.map +1 -0
- package/fesm2022/core.mjs +2499 -4185
- package/fesm2022/core.mjs.map +1 -1
- package/fesm2022/primitives-di.mjs +23 -0
- package/fesm2022/primitives-di.mjs.map +1 -0
- package/fesm2022/primitives-event-dispatch.mjs +788 -0
- package/fesm2022/primitives-event-dispatch.mjs.map +1 -0
- package/fesm2022/primitives-signals.mjs +187 -0
- package/fesm2022/primitives-signals.mjs.map +1 -0
- package/fesm2022/rxjs-interop.mjs +210 -308
- package/fesm2022/rxjs-interop.mjs.map +1 -1
- package/fesm2022/testing.mjs +2309 -3170
- package/fesm2022/testing.mjs.map +1 -1
- package/package.json +18 -12
- package/resources/best-practices.md +56 -0
- package/schematics/bundles/add-bootstrap-context-to-server-main.cjs +117 -0
- package/schematics/bundles/application-config-core.cjs +84 -0
- package/schematics/bundles/{apply_import_manager-DR9xXCle.cjs → apply_import_manager-1Zs_gpB6.cjs} +4 -5
- package/schematics/bundles/bootstrap-options-migration.cjs +598 -0
- package/schematics/bundles/cleanup-unused-imports.cjs +9 -13
- package/schematics/bundles/common-to-standalone-migration.cjs +381 -0
- package/schematics/bundles/{compiler_host-BXBP7CE2.cjs → compiler_host-DBwYMlTo.cjs} +10 -11
- package/schematics/bundles/control-flow-migration.cjs +122 -119
- package/schematics/bundles/{imports-CIX-JgAN.cjs → imports-DP72APSx.cjs} +6 -1
- package/schematics/bundles/{index-CfTQUOiz.cjs → index-B7I9sIUx.cjs} +36 -39
- package/schematics/bundles/inject-migration.cjs +148 -70
- package/schematics/bundles/leading_space-D9nQ8UQC.cjs +1 -1
- package/schematics/bundles/{migrate_ts_type_references-6NtAj-Wk.cjs → migrate_ts_type_references-UGIUl7En.cjs} +500 -24
- package/schematics/bundles/ng_component_template-Dsuq1Lw7.cjs +185 -0
- package/schematics/bundles/{ng_decorators-B5HCqr20.cjs → ng_decorators-DSFlWYQY.cjs} +2 -2
- package/schematics/bundles/ngclass-to-class-migration.cjs +542 -0
- package/schematics/bundles/ngstyle-to-style-migration.cjs +487 -0
- package/schematics/bundles/nodes-B16H9JUd.cjs +1 -1
- package/schematics/bundles/output-migration.cjs +16 -19
- package/schematics/bundles/parse_html-8VLCL37B.cjs +132 -0
- package/schematics/bundles/{project_paths-DcaODbky.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 +54 -26
- package/schematics/bundles/router-current-navigation.cjs +7 -18
- package/schematics/bundles/router-last-successful-navigation.cjs +7 -18
- package/schematics/bundles/router-testing-module-migration.cjs +502 -0
- package/schematics/bundles/self-closing-tags-migration.cjs +17 -216
- package/schematics/bundles/signal-input-migration.cjs +93 -29
- package/schematics/bundles/signal-queries-migration.cjs +22 -25
- package/schematics/bundles/signals.cjs +10 -13
- package/schematics/bundles/standalone-migration.cjs +135 -102
- package/schematics/bundles/{symbol-VPWguRxr.cjs → symbol-BObKoqes.cjs} +3 -2
- package/schematics/collection.json +23 -0
- package/schematics/migrations/common-to-standalone-migration/schema.json +14 -0
- package/schematics/migrations/ngclass-to-class-migration/schema.json +20 -0
- package/schematics/migrations/ngstyle-to-style-migration/schema.json +20 -0
- package/schematics/migrations/router-testing-module-migration/schema.json +14 -0
- package/schematics/migrations.json +16 -2
- package/{api.d.d.ts → types/_api-chunk.d.ts} +9 -6
- package/{chrome_dev_tools_performance.d.d.ts → types/_chrome_dev_tools_performance-chunk.d.ts} +26 -31
- package/{discovery.d.d.ts → types/_discovery-chunk.d.ts} +135 -98
- package/{signal.d.d.ts → types/_effect-chunk.d.ts} +14 -5
- package/{event_dispatcher.d.d.ts → types/_event_dispatcher-chunk.d.ts} +2 -2
- package/{graph.d.d.ts → types/_formatter-chunk.d.ts} +40 -7
- package/{weak_ref.d.d.ts → types/_weak_ref-chunk.d.ts} +2 -2
- package/{index.d.ts → types/core.d.ts} +233 -305
- package/{primitives/di/index.d.ts → types/primitives-di.d.ts} +2 -2
- package/{primitives/event-dispatch/index.d.ts → types/primitives-event-dispatch.d.ts} +4 -4
- package/{primitives/signals/index.d.ts → types/primitives-signals.d.ts} +7 -8
- package/{rxjs-interop/index.d.ts → types/rxjs-interop.d.ts} +8 -6
- package/{testing/index.d.ts → types/testing.d.ts} +7 -7
- package/fesm2022/attribute.mjs +0 -24
- package/fesm2022/attribute.mjs.map +0 -1
- package/fesm2022/debug_node.mjs +0 -31833
- package/fesm2022/debug_node.mjs.map +0 -1
- package/fesm2022/not_found.mjs +0 -56
- package/fesm2022/not_found.mjs.map +0 -1
- package/fesm2022/primitives/di.mjs +0 -23
- package/fesm2022/primitives/di.mjs.map +0 -1
- package/fesm2022/primitives/event-dispatch.mjs +0 -1622
- package/fesm2022/primitives/event-dispatch.mjs.map +0 -1
- package/fesm2022/primitives/signals.mjs +0 -89
- package/fesm2022/primitives/signals.mjs.map +0 -1
- package/fesm2022/resource.mjs +0 -633
- package/fesm2022/resource.mjs.map +0 -1
- package/fesm2022/root_effect_scheduler.mjs +0 -4007
- package/fesm2022/root_effect_scheduler.mjs.map +0 -1
- package/fesm2022/signal.mjs +0 -560
- package/fesm2022/signal.mjs.map +0 -1
- package/fesm2022/untracked.mjs +0 -117
- package/fesm2022/untracked.mjs.map +0 -1
- package/fesm2022/weak_ref.mjs +0 -12
- package/fesm2022/weak_ref.mjs.map +0 -1
- package/schematics/bundles/index-esqfDjNB.cjs +0 -22074
- package/schematics/bundles/project_tsconfig_paths-CS-eSeHC.cjs +0 -51062
package/fesm2022/resource.mjs
DELETED
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Angular v21.0.0-next.0
|
|
3
|
-
* (c) 2010-2025 Google LLC. https://angular.io/
|
|
4
|
-
* License: MIT
|
|
5
|
-
*/
|
|
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.mjs';
|
|
8
|
-
import { setActiveConsumer, createComputed, SIGNAL, consumerDestroy, REACTIVE_NODE, isInNotificationPhase, consumerPollProducersForChange, consumerBeforeComputation, consumerAfterComputation } from './signal.mjs';
|
|
9
|
-
import { untracked as untracked$1, createLinkedSignal, linkedSignalSetFn, linkedSignalUpdateFn } from './untracked.mjs';
|
|
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
|
-
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
|
-
});
|
|
36
|
-
}
|
|
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.');
|
|
42
|
-
}
|
|
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
|
-
};
|
|
52
|
-
}
|
|
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);
|
|
65
|
-
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);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/** Gets the owning `DestroyRef` for the given output. */
|
|
81
|
-
function getOutputDestroyRef(ref) {
|
|
82
|
-
return ref.destroyRef;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function
|
|
87
|
-
* can, optionally, return a value.
|
|
88
|
-
*/
|
|
89
|
-
function untracked(nonReactiveReadsFn) {
|
|
90
|
-
return untracked$1(nonReactiveReadsFn);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Create a computed `Signal` which derives a reactive value from an expression.
|
|
95
|
-
*/
|
|
96
|
-
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;
|
|
103
|
-
}
|
|
104
|
-
|
|
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 BASE_EFFECT_NODE =
|
|
179
|
-
/* @__PURE__ */ (() => ({
|
|
180
|
-
...REACTIVE_NODE,
|
|
181
|
-
consumerIsAlwaysLive: true,
|
|
182
|
-
consumerAllowSignalWrites: true,
|
|
183
|
-
dirty: true,
|
|
184
|
-
hasRun: false,
|
|
185
|
-
cleanupFns: undefined,
|
|
186
|
-
zone: null,
|
|
187
|
-
kind: 'effect',
|
|
188
|
-
onDestroyFn: noop,
|
|
189
|
-
run() {
|
|
190
|
-
this.dirty = false;
|
|
191
|
-
if (ngDevMode && isInNotificationPhase()) {
|
|
192
|
-
throw new Error(`Schedulers cannot synchronously execute watches while scheduling.`);
|
|
193
|
-
}
|
|
194
|
-
if (this.hasRun && !consumerPollProducersForChange(this)) {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
this.hasRun = true;
|
|
198
|
-
const registerCleanupFn = (cleanupFn) => (this.cleanupFns ??= []).push(cleanupFn);
|
|
199
|
-
const prevNode = consumerBeforeComputation(this);
|
|
200
|
-
// We clear `setIsRefreshingViews` so that `markForCheck()` within the body of an effect will
|
|
201
|
-
// cause CD to reach the component in question.
|
|
202
|
-
const prevRefreshingViews = setIsRefreshingViews(false);
|
|
203
|
-
try {
|
|
204
|
-
this.maybeCleanup();
|
|
205
|
-
this.fn(registerCleanupFn);
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
setIsRefreshingViews(prevRefreshingViews);
|
|
209
|
-
consumerAfterComputation(this, prevNode);
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
maybeCleanup() {
|
|
213
|
-
if (!this.cleanupFns?.length) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const prevConsumer = setActiveConsumer(null);
|
|
217
|
-
try {
|
|
218
|
-
// Attempt to run the cleanup functions. Regardless of failure or success, we consider
|
|
219
|
-
// cleanup "completed" and clear the list for the next run of the effect. Note that an error
|
|
220
|
-
// from the cleanup function will still crash the current run of the effect.
|
|
221
|
-
while (this.cleanupFns.length) {
|
|
222
|
-
this.cleanupFns.pop()();
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
this.cleanupFns = [];
|
|
227
|
-
setActiveConsumer(prevConsumer);
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
}))();
|
|
231
|
-
const ROOT_EFFECT_NODE =
|
|
232
|
-
/* @__PURE__ */ (() => ({
|
|
233
|
-
...BASE_EFFECT_NODE,
|
|
234
|
-
consumerMarkedDirty() {
|
|
235
|
-
this.scheduler.schedule(this);
|
|
236
|
-
this.notifier.notify(12 /* NotificationSource.RootEffect */);
|
|
237
|
-
},
|
|
238
|
-
destroy() {
|
|
239
|
-
consumerDestroy(this);
|
|
240
|
-
this.onDestroyFn();
|
|
241
|
-
this.maybeCleanup();
|
|
242
|
-
this.scheduler.remove(this);
|
|
243
|
-
},
|
|
244
|
-
}))();
|
|
245
|
-
const VIEW_EFFECT_NODE =
|
|
246
|
-
/* @__PURE__ */ (() => ({
|
|
247
|
-
...BASE_EFFECT_NODE,
|
|
248
|
-
consumerMarkedDirty() {
|
|
249
|
-
this.view[FLAGS] |= 8192 /* LViewFlags.HasChildViewsToRefresh */;
|
|
250
|
-
markAncestorsForTraversal(this.view);
|
|
251
|
-
this.notifier.notify(13 /* NotificationSource.ViewEffect */);
|
|
252
|
-
},
|
|
253
|
-
destroy() {
|
|
254
|
-
consumerDestroy(this);
|
|
255
|
-
this.onDestroyFn();
|
|
256
|
-
this.maybeCleanup();
|
|
257
|
-
this.view[EFFECTS]?.delete(this);
|
|
258
|
-
},
|
|
259
|
-
}))();
|
|
260
|
-
function createViewEffect(view, notifier, fn) {
|
|
261
|
-
const node = Object.create(VIEW_EFFECT_NODE);
|
|
262
|
-
node.view = view;
|
|
263
|
-
node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
|
|
264
|
-
node.notifier = notifier;
|
|
265
|
-
node.fn = fn;
|
|
266
|
-
view[EFFECTS] ??= new Set();
|
|
267
|
-
view[EFFECTS].add(node);
|
|
268
|
-
node.consumerMarkedDirty(node);
|
|
269
|
-
return node;
|
|
270
|
-
}
|
|
271
|
-
function createRootEffect(fn, scheduler, notifier) {
|
|
272
|
-
const node = Object.create(ROOT_EFFECT_NODE);
|
|
273
|
-
node.fn = fn;
|
|
274
|
-
node.scheduler = scheduler;
|
|
275
|
-
node.notifier = notifier;
|
|
276
|
-
node.zone = typeof Zone !== 'undefined' ? Zone.current : null;
|
|
277
|
-
node.scheduler.add(node);
|
|
278
|
-
node.notifier.notify(12 /* NotificationSource.RootEffect */);
|
|
279
|
-
return node;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const identityFn = (v) => v;
|
|
283
|
-
function linkedSignal(optionsOrComputation, options) {
|
|
284
|
-
if (typeof optionsOrComputation === 'function') {
|
|
285
|
-
const getter = createLinkedSignal(optionsOrComputation, (identityFn), options?.equal);
|
|
286
|
-
return upgradeLinkedSignalGetter(getter);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
const getter = createLinkedSignal(optionsOrComputation.source, optionsOrComputation.computation, optionsOrComputation.equal);
|
|
290
|
-
return upgradeLinkedSignalGetter(getter);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
function upgradeLinkedSignalGetter(getter) {
|
|
294
|
-
if (ngDevMode) {
|
|
295
|
-
getter.toString = () => `[LinkedSignal: ${getter()}]`;
|
|
296
|
-
}
|
|
297
|
-
const node = getter[SIGNAL];
|
|
298
|
-
const upgradedGetter = getter;
|
|
299
|
-
upgradedGetter.set = (newValue) => linkedSignalSetFn(node, newValue);
|
|
300
|
-
upgradedGetter.update = (updateFn) => linkedSignalUpdateFn(node, updateFn);
|
|
301
|
-
upgradedGetter.asReadonly = signalAsReadonlyFn.bind(getter);
|
|
302
|
-
return upgradedGetter;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Whether a `Resource.value()` should throw an error when the resource is in the error state.
|
|
307
|
-
*
|
|
308
|
-
* This internal flag is being used to gradually roll out this behavior.
|
|
309
|
-
*/
|
|
310
|
-
let RESOURCE_VALUE_THROWS_ERRORS_DEFAULT = true;
|
|
311
|
-
function resource(options) {
|
|
312
|
-
if (ngDevMode && !options?.injector) {
|
|
313
|
-
assertInInjectionContext(resource);
|
|
314
|
-
}
|
|
315
|
-
const oldNameForParams = options.request;
|
|
316
|
-
const params = (options.params ?? oldNameForParams ?? (() => null));
|
|
317
|
-
return new ResourceImpl(params, getLoader(options), options.defaultValue, options.equal ? wrapEqualityFn(options.equal) : undefined, options.injector ?? inject(Injector), RESOURCE_VALUE_THROWS_ERRORS_DEFAULT);
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Base class which implements `.value` as a `WritableSignal` by delegating `.set` and `.update`.
|
|
321
|
-
*/
|
|
322
|
-
class BaseWritableResource {
|
|
323
|
-
value;
|
|
324
|
-
constructor(value) {
|
|
325
|
-
this.value = value;
|
|
326
|
-
this.value.set = this.set.bind(this);
|
|
327
|
-
this.value.update = this.update.bind(this);
|
|
328
|
-
this.value.asReadonly = signalAsReadonlyFn;
|
|
329
|
-
}
|
|
330
|
-
isError = computed(() => this.status() === 'error');
|
|
331
|
-
update(updateFn) {
|
|
332
|
-
this.set(updateFn(untracked(this.value)));
|
|
333
|
-
}
|
|
334
|
-
isLoading = computed(() => this.status() === 'loading' || this.status() === 'reloading');
|
|
335
|
-
// Use a computed here to avoid triggering reactive consumers if the value changes while staying
|
|
336
|
-
// either defined or undefined.
|
|
337
|
-
isValueDefined = computed(() => {
|
|
338
|
-
// Check if it's in an error state first to prevent the error from bubbling up.
|
|
339
|
-
if (this.isError()) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
return this.value() !== undefined;
|
|
343
|
-
});
|
|
344
|
-
hasValue() {
|
|
345
|
-
return this.isValueDefined();
|
|
346
|
-
}
|
|
347
|
-
asReadonly() {
|
|
348
|
-
return this;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Implementation for `resource()` which uses a `linkedSignal` to manage the resource's state.
|
|
353
|
-
*/
|
|
354
|
-
class ResourceImpl extends BaseWritableResource {
|
|
355
|
-
loaderFn;
|
|
356
|
-
equal;
|
|
357
|
-
pendingTasks;
|
|
358
|
-
/**
|
|
359
|
-
* The current state of the resource. Status, value, and error are derived from this.
|
|
360
|
-
*/
|
|
361
|
-
state;
|
|
362
|
-
/**
|
|
363
|
-
* Combines the current request with a reload counter which allows the resource to be reloaded on
|
|
364
|
-
* imperative command.
|
|
365
|
-
*/
|
|
366
|
-
extRequest;
|
|
367
|
-
effectRef;
|
|
368
|
-
pendingController;
|
|
369
|
-
resolvePendingTask = undefined;
|
|
370
|
-
destroyed = false;
|
|
371
|
-
unregisterOnDestroy;
|
|
372
|
-
constructor(request, loaderFn, defaultValue, equal, injector, throwErrorsFromValue = RESOURCE_VALUE_THROWS_ERRORS_DEFAULT) {
|
|
373
|
-
super(
|
|
374
|
-
// Feed a computed signal for the value to `BaseWritableResource`, which will upgrade it to a
|
|
375
|
-
// `WritableSignal` that delegates to `ResourceImpl.set`.
|
|
376
|
-
computed(() => {
|
|
377
|
-
const streamValue = this.state().stream?.();
|
|
378
|
-
if (!streamValue) {
|
|
379
|
-
return defaultValue;
|
|
380
|
-
}
|
|
381
|
-
// Prevents `hasValue()` from throwing an error when a reload happened in the error state
|
|
382
|
-
if (this.state().status === 'loading' && this.error()) {
|
|
383
|
-
return defaultValue;
|
|
384
|
-
}
|
|
385
|
-
if (!isResolved(streamValue)) {
|
|
386
|
-
if (throwErrorsFromValue) {
|
|
387
|
-
throw new ResourceValueError(this.error());
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
return defaultValue;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return streamValue.value;
|
|
394
|
-
}, { equal }));
|
|
395
|
-
this.loaderFn = loaderFn;
|
|
396
|
-
this.equal = equal;
|
|
397
|
-
// Extend `request()` to include a writable reload signal.
|
|
398
|
-
this.extRequest = linkedSignal({
|
|
399
|
-
source: request,
|
|
400
|
-
computation: (request) => ({ request, reload: 0 }),
|
|
401
|
-
});
|
|
402
|
-
// The main resource state is managed in a `linkedSignal`, which allows the resource to change
|
|
403
|
-
// state instantaneously when the request signal changes.
|
|
404
|
-
this.state = linkedSignal({
|
|
405
|
-
// Whenever the request changes,
|
|
406
|
-
source: this.extRequest,
|
|
407
|
-
// Compute the state of the resource given a change in status.
|
|
408
|
-
computation: (extRequest, previous) => {
|
|
409
|
-
const status = extRequest.request === undefined ? 'idle' : 'loading';
|
|
410
|
-
if (!previous) {
|
|
411
|
-
return {
|
|
412
|
-
extRequest,
|
|
413
|
-
status,
|
|
414
|
-
previousStatus: 'idle',
|
|
415
|
-
stream: undefined,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
return {
|
|
420
|
-
extRequest,
|
|
421
|
-
status,
|
|
422
|
-
previousStatus: projectStatusOfState(previous.value),
|
|
423
|
-
// If the request hasn't changed, keep the previous stream.
|
|
424
|
-
stream: previous.value.extRequest.request === extRequest.request
|
|
425
|
-
? previous.value.stream
|
|
426
|
-
: undefined,
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
this.effectRef = effect(this.loadEffect.bind(this), {
|
|
432
|
-
injector,
|
|
433
|
-
manualCleanup: true,
|
|
434
|
-
});
|
|
435
|
-
this.pendingTasks = injector.get(PendingTasks);
|
|
436
|
-
// Cancel any pending request when the resource itself is destroyed.
|
|
437
|
-
this.unregisterOnDestroy = injector.get(DestroyRef).onDestroy(() => this.destroy());
|
|
438
|
-
}
|
|
439
|
-
status = computed(() => projectStatusOfState(this.state()));
|
|
440
|
-
error = computed(() => {
|
|
441
|
-
const stream = this.state().stream?.();
|
|
442
|
-
return stream && !isResolved(stream) ? stream.error : undefined;
|
|
443
|
-
});
|
|
444
|
-
/**
|
|
445
|
-
* Called either directly via `WritableResource.set` or via `.value.set()`.
|
|
446
|
-
*/
|
|
447
|
-
set(value) {
|
|
448
|
-
if (this.destroyed) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const error = untracked(this.error);
|
|
452
|
-
const state = untracked(this.state);
|
|
453
|
-
if (!error) {
|
|
454
|
-
const current = untracked(this.value);
|
|
455
|
-
if (state.status === 'local' &&
|
|
456
|
-
(this.equal ? this.equal(current, value) : current === value)) {
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// Enter Local state with the user-defined value.
|
|
461
|
-
this.state.set({
|
|
462
|
-
extRequest: state.extRequest,
|
|
463
|
-
status: 'local',
|
|
464
|
-
previousStatus: 'local',
|
|
465
|
-
stream: signal({ value }),
|
|
466
|
-
});
|
|
467
|
-
// We're departing from whatever state the resource was in previously, so cancel any in-progress
|
|
468
|
-
// loading operations.
|
|
469
|
-
this.abortInProgressLoad();
|
|
470
|
-
}
|
|
471
|
-
reload() {
|
|
472
|
-
// We don't want to restart in-progress loads.
|
|
473
|
-
const { status } = untracked(this.state);
|
|
474
|
-
if (status === 'idle' || status === 'loading') {
|
|
475
|
-
return false;
|
|
476
|
-
}
|
|
477
|
-
// Increment the request reload to trigger the `state` linked signal to switch us to `Reload`
|
|
478
|
-
this.extRequest.update(({ request, reload }) => ({ request, reload: reload + 1 }));
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
destroy() {
|
|
482
|
-
this.destroyed = true;
|
|
483
|
-
this.unregisterOnDestroy();
|
|
484
|
-
this.effectRef.destroy();
|
|
485
|
-
this.abortInProgressLoad();
|
|
486
|
-
// Destroyed resources enter Idle state.
|
|
487
|
-
this.state.set({
|
|
488
|
-
extRequest: { request: undefined, reload: 0 },
|
|
489
|
-
status: 'idle',
|
|
490
|
-
previousStatus: 'idle',
|
|
491
|
-
stream: undefined,
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
async loadEffect() {
|
|
495
|
-
const extRequest = this.extRequest();
|
|
496
|
-
// Capture the previous status before any state transitions. Note that this is `untracked` since
|
|
497
|
-
// we do not want the effect to depend on the state of the resource, only on the request.
|
|
498
|
-
const { status: currentStatus, previousStatus } = untracked(this.state);
|
|
499
|
-
if (extRequest.request === undefined) {
|
|
500
|
-
// Nothing to load (and we should already be in a non-loading state).
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
else if (currentStatus !== 'loading') {
|
|
504
|
-
// We're not in a loading or reloading state, so this loading request is stale.
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
// Cancel any previous loading attempts.
|
|
508
|
-
this.abortInProgressLoad();
|
|
509
|
-
// Capturing _this_ load's pending task in a local variable is important here. We may attempt to
|
|
510
|
-
// resolve it twice:
|
|
511
|
-
//
|
|
512
|
-
// 1. when the loading function promise resolves/rejects
|
|
513
|
-
// 2. when cancelling the loading operation
|
|
514
|
-
//
|
|
515
|
-
// After the loading operation is cancelled, `this.resolvePendingTask` no longer represents this
|
|
516
|
-
// particular task, but this `await` may eventually resolve/reject. Thus, when we cancel in
|
|
517
|
-
// response to (1) below, we need to cancel the locally saved task.
|
|
518
|
-
let resolvePendingTask = (this.resolvePendingTask =
|
|
519
|
-
this.pendingTasks.add());
|
|
520
|
-
const { signal: abortSignal } = (this.pendingController = new AbortController());
|
|
521
|
-
try {
|
|
522
|
-
// The actual loading is run through `untracked` - only the request side of `resource` is
|
|
523
|
-
// reactive. This avoids any confusion with signals tracking or not tracking depending on
|
|
524
|
-
// which side of the `await` they are.
|
|
525
|
-
const stream = await untracked(() => {
|
|
526
|
-
return this.loaderFn({
|
|
527
|
-
params: extRequest.request,
|
|
528
|
-
// TODO(alxhub): cleanup after g3 removal of `request` alias.
|
|
529
|
-
request: extRequest.request,
|
|
530
|
-
abortSignal,
|
|
531
|
-
previous: {
|
|
532
|
-
status: previousStatus,
|
|
533
|
-
},
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
// If this request has been aborted, or the current request no longer
|
|
537
|
-
// matches this load, then we should ignore this resolution.
|
|
538
|
-
if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
this.state.set({
|
|
542
|
-
extRequest,
|
|
543
|
-
status: 'resolved',
|
|
544
|
-
previousStatus: 'resolved',
|
|
545
|
-
stream,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
catch (err) {
|
|
549
|
-
if (abortSignal.aborted || untracked(this.extRequest) !== extRequest) {
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
this.state.set({
|
|
553
|
-
extRequest,
|
|
554
|
-
status: 'resolved',
|
|
555
|
-
previousStatus: 'error',
|
|
556
|
-
stream: signal({ error: encapsulateResourceError(err) }),
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
finally {
|
|
560
|
-
// Resolve the pending task now that the resource has a value.
|
|
561
|
-
resolvePendingTask?.();
|
|
562
|
-
resolvePendingTask = undefined;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
abortInProgressLoad() {
|
|
566
|
-
untracked(() => this.pendingController?.abort());
|
|
567
|
-
this.pendingController = undefined;
|
|
568
|
-
// Once the load is aborted, we no longer want to block stability on its resolution.
|
|
569
|
-
this.resolvePendingTask?.();
|
|
570
|
-
this.resolvePendingTask = undefined;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Wraps an equality function to handle either value being `undefined`.
|
|
575
|
-
*/
|
|
576
|
-
function wrapEqualityFn(equal) {
|
|
577
|
-
return (a, b) => (a === undefined || b === undefined ? a === b : equal(a, b));
|
|
578
|
-
}
|
|
579
|
-
function getLoader(options) {
|
|
580
|
-
if (isStreamingResourceOptions(options)) {
|
|
581
|
-
return options.stream;
|
|
582
|
-
}
|
|
583
|
-
return async (params) => {
|
|
584
|
-
try {
|
|
585
|
-
return signal({ value: await options.loader(params) });
|
|
586
|
-
}
|
|
587
|
-
catch (err) {
|
|
588
|
-
return signal({ error: encapsulateResourceError(err) });
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
function isStreamingResourceOptions(options) {
|
|
593
|
-
return !!options.stream;
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* Project from a state with `ResourceInternalStatus` to the user-facing `ResourceStatus`
|
|
597
|
-
*/
|
|
598
|
-
function projectStatusOfState(state) {
|
|
599
|
-
switch (state.status) {
|
|
600
|
-
case 'loading':
|
|
601
|
-
return state.extRequest.reload === 0 ? 'loading' : 'reloading';
|
|
602
|
-
case 'resolved':
|
|
603
|
-
return isResolved(state.stream()) ? 'resolved' : 'error';
|
|
604
|
-
default:
|
|
605
|
-
return state.status;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
function isResolved(state) {
|
|
609
|
-
return state.error === undefined;
|
|
610
|
-
}
|
|
611
|
-
function encapsulateResourceError(error) {
|
|
612
|
-
if (error instanceof Error) {
|
|
613
|
-
return error;
|
|
614
|
-
}
|
|
615
|
-
return new ResourceWrappedError(error);
|
|
616
|
-
}
|
|
617
|
-
class ResourceValueError extends Error {
|
|
618
|
-
constructor(error) {
|
|
619
|
-
super(ngDevMode
|
|
620
|
-
? `Resource is currently in an error state (see Error.cause for details): ${error.message}`
|
|
621
|
-
: error.message, { cause: error });
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
class ResourceWrappedError extends Error {
|
|
625
|
-
constructor(error) {
|
|
626
|
-
super(ngDevMode
|
|
627
|
-
? `Resource returned an error that's not an Error instance: ${String(error)}. Check this error's .cause for the actual error.`
|
|
628
|
-
: String(error), { cause: error });
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
export { OutputEmitterRef, ResourceImpl, computed, effect, encapsulateResourceError, getOutputDestroyRef, linkedSignal, resource, untracked };
|
|
633
|
-
//# sourceMappingURL=resource.mjs.map
|