OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / hyperblobs / lib / streams.js

351 lines · javascript ref:
const { Readable, Writable } = require('streamx')
const { BLOCK_NOT_AVAILABLE } = require('hypercore-errors')
const blockMap = require('./block-map')

class BlobWriteStream extends Writable {
  constructor(core, lock, opts = {}) {
    super(opts)
    this.id = { blockOffset: 0, byteOffset: 0, blockLength: 0, byteLength: 0 }
    this.core = core

    this._dedup = !!opts.dedup
    this._blob = opts.blob || null
    this._hashes = null
    this._addBlockMap = !!opts.blockMap || this._dedup
    this._blockMap = this._addBlockMap ? { version: 0, blocks: [] } : null
    this._lock = lock
    this._release = null
    this._batch = []

    if (this._addBlockMap) this.id.blockMap = true
  }

  async _openp() {
    await this.core.ready()
    const release = await new Promise((resolve) => this._lock(resolve))

    this._release = release
    this.id.byteOffset = this.core.byteLength
    this.id.blockOffset = this.core.length

    if (!this._dedup) return

    if (this._blob) {
      const map = await blockMap.get(this.core, this._blob, { hashes: true })
      this._hashes = map.hashes
    } else {
      this._hashes = new Map()
    }
  }

  _open(cb) {
    this._openp().then(cb, cb)
  }

  async _finalp() {
    await this._append()

    if (this._blockMap) {
      const buffers = await blockMap.encode(this._blockMap)
      this.id.blockOffset = this.core.length
      this.id.byteOffset = this.core.byteLength
      await this.core.append(buffers)
    }

    this.id.blockLength = this.core.length - this.id.blockOffset
    this.id.byteLength = this.core.byteLength - this.id.byteOffset
  }

  _final(cb) {
    this._finalp().then(cb, cb)
  }

  _destroy(cb) {
    if (this._release) this._release()
    cb(null)
  }

  async _append() {
    if (!this._batch.length) return

    const batch = this._batch
    this._batch = []

    await this.core.append(batch)
  }

  _write(data, cb) {
    let dup = false

    if (this._blockMap) {
      let entry = {
        index: this.core.length + this._batch.length,
        byteLength: data.byteLength
      }

      if (this._hashes) {
        const id = blockMap.hash(data)
        const existing = this._hashes.get(id)

        if (existing) {
          entry = existing
          dup = true
        } else {
          this._hashes.set(id, entry)
        }
      }

      this._blockMap.blocks.push(entry)
    }

    if (dup) return cb()

    this._batch.push(data)

    if (this._batch.length >= 16) {
      this._append().then(cb, cb)
      return
    }

    return cb()
  }
}

class BlockMapReadStream extends Readable {
  constructor(core, id, opts = {}) {
    super(opts)
    this.id = id
    this.core = core.session({ wait: opts.wait, timeout: opts.timeout })

    const noPrefetch = opts.wait === false || opts.prefetch === false || !core.core
    const start = opts.start || 0
    const end =
      opts.end === undefined ? (opts.length === undefined ? -1 : start + opts.length) : opts.end + 1

    this._blockMap = null

    this._rangeStart = start
    this._rangeEnd = end

    this._startIndex = 0
    this._startOffset = 0
    this._endIndex = -1
    this._endOffset = -1

    this._range = null
    this._noPrefetch = noPrefetch
  }

  async _openp() {
    this._blockMap = await blockMap.get(this.core, this.id)

    const [startIndex, startOffset, endIndex, endLength] = seekBlockMap(
      this._blockMap,
      this._rangeStart,
      this._rangeEnd
    )

    this._startIndex = startIndex
    this._startOffset = startOffset
    this._endIndex = endIndex
    this._endOffset = endLength

    if (this._endIndex === -1) {
      this._endIndex = this._blockMap.blocks.length
      this._endOffset = 0
    }
  }

  _open(cb) {
    this._openp().then(cb, cb)
  }

  _predestroy() {
    if (this._range) this._range.destroy()
    this.core.close().then(noop, noop)
  }

  _destroy(cb) {
    if (this._range) this._range.destroy()
    this.core.close().then(cb, cb)
  }

