vite-plugin-react18-pages
Version:
<p> <a href="https://www.npmjs.com/package/vite-plugin-react-pages" target="_blank" rel="noopener"><img src="https://img.shields.io/npm/v/vite-plugin-react-pages.svg" alt="npm package" /></a> </p>
358 lines (331 loc) • 11.5 kB
text/typescript
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?: (moduleId: string) => boolean): string[] {
const ids = Array.from(this.modules.keys())
if (filter) return ids.filter(filter)
return ids
}
public getModuleData(moduleId: string): any[] {
const module = this.modules.get(moduleId)
if (!module) return []
return module.getData()
}
public getModules(filter?: (moduleId: string) => boolean) {
let entries = Array.from(this.modules.entries())
// filter is a performance optimization:
// don't call module.getData() for filtered-out modules
if (filter) entries = entries.filter(([moduleId]) => filter(moduleId))
const modules: { [id: string]: any[] } = {}
entries.forEach(([moduleId, module]) => {
modules[moduleId] = module.getData()
})
return modules
}
/**
* This is the only way to update virtule modules
*/
public scheduleUpdate(updaterId: string, updater: Update['updater']) {
this.updateQueue.push(updaterId, updater)
this.updateQueueEmptyState.isPending = true
// don't schedule setTimeout if there is already one
if (this.updateQueue.size === 1) {
setTimeout(() => {
this.executeUpdates()
}, 0)
}
}
public addModuleListener(
handler: ModuleListener,
filter?: (moduleId: string) => boolean
) {
return this._addModuleListener((moduleId, data, prevData) => {
if (filter && !filter(moduleId)) return
handler(moduleId, data, prevData)
})
}
/**
* listen to virtule module updates.
* users can scheduleUpdate in these listeners, creating dependency chain of
* virtule modules.
* (.i.e when a virtule module changes, it will update another virtule module)
*
* users will reveive new module data and previous module data,
* so users can diff them to decide whether the module has "really" changed.
* if users think they are the same, the can skip updating other virtule modules.
* VirtualModuleGraph works on a very low level. It don't know what module data means. So it send updates event to users very often and let users to interpret module data.
*
* @return unsubscribe function
*/
private _addModuleListener(cb: ModuleListener) {
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: Map<Module, { prevData: any[] }>
) {
updatedModules.forEach(({ prevData }, module) => {
const data = module.getData()
this.moduleUpdateListeners.forEach((moduleUpdateListener) => {
moduleUpdateListener(module.id, data, prevData)
})
})
}
// executeUpdates_Inner is not reentrant
// use a state(lock) to prevent concurrent execution
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.`
)
// record the updatedModules so that we can notify listeners in the end
// also store prevData so users can diff it with new data
const updatedModules = new Map<Module, { prevData: any[] }>()
/** it must be called before updating data so that it can record prevData */
const recordAffectedModule = (module: Module) => {
if (updatedModules.has(module)) return
updatedModules.set(module, { prevData: module.getData() })
}
while (true) {
const update = this.updateQueue.pop()
if (!update) break
// cleanup the effects of previous update with same updaterId
cleanupEdgesWithUpdaterId(update.updaterId, recordAffectedModule)
const { disableAPIs, ...apis } = this.createUpdateAPIs(
update.updaterId,
recordAffectedModule
)
await update.updater(apis)
disableAPIs()
}
this.callModuleUpdateListeners(updatedModules)
// if the listeners schedule more updates,
// execute them synchronously and recursively
await this.executeUpdates_Inner(depth + 1)
}
private createUpdateAPIs(
updaterId: string,
recordAffectedModule: (module: Module) => void
): VirtuleModuleAPIs & { disableAPIs(): void } {
let outdated = false
const _this = this
const OUTDATED_ERROR_MSG = `You should not call update APIs after the updater async function.`
return {
addModuleData(moduleId: string, data: any, upstreamModuleId: string) {
if (outdated) throw new Error(OUTDATED_ERROR_MSG)
if (moduleId === upstreamModuleId)
throw new Error(
`addModuleData param error: source and target modules are the same`
)
// upstreamModuleId may be real file in fs
const fromModule = _this.ensureModule(upstreamModuleId)
const toModule = _this.ensureModule(moduleId)
recordAffectedModule(toModule)
Edge.addEdge(fromModule, toModule, data, updaterId)
},
getModuleData(moduleId: string) {
if (outdated) throw new Error(OUTDATED_ERROR_MSG)
return _this.getModuleData(moduleId)
},
deleteModule(moduleId: string) {
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: string): Module {
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: string) {}
public getData() {
return Array.from(this.data).map(({ data }) => data)
}
/** unlink this module */
public delete(recordAffectedModule: (module: Module) => void) {
if (this.data.size > 0) {
// there are upstream modules that are "piping" data to this module
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: Module, to: Module, data: any, updaterId: string) {
const edge = new Edge(from, to, data, updaterId)
// set private fields of modules
;(from as unknown as ModuleInternal).downstream.add(edge)
;(to as unknown as ModuleInternal).data.add(edge)
bindEdgeWithUpdaterId(edge)
}
private constructor(
public from: Module,
public to: Module,
public data: any,
public updaterId: string
) {}
private hasUnlinked = false
public unlink() {
if (this.hasUnlinked) {
return
}
// set private fields of modules
;(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: Edge) {
const { updaterId } = edge
let edges = mapUpdaterIdToEdges.get(updaterId)
if (!edges) {
edges = new Set()
mapUpdaterIdToEdges.set(updaterId, edges)
}
edges.add(edge)
}
function unbindEdgeWithUpdaterId(edge: 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: string,
recordAffectedModule: (module: Module) => void
) {
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: string, data: any, upstreamModuleId: string): void
getModuleData(moduleId: string): any[]
deleteModule(moduleId: string): void
}
class Update {
constructor(
public updaterId: string,
public updater: (apis: VirtuleModuleAPIs) => void | Promise<void>
) {}
}
class UpdateQueue {
private queue: Update[] = []
private map = new Map<string, Update>()
get size() {
return this.queue.length
}
push(updaterId: string, updater: Update['updater']) {
// ignore it if the updaterId already exists in the queue
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
}
}
// it indicates the depth of virtule modules
const MAX_CASCADE_UPDATE_DEPTH = 10