@lotto24-angular/imports-orchestrator
Version:
Orchestrate dynamically imported components in Angular applications
904 lines (870 loc) • 40.3 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Directive, Output, inject, Renderer2, ElementRef, Input, InjectionToken, Injectable, Injector, runInInjectionContext, ViewContainerRef, NgModuleRef, ChangeDetectorRef, createNgModule, Component, ChangeDetectionStrategy, ViewChild, makeEnvironmentProviders } from '@angular/core';
import { Subscription, Subject, ReplaySubject, takeUntil, share, firstValueFrom, tap, filter, of, shareReplay, map, observeOn, asyncScheduler, startWith, pairwise, mergeMap, skip, race, timeout, catchError, TimeoutError, from, take } from 'rxjs';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { assertPromise, assertSignal, resolvePromiseWithRetries, assertStandalone } from '@lotto24-angular/util';
import { toObservable } from '@angular/core/rxjs-interop';
class ImportsOrchestratorLifecycleDirective {
constructor() {
this.importQueued = new EventEmitter();
this.importStarted = new EventEmitter();
this.importFinished = new EventEmitter();
this.importComponent = new EventEmitter();
this.importErrored = new EventEmitter();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorLifecycleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorLifecycleDirective, isStandalone: true, selector: "[importLifecycle]", outputs: { importQueued: "importQueued", importStarted: "importStarted", importFinished: "importFinished", importComponent: "importComponent", importErrored: "importErrored" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorLifecycleDirective, decorators: [{
type: Directive,
args: [{
selector: '[importLifecycle]',
standalone: true,
}]
}], propDecorators: { importQueued: [{
type: Output
}], importStarted: [{
type: Output
}], importFinished: [{
type: Output
}], importComponent: [{
type: Output
}], importErrored: [{
type: Output
}] } });
class ImportsOrchestratorCSSClassDirective {
constructor() {
this.subscriptions = new Subscription();
this.lifecycle = inject(ImportsOrchestratorLifecycleDirective, {
self: true,
});
this.subscriptions.add(this.lifecycle.importComponent.subscribe(this.onImportComponent.bind(this)));
}
onImportComponent(componentRef) {
if (!this.cssClass)
return;
const renderer2 = componentRef.injector.get(Renderer2);
const elementRef = componentRef.injector.get(ElementRef);
const htmlElement = elementRef.nativeElement;
const classes = this.cssClass.match(/[^\s]+/gi);
classes?.forEach((c) => renderer2.addClass(htmlElement, c));
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorCSSClassDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorCSSClassDirective, isStandalone: true, selector: "[importCSSClass]", inputs: { cssClass: "cssClass" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorCSSClassDirective, decorators: [{
type: Directive,
args: [{
selector: '[importCSSClass]',
standalone: true,
}]
}], ctorParameters: () => [], propDecorators: { cssClass: [{
type: Input
}] } });
class ImportsOrchestratorIODirective {
constructor() {
this.destroy$ = new Subject();
this._inputs = new ReplaySubject(1);
this.inputs$ = this._inputs.pipe(takeUntil(this.destroy$), share());
this._outputs = new ReplaySubject(1);
this.outputs$ = this._outputs.pipe(takeUntil(this.destroy$), share());
}
set inputs(value) {
this._inputs.next(value ?? {});
}
set outputs(value) {
this._outputs.next(value ?? {});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorIODirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorIODirective, isStandalone: true, selector: "[importLifecycle]", inputs: { inputs: "inputs", outputs: "outputs" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorIODirective, decorators: [{
type: Directive,
args: [{
selector: '[importLifecycle]',
standalone: true,
}]
}], propDecorators: { inputs: [{
type: Input
}], outputs: [{
type: Input
}] } });
const IMPORT_PRIORITY_LOWEST = 9999999999;
function findImportPriority(priorities, importId, logger) {
if (typeof priorities[importId] === 'number') {
return priorities[importId];
}
const key = Object.keys(priorities).find((key) => importId.startsWith(key));
if (key) {
return priorities[key];
}
logger.warn(`no priority found for import '${importId}; falling back to lowest priority'`);
return IMPORT_PRIORITY_LOWEST;
}
function findFn(config, key, trail = []) {
const stringOrFn = config[key];
if (trail.includes(key)) {
throw new Error(`circular reference found: ${[...trail, key].join(' => ')}`);
}
if (typeof stringOrFn === 'string') {
return findFn(config, stringOrFn, [...trail, key]);
}
if (typeof stringOrFn === 'function') {
return stringOrFn;
}
throw new Error(`Missing entry for key="${key}"`);
}
/**
* recursive loading of queued features
*/
async function processQueueItem(queue, logger) {
// let's take the next item off the queue
const item = queue.take();
// let's stop if there are no items in the queue
if (!item) {
logger.debug('queue is drained');
return;
}
logger.debug(`queue item resolve (${item})`);
try {
item.hooks.start.next(item);
item.lifecycle?.importStarted?.emit();
const result = await item.resolveFn(item);
item.hooks.finish.next(item);
item.lifecycle?.importFinished?.emit(result);
item.callback && item.callback(result, null);
logger.debug(`queue item resolved (${item})`);
}
catch (x) {
item.hooks.error.next([item, x]);
item.lifecycle?.importErrored?.emit(x);
item.callback && item.callback(null, x);
logger.error(`error resolving queue item (${item})`, x);
}
finally {
item.hooks.start.complete();
item.hooks.finish.complete();
item.hooks.error.complete();
}
}
const IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_CONCURRENCY');
const IMPORTS_ORCHESTRATOR_FEATURE_INTERCEPTOR = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_INTERCEPTOR');
const IMPORTS_ORCHESTRATOR_FEATURE_ROUTING = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_DEFER_UNTIL_FIRST_NAVIGATION');
const IMPORTS_ORCHESTRATOR_FEATURE_TIMEOUT = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_TIMEOUT');
const IMPORTS_ORCHESTRATOR_FEATURE_LOGGER = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_LOGGER');
const IMPORTS_ORCHESTRATOR_FEATURE_QUEUE = new InjectionToken('IMPORTS_ORCHESTRATOR_FEATURE_QUEUE');
const IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION = new InjectionToken('IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION');
const IMPORTS_STORE = {};
const IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE = new InjectionToken('IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE', { providedIn: 'platform', factory: () => IMPORTS_STORE });
class ImportsQueueProcessor {
constructor() {
this.logger = inject(IMPORTS_ORCHESTRATOR_FEATURE_LOGGER);
this.queue = inject(IMPORTS_ORCHESTRATOR_FEATURE_QUEUE);
this.isRoutingActive$ = inject(IMPORTS_ORCHESTRATOR_FEATURE_ROUTING);
this.concurrency = inject(IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY);
this.running = 0;
}
static { this.processing = false; }
process() {
if (!ImportsQueueProcessor.processing) {
// do not await, as it would block the lifecycle callback from completing until the queue is processed
ImportsQueueProcessor.processing = true;
this.processQueue()
.then(() => {
this.logger.debug('queue processing ended');
})
.catch(() => {
this.logger.debug('queue processing failed');
})
.finally(() => {
ImportsQueueProcessor.processing = false;
});
}
}
async processQueue() {
await this.suspendForNavigation();
const concurrency = this.updateConcurrency();
const concurrentBatch = [];
for (let i = this.running; i < concurrency; i++) {
this.running++;
concurrentBatch.push(this.processItem());
}
this.logger.debug(`queue starting ${concurrentBatch.length} item(s) to reach max concurrency (running=${this.running})`);
await Promise.all(concurrentBatch);
}
async processItem() {
await processQueueItem(this.queue, this.logger);
this.running--;
if (!this.queue.empty) {
await this.processQueue();
}
}
updateConcurrency() {
const value = typeof this.concurrency === 'function'
? this.concurrency()
: this.concurrency;
if (value !== this.concurrency) {
this.logger.debug(`queue concurrency changed to ${value}`);
}
return value;
}
async suspendForNavigation() {
// suspend processing while routing, as navigation takes precedence
return firstValueFrom(this.isRoutingActive$.pipe(tap((active) => {
if (active) {
this.logger.debug('queue processing suspended for navigation');
}
}), filter((active) => !active)));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsQueueProcessor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsQueueProcessor, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsQueueProcessor, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class ImportService {
constructor() {
this.queueProcessor = inject(ImportsQueueProcessor);
this.timeout = inject(IMPORTS_ORCHESTRATOR_FEATURE_TIMEOUT);
this.logger = inject(IMPORTS_ORCHESTRATOR_FEATURE_LOGGER);
this.queue = inject(IMPORTS_ORCHESTRATOR_FEATURE_QUEUE);
this.orchestration = inject(IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION);
this.interceptor = inject(IMPORTS_ORCHESTRATOR_FEATURE_INTERCEPTOR, { optional: true });
this.injector = inject(Injector);
}
createQueueItem(identifier, destroy$, options = {}) {
const opts = {
...options,
injector: options.injector ?? this.injector,
timeout: options.timeout ?? this.timeout,
};
const imports = this.importsFromDI(opts.injector);
const resolveFn = this.resolveFnFromImports(imports, identifier);
const priority = findImportPriority(this.orchestration, identifier, this.logger);
const hooks = {
queued: new Subject(),
start: new Subject(),
finish: new Subject(),
error: new Subject(),
};
runInInjectionContext(this.injector, () => {
if (this.interceptor) {
this.interceptor(identifier, hooks);
}
});
return {
...opts,
priority,
identifier,
resolveFn,
destroy$,
hooks,
logger: this.logger,
toString: () => `="${identifier}", =${priority}`,
};
}
async addItemToQueue(item) {
const promise = new Promise((resolve, reject) => {
item.callback = (result, err) => {
if (err) {
reject(err);
}
else {
resolve(result);
}
};
});
this.queue.insert(item.priority, item);
item.lifecycle?.importQueued?.emit();
item.hooks.queued.next(item);
item.hooks.queued.complete();
this.logger.debug(`queue insert ${item.toString()}`);
this.queueProcessor.process();
return promise;
}
async bypassQueue(item) {
this.logger.debug(`bypass queue ${item.toString()}`);
return item.resolveFn(item);
}
removeItemFromQueue(item) {
return this.queue.take(item) !== undefined;
}
importsFromDI(injector) {
try {
const store = injector.get(IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE);
return store;
}
catch (x) {
throw new Error(`
Could not inject ${IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE}. Did you \`provideImports({...})\` in a component or module? If you did, you may need to provide an Injector when calling createQueueItem.
${x}`);
}
}
resolveFnFromImports(imports, identifier) {
try {
return findFn(imports, identifier);
}
catch (x) {
throw new Error(`
Could not find ImportResolveFn. Did you \`provideImports({...})\` in a component or module? If you did, you may need to provide an Injector when calling createQueueItem.
${x}`);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class ImportsOrchestratorQueueDirective {
constructor() {
this.io = inject(ImportsOrchestratorIODirective, {
self: true,
});
this.lifecycle = inject(ImportsOrchestratorLifecycleDirective, {
self: true,
});
this.destroyComponents$ = new Subject();
this.viewContainerRef = inject(ViewContainerRef);
this.moduleRef = inject(NgModuleRef);
this.importService = inject(ImportService);
this.item = null;
}
ngOnChanges(changes) {
const importInput = changes['import'];
if (importInput !== undefined &&
importInput.currentValue !== importInput.previousValue) {
this.createAndAddItemToQueue();
}
}
createAndAddItemToQueue() {
this.removeItemFromQueue();
this.destroyComponents$.next(); // destroy a previously mounted component(s)
const injector = Injector.create({
providers: [
...(this.providers ?? []),
{ provide: ViewContainerRef, useValue: this.viewContainerRef },
],
parent: this.moduleRef.injector,
});
this.item = this.importService.createQueueItem(this.import, this.destroyComponents$, {
...this,
injector,
lifecycle: this.lifecycle,
});
this.importService.addItemToQueue(this.item);
}
removeItemFromQueue() {
if (this.item) {
this.importService.removeItemFromQueue(this.item);
this.item = null;
}
}
ngOnDestroy() {
this.removeItemFromQueue();
this.destroyComponents$.next();
this.destroyComponents$.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorQueueDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorQueueDirective, isStandalone: true, selector: "[importQueue]", inputs: { import: "import", providers: "providers", timeout: "timeout" }, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorQueueDirective, decorators: [{
type: Directive,
args: [{
selector: '[importQueue]',
standalone: true,
}]
}], propDecorators: { import: [{
type: Input
}], providers: [{
type: Input
}], timeout: [{
type: Input
}] } });
var ImportsOrchestratorFeatureKind;
(function (ImportsOrchestratorFeatureKind) {
ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Logger"] = 0] = "Logger";
ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Timeout"] = 1] = "Timeout";
ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Routing"] = 2] = "Routing";
ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Concurrency"] = 3] = "Concurrency";
ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Interceptor"] = 4] = "Interceptor";
// internal
ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Orchestration"] = 5] = "Orchestration";
ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Queue"] = 6] = "Queue";
})(ImportsOrchestratorFeatureKind || (ImportsOrchestratorFeatureKind = {}));
function importsOrchestratorFeature(kind, providers) {
return { kind, providers };
}
function withConcurrencyRelativeToDownlinkSpeed(max = 4, min = 1) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY,
useValue: downlinkToConcurrencyFn(max, min),
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Concurrency, providers);
}
/**
* @param onUpdateConcurrencyFn called when queue processor updates concurrency;
*/
function withConcurrencyUpdateFn(onUpdateConcurrencyFn) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY,
useValue: onUpdateConcurrencyFn,
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Concurrency, providers);
}
function withConcurrencyStatic(value) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY,
useValue: value,
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Concurrency, providers);
}
function downlinkToConcurrencyFn(max, min = 1) {
return () => {
const downlink = navigator.connection?.downlink ?? 10;
if (downlink < 1) {
return min;
}
else if (downlink < 2) {
return Math.max(min, Math.floor(min + (max - min) / 4));
}
else if (downlink < 4) {
return Math.max(min, Math.floor(min + (max - min) / 2));
}
else if (downlink < 8) {
return Math.max(min, Math.floor(min + (max - min) / 1.5));
}
return max;
};
}
function withInterceptor(interceptor) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_INTERCEPTOR,
useValue: interceptor,
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Interceptor, providers);
}
function withSuspendWhileRouting(suspendForInitialNavigation = true) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_ROUTING,
useFactory: (router) => isRoutingActive$(router, suspendForInitialNavigation),
deps: [Router],
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Routing, providers);
}
function withoutRouting() {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_ROUTING,
useValue: of(false).pipe(shareReplay(1)),
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Routing, providers);
}
function isRoutingActive$(router, suspendForInitialNavigation) {
const navigationActive$ = router.events.pipe(filter((event) => event instanceof NavigationStart || event instanceof NavigationEnd), map((event) => {
if (event instanceof NavigationStart) {
return true;
}
return false;
}), observeOn(asyncScheduler), startWith(suspendForInitialNavigation));
return navigationActive$.pipe(shareReplay(1));
}
function withLogger(logger) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_LOGGER,
useValue: logger,
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Logger, providers);
}
function withTimeout(timeout = 10000) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_TIMEOUT,
useValue: timeout,
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Timeout, providers);
}
function bindComponentInputs(componentRef, item) {
item.io?.inputs$
.pipe(takeUntil(item.destroy$), startWith({}), pairwise(), filter(([_, current]) => Object.keys(current).length > 0), mergeMap(([previous, current]) => Object.entries(current).filter(([key, value]) => previous[key] !== value)))
.subscribe(([key, value]) => componentRef.setInput(key, value));
}
function bindComponentOutputs(componentRef, item) {
const outputs$ = item.io?.outputs$.pipe(share());
if (!outputs$)
return;
const output$ = outputs$.pipe(takeUntil(item.destroy$), startWith({}), pairwise(), filter(([_, current]) => Object.keys(current).length > 0), mergeMap(([previous, current]) => Object.entries(current).filter(([key, value]) => !previous[key]))
// tap(([key, value]) => console.log(key, value))
);
output$.subscribe(([key, value]) => {
if (typeof value !== 'function') {
throw new Error(`outputs.${key} must be a function, got '${typeof value}'`);
}
componentRef.instance[key]
.pipe(takeUntil(item.destroy$), takeUntil(output$.pipe(filter(([k]) => k === key), skip(1) // skip the first value emitted from replay
// tap(() => console.log('unsubscribe componentRef, key=' + key))
)))
.subscribe((data) => {
value(data);
});
});
}
const NG_MOD_DEF = getClosureSafeProperty({
ɵmod: getClosureSafeProperty,
});
const ES_MODULE = getClosureSafeProperty({
__esModule: getClosureSafeProperty,
});
function isESModule(type) {
return type[Symbol.toStringTag] === 'Module' || type[ES_MODULE];
}
function isNgModuleDef(type) {
return !!type[NG_MOD_DEF];
}
function getClosureSafeProperty(objWithPropertyToExtract) {
for (const key in objWithPropertyToExtract) {
if (objWithPropertyToExtract[key] === getClosureSafeProperty) {
return key;
}
}
throw Error('Could not find renamed property on target object.');
}
function deferUntilComponentReady$(componentRef, item) {
const instance = componentRef.instance;
if (!assertImportedComponentReadyEmitter(instance)) {
return of(undefined);
}
item.logger.debug(`deferring until component w/ import=${item.identifier} emits ready`);
componentRef.injector.get(ChangeDetectorRef).markForCheck(); // ensure Lifecycle hooks are called
return race(resolveReady(instance), item.destroy$).pipe(timeout(item.timeout), catchError((err) => {
if (err instanceof TimeoutError) {
item.logger.warn(`deferred component w/ import=${item.identifier} timed out after ${item.timeout}ms`);
}
else {
item.logger.error(`deferred component w/ import=${item.identifier} errored: ${err}`);
}
return of(undefined);
}));
}
function assertImportedComponentReadyEmitter(type) {
return type.importedComponentReady !== undefined;
}
function resolveReady(instance) {
const callback = instance.importedComponentReady.call(instance);
if (assertPromise(callback)) {
return from(callback);
}
return (assertSignal(callback)
? toObservable(callback)
: callback).pipe(filter((value) => value), map(() => undefined), take(1));
}
async function mountComponent(componentRef, item) {
item.destroy$.subscribe(() => componentRef.destroy());
if (item.io) {
bindComponentInputs(componentRef, item);
bindComponentOutputs(componentRef, item);
}
try {
await firstValueFrom(deferUntilComponentReady$(componentRef, item));
}
catch (x) {
// no-op
}
// This will trigger Angular lifecycle on componentRef's entire component tree
// * Bindings will be resolved
// * Projected content will be processed
// * Usages of ImportsOrchestratorQueueDirective in the tree will then insert items to the queue
// * It is of vital importance that items are queued before triggering processQueue again
// IMPORTANT: markForCheck is not enough, as it would not cause an immediate change detection cycle
componentRef.injector.get(ChangeDetectorRef).detectChanges();
}
function isAssumedESModuleContainingAngularComponentsOrModules(type) {
return typeof type === 'object' && Object.keys(type).some(key => key.includes('Module') || key.includes('Component'));
}
function resolveConstructorsFromESModule(esm) {
if (isESModule(esm) || isAssumedESModuleContainingAngularComponentsOrModules(esm)) {
const constructors = Object.values(esm).filter((v) => typeof v === 'function');
if (!constructors) {
throw new Error('Class not found');
}
return constructors;
}
return [esm];
}
function importStandalone(promise) {
return async (item) => {
const resolvedImport = (await resolvePromiseWithRetries(promise));
const constructor = resolveConstructorsFromESModule(resolvedImport).shift();
if (!constructor) {
throw new Error('class not found');
}
assertStandalone(constructor);
const viewContainerRef = item.injector.get(ViewContainerRef);
const componentRef = viewContainerRef.createComponent(constructor, {
injector: item.injector,
});
await mountComponent(componentRef, item);
item.lifecycle?.importComponent?.emit(componentRef);
return componentRef;
};
}
function importNgModule(promise) {
return async (item) => {
const resolvedImport = (await resolvePromiseWithRetries(promise));
const ngModuleConstructors = resolveConstructorsFromESModule(resolvedImport)?.filter((type) => isNgModuleDef(type));
if (ngModuleConstructors?.length > 1) {
item.logger.warn(`ES module should return a single NgModule definition, found ${ngModuleConstructors?.length}. Please append \`.then(m => m.{DesiredModule})\` to your dynamic import.`);
}
const ngModuleConstructor = ngModuleConstructors?.shift();
if (!ngModuleConstructor) {
throw new Error('no ngModuleRef constructor found');
}
const ngModuleRef = createNgModule(ngModuleConstructor, item.injector);
const componentConstructors = ngModuleRef._bootstrapComponents;
if (!componentConstructors?.length) {
item.logger.debug('no bootstrap components found in ngModuleRef');
item.lifecycle?.importFinished?.emit(undefined);
return;
}
const viewContainerRef = ngModuleRef.injector.get(ViewContainerRef);
const mountComponentPromises = componentConstructors.map((componentConstructor) => {
const componentRef = viewContainerRef.createComponent(componentConstructor, { injector: ngModuleRef.injector, ngModuleRef });
return mountComponent(componentRef, item).then(() => componentRef);
});
const componentRefs = await Promise.all(mountComponentPromises);
componentRefs.forEach((componentRef) => item.lifecycle?.importComponent?.emit(componentRef));
return componentRefs;
};
}
function importPromise(promise) {
return async () => resolvePromiseWithRetries(promise);
}
class ImportsOrchestratorComponent {
constructor() {
this.queue = inject(ImportsOrchestratorQueueDirective, {
self: true,
});
}
ngOnChanges(changes) {
const importInput = changes['import'];
if (importInput !== undefined &&
importInput.currentValue !== importInput.previousValue) {
this.createAndAddItemToQueue();
}
}
ngAfterViewInit() {
this.createAndAddItemToQueue();
}
createAndAddItemToQueue() {
if (!this.identifier || !this.container) {
return;
}
this.queue.viewContainerRef = this.container;
this.queue.import = this.identifier;
this.queue.createAndAddItemToQueue();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorComponent, isStandalone: true, selector: "import", inputs: { identifier: "identifier" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef }], usesOnChanges: true, hostDirectives: [{ directive: ImportsOrchestratorQueueDirective, inputs: ["providers", "providers", "timeout", "timeout"] }, { directive: ImportsOrchestratorIODirective, inputs: ["inputs", "inputs", "outputs", "outputs"] }, { directive: ImportsOrchestratorLifecycleDirective, outputs: ["importQueued", "importQueued", "importStarted", "importStarted", "importFinished", "importFinished", "importComponent", "importComponent", "importErrored", "importErrored"] }, { directive: ImportsOrchestratorCSSClassDirective, inputs: ["cssClass", "cssClass"] }], ngImport: i0, template: `<ng-container #container></ng-container>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorComponent, decorators: [{
type: Component,
args: [{
selector: 'import',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [
{
directive: ImportsOrchestratorQueueDirective,
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['providers', 'timeout'],
},
{
directive: ImportsOrchestratorIODirective,
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['inputs', 'outputs'],
},
{
directive: ImportsOrchestratorLifecycleDirective,
// eslint-disable-next-line @angular-eslint/no-outputs-metadata-property
outputs: [
'importQueued',
'importStarted',
'importFinished',
'importComponent',
'importErrored',
],
},
{
directive: ImportsOrchestratorCSSClassDirective,
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['cssClass'],
},
],
template: `<ng-container #container></ng-container>`,
}]
}], propDecorators: { identifier: [{
type: Input
}], container: [{
type: ViewChild,
args: ['container', { read: ViewContainerRef }]
}] } });
const Imports = (imports) => {
Object.entries(imports).forEach(([key, value]) => {
IMPORTS_STORE[key] = value;
});
return function (target) {
// noop
};
};
class ImportsOrchestratorDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorDirective, isStandalone: true, selector: "[import]", hostDirectives: [{ directive: ImportsOrchestratorQueueDirective, inputs: ["import", "import", "providers", "providers", "timeout", "timeout"] }, { directive: ImportsOrchestratorIODirective, inputs: ["inputs", "inputs", "outputs", "outputs"] }, { directive: ImportsOrchestratorLifecycleDirective, outputs: ["importQueued", "importQueued", "importStarted", "importStarted", "importFinished", "importFinished", "importComponent", "importComponent", "importErrored", "importErrored"] }, { directive: ImportsOrchestratorCSSClassDirective, inputs: ["cssClass", "cssClass"] }], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorDirective, decorators: [{
type: Directive,
args: [{
selector: '[import]',
standalone: true,
hostDirectives: [
{
directive: ImportsOrchestratorQueueDirective,
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['import', 'providers', 'timeout'],
},
{
directive: ImportsOrchestratorIODirective,
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['inputs', 'outputs'],
},
{
directive: ImportsOrchestratorLifecycleDirective,
// eslint-disable-next-line @angular-eslint/no-outputs-metadata-property
outputs: [
'importQueued',
'importStarted',
'importFinished',
'importComponent',
'importErrored',
],
},
{
directive: ImportsOrchestratorCSSClassDirective,
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['cssClass'],
},
],
}]
}] });
class Queue {
constructor() {
this.data = [];
}
insert(priority, payload) {
this.data = [...this.data, { priority, payload }].sort(comparePriority);
}
take(payload = null) {
if (!payload) {
return this.data.shift()?.payload || null;
}
return this.takeSpecific(payload);
}
peek() {
return this.data[0]?.payload;
}
get length() {
return this.data.length;
}
get empty() {
return this.length < 1;
}
takeSpecific(payload) {
const index = this.data.findIndex((s) => s.payload === payload);
if (index < 0) {
return null;
}
payload = this.data[index].payload;
const updated = this.data.slice();
updated.splice(index, 1);
this.data = updated;
return payload;
}
}
function comparePriority(a, b) {
return a.priority - b.priority;
}
function withQueue(queue) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_QUEUE,
useValue: queue,
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Queue, providers);
}
function withOrchestration(orchestration) {
const providers = [
{
provide: IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION,
useFactory: (logger) => validateOrchestration(orchestration, logger),
deps: [IMPORTS_ORCHESTRATOR_FEATURE_LOGGER],
},
];
return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Orchestration, providers);
}
function validateOrchestration(orchestration, logger) {
const conflicts = findConflictingPriorities(orchestration).map(([priority, imports]) => `conflicting priority=${priority} for ="${imports.join('", "')}"`);
if (conflicts.length > 0) {
logger.warn(conflicts.join('\n'));
}
return orchestration;
}
function findConflictingPriorities(orchestration) {
const byPriority = Object.entries(orchestration).reduce((acc, [key, value]) => {
acc[value] = [...(acc[value] || []), key];
return acc;
}, {});
return Object.entries(byPriority).filter(([order, imports]) => imports.length > 1);
}
const provideImportsOrchestration = (orchestration, ...features) => makeEnvironmentProviders([
// default values:
...[
withoutRouting(),
withConcurrencyStatic(2),
withLogger(console),
withTimeout(10000),
withQueue(new Queue()),
].map((feature) => feature.providers),
// configured values from features
...(features || []).map((feature) => feature.providers),
...withOrchestration(orchestration).providers,
]);
/**
* Generated bundle index. Do not edit.
*/
export { ES_MODULE, ImportService, Imports, ImportsOrchestratorCSSClassDirective, ImportsOrchestratorComponent, ImportsOrchestratorDirective, ImportsOrchestratorIODirective, ImportsOrchestratorLifecycleDirective, ImportsOrchestratorQueueDirective, NG_MOD_DEF, bindComponentInputs, bindComponentOutputs, importNgModule, importPromise, importStandalone, isAssumedESModuleContainingAngularComponentsOrModules, isESModule, isNgModuleDef, mountComponent, provideImportsOrchestration, resolveConstructorsFromESModule, withConcurrencyRelativeToDownlinkSpeed, withConcurrencyStatic, withConcurrencyUpdateFn, withInterceptor, withLogger, withSuspendWhileRouting, withTimeout, withoutRouting };
//# sourceMappingURL=lotto24-angular-imports-orchestrator.mjs.map