  _prefetch(index) {
    const blocks = []
    for (; index < this._endIndex; index++) blocks.push(this._blockMap.blocks[index].index)
    this._range = this.core.download({ blocks })
  }

  async _readp() {
    if (this._startIndex >= this._endIndex) {
      this.push(null)
      return
    }

    let block = null

    const index = this._startIndex++
    const b = this._blockMap.blocks[index]

    if (!this._range && !this._noPrefetch) {
      block = await this.core.get(b.index, { wait: false })
      if (!block && !this._range) this._prefetch(index)
    }

    if (!block) {
      block = await this.core.get(b.index)
    }

    if (!block) throw BLOCK_NOT_AVAILABLE()

    if (this._startOffset) {
      block = block.subarray(this._startOffset)
      this._startOffset = 0
    }

    if (this._startIndex === this._endIndex && this._endOffset) {
      block = block.subarray(0, block.byteLength - this._endOffset)
    }

    this.push(block)
  }

  _read(cb) {
    this._readp().then(cb, cb)
  }
}

class BlobReadStream extends Readable {
  constructor(core, id, opts = {}) {
    super(opts)
    this.id = id
    this.core = core.session({ wait: opts.wait, timeout: opts.timeout })

    const start = id.blockOffset
    const end = id.blockOffset + id.blockLength
    const noPrefetch = opts.wait === false || opts.prefetch === false || !core.core

    this._prefetch = !noPrefetch
    this._range = null
    this._pos = opts.start !== undefined ? id.byteOffset + opts.start : id.byteOffset

    if (opts.length !== undefined) this._end = this._pos + opts.length
    else if (opts.end !== undefined) this._end = id.byteOffset + opts.end + 1
    else this._end = id.byteOffset + id.byteLength

    this._index = 0
    this._relativeOffset = 0
    this._bytesRead = 0
  }

  async _setStart() {
    if (this._pos === this.id.byteOffset) {
      this._index = this.id.blockOffset
      this._relativeOffset = 0
      return this._index
    }

    const result = await this.core.seek(this._pos)
    if (!result) throw BLOCK_NOT_AVAILABLE()

    this._index = result[0]
    this._relativeOffset = result[1]

    return this._index
  }

  async _setEnd() {
    if (this._end === this.id.byteOffset + this.id.byteLength) {
      return this.id.blockOffset + this.id.blockLength
    }

    const result = await this.core.seek(this._end)
    if (!result) return this.id.blockOffset + this.id.blockLength
    return result[0]
  }

  async _openp() {
    const [start, end] = await Promise.all([this._setStart(), this._setEnd()])
    if (this._prefetch) this._range = this.core.download({ start, end, linear: true })
  }

  _open(cb) {
    this._openp().then(cb, cb)
  }

  _predestroy() {
    if (this._range) this._range.destroy()
    this.core.close().then(noop, noop)
  }

  _destroy(cb) {
    if (this._range) this._range.destroy()
    this.core.close().then(cb, cb)
  }

  async _readp() {
    if (this._pos >= this._end) {
      this.push(null)
      return
    }

    let block = await this.core.get(this._index)
    if (!block) throw BLOCK_NOT_AVAILABLE()

    const remainder = this._end - this._pos
    if (this._relativeOffset || remainder < block.length) {
      block = block.subarray(this._relativeOffset, this._relativeOffset + remainder)
    }

    this._index++
    this._relativeOffset = 0
    this._pos += block.length
    this._bytesRead += block.length

    this.push(block)
  }

  _read(cb) {
    this._readp().then(cb, cb)
  }
}

module.exports = {
  BlockMapReadStream,
  BlobReadStream,
  BlobWriteStream
}

function noop() {}

function seekBlockMap(map, start, end) {
  let s = -1
  let so = -1
  let e = -1
  let eo = -1

  for (let i = 0; i < map.blocks.length; i++) {
    const b = map.blocks[i]

    if (s === -1) {
      if (start < b.byteLength) {
        s = i
        so = start
      } else {
        start -= b.byteLength
      }
    }

    if (e === -1 && end > -1) {
      if (end <= b.byteLength) {
        e = i + 1
        eo = b.byteLength - end
      } else {
        end -= b.byteLength
      }
    }
  }

  return [s, so, e, eo]
}