import { PendingState } from './utils' ; export class VirtualModuleGraph { /**
     * the module inside this graph may be virtule module or real fs module
     */ private readonly modules : Map < string , Module > = new Map ( ) ; /**
     * Serialize the update works (instead of doing them concurrently)
     * to make the result more predictable.
     *
     * If there is already a queuing update with same updaterId,
     * it won't schedule a new one.
     *
     * Before executing an updater, it will automatically cleanup the effects of
     * previous update with same updaterId.
     * Example:
     * When find module1 for the first time:
     *   the updater set data for module2 and module3 (upstreamModule is module1)
     * Then, when observe that module1 is updated:
     *   the updater set data for module2 (upstreamModule is module1)
     * At this time, the data in module3 should be automatically cleanup!
     * So the updater(users) don't need to manually delete the old data in module3.
     */ private updateQueue = new UpdateQueue ( ) ; /** track updateQueue empty state (isPending means not empty) */ public updateQueueEmptyState = new PendingState ( ) ; public getModuleIds ( filter ? ) { const ids = Array . from ( this . modules . keys ( ) ) ; if ( filter ) return ids . filter ( filter ) ; return ids ; } public getModuleData ( moduleId ) { const module = this . modules . get ( moduleId ) ; if ( ! module ) return [ ] ; return module . getData ( ) ; } public getModules ( filter ? ) { let entries = Array . from ( this . modules . entries ( ) ) ; if ( filter ) entries = entries . filter ( ( [ moduleId ] ) => filter ( moduleId ) ) ; const modules = { } ; entries . forEach ( ( [ moduleId , module ] ) => { modules [ moduleId ] = module . getData ( ) ; } ) ; return modules ; } /**
     * This is the only way to update virtule modules
     */ public scheduleUpdate ( updaterId , updater ) { this . updateQueue . push ( updaterId , updater ) ; this . updateQueueEmptyState . isPending = true ; if ( this . updateQueue . size === 1 ) { setTimeout ( ( ) => { this . executeUpdates ( ) ; } , 0 ) ; } } public addModuleListener ( handler , filter ? ) { return this . _addModuleListener ( ( moduleId , data , prevData ) => { if ( filter && ! filter ( moduleId ) ) return ; handler ( moduleId , data , prevData ) ; } ) ; } return private _addModuleListener ( cb ) { this . moduleUpdateListeners . push ( cb ) ; return ( ) => { const index = this . moduleUpdateListeners . indexOf ( cb ) ; if ( index === - 1 ) return ; this . moduleUpdateListeners . splice ( index , 1 ) ; } ; } private moduleUpdateListeners : ModuleListener [ ] = [ ] ; private callModuleUpdateListeners ( updatedModules ) { updatedModules . forEach ( ( { prevData } , module ) => { const data = module . getData ( ) ; this . moduleUpdateListeners . forEach ( ( moduleUpdateListener ) => { moduleUpdateListener ( module . id , data , prevData ) ; } ) ; } ) ; } public updateExecutingState = new PendingState ( ) ; private async executeUpdates ( ) { if ( this . updateExecutingState . isPending ) return ; this . updateExecutingState . isPending = true ; try { await this . executeUpdates_Inner ( ) ; } finally { this . updateExecutingState . isPending = false ; if ( this . updateQueue . size === 0 ) this . updateQueueEmptyState . isPending = false ; } } private async executeUpdates_Inner ( depth = 1 ) { if ( this . updateQueue . size === 0 ) return ; if ( depth > MAX_CASCADE_UPDATE_DEPTH ) throw new Error ( `Cascaded updates exceed max depth ${ MAX_CASCADE_UPDATE_DEPTH }. Probably because the depth of the virtule module tree is too high, or there is a cycle in the virtule module graph.` ) ; const updatedModules = new Map < Module , { prevData : any [ ] ; } > ( ) ; /** it must be called before updating data so that it can record prevData */ const recordAffectedModule = ( module ) => { if ( updatedModules . has ( module ) ) return ; updatedModules . set ( module , { prevData : module . getData ( ) } ) ; } ; while ( true ) { const update = this . updateQueue . pop ( ) ; if ( ! update ) break ; cleanupEdgesWithUpdaterId ( update . updaterId , recordAffectedModule ) ; const { disableAPIs , ... apis } = this . createUpdateAPIs ( update . updaterId , recordAffectedModule ) ; await update . updater ( apis ) ; disableAPIs ( ) ; } this . callModuleUpdateListeners ( updatedModules ) ; await this . executeUpdates_Inner ( depth + 1 ) ; } private createUpdateAPIs ( updaterId , recordAffectedModule ) { let outdated = false ; const _this = this ; const OUTDATED_ERROR_MSG = `You should not call update APIs after the updater async function.` ; return { addModuleData ( moduleId , data , upstreamModuleId ) { if ( outdated ) throw new Error ( OUTDATED_ERROR_MSG ) ; if ( moduleId === upstreamModuleId ) throw new Error ( `addModuleData param error: source and target modules are the same` ) ; const fromModule = _this . ensureModule ( upstreamModuleId ) ; const toModule = _this . ensureModule ( moduleId ) ; recordAffectedModule ( toModule ) ; Edge . addEdge ( fromModule , toModule , data , updaterId ) ; } , getModuleData ( moduleId ) { if ( outdated ) throw new Error ( OUTDATED_ERROR_MSG ) ; return _this . getModuleData ( moduleId ) ; } , deleteModule ( moduleId ) { if ( outdated ) throw new Error ( OUTDATED_ERROR_MSG ) ; const module = _this . modules . get ( moduleId ) ; if ( ! module ) return ; module . delete ( recordAffectedModule ) ; _this . modules . delete ( moduleId ) ; } , disableAPIs ( ) { outdated = true ; } , } ; } private ensureModule ( moduleId ) { let result = this . modules . get ( moduleId ) ; if ( ! result ) { result = new Module ( moduleId ) ; this . modules . set ( moduleId , result ) ; } return result ; } } export type ModuleListener = ( moduleId : string , data : any [ ] , prevData : any [ ] ) => void ; /**
 * Modules are nodes in the graph
 */ class Module { constructor ( public id ) { } public getData ( ) { return Array . from ( this . data ) . map ( ( { data } ) => data ) ; } /** unlink this module */ public delete ( recordAffectedModule ) { if ( this . data . size > 0 ) { throw new Error ( `This module has upstream modules. You should delete modules in topological order. moduleID: ${ this . id }` ) ; } recordAffectedModule ( this ) ; this . downstream . forEach ( ( edge ) => { recordAffectedModule ( edge . to ) ; edge . unlink ( ) ; } ) ; } /**
     * incoming edges of the node
     * indicating the data of this virtule module
     *
     * real fs module won't need this
     */ private data : Set < Edge > = new Set ( ) ; /**
     * outcoming edges of the node
     * indicating which modules depend on this module
     *
     * it is needed because we need to update downstream modules
     * when a fs module is deleted
     */ private downstream : Set < Edge > = new Set ( ) ; } interface ModuleInternal { data : Set < Edge > ; downstream : Set < Edge > ; } class Edge { static addEdge ( from , to , data , updaterId ) { const edge = new Edge ( from , to , data , updaterId ) ; ( from as unknown as ModuleInternal ) . downstream . add ( edge ) ; ( to as unknown as ModuleInternal ) . data . add ( edge ) ; bindEdgeWithUpdaterId ( edge ) ; } private constructor ( public from , public to , public data , public updaterId ) { } private hasUnlinked = false ; public unlink ( ) { if ( this . hasUnlinked ) { return ; } ; ( this . from as unknown as ModuleInternal ) . downstream . delete ( this ) ; ( this . to as unknown as ModuleInternal ) . data . delete ( this ) ; unbindEdgeWithUpdaterId ( this ) ; this . hasUnlinked = true ; } } const mapUpdaterIdToEdges = new Map < string , Set < Edge > > ( ) ; function bindEdgeWithUpdaterId ( edge ) { const { updaterId } = edge ; let edges = mapUpdaterIdToEdges . get ( updaterId ) ; if ( ! edges ) { edges = new Set ( ) ; mapUpdaterIdToEdges . set ( updaterId , edges ) ; } edges . add ( edge ) ; } function unbindEdgeWithUpdaterId ( edge ) { const { updaterId } = edge ; const edges = mapUpdaterIdToEdges . get ( updaterId ) ; if ( ! edges || ! edges . has ( edge ) ) throw new Error ( `assertion fail: unlinkEdgeWithUpdaterId` ) ; edges . delete ( edge ) ; } function cleanupEdgesWithUpdaterId ( updaterId , recordAffectedModule ) { const edges = mapUpdaterIdToEdges . get ( updaterId ) ; if ( ! edges ) return ; edges . forEach ( ( edge ) => { recordAffectedModule ( edge . to ) ; edge . unlink ( ) ; } ) ; if ( edges . size > 0 ) throw new Error ( `assertion fail: all edges with updaterId should already be unlinked` ) ; edges . clear ( ) ; } export interface VirtuleModuleAPIs { addModuleData ( moduleId , data , upstreamModuleId ) : void ; getModuleData ( moduleId ) : any [ ] ; deleteModule ( moduleId ) : void ; } class Update { constructor ( public updaterId , public updater ) { } } class UpdateQueue { private queue : Update [ ] = [ ] ; private map = new Map < string , Update > ( ) ; get size ( ) { return this . queue . length ; } push ( updaterId , updater ) { if ( this . map . has ( updaterId ) ) return ; const update = new Update ( updaterId , updater ) ; this . queue . push ( update ) ; this . map . set ( updaterId , update ) ; } pop ( ) { const update = this . queue . shift ( ) ; if ( ! update ) return null ; const { updaterId } = update ; this . map . delete ( updaterId ) ; return update ; } } const MAX_CASCADE_UPDATE_DEPTH = 10 ;