/**
* room-meta.js — Room metadata backed by MetadataBase (Autobase)
*
* Thin wrapper over MetadataBase that provides the room-specific
* interface consumed by topic-announce.js, registry-room.js, and router.js.
*
* The MetadataBase Autobase handles:
* - Multi-writer conflict resolution (field-level LWW)
* - Replication via Hyperswarm + Protomux writer exchange
* - Persistent state in Hyperbee view
* - Secondary indexes (topic search)
*
* This module provides:
* - Backward-compatible get/set/getAll interface
* - parseHsMessage/createHsMessage helpers (used by dm-shares, registry-room)
* - One-time migration from legacy JSON cache
*
* Usage:
* const meta = new RoomMeta(metadataBase)
* await meta.get(roomId) → { topics: [...], discoverable: true, ... }
* await meta.set(roomId, { topics: ["rust"], discoverable: true })
* await meta.getAll() → { roomId: { ... }, ... }
*/
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
const HS_PREFIX = '{"_hs":'
export class RoomMeta {
/**
* @param {import('./metadata-base.js').MetadataBase} metadataBase
* @param {object} [opts]
* @param {string} [opts.storagePath] - For legacy migration (reads old JSON cache)
*/
constructor (metadataBase, opts = {}) {
this._base = metadataBase
this._migrated = false
// Legacy path (for JSON cache migration only)
if (opts.storagePath) {
const configDir = path.join(opts.storagePath, '.config', 'keet-cli')
this._legacyFilePath = path.join(configDir, 'room-meta.json')
} else {
const home = os.homedir() || '/tmp'
const configDir = path.join(home, '.config', 'keet-cli')
this._legacyFilePath = path.join(configDir, 'room-meta.json')
}
}
/**
* Get room metadata.
* Returns metadata object, or {} if no metadata exists.
* @param {string} roomId
* @returns {Promise<object>}
*/
async get (roomId) {
const value = await this._base.get('room', roomId)
if (!value) return {}
// Strip internal fields from public interface
const { _by, _ts, _fieldTs, _ns, _raw, ...fields } = value
return fields
}
/**
* Set room metadata. Merges with existing (field-level LWW in Autobase).
* @param {string} roomId
* @param {object} metadata - Fields to set (null values delete the field)
* @returns {Promise<object>} The current metadata after set
*/
async set (roomId, metadata) {
await this._base.set('room', roomId, metadata)
return this.get(roomId)
}
/**
* Get all room metadata.
* @returns {Promise<object>} Map of roomId → metadata
*/
async getAll () {
const raw = await this._base.getAll('room')
const result = {}
for (const [id, value] of Object.entries(raw)) {
const { _by, _ts, _fieldTs, _ns, _raw, ...fields } = value
result[id] = fields
}
return result
}
/**
* Search rooms by topic (uses Autobase secondary index).
* @param {string} topic
* @returns {Promise<Array<{roomId, topic}>>}
*/
async searchByTopic (topic) {
return this._base.searchByTopic(topic)
}
/**
* Migrate legacy JSON cache into MetadataBase.
* Called once on first startup after upgrade. Reads the old
* room-meta.json file and imports all entries into the Autobase.
*/
async migrateLegacy () {
if (this._migrated) return { migrated: 0 }
let count = 0
// Migrate from local JSON cache
try {
const data = fs.readFileSync(this._legacyFilePath, 'utf8')
const store = JSON.parse(data)
for (const [roomId, meta] of Object.entries(store)) {
// Strip legacy internal fields
const { _syncTs, ...fields } = meta
if (Object.keys(fields).length > 0) {
await this._base.set('room', roomId, fields)
count++
}
}
if (count > 0) {
// Rename legacy file to mark migration complete
const backupPath = this._legacyFilePath + '.migrated'
try {
fs.renameSync(this._legacyFilePath, backupPath)
} catch {
// Not fatal — may fail on some platforms
}
console.log(`[room-meta] Migrated ${count} rooms from legacy JSON cache`)
}
} catch {
// No legacy file — nothing to migrate
}
this._migrated = true
return { migrated: count }
}
}
/**
* Parse any _hs message from text. Returns { _hs, ...fields } or null.
* The _hs field contains the message type (e.g. "room-listing", "room-share").
*/
export function parseHsMessage (text) {
if (!text || !text.startsWith(HS_PREFIX)) return null
try {
const parsed = JSON.parse(text)
if (parsed._hs) return parsed
} catch {
// Not valid JSON
}
return null
}
/**
* Create an _hs message string.
*/
export function createHsMessage (type, fields) {
return JSON.stringify({ _hs: type, ...fields })
}