OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / scripts / compact-autobases.js

162 lines · javascript ref:
#!/usr/bin/env node

/**
 * Compact hypernote Autobases — remove duplicate entries from sync loop bloat.
 *
 * Approach:
 * 1. Open each SharedNotebook and SharedRegistry Autobase
 * 2. Read the current view state (already deduplicated by Autobase linearization)
 * 3. Check how many entries the local writer has vs unique entries in view
 * 4. Report what can be compacted
 *
 * The actual compaction: delete the multiwriter config from the registry,
 * which causes the server to create fresh Autobases on next start.
 * The event-driven sync re-populates from local Hyperdrives (notes) and
 * identity registry (users).
 *
 * Usage:
 *   node scripts/compact-autobases.js <storage-path> [--apply]
 */

import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import Autobase from 'autobase'

const args = process.argv.slice(2)
const storagePath = args[0] || process.env.HOME + '/.config/hypernote/server-data'
const apply = args.includes('--apply')

console.log('=== Autobase Compaction ===')
console.log('Storage:', storagePath)
console.log('Mode:', apply ? 'APPLY (will modify data)' : 'DRY RUN (read-only)')
console.log('')

const store = new Corestore(storagePath)
await store.ready()

// Open the main registry to find multiwriter config
const registryCore = store.get({ name: 'registry' })
const registry = new Hyperbee(registryCore, { keyEncoding: 'utf-8', valueEncoding: 'json' })
await registry.ready()

const mwConfig = await registry.get('multiwriter-config')
if (!mwConfig || !mwConfig.value) {
  console.log('No multiwriter config found — nothing to compact.')
  await store.close()
  process.exit(0)
}

const bootstrapKey = mwConfig.value.registryBootstrapKey
console.log('Multiwriter config:', JSON.stringify(mwConfig.value, null, 2))
console.log('')

// Open SharedRegistry to inspect its state
const regStore = store.namespace('shared-registry')
const regBase = new Autobase(regStore, Buffer.from(bootstrapKey, 'hex'), {
  open: (store) => new Hyperbee(store.get('view'), {
    keyEncoding: 'utf-8',
    valueEncoding: 'json'
  }),
  apply: async (nodes, view, base) => {
    for (const node of nodes) {
      const op = node.value
      if (!op || !op.key) continue
      await view.put(op.key, op.value)
    }
  },
  valueEncoding: 'json'
})
await regBase.ready()
await regBase.update()

// Count view entries
let viewEntries = 0
let notebooks = []
for await (const entry of regBase.view.createReadStream()) {
  viewEntries++
  if (entry.key.startsWith('notebook:')) {
    const nb = entry.value
    if (nb && nb.slug) notebooks.push({ slug: nb.slug, autobaseKey: nb.autobaseKey, shared: nb.shared })
  }
}

const localWriterLen = regBase.local ? regBase.local.length : 0
console.log('SharedRegistry:')
console.log('  View entries (unique):', viewEntries)
console.log('  Local writer blocks:', localWriterLen)
console.log('  Bloat ratio:', localWriterLen > 0 ? (localWriterLen / Math.max(viewEntries, 1)).toFixed(1) + 'x' : 'N/A')
console.log('  Notebooks:', notebooks.map(n => n.slug).join(', '))
console.log('')

// Inspect each SharedNotebook
for (const nb of notebooks) {
  if (!nb.shared || !nb.autobaseKey) continue

  const nbStore = store.namespace('shared-notebook-' + nb.slug)
  try {
    const nbBase = new Autobase(nbStore, Buffer.from(nb.autobaseKey, 'hex'), {
      open: (store) => new Hyperbee(store.get('view'), {
        keyEncoding: 'utf-8',
        valueEncoding: 'json'
      }),
      apply: async (nodes, view, base) => {
        for (const node of nodes) {
          const op = node.value
          if (!op || !op.key) continue
          await view.put(op.key, op.value)
        }
      },
      valueEncoding: 'json'
    })
    await nbBase.ready()
    await nbBase.update()

    let nbViewEntries = 0
    for await (const entry of nbBase.view.createReadStream()) {
      nbViewEntries++
    }

    const nbLocalLen = nbBase.local ? nbBase.local.length : 0
    console.log(`SharedNotebook [${nb.slug}]:`)
    console.log('  View entries (unique):', nbViewEntries)
    console.log('  Local writer blocks:', nbLocalLen)
    console.log('  Bloat ratio:', nbLocalLen > 0 ? (nbLocalLen / Math.max(nbViewEntries, 1)).toFixed(1) + 'x' : 'N/A')

    await nbBase.close()
  } catch (err) {
    console.log(`SharedNotebook [${nb.slug}]: ERROR — ${err.message}`)
  }
}

console.log('')

if (!apply) {
  console.log('=== Dry run complete. Use --apply to compact. ===')
  console.log('')
  console.log('Compaction will:')
  console.log('  1. Delete multiwriter-config from registry')
  console.log('  2. Server creates fresh Autobases on next start')
  console.log('  3. Event-driven sync re-populates from local data')
  console.log('  4. Old Autobase cores become orphaned (reclaimable later)')
  await regBase.close()
  await store.close()
  process.exit(0)
}

// APPLY compaction
console.log('=== Applying compaction ===')
console.log('Deleting multiwriter-config from registry...')
await registry.del('multiwriter-config')
console.log('Done. Multiwriter config removed.')
console.log('')
console.log('On next server start:')
console.log('  - Fresh Autobases will be created')
console.log('  - Mirror sync will re-populate from local Hyperdrives')
console.log('  - Memory usage will drop significantly')
console.log('')
console.log('Old Autobase cores are still on disk but unused.')
console.log('Run hyper-inspector to verify storage savings after restart.')

await regBase.close()
await store.close()