Skip to content

Commit 6c8faaa

Browse files
committed
refactor(core): Rename InitialRenderPendingTasks and restructure isStable observable (angular#53534)
The InitialRenderPendingTasks currently attempts to only contribute to ApplicationRef stableness one time to support SSR. This isn't actually how the switchMap works in reality. This commit updates the isStable observable to be more clear that it's always a combination of the zone stableness and pending tasks. In addition, this commit renames the service to just be PendingTasks because it doesn't directly relate to rendering. While the purpose is to track things that might cause rendering to happen, we don't know if the tasks will affect rendering at all. PR Close angular#53534
1 parent 8bf7525 commit 6c8faaa

File tree

18 files changed

+294
-77
lines changed

18 files changed

+294
-77
lines changed

packages/common/http/src/interceptor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {isPlatformServer} from '@angular/common';
10-
import {EnvironmentInjector, inject, Injectable, InjectionToken, PLATFORM_ID, runInInjectionContext, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵInitialRenderPendingTasks as InitialRenderPendingTasks} from '@angular/core';
10+
import {EnvironmentInjector, inject, Injectable, InjectionToken, PLATFORM_ID, runInInjectionContext, ɵConsole as Console, ɵformatRuntimeError as formatRuntimeError, ɵPendingTasks as PendingTasks} from '@angular/core';
1111
import {Observable} from 'rxjs';
1212
import {finalize} from 'rxjs/operators';
1313

@@ -217,7 +217,7 @@ export function legacyInterceptorFnFactory(): HttpInterceptorFn {
217217
adaptLegacyInterceptorToChain, interceptorChainEndFn as ChainedInterceptorFn<any>);
218218
}
219219

