// read-statebase.mjs — proper read-only inspector for a room's StateBase v3
// HyperDB view. Dumps exactly what is persisted on disk + the autobase writer
// state, so we can verify persistence WITHOUT launching the game.
//
// Usage (game/daemon for this identity must be STOPPED — it holds the corestore lock):
// cd packages/keet-daemon && bare tools/read-statebase.mjs <roomId> [--keys-only] [--raw]
//
// It boots keet-daemon against the active identity (so roomSeed/encryptionKey +
// membership providers are wired exactly as the game wires them), opens the
// room's StateBase read-only via the same router path the game uses, and dumps:
// • base.key, writable, writer/indexer count
// • __members__ projection (who the base thinks can write)
// • every @state/entries key=value (the actual persisted game state)
import { startDaemon } from '../index.js'
const args = Bare.argv.slice(2)
const roomId = args.find(a => !a.startsWith('--'))
const keysOnly = args.includes('--keys-only')
const raw = args.includes('--raw')
if (!roomId) {
console.log('usage: bare tools/read-statebase.mjs <roomId> [--keys-only] [--raw]')
Bare.exit(1)
}
setTimeout(() => { console.error('TIMEOUT (90s)'); Bare.exit(2) }, 90000)
const d = await startDaemon({ socketPath: '/tmp/keet-reader.sock', pidFile: '/tmp/keet-reader.pid', debug: false })
await d.core.backendAPI.ready()
const sb = await d.router._getStateBase(roomId)
console.log('================ StateBase v3 ================')
console.log('room :', roomId)
console.log('base.key :', sb.stateBaseKey ? sb.stateBaseKey.toString('hex') : '(none)')
console.log('writable :', sb.writable)
console.log('hasBootstrap:', sb.hasBootstrap)
// autobase writer/indexer introspection (best-effort across autobase versions)
try {
const b = sb.base
const writers = (b.activeWriters && b.activeWriters.size) || (b.writers && b.writers.length) || '?'
const indexers = (b.linearizer && b.linearizer.indexers && b.linearizer.indexers.length) || '?'
console.log('writers :', writers, ' indexers:', indexers)
} catch (e) { console.log('writers : (introspection failed:', e.message, ')') }
const all = await sb.list('')
const keys = Object.keys(all).sort()
// Pull out the membership projection explicitly — it gates admission.
if (all.__members__ !== undefined) {
let m = all.__members__
try { m = typeof m === 'string' ? JSON.parse(m) : m } catch {}
console.log('__members__:', JSON.stringify(m))
}
// Group by prefix so it's readable (world:players:, world:terrain:edit:, ...)
const groups = {}
for (const k of keys) {
if (k === '__members__') continue
const pfx = k.split(':').slice(0, 2).join(':')
groups[pfx] = (groups[pfx] || 0) + 1
}
console.log('---- key groups (count) ----')
for (const g of Object.keys(groups).sort()) console.log(' %s\t%d', g, groups[g])
console.log('---- entries (%d total) ----', keys.length)
for (const k of keys) {
if (keysOnly) { console.log(' ' + k); continue }
let v = all[k]
let s = raw ? (typeof v === 'string' ? v : JSON.stringify(v)) : JSON.stringify(v)
if (!raw && s && s.length > 120) s = s.slice(0, 120) + '…'
console.log(' %s = %s', k, s)
}
Bare.exit(0)