refactor(core): better organization of @defer runtime code (#52152)

This commit splits the `render3/instructions/defer.ts` file (that contained most of the runtime code) into smalle
r files that are easier to maintain.

There are no functional changes in this PR, just organizing code.

PR Close #52152
This commit is contained in:
Andrew Kushnir 2023-10-10 16:09:21 -07:00 committed by Pawel Kozlowski
parent 11c29f9cc3
commit 908f660a72
13 changed files with 859 additions and 763 deletions

View file

@ -11,6 +11,9 @@ export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateAp
export {ENABLED_SSR_FEATURES as ɵENABLED_SSR_FEATURES, IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_DEFAULTS, ImageConfig as ɵImageConfig} from './application_tokens';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {Console as ɵConsole} from './console';
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks} from './defer/discovery';
export {renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './defer/instructions';
export {DeferBlockBehavior as ɵDeferBlockBehavior, DeferBlockConfig as ɵDeferBlockConfig, DeferBlockState as ɵDeferBlockState} from './defer/interfaces';
export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility';
export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDeclaration, ɵɵInjectorDef} from './di/interface/defs';
export {InternalEnvironmentProviders as ɵInternalEnvironmentProviders, isEnvironmentProviders as ɵisEnvironmentProviders} from './di/interface/provider';
@ -29,8 +32,6 @@ export {ComponentFactory as ɵComponentFactory} from './linker/component_factory
export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution as ɵisComponentDefPendingResolution, resolveComponentResources as ɵresolveComponentResources, restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue} from './metadata/resource_loading';
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {InjectorProfilerContext as ɵInjectorProfilerContext, setInjectorProfilerContext as ɵsetInjectorProfilerContext} from './render3/debug/injector_profiler';
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks, renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './render3/instructions/defer';
export {DeferBlockBehavior as ɵDeferBlockBehavior, DeferBlockConfig as ɵDeferBlockConfig, DeferBlockState as ɵDeferBlockState} from './render3/interfaces/defer';
export {allowSanitizationBypassAndThrow as ɵallowSanitizationBypassAndThrow, BypassType as ɵBypassType, getSanitizationBypassType as ɵgetSanitizationBypassType, SafeHtml as ɵSafeHtml, SafeResourceUrl as ɵSafeResourceUrl, SafeScript as ɵSafeScript, SafeStyle as ɵSafeStyle, SafeUrl as ɵSafeUrl, SafeValue as ɵSafeValue, unwrapSafeValue as ɵunwrapSafeValue} from './sanitization/bypass';
export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';

View file

@ -0,0 +1,77 @@
/*!
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injector, ɵɵdefineInjectable} from '../di';
import {TDeferBlockDetails} from './interfaces';
/**
* Registers a cleanup function associated with a prefetching trigger
* of a given defer block.
*/
export 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.
*/
export 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.
*/
export 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(),
});
}

View file

@ -0,0 +1,60 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CONTAINER_HEADER_OFFSET, LContainer} from '../render3/interfaces/container';
import {TNode} from '../render3/interfaces/node';
import {isLContainer, isLView} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, LView, TVIEW} from '../render3/interfaces/view';
import {TDeferBlockDetails} from './interfaces';
import {getTDeferBlockDetails, isTDeferBlockDetails} from './utils';
/**
* Defer block instance for testing.
*/
export interface DeferBlockDetails {
lContainer: LContainer;
lView: LView;
tNode: TNode;
tDetails: TDeferBlockDetails;
}
/**
* Retrieves all defer blocks in a given LView.
*
* @param lView lView with defer blocks
* @param deferBlocks defer block aggregator array
*/
export function getDeferBlocks(lView: LView, deferBlocks: DeferBlockDetails[]) {
const tView = lView[TVIEW];
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
if (isLContainer(lView[i])) {
const lContainer = lView[i];
// An LContainer may represent an instance of a defer block, in which case
// we store it as a result. Otherwise, keep iterating over LContainer views and
// look for defer blocks.
const isLast = i === tView.bindingStartIndex - 1;
if (!isLast) {
const tNode = tView.data[i] as TNode;
const tDetails = getTDeferBlockDetails(tView, tNode);
if (isTDeferBlockDetails(tDetails)) {
deferBlocks.push({lContainer, lView, tNode, tDetails});
// This LContainer represents a defer block, so we exit
// this iteration and don't inspect views in this LContainer.
continue;
}
}
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
getDeferBlocks(lContainer[i] as LView, deferBlocks);
}
} else if (isLView(lView[i])) {
// This is a component, enter the `getDeferBlocks` recursively.
getDeferBlocks(lView[i], deferBlocks);
}
}
}

View file