220-
const pendingTasks = inject(InitialRenderPendingTasks);
220+
const pendingTasks = inject(PendingTasks);
221221
const taskId = pendingTasks.add();
222222
return chain(req, handler).pipe(finalize(() => pendingTasks.remove(taskId)));
223223
};
@@ -233,7 +233,7 @@ export function resetFetchBackendWarningFlag() {
233233
@Injectable()
234234
export class HttpInterceptorHandler extends HttpHandler {
235235
private chain: ChainedInterceptorFn<unknown>|null = null;
236-
private readonly pendingTasks = inject(InitialRenderPendingTasks);
236+
private readonly pendingTasks = inject(PendingTasks);
237237

238238
constructor(private backend: HttpBackend, private injector: EnvironmentInjector) {
239239
super();

packages/core/src/application/application_ref.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import '../util/ng_jit_mode';
1010

1111
import {setThrowInvalidWriteToSignalError} from '@angular/core/primitives/signals';
12-
import {Observable, of} from 'rxjs';
13-
import {distinctUntilChanged, first, switchMap} from 'rxjs/operators';
12+
import {combineLatest, Observable} from 'rxjs';
13+
import {distinctUntilChanged, first, map} from 'rxjs/operators';
1414

1515
import {getCompilerFacade, JitCompilerUsage} from '../compiler/compiler_facade';
1616
import {Console} from '../console';
@@ -21,14 +21,14 @@ import {Injector} from '../di/injector';
2121
import {EnvironmentInjector} from '../di/r3_injector';
2222
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../error_handler';
2323
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
24-
import {InitialRenderPendingTasks} from '../initial_render_pending_tasks';
2524
import {Type} from '../interface/type';
2625
import {COMPILER_OPTIONS, CompilerOptions} from '../linker/compiler';
2726
import {ComponentFactory, ComponentRef} from '../linker/component_factory';
2827
import {ComponentFactoryResolver} from '../linker/component_factory_resolver';
2928
import {NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory';
3029
import {ViewRef} from '../linker/view_ref';
3130
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from '../metadata/resource_loading';
31+
import {PendingTasks} from '../pending_tasks';
3232
import {assertNgModuleType} from '../render3/assert';
3333
import {ComponentFactory as R3ComponentFactory} from '../render3/component_ref';
3434
import {isStandalone} from '../render3/definition';
@@ -324,6 +324,8 @@ export class ApplicationRef {
324324
_views: InternalViewRef<unknown>[] = [];
325325
private readonly internalErrorHandler = inject(INTERNAL_APPLICATION_ERROR_HANDLER);
326326
private readonly zoneIsStable = inject(ZONE_IS_STABLE_OBSERVABLE);
327+
private readonly noPendingTasks =
328+
inject(PendingTasks).hasPendingTasks.pipe(map(pending => !pending));
327329

328330
/**
329331
* Indicates whether this instance was destroyed.
@@ -347,9 +349,9 @@ export class ApplicationRef {
347349
* Returns an Observable that indicates when the application is stable or unstable.
348350
*/
349351
public readonly isStable: Observable<boolean> =
350-
inject(InitialRenderPendingTasks)
351-
.hasPendingTasks.pipe(
352-
switchMap(hasPendingTasks => hasPendingTasks ? of(false) : this.zoneIsStable),
352+
combineLatest([this.zoneIsStable, this.noPendingTasks])
353+
.pipe(
354+
map(indicators => indicators.every(stable => stable)),
353355
distinctUntilChanged(),
354356
);
355357

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from
2828
export {SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER} from './hydration/utils';
2929
export {CurrencyIndex as ɵCurrencyIndex, ExtraLocaleDataIndex as ɵExtraLocaleDataIndex, findLocaleData as ɵfindLocaleData, getLocaleCurrencyCode as ɵgetLocaleCurrencyCode, getLocalePluralCase as ɵgetLocalePluralCase, LocaleDataIndex as ɵLocaleDataIndex, registerLocaleData as ɵregisterLocaleData, unregisterAllLocaleData as ɵunregisterLocaleData} from './i18n/locale_data_api';
3030
export {DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID} from './i18n/localization';
31-
export {InitialRenderPendingTasks as ɵInitialRenderPendingTasks} from './initial_render_pending_tasks';
3231
export {Writable as ɵWritable} from './interface/type';
3332
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
3433
export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution as ɵisComponentDefPendingResolution, resolveComponentResources as ɵresolveComponentResources, restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue} from './metadata/resource_loading';
34+
export {PendingTasks as ɵPendingTasks} from './pending_tasks';
3535
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS} from './platform/platform';
3636
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
3737
export {AnimationRendererType as ɵAnimationRendererType} from './render/api';

packages/core/src/initial_render_pending_tasks.ts renamed to packages/core/src/pending_tasks.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ import {Injectable} from './di';
1212
import {OnDestroy} from './interface/lifecycle_hooks';
1313

1414
/**
15-
* *Internal* service that keeps track of pending tasks happening in the system
16-
* during the initial rendering. No tasks are tracked after an initial
17-
* rendering.
15+
* *Internal* service that keeps track of pending tasks happening in the system.
1816
*
1917
* This information is needed to make sure that the serialization on the server
2018
* is delayed until all tasks in the queue (such as an initial navigation or a
2119
* pending HTTP request) are completed.
20+
*
21+
* Pending tasks continue to contribute to the stableness of `ApplicationRef`
22+
* throughout the lifetime of the application.
2223
*/
2324
@Injectable({providedIn: 'root'})
24-
export class InitialRenderPendingTasks implements OnDestroy {
25+
export class PendingTasks implements OnDestroy {
2526
private taskId = 0;
2627
private pendingTasks = new Set<number>();
2728
hasPendingTasks = new BehaviorSubject<boolean>(false);

packages/core/test/acceptance/initial_render_pending_tasks_spec.ts renamed to packages/core/test/acceptance/pending_tasks_spec.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88

99
import {TestBed} from '@angular/core/testing';
1010
import {EMPTY, of} from 'rxjs';
11-
import {map, withLatestFrom} from 'rxjs/operators';
11+
import {map, take, withLatestFrom} from 'rxjs/operators';
1212

13-
import {InitialRenderPendingTasks} from '../../src/initial_render_pending_tasks';
13+
import {ApplicationRef} from '../../src/application/application_ref';
14+
import {PendingTasks} from '../../src/pending_tasks';
1415

15-
describe('InitialRenderPendingTasks', () => {
16+
describe('PendingTasks', () => {
1617
it('should wait until all tasks are completed', async () => {
17-
const pendingTasks = TestBed.inject(InitialRenderPendingTasks);
18+
const pendingTasks = TestBed.inject(PendingTasks);
1819
const taskA = pendingTasks.add();
1920
const taskB = pendingTasks.add();
2021
const taskC = pendingTasks.add();
@@ -26,7 +27,7 @@ describe('InitialRenderPendingTasks', () => {
2627
});
2728

2829
it('should allow calls to remove the same task multiple times', async () => {
29-
const pendingTasks = TestBed.inject(InitialRenderPendingTasks);
30+
const pendingTasks = TestBed.inject(PendingTasks);
3031
expect(await hasPendingTasks(pendingTasks)).toBeFalse();
3132

3233
const taskA = pendingTasks.add();
@@ -40,7 +41,7 @@ describe('InitialRenderPendingTasks', () => {
4041
});
4142

4243
it('should be tolerant to removal of non-existent ids', async () => {
43-
const pendingTasks = TestBed.inject(InitialRenderPendingTasks);
44+
const pendingTasks = TestBed.inject(PendingTasks);
4445
expect(await hasPendingTasks(pendingTasks)).toBeFalse();
4546

4647
pendingTasks.remove(Math.random());
@@ -49,9 +50,28 @@ describe('InitialRenderPendingTasks', () => {
4950

5051
expect(await hasPendingTasks(pendingTasks)).toBeFalse();
5152
});
53+
54+
it('contributes to applicationRef stableness', async () => {
55+
const appRef = TestBed.inject(ApplicationRef);
56+
const pendingTasks = TestBed.inject(PendingTasks);
57+
58+
const taskA = pendingTasks.add();
59+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
60+
pendingTasks.remove(taskA);
61+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(true);
62+
63+
const taskB = pendingTasks.add();
64+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
65+
pendingTasks.remove(taskB);
66+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(true);
67+
});
5268
});
5369

54-
function hasPendingTasks(pendingTasks: InitialRenderPendingTasks): Promise<boolean> {
70+
function applicationRefIsStable(applicationRef: ApplicationRef) {
71+
return applicationRef.isStable.pipe(take(1)).toPromise();
72+
}
73+
74+
function hasPendingTasks(pendingTasks: PendingTasks): Promise<boolean> {
5575
return of(EMPTY)
5676
.pipe(
5777
withLatestFrom(pendingTasks.hasPendingTasks),

packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,6 @@
254254
{
255255
"name": "INTERNAL_BROWSER_PLATFORM_PROVIDERS"
256256
},
257-
{
258-
"name": "InitialRenderPendingTasks"
259-
},
260257
{
261258
"name": "InjectFlags"
262259
},
@@ -410,6 +407,9 @@
410407
{
411408
"name": "PRESERVE_HOST_CONTENT"
412409
},
410+
{
411+
"name": "PendingTasks"
412+
},
413413
{
414414
"name": "R3Injector"
415415
},
@@ -689,6 +689,9 @@
689689
{
690690
"name": "collectNativeNodesInLContainer"
691691
},
692+
{
693+
"name": "combineLatest"
694+
},
692695
{
693696
"name": "computeStaticStyling"
694697
},
@@ -1187,6 +1190,9 @@
11871190
{
11881191
"name": "makeTimingAst"
11891192
},
1193+
{
1194+
"name": "map"
1195+
},
11901196
{
11911197
"name": "markAncestorsForTraversal"
11921198
},
@@ -1202,9 +1208,15 @@
12021208
{
12031209
"name": "markedFeatures"
12041210
},
1211+
{
1212+
"name": "maybeSchedule"
1213+
},
12051214
{
12061215
"name": "maybeWrapInNotSelector"
12071216
},
1217+
{
1218+
"name": "merge"
1219+
},
12081220
{
12091221
"name": "mergeHostAttribute"
12101222
},
@@ -1463,6 +1475,15 @@
14631475
{
14641476
"name": "writeToDirectiveInput"
14651477
},
1478+
{
1479+
"name": "{getPrototypeOf:getPrototypeOf,prototype:objectProto,keys:getKeys}"
1480+
},
1481+
{
1482+
"name": "{isArray:isArray2}"
1483+
},
1484+
{
1485+
"name": "{isArray:isArray}"
1486+
},
14661487
{
14671488
"name": "ɵɵStandaloneFeature"
14681489
},

packages/core/test/bundling/animations/bundle.golden_symbols.json

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,6 @@
275275
{
276276
"name": "INTERNAL_APPLICATION_ERROR_HANDLER"
277277
},
278-
{
279-
"name": "InitialRenderPendingTasks"
280-
},
281278
{
282279
"name": "InjectFlags"
283280
},
@@ -449,6 +446,9 @@
449446
{
450447
"name": "PRESERVE_HOST_CONTENT"
451448
},
449+
{
450+
"name": "PendingTasks"
451+
},
452452
{
453453
"name": "PlatformRef"
454454
},
@@ -749,6 +749,9 @@
749749
{
750750
"name": "collectNativeNodesInLContainer"
751751
},
752+
{
753+
"name": "combineLatest"
754+
},
752755
{
753756
"name": "computeStaticStyling"
754757
},
@@ -1256,6 +1259,9 @@
12561259
{
12571260
"name": "makeTimingAst"
12581261
},
1262+
{
1263+
"name": "map"
1264+
},
12591265
{
12601266
"name": "markAncestorsForTraversal"
12611267
},
@@ -1268,9 +1274,15 @@
12681274
{
12691275
"name": "markViewForRefresh"
12701276
},
1277+
{
1278+
"name": "maybeSchedule"
1279+
},
12711280
{
12721281
"name": "maybeWrapInNotSelector"
12731282
},
1283+
{
1284+
"name": "merge"
1285+
},
12741286
{
12751287
"name": "mergeHostAttribute"
12761288
},
@@ -1535,6 +1547,15 @@
15351547
{
15361548
"name": "writeToDirectiveInput"
15371549
},
1550+
{
1551+
"name": "{getPrototypeOf:getPrototypeOf,prototype:objectProto,keys:getKeys}"
1552+
},
1553+
{
1554+
"name": "{isArray:isArray2}"
1555+
},
1556+
{
1557+
"name": "{isArray:isArray}"
1558+
},
15381559
{
15391560
"name": "ɵɵdefineComponent"
15401561
},

packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,6 @@
179179
{
180180
"name": "INTERNAL_APPLICATION_ERROR_HANDLER"
181181
},
182-
{
183-
"name": "InitialRenderPendingTasks"
184-
},
185182
{
186183
"name": "InjectFlags"
187184
},
@@ -335,6 +332,9 @@
335332
{
336333
"name": "PRESERVE_HOST_CONTENT"
337334
},
335+
{
336+
"name": "PendingTasks"
337+
},
338338
{
339339
"name": "PlatformRef"
340340
},
@@ -557,6 +557,9 @@
557557
{
558558
"name": "collectNativeNodesInLContainer"
559559
},
560+
{
561+
"name": "combineLatest"
562+
},
560563
{
561564
"name": "computeStaticStyling"
562565
},
@@ -1004,6 +1007,9 @@
10041007
{
10051008
"name": "makeRecord"
10061009
},
1010+
{
1011+
"name": "map"
1012+
},
10071013
{
10081014
"name": "markAncestorsForTraversal"
10091015
},
@@ -1016,9 +1022,15 @@
10161022
{
10171023
"name": "markViewForRefresh"
10181024
},
1025+
{
1026+
"name": "maybeSchedule"
1027+
},
10191028
{
10201029
"name": "maybeWrapInNotSelector"
10211030
},
1031+
{
1032+
"name": "merge"
1033+
},
10221034
{
10231035
"name": "mergeHostAttribute"
10241036
},
@@ -1241,6 +1253,15 @@
12411253
{
12421254
"name": "writeToDirectiveInput"
12431255
},
1256+
{
1257+
"name": "{getPrototypeOf:getPrototypeOf,prototype:objectProto,keys:getKeys}"
1258+
},
1259+
{
1260+
"name": "{isArray:isArray2}"
1261+
},
1262+
{
1263+
"name": "{isArray:isArray}"
1264+
},
12441265
{
12451266
"name": "ɵɵdefineComponent"
12461267
},

0 commit comments

Comments
 (0)