OverviewFilesCommitsIssuesDocsReleasesPeers

codetree / lib / reducers / tree-reducer.js

111 lines · javascript ref:
/**
 * TreeReducer — subscribes to scan-stream events and builds the same graph
 * structure that walkGraph() used to return synchronously.
 *
 * Stateful. Apply events in order. Call .finalize() after scan-end to get
 * the same shape as before: { modules, files, root, dirTree, projectType,
 * languages, resolutionContext }.
 *
 * Each reducer is a single subscriber — different reducers (doc-cruft,
 * metrics, UI updaters) can listen to the same event source in parallel.
 */
export class TreeReducer {
  constructor () {
    this.root = null
    this.projectType = null
    this.languages = []
    this.resolutionContext = null
    this.modules = new Map()      // modName -> mod
    this.files = new Map()        // rootRelPath -> fileInfo
    this.dirTree = null
  }

  /** Attach this reducer to a scan-stream emitter. */
  attach (emitter) {
    emitter.on('scan-start', e => this.onScanStart(e))
    emitter.on('module-start', e => this.onModuleStart(e))
    emitter.on('file-discovered', e => this.onFileDiscovered(e))
    emitter.on('file-parsed', e => this.onFileParsed(e))
    emitter.on('import-edge', e => this.onImportEdge(e))
    emitter.on('scan-end', e => this.onScanEnd(e))
    return this
  }

  onScanStart (e) {
    this.root = e.root
    this.projectType = e.projectType
    this.languages = e.languages
    this.resolutionContext = e.resolutionContext
  }

  onModuleStart (e) {
    this.modules.set(e.name, {
      name: e.name,
      version: e.version,
      path: e.path,
      absPath: e.absPath,
      entry: e.entry,
      dependencies: e.dependencies,
      devDependencies: e.devDependencies,
      importMaps: e.importMaps,
      testCommand: e.testCommand,
      type: e.type,
      projectType: e.projectType,
      files: new Map()
    })
  }

  onFileDiscovered (e) {
    // Intentionally NOT retaining e.source. That was the main memory leak that
    // OOM'd recursive scans. Downstream consumers can re-read from absPath
    // via /api/file if they actually need the bytes.
    const fileInfo = {
      absPath: e.absPath,
      relPath: e.relPath,
      relInPkg: e.relInPkg,
      size: e.size,
      localImports: [],
      parsed: null  // filled by onFileParsed
    }
    this.files.set(e.relPath, fileInfo)
    const mod = this.modules.get(e.modName)
    if (mod) mod.files.set(e.relInPkg, fileInfo)
  }

  onFileParsed (e) {
    const mod = this.modules.get(e.modName)
    if (!mod) return
    const file = mod.files.get(e.relInPkg)
    if (!file) return
    file.parsed = e.parsed
  }

  onImportEdge (e) {
    const mod = this.modules.get(e.modName)
    if (!mod) return
    const file = mod.files.get(e.fromRelInPkg)
    if (!file) return
    const edge = { specifier: e.specifier, resolved: e.resolvedRelPath }
    if (e.names && e.names.length > 0) edge.names = e.names.slice()
    if (e.implicit) edge.implicit = true
    file.localImports.push(edge)
  }

  onScanEnd (e) {
    this.dirTree = e.dirTree
  }

  /** Return the graph in the shape walkGraph() used to produce. */
  finalize () {
    return {
      modules: this.modules,
      files: this.files,
      root: this.root,
      dirTree: this.dirTree,
      projectType: this.projectType,
      languages: this.languages,
      resolutionContext: this.resolutionContext
    }
  }
}