Skip to content

Commit

Permalink
WIP: further cleanup of defer runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKushnir committed Aug 1, 2023
1 parent fbcfc9d commit 1457e56
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 72 deletions.
83 changes: 53 additions & 30 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,62 @@
* 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 {Type} from '../../interface/type';
import {assertDefined, assertFunction} from '../../util/assert';

import {assertDefined} from '../../util/assert';
import {assertLContainer} from '../assert';
import {bindingUpdated} from '../bindings';
import {DEFER_DETAILS, DeferInstanceState, LContainer} from '../interfaces/container';
import {ComponentTemplate} from '../interfaces/definition';
import {DeferDepsLoadingState, TContainerNode, TDeferDetails, TNode} from '../interfaces/node';
import {getComponentDef, getDirectiveDef, getPipeDef} from '../definition';
import {DEFER_BLOCK_DETAILS, DeferInstanceState, LContainer} from '../interfaces/container';
import {ComponentTemplate, DependencyResolverFn, DirectiveDefList, PipeDefList} from '../interfaces/definition';
import {DeferDepsLoadingState, TContainerNode, TDeferBlockDetails, TNode} from '../interfaces/node';
import {isDestroyed} from '../interfaces/type_checks';
import {HEADER_OFFSET, LView, PARENT, TVIEW} from '../interfaces/view';
import {getCurrentTNode, getLView, getSelectedTNode, nextBindingIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {getTNode, storeLViewOnDestroy} from '../util/view_utils';
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer} from '../view_manipulation';

import {DeferredDepsFn, templateInternal} from './template';
import {templateInternal} from './template';

// ***** IMPLEMENTATION DETAILS *****
//
// Questions:
// - how dow we want to store things like IntersectionObserver?
// should it be a instance per application (e.g. `providedIn: root`) or per-LView?
// - how would hydration pick up currently activated case?
// - do we need smth like `deferApply` that would activate the necessary logic?
// - for ex. if we need to use IntersectionObserver, we want to activate it during
// the "update" phase?
// - how do we deal with prefetch? do we need a special flag for prefetch status?
//
// Important notes:
// - we should make sure to store cleanup fns to cleanup on destroy