@ -6,8 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {inject, Injector, ɵɵdefineInjectable} from '../../di';
import {NgZone} from '../../zone';
import {inject, Injector, ɵɵdefineInjectable} from '../di';
import {afterRender} from '../render3/after_render_hooks';
import {assertLContainer, assertLView} from '../render3/assert';
import {CONTAINER_HEADER_OFFSET} from '../render3/interfaces/container';
import {TNode} from '../render3/interfaces/node';
import {FLAGS, HEADER_OFFSET, INJECTOR, LView, LViewFlags} from '../render3/interfaces/view';
import {getNativeByIndex, removeLViewOnDestroy, storeLViewOnDestroy, walkUpViews} from '../render3/util/view_utils';
import {assertElement, assertEqual} from '../util/assert';
import {NgZone} from '../zone';
import {DEFER_BLOCK_STATE, DeferBlockInternalState, DeferBlockState} from './interfaces';
import {getLDeferBlockDetails} from './utils';
/** Configuration object used to register passive and capturing events. */
const eventListenerOptions: AddEventListenerOptions = {
@ -205,3 +215,117 @@ class DeferIntersectionManager {
}
}
}
/**
* Helper function to get the LView in which a deferred block's trigger is rendered.
* @param deferredHostLView LView in which the deferred block is defined.
* @param deferredTNode TNode defining the deferred block.
* @param walkUpTimes Number of times to go up in the view hierarchy to find the trigger's view.
* A negative value means that the trigger is inside the block's placeholder, while an undefined
* value means that the trigger is in the same LView as the deferred block.
*/
export function getTriggerLView(
deferredHostLView: LView, deferredTNode: TNode, walkUpTimes: number|undefined): LView|null {
// The trigger is in the same view, we don't need to traverse.
if (walkUpTimes == null) {
return deferredHostLView;
}
// A positive value or zero means that the trigger is in a parent view.
if (walkUpTimes >= 0) {
return walkUpViews(walkUpTimes, deferredHostLView);
}
// If the value is negative, it means that the trigger is inside the placeholder.
const deferredContainer = deferredHostLView[deferredTNode.index];
ngDevMode && assertLContainer(deferredContainer);
const triggerLView = deferredContainer[CONTAINER_HEADER_OFFSET] ?? null;
// We need to null check, because the placeholder might not have been rendered yet.
if (ngDevMode && triggerLView !== null) {
const lDetails = getLDeferBlockDetails(deferredHostLView, deferredTNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
assertEqual(
renderedState, DeferBlockState.Placeholder,
'Expected a placeholder to be rendered in this defer block.');
assertLView(triggerLView);
}
return triggerLView;
}
/**
* Gets the element that a deferred block's trigger is pointing to.
* @param triggerLView LView in which the trigger is defined.
* @param triggerIndex Index at which the trigger element should've been rendered.
*/
export function getTriggerElement(triggerLView: LView, triggerIndex: number): Element {
const element = getNativeByIndex(HEADER_OFFSET + triggerIndex, triggerLView);
ngDevMode && assertElement(element);
return element as Element;
}
/**
* Registers a DOM-node based trigger.
* @param initialLView LView in which the defer block is rendered.
* @param tNode TNode representing the defer block.
* @param triggerIndex Index at which to find the trigger element.
* @param walkUpTimes Number of times to go up/down in the view hierarchy to find the trigger.
* @param registerFn Function that will register the DOM events.
* @param callback Callback to be invoked when the trigger receives the event that should render
* the deferred block.
*/
export function registerDomTrigger(
initialLView: LView, tNode: TNode, triggerIndex: number, walkUpTimes: number|undefined,
registerFn: (element: Element, callback: VoidFunction, injector: Injector) => VoidFunction,
callback: VoidFunction) {
const injector = initialLView[INJECTOR]!;
// Assumption: the `afterRender` reference should be destroyed
// automatically so we don't need to keep track of it.
const afterRenderRef = afterRender(() => {
const lDetails = getLDeferBlockDetails(initialLView, tNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
// If the block was loaded before the trigger was resolved, we don't need to do anything.
if (renderedState !== DeferBlockInternalState.Initial &&
renderedState !== DeferBlockState.Placeholder) {
afterRenderRef.destroy();
return;
}
const triggerLView = getTriggerLView(initialLView, tNode, walkUpTimes);
// Keep polling until we resolve the trigger's LView.
// `afterRender` should stop automatically if the view is destroyed.
if (!triggerLView) {
return;
}
// It's possible that the trigger's view was destroyed before we resolved the trigger element.
if (triggerLView[FLAGS] & LViewFlags.Destroyed) {
afterRenderRef.destroy();
return;
}
// TODO: add integration with `DeferBlockCleanupManager`.
const element = getTriggerElement(triggerLView, triggerIndex);
const cleanup = registerFn(element, () => {
callback();
removeLViewOnDestroy(triggerLView, cleanup);
if (initialLView !== triggerLView) {
removeLViewOnDestroy(initialLView, cleanup);
}
cleanup();
}, injector);
afterRenderRef.destroy();
storeLViewOnDestroy(triggerLView, cleanup);
// Since the trigger and deferred block might be in different
// views, we have to register the callback in both locations.
if (initialLView !== triggerLView) {
storeLViewOnDestroy(initialLView, cleanup);
}
}, {injector});
}

View file

@ -0,0 +1,128 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {inject, ɵɵdefineInjectable} from '../di';
import {INJECTOR, LView} from '../render3/interfaces/view';
import {NgZone} from '../zone';
import {wrapWithLViewCleanup} from './utils';
/**
* Helper function to schedule a callback to be invoked when a browser becomes idle.
*
* @param callback A function to be invoked when a browser becomes idle.
* @param lView LView that hosts an instance of a defer block.
* @param withLViewCleanup A flag that indicates whether a scheduled callback
* should be cancelled in case an LView is destroyed before a callback
* was invoked.
*/
export function onIdle(callback: VoidFunction, lView: LView, withLViewCleanup: boolean) {
const injector = lView[INJECTOR]!;
const scheduler = injector.get(IdleScheduler);
const cleanupFn = () => scheduler.remove(callback);
const wrappedCallback =
withLViewCleanup ? wrapWithLViewCleanup(callback, lView, cleanupFn) : callback;
scheduler.add(wrappedCallback);
return cleanupFn;
}
/**
* Use shims for the `requestIdleCallback` and `cancelIdleCallback` functions for
* environments where those functions are not available (e.g. Node.js and Safari).
*
* Note: we wrap the `requestIdleCallback` call into a function, so that it can be
* overridden/mocked in test environment and picked up by the runtime code.
*/
const _requestIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout;
const _cancelIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
/**
* Helper service to schedule `requestIdleCallback`s for batches of defer blocks,
* to avoid calling `requestIdleCallback` for each defer block (e.g. if
* defer blocks are defined inside a for loop).
*/
export class IdleScheduler {
// Indicates whether current callbacks are being invoked.
executingCallbacks = false;
// Currently scheduled idle callback id.
idleId: number|null = null;
// Set of callbacks to be invoked next.
current = new Set<VoidFunction>();
// Set of callbacks collected while invoking current set of callbacks.
// Those callbacks are scheduled for the next idle period.
deferred = new Set<VoidFunction>();
ngZone = inject(NgZone);
requestIdleCallback = _requestIdleCallback().bind(globalThis);
cancelIdleCallback = _cancelIdleCallback().bind(globalThis);
add(callback: VoidFunction) {
const target = this.executingCallbacks ? this.deferred : this.current;
target.add(callback);
if (this.idleId === null) {
this.scheduleIdleCallback();
}
}
remove(callback: VoidFunction) {
this.current.delete(callback);
this.deferred.delete(callback);
}
private scheduleIdleCallback() {
const callback = () => {
this.cancelIdleCallback(this.idleId!);
this.idleId = null;
this.executingCallbacks = true;
for (const callback of this.current) {
callback();
}
this.current.clear();
this.executingCallbacks = false;
// If there are any callbacks added during an invocation
// of the current ones - make them "current" and schedule
// a new idle callback.
if (this.deferred.size > 0) {
for (const callback of this.deferred) {
this.current.add(callback);
}
this.deferred.clear();
this.scheduleIdleCallback();
}
};
// Ensure that the callback runs in the NgZone since
// the `requestIdleCallback` is not currently patched by Zone.js.
this.idleId = this.requestIdleCallback(() => this.ngZone.run(callback)) as number;
}
ngOnDestroy() {
if (this.idleId !== null) {
this.cancelIdleCallback(this.idleId);
this.idleId = null;
}
this.current.clear();
this.deferred.clear();
}
/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: IdleScheduler,
providedIn: 'root',
factory: () => new IdleScheduler(),
});
}

View file

