Files
peardata/server/db/replicate.js
T
Raven Scott 1d46b7b3ad
CI / test (push) Failing after 6s
Release rolling / release (push) Has been cancelled
updates
2026-07-18 19:11:32 -04:00

161 lines
4.3 KiB
JavaScript

/**
* Hyperswarm replication for the agent Corestore / HyperDB.
*
* Pattern (hyperdb-workshop bin.js):
* swarm.on('connection', (conn) => store.replicate(conn))
* swarm.join(db.discoveryKey, { server: true, client: true })
*
* Enable with PEARDATA_SWARM=1 (off by default — Phase B).
* Requires HyperDB to be open first.
*/
import Hyperswarm from 'hyperswarm'
import b4a from 'b4a'
import { getCorestore, getDb } from './index.js'
import { openRemoteDb } from './remote.js'
import { getServerPublicKeyHex } from '../core/auth-keys.js'
import logger from '../utils/logger.js'
const log = logger.child('db:replicate')
/** @type {import('hyperswarm')|null} */
let swarm = null
export function isSwarmEnabled() {
const v = process.env.PEARDATA_SWARM
return v === '1' || v === 'true' || v === 'on'
}
/**
* @returns {Promise<import('hyperswarm')|null>}
*/
export async function startReplication() {
if (!isSwarmEnabled()) {
log.info('Swarm replication disabled (set PEARDATA_SWARM=1 to enable)')
return null
}
const store = getCorestore()
const model = getDb()
if (!store || !model) {
log.warn('Cannot start swarm — HyperDB not open')
return null
}
if (swarm) return swarm
swarm = new Hyperswarm({
keyPair: await store.createKeyPair('peardata-swarm'),
})
swarm.on('connection', (conn, info) => {
const peer = info?.publicKey ? b4a.toString(info.publicKey, 'hex').slice(0, 12) : '?'
log.info('Swarm peer connected', { peer })
store.replicate(conn)
conn.on('close', () => log.info('Swarm peer disconnected', { peer }))
conn.on('error', (err) => log.warn('Swarm conn error', { peer, error: err.message }))
})
const topic = model.discoveryKey
swarm.join(topic, { server: true, client: true })
await swarm.flush()
log.info('Swarm joined HyperDB topic', {
discoveryKeyHex: model.discoveryKeyHex,
dbKeyHex: model.publicKeyHex,
})
await rejoinLinkedPeers().catch((err) => {
log.warn('rejoinLinkedPeers failed', { error: err.message })
})
return swarm
}
/**
* Join an additional remote DB discovery topic (pull mode for linked peers).
* @param {string|Buffer} discoveryKeyHexOrBuf
*/
export async function joinRemoteTopic(discoveryKeyHexOrBuf) {
if (!swarm) {
throw new Error('Swarm not started — enable PEARDATA_SWARM=1')
}
const key =
typeof discoveryKeyHexOrBuf === 'string'
? b4a.from(discoveryKeyHexOrBuf, 'hex')
: discoveryKeyHexOrBuf
swarm.join(key, { server: false, client: true })
log.info('Joined remote topic', {
discoveryKeyHex:
typeof discoveryKeyHexOrBuf === 'string'
? discoveryKeyHexOrBuf
: b4a.toString(discoveryKeyHexOrBuf, 'hex'),
})
}
/**
* Persist + open a linked peer for warm pull.
* @param {{ discoveryKeyHex?: string|null, dbKeyHex?: string|null, syncMode?: string }} link
*/
export async function attachLinkedPeer(link) {
const mode = link.syncMode || 'both'
if (mode !== 'pull' && mode !== 'both') return { joined: false, opened: false }
let joined = false
if (isSwarmEnabled() && link.discoveryKeyHex) {
try {
await joinRemoteTopic(link.discoveryKeyHex)
joined = true
} catch (err) {
log.warn('joinRemoteTopic failed', { error: err.message })
}
}
let opened = false
if (link.dbKeyHex) {
const remote = await openRemoteDb(link.dbKeyHex)
opened = Boolean(remote)
}
return { joined, opened }
}
/**
* Re-join swarm topics + open remote bees from persisted peer-links (boot).
*/
export async function rejoinLinkedPeers() {
const db = getDb()
if (!db) return { links: 0, joined: 0, opened: 0 }
let localNodeId
try {
localNodeId = getServerPublicKeyHex().slice(0, 16)
} catch {
return { links: 0, joined: 0, opened: 0 }
}
const links = await db.listPeerLinks(localNodeId)
let joined = 0
let opened = 0
for (const link of links) {
const res = await attachLinkedPeer(link)
if (res.joined) joined++
if (res.opened) opened++
}
if (links.length) {
log.info('Rejoined linked peers', { links: links.length, joined, opened })
}
return { links: links.length, joined, opened }
}
export async function stopReplication() {
if (!swarm) return
try {
await swarm.destroy()
} catch {
// ignore
}
swarm = null
log.info('Swarm stopped')
}
export function getSwarm() {
return swarm
}