 op();
      });

      if (err || result === null) {
        return callback(err, result);
      }

      try {
        this.processReadResult(this[kDirPath], result);
        const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
        if (this[kDirOptions].recursive && dirent.isDirectory()) {
          this.readSyncRecursive(dirent);
        }
        callback(null, dirent);
      } catch (error) {
        callback(error);
      }
    };

    this[kDirOperationQueue] = [];
    this[kDirHandle].read(
      this[kDirOptions].encoding,
      this[kDirOptions].bufferSize,
      req,
    );
  }

  processReadResult(path, result) {
    for (let i = 0; i < result.length; i += 2) {
      ArrayPrototypePush(
        this[kDirBufferedEntries],
        getDirent(
          path,
          result[i],
          result[i + 1],
          true, // Quirk to not introduce a breaking change.
        ),
      );
    }
  }

  readSyncRecursive(dirent) {
    const ctx = { path: dirent.path };
    const handle = dirBinding.opendir(
      pathModule.toNamespacedPath(dirent.path),
      this[kDirOptions].encoding,
      undefined,
      ctx,
    );
    handleErrorFromBinding(ctx);
    const result = handle.read(
      this[kDirOptions].encoding,
      this[kDirOptions].bufferSize,
      undefined,
      ctx,
    );

    if (result) {
      this.processReadResult(dirent.path, result);
    }

    handle.close(undefined, ctx);
    handleErrorFromBinding(ctx);
  }

  readSync() {
    if (this[kDirClosed] === true) {
      throw new ERR_DIR_CLOSED();
    }

    if (this[kDirOperationQueue] !== null) {
      throw new ERR_DIR_CONCURRENT_OPERATION();
    }

    if (this[kDirBufferedEntries].length > 0) {
      const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
      if (this[kDirOptions].recursive && dirent.isDirectory()) {
        this.readSyncRecursive(dirent);
      }
      return dirent;
    }

    const ctx = { path: this[kDirPath] };
    const result = this[kDirHandle].read(
      this[kDirOptions].encoding,
      this[kDirOptions].bufferSize,
      undefined,
      ctx,
    );
    handleErrorFromBinding(ctx);

    if (result === null) {
      return result;
    }

    this.processReadResult(this[kDirPath], result);

    const dirent = ArrayPrototypeShift(this[kDirBufferedEntries]);
    if (this[kDirOptions].recursive && dirent.isDirectory()) {
      this.readSyncRecursive(dirent);
    }
    return dirent;
  }

  close(callback) {
    // Promise
    if (callback === undefined) {
      if (this[kDirClosed] === true) {
        return PromiseReject(new ERR_DIR_CLOSED());
      }
      return this[kDirClosePromisified]();
    }

    // callback
    validateFunction(callback, 'callback');

    if (this[kDirClosed] === true) {
      process.nextTick(callback, new ERR_DIR_CLOSED());
      return;
    }

    if (this[kDirOperationQueue] !== null) {
      ArrayPrototypePush(this[kDirOperationQueue], () => {
        this.close(callback);
      });
      return;
    }

    this[kDirClosed] = true;
    const req = new FSReqCallback();
    req.oncomplete = callback;
    this[kDirHandle].close(req);
  }

  closeSync() {
    if (this[kDirClosed] === true) {
      throw new ERR_DIR_CLOSED();
    }

    if (this[kDirOperationQueue] !== null) {
      throw new ERR_DIR_CONCURRENT_OPERATION();
    }

    this[kDirClosed] = true;
    const ctx = { path: this[kDirPath] };
    const result = this[kDirHandle].close(undefined, ctx);
    handleErrorFromBinding(ctx);
    return result;
  }

  async* entries() {
    try {
      while (true) {
        const result = await this[kDirReadPromisified]();
        if (result === null) {
          break;
        }
        yield result;
      }
    } finally {
      await this[kDirClosePromisified]();
    }
  }
}

ObjectDefineProperty(Dir.prototype, SymbolAsyncIterator, {
  __proto__: null,
  value: Dir.prototype.entries,
  enumerable: false,
  writable: true,
  configurable: true,
});

function opendir(path, options, callback) {
  callback = typeof options === 'function' ? options : callback;
  validateFunction(callback, 'callback');

  path = getValidatedPath(path);
  options = getOptions(options, {
    encoding: 'utf8',
  });

  function opendirCallback(error, handle) {
    if (error) {
      callback(error);
    } else {
      callback(null, new Dir(handle, path, options));
    }
  }

  const req = new FSReqCallback();
  req.oncomplete = opendirCallback;

  dirBinding.opendir(
    pathModule.toNamespacedPath(path),
    options.encoding,
    req,
  );
}

function opendirSync(path, options) {
  path = getValidatedPath(path);
  options = getOptions(options, {
    encoding: 'utf8',
  });

  const ctx = { path };
  const handle = dirBinding.opendir(
    pathModule.toNamespacedPath(path),
    options.encoding,
    undefined,
    ctx,
  );
  handleErrorFromBinding(ctx);

  return new Dir(handle, path, options);
}

module.exports = {
  Dir,
  opendir,
  opendirSync,
};