@ -6,30 +6,47 @@
* found in the LICENSE file at https://angular.io/license
*/
import {inject, InjectionToken, Injector, ɵɵdefineInjectable} from '../../di';
import {findMatchingDehydratedView} from '../../hydration/views';
import {populateDehydratedViewsInLContainer} from '../../linker/view_container_ref';
import {arrayInsert2, arraySplice} from '../../util/array_utils';
import {assertDefined, assertElement, assertEqual, throwError} from '../../util/assert';
import {NgZone} from '../../zone';
import {afterRender} from '../after_render_hooks';
import {assertIndexInDeclRange, assertLContainer, assertLView, 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, DeferBlockTriggers, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, LOADING_AFTER_CLEANUP_FN, LOADING_AFTER_SLOT, MINIMUM_SLOT, NEXT_DEFER_BLOCK_STATE, STATE_IS_FROZEN_UNTIL, TDeferBlockDetails} from '../interfaces/defer';
import {DependencyDef, DirectiveDefList, PipeDefList} from '../interfaces/definition';
import {TContainerNode, TNode} from '../interfaces/node';
import {isDestroyed, isLContainer, isLView} from '../interfaces/type_checks';
import {FLAGS, HEADER_OFFSET, INJECTOR, LView, LViewFlags, PARENT, TVIEW, TView} from '../interfaces/view';
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
import {isPlatformBrowser} from '../util/misc_utils';
import {getConstant, getNativeByIndex, getTNode, removeLViewOnDestroy, storeLViewOnDestroy, walkUpViews} from '../util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer, shouldAddViewToDom} from '../view_manipulation';
import {InjectionToken, Injector} from '../di';
import {findMatchingDehydratedView} from '../hydration/views';
import {populateDehydratedViewsInLContainer} from '../linker/view_container_ref';
import {assertLContainer, assertTNodeForLView} from '../render3/assert';
import {bindingUpdated} from '../render3/bindings';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../render3/definition';
import {markViewDirty} from '../render3/instructions/mark_view_dirty';
import {ɵɵtemplate} from '../render3/instructions/template';
import {LContainer} from '../render3/interfaces/container';
import {DirectiveDefList, PipeDefList} from '../render3/interfaces/definition';
import {TContainerNode, TNode} from '../render3/interfaces/node';
import {isDestroyed} from '../render3/interfaces/type_checks';
import {HEADER_OFFSET, INJECTOR, LView, PARENT, TVIEW, TView} from '../render3/interfaces/view';
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../render3/state';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {getConstant, getTNode} from '../render3/util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer, shouldAddViewToDom} from '../render3/view_manipulation';
import {assertDefined, throwError} from '../util/assert';
import {onHover, onInteraction, onViewport} from './defer_events';
import {markViewDirty} from './mark_view_dirty';
import {ɵɵtemplate} from './template';
import {DeferBlockCleanupManager, invokeTDetailsCleanup, registerTDetailsCleanup} from './cleanup';
import {onHover, onInteraction, onViewport, registerDomTrigger} from './dom_triggers';
import {onIdle} from './idle_scheduler';
import {DEFER_BLOCK_STATE, DeferBlockBehavior, DeferBlockConfig, DeferBlockDependencyInterceptor, DeferBlockInternalState, DeferBlockState, DeferBlockTriggers, DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, LDeferBlockDetails, LOADING_AFTER_CLEANUP_FN, NEXT_DEFER_BLOCK_STATE, STATE_IS_FROZEN_UNTIL, TDeferBlockDetails} from './interfaces';
import {onTimer, scheduleTimerTrigger} from './timer_scheduler';
import {addDepsToRegistry, assertDeferredDependenciesLoaded, getLDeferBlockDetails, getLoadingBlockAfter, getMinimumDurationForState, getPrimaryBlockTNode, getTDeferBlockDetails, getTemplateIndexForState, setLDeferBlockDetails, setTDeferBlockDetails} from './utils';
/**
* **INTERNAL**, avoid referencing it in application code.
*
* Injector token that allows to provide `DeferBlockDependencyInterceptor` class
* implementation.
*/
export const DEFER_BLOCK_DEPENDENCY_INTERCEPTOR =
new InjectionToken<DeferBlockDependencyInterceptor>(
ngDevMode ? 'DEFER_BLOCK_DEPENDENCY_INTERCEPTOR' : '');
/**
* **INTERNAL**, token used for configuring defer block behavior.
*/
export const DEFER_BLOCK_CONFIG =
new InjectionToken<DeferBlockConfig>(ngDevMode ? 'DEFER_BLOCK_CONFIG' : '');
/**
* Returns whether defer blocks should be triggered.
@ -407,265 +424,6 @@ function scheduleDelayedPrefetching(
}
}
/**
* Helper function to get the LView in which a deferred block's trigger is rendered.
* @param deferredHostLView LView in which the deferred block is defined.
* @param deferredTNode TNode defining the deferred block.
* @param walkUpTimes Number of times to go up in the view hierarchy to find the trigger's view.
* A negative value means that the trigger is inside the block's placeholder, while an undefined
* value means that the trigger is in the same LView as the deferred block.
*/
function getTriggerLView(
deferredHostLView: LView, deferredTNode: TNode, walkUpTimes: number|undefined): LView|null {
// The trigger is in the same view, we don't need to traverse.
if (walkUpTimes == null) {
return deferredHostLView;
}
// A positive value or zero means that the trigger is in a parent view.
if (walkUpTimes >= 0) {
return walkUpViews(walkUpTimes, deferredHostLView);
}
// If the value is negative, it means that the trigger is inside the placeholder.
const deferredContainer = deferredHostLView[deferredTNode.index];
ngDevMode && assertLContainer(deferredContainer);
const triggerLView = deferredContainer[CONTAINER_HEADER_OFFSET] ?? null;
// We need to null check, because the placeholder might not have been rendered yet.
if (ngDevMode && triggerLView !== null) {
const lDetails = getLDeferBlockDetails(deferredHostLView, deferredTNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
assertEqual(
renderedState, DeferBlockState.Placeholder,
'Expected a placeholder to be rendered in this defer block.');
assertLView(triggerLView);
}
return triggerLView;
}
/**
* Gets the element that a deferred block's trigger is pointing to.
* @param triggerLView LView in which the trigger is defined.
* @param triggerIndex Index at which the trigger element should've been rendered.
*/
function getTriggerElement(triggerLView: LView, triggerIndex: number): Element {
const element = getNativeByIndex(HEADER_OFFSET + triggerIndex, triggerLView);
ngDevMode && assertElement(element);
return element as Element;
}
/**
* Registers a DOM-node based trigger.
* @param initialLView LView in which the defer block is rendered.
* @param tNode TNode representing the defer block.
* @param triggerIndex Index at which to find the trigger element.
* @param walkUpTimes Number of times to go up/down in the view hierarchy to find the trigger.
* @param registerFn Function that will register the DOM events.
* @param callback Callback to be invoked when the trigger receives the event that should render
* the deferred block.
*/
function registerDomTrigger(
initialLView: LView, tNode: TNode, triggerIndex: number, walkUpTimes: number|undefined,
registerFn: (element: Element, callback: VoidFunction, injector: Injector) => VoidFunction,
callback: VoidFunction) {
const injector = initialLView[INJECTOR]!;
// Assumption: the `afterRender` reference should be destroyed
// automatically so we don't need to keep track of it.
const afterRenderRef = afterRender(() => {
const lDetails = getLDeferBlockDetails(initialLView, tNode);
const renderedState = lDetails[DEFER_BLOCK_STATE];
// If the block was loaded before the trigger was resolved, we don't need to do anything.
if (renderedState !== DeferBlockInternalState.Initial &&
renderedState !== DeferBlockState.Placeholder) {
afterRenderRef.destroy();
return;
}
const triggerLView = getTriggerLView(initialLView, tNode, walkUpTimes);
// Keep polling until we resolve the trigger's LView.
// `afterRender` should stop automatically if the view is destroyed.
if (!triggerLView) {
return;
}
// It's possible that the trigger's view was destroyed before we resolved the trigger element.
if (triggerLView[FLAGS] & LViewFlags.Destroyed) {
afterRenderRef.destroy();
return;
}
// TODO: add integration with `DeferBlockCleanupManager`.
const element = getTriggerElement(triggerLView, triggerIndex);
const cleanup = registerFn(element, () => {
callback();
removeLViewOnDestroy(triggerLView, cleanup);
if (initialLView !== triggerLView) {
removeLViewOnDestroy(initialLView, cleanup);
}
cleanup();
}, injector);
afterRenderRef.destroy();
storeLViewOnDestroy(triggerLView, cleanup);
// Since the trigger and deferred block might be in different
// views, we have to register the callback in both locations.
if (initialLView !== triggerLView) {
storeLViewOnDestroy(initialLView, cleanup);
}
}, {injector});
}
/**
* Helper function to schedule a callback to be invoked when a browser becomes idle.
*
* @param callback A function to be invoked when a browser becomes idle.
* @param lView LView that hosts an instance of a defer block.
* @param withLViewCleanup A flag that indicates whether a scheduled callback
* should be cancelled in case an LView is destroyed before a callback
* was invoked.
*/
function onIdle(callback: VoidFunction, lView: LView, withLViewCleanup: boolean) {
const injector = lView[INJECTOR]!;
const scheduler = injector.get(OnIdleScheduler);
const cleanupFn = () => scheduler.remove(callback);
const wrappedCallback =
withLViewCleanup ? wrapWithLViewCleanup(callback, lView, cleanupFn) : callback;
scheduler.add(wrappedCallback);
return cleanupFn;
}
/**
* Returns a function that captures a provided delay.
* Invoking the returned function schedules a trigger.
*/
function onTimer(delay: number) {
return (callback: VoidFunction, lView: LView, withLViewCleanup: boolean) =>
scheduleTimerTrigger(delay, callback, lView, withLViewCleanup);
}
/**
* Schedules a callback to be invoked after a given timeout.
*
* @param delay A number of ms to wait until firing a callback.
* @param callback A function to be invoked after a timeout.
* @param lView LView that hosts an instance of a defer block.
* @param withLViewCleanup A flag that indicates whether a scheduled callback
* should be cancelled in case an LView is destroyed before a callback
* was invoked.
*/
function scheduleTimerTrigger(
delay: number, callback: VoidFunction, lView: LView, withLViewCleanup: boolean) {
const injector = lView[INJECTOR]!;
const scheduler = injector.get(TimerScheduler);
const cleanupFn = () => scheduler.remove(callback);
const wrappedCallback =
withLViewCleanup ? wrapWithLViewCleanup(callback, lView, cleanupFn) : callback;
scheduler.add(delay, wrappedCallback);
return cleanupFn;
}
/**
* Wraps a given callback into a logic that registers a cleanup function
* in the LView cleanup slot, to be invoked when an LView is destroyed.
*/
function wrapWithLViewCleanup(
callback: VoidFunction, lView: LView, cleanup: VoidFunction): VoidFunction {
const wrappedCallback = () => {
callback();
removeLViewOnDestroy(lView, cleanup);
};
storeLViewOnDestroy(lView, cleanup);
return wrappedCallback;
}
/**
* Calculates a data slot index for defer block info (either static or
* instance-specific), given an index of a defer instruction.
*/
function getDeferBlockDataIndex(deferBlockIndex: number) {
// Instance state is located at the *next* position
// after the defer block slot in an LView or TView.data.
return deferBlockIndex + 1;
}
/** Retrieves a defer block state from an LView, given a TNode that represents a block. */
function getLDeferBlockDetails(lView: LView, tNode: TNode): LDeferBlockDetails {
const tView = lView[TVIEW];
const slotIndex = getDeferBlockDataIndex(tNode.index);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
return lView[slotIndex];
}
/** Stores a defer block instance state in LView. */
function setLDeferBlockDetails(
lView: LView, deferBlockIndex: number, lDetails: LDeferBlockDetails) {
const tView = lView[TVIEW];
const slotIndex = getDeferBlockDataIndex(deferBlockIndex);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
lView[slotIndex] = lDetails;
}
/** Retrieves static info about a defer block, given a TView and a TNode that represents a block. */
function getTDeferBlockDetails(tView: TView, tNode: TNode): TDeferBlockDetails {
const slotIndex = getDeferBlockDataIndex(tNode.index);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
return tView.data[slotIndex] as TDeferBlockDetails;
}
/** Stores a defer block static info in `TView.data`. */
function setTDeferBlockDetails(
tView: TView, deferBlockIndex: number, deferBlockConfig: TDeferBlockDetails) {
const slotIndex = getDeferBlockDataIndex(deferBlockIndex);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
tView.data[slotIndex] = deferBlockConfig;
}
function getTemplateIndexForState(
newState: DeferBlockState, hostLView: LView, tNode: TNode): number|null {
const tView = hostLView[TVIEW];
const tDetails = getTDeferBlockDetails(tView, tNode);
switch (newState) {
case DeferBlockState.Complete:
return tDetails.primaryTmplIndex;
case DeferBlockState.Loading:
return tDetails.loadingTmplIndex;
case DeferBlockState.Error:
return tDetails.errorTmplIndex;
case DeferBlockState.Placeholder:
return tDetails.placeholderTmplIndex;
default:
ngDevMode && throwError(`Unexpected defer block state: ${newState}`);
return null;
}
}
/**
* Returns a minimum amount of time that a given state should be rendered for,
* taking into account `minimum` parameter value. If the `minimum` value is
* not specified - returns `null`.
*/
function getMinimumDurationForState(
tDetails: TDeferBlockDetails, currentState: DeferBlockState): number|null {
if (currentState === DeferBlockState.Placeholder) {
return tDetails.placeholderBlockConfig?.[MINIMUM_SLOT] ?? null;
} else if (currentState === DeferBlockState.Loading) {
return tDetails.loadingBlockConfig?.[MINIMUM_SLOT] ?? null;
}
return null;
}
/** Retrieves the value of the `after` parameter on the @loading block. */
function getLoadingBlockAfter(tDetails: TDeferBlockDetails): number|null {
return tDetails.loadingBlockConfig?.[LOADING_AFTER_SLOT] ?? null;
}
/**
* Transitions a defer block to the new state. Updates the necessary
* data structures and renders corresponding block.
@ -921,25 +679,6 @@ export function triggerResourceLoading(tDetails: TDeferBlockDetails, lView: LVie
});
}
/**
* Adds downloaded dependencies into a directive or a pipe registry,
* making sure that a dependency doesn't yet exist in the registry.
*/
function addDepsToRegistry<T extends DependencyDef[]>(currentDeps: T|null, newDeps: T): T {
if (!currentDeps || currentDeps.length === 0) {
return newDeps;
}
const currentDepSet = new Set(currentDeps);
for (const dep of newDeps) {
currentDepSet.add(dep);
}
// If `currentDeps` is the same length, there were no new deps and can
// return the original array.
return (currentDeps.length === currentDepSet.size) ? currentDeps : Array.from(currentDepSet) as T;
}
/** Utility function to render placeholder content (if present) */
function renderPlaceholder(lView: LView, tNode: TNode) {
const lContainer = lView[tNode.index];
@ -974,12 +713,6 @@ function renderDeferStateAfterResourceLoading(
});
}
/** Retrieves a TNode that represents main content of a defer block. */
function getPrimaryBlockTNode(tView: TView, tDetails: TDeferBlockDetails): TContainerNode {
const adjustedIndex = tDetails.primaryTmplIndex + HEADER_OFFSET;
return getTNode(tView, adjustedIndex) as TContainerNode;
}
/**
* Attempts to trigger loading of defer block dependencies.
* If the block is already in a loading, completed or an error state -
@ -1022,440 +755,3 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {
}
}
}
/**
* Asserts whether all dependencies for a defer block are loaded.
* Always run this function (in dev mode) before rendering a defer
* block in completed state.
*/
function assertDeferredDependenciesLoaded(tDetails: TDeferBlockDetails) {
assertEqual(
tDetails.loadingState, DeferDependenciesLoadingState.COMPLETE,
'Expecting all deferred dependencies to be loaded.');
}
/**
* **INTERNAL**, avoid referencing it in application code.
*
* Describes a helper class that allows to intercept a call to retrieve current
* dependency loading function and replace it with a different implementation.
* This interceptor class is needed to allow testing blocks in different states
* by simulating loading response.
*/
export interface DeferBlockDependencyInterceptor {
/**
* Invoked for each defer block when dependency loading function is accessed.
*/
intercept(dependencyFn: DependencyResolverFn|null): DependencyResolverFn|null;
/**
* Allows to configure an interceptor function.
*/
setInterceptor(interceptorFn: (current: DependencyResolverFn) => DependencyResolverFn): void;
}
/**
* **INTERNAL**, avoid referencing it in application code.
*
* Injector token that allows to provide `DeferBlockDependencyInterceptor` class
* implementation.
*/
export const DEFER_BLOCK_DEPENDENCY_INTERCEPTOR =
new InjectionToken<DeferBlockDependencyInterceptor>(
ngDevMode ? 'DEFER_BLOCK_DEPENDENCY_INTERCEPTOR' : '');
/**
* Determines if a given value matches the expected structure of a defer block
*
* We can safely rely on the primaryTmplIndex because every defer block requires
* that a primary template exists. All the other template options are optional.
*/
function isTDeferBlockDetails(value: unknown): value is TDeferBlockDetails {
return (typeof value === 'object') &&
(typeof (value as TDeferBlockDetails).primaryTmplIndex === 'number');
}
/**
* Internal token used for configuring defer block behavior.
*/
export const DEFER_BLOCK_CONFIG =
new InjectionToken<DeferBlockConfig>(ngDevMode ? 'DEFER_BLOCK_CONFIG' : '');
/**
* Defer block instance for testing.
*/
export interface DeferBlockDetails {
lContainer: LContainer;
lView: LView;
tNode: TNode;
tDetails: TDeferBlockDetails;
}
/**
* Retrieves all defer blocks in a given LView.
*
* @param lView lView with defer blocks
* @param deferBlocks defer block aggregator array
*/
export function getDeferBlocks(lView: LView, deferBlocks: DeferBlockDetails[]) {
const tView = lView[TVIEW];
for (let i = HEADER_OFFSET; i < tView.bindingStartIndex; i++) {
if (isLContainer(lView[i])) {
const lContainer = lView[i];
// An LContainer may represent an instance of a defer block, in which case
// we store it as a result. Otherwise, keep iterating over LContainer views and
// look for defer blocks.
const isLast = i === tView.bindingStartIndex - 1;
if (!isLast) {
const tNode = tView.data[i] as TNode;
const tDetails = getTDeferBlockDetails(tView, tNode);
if (isTDeferBlockDetails(tDetails)) {
deferBlocks.push({lContainer, lView, tNode, tDetails});
// This LContainer represents a defer block, so we exit
// this iteration and don't inspect views in this LContainer.
continue;
}
}
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
getDeferBlocks(lContainer[i] as LView, deferBlocks);
}
} else if (isLView(lView[i])) {
// This is a component, enter the `getDeferBlocks` recursively.
getDeferBlocks(lView[i], deferBlocks);
}
}
}
/**
* 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(),
});
}
/**
* Use shims for the `requestIdleCallback` and `cancelIdleCallback` functions for
* environments where those functions are not available (e.g. Node.js and Safari).
*
* Note: we wrap the `requestIdleCallback` call into a function, so that it can be
* overridden/mocked in test environment and picked up by the runtime code.
*/
const _requestIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? requestIdleCallback : setTimeout;
const _cancelIdleCallback = () =>
typeof requestIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout;
/**
* Helper service to schedule `requestIdleCallback`s for batches of defer blocks,
* to avoid calling `requestIdleCallback` for each defer block (e.g. if
* defer blocks are defined inside a for loop).
*/
class OnIdleScheduler {
// Indicates whether current callbacks are being invoked.
executingCallbacks = false;
// Currently scheduled idle callback id.
idleId: number|null = null;
// Set of callbacks to be invoked next.
current = new Set<VoidFunction>();
// Set of callbacks collected while invoking current set of callbacks.
// Those callbacks are scheduled for the next idle period.
deferred = new Set<VoidFunction>();
ngZone = inject(NgZone);
requestIdleCallback = _requestIdleCallback().bind(globalThis);
cancelIdleCallback = _cancelIdleCallback().bind(globalThis);
add(callback: VoidFunction) {
const target = this.executingCallbacks ? this.deferred : this.current;
target.add(callback);
if (this.idleId === null) {
this.scheduleIdleCallback();
}
}
remove(callback: VoidFunction) {
this.current.delete(callback);
this.deferred.delete(callback);
}
private scheduleIdleCallback() {
const callback = () => {
this.cancelIdleCallback(this.idleId!);
this.idleId = null;
this.executingCallbacks = true;
for (const callback of this.current) {
callback();
}
this.current.clear();
this.executingCallbacks = false;
// If there are any callbacks added during an invocation
// of the current ones - make them "current" and schedule
// a new idle callback.
if (this.deferred.size > 0) {
for (const callback of this.deferred) {
this.current.add(callback);
}
this.deferred.clear();
this.scheduleIdleCallback();
}
};
// Ensure that the callback runs in the NgZone since
// the `requestIdleCallback` is not currently patched by Zone.js.
this.idleId = this.requestIdleCallback(() => this.ngZone.run(callback)) as number;
}
ngOnDestroy() {
if (this.idleId !== null) {
this.cancelIdleCallback(this.idleId);
this.idleId = null;
}
this.current.clear();
this.deferred.clear();
}
/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: OnIdleScheduler,
providedIn: 'root',
factory: () => new OnIdleScheduler(),
});
}
/**
* Helper service to schedule `setTimeout`s for batches of defer blocks,
* to avoid calling `setTimeout` for each defer block (e.g. if defer blocks
* are created inside a for loop).
*/
class TimerScheduler {
// Indicates whether current callbacks are being invoked.
executingCallbacks = false;
// Currently scheduled `setTimeout` id.
timeoutId: number|null = null;
// When currently scheduled timer would fire.
invokeTimerAt: number|null = null;
// List of callbacks to be invoked.
// For each callback we also store a timestamp on when the callback
// should be invoked. We store timestamps and callback functions
// in a flat array to avoid creating new objects for each entry.
// [timestamp1, callback1, timestamp2, callback2, ...]
current: Array<number|VoidFunction> = [];
// List of callbacks collected while invoking current set of callbacks.
// Those callbacks are added to the "current" queue at the end of
// the current callback invocation. The shape of this list is the same
// as the shape of the `current` list.
deferred: Array<number|VoidFunction> = [];
add(delay: number, callback: VoidFunction) {
const target = this.executingCallbacks ? this.deferred : this.current;
this.addToQueue(target, Date.now() + delay, callback);
this.scheduleTimer();
}
remove(callback: VoidFunction) {
const callbackIndex = this.removeFromQueue(this.current, callback);
if (callbackIndex === -1) {
// Try cleaning up deferred queue only in case
// we didn't find a callback in the "current" queue.
this.removeFromQueue(this.deferred, callback);
}
}
private addToQueue(target: Array<number|VoidFunction>, invokeAt: number, callback: VoidFunction) {
let insertAtIndex = target.length;
for (let i = 0; i < target.length; i += 2) {
const invokeQueuedCallbackAt = target[i] as number;
if (invokeQueuedCallbackAt > invokeAt) {
// We've reached a first timer that is scheduled
// for a later time than what we are trying to insert.
// This is the location at which we need to insert,
// no need to iterate further.
insertAtIndex = i;
break;
}
}
arrayInsert2(target, insertAtIndex, invokeAt, callback);
}
private removeFromQueue(target: Array<number|VoidFunction>, callback: VoidFunction) {
let index = -1;
for (let i = 0; i < target.length; i += 2) {
const queuedCallback = target[i + 1];
if (queuedCallback === callback) {
index = i;
break;
}
}
if (index > -1) {
// Remove 2 elements: a timestamp slot and
// the following slot with a callback function.
arraySplice(target, index, 2);
}
return index;
}
private scheduleTimer() {
const callback = () => {
clearTimeout(this.timeoutId!);
this.timeoutId = null;
this.executingCallbacks = true;
// Invoke callbacks that were scheduled to run
// before the current time.
let now = Date.now();
let lastCallbackIndex: number|null = null;
for (let i = 0; i < this.current.length; i += 2) {
const invokeAt = this.current[i] as number;
const callback = this.current[i + 1] as VoidFunction;
if (invokeAt <= now) {
callback();
// Point at the invoked callback function, which is located
// after the timestamp.
lastCallbackIndex = i + 1;
} else {
// We've reached a timer that should not be invoked yet.
break;
}
}
if (lastCallbackIndex !== null) {
// If last callback index is `null` - no callbacks were invoked,
// so no cleanup is needed. Otherwise, remove invoked callbacks
// from the queue.
arraySplice(this.current, 0, lastCallbackIndex + 1);
}
this.executingCallbacks = false;
// If there are any callbacks added during an invocation
// of the current ones - move them over to the "current"
// queue.
if (this.deferred.length > 0) {
for (let i = 0; i < this.deferred.length; i += 2) {
const invokeAt = this.deferred[i] as number;
const callback = this.deferred[i + 1] as VoidFunction;
this.addToQueue(this.current, invokeAt, callback);
}
this.deferred.length = 0;
}
this.scheduleTimer();
};
// Avoid running timer callbacks more than once per
// average frame duration. This is needed for better
// batching and to avoid kicking off excessive change
// detection cycles.
const FRAME_DURATION_MS = 16; // 1000ms / 60fps
if (this.current.length > 0) {
const now = Date.now();
// First element in the queue points at the timestamp
// of the first (earliest) event.
const invokeAt = this.current[0] as number;
if (!this.timeoutId ||
// Reschedule a timer in case a queue contains an item with
// an earlier timestamp and the delta is more than an average
// frame duration.
(this.invokeTimerAt && (this.invokeTimerAt - invokeAt > FRAME_DURATION_MS))) {
if (this.timeoutId !== null) {
// There was a timeout already, but an earlier event was added
// into the queue. In this case we drop an old timer and setup
// a new one with an updated (smaller) timeout.
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
const timeout = Math.max(invokeAt - now, FRAME_DURATION_MS);
this.invokeTimerAt = invokeAt;
this.timeoutId = setTimeout(callback, timeout) as unknown as number;
}
}
}
ngOnDestroy() {
if (this.timeoutId !== null) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
this.current.length = 0;
this.deferred.length = 0;
}
/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: TimerScheduler,
providedIn: 'root',
factory: () => new TimerScheduler(),
});
}

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import type {DependencyType} from './definition';
import type {DependencyType} from '../render3/interfaces/definition';
/**
* Describes the shape of a function generated by the compiler
@ -204,3 +204,23 @@ export enum DeferBlockBehavior {
*/
Playthrough,
}
/**
* **INTERNAL**, avoid referencing it in application code.
*
* Describes a helper class that allows to intercept a call to retrieve current
* dependency loading function and replace it with a different implementation.
* This interceptor class is needed to allow testing blocks in different states
* by simulating loading response.
*/
export interface DeferBlockDependencyInterceptor {
/**
* Invoked for each defer block when dependency loading function is accessed.
*/
intercept(dependencyFn: DependencyResolverFn|null): DependencyResolverFn|null;
/**
* Allows to configure an interceptor function.
*/
setInterceptor(interceptorFn: (current: DependencyResolverFn) => DependencyResolverFn): void;
}

