Skip to content

Commit a5981b8

Browse files
feat(core): support customization of @defer's on idle behavior
This commit makes the behavior of `on idle` in `@defer` configurable via DI. It defines an `IdleService` interface that an application can implement and provide to Angular: ```ts @Injectable({providedIn: 'root'}) export class CustomIdleService implements IdleService { requestOnIdle(callback: () => void): number {...} cancelOnIdle(id: number): void {...} } ``` Then the idle service can be used by providing the IDLE_SERVICE token with the custom implementation.
1 parent 0cd00b9 commit a5981b8

File tree

5 files changed

+159
-47
lines changed

5 files changed

+159
-47
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,12 @@ export interface HostListenerDecorator {
843843
new (eventName: string, args?: string[]): any;
844844
}
845845

846+
// @public
847+
export interface IdleService {
848+
cancelOnIdle(id: number): void;
849+
requestOnIdle(callback: (deadline?: IdleDeadline) => void): number;
850+
}
851+
846852
// @public
847853
export function importProvidersFrom(...sources: ImportProvidersSource[]): EnvironmentProviders;
848854

@@ -1489,6 +1495,9 @@ export function provideCheckNoChangesConfig(options: {
14891495
// @public
14901496
export function provideEnvironmentInitializer(initializerFn: () => void): EnvironmentProviders;
14911497

1498+
// @public
1499+
export function provideIdleServiceWith(useExisting: AbstractType<IdleService> | InjectionToken<IdleService>): EnvironmentProviders;
1500+
14921501
// @public
14931502
export function provideNgReflectAttributes(): EnvironmentProviders;
14941503

packages/core/src/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export {
131131
AnimationFunction,
132132
MAX_ANIMATION_TIMEOUT,
133133
} from './animation/interfaces';
134+
export {IdleService, provideIdleServiceWith} from './defer/idle_service';
134135

135136
import {global} from './util/global';
136137
if (typeof ngDevMode !== 'undefined' && ngDevMode) {

packages/core/src/defer/idle_scheduler.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import type {OnDestroy} from '../core';
1010
import {Injector, inject, ɵɵdefineInjectable} from '../di';
1111
import {NgZone} from '../zone';
12+
import {IDLE_SERVICE} from './idle_service';
1213

1314
/**
1415
* Helper function to schedule a callback to be invoked when a browser becomes idle.
@@ -23,18 +24,6 @@ export function onIdle(callback: VoidFunction, injector: Injector) {
2324
return cleanupFn;
2425
}
2526

26-
/**
27-
* Use shims for the `requestIdleCallback` and `cancelIdleCallback` functions for
28-
* environments where those functions are not available (e.g. Node.js and Safari).
29-
*
30-
* Note: we wrap the `requestIdleCallback` call into a function, so that it can be
31-
* overridden/mocked in test environment and picked up by the runtime code.
32-
*/
33-
const _requestIdleCallback = () =>
34-
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout;
35-
const _cancelIdleCallback = () =>
36-
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
37-
3827
/**
3928
* Helper service to schedule `requestIdleCallback`s for batches of defer blocks,
4029
* to avoid calling `requestIdleCallback` for each defer block (e.g. if
@@ -48,9 +37,7 @@ export class IdleScheduler implements OnDestroy {
4837
queue = new Set<VoidFunction>();
4938

5039
ngZone = inject(NgZone);
51-
52-
requestIdleCallbackFn = _requestIdleCallback().bind(globalThis);
53-
cancelIdleCallbackFn = _cancelIdleCallback().bind(globalThis);
40+
private readonly idleService = inject(IDLE_SERVICE);
5441

5542
add(callback: VoidFunction) {
5643
this.queue.add(callback);
@@ -90,14 +77,14 @@ export class IdleScheduler implements OnDestroy {
9077
};
9178
// Ensure that the callback runs in the NgZone since
9279
// the `requestIdleCallback` is not currently patched by Zone.js.
93-
this.idleId = this.requestIdleCallbackFn((deadline) =>
80+
this.idleId = this.idleService.requestOnIdle((deadline) =>
9481
this.ngZone.run(() => callback(deadline)),
9582
) as number;
9683
}
9784

9885
private cancelIdleCallback() {
9986
if (this.idleId !== null) {
100-
this.cancelIdleCallbackFn(this.idleId);
87+
this.idleService.cancelOnIdle(this.idleId);
10188
this.idleId = null;
10289
}
10390
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {AbstractType} from '../interface/type';
10+
import {InjectionToken} from '../di/injection_token';
11+
import type {EnvironmentProviders} from '../di/interface/provider';
12+
import {makeEnvironmentProviders} from '../di/provider_collection';
13+
14+
/**
15+
* Use shims for the `requestIdleCallback` and `cancelIdleCallback` functions for
16+
* environments where those functions are not available (e.g. Node.js and Safari).
17+
*
18+
* Note: we wrap the `requestIdleCallback` call into a function, so that it can be
19+
* overridden/mocked in test environment and picked up by the runtime code.
20+
*/
21+
const _requestIdleCallback = () =>
22+
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback.bind(globalThis) : setTimeout;
23+
const _cancelIdleCallback = () =>
24+
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback.bind(globalThis) : clearTimeout;
25+
26+
/**
27+
* Service which configures custom 'on idle' behavior for Angular features like `@defer`.
28+
*
29+
* @publicApi
30+
*/
31+
export interface IdleService {
32+
/**
33+
* Schedule `callback` to be executed when the current application or browser is considered idle.
34+
*
35+
* @returns an id which allows the scheduled callback to be cancelled before it executes.
36+
*/
37+
requestOnIdle(callback: (deadline?: IdleDeadline) => void): number;
38+
39+
/**
40+
* Cancel a previously scheduled callback using the id associated with it.
41+
*/
42+
cancelOnIdle(id: number): void;
43+
}
44+
45+
export const IDLE_SERVICE = new InjectionToken<IdleService>(ngDevMode ? 'IDLE_SERVICE' : '', {
46+
providedIn: 'root',
47+
factory: () => new RequestIdleCallbackService(),
48+
});
49+
50+
/**
51+
* Configures Angular to use the given DI token as its `IdleService`.
52+
*
53+
* The given token must be available for injection from the root injector, and the injected value
54+
* must implement the `IdleService` interface.
55+
*
56+
* @publicApi
57+
*/
58+
export function provideIdleServiceWith(
59+
useExisting: AbstractType<IdleService> | InjectionToken<IdleService>,
60+
): EnvironmentProviders {
61+
return makeEnvironmentProviders([
62+
{
63+
provide: IDLE_SERVICE,
64+
useExisting,
65+
},
66+
]);
67+
}
68+
69+
/**
70+
* Default implementation of `IDLE_SERVICE` which uses `requestIdleCallback` when available or
71+
* `setTimeout` when not.
72+
*/
73+
class RequestIdleCallbackService implements IdleService {
74+
private readonly requestIdleCallback = _requestIdleCallback();
75+
private readonly cancelIdleCallback = _cancelIdleCallback();
76+
77+
requestOnIdle(callback: (deadline?: IdleDeadline) => void): number {
78+
return this.requestIdleCallback(callback) as unknown as number;
79+
}
80+
81+
cancelOnIdle(id: number): void {
82+
return this.cancelIdleCallback(id);
83+
}
84+
}

packages/core/test/acceptance/defer_spec.ts

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
ViewChildren,
4040
ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
4141
} from '../../src/core';
42+
import {IDLE_SERVICE, IdleService, provideIdleServiceWith} from '../../src/defer/idle_service';
4243
import {IdleScheduler} from '../../src/defer/idle_scheduler';
4344
import {TimerScheduler} from '../../src/defer/timer_scheduler';
4445
import {formatRuntimeErrorCode, RuntimeErrorCode} from '../../src/errors';
@@ -1814,8 +1815,31 @@ describe('@defer', () => {
18141815
},
18151816
};
18161817

1818+
@Injectable({providedIn: 'root'})
1819+
class CustomIdleService implements IdleService {
1820+
private callbacks: Array<((deadline?: IdleDeadline) => void) | undefined> = [];
1821+
1822+
requestOnIdle(callback: (deadline?: IdleDeadline) => void): number {
1823+
return this.callbacks.push(callback) - 1;
1824+
}
1825+
1826+
cancelOnIdle(id: number): void {
1827+
this.callbacks[id] = undefined;
1828+
}
1829+
1830+
trigger(): void {
1831+
for (const callback of this.callbacks) {
1832+
callback?.();
1833+
}
1834+
this.callbacks.length = 0;
1835+
}
1836+
}
1837+
18171838
TestBed.configureTestingModule({
1818-
providers: [{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor}],
1839+
providers: [
1840+
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
1841+
provideIdleServiceWith(CustomIdleService),
1842+
],
18191843
});
18201844

18211845
clearDirectiveDefs(RootCmp);
@@ -1828,7 +1852,7 @@ describe('@defer', () => {
18281852
// Make sure loading function is not yet invoked.
18291853
expect(loadingFnInvokedTimes).toBe(0);
18301854

1831-
triggerIdleCallbacks();
1855+
TestBed.inject(CustomIdleService).trigger();
18321856
await allPendingDynamicImports();
18331857
fixture.detectChanges();
18341858

@@ -4429,8 +4453,23 @@ describe('@defer', () => {
44294453

44304454
describe('IdleScheduler', () => {
44314455
let scheduler: IdleScheduler;
4456+
let customIdleService: CustomIdleService;
4457+
4458+
class CustomIdleService implements IdleService {
4459+
requestOnIdleSpy = jasmine.createSpy('requestOnIdleFn');
4460+
4461+
requestOnIdle(callback: (deadline?: IdleDeadline) => void): number {
4462+
return this.requestOnIdleSpy(callback);
4463+
}
4464+
4465+
cancelOnIdle(id: number): void {}
4466+
}
44324467

44334468
beforeEach(() => {
4469+
customIdleService = new CustomIdleService();
4470+
TestBed.configureTestingModule({
4471+
providers: [{provide: IDLE_SERVICE, useValue: customIdleService}],
4472+
});
44344473
scheduler = TestBed.inject(IdleScheduler);
44354474
});
44364475

@@ -4442,13 +4481,11 @@ describe('IdleScheduler', () => {
44424481
let capturedCb: ((deadline: any) => void) | null = null;
44434482
let ricCount = 0;
44444483

4445-
scheduler.requestIdleCallbackFn = jasmine
4446-
.createSpy('requestIdleCallbackFn')
4447-
.and.callFake((cb: any) => {
4448-
ricCount++;
4449-
capturedCb = cb;
4450-
return 100 + ricCount;
4451-
});
4484+
customIdleService.requestOnIdleSpy.and.callFake((cb: any) => {
4485+
ricCount++;
4486+
capturedCb = cb;
4487+
return 100 + ricCount;
4488+
});
44524489

44534490
const cb1 = jasmine.createSpy('cb1');
44544491
const cb2 = jasmine.createSpy('cb2');
@@ -4479,13 +4516,11 @@ describe('IdleScheduler', () => {
44794516
let capturedCb: ((deadline: any) => void) | null = null;
44804517
let ricCount = 0;
44814518

4482-
scheduler.requestIdleCallbackFn = jasmine
4483-
.createSpy('requestIdleCallbackFn')
4484-
.and.callFake((cb: any) => {
4485-
ricCount++;
4486-
capturedCb = cb;
4487-
return 100 + ricCount;
4488-
});
4519+
customIdleService.requestOnIdleSpy.and.callFake((cb: any) => {
4520+
ricCount++;
4521+
capturedCb = cb;
4522+
return 100 + ricCount;
4523+
});
44894524

44904525
const cb1 = jasmine.createSpy('cb1');
44914526
const cb2 = jasmine.createSpy('cb2');
@@ -4540,13 +4575,11 @@ describe('IdleScheduler', () => {
45404575
let capturedCb: ((deadline: any) => void) | null = null;
45414576
let ricCount = 0;
45424577

4543-
scheduler.requestIdleCallbackFn = jasmine
4544-
.createSpy('requestIdleCallbackFn')
4545-
.and.callFake((cb: any) => {
4546-
ricCount++;
4547-
capturedCb = cb;
4548-
return 100 + ricCount;
4549-
});
4578+
customIdleService.requestOnIdleSpy.and.callFake((cb: any) => {
4579+
ricCount++;
4580+
capturedCb = cb;
4581+
return 100 + ricCount;
4582+
});
45504583

45514584
const cb1 = jasmine.createSpy('cb1');
45524585
const cb2 = jasmine.createSpy('cb2');
@@ -4577,13 +4610,11 @@ describe('IdleScheduler', () => {
45774610
let capturedCb: ((deadline: any) => void) | null = null;
45784611
let ricCount = 0;
45794612

4580-
scheduler.requestIdleCallbackFn = jasmine
4581-
.createSpy('requestIdleCallbackFn')
4582-
.and.callFake((cb: any) => {
4583-
ricCount++;
4584-
capturedCb = cb;
4585-
return 100 + ricCount;
4586-
});
4613+
customIdleService.requestOnIdleSpy.and.callFake((cb: any) => {
4614+
ricCount++;
4615+
capturedCb = cb;
4616+
return 100 + ricCount;
4617+
});
45874618

45884619
// Test with undefined (empty arguments, typical of setTimeout)
45894620
let cb1 = jasmine.createSpy('cb1');

0 commit comments

Comments
 (0)