next.js/packages/next/src/lib/multi-file-writer.ts
multi-file-writer.ts100 lines2.7 KB
import path from '../shared/lib/isomorphic/path'
import type { CacheFs } from '../shared/lib/utils'

/**
 * A task to be written.
 */
type Task = [
  /**
   * The directory to create.
   */
  directory: string,

  /**
   * The promise to create the directory.
   */
  mkdir: Promise<unknown>,

  /**
   * The promises to write the files that are dependent on the directory being
   * created.
   */
  writeFile: Promise<unknown>[],
]
/**
 * MultiFileWriter is a utility for writing multiple files in parallel that
 * guarantees that all files will be written after their containing directory
 * is created, and that the directory will only be created once.
 */
export class MultiFileWriter {
  /**
   * The tasks to be written.
   */
  private readonly tasks: Task[] = []

  constructor(
    /**
     * The file system methods to use.
     */
    private readonly fs: Pick<CacheFs, 'mkdir' | 'writeFile'>
  ) {}

  /**
   * Finds or creates a task for a directory.
   *
   * @param directory - The directory to find or create a task for.
   * @returns The task for the directory.
   */
  private findOrCreateTask(directory: string): Task {
    // See if this directory already has a task to create it.
    for (const task of this.tasks) {
      if (task[0] === directory) {
        return task
      }
    }

    const promise = this.fs.mkdir(directory)

    // Attach a catch handler so that it doesn't throw an unhandled promise
    // rejection warning.
    promise.catch(() => {})

    // Otherwise, create a new task for this directory.
    const task: Task = [directory, promise, []]
    this.tasks.push(task)

    return task
  }

  /**
   * Appends a file to the writer to be written after its containing directory
   * is created. The file writer should be awaited after all the files have been
   * appended. Any async operation that occurs between appending and awaiting
   * may cause an unhandled promise rejection warning and potentially crash the
   * process.
   *
   * @param filePath - The path to the file to write.
   * @param data - The data to write to the file.
   */
  public append(filePath: string, data: Buffer | string): void {
    // Find or create a task for the directory that contains the file.
    const task = this.findOrCreateTask(path.dirname(filePath))

    const promise = task[1].then(() => this.fs.writeFile(filePath, data))

    // Attach a catch handler so that it doesn't throw an unhandled promise
    // rejection warning.
    promise.catch(() => {})

    // Add the file write to the task AFTER the directory promise has resolved.
    task[2].push(promise)
  }

  /**
   * Returns a promise that resolves when all the files have been written.
   */
  public wait(): Promise<unknown> {
    return Promise.all(this.tasks.flatMap((task) => task[2]))
  }
}
Quest for Codev2.0.0
/
SIGN IN