View file

@ -0,0 +1,213 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵdefineInjectable} from '../di';
import {INJECTOR, LView} from '../render3/interfaces/view';
import {arrayInsert2, arraySplice} from '../util/array_utils';
import {wrapWithLViewCleanup} from './utils';
/**
* Returns a function that captures a provided delay.
* Invoking the returned function schedules a trigger.
*/
export function onTimer(delay: number) {
return (callback: VoidFunction, lView: LView, withLViewCleanup: boolean) =>
scheduleTimerTrigger(delay, callback, lView, withLViewCleanup);
}
/**
* Schedules a callback to be invoked after a given timeout.
*
* @param delay A number of ms to wait until firing a callback.
* @param callback A function to be invoked after a timeout.
* @param lView LView that hosts an instance of a defer block.
* @param withLViewCleanup A flag that indicates whether a scheduled callback
* should be cancelled in case an LView is destroyed before a callback
* was invoked.
*/
export function scheduleTimerTrigger(
delay: number, callback: VoidFunction, lView: LView, withLViewCleanup: boolean) {
const injector = lView[INJECTOR]!;
const scheduler = injector.get(TimerScheduler);
const cleanupFn = () => scheduler.remove(callback);
const wrappedCallback =
withLViewCleanup ? wrapWithLViewCleanup(callback, lView, cleanupFn) : callback;
scheduler.add(delay, wrappedCallback);
return cleanupFn;
}
/**
* Helper service to schedule `setTimeout`s for batches of defer blocks,
* to avoid calling `setTimeout` for each defer block (e.g. if defer blocks
* are created inside a for loop).
*/
export class TimerScheduler {
// Indicates whether current callbacks are being invoked.
executingCallbacks = false;
// Currently scheduled `setTimeout` id.
timeoutId: number|null = null;
// When currently scheduled timer would fire.
invokeTimerAt: number|null = null;
// List of callbacks to be invoked.
// For each callback we also store a timestamp on when the callback
// should be invoked. We store timestamps and callback functions
// in a flat array to avoid creating new objects for each entry.
// [timestamp1, callback1, timestamp2, callback2, ...]
current: Array<number|VoidFunction> = [];
// List of callbacks collected while invoking current set of callbacks.
// Those callbacks are added to the "current" queue at the end of
// the current callback invocation. The shape of this list is the same
// as the shape of the `current` list.
deferred: Array<number|VoidFunction> = [];
add(delay: number, callback: VoidFunction) {
const target = this.executingCallbacks ? this.deferred : this.current;
this.addToQueue(target, Date.now() + delay, callback);
this.scheduleTimer();
}
remove(callback: VoidFunction) {
const callbackIndex = this.removeFromQueue(this.current, callback);
if (callbackIndex === -1) {
// Try cleaning up deferred queue only in case
// we didn't find a callback in the "current" queue.
this.removeFromQueue(this.deferred, callback);
}
}
private addToQueue(target: Array<number|VoidFunction>, invokeAt: number, callback: VoidFunction) {
let insertAtIndex = target.length;
for (let i = 0; i < target.length; i += 2) {
const invokeQueuedCallbackAt = target[i] as number;
if (invokeQueuedCallbackAt > invokeAt) {
// We've reached a first timer that is scheduled
// for a later time than what we are trying to insert.
// This is the location at which we need to insert,
// no need to iterate further.
insertAtIndex = i;
break;
}
}
arrayInsert2(target, insertAtIndex, invokeAt, callback);
}
private removeFromQueue(target: Array<number|VoidFunction>, callback: VoidFunction) {
let index = -1;
for (let i = 0; i < target.length; i += 2) {
const queuedCallback = target[i + 1];
if (queuedCallback === callback) {
index = i;
break;
}
}
if (index > -1) {
// Remove 2 elements: a timestamp slot and
// the following slot with a callback function.
arraySplice(target, index, 2);
}
return index;
}
private scheduleTimer() {
const callback = () => {
clearTimeout(this.timeoutId!);
this.timeoutId = null;
this.executingCallbacks = true;
// Invoke callbacks that were scheduled to run
// before the current time.
let now = Date.now();
let lastCallbackIndex: number|null = null;
for (let i = 0; i < this.current.length; i += 2) {
const invokeAt = this.current[i] as number;
const callback = this.current[i + 1] as VoidFunction;
if (invokeAt <= now) {
callback();
// Point at the invoked callback function, which is located
// after the timestamp.
lastCallbackIndex = i + 1;
} else {
// We've reached a timer that should not be invoked yet.
break;
}
}
if (lastCallbackIndex !== null) {
// If last callback index is `null` - no callbacks were invoked,
// so no cleanup is needed. Otherwise, remove invoked callbacks
// from the queue.
arraySplice(this.current, 0, lastCallbackIndex + 1);
}
this.executingCallbacks = false;
// If there are any callbacks added during an invocation
// of the current ones - move them over to the "current"
// queue.
if (this.deferred.length > 0) {
for (let i = 0; i < this.deferred.length; i += 2) {
const invokeAt = this.deferred[i] as number;
const callback = this.deferred[i + 1] as VoidFunction;
this.addToQueue(this.current, invokeAt, callback);
}
this.deferred.length = 0;
}
this.scheduleTimer();
};
// Avoid running timer callbacks more than once per
// average frame duration. This is needed for better
// batching and to avoid kicking off excessive change
// detection cycles.
const FRAME_DURATION_MS = 16; // 1000ms / 60fps
if (this.current.length > 0) {
const now = Date.now();
// First element in the queue points at the timestamp
// of the first (earliest) event.
const invokeAt = this.current[0] as number;
if (!this.timeoutId ||
// Reschedule a timer in case a queue contains an item with
// an earlier timestamp and the delta is more than an average
// frame duration.
(this.invokeTimerAt && (this.invokeTimerAt - invokeAt > FRAME_DURATION_MS))) {
if (this.timeoutId !== null) {
// There was a timeout already, but an earlier event was added
// into the queue. In this case we drop an old timer and setup
// a new one with an updated (smaller) timeout.
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
const timeout = Math.max(invokeAt - now, FRAME_DURATION_MS);
this.invokeTimerAt = invokeAt;
this.timeoutId = setTimeout(callback, timeout) as unknown as number;
}
}
}
ngOnDestroy() {
if (this.timeoutId !== null) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
this.current.length = 0;
this.deferred.length = 0;
}
/** @nocollapse */
static ɵprov = /** @pureOrBreakMyCode */ ɵɵdefineInjectable({
token: TimerScheduler,
providedIn: 'root',
factory: () => new TimerScheduler(),
});
}

View file

@ -0,0 +1,159 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {assertIndexInDeclRange} from '../render3/assert';
import {DependencyDef} from '../render3/interfaces/definition';
import {TContainerNode, TNode} from '../render3/interfaces/node';
import {HEADER_OFFSET, LView, TVIEW, TView} from '../render3/interfaces/view';
import {getTNode, removeLViewOnDestroy, storeLViewOnDestroy} from '../render3/util/view_utils';
import {assertEqual, throwError} from '../util/assert';
import {DeferBlockState, DeferDependenciesLoadingState, LDeferBlockDetails, LOADING_AFTER_SLOT, MINIMUM_SLOT, TDeferBlockDetails} from './interfaces';
/**
* Wraps a given callback into a logic that registers a cleanup function
* in the LView cleanup slot, to be invoked when an LView is destroyed.
*/
export function wrapWithLViewCleanup(
callback: VoidFunction, lView: LView, cleanup: VoidFunction): VoidFunction {
const wrappedCallback = () => {
callback();
removeLViewOnDestroy(lView, cleanup);
};
storeLViewOnDestroy(lView, cleanup);
return wrappedCallback;
}
/**
* Calculates a data slot index for defer block info (either static or
* instance-specific), given an index of a defer instruction.
*/
export function getDeferBlockDataIndex(deferBlockIndex: number) {
// Instance state is located at the *next* position
// after the defer block slot in an LView or TView.data.
return deferBlockIndex + 1;
}
/** Retrieves a defer block state from an LView, given a TNode that represents a block. */
export function getLDeferBlockDetails(lView: LView, tNode: TNode): LDeferBlockDetails {
const tView = lView[TVIEW];
const slotIndex = getDeferBlockDataIndex(tNode.index);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
return lView[slotIndex];
}
/** Stores a defer block instance state in LView. */
export function setLDeferBlockDetails(
lView: LView, deferBlockIndex: number, lDetails: LDeferBlockDetails) {
const tView = lView[TVIEW];
const slotIndex = getDeferBlockDataIndex(deferBlockIndex);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
lView[slotIndex] = lDetails;
}
/** Retrieves static info about a defer block, given a TView and a TNode that represents a block. */
export function getTDeferBlockDetails(tView: TView, tNode: TNode): TDeferBlockDetails {
const slotIndex = getDeferBlockDataIndex(tNode.index);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
return tView.data[slotIndex] as TDeferBlockDetails;
}
/** Stores a defer block static info in `TView.data`. */
export function setTDeferBlockDetails(
tView: TView, deferBlockIndex: number, deferBlockConfig: TDeferBlockDetails) {
const slotIndex = getDeferBlockDataIndex(deferBlockIndex);
ngDevMode && assertIndexInDeclRange(tView, slotIndex);
tView.data[slotIndex] = deferBlockConfig;
}
export function getTemplateIndexForState(
newState: DeferBlockState, hostLView: LView, tNode: TNode): number|null {
const tView = hostLView[TVIEW];
const tDetails = getTDeferBlockDetails(tView, tNode);
switch (newState) {
case DeferBlockState.Complete:
return tDetails.primaryTmplIndex;
case DeferBlockState.Loading:
return tDetails.loadingTmplIndex;
case DeferBlockState.Error:
return tDetails.errorTmplIndex;
case DeferBlockState.Placeholder:
return tDetails.placeholderTmplIndex;
default:
ngDevMode && throwError(`Unexpected defer block state: ${newState}`);
return null;
}
}
/**
* Returns a minimum amount of time that a given state should be rendered for,
* taking into account `minimum` parameter value. If the `minimum` value is
* not specified - returns `null`.
*/
export function getMinimumDurationForState(
tDetails: TDeferBlockDetails, currentState: DeferBlockState): number|null {
if (currentState === DeferBlockState.Placeholder) {
return tDetails.placeholderBlockConfig?.[MINIMUM_SLOT] ?? null;
} else if (currentState === DeferBlockState.Loading) {
return tDetails.loadingBlockConfig?.[MINIMUM_SLOT] ?? null;
}
return null;
}
/** Retrieves the value of the `after` parameter on the @loading block. */
export function getLoadingBlockAfter(tDetails: TDeferBlockDetails): number|null {
return tDetails.loadingBlockConfig?.[LOADING_AFTER_SLOT] ?? null;
}
/**
* Adds downloaded dependencies into a directive or a pipe registry,
* making sure that a dependency doesn't yet exist in the registry.
*/
export function addDepsToRegistry<T extends DependencyDef[]>(currentDeps: T|null, newDeps: T): T {
if (!currentDeps || currentDeps.length === 0) {
return newDeps;
}
const currentDepSet = new Set(currentDeps);
for (const dep of newDeps) {
currentDepSet.add(dep);
}
// If `currentDeps` is the same length, there were no new deps and can
// return the original array.
return (currentDeps.length === currentDepSet.size) ? currentDeps : Array.from(currentDepSet) as T;
}
/** Retrieves a TNode that represents main content of a defer block. */
export function getPrimaryBlockTNode(tView: TView, tDetails: TDeferBlockDetails): TContainerNode {
const adjustedIndex = tDetails.primaryTmplIndex + HEADER_OFFSET;
return getTNode(tView, adjustedIndex) as TContainerNode;
}
/**
* Asserts whether all dependencies for a defer block are loaded.
* Always run this function (in dev mode) before rendering a defer
* block in completed state.
*/
export function assertDeferredDependenciesLoaded(tDetails: TDeferBlockDetails) {
assertEqual(
tDetails.loadingState, DeferDependenciesLoadingState.COMPLETE,
'Expecting all deferred dependencies to be loaded.');
}
/**
* Determines if a given value matches the expected structure of a defer block
*
* We can safely rely on the primaryTmplIndex because every defer block requires
* that a primary template exists. All the other template options are optional.
*/
export function isTDeferBlockDetails(value: unknown): value is TDeferBlockDetails {
return (typeof value === 'object') &&
(typeof (value as TDeferBlockDetails).primaryTmplIndex === 'number');
}

View file

@ -160,11 +160,14 @@ export {
ɵsetUnknownElementStrictMode,
ɵgetUnknownPropertyStrictMode,
ɵsetUnknownPropertyStrictMode,
DeferBlockDependencyInterceptor as ɵDeferBlockDependencyInterceptor,
} from './instructions/all';
export {
DEFER_BLOCK_DEPENDENCY_INTERCEPTOR as ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
DEFER_BLOCK_CONFIG as ɵDEFER_BLOCK_CONFIG,
} from './instructions/all';
} from '../defer/instructions';
export {
DeferBlockDependencyInterceptor as ɵDeferBlockDependencyInterceptor,
} from '../defer/interfaces';
export {ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp,ɵɵi18nPostprocess, ɵɵi18nStart} from './instructions/i18n';
export {RenderFlags} from './interfaces/definition';
export {

View file

@ -32,7 +32,7 @@ export * from './change_detection';
export * from './class_map_interpolation';
export * from './component_instance';
export * from './control_flow';
export * from './defer';
export * from '../../defer/instructions';
export * from './di';
export * from './di_attr';
export * from './element';

View file

@ -23,7 +23,7 @@ import {LQueries, TQueries} from './query';
import {Renderer, RendererFactory} from './renderer';
import {RElement} from './renderer_dom';
import {TStylingKey, TStylingRange} from './styling';
import {TDeferBlockDetails} from './defer';
import {TDeferBlockDetails} from '../../defer/interfaces';

View file

@ -1115,6 +1115,9 @@
{
"name": "init_cleanup"
},
{
"name": "init_cleanup2"
},
{
"name": "init_closure"
},
@ -1220,18 +1223,9 @@
{
"name": "init_default_keyvalue_differ"
},
{
"name": "init_defer"
},
{
"name": "init_defer2"
},
{
"name": "init_defer_component"
},
{
"name": "init_defer_events"
},
{
"name": "init_definition"
},
@ -1274,6 +1268,9 @@
{
"name": "init_directives"
},
{
"name": "init_discovery"
},
{
"name": "init_discovery_utils"
},
@ -1286,6 +1283,9 @@
{
"name": "init_dom"
},
{
"name": "init_dom_triggers"
},
{
"name": "init_effect"
},
@ -1421,6 +1421,9 @@
{
"name": "init_identity"
},
{
"name": "init_idle_scheduler"
},
{
"name": "init_iframe_attrs_validation"
},
@ -1481,9 +1484,15 @@
{
"name": "init_input_transforms_feature"
},
{
"name": "init_instructions"
},
{
"name": "init_interfaces"
},
{
"name": "init_interfaces2"
},
{
"name": "init_internal_tokens"
},
@ -1889,6 +1898,9 @@
{
"name": "init_text_interpolation"
},
{
"name": "init_timer_scheduler"
},
{
"name": "init_toSubscriber"
},
@ -1931,6 +1943,9 @@
{
"name": "init_utils"
},
{
"name": "init_utils2"
},
{
"name": "init_version"
},