// TODO: add docs here
export function ɵɵdefer(
index: number, templateFn: ComponentTemplate<any>|null, deferredDepsFn: DeferredDepsFn,
decls: number, vars: number, loadingTmplIndex: number|null = null,
placeholderTmplIndex: number|null = null, errorTmplIndex: number|null = null,
loadingConfigIndex: number|null = null, placeholderConfigIndex: number|null = null) {
// TODO: move `deferredDepsFn` to `TDeferDetails`?
deferredDepsFn = deferredDepsFn ?? (() => []);

const deferConfig: TDeferDetails = {
index: number, templateFn: ComponentTemplate<any>|null,
dependencyResolverFn: DependencyResolverFn|null, decls: number, vars: number,
loadingTmplIndex: number|null = null, placeholderTmplIndex: number|null = null,
errorTmplIndex: number|null = null, loadingConfigIndex: number|null = null,
placeholderConfigIndex: number|null = null) {
const deferConfig: TDeferBlockDetails = {
loadingTmplIndex,
loadingConfigIndex,
placeholderTmplIndex,
placeholderConfigIndex,
errorTmplIndex,
dependencyResolverFn,
loadingPromise: null,
loadingState: DeferDepsLoadingState.NOT_STARTED,
loadingFailedReason: null,
};

templateInternal(index, templateFn, deferredDepsFn, decls, vars, deferConfig);
templateInternal(index, templateFn, decls, vars, deferConfig);

const lView = getLView();
const adjustedIndex = index + HEADER_OFFSET;
const lContainer = lView[adjustedIndex];

// Init instance-specific defer details for this LContainer.
lContainer[DEFER_DETAILS] = {state: DeferInstanceState.INITIAL};
lContainer[DEFER_BLOCK_DETAILS] = {state: DeferInstanceState.INITIAL};
}

// TODO: add docs
Expand Down Expand Up @@ -166,10 +164,10 @@ function renderDeferState(

ngDevMode &&
assertDefined(
lContainer[DEFER_DETAILS],
lContainer[DEFER_BLOCK_DETAILS],
'Expected an LContainer that represents ' +
'a defer block, but got a regular LContainer');
const lDetails = lContainer[DEFER_DETAILS]!;
const lDetails = lContainer[DEFER_BLOCK_DETAILS]!;

// Note: we transition to the next state if the previous state was
// less than the next state. For example, if the current state is "loading",
Expand All @@ -196,7 +194,8 @@ function renderDeferState(
* @param tNode Represents Defer block info shared between instances.
*/
function triggerResourceLoading(tNode: TNode) {
const tDetails = tNode.value as TDeferDetails;
const tView = tNode.tView!;
const tDetails = tNode.value as TDeferBlockDetails;

if (tDetails.loadingState !== DeferDepsLoadingState.NOT_STARTED) {
// If the loading status is different from initial one, it means that
Expand All @@ -208,18 +207,35 @@ function triggerResourceLoading(tNode: TNode) {
// Switch from NOT_STARTED -> IN_PROGRESS state.
tDetails.loadingState = DeferDepsLoadingState.IN_PROGRESS;

// At this state we expect that dependency function wasn't invoked yet.
const dependenciesFn = tNode.tView!.dependencies as Function;
ngDevMode && assertFunction(dependenciesFn, 'Expected dependency function for this defer block');
// The `dependenciesFn` might be `null` when all dependencies within
// a given `{#defer}` block were eagerly references elsewhere in a file,
// thus no dynamic `import()`s were produced.
const dependenciesFn = tDetails.dependencyResolverFn;
if (!dependenciesFn) {
tDetails.loadingPromise = Promise.resolve().then(() => {
tDetails.loadingState = DeferDepsLoadingState.COMPLETE;
});
return;
}

// Start downloading...
tDetails.loadingPromise = Promise.allSettled(dependenciesFn()).then(results => {
let failedReason = null;
const loadedDependencies: Array<Type<unknown>> = [];
const directiveDefs: DirectiveDefList = [];
const pipeDefs: PipeDefList = [];

for (const result of results) {
if (result.status === 'fulfilled') {
loadedDependencies.push(result.value);
const dependency = result.value;
const directiveDef = getComponentDef(dependency) || getDirectiveDef(dependency);
if (directiveDef) {
directiveDefs.push(directiveDef);
} else {
const pipeDef = getPipeDef(dependency);
if (pipeDef) {
pipeDefs.push(pipeDef);
}
}
} else {
failedReason = result.reason;
break;
Expand All @@ -235,8 +251,15 @@ function triggerResourceLoading(tNode: TNode) {
} else {
tDetails.loadingState = DeferDepsLoadingState.COMPLETE;

// Replace dependency function with loaded list of dependencies.
tNode.tView!.dependencies = loadedDependencies;
// Update directive and pipe registries to add newly downloaded dependencies.
if (directiveDefs.length > 0) {
tView.directiveRegistry = tView.directiveRegistry ?
[...tView.directiveRegistry, ...directiveDefs] :
directiveDefs;
}
if (pipeDefs.length > 0) {
tView.pipeRegistry = tView.pipeRegistry ? [...tView.pipeRegistry, ...pipeDefs] : pipeDefs;
}
}
});
}
Expand All @@ -249,7 +272,7 @@ function triggerResourceLoading(tNode: TNode) {
* @param tNode Represents defer block info shared across all instances.
*/
function renderDeferStateAfterResourceLoading(lContainer: LContainer, tNode: TNode) {
const tDetails = tNode.value as TDeferDetails;
const tDetails = tNode.value as TDeferBlockDetails;

ngDevMode &&
assertDefined(
Expand Down Expand Up @@ -278,7 +301,7 @@ function renderDeferBlock(
const lContainer = lView[tNode.index];
ngDevMode && assertLContainer(lContainer);

const tDetails = tNode.value as TDeferDetails;
const tDetails = tNode.value as TDeferBlockDetails;

if (oldValue === NO_CHANGE && newValue === false) {
// We set the value for the first time, render a placeholder.
Expand Down
22 changes: 12 additions & 10 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import {getFactoryDef} from '../definition_factory';
import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} from '../di';
import {throwMultipleComponentError} from '../errors';
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefList, DirectiveDefListOrFactory, HostBindingsFunction, HostDirectiveBindingMap, HostDirectiveDefs, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {ComponentDef, ComponentTemplate, DependencyResolverFn, DirectiveDef, DirectiveDefList, 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, TDeferDetails, 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 @@ -141,7 +141,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|TDeferDetails|null,
tView: TView, index: number, type: TNodeType.Container, name: string|TDeferBlockDetails|null,
attrs: TAttributes|null): TContainerNode;
export function getOrCreateTNode(
tView: TView, index: number, type: TNodeType.Projection, name: null,
Expand All @@ -153,7 +153,7 @@ 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, value: string|TDeferDetails|null,
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
Expand Down Expand Up @@ -186,7 +186,7 @@ export function getOrCreateTNode(
}

export function createTNodeAtIndex(
tView: TView, index: number, type: TNodeType, value: string|TDeferDetails|null,
tView: TView, index: number, type: TNodeType, value: string|TDeferBlockDetails|null,
attrs: TAttributes|null) {
const currentTNode = getCurrentTNodePlaceholderOk();
const isParent = isCurrentTNodeParent();
Expand Down Expand Up @@ -403,7 +403,6 @@ export function createTView(
data: blueprint.slice().fill(null, bindingStartIndex),
bindingStartIndex: bindingStartIndex,
expandoStartIndex: initialViewLength,
dependencies: null,
hostBindingOpCodes: null,
firstCreatePass: true,
firstUpdatePass: true,
Expand Down Expand Up @@ -565,7 +564,7 @@ export function createTNode(
index: number, tagName: string|null, attrs: TAttributes|null): TContainerNode;
export function createTNode(
tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType.Container,
index: number, value: TDeferDetails|null, attrs: TAttributes|null): TContainerNode;
index: number, value: 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 @@ -583,7 +582,7 @@ export function createTNode(
tagName: string|null, attrs: TAttributes|null): TNode;
export function createTNode(
tView: TView, tParent: TElementNode|TContainerNode|null, type: TNodeType, index: number,
value: string|TDeferDetails|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 Down Expand Up @@ -1073,9 +1072,10 @@ function findDirectiveDefMatches(
ngDevMode && assertFirstCreatePass(tView);
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer);

const registry = tView.directiveRegistry;
/*
let registry = [...(tView.directiveRegistry ?? [])];
// TODO: do we do it once per tView?
// TODO: separate pipes and directives.
// TODO: add an assert that tView.dependencies are resolved!
if (Array.isArray(tView.dependencies)) {
for (const dep of tView.dependencies) {
const dir = getComponentDef(dep) || getDirectiveDef(dep);
Expand All @@ -1086,6 +1086,8 @@ function findDirectiveDefMatches(
} else if (tView.dependencies instanceof Function) {
throw new Error(`Unresolved lazy dependencies... should not be possible!`);
}
*/

let matches: DirectiveDef<unknown>[]|null = null;
let hostDirectiveDefs: HostDirectiveDefs|null = null;
if (registry) {
Expand Down
29 changes: 11 additions & 18 deletions packages/core/src/render3/instructions/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import {validateMatchingNode, validateNodeExists} from '../../hydration/error_ha
import {TEMPLATES} from '../../hydration/interfaces';
import {locateNextRNode, siblingAfter} from '../../hydration/node_lookup_utils';
import {calcSerializedContainerSize, isDisconnectedNode, markRNodeAsClaimedByHydration, setSegmentHead} from '../../hydration/utils';
import {Type} from '../../interface/type';
import {assertEqual} from '../../util/assert';
import {assertFirstCreatePass} from '../assert';
import {attachPatchData} from '../context_discovery';
import {registerPostOrderHooks} from '../hooks';
import {ComponentTemplate} from '../interfaces/definition';
import {LocalRefExtractor, TAttributes, TContainerNode, TDeferDetails, TNode, TNodeType} from '../interfaces/node';
import {ComponentTemplate, DependencyResolverFn} from '../interfaces/definition';
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 @@ -25,12 +24,9 @@ import {getConstant} from '../util/view_utils';

import {addToViewTree, createDirectivesInstances, createLContainer, createTView, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared';

export type DeferredDepsFn = () => Array<Promise<Type<unknown>>|Type<unknown>>;

export function templateFirstCreatePass(
index: number, tView: TView, lView: LView, templateFn: ComponentTemplate<any>|null,
deferredDepsFn: DeferredDepsFn|null, decls: number, vars: number,
value?: string|TDeferDetails|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++;
Expand All @@ -48,9 +44,6 @@ export function templateFirstCreatePass(
TViewType.Embedded, tNode, templateFn, decls, vars, tView.directiveRegistry,
tView.pipeRegistry, null, tView.schemas, tViewConsts, null /* ssrId */);

// TODO: pass this info via `createTView` instead.
embeddedTView.dependencies = deferredDepsFn ?? tView.dependencies;

if (tView.queries !== null) {
tView.queries.template(tView, tNode);
embeddedTView.queries = tView.queries.embeddedTView(tNode);
Expand Down Expand Up @@ -83,21 +76,21 @@ export function ɵɵtemplate(
tagName?: string|null, attrsIndex?: number|null, localRefsIndex?: number|null,
localRefExtractor?: LocalRefExtractor) {
return templateInternal(
index, templateFn, null, decls, vars, tagName, attrsIndex, localRefsIndex, localRefExtractor);
index, templateFn, decls, vars, tagName, attrsIndex, localRefsIndex, localRefExtractor);
}

export function templateInternal(
index: number, templateFn: ComponentTemplate<any>|null, deferredDepsFn: DeferredDepsFn|null,
decls: number, vars: number, value?: string|TDeferDetails|null, attrsIndex?: number|null,
localRefsIndex?: number|null, localRefExtractor?: LocalRefExtractor) {
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, deferredDepsFn,
decls, vars, value, attrsIndex, localRefsIndex) :
tView.data[adjustedIndex] as TContainerNode;
const tNode = tView.firstCreatePass ?
templateFirstCreatePass(
adjustedIndex, tView, lView, templateFn, decls, vars, value, attrsIndex, localRefsIndex) :
tView.data[adjustedIndex] as TContainerNode;
setCurrentTNode(tNode, false);

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


/**
Expand All @@ -68,10 +68,15 @@ export const enum DeferInstanceState {
ERROR
}

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

/**
Expand Down Expand Up @@ -162,7 +167,11 @@ export interface LContainer extends Array<any> {
*/
[DEHYDRATED_VIEWS]: DehydratedContainerView[]|null;

[DEFER_DETAILS]: LDeferDetails|null;
/**
* If this LContainer represents an instance of a `{#defer}` block -
* this field contains instance-specific information about this 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
Loading

0 comments on commit 1457e56

Please sign in to comment.