OverviewFilesCommitsIssuesDocsReleasesPeers

autobase-doctor / lib / repair.js

145 lines · javascript ref:
/**
 * Repair — fix corrupted Autobase instances.
 *
 * Two repair operations:
 *   1. truncateWriterCore — remove poisoned ops from the writer log
 *   2. resetSystemState — truncate the internal system + view batches
 *      so Autobase re-bootstraps from the clean writer log
 *
 * Both are needed because:
 *   - The writer core has the poisoned op in the append-only log
 *   - The system core has persisted state built from the poisoned op
 *   - Truncating only the writer core leaves stale system state
 *   - Resetting only the system core causes it to replay the poison
 */

import Autobase from 'autobase'
import Hyperbee from 'hyperbee'

/**
 * Truncate a writer core at the poisoned op index.
 * All ops before the poison are preserved.
 *
 * @param {Corestore} ns - Namespaced corestore
 * @param {number} poisonIndex - Index of the poisoned op
 * @returns {{ before: number, after: number }}
 */
export async function truncateWriterCore (ns, poisonIndex) {
  const local = ns.get({ name: 'local' })
  await local.ready()

  const before = local.length
  await local.truncate(poisonIndex)
  const after = local.length

  await local.close()
  return { before, after }
}

/**
 * Reset Autobase system state so it re-bootstraps from the writer log.
 *
 * Opens the Autobase, accesses the internal ViewCore batch sessions
 * (which are writable even when the read-only session is not), and
 * truncates them to 0. Also clears the autobase/boot userData.
 *
 * Why the batch, not the core directly:
 *   - base.core is a read-only session (Autobase creates it with writable: false)
 *   - Opening the core by key doesn't provide the manifest, so corestore
 *     treats it as remote (not writable)
 *   - The ViewCore's batch session IS writable — it was created with
 *     { name: 'batch', writable: true } by AutoStore
 *
 * @param {Corestore} ns - Namespaced corestore
 * @param {Object} opts
 * @param {Function} opts.open - Autobase open handler (creates the view)
 * @param {Function} opts.apply - Autobase apply handler
 * @returns {{ systemTruncated: number, viewTruncated: number, bootCleared: boolean }}
 */
export async function resetSystemState (ns, opts = {}) {
  const {
    open = defaultOpen,
    apply = defaultApply
  } = opts

  const base = new Autobase(ns, null, {
    open,
    apply,
    valueEncoding: 'binary'
  })
  await base.ready()

  const result = { systemTruncated: 0, viewTruncated: 0, bootCleared: false }

  // Truncate system batch
  const sysView = base._viewStore.getSystemView()
  if (sysView.batch && sysView.batch.writable && sysView.batch.length > 0) {
    result.systemTruncated = sysView.batch.length
    await sysView.batch.truncate(0)
  }

  // Truncate view batch
  const viewView = base._viewStore.byName.get('view')
  if (viewView && viewView.batch && viewView.batch.writable && viewView.batch.length > 0) {
    result.viewTruncated = viewView.batch.length
    await viewView.batch.truncate(0)
  }

  // Clear boot record
  const boot = await base.local.getUserData('autobase/boot')
  if (boot) {
    await base.local.setUserData('autobase/boot', null)
    result.bootCleared = true
  }

  await base.close()
  return result
}

/**
 * Check if an Autobase has poisoned system state.
 * Signs: writable=false with members > 0 on a self-bootstrapped Autobase.
 */
export async function hasPoisonedSystemState (ns, opts = {}) {
  const {
    open = defaultOpen,
    apply = defaultApply
  } = opts

  const base = new Autobase(ns, null, {
    open,
    apply,
    valueEncoding: 'binary'
  })
  await base.ready()

  const poisoned = !base.writable && base.system.members > 0
  const info = {
    poisoned,
    writable: base.writable,
    members: base.system.members,
    indexers: base.system.indexers.length,
    localLength: base.local.length,
    systemBatchLength: 0
  }

  const sysView = base._viewStore.getSystemView()
  if (sysView.batch) {
    info.systemBatchLength = sysView.batch.length
  }

  await base.close()
  return info
}

function defaultOpen (store) {
  return new Hyperbee(store.get('view', { valueEncoding: 'json' }), {
    keyEncoding: 'utf-8',
    valueEncoding: 'json'
  })
}

async function defaultApply (nodes, view, host) {
  await view.batch().flush()
}