import Constants from './Constants' ; import { EditorClient , triggerPageModelLoaded } from './EditorClient' ; import MetaProperty from './MetaProperty' ; import { Model } from './Model' ; import { ModelClient } from './ModelClient' ; import { ModelStore } from './ModelStore' ; import { PathUtils } from './PathUtils' ; import { AuthoringUtils } from './AuthoringUtils' ; import { isRouteExcluded } from './ModelRouter' ; param model param childPath private returns function hasChildOfPath ( model , childPath ) { const sanited = PathUtils . sanitize ( childPath ) ; if ( ! sanited ) { return false ; } return ! ! ( model && childPath && model [ Constants . CHILDREN_PROP ] && model [ Constants . CHILDREN_PROP ] [ sanited ] ) ; } param pagePath param modelRootPath returns private function isPageURLRoot ( pagePath , modelRootPath ) { return ! pagePath || ! modelRootPath || ( PathUtils . sanitize ( pagePath ) === PathUtils . sanitize ( modelRootPath ) ) ; } export interface ModelManagerConfiguration { forceReload ? : boolean ; model ? : Model ; modelClient ? : ModelClient ; path ? : string ; errorPageRoot ? : string ; } interface ModelPaths { rootModelURL ? : string ; rootModelPath ? : string ; currentPathname ? : string | null ; metaPropertyModelURL ? : string ; } interface Page { pagePath : string ; pageData ? : Model ; } private export type ListenerFunction = ( ) => void ; /**
 * ModelManager is main entry point of this module.
 *
 * Example:
 *
 * Boostrap: `index.html`
 * ```
 * <head>
 *     <meta property="cq:pagemodel_root_url" content="{PATH}.model.json"/>
 * </head>
 * ```
 *
 * Bootstrap: `index.js`
 * ```
 * import { ModelManager } from '@adobe/aem-spa-page-model-manager';
 *
 * ModelManager.initialize().then((model) => {
 *     // Render the App content using the provided model
 *     render(model);
 * });
 *
 * // Loading a specific portion of model
 * ModelManager.getData("/content/site/page/jcr:content/path/to/component").then(...);
 * ```
 *
 *
 * For asynchronous loading of root model/standalone item model
 * ```
 * import { ModelManager } from '@adobe/aem-spa-page-model-manager';
 *
 * ModelManager.initializeAsync();
 * ...
 * // Render the App independent of the model
 * render();
 *
 * ```
 * For root model, custom event is fired on window with fetched model - cq-pagemodel-loaded
 */ export class ModelManager { private _modelClient : ModelClient | undefined ; private _modelStore : ModelStore | undefined ; private _listenersMap : { [ key ] : ListenerFunction [ ] ; } = { } ; private _fetchPromises : { [ key ] : Promise < Model > ; } = { } ; private _initPromise : any ; private _editorClient : EditorClient | undefined ; private _clientlibUtil : AuthoringUtils | undefined ; private _modelPaths : ModelPaths = { } ; private _errorPageRoot : string | undefined ; public get modelClient ( ) : ModelClient { if ( ! this . _modelClient ) { throw new Error ( 'ModelClient is undefined. Call initialize first!' ) ; } return this . _modelClient ; } public get modelStore ( ) : ModelStore { if ( ! this . _modelStore ) { throw new Error ( 'ModelStore is undefined. Call initialize first!' ) ; } return this . _modelStore ; } public get clientlibUtil ( ) : AuthoringUtils { if ( ! this . _clientlibUtil ) { throw new Error ( 'AuthoringUtils is undefined. Call initialize first!' ) ; } return this . _clientlibUtil ; } fires return { Promise } public initialize < M extends Model > ( config ? ) { this . initializeAsync ( config ) ; const { rootModelURL , rootModelPath } = this . _modelPaths ; if ( ! rootModelURL ) { throw new Error ( 'Provide root model url to initialize ModelManager.' ) ; } if ( ! rootModelPath ) { throw new Error ( 'No root modelpath resolved! This should never happen.' ) ; } return this . _initPromise ; } fires public initializeAsync ( config ? ) { this . destroy ( ) ; const modelConfig = this . _toModelConfig ( config ) ; const initialModel = modelConfig && modelConfig . model ; this . _initializeFields ( modelConfig ) ; this . _initPromise = this . _attachAEMLibraries ( ) ; const { rootModelPath } = this . _modelPaths ; this . _modelStore = new ModelStore ( rootModelPath , initialModel ) ; if ( rootModelPath ) { this . _setInitializationPromise ( rootModelPath ) ; } } private private _attachAEMLibraries ( ) { if ( ! PathUtils . isBrowser ( ) ) { return Promise . resolve ( ) ; } const docFragment = this . clientlibUtil . getAemLibraries ( ) ; if ( ! docFragment . hasChildNodes ( ) ) { return Promise . resolve ( ) ; } let outResolve ; const promise = new Promise ( resolve => { outResolve = resolve ; } ) ; this . clientlibUtil . setOnLoadCallback ( docFragment , outResolve ) ; window . document . head . appendChild ( docFragment ) ; return promise ; } /**
     * Initializes the class fields for ModelManager
     */ private _initializeFields ( config ? ) { this . _listenersMap = { } ; this . _fetchPromises = { } ; this . _initPromise = null ; this . _modelClient = ( ( config && config . modelClient ) || new ModelClient ( ) ) ; this . _errorPageRoot = config && config . errorPageRoot || undefined ; this . _editorClient = new EditorClient ( this ) ; this . _clientlibUtil = new AuthoringUtils ( this . modelClient . apiHost ) ; this . _modelPaths = this . _getPathsForModel ( config ) ; } /**
     * Returns paths required for fetching root model
     */ private _getPathsForModel ( config ? ) { const path = config ?. path ; const pageModelRoot = PathUtils . getMetaPropertyValue ( MetaProperty . PAGE_MODEL_ROOT_URL ) ; const metaPropertyModelURL = PathUtils . internalize ( pageModelRoot ) ; const currentPathname = this . _isRemoteApp ( ) ? '' : PathUtils . getCurrentPathname ( ) ; const sanitizedCurrentPathname = ( ( currentPathname && PathUtils . sanitize ( currentPathname ) ) || '' ) as string ; const rootModelURL = path || metaPropertyModelURL || sanitizedCurrentPathname ; const rootModelPath = PathUtils . sanitize ( rootModelURL ) || '' ; return { currentPathname , metaPropertyModelURL , rootModelURL , rootModelPath } ; } returns private _fetchPageModelFromStore ( ) { const data = this . modelStore . getData ( ) ; triggerPageModelLoaded ( data ) ; return data ; } /**
     * Sets initialization promise to fetch model if root path is available
     * Also, to be returned on synchronous initialization
     */ private _setInitializationPromise ( rootModelPath ) { const { rootModelURL } = this . _modelPaths ; this . _initPromise = ( this . _initPromise . then ( ( ) => this . _checkDependencies ( ) ) . then ( ( ) => { const data = this . modelStore . getData ( rootModelPath ) ; if ( data && ( Object . keys ( data ) . length > 0 ) ) { triggerPageModelLoaded ( data ) ; return data ; } else if ( rootModelURL ) { return this . _fetchData ( rootModelURL ) . then ( ( rootModel ) => { try { this . modelStore . initialize ( rootModelPath , rootModel ) ; return ( this . _fetchActivePageModel ( rootModel ) || this . _fetchPageModelFromStore ( ) ) ; } catch ( e ) { console . error ( `Error on initialization - ${ e }` ) ; } } ) ; } } ) ) ; } /**
     * Fetch model for the currently active page
     */ private _fetchActivePageModel ( rootModel ) { const { currentPathname , metaPropertyModelURL } = this . _modelPaths ; const sanitizedCurrentPathname = ( ( currentPathname && PathUtils . sanitize ( currentPathname ) ) || '' ) as string ; if ( ! ! currentPathname && ! ! sanitizedCurrentPathname && ! isRouteExcluded ( currentPathname ) && ! isPageURLRoot ( currentPathname , metaPropertyModelURL ) && ! hasChildOfPath ( rootModel , currentPathname ) ) { return this . _fetchData ( currentPathname ) . then ( ( model ) => { this . modelStore . insertData ( sanitizedCurrentPathname , model ) ; return this . _fetchPageModelFromStore ( ) ; } ) . catch ( e => { console . warn ( 'caught' , e ) ; } ) ; } else if ( ! ! currentPathname && isRouteExcluded ( currentPathname ) ) { return this . _fetchPageModelFromStore ( ) ; } else if ( ! PathUtils . isBrowser ( ) ) { throw new Error ( `Attempting to retrieve model data from a non-browser.
                Please provide the initial data with the property key model` ) ; } } returns public get rootPath ( ) : string { return this . modelStore . rootPath ; } param config returns public getData < M extends Model > ( config ? ) { let path = '' ; let forceReload = false ; if ( typeof config === 'string' ) { path = config ; } else if ( config ) { path = config . path || '' ; forceReload = ! ! config . forceReload ; } const initPromise = this . _initPromise || Promise . resolve ( ) ; return initPromise . then ( ( ) => this . _checkDependencies ( ) ) . then ( ( ) => { if ( ! forceReload ) { const item = this . modelStore . getData ( path ) ; if ( item ) { return Promise . resolve ( item ) ; } } if ( PathUtils . isItem ( path ) ) { const { pageData , pagePath } = this . _getParentPage ( path ) ; if ( ! pageData ) { return this . _fetchData ( pagePath ) . then ( ( data ) => { this . _storeData ( pagePath , data ) ; return this . modelStore . getData ( path ) ; } ) ; } } return this . _fetchData ( path ) . then ( ( data ) => this . _storeData ( path , data ) ) ; } ) ; } param path private returns public _fetchData ( path ) { if ( Object . prototype . hasOwnProperty . call ( this . _fetchPromises , path ) ) { return this . _fetchPromises [ path ] ; } if ( this . modelClient ) { return new Promise < Model > ( ( resolve , reject ) => { const promise = this . modelClient . fetch ( this . _toModelPath ( path ) ) ; this . _fetchPromises [ path ] = promise ; promise . then ( ( obj ) => { delete this . _fetchPromises [ path ] ; if ( this . _isRemoteApp ( ) ) { triggerPageModelLoaded ( obj ) ; } resolve ( obj ) ; } ) . catch ( ( error ) => { delete this . _fetchPromises [ path ] ; if ( this . _errorPageRoot !== undefined ) { const code = typeof error !== 'string' && error . response ? error . response . status : '500' ; const errorPagePath = this . _errorPageRoot + code + '.model.json' ; if ( path . indexOf ( Constants . JCR_CONTENT ) === - 1 && path !== errorPagePath ) { this . _fetchData ( errorPagePath ) . then ( ( response => { response [ Constants . PATH_PROP ] = PathUtils . sanitize ( path ) || path ; resolve ( response ) ; } ) ) . catch ( reject ) ; } else { reject ( error ) ; } } else { reject ( error ) ; } } ) ; } ) ; } else { throw new Error ( 'ModelClient not initialized!' ) ; } } param path private public _notifyListeners ( path ) { path = PathUtils . adaptPagePath . call ( this , path ) ; if ( ! this . _listenersMap ) { throw new Error ( 'ListenersMap is undefined.' ) ; } const listenersForPath = this . _listenersMap [ path ] ; if ( ! listenersForPath ) { return ; } if ( listenersForPath . length ) { listenersForPath . forEach ( ( listener ) => { try { listener ( ) ; } catch ( e ) { console . error ( `Error in listener ${ listenersForPath } at path ${ path }: ${ e }` ) ; } } ) ; } } param path param callback public addListener ( path , callback ) { if ( ! path || ( typeof path !== 'string' ) || ( typeof callback !== 'function' ) ) { return ; } const adaptedPath = PathUtils . adaptPagePath ( path , this . modelStore ?. rootPath ) ; this . _listenersMap [ adaptedPath ] = this . _listenersMap [ path ] || [ ] ; this . _listenersMap [ adaptedPath ] . push ( callback ) ; } param path param callback public removeListener ( path , callback ) { if ( ! path || ( typeof path !== 'string' ) || ( typeof callback !== 'function' ) ) { return ; } const adaptedPath = PathUtils . adaptPagePath ( path , this . modelStore ?. rootPath ) ; const listenersForPath = this . _listenersMap [ adaptedPath ] ; if ( listenersForPath ) { const index = listenersForPath . indexOf ( callback ) ; if ( index !== - 1 ) { listenersForPath . splice ( index , 1 ) ; } } } private private destroy ( ) { if ( this . _modelClient && this . _modelClient . destroy ) { this . _modelClient . destroy ( ) ; } if ( this . _modelStore && this . _modelStore . destroy ) { this . _modelStore . destroy ( ) ; } if ( this . _editorClient && this . _editorClient . destroy ) { this . _editorClient . destroy ( ) ; } } private _storeData ( path , data ) { let isItem = false ; if ( this . _modelStore ) { isItem = PathUtils . isItem ( path ) ; } if ( data && ( Object . keys ( data ) . length > 0 ) ) { this . modelStore . insertData ( path , data ) ; this . _notifyListeners ( path ) ; } if ( ! isItem ) { this . _notifyListeners ( '' ) ; } return data ; } private return { * } private _toModelPath ( path ) { let url = PathUtils . addSelector ( path , 'model' ) ; url = PathUtils . addExtension ( url , 'json' ) ; url = PathUtils . externalize ( url ) ; return PathUtils . makeAbsolute ( url ) ; } return { object } private private _toModelConfig ( config ? ) { if ( ! config || typeof config !== 'string' ) { return ( ( config || { } ) as ModelManagerConfiguration ) ; } return { path : config } ; } return { Promise } private private _checkDependencies ( ) { if ( ! this . modelClient ) { return Promise . reject ( 'No ModelClient registered.' ) ; } if ( ! this . modelStore ) { return Promise . reject ( 'No ModelManager registered.' ) ; } return Promise . resolve ( ) ; } return { object } private _getParentPage ( path ) { const dataPaths = PathUtils . splitPageContentPaths ( path ) ; const pagePath = dataPaths ?. pagePath || '' ; const pageData = this . modelStore . getData ( pagePath ) ; return { pageData , pagePath } ; } returns public _isRemoteApp ( ) { const aemApiHost = this . modelClient . apiHost || '' ; return ( PathUtils . isBrowser ( ) && aemApiHost . length > 0 && ( PathUtils . getCurrentURL ( ) !== aemApiHost ) ) ; } } export default new ModelManager ( ) ;