Skip to content

Commit

Permalink
refactor(core): cleanup prefetch triggers when resource loading starts (
Browse files Browse the repository at this point in the history
#51856)

This commit adds the necessary mechanisms to perform cleanup of prefetch triggers when resource loading starts. Previously, this logic was missing, which resulted in retaining those triggers.

PR Close #51856
  • Loading branch information
AndrewKushnir authored and dylhunn committed Sep 22, 2023
1 parent 0a4f18a commit 917203d
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 17 deletions.
105 changes: 91 additions & 14 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/

import {InjectionToken, Injector} from '../../di';
import {InjectionToken, Injector, ɵɵdefineInjectable} from '../../di';
import {findMatchingDehydratedView} from '../../hydration/views';
import {populateDehydratedViewsInContainer} from '../../linker/view_container_ref';
import {assertDefined, assertEqual, throwError} from '../../util/assert';
import {assertIndexInDeclRange, assertLContainer, assertTNodeForLView} from '../assert';
import {bindingUpdated} from '../bindings';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockInternalState, DeferBlockState, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, TDeferBlockDetails} from '../interfaces/defer';
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockInternalState, DeferBlockState, DeferBlockTriggers, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, TDeferBlockDetails} from '../interfaces/defer';
import {DirectiveDefList, PipeDefList} from '../interfaces/definition';
import {TContainerNode, TNode} from '../interfaces/node';
import {isDestroyed, isLContainer, isLView} from '../interfaces/type_checks';
Expand Down Expand Up @@ -182,14 +182,20 @@ export function ɵɵdeferPrefetchOnIdle() {
const tDetails = getTDeferBlockDetails(tView, tNode);

if (tDetails.loadingState === DeferDependenciesLoadingState.NOT_STARTED) {
// Set loading to the scheduled state, so that we don't register it again.
tDetails.loadingState = DeferDependenciesLoadingState.SCHEDULED;

// In case of prefetching, we intentionally avoid cancelling prefetching if
// an underlying LView get destroyed (thus passing `null` as a second argument),
// because there might be other LViews (that represent embedded views) that
// depend on resource loading.
onIdle(() => triggerPrefetching(tDetails, lView), null /* LView */);
// Prevent scheduling more than one `requestIdleCallback` call
// for each defer block. For this reason we use only a trigger
// identifier in a key, so all instances would use the same key.
const key = String(DeferBlockTriggers.OnIdle);
const injector = lView[INJECTOR]!;
const manager = injector.get(DeferBlockCleanupManager);
if (!manager.has(tDetails, key)) {
// In case of prefetching, we intentionally avoid cancelling resource loading if
// an underlying LView get destroyed (thus passing `null` as a second argument),
// because there might be other LViews (that represent embedded views) that
// depend on resource loading.
const cleanupFn = onIdle(() => triggerPrefetching(tDetails, lView), null /* LView */);
registerTDetailsCleanup(injector, tDetails, key, cleanupFn);
}
}
}

