mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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:
parent
11c29f9cc3
commit
908f660a72
13 changed files with 859 additions and 763 deletions
|
|
@ -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';
|
||||
|
|
|
|||
77
packages/core/src/defer/cleanup.ts
Normal file
77
packages/core/src/defer/cleanup.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
60
packages/core/src/defer/discovery.ts
Normal file
60
packages/core/src/defer/discovery.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
128
packages/core/src/defer/idle_scheduler.ts
Normal file
128
packages/core/src/defer/idle_scheduler.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
213
packages/core/src/defer/timer_scheduler.ts
Normal file
213
packages/core/src/defer/timer_scheduler.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
159
packages/core/src/defer/utils.ts
Normal file
159
packages/core/src/defer/utils.ts
Normal 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');
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue