161 lines
4.3 KiB
JavaScript
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
|
|
}
|