Skip to content

Commit

Permalink
fixup! refactor(core): initial implementation of {#defer} block run…
Browse files Browse the repository at this point in the history
…time
  • Loading branch information
AndrewKushnir committed Aug 20, 2023
1 parent 155bd5b commit a786a09
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 196 deletions.
4 changes: 4 additions & 0 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver

this.createDeferTriggerInstructions(deferredIndex, triggers, false);
this.createDeferTriggerInstructions(deferredIndex, prefetchTriggers, true);

// Allocate an extra data slot right after a defer block slot to store
// instance-specific state of that defer block at runtime.
this.allocateDataSlot();
}

private createDeferredDepsFunction(name: string, deferred: t.DeferredBlock) {
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/render3/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,7 @@ export function assertDirectiveDef<T>(obj: any): asserts obj is DirectiveDef<T>
}
}

export function assertIndexInDeclRange(lView: LView, index: number) {
const tView = lView[1];
export function assertIndexInDeclRange(tView: TView, index: number) {
assertBetween(HEADER_OFFSET, tView.bindingStartIndex, index);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/render3/instructions/advance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import {assertGreaterThan} from '../../util/assert';
import {assertIndexInDeclRange} from '../assert';
import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks';
import {FLAGS, InitPhaseState, LView, LViewFlags, TView} from '../interfaces/view';
import {FLAGS, InitPhaseState, LView, LViewFlags, TVIEW, TView} from '../interfaces/view';
import {getLView, getSelectedIndex, getTView, isInCheckNoChangesMode, setSelectedIndex} from '../state';


Expand Down Expand Up @@ -43,7 +43,7 @@ export function ɵɵadvance(delta: number): void {

export function selectIndexInternal(
tView: TView, lView: LView, index: number, checkNoChangesMode: boolean) {
ngDevMode && assertIndexInDeclRange(lView, index);
ngDevMode && assertIndexInDeclRange(lView[TVIEW], index);

// Flush the initial hooks for elements in the view that have been added up to this point.
// PERF WARNING: do NOT extract this to a separate function without running benchmarks
Expand Down
185 changes: 121 additions & 64 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
*/

import {InjectionToken, Injector} from '../../di';
import {assertDefined, assertEqual, assertNotDefined, throwError} from '../../util/assert';
import {NgZone} from '../../zone';
import {assertLContainer} from '../assert';
import {assertDefined, assertEqual, throwError} from '../../util/assert';
import {assertIndexInDeclRange, assertLContainer, assertTNodeForLView} from '../assert';
import {bindingUpdated} from '../bindings';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition';
import {DEFER_BLOCK_DETAILS, DeferBlockInstanceState, LContainer} from '../interfaces/container';
import {DependencyResolverFn, DirectiveDefList, PipeDefList} from '../interfaces/definition';
import {DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, TContainerNode, TDeferBlockDetails, TNode} from '../interfaces/node';
import {DEFER_BLOCK_STATE, DeferBlockInstanceState, LContainer, LDeferBlockDetails} from '../interfaces/container';
import {DeferDependenciesLoadingState, DeferredLoadingBlockConfig, DeferredPlaceholderBlockConfig, DependencyResolverFn, TDeferBlockDetails} from '../interfaces/defer';
import {DirectiveDefList, PipeDefList} from '../interfaces/definition';
import {TContainerNode, TNode} from '../interfaces/node';
import {isDestroyed} from '../interfaces/type_checks';
import {HEADER_OFFSET, INJECTOR, LView, PARENT, TVIEW} from '../interfaces/view';
import {HEADER_OFFSET, INJECTOR, LView, PARENT, TVIEW, TView} from '../interfaces/view';
import {getCurrentTNode, getLView, getSelectedTNode, getTView, nextBindingIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {getConstant, getTNode, storeLViewOnDestroy} from '../util/view_utils';
import {getConstant, getTNode, removeLViewOnDestroy, storeLViewOnDestroy} from '../util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer} from '../view_manipulation';

import {templateInternal} from './template';
Expand Down Expand Up @@ -57,33 +57,34 @@ export function ɵɵdefer(
const lView = getLView();
const tView = getTView();
const tViewConsts = tView.consts;
const adjustedIndex = index + HEADER_OFFSET;

// Defer block details are needed only once during the first creation pass,
// so we wrap an object with defer block details into a function that is only
// invoked once to avoid re-constructing the same object for each subsequent
// creation run.
const deferBlockConfig: () => TDeferBlockDetails = () => ({
primaryTmplIndex,
loadingTmplIndex: loadingTmplIndex ?? null,
placeholderTmplIndex: placeholderTmplIndex ?? null,
errorTmplIndex: errorTmplIndex ?? null,
placeholderBlockConfig: placeholderConfigIndex != null ?
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex) :
null,
loadingBlockConfig: loadingConfigIndex != null ?
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex) :
null,
dependencyResolverFn: dependencyResolverFn ?? null,
loadingState: DeferDependenciesLoadingState.NOT_STARTED,
loadingPromise: null,
});

templateInternal(index, null, 0, 0, deferBlockConfig);
templateInternal(index, null, 0, 0, null);

if (tView.firstCreatePass) {
const deferBlockConfig: TDeferBlockDetails = {
primaryTmplIndex,
loadingTmplIndex: loadingTmplIndex ?? null,
placeholderTmplIndex: placeholderTmplIndex ?? null,
errorTmplIndex: errorTmplIndex ?? null,
placeholderBlockConfig: placeholderConfigIndex != null ?
getConstant<DeferredPlaceholderBlockConfig>(tViewConsts, placeholderConfigIndex) :
null,
loadingBlockConfig: loadingConfigIndex != null ?
getConstant<DeferredLoadingBlockConfig>(tViewConsts, loadingConfigIndex) :
null,
dependencyResolverFn: dependencyResolverFn ?? null,
loadingState: DeferDependenciesLoadingState.NOT_STARTED,
loadingPromise: null,
};

setTDeferBlockDetails(tView, adjustedIndex, deferBlockConfig);
}

// Init instance-specific defer details for this LContainer.
const adjustedIndex = index + HEADER_OFFSET;
const lContainer = lView[adjustedIndex];
lContainer[DEFER_BLOCK_DETAILS] = {state: DeferBlockInstanceState.INITIAL};
// Init instance-specific defer details and store it.
const lDetails = [];
lDetails[DEFER_BLOCK_STATE] = DeferBlockInstanceState.INITIAL;
setLDeferBlockDetails(lView, adjustedIndex, lDetails as LDeferBlockDetails);
}

/**
Expand Down Expand Up @@ -125,15 +126,24 @@ export function ɵɵdeferOnIdle() {

renderPlaceholder(lView, tNode);

const id = _requestIdleCallback(() => {
triggerDeferBlock(lView, tNode);
cancelIdleCallback(id);
}) as number;
storeLViewOnDestroy(lView, () => _cancelIdleCallback(id));
let id: number;
const removeIdleCallback = () => _cancelIdleCallback(id);
id = _requestIdleCallback(() => {
removeIdleCallback();
// The idle callback is invoked, we no longer need
// to retain a cleanup callback in an LView.
removeLViewOnDestroy(lView, removeIdleCallback);
triggerDeferBlock(lView, tNode);
}) as number;

// Store a cleanup function on LView, so that we cancel idle
// callback in case this LView was destroyed before a callback
// was invoked.
storeLViewOnDestroy(lView, removeIdleCallback);
}

/**
* Creates runtime data structures for the `prefetech on idle` deferred trigger.
* Creates runtime data structures for the `prefetch on idle` deferred trigger.
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnIdle() {} // TODO: implement runtime logic.
Expand All @@ -146,7 +156,7 @@ export function ɵɵdeferOnImmediate() {} // TODO: implement runtime logic.


/**
* Creates runtime data structures for the `prefetech on immediate` deferred trigger.
* Creates runtime data structures for the `prefetch on immediate` deferred trigger.
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnImmediate() {} // TODO: implement runtime logic.
Expand All @@ -172,7 +182,7 @@ export function ɵɵdeferPrefetchOnTimer(delay: number) {} // TODO: implement r
export function ɵɵdeferOnHover() {} // TODO: implement runtime logic.

/**
* Creates runtime data structures for the `prefetech on hover` deferred trigger.
* Creates runtime data structures for the `prefetch on hover` deferred trigger.
* @codeGenApi
*/
export function ɵɵdeferPrefetchOnHover() {} // TODO: implement runtime logic.
Expand Down Expand Up @@ -207,36 +217,79 @@ export function ɵɵdeferPrefetchOnViewport(target?: unknown) {} // TODO: imple

/********** Helper functions **********/

/**
* 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;
}

/**
* Transitions a defer block to the new state. Updates the necessary
* data structures and renders corresponding block.
*
* @param newState New state that should be applied to the defer block.
* @param tNode TNode that represents a defer block.
* @param lContainer Represents an instance of a defer block.
* @param stateTmplIndex Index of a template that should be rendered.
*/
function renderDeferBlockState(
newState: DeferBlockInstanceState, lContainer: LContainer, stateTmplIndex: number|null): void {
newState: DeferBlockInstanceState, tNode: TNode, lContainer: LContainer,
stateTmplIndex: number|null): void {
const hostLView = lContainer[PARENT];

// Check if this view is not destroyed. Since the loading process was async,
// the view might end up being destroyed by the time rendering happens.
if (isDestroyed(hostLView)) return;

ngDevMode &&
assertDefined(
lContainer[DEFER_BLOCK_DETAILS],
'Expected an LContainer that represents ' +
'a defer block, but got a regular LContainer');
// Make sure this TNode belongs to TView that represents host LView.
ngDevMode && assertTNodeForLView(tNode, hostLView);

const lDetails = getLDeferBlockDetails(hostLView, tNode);

const lDetails = lContainer[DEFER_BLOCK_DETAILS]!;
ngDevMode && assertDefined(lDetails, 'Expected a defer block state defined');

// Note: we transition to the next state if the previous state was represented
// with a number that is less than the next state. For example, if the current
// state is "loading" (represented as `2`), we should not show a placeholder
// (represented as `1`).
if (lDetails.state < newState && stateTmplIndex !== null) {
lDetails.state = newState;
if (lDetails[DEFER_BLOCK_STATE] < newState && stateTmplIndex !== null) {
lDetails[DEFER_BLOCK_STATE] = newState;
const hostTView = hostLView[TVIEW];
const adjustedIndex = stateTmplIndex + HEADER_OFFSET;
const tNode = getTNode(hostTView, adjustedIndex) as TContainerNode;
Expand All @@ -245,7 +298,7 @@ function renderDeferBlockState(
// represents a `{#defer}` block, so always refer to the first one.
const viewIndex = 0;
removeLViewFromLContainer(lContainer, viewIndex);
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, {});
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null);
addLViewToLContainer(lContainer, embeddedLView, viewIndex);
}
}
Expand Down Expand Up @@ -336,12 +389,13 @@ function triggerResourceLoading(

/** Utility function to render `{:placeholder}` content (if present) */
function renderPlaceholder(lView: LView, tNode: TNode) {
const tView = lView[TVIEW];
const lContainer = lView[tNode.index];
ngDevMode && assertLContainer(lContainer);

const tDetails = tNode.value as TDeferBlockDetails;
const tDetails = getTDeferBlockDetails(tView, tNode);
renderDeferBlockState(
DeferBlockInstanceState.PLACEHOLDER, lContainer, tDetails.placeholderTmplIndex);
DeferBlockInstanceState.PLACEHOLDER, tNode, lContainer, tDetails.placeholderTmplIndex);
}

/**
Expand All @@ -351,9 +405,8 @@ function renderPlaceholder(lView: LView, tNode: TNode) {
* @param lContainer Represents an instance of a defer block.
* @param tNode Represents defer block info shared across all instances.
*/
function renderDeferStateAfterResourceLoading(lContainer: LContainer, tNode: TNode) {
const tDetails = tNode.value as TDeferBlockDetails;

function renderDeferStateAfterResourceLoading(
tDetails: TDeferBlockDetails, tNode: TNode, lContainer: LContainer) {
ngDevMode &&
assertDefined(
tDetails.loadingPromise, 'Expected loading Promise to exist on this defer block');
Expand All @@ -364,10 +417,11 @@ function renderDeferStateAfterResourceLoading(lContainer: LContainer, tNode: TNo

// Everything is loaded, show the primary block content
renderDeferBlockState(
DeferBlockInstanceState.COMPLETE, lContainer, tDetails.primaryTmplIndex);
DeferBlockInstanceState.COMPLETE, tNode, lContainer, tDetails.primaryTmplIndex);

} else if (tDetails.loadingState === DeferDependenciesLoadingState.FAILED) {
renderDeferBlockState(DeferBlockInstanceState.ERROR, lContainer, tDetails.errorTmplIndex);
renderDeferBlockState(
DeferBlockInstanceState.ERROR, tNode, lContainer, tDetails.errorTmplIndex);
}
});
}
Expand All @@ -378,14 +432,16 @@ function renderDeferStateAfterResourceLoading(lContainer: LContainer, tNode: TNo
* no additional actions are taken.
*/
function triggerDeferBlock(lView: LView, tNode: TNode) {
const tView = lView[TVIEW];
const lContainer = lView[tNode.index];
ngDevMode && assertLContainer(lContainer);

const tDetails = tNode.value as TDeferBlockDetails;
const tDetails = getTDeferBlockDetails(tView, tNode);

// Condition is triggered, try to render loading state and start downloading.
// Note: if a block is in a loading, completed or an error state, this call would be a noop.
renderDeferBlockState(DeferBlockInstanceState.LOADING, lContainer, tDetails.loadingTmplIndex);
renderDeferBlockState(
DeferBlockInstanceState.LOADING, tNode, lContainer, tDetails.loadingTmplIndex);

switch (tDetails.loadingState) {
case DeferDependenciesLoadingState.NOT_STARTED:
Expand All @@ -396,19 +452,20 @@ function triggerDeferBlock(lView: LView, tNode: TNode) {
// The `loadingState` might have changed to "loading".
if ((tDetails.loadingState as DeferDependenciesLoadingState) ===
DeferDependenciesLoadingState.IN_PROGRESS) {
renderDeferStateAfterResourceLoading(lContainer, tNode);
renderDeferStateAfterResourceLoading(tDetails, tNode, lContainer);
}
break;
case DeferDependenciesLoadingState.IN_PROGRESS:
renderDeferStateAfterResourceLoading(lContainer, tNode);
renderDeferStateAfterResourceLoading(tDetails, tNode, lContainer);
break;
case DeferDependenciesLoadingState.COMPLETE:
ngDevMode && assertDeferredDependenciesLoaded(tDetails);
renderDeferBlockState(
DeferBlockInstanceState.COMPLETE, lContainer, tDetails.primaryTmplIndex);
DeferBlockInstanceState.COMPLETE, tNode, lContainer, tDetails.primaryTmplIndex);
break;
case DeferDependenciesLoadingState.FAILED:
renderDeferBlockState(DeferBlockInstanceState.ERROR, lContainer, tDetails.errorTmplIndex);
renderDeferBlockState(
DeferBlockInstanceState.ERROR, tNode, lContainer, tDetails.errorTmplIndex);
break;
default:
if (ngDevMode) {
Expand Down
Loading

0 comments on commit a786a09

Please sign in to comment.