Skip to content

Commit 3ebe6b4

Browse files
atscottpkozlowski-opensource
authored andcommitted
feat(core): Add async run method on ExperimentalPendingTasks (angular#56546)
This helper method is simply a convenience function that reduces some boilerplate with manually adding and removing a task around some asynchronous function. PR Close angular#56546
1 parent 6144612 commit 3ebe6b4

File tree

3 files changed

+80
-3
lines changed

3 files changed

+80
-3
lines changed

goldens/public-api/core/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ export interface ExistingSansProvider {
711711
// @public
712712
export class ExperimentalPendingTasks {
713713
add(): () => void;
714+
run<T>(fn: () => Promise<T>): Promise<T>;
714715
// (undocumented)
715716
static ɵprov: unknown;
716717
}

packages/core/src/pending_tasks.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,35 @@ export class ExperimentalPendingTasks {
100100
};
101101
}
102102

103+
/**
104+
* Runs an asynchronous function and blocks the application's stability until the function completes.
105+
*
106+
* ```
107+
* pendingTasks.run(async () => {
108+
* const userData = await fetch('/api/user');
109+
* this.userData.set(userData);
110+
* });
111+
* ```
112+
*
113+
* Application stability is at least delayed until the next tick after the `run` method resolves
114+
* so it is safe to make additional updates to application state that would require UI synchronization:
115+
*
116+
* ```
117+
* const userData = await pendingTasks.run(() => fetch('/api/user'));
118+
* this.userData.set(userData);
119+
* ```
120+
*
121+
* @param fn The asynchronous function to execute
122+
*/
123+
async run<T>(fn: () => Promise<T>): Promise<T> {
124+
const removeTask = this.add();
125+
try {
126+
return await fn();
127+
} finally {
128+
removeTask();
129+
}
130+
}
131+
103132
/** @nocollapse */
104133
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
105134
token: ExperimentalPendingTasks,

packages/core/test/acceptance/pending_tasks_spec.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import {ApplicationRef, ExperimentalPendingTasks} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
11-
import {EMPTY, of} from 'rxjs';
12-
import {map, take, withLatestFrom} from 'rxjs/operators';
11+
import {EMPTY, firstValueFrom, of} from 'rxjs';
12+
import {filter, map, take, withLatestFrom} from 'rxjs/operators';
1313

1414
import {PendingTasks} from '../../src/pending_tasks';
1515

@@ -80,10 +80,57 @@ describe('public ExperimentalPendingTasks', () => {
8080
TestBed.inject(ApplicationRef).tick();
8181
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(true);
8282
});
83+
84+
it('should allow blocking stability with run', async () => {
85+
const appRef = TestBed.inject(ApplicationRef);
86+
const pendingTasks = TestBed.inject(ExperimentalPendingTasks);
87+
88+
let resolveFn: () => void;
89+
pendingTasks.run(() => {
90+
return new Promise<void>((r) => {
91+
resolveFn = r;
92+
});
93+
});
94+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
95+
resolveFn!();
96+
await expectAsync(TestBed.inject(ApplicationRef).whenStable()).toBeResolved();
97+
});
98+
99+
it('should return the result of the run function', async () => {
100+
const appRef = TestBed.inject(ApplicationRef);
101+
const pendingTasks = TestBed.inject(ExperimentalPendingTasks);
102+
103+
const result = await pendingTasks.run(async () => {
104+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
105+
return 1;
106+
});
107+
108+
expect(result).toBe(1);
109+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
110+
await expectAsync(TestBed.inject(ApplicationRef).whenStable()).toBeResolved();
111+
});
112+
113+
xit('should stop blocking stability if run promise rejects', async () => {
114+
const appRef = TestBed.inject(ApplicationRef);
115+
const pendingTasks = TestBed.inject(ExperimentalPendingTasks);
116+
117+
let rejectFn: () => void;
118+
const task = pendingTasks.run(() => {
119+
return new Promise<void>((_, reject) => {
120+
rejectFn = reject;
121+
});
122+
});
123+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
124+
try {
125+
rejectFn!();
126+
await task;
127+
} catch {}
128+
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(true);
129+
});
83130
});
84131

85132
function applicationRefIsStable(applicationRef: ApplicationRef) {
86-
return applicationRef.isStable.pipe(take(1)).toPromise();
133+
return firstValueFrom(applicationRef.isStable);
87134
}
88135

89136
function hasPendingTasks(pendingTasks: PendingTasks): Promise<boolean> {

0 commit comments

Comments
 (0)