iterative/vscode-dvc

View on GitHub
extension/src/data/index.ts

Summary

Maintainability
A
2 hrs
Test Coverage
B
86%
import { relative } from 'path'
import { EventEmitter, Event } from 'vscode'
import { getRelativePattern } from '../fileSystem/relativePattern'
import { createFileSystemWatcher } from '../fileSystem/watcher'
import { ProcessManager } from '../process/manager'
import { InternalCommands } from '../commands/internal'
import { ExpShowOutput, PlotsOutputOrError } from '../cli/dvc/contract'
import { uniqueValues } from '../util/array'
import { DeferredDisposable } from '../class/deferred'
import { isPathInSubProject, isSameOrChild } from '../fileSystem'

type LocalExperimentsOutput = {
  availableNbCommits: { [branch: string]: number }
  expShow: ExpShowOutput
  gitLog: string
  rowOrder: { branch: string; sha: string }[]
}

type RemoteExperimentsOutput = { lsRemoteOutput: string }

type StudioExperimentsOutput = {
  viewUrl: string | undefined
  live: { baselineSha: string; name: string }[]
  pushed: string[]
}

export type ExperimentsOutput =
  | LocalExperimentsOutput
  | RemoteExperimentsOutput
  | StudioExperimentsOutput

export const isRemoteExperimentsOutput = (
  data: ExperimentsOutput
): data is RemoteExperimentsOutput =>
  (data as RemoteExperimentsOutput).lsRemoteOutput !== undefined

export const isStudioExperimentsOutput = (
  data: ExperimentsOutput
): data is StudioExperimentsOutput =>
  (data as StudioExperimentsOutput).live !== undefined

export abstract class BaseData<
  T extends
    | { data: PlotsOutputOrError; revs: string[] }
    | ExperimentsOutput
    | { dag: string; stages: { [pipeline: string]: string | undefined } }
> extends DeferredDisposable {
  public readonly onDidUpdate: Event<T>

  protected readonly dvcRoot: string
  protected readonly processManager: ProcessManager
  protected readonly internalCommands: InternalCommands
  protected collectedFiles: string[] = []

  private readonly relSubProjects: string[]
  private readonly staticFiles: string[]

  private readonly updated: EventEmitter<T> = this.dispose.track(
    new EventEmitter()
  )

  constructor(
    dvcRoot: string,
    internalCommands: InternalCommands,
    updateProcesses: { name: string; process: () => Promise<unknown> }[],
    subProjects: string[],
    staticFiles: string[] = []
  ) {
    super()

    this.dvcRoot = dvcRoot
    this.processManager = this.dispose.track(
      new ProcessManager(...updateProcesses)
    )
    this.internalCommands = internalCommands
    this.onDidUpdate = this.updated.event
    this.relSubProjects = subProjects.map(subProject =>
      relative(this.dvcRoot, subProject)
    )
    this.staticFiles = staticFiles

    this.watchFiles()
  }

  protected notifyChanged(data: T) {
    this.updated.fire(data)
  }

  protected waitForInitialData() {
    const waitForInitialData = this.dispose.track(
      this.onDidUpdate(() => {
        this.dispose.untrack(waitForInitialData)
        waitForInitialData.dispose()
        this.deferred.resolve()
      })
    )
  }

  private listener(path: string) {
    const relPath = relative(this.dvcRoot, path)
    if (
      this.getWatchedFiles().some(
        watchedRelPath =>
          path.endsWith(watchedRelPath) ||
          isSameOrChild(relPath, watchedRelPath)
      ) &&
      !isPathInSubProject(relPath, this.relSubProjects)
    ) {
      void this.managedUpdate(path)
    }
  }

  private getWatchedFiles() {
    return uniqueValues([...this.staticFiles, ...this.collectedFiles])
  }

  private watchFiles() {
    return createFileSystemWatcher(
      disposable => this.dispose.track(disposable),
      getRelativePattern(this.dvcRoot, '**'),
      path => this.listener(path)
    )
  }

  abstract managedUpdate(path?: string): Promise<void>
}