Skip to content

Commit

Permalink
refactor(core): initial implementation of {#defer} block runtime
Browse files Browse the repository at this point in the history
This commit adds an initial implementation of the `{#defer}` block runtime.
  • Loading branch information
AndrewKushnir committed Aug 12, 2023
1 parent 5212b47 commit 376e6b0
Show file tree
Hide file tree
Showing 8 changed files with 554 additions and 49 deletions.
341 changes: 315 additions & 26 deletions packages/core/src/render3/instructions/defer.ts

Large diffs are not rendered by default.

29 changes: 16 additions & 13 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, HostBindingsFunction, HostDirectiveBindingMap, HostDirectiveDefs, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {NodeInjectorFactory} from '../interfaces/injector';
import {getUniqueLViewId} from '../interfaces/lview_tracking';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDeferBlockDetails, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
import {Renderer} from '../interfaces/renderer';
import {RComment, RElement, RNode, RText} from '../interfaces/renderer_dom';
import {SanitizerFn} from '../interfaces/sanitization';
Expand Down Expand Up @@ -140,7 +140,7 @@ export function getOrCreateTNode(
tView: TView, index: number, type: TNodeType.Element|TNodeType.Text, name: string|null,
attrs: TAttributes|null): TElementNode;
export function getOrCreateTNode(
tView: TView, index: number, type: TNodeType.Container, name: string|null,
tView: TView, index: number, type: TNodeType.Container, value: string|TDeferBlockDetails|null,
attrs: TAttributes|null): TContainerNode;
export function getOrCreateTNode(
tView: TView, index: number, type: TNodeType.Projection, name: null,
Expand All @@ -152,16 +152,17 @@ export function getOrCreateTNode(
tView: TView, index: number, type: TNodeType.Icu, name: null,
attrs: TAttributes|null): TElementContainerNode;
export function getOrCreateTNode(
tView: TView, index: number, type: TNodeType, name: string|null, attrs: TAttributes|null):
TElementNode&TContainerNode&TElementContainerNode&TProjectionNode&TIcuContainerNode {
tView: TView, index: number, type: TNodeType, value: string|TDeferBlockDetails|null,
attrs: TAttributes|null): TElementNode&TContainerNode&TElementContainerNode&TProjectionNode&
TIcuContainerNode {
ngDevMode && index !== 0 && // 0 are bogus nodes and they are OK. See `createContainerRef` in
// `view_engine_compatibility` for additional context.
assertGreaterThanOrEqual(index, HEADER_OFFSET, 'TNodes can\'t be in the LView header.');
// Keep this function short, so that the VM will inline it.
ngDevMode && assertPureTNodeType(type);
let tNode = tView.data[index] as TNode;
if (tNode === null) {
tNode = createTNodeAtIndex(tView, index, type, name, attrs);
tNode = createTNodeAtIndex(tView, index, type, value, attrs);
if (isInI18nBlock()) {
// If we are in i18n block then all elements should be pre declared through `Placeholder`
// See `TNodeType.Placeholder` and `LFrame.inI18n` for more context.
Expand All @@ -171,7 +172,7 @@ export function getOrCreateTNode(
}
} else if (tNode.type & TNodeType.Placeholder) {
tNode.type = type;
tNode.value = name;
tNode.value = value;
tNode.attrs = attrs;
const parent = getCurrentParentTNode();
tNode.injectorIndex = parent === null ? -1 : parent.injectorIndex;
Expand All @@ -184,13 +185,14 @@ export function getOrCreateTNode(
}

export function createTNodeAtIndex(
tView: TView, index: number, type: TNodeType, name: string|null, attrs: TAttributes|null) {
tView: TView, index: number, type: TNodeType, value: string|TDeferBlockDetails|null,
attrs: TAttributes|null) {
const currentTNode = getCurrentTNodePlaceholderOk();
const isParent = isCurrentTNodeParent();
const parent = isParent ? currentTNode : currentTNode && currentTNode.parent;
// Parents cannot cross component boundaries because components will be used in multiple places.
const tNode = tView.data[index] =
createTNode(tView, parent as TElementNode | TContainerNode, type, index, name, attrs);
createTNode(tView, parent as TElementNode | TContainerNode, type, index, value, attrs);
// Assign a pointer to the first child node of a given view. The first node is not always the one
// at index 0, in case of i18n, index 0 can be the instruction `i18nStart` and the first node has
// the index 1 or more, so we can't just check node index.
Expand Down Expand Up @@ -558,7 +560,7 @@ export function storeCleanupWithContext(
*/
export function createTNode(
tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType.Container,
index: number, tagName: string|null, attrs: TAttributes|null): TContainerNode;
index: number, value: string|TDeferBlockDetails|null, attrs: TAttributes|null): TContainerNode;
export function createTNode(
tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType.Element|TNodeType.Text,
index: number, tagName: string|null, attrs: TAttributes|null): TElementNode;
Expand All @@ -573,10 +575,10 @@ export function createTNode(
index: number, tagName: string|null, attrs: TAttributes|null): TProjectionNode;
export function createTNode(
tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType, index: number,
tagName: string|null, attrs: TAttributes|null): TNode;
value: string|TDeferBlockDetails|null, attrs: TAttributes|null): TNode;
export function createTNode(
tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType, index: number,
value: string|null, attrs: TAttributes|null): TNode {
value: string|TDeferBlockDetails|null, attrs: TAttributes|null): TNode {
ngDevMode && index !== 0 && // 0 are bogus nodes and they are OK. See `createContainerRef` in
// `view_engine_compatibility` for additional context.
assertGreaterThanOrEqual(index, HEADER_OFFSET, 'TNodes can\'t be in the LView header.');
Expand All @@ -600,8 +602,8 @@ export function createTNode(
propertyBindings: null,
flags,
providerIndexes: 0,
value: value,
attrs: attrs,
value,
attrs,
mergedAttrs: null,
localNames: null,
initialInputs: undefined,
Expand Down Expand Up @@ -1411,6 +1413,7 @@ export function createLContainer(
null, // view refs
null, // moved views
null, // dehydrated views
null, // defer block details
];
ngDevMode &&
assertEqual(
Expand Down
28 changes: 20 additions & 8 deletions packages/core/src/render3/instructions/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {assertFirstCreatePass} from '../assert';
import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks';
import {ComponentTemplate} from '../interfaces/definition';
import {LocalRefExtractor, TAttributes, TContainerNode, TNode, TNodeType} from '../interfaces/node';
import {LocalRefExtractor, TAttributes, TContainerNode, TDeferBlockDetails, TNode, TNodeType} from '../interfaces/node';
import {RComment} from '../interfaces/renderer_dom';
import {isDirectiveHost} from '../interfaces/type_checks';
import {HEADER_OFFSET, HYDRATION, LView, RENDERER, TView, TViewType} from '../interfaces/view';
Expand All @@ -26,15 +26,15 @@ import {addToViewTree, createDirectivesInstances, createLContainer, createTView,

function templateFirstCreatePass(
index: number, tView: TView, lView: LView, templateFn: ComponentTemplate<any>|null,
decls: number, vars: number, tagName?: string|null, attrsIndex?: number|null,
decls: number, vars: number, value?: string|TDeferBlockDetails|null, attrsIndex?: number|null,
localRefsIndex?: number|null): TContainerNode {
ngDevMode && assertFirstCreatePass(tView);
ngDevMode && ngDevMode.firstCreatePass++;
const tViewConsts = tView.consts;

// TODO(pk): refactor getOrCreateTNode to have the "create" only version
const tNode = getOrCreateTNode(
tView, index, TNodeType.Container, tagName || null,
tView, index, TNodeType.Container, value || null,
getConstant<TAttributes>(tViewConsts, attrsIndex));

resolveDirectives(tView, lView, tNode, getConstant<string[]>(tViewConsts, localRefsIndex));
Expand Down Expand Up @@ -75,14 +75,26 @@ export function ɵɵtemplate(
index: number, templateFn: ComponentTemplate<any>|null, decls: number, vars: number,
tagName?: string|null, attrsIndex?: number|null, localRefsIndex?: number|null,
localRefExtractor?: LocalRefExtractor) {
templateInternal(
index, templateFn, decls, vars, tagName, attrsIndex, localRefsIndex, localRefExtractor);
}

export function templateInternal(
index: number, templateFn: ComponentTemplate<any>|null, decls: number, vars: number,
value?: string|(() => TDeferBlockDetails)|null, attrsIndex?: number|null,
localRefsIndex?: number|null, localRefExtractor?: LocalRefExtractor) {
const lView = getLView();
const tView = getTView();
const adjustedIndex = index + HEADER_OFFSET;

const tNode = tView.firstCreatePass ? templateFirstCreatePass(
adjustedIndex, tView, lView, templateFn, decls, vars,
tagName, attrsIndex, localRefsIndex) :
tView.data[adjustedIndex] as TContainerNode;
const adjustedIndex = index + HEADER_OFFSET;
const tNode = tView.firstCreatePass ?
templateFirstCreatePass(
adjustedIndex, tView, lView, templateFn, decls, vars,
// Defer block details are needed only once during the first creation pass,
// so we pass a function and invoke it here to avoid re-constructing the same
// object for each subsequent creation run.
(typeof value === 'function' ? value() : value), attrsIndex, localRefsIndex) :
tView.data[adjustedIndex] as TContainerNode;
setCurrentTNode(tNode, false);

const comment = _locateOrCreateContainerAnchor(tView, lView, tNode, index) as RComment;
Expand Down
40 changes: 39 additions & 1 deletion packages/core/src/render3/interfaces/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const NATIVE = 7;
export const VIEW_REFS = 8;
export const MOVED_VIEWS = 9;
export const DEHYDRATED_VIEWS = 10;
export const DEFER_BLOCK_DETAILS = 11;


/**
Expand All @@ -55,7 +56,38 @@ export const DEHYDRATED_VIEWS = 10;
* which views are already in the DOM (and don't need to be re-added) and so we can
* remove views from the DOM when they are no longer required.
*/
export const CONTAINER_HEADER_OFFSET = 11;
export const CONTAINER_HEADER_OFFSET = 12;

/**
* Describes the current state of this {#defer} block instance.
*/
export const enum DeferBlockInstanceState {
/** Initial state, nothing is rendered yet */
INITIAL,

/** The {:placeholder} block content is rendered */
PLACEHOLDER,

/** The {:loading} block content is rendered */
LOADING,

/** The main content block content is rendered */
COMPLETE,

/** The {:error} block content is rendered */
ERROR
}

/**
* Describes instance-specific {#defer} block data.
*
* Note: currently there is only the `state` field, but more fields
* would be added later to keep track of `after` and `maximum` features
* (which would require per-instance state).
*/
export interface LDeferBlockDetails {
state: DeferBlockInstanceState;
}

/**
* The state associated with a container.
Expand Down Expand Up @@ -144,6 +176,12 @@ export interface LContainer extends Array<any> {
* logic finishes.
*/
[DEHYDRATED_VIEWS]: DehydratedContainerView[]|null;

/**
* If this LContainer represents an instance of a `{#defer}` block -
* this field contains instance-specific information about the block.
*/
[DEFER_BLOCK_DETAILS]: LDeferBlockDetails|null;
}

// Note: This hack is necessary so we don't erroneously get a circular dependency
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/render3/interfaces/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,11 @@ export type DirectiveTypeList =
(DirectiveType<any>|ComponentType<any>|
Type<any>/* Type as workaround for: Microsoft/TypeScript/issues/4881 */)[];

export type DependencyTypeList = (DirectiveType<any>|ComponentType<any>|PipeType<any>|Type<any>)[];
export type DependencyType = DirectiveType<any>|ComponentType<any>|PipeType<any>|Type<any>;

export type DependencyTypeList = Array<DependencyType>;

export type DependencyResolverFn = () => Array<Promise<DependencyType>>;

export type TypeOrFactory<T> = T|(() => T);

Expand Down
89 changes: 89 additions & 0 deletions packages/core/src/render3/interfaces/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import {KeyValueArray} from '../../util/array_utils';
import {TStylingRange} from '../interfaces/styling';

import {DependencyResolverFn} from './definition';
import {TIcu} from './i18n';
import {CssSelector} from './projection';
import {RNode} from './renderer_dom';
Expand Down Expand Up @@ -858,6 +859,94 @@ export interface TProjectionNode extends TNode {
value: null;
}

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

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

/** Dependency loading has completed successfully */
COMPLETE,

/** Dependency loading has failed */
FAILED,
}

/** Configuration object for a `{:loading}` block as it is stored in the component constants. */
export type DeferredLoadingBlockConfig = [minimumTime: number|null, afterTime: number|null];

/** Configuration object for a `{:placeholder}` block as it is stored in the component constants. */
export type DeferredPlaceholderBlockConfig = [afterTime: number|null];


/**
* Describes the data shared across all instances of a {#defer} block.
*/
export interface TDeferBlockDetails {
/**
* Index in an LView and TData arrays where a template for the primary content
* can be found.
*/
primaryTmplIndex: number;

/**
* Index in an LView and TData arrays where a template for the `{:loading}`
* block can be found.
*/
loadingTmplIndex: number|null;

/**
* Extra configuration parameters (such as `after` and `minimum`)
* for the `{:loading}` block.
*/
loadingBlockConfig: DeferredLoadingBlockConfig|null;

/**
* Index in an LView and TData arrays where a template for the `{:placeholder}`
* block can be found.
*/
placeholderTmplIndex: number|null;

/**
* Extra configuration parameters (such as `after` and `minimum`)
* for the `{:placeholder}` block.
*/
placeholderBlockConfig: DeferredPlaceholderBlockConfig|null;

/**
* Index in an LView and TData arrays where a template for the `{:error}`
* block can be found.
*/
errorTmplIndex: number|null;

/**
* Compiler-generated function that loads all dependencies for a `{#defer}` block.
*/
dependencyResolverFn: DependencyResolverFn|null;

/**
* Keeps track of the current loading state of defer block dependencies.
*/
loadingState: DeferDependenciesLoadingState;

/**
* Dependency loading Promise. This Promise is helpful for cases when there
* are multiple instances of a defer block (e.g. if it was used inside of an *ngFor),
* which all await the same set of dependencies.
*/
loadingPromise: Promise<unknown>|null;

/**
* If dependency loading fails, this field retains the error message, which can
* be accessed by all instances of a defer block.
*/
loadingFailedReason: string|null;
}

/**
* A union type representing all TNode types that can host a directive.
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/render3/interfaces/type_checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ export function isProjectionTNode(tNode: TNode): boolean {
export function hasI18n(lView: LView): boolean {
return (lView[FLAGS] & LViewFlags.HasI18n) === LViewFlags.HasI18n;
}

export function isDestroyed(lView: LView): boolean {
return (lView[FLAGS] & LViewFlags.Destroyed) === LViewFlags.Destroyed;
}
Loading

0 comments on commit 376e6b0

Please sign in to comment.