OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-inspector / lib / rocksdb-inspector.js

130 lines · javascript ref:
import RocksDB from 'rocksdb-native'
import b4a from 'b4a'

const CORESTORE_CF = 'corestore'

// Key type labels based on hypercore-storage schema
const KEY_TYPES = {
  0x00: 'seed',
  0x01: 'core-key',
  0x02: 'discovery-key',
  0x03: 'core-info',
  0x04: 'data-block'
}

export async function openDB (dbPath, opts = {}) {
  const { readOnly = false } = opts
  const db = new RocksDB(dbPath, {
    readOnly,
    columnFamilies: [CORESTORE_CF]
  })
  await db.ready()
  return db
}

export async function scanColumnFamily (session, opts = {}) {
  const { limit = 100000 } = opts

  const keyTypes = new Map()
  let totalKeys = 0
  let totalBytes = 0
  const readable = []

  const iter = session.iterator({ limit })

  return new Promise((resolve, reject) => {
    iter.on('data', ({ key, value }) => {
      totalKeys++
      const valLen = value ? value.length : 0
      totalBytes += valLen

      const firstByte = key[0]
      const info = keyTypes.get(firstByte) || { count: 0, bytes: 0 }
      info.count++
      info.bytes += valLen
      keyTypes.set(firstByte, info)

      // Collect readable strings
      if (value && valLen > 2 && valLen < 2000) {
        try {
          const str = b4a.toString(value, 'utf8')
          if (/^[\x20-\x7e\n\r\t]+$/.test(str) && readable.length < 100) {
            readable.push({
              key: b4a.toString(key, 'hex'),
              type: firstByte,
              value: str.slice(0, 300)
            })
          }
        } catch {}
      }
    })

    iter.on('end', () => resolve({ totalKeys, totalBytes, keyTypes, readable }))
    iter.on('error', reject)
  })
}

export async function listApps (db) {
  const defaultIter = db.iterator({ limit: 10000 })

  const pearApps = []
  const fileSources = []

  return new Promise((resolve, reject) => {
    defaultIter.on('data', ({ key, value }) => {
      if (key.length < 2) return

      // Extract readable string from key (skip prefix byte)
      let keyStr = ''
      for (let i = 1; i < key.length; i++) {
        if (key[i] >= 0x20 && key[i] <= 0x7e) {
          keyStr += String.fromCharCode(key[i])
        }
      }

      // Extract readable string from value
      let valStr = ''
      if (value && value.length < 500) {
        for (let i = 0; i < value.length; i++) {
          if (value[i] >= 0x20 && value[i] <= 0x7e) {
            valStr += String.fromCharCode(value[i])
          }
        }
      }

      const prefix = key[0]
      if (keyStr.startsWith('pear://')) {
        pearApps.push({ prefix, url: keyStr, storage: valStr, valLen: value ? value.length : 0 })
      } else if (keyStr.startsWith('file://')) {
        fileSources.push({ prefix, path: keyStr, storage: valStr, valLen: value ? value.length : 0 })
      }
    })

    defaultIter.on('end', () => resolve({ pearApps, fileSources }))
    defaultIter.on('error', reject)
  })
}

export async function getCorestoreStats (db) {
  const cs = db.columnFamily(CORESTORE_CF)
  const stats = await scanColumnFamily(cs)

  return {
    ...stats,
    typeBreakdown: [...stats.keyTypes.entries()].map(([byte, info]) => ({
      type: KEY_TYPES[byte] || `unknown-0x${byte.toString(16)}`,
      byte,
      count: info.count,
      bytes: info.bytes,
      mb: (info.bytes / 1024 / 1024).toFixed(2)
    })).sort((a, b) => b.bytes - a.bytes)
  }
}

export function formatBytes (bytes) {
  if (bytes >= 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB'
  if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'
  if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'
  return bytes + ' B'
}