// Autobase-backed collaborative catalog manager (EXPERIMENTAL — feature
// flagged, off by default). CommonJS (.cjs) so Bare can require() it and Node
// can default-import it for the smoke test. Models the proven in-app Autobase
// usage in backend/pear-bridge.js (Sync Groups).
//
// Architecture: Autobase provides a replicated, linearized OP LOG; the view
// Hyperbee records each op in apply-order under `op!<index>`; the materialized
// catalog is produced by the PURE, unit-tested reducer (autobee-catalog-apply).
// Autobase owns ordering + replication, PearBrowser owns conflict semantics —
// they can't drift. No wall-clock anywhere.
const Autobase = require('autobase')
const Hyperbee = require('hyperbee')
const {
upsertOp, removeOp, renameOp, addWriterOp, validateOp, OP_ADD_WRITER
} = require('./autobee-catalog-ops.cjs')
const { applyView, toCatalogData } = require('./autobee-catalog-apply.cjs')
class AutobeeCatalogManager {
constructor (store, opts = {}) {
this.store = store
// Accept bootstrap as a hex string or Buffer (or null for a new catalog).
const b = opts.bootstrap || null
this.bootstrap = typeof b === 'string' ? Buffer.from(b, 'hex') : b
this._ns = opts.namespace ||
(this.bootstrap ? Buffer.from(this.bootstrap).toString('hex') : 'local')
this.base = null
}
async ready () {
const ns = this._ns
// CRITICAL: give the Autobase its OWN namespaced substore rather than the raw
// store. base.close() runs store.close(); on the shared ROOT Corestore that
// tears down Hyperdrive/UserData/Names/replication for the WHOLE app (a single
// consumer's close kills everything — verified by the shared-store regression
// test). A namespace session's close() frees only its own cores. Unlike the
// single-instance sync/name-registry bases, a store can hold MANY catalogs at
// once (one per key — see catalog-manager `_ensureAutobeeManager`), so the
// substore is keyed by `_ns` (the bootstrap key on load/join/reopen) to keep
// each catalog's cores isolated. The owner-create mint uses a unique throwaway
// `_ns`; reopen-by-key recovers the minted writer core (openable by key across
// substores), so create stays writable — verified by the catalog smoke.
const baseStore = typeof this.store.namespace === 'function'
? this.store.namespace('acat-' + ns) : this.store
this.base = new Autobase(baseStore, this.bootstrap, {
valueEncoding: 'json',
open: (store) => new Hyperbee(store.get({ name: `autobee-catalog-${ns}-view` }), {
extension: false, keyEncoding: 'utf-8', valueEncoding: 'json'
}),
apply: async (nodes, view, host) => {
const head = await view.get('meta!count').catch(() => null)
let count = head && Number.isFinite(head.value) ? head.value : 0
for (const node of nodes) {
const op = node.value
if (op && op.type === OP_ADD_WRITER && /^[0-9a-f]{64}$/i.test(op.key || '')) {
await host.addWriter(Buffer.from(op.key, 'hex'), { indexer: true })
continue
}
const verdict = validateOp(op)
if (!verdict.ok && !verdict.retain) continue
await view.put('op!' + String(count).padStart(12, '0'), op)
count++
}
await view.put('meta!count', count)
}
})
await this.base.ready()
return this
}
get writable () { return !!(this.base && this.base.writable) }
get key () { return this.base && this.base.key ? Buffer.from(this.base.key).toString('hex') : '' }
get discoveryKey () { return this.base && this.base.discoveryKey ? Buffer.from(this.base.discoveryKey) : null }
get localKey () { return this.base && this.base.local && this.base.local.key ? Buffer.from(this.base.local.key).toString('hex') : '' }
async rename (name) { await this.base.append(renameOp(name)) }
async upsertApp (app) {
const op = upsertOp(app)
const verdict = validateOp(op)
if (!verdict.ok) throw new Error('invalid app: ' + (verdict.reason || 'rejected'))
await this.base.append(op)
}
async removeApp (id) { await this.base.append(removeOp(id)) }
async addWriter (localKeyHex) { await this.base.append(addWriterOp(localKeyHex)) }
async update () { await this.base.update() }
// Materialize the catalog DTO from the linearized op log via the pure
// reducer — the single source of truth for conflict resolution.
async catalog () {
await this.base.update()
const view = this.base.view
const ops = []
for await (const entry of view.createReadStream({ gte: 'op!', lt: 'op!~' })) {
ops.push(entry.value)
}
return toCatalogData(applyView(ops), this.key, this.writable)
}
async close () { try { if (this.base) await this.base.close() } catch {} }
}
module.exports = { AutobeeCatalogManager }