diff --git a/goldens/public-api/core/index.api.md b/goldens/public-api/core/index.api.md index 79fac41c5876..89e0b71898e2 100644 --- a/goldens/public-api/core/index.api.md +++ b/goldens/public-api/core/index.api.md @@ -711,6 +711,7 @@ export interface ExistingSansProvider { // @public export class ExperimentalPendingTasks { add(): () => void; + run(fn: () => Promise): Promise; // (undocumented) static ɵprov: unknown; } diff --git a/packages/core/src/pending_tasks.ts b/packages/core/src/pending_tasks.ts index 59b885e7f91b..e6b91c4f4c82 100644 --- a/packages/core/src/pending_tasks.ts +++ b/packages/core/src/pending_tasks.ts @@ -100,6 +100,35 @@ export class ExperimentalPendingTasks { }; } + /** + * Runs an asynchronous function and blocks the application's stability until the function completes. + * + * ``` + * pendingTasks.run(async () => { + * const userData = await fetch('/api/user'); + * this.userData.set(userData); + * }); + * ``` + * + * Application stability is at least delayed until the next tick after the `run` method resolves + * so it is safe to make additional updates to application state that would require UI synchronization: + * + * ``` + * const userData = await pendingTasks.run(() => fetch('/api/user')); + * this.userData.set(userData); + * ``` + * + * @param fn The asynchronous function to execute + */ + async run(fn: () => Promise): Promise { + const removeTask = this.add(); + try { + return await fn(); + } finally { + removeTask(); + } + } + /** @nocollapse */ static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({ token: ExperimentalPendingTasks, diff --git a/packages/core/test/acceptance/pending_tasks_spec.ts b/packages/core/test/acceptance/pending_tasks_spec.ts index a3fa0dea4580..84d9ba0fd606 100644 --- a/packages/core/test/acceptance/pending_tasks_spec.ts +++ b/packages/core/test/acceptance/pending_tasks_spec.ts @@ -8,8 +8,8 @@ import {ApplicationRef, ExperimentalPendingTasks} from '@angular/core'; import {TestBed} from '@angular/core/testing'; -import {EMPTY, of} from 'rxjs'; -import {map, take, withLatestFrom} from 'rxjs/operators'; +import {EMPTY, firstValueFrom, of} from 'rxjs'; +import {filter, map, take, withLatestFrom} from 'rxjs/operators'; import {PendingTasks} from '../../src/pending_tasks'; @@ -80,10 +80,57 @@ describe('public ExperimentalPendingTasks', () => { TestBed.inject(ApplicationRef).tick(); await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(true); }); + + it('should allow blocking stability with run', async () => { + const appRef = TestBed.inject(ApplicationRef); + const pendingTasks = TestBed.inject(ExperimentalPendingTasks); + + let resolveFn: () => void; + pendingTasks.run(() => { + return new Promise((r) => { + resolveFn = r; + }); + }); + await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false); + resolveFn!(); + await expectAsync(TestBed.inject(ApplicationRef).whenStable()).toBeResolved(); + }); + + it('should return the result of the run function', async () => { + const appRef = TestBed.inject(ApplicationRef); + const pendingTasks = TestBed.inject(ExperimentalPendingTasks); + + const result = await pendingTasks.run(async () => { + await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false); + return 1; + }); + + expect(result).toBe(1); + await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false); + await expectAsync(TestBed.inject(ApplicationRef).whenStable()).toBeResolved(); + }); + + xit('should stop blocking stability if run promise rejects', async () => { + const appRef = TestBed.inject(ApplicationRef); + const pendingTasks = TestBed.inject(ExperimentalPendingTasks); + + let rejectFn: () => void; + const task = pendingTasks.run(() => { + return new Promise((_, reject) => { + rejectFn = reject; + }); + }); + await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false); + try { + rejectFn!(); + await task; + } catch {} + await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(true); + }); }); function applicationRefIsStable(applicationRef: ApplicationRef) { - return applicationRef.isStable.pipe(take(1)).toPromise(); + return firstValueFrom(applicationRef.isStable); } function hasPendingTasks(pendingTasks: PendingTasks): Promise {