Skip to content

Commit

Permalink
feat(core): provide ExperimentalPendingTasks API
Browse files Browse the repository at this point in the history
The new ExperimentalPendingTasks API lets developers to add and remove
tasks that control applications stability: a pending task prevents
application from being stable.

This API is important for all the use-cases that depend on the concept
of stability and SSR serialization is a notable example.

Closes angular#53381
  • Loading branch information
pkozlowski-opensource committed Apr 24, 2024
1 parent 811fe00 commit 61b026d
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 25 deletions.
17 changes: 17 additions & 0 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,23 @@ export interface ExistingSansProvider {
useExisting: any;
}

// @public
export type ExperimentalPendingTaskHandle = {
__brand: 'experimentalPendingTask';
};

// @public
export abstract class ExperimentalPendingTasks {
// (undocumented)
abstract add(): ExperimentalPendingTaskHandle;
// (undocumented)
abstract remove(task: ExperimentalPendingTaskHandle): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<ExperimentalPendingTasks, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<ExperimentalPendingTasks>;
}

// @public
export interface FactoryProvider extends FactorySansProvider {
multi?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {ApplicationRef} from '../../application/application_ref';
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, inject, Injectable, InjectionToken, makeEnvironmentProviders, StaticProvider} from '../../di';
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {PendingTasks} from '../../pending_tasks';
import {ExperimentalPendingTaskHandle, PendingTasks} from '../../pending_tasks';
import {performanceMarkFeature} from '../../util/performance';
import {NgZone} from '../../zone';
import {InternalNgZoneOptions} from '../../zone/ng_zone';
Expand Down Expand Up @@ -240,7 +240,7 @@ export class ZoneStablePendingTask {
}
this.initialized = true;

