async function parquetMetadataAsync(asyncBuffer, { parsers, initialFetchSize = defaultInitialFetchSize } = {}) {
  if (!asyncBuffer || !(asyncBuffer.byteLength >= 0)) throw new Error('parquet expected AsyncBuffer')

  // fetch last bytes (footer) of the file
  const footerOffset = Math.max(0, asyncBuffer.byteLength - initialFetchSize)
  const footerBuffer = await asyncBuffer.slice(footerOffset, asyncBuffer.byteLength)

  // Check for parquet magic number "PAR1"
  const footerView = new DataView(footerBuffer)
  if (footerView.getUint32(footerBuffer.byteLength - 4, true) !== 0x31524150) {
    throw new Error('parquet file invalid (footer != PAR1)')
  }

  // Parquet files store metadata at the end of the file
  // Metadata length is 4 bytes before the last PAR1
  const metadataLength = footerView.getUint32(footerBuffer.byteLength - 8, true)
  if (metadataLength > asyncBuffer.byteLength - 8) {
    throw new Error(`parquet metadata length ${metadataLength} exceeds available buffer ${asyncBuffer.byteLength - 8}`)
  }

  // check if metadata size fits inside the initial fetch
  if (metadataLength + 8 > initialFetchSize) {
    // fetch the rest of the metadata
    const metadataOffset = asyncBuffer.byteLength - metadataLength - 8
    const metadataBuffer = await asyncBuffer.slice(metadataOffset, footerOffset)
    // combine initial fetch with the new slice
    const combinedBuffer = new ArrayBuffer(metadataLength + 8)
    const combinedView = new Uint8Array(combinedBuffer)
    combinedView.set(new Uint8Array(metadataBuffer))
    combinedView.set(new Uint8Array(footerBuffer), footerOffset - metadataOffset)
    return parquetMetadata(combinedBuffer, { parsers })
  } else {
    // parse metadata from the footer
    return parquetMetadata(footerBuffer, { parsers })
  }
}
Read parquet metadata from an async buffer.

An AsyncBuffer is like an ArrayBuffer, but the slices are loaded
asynchronously, possibly over the network.

You must provide the byteLength of the buffer, typically from a HEAD request.

In theory, you could use suffix-range requests to fetch the end of the file,
and save a round trip. But in practice, this doesn't work because chrome
deems suffix-range requests as a not-safe-listed header, and will require
a pre-flight. So the byteLength is required.

To make this efficient, we initially request the last 512kb of the file,
which is likely to contain the metadata. If the metadata length exceeds the
initial fetch, 512kb, we request the rest of the metadata from the AsyncBuffer.

This ensures that we either make one 512kb initial request for the metadata,
or a second request for up to the metadata size.

@param {AsyncBuffer} asyncBuffer parquet file contents
@param {MetadataOptions & { initialFetchSize?: number }} options initial fetch size in bytes (default 512kb)
@returns {Promise<FileMetaData>} parquet metadata object