OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / standalone / server.js

175 lines · javascript ref:
/**
 * Standalone Block Storage Server
 * ================================
 * Holepunch Technical Challenge — Clean Implementation
 *
 * Accepts blocks from peers via protomux-rpc, appends them to a Hypercore.
 * Serves block reads back on request. Uses a single Hyperswarm for both
 * discovery and data transport.
 *
 * Design decisions:
 *  1. Single networking stack (Hyperswarm only — no redundant @hyperswarm/rpc)
 *  2. protomux-rpc for RPC — multiplexes over the existing swarm connection
 *  3. Server-as-topic: the server's public key IS the discovery topic
 *  4. Append-only semantics: blocks are immutable once stored
 */

import Corestore from 'corestore'
import Hyperswarm from 'hyperswarm'
import ProtomuxRPC from 'protomux-rpc'
import c from 'compact-encoding'
import b4a from 'b4a'
import gracedown from 'pear-gracedown'
import { mkdirSync } from 'fs'

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

const STORAGE_DIR = process.env.STORAGE_DIR || './storage-server'
// $PORT is intentionally unused — Hyperswarm chooses its own port. Kept
// here as documentation for operators who might be tempted to set it.

// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------

let store, swarm, core
const peers = new Map() // remotePublicKey hex → { rpc, connected }
let blockCount = 0

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main () {
  console.log('┌─────────────────────────────────────────┐')
  console.log('│  Standalone Block Storage Server         │')
  console.log('└─────────────────────────────────────────┘')

  // 1. Open Corestore + Hypercore
  mkdirSync(STORAGE_DIR, { recursive: true })
  store = new Corestore(STORAGE_DIR)
  core = store.get({ name: 'block-storage' })
  await core.ready()

  blockCount = core.length
  console.log('\n  Hypercore ready')
  console.log(`  Public key : ${b4a.toString(core.key, 'hex')}`)
  console.log(`  Blocks     : ${blockCount}`)

  // 2. Create Hyperswarm, replicate corestore on every connection
  swarm = new Hyperswarm()
  swarm.on('connection', onConnection)

  // 3. Join the swarm as server — topic = core's discovery key
  const discovery = swarm.join(core.discoveryKey, { server: true, client: false })
  await discovery.flushed()

  console.log('\n  Swarm listening')
  console.log(`  Discovery  : ${b4a.toString(core.discoveryKey, 'hex')}`)
  console.log('\n  Waiting for peers...\n')

  // 4. Graceful shutdown
  gracedown(async () => {
    console.log('\n  Shutting down...')
    for (const p of peers.values()) {
      if (p.rpc) p.rpc.destroy()
    }
    await swarm.destroy()
    await store.close()
    console.log('  Done.')
  })
}

// ---------------------------------------------------------------------------
// Connection handler
// ---------------------------------------------------------------------------

function onConnection (conn, info) {
  const remotePubKeyHex = b4a.toString(conn.remotePublicKey, 'hex')
  const shortKey = remotePubKeyHex.slice(0, 8)
  console.log(`  ← Peer connected: ${shortKey}...`)

  // Replicate all cores over this connection (Hypercore sync)
  store.replicate(conn)

  // Open RPC channel over the same multiplexed connection
  const rpc = new ProtomuxRPC(conn, {
    id: b4a.from('block-storage-rpc'),
    valueEncoding: c.json
  })

  // Track peer
  peers.set(remotePubKeyHex, { rpc, connected: Date.now() })

  // ── RPC: store-block ──────────────────────────────────────
  // Client sends { data } (base64-encoded block)
  // Server appends to Hypercore, returns { seq, length }
  rpc.respond('store-block', async (req) => {
    try {
      const buf = b4a.from(req.data, 'base64')
      const result = await core.append(buf)
      const seq = result.length - 1
      blockCount = core.length
      console.log(`  ✓ Block #${seq} stored (${buf.length} bytes) from ${shortKey}`)
      return { seq, length: core.length }
    } catch (err) {
      console.error(`  ✗ store-block error: ${err.message}`)
      return { error: err.message }
    }
  })

  // ── RPC: get-block ────────────────────────────────────────
  // Client sends { seq }
  // Server reads block at that index, returns { data, seq }
  rpc.respond('get-block', async (req) => {
    try {
      const seq = req.seq
      if (seq < 0 || seq >= core.length) {
        return { error: `Block ${seq} out of range (0..${core.length - 1})` }
      }
      const block = await core.get(seq)
      return { seq, data: b4a.toString(block, 'base64'), length: block.length }
    } catch (err) {
      console.error(`  ✗ get-block error: ${err.message}`)
      return { error: err.message }
    }
  })

  // ── RPC: get-info ─────────────────────────────────────────
  // Returns core metadata: key, length, byteLength
  rpc.respond('get-info', async () => {
    return {
      key: b4a.toString(core.key, 'hex'),
      length: core.length,
      byteLength: core.byteLength,
      peers: peers.size,
      uptime: Math.floor((Date.now() - startTime) / 1000)
    }
  })

  // Cleanup on disconnect
  conn.on('close', () => {
    console.log(`  → Peer disconnected: ${shortKey}...`)
    peers.delete(remotePubKeyHex)
  })

  conn.on('error', (err) => {
    console.error(`  ✗ Connection error (${shortKey}): ${err.message}`)
    peers.delete(remotePubKeyHex)
  })
}

// ---------------------------------------------------------------------------
// Start
// ---------------------------------------------------------------------------

const startTime = Date.now()
main().catch((err) => {
  console.error('Fatal:', err)
  process.exit(1)
})

export { main, core, swarm, store }