let task: number|null = null;
let task: ExperimentalPendingTaskHandle|null = null;
if (!this.zone.isStable && !this.zone.hasPendingMacrotasks && !this.zone.hasPendingMicrotasks) {
task = this.pendingTasks.add();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {inject} from '../../di/injector_compatibility';
import {EnvironmentProviders} from '../../di/interface/provider';
import {makeEnvironmentProviders} from '../../di/provider_collection';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {PendingTasks} from '../../pending_tasks';
import {ExperimentalPendingTaskHandle, PendingTasks} from '../../pending_tasks';
import {scheduleCallbackWithMicrotask, scheduleCallbackWithRafRace} from '../../util/callback_scheduler';
import {performanceMarkFeature} from '../../util/performance';
import {NgZone, NoopNgZone} from '../../zone/ng_zone';
Expand Down Expand Up @@ -45,7 +45,7 @@ function trackMicrotaskNotificationForDebugging() {
export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
private appRef = inject(ApplicationRef);
private taskService = inject(PendingTasks);
private pendingRenderTaskId: number|null = null;
private pendingRenderTaskId: ExperimentalPendingTaskHandle|null = null;
private shouldRefreshViews = false;
private readonly ngZone = inject(NgZone);
runningTick = false;
Expand Down Expand Up @@ -225,8 +225,8 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
* ```
*
* This API is experimental. Neither the shape, nor the underlying behavior is stable and can change
* in patch versions. There are known feature gaps, including the lack of a public zoneless API
* which prevents the application from serializing too early with SSR.
* in patch versions. There are known feature gaps and API ergonomic considerations. We will iterate
* on the exact API based on the feedback and our understanding of the problem and solution space.
*
* @publicApi
* @experimental
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {PlatformRef} from './platform/platform_ref';
export {createPlatform, createPlatformFactory, assertPlatform, destroyPlatform, getPlatform} from './platform/platform';
export {provideZoneChangeDetection, NgZoneOptions} from './change_detection/scheduling/ng_zone_scheduling';
export {provideExperimentalZonelessChangeDetection} from './change_detection/scheduling/zoneless_scheduling_impl';
export {ExperimentalPendingTasks, ExperimentalPendingTaskHandle} from './pending_tasks';
export {enableProdMode, isDevMode} from './util/is_dev_mode';
export {APP_ID, PACKAGE_ROOT_URL, PLATFORM_INITIALIZER, PLATFORM_ID, ANIMATION_MODULE_TYPE, CSP_NONCE} from './application/application_tokens';
export {APP_INITIALIZER, ApplicationInitStatus} from './application/application_init';
Expand Down
75 changes: 61 additions & 14 deletions packages/core/src/pending_tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,31 @@

import {BehaviorSubject} from 'rxjs';

import {Injectable} from './di';
import {Injectable} from './di/injectable';
import {OnDestroy} from './interface/lifecycle_hooks';

/**
* *Internal* service that keeps track of pending tasks happening in the system.
*
* This information is needed to make sure that the serialization on the server
* is delayed until all tasks in the queue (such as an initial navigation or a
* pending HTTP request) are completed.
*
* Pending tasks continue to contribute to the stableness of `ApplicationRef`
* throughout the lifetime of the application.
* Internal implementation of the pending tasks service.
*/
@Injectable({providedIn: 'root'})
export class PendingTasks implements OnDestroy {
export class PendingTasks implements ExperimentalPendingTasks, OnDestroy {
private taskId = 0;
private pendingTasks = new Set<number>();
private pendingTasks = new Set<ExperimentalPendingTaskHandle>();
private get _hasPendingTasks() {
return this.hasPendingTasks.value;
}
hasPendingTasks = new BehaviorSubject<boolean>(false);

add(): number {
add(): ExperimentalPendingTaskHandle {
if (!this._hasPendingTasks) {
this.hasPendingTasks.next(true);
}
const taskId = this.taskId++;
const taskId = this.taskId++ as unknown as ExperimentalPendingTaskHandle;
this.pendingTasks.add(taskId);
return taskId;
}

remove(taskId: number): void {
remove(taskId: ExperimentalPendingTaskHandle): void {
this.pendingTasks.delete(taskId);
if (this.pendingTasks.size === 0 && this._hasPendingTasks) {
this.hasPendingTasks.next(false);
Expand All @@ -53,3 +46,57 @@ export class PendingTasks implements OnDestroy {
}
}
}

/**
* An opaque handle representing a pending task. Users should not assume anything about the internal
* type or structure of this handle.
*
* @publicApi
* @experimental
*/
export type ExperimentalPendingTaskHandle = {
__brand: 'experimentalPendingTask'
};

/**
* Experimental service that keeps track of pending tasks contributing to the stableness of Angular
* application. While several existing Angular services (ex.: `HttpClient`) will internally manage
* tasks influencing stability, this API gives control over stability to library and application
* developers for specific cases not covered by Angular internals.
*
* The concept of stability comes into play in several important scenarios:
* - SSR process needs to wait for the application stability before serializing and sending rendered
* HTML;
* - tests might want to delay assertions until the application becomes stable;
*
* @usageNotes
* ```typescript
* const pendingTasks = inject(ExperimentalPendingTasks);
* const task = pendingTasks.add();
* // do work that should block application's stability and then:
* pendingTasks.remove(task);
* ```
*
* This API is experimental. Neither the shape, nor the underlying behavior is stable and can change
* in patch versions. We will iterate on the exact API based on the feedback and our understanding
* of the problem and solution space.
*
* @publicApi
* @experimental
*/
@Injectable({
providedIn: 'root',
useExisting: PendingTasks,
})
export abstract class ExperimentalPendingTasks {
/**
* Adds a new task that should block application's stability.
* @returns An opaque task handle that can be used to remove a task.
*/
abstract add(): ExperimentalPendingTaskHandle;

/**
* Removes a task given its handle.
*/
abstract remove(task: ExperimentalPendingTaskHandle): void;
}
4 changes: 2 additions & 2 deletions packages/core/src/render3/reactivity/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {FLAGS, LViewFlags, EFFECTS_TO_SCHEDULE} from '../interfaces/view';

import {assertNotInReactiveContext} from './asserts';
import {performanceMarkFeature} from '../../util/performance';
import {PendingTasks} from '../../pending_tasks';
import {ExperimentalPendingTaskHandle, PendingTasks} from '../../pending_tasks';


/**
Expand Down Expand Up @@ -86,7 +86,7 @@ export class ZoneAwareEffectScheduler implements EffectScheduler {
private queuedEffectCount = 0;
private queues = new Map<Zone|null, Set<SchedulableEffect>>();
private readonly pendingTasks = inject(PendingTasks);
private taskId: number|null = null;
private taskId: ExperimentalPendingTaskHandle|null = null;

scheduleEffect(handle: SchedulableEffect): void {
this.enqueue(handle);
Expand Down
7 changes: 4 additions & 3 deletions packages/core/test/acceptance/pending_tasks_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ExperimentalPendingTaskHandle} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {EMPTY, of} from 'rxjs';
import {map, take, withLatestFrom} from 'rxjs/operators';
Expand Down Expand Up @@ -44,9 +45,9 @@ describe('PendingTasks', () => {
const pendingTasks = TestBed.inject(PendingTasks);
expect(await hasPendingTasks(pendingTasks)).toBeFalse();

pendingTasks.remove(Math.random());
pendingTasks.remove(Math.random());
pendingTasks.remove(Math.random());
pendingTasks.remove(Math.random() as unknown as ExperimentalPendingTaskHandle);
pendingTasks.remove(Math.random() as unknown as ExperimentalPendingTaskHandle);
pendingTasks.remove(Math.random() as unknown as ExperimentalPendingTaskHandle);

expect(await hasPendingTasks(pendingTasks)).toBeFalse();
});
Expand Down

0 comments on commit 61b026d

Please sign in to comment.