import equal from 'fast-deep-equal' ; import { PageUpdateBuffer } from './UpdateBuffer' ; import { VirtuleModuleAPIs , FileHandlerAPIs , VirtualModulesManager , } from '../utils/virtual-module' ; import type { FileHandler , PageAPIs , DataPiece } from './types.doc' ; const PAGE_MODULE_PREFIX = '/@vp-page' ; const ensurePageId = ( moduleId ) => moduleId . startsWith ( PAGE_MODULE_PREFIX ) ? moduleId . slice ( PAGE_MODULE_PREFIX . length ) : moduleId ; const ensureModuleId = ( pageId ) => pageId . startsWith ( PAGE_MODULE_PREFIX ) ? pageId : PAGE_MODULE_PREFIX + pageId ; const isPageRelatedModule = ( moduleId ) => moduleId . startsWith ( PAGE_MODULE_PREFIX ) ; /**
 * building upon VirtualModulesManager,
 * PagesDataKeeper recognize and handle page modules.
 */ export class PagesDataKeeper extends PageUpdateBuffer { /**
     * this.pages is a cache of this.virtualModulesManager.getModules
     * which is updated in batch (may be outdated for a short time)
     */ private readonly pages : PagesDataInternal = { } ; constructor ( private readonly virtualModulesManager ) { super ( ) ; virtualModulesManager . getModules ( ( modules ) => { Object . entries ( modules ) . forEach ( ( [ moduleId , data ] ) => { this . setPageData ( moduleId , data ) ; } ) ; virtualModulesManager . addModuleListener ( ( moduleId , data ) => { this . setPageData ( moduleId , data ) ; } , isPageRelatedModule ) ; } , isPageRelatedModule ) ; } /** turn PagesDataInternal to PagesData */ public getPages ( ) { return Object . fromEntries ( Object . entries ( this . pages ) . map ( ( [ pageId , page ] ) => [ pageId , transformOnePageDataInternal ( page ) , ] ) ) ; } public getPage ( pageId ) { const page = this . pages [ pageId ] ; if ( ! page ) return null ; return transformOnePageDataInternal ( page ) ; } /**
     * when low-level page virtule modules has changed, update this.pages data
     * and notify listeners
     */ private setPageData ( moduleId , rawData ) { const pageId = ensurePageId ( moduleId ) ; const oldPageData = this . pages [ pageId ] ; const pageData = this . createPageDataFromRaw ( rawData ) ; if ( ! pageData ) { if ( oldPageData ) { delete this . pages [ pageId ] ; this . scheduleUpdate ( { type : 'delete' , pageId , } ) ; } return ; } if ( ! oldPageData ) { this . pages [ pageId ] = pageData ; this . scheduleUpdate ( { type : 'add' , pageId , } ) ; return ; } this . pages [ pageId ] = pageData ; if ( ! equal ( pageData . runtimeData , oldPageData . runtimeData ) ) { this . scheduleUpdate ( { type : 'update' , dataType : 'runtime' , pageId , } ) ; } if ( ! equal ( pageData . staticData , oldPageData . staticData ) ) { this . scheduleUpdate ( { type : 'update' , dataType : 'static' , pageId , } ) ; } } private createPageDataFromRaw ( rawData ) { const pageData = { runtimeData : { } , staticData : { } , } ; const { runtimeData : dataMap , staticData : staticDataMap } = pageData ; rawData . forEach ( ( data ) => { if ( ! data ) return ; const { dataPath , staticData } = data ; if ( ! dataPath && ! staticData ) return ; const key = data . key ?? 'main' ; const priority = data . priority ?? 1 ; if ( dataPath ) { if ( ! dataMap [ key ] || priority > dataMap [ key ] . priority ) dataMap [ key ] = { dataPath , priority } ; } if ( staticData ) { if ( ! staticDataMap [ key ] || priority > staticDataMap [ key ] . priority ) staticDataMap [ key ] = { staticData , priority } ; } } ) ; if ( isEmptyPage ( pageData ) ) return null ; return pageData ; function isEmptyPage ( pageData ) { const { runtimeData , staticData } = pageData ; return ( Object . keys ( runtimeData ) . length === 0 && Object . keys ( staticData ) . length === 0 ) ; } } /**
     * update page virtule modules according to fs files
     */ public addFSWatcher ( baseDir , globs , fileHandler ) { this . virtualModulesManager . addFSWatcher ( baseDir , globs , async ( file , lowerAPI ) => { const pageAPIs = this . createPageAPIs ( lowerAPI ) ; const res = await fileHandler ( file , pageAPIs ) ; if ( res ) { pageAPIs . addPageData ( res ) ; } } ) ; } public createOneTimePageAPIs ( updaterAPIs ) { const handlerAPI = { addModuleData ( moduleId , data ) { updaterAPIs . addModuleData ( moduleId , data , 'VP_ANONYMOUS_MODULE' ) ; } , getModuleData : updaterAPIs . getModuleData , } ; return this . createPageAPIs ( handlerAPI ) ; } /**
     * TODO:
     * getRuntimeData and getStaticData are very inefficient to implement,
     * redesign them in the next verion
     */ private createPageAPIs ( lowerAPI ) { const getRuntimeData = ( pageId ) => { const moduleId = ensureModuleId ( pageId ) ; const getDataObject = ( ) => { const rawData = this . virtualModulesManager . _getModuleDataNow ( moduleId ) ; const pageData = this . createPageDataFromRaw ( rawData ) ; if ( ! pageData ) return { } ; return pageData . runtimeData ; } ; const setData = ( key , value ) => { lowerAPI . addModuleData ( moduleId , { key , dataPath : value , } as DataPiece ) ; } ; const getData = ( key ) => { const existValue = getDataObject ( ) [ key ] ; return existValue ?. dataPath ; } ; return createProxy ( { getDataObject , setData , getData } ) ; } ; const getStaticData = ( pageId ) => { const moduleId = ensureModuleId ( pageId ) ; const getDataObject = ( ) => { const rawData = this . virtualModulesManager . _getModuleDataNow ( moduleId ) ; const pageData = this . createPageDataFromRaw ( rawData ) ; if ( ! pageData ) return { } ; return pageData . staticData ; } ; const setData = ( key , value ) => { lowerAPI . addModuleData ( moduleId , { key , staticData : value , } as DataPiece ) ; } ; const getData = ( key ) => { const existValue = getDataObject ( ) [ key ] ; return existValue ?. staticData ; } ; return createProxy ( { getDataObject , setData , getData } ) ; } ; const addPageData = ( dataPiece ) => { const moduleId = ensureModuleId ( dataPiece . pageId ) ; lowerAPI . addModuleData ( moduleId , dataPiece ) ; } ; return { getRuntimeData , getStaticData , addPageData , } ; function createProxy ( { getDataObject , setData , getData , } ) { return new Proxy ( { } as { [ key ] : string ; } , { ... defaultProxyTraps , set ( target , key , value ) { setData ( key , value ) ; return true ; } , get ( target , key ) { return getData ( key ) ; } , has ( target , key ) { return Reflect . has ( getDataObject ( ) , key ) ; } , ownKeys : function ( target ) { return Reflect . ownKeys ( getDataObject ( ) ) ; } , } ) ; } } } export interface PagesData { example [ pageId ] : OnePageData ; } export interface OnePageData { data : { [ key ] : string ; } ; staticData : { [ key ] : any ; } ; } interface PagesDataInternal { [ pageId ] : OnePageDataInternal ; } interface OnePageDataInternal { runtimeData : { /**
         * The value of runtimeData shoule be a path to module
         * (to be evaluated at runtime)
         */ [ key ] : { dataPath : string ; priority : number ; } ; } ; staticData : { /**
         * The value of staticData can be any json value
         */ [ key ] : { staticData : any ; priority : number ; } ; } ; } const defaultProxyTraps = Object . fromEntries ( Object . getOwnPropertyNames ( Reflect ) . map ( ( fnName ) => [ fnName , ( ) => { throw new Error ( `unsupported operation on page data obejct proxy` ) ; } , ] ) ) ; function transformOnePageDataInternal ( page ) { const runtimeData = Object . fromEntries ( Object . entries ( page . runtimeData ) . map ( ( [ key , { dataPath } ] ) => [ key , dataPath , ] ) ) ; const staticData = Object . fromEntries ( Object . entries ( page . staticData ) . map ( ( [ key , { staticData } ] ) => [ key , staticData , ] ) ) ; return { data : runtimeData , staticData } ; }