Expand Down Expand Up @@ -272,7 +278,7 @@ export function ɵɵdeferPrefetchOnViewport(target?: unknown) {} // TODO: imple
* helpful for cases when a defer block has scheduled rendering, but an underlying
* LView got destroyed prior to th block rendering.
*/
function onIdle(callback: VoidFunction, lView: LView|null) {
function onIdle(callback: VoidFunction, lView: LView|null): VoidFunction {
let id: number;
const removeIdleCallback = () => _cancelIdleCallback(id);
id = _requestIdleCallback(() => {
Expand All @@ -291,6 +297,7 @@ function onIdle(callback: VoidFunction, lView: LView|null) {
// is invoked.
storeLViewOnDestroy(lView, removeIdleCallback);
}
return removeIdleCallback;
}

/**
Expand Down Expand Up @@ -423,8 +430,7 @@ export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LVie
const injector = lView[INJECTOR]!;
const tView = lView[TVIEW];

if (tDetails.loadingState !== DeferDependenciesLoadingState.NOT_STARTED &&
tDetails.loadingState !== DeferDependenciesLoadingState.SCHEDULED) {
if (tDetails.loadingState !== DeferDependenciesLoadingState.NOT_STARTED) {
// If the loading status is different from initial one, it means that
// the loading of dependencies is in progress and there is nothing to do
// in this function. All details can be obtained from the `tDetails` object.
Expand Down Expand Up @@ -454,6 +460,10 @@ export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LVie
return;
}

// Defer block may have multiple prefetch triggers. Once the loading
// starts, invoke all clean functions, since they are no longer needed.
invokeTDetailsCleanup(injector, tDetails);

// Start downloading of defer block dependencies.
tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then(results => {
let failed = false;
Expand Down Expand Up @@ -565,7 +575,6 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {

switch (tDetails.loadingState) {
case DeferDependenciesLoadingState.NOT_STARTED:
case DeferDependenciesLoadingState.SCHEDULED:
triggerResourceLoading(tDetails, lView);

// The `loadingState` might have changed to "loading".
Expand Down Expand Up @@ -693,3 +702,71 @@ export function getDeferBlocks(lView: LView, deferBlocks: DeferBlockDetails[]) {
}
}
}

/**
* Registers a cleanup function associated with a prefetching trigger
* of a given defer block.
*/
function registerTDetailsCleanup(
injector: Injector, tDetails: TDeferBlockDetails, key: string, cleanupFn: VoidFunction) {
injector.get(DeferBlockCleanupManager).add(tDetails, key, cleanupFn);
}

/**
* Invokes all registered prefetch cleanup triggers
* and removes all cleanup functions afterwards.
*/
function invokeTDetailsCleanup(injector: Injector, tDetails: TDeferBlockDetails) {
injector.get(DeferBlockCleanupManager).cleanup(tDetails);
}

/**
* Internal service to keep track of cleanup functions associated
* with defer blocks. This class is used to manage cleanup functions
* created for prefetching triggers.
*/
class DeferBlockCleanupManager {
private blocks = new Map<TDeferBlockDetails, Map<string, VoidFunction[]>>();

add(tDetails: TDeferBlockDetails, key: string, callback: VoidFunction) {
if (!this.blocks.has(tDetails)) {
this.blocks.set(tDetails, new Map());
}
const block = this.blocks.get(tDetails)!;
if (!block.has(key)) {
block.set(key, []);
}
const callbacks = block.get(key)!;
callbacks.push(callback);
}

has(tDetails: TDeferBlockDetails, key: string): boolean {
return !!this.blocks.get(tDetails)?.has(key);
}

cleanup(tDetails: TDeferBlockDetails) {
const block = this.blocks.get(tDetails);
if (block) {
for (const callbacks of Object.values(block)) {
for (const callback of callbacks) {
callback();
}
}
this.blocks.delete(tDetails);
}
}

ngOnDestroy() {
for (const [block] of this.blocks) {
this.cleanup(block);
}
this.blocks.clear();
}

/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: DeferBlockCleanupManager,
providedIn: 'root',
factory: () => new DeferBlockCleanupManager(),
});
}
15 changes: 12 additions & 3 deletions packages/core/src/render3/interfaces/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@ import type {DependencyType} from './definition';
*/
export type DependencyResolverFn = () => Array<Promise<DependencyType>>;

/**
* Enumerates all `on` triggers of a defer block.
*/
export const enum DeferBlockTriggers {
OnIdle,
OnTimer,
OnImmediate,
OnHover,
OnInteraction,
OnViewport,
}

/**
* Describes the state of defer block dependency loading.
*/
export enum DeferDependenciesLoadingState {
/** Initial state, dependency loading is not yet triggered */
NOT_STARTED,

/** Dependency loading was scheduled (e.g. `on idle`), but has not started yet */
SCHEDULED,

/** Dependency loading is in progress */
IN_PROGRESS,

Expand Down

0 comments on commit 917203d

Please sign in to comment.