OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore / pear-app / test-relay-sync.mjs

337 lines · javascript ref:
#!/usr/bin/env node
/**
 * Comprehensive Relay Sync Test
 *
 * Tests the full pipeline:
 * 1. Pull existing posts from relay
 * 2. Check which local posts relay doesn't have (diff)
 * 3. Push only new posts
 * 4. Verify deduplication works
 * 5. Test content-by-hash retrieval
 */

import https from 'https'
import { ANProto } from './lib/anproto.js'
import { YAMLHelper } from './lib/yaml-helper.js'
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import b4a from 'b4a'
import { RelayClient } from './lib/relay-client.js'

const RELAY_URL = 'https://7rnx.net/relay'
const STORAGE_PATH = process.env.PEAR_STORAGE || './wirecore-storage'

// Color codes for output
const colors = {
  reset: '\x1b[0m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  red: '\x1b[31m',
  gray: '\x1b[90m'
}

function log(emoji, message, color = 'reset') {
  console.log(`${colors[color]}${emoji} ${message}${colors.reset}`)
}

class RelaySyncTest {
  constructor() {
    this.store = null
    this.metaBee = null
    this.contentCache = null
    this.myFeed = null
    this.anprotoKeypair = null
    this.relayClient = new RelayClient(RELAY_URL)
    this.stats = {
      relayTotal: 0,
      relayOurs: 0,
      localTotal: 0,
      localNotInRelay: 0,
      duplicates: 0,
      pushed: 0,
      pulled: 0
    }
  }

  async init() {
    log('🔧', 'Initializing storage...', 'blue')

    // Open Corestore
    this.store = new Corestore(STORAGE_PATH)
    await this.store.ready()

    // Load metadata
    this.metaBee = new Hyperbee(this.store.get({ name: 'metadata' }), {
      keyEncoding: 'utf-8',
      valueEncoding: 'json'
    })
    await this.metaBee.ready()

    // Load content cache
    this.contentCache = new Hyperbee(this.store.get({ name: 'content-cache' }), {
      keyEncoding: 'utf-8',
      valueEncoding: 'utf-8'
    })
    await this.contentCache.ready()

    // Load ANProto keypair
    const keypairNode = await this.metaBee.get('anproto-keypair')
    if (!keypairNode) {
      throw new Error('Not initialized. Run: pear run . cli init')
    }

    const keypairStr = keypairNode.value
    this.anprotoKeypair = ANProto.loadKeypair(keypairStr)

    // Load own feed
    this.myFeed = this.store.get({
      publicKey: this.anprotoKeypair.publicKey,
      secretKey: this.anprotoKeypair.secretKey
    })
    await this.myFeed.ready()

    const pubkey = b4a.toString(this.anprotoKeypair.publicKey, 'base64')
    log('✅', `Initialized as: ${pubkey.substring(0, 20)}...`, 'green')
  }

  async pullAllFromRelay() {
    log('📥', 'Step 1: Pulling all posts from relay...', 'blue')

    const posts = await this.relayClient.pull()
    this.stats.relayTotal = posts.length

    log('✅', `Relay has ${this.stats.relayTotal} total posts`, 'green')

    // Count how many are ours
    const ourPubkey = b4a.toString(this.anprotoKeypair.publicKey, 'base64')
    this.stats.relayOurs = posts.filter(p => p.author === ourPubkey).length

    log('📊', `  Our posts on relay: ${this.stats.relayOurs}`, 'gray')

    return posts
  }

  async getLocalPosts() {
    log('📖', 'Step 2: Reading local feed...', 'blue')

    const localPosts = []

    for (let i = 0; i < this.myFeed.length; i++) {
      try {
        const block = await this.myFeed.get(i)
        const entry = JSON.parse(block.toString())

        if (entry.yaml) {
          const contentHash = entry.contentHash || ANProto.hash(entry.yaml)
          localPosts.push({
            index: i,
            yaml: entry.yaml,
            contentHash,
            timestamp: entry.timestamp
          })
        }
      } catch (err) {
        log('⚠️', `  Skipping block ${i}: ${err.message}`, 'yellow')
      }
    }

    this.stats.localTotal = localPosts.length
    log('✅', `Local feed has ${this.stats.localTotal} posts`, 'green')

    return localPosts
  }

  async diffLocalVsRelay(localPosts, relayPosts) {
    log('🔍', 'Step 3: Computing diff (what relay is missing)...', 'blue')

    // Build hash set of relay content
    const relayHashes = new Set()
    for (const post of relayPosts) {
      if (post.text) {
        const hash = ANProto.hash(post.text)
        relayHashes.add(hash)
      }
    }

    log('📊', `  Relay content hashes: ${relayHashes.size}`, 'gray')

    // Find local posts not in relay
    const missing = []
    const duplicates = []

    for (const post of localPosts) {
      if (relayHashes.has(post.contentHash)) {
        duplicates.push(post)
      } else {
        missing.push(post)
      }
    }

    this.stats.localNotInRelay = missing.length
    this.stats.duplicates = duplicates.length

    log('✅', `Diff complete:`, 'green')
    log('  ', `  Duplicates (already on relay): ${this.stats.duplicates}`, 'gray')
    log('  ', `  Missing (need to push): ${this.stats.localNotInRelay}`, 'yellow')

    return { missing, duplicates }
  }

  async pushMissingPosts(missingPosts) {
    log('📤', `Step 4: Pushing ${missingPosts.length} missing posts to relay...`, 'blue')

    if (missingPosts.length === 0) {
      log('✅', 'Nothing to push - all posts already on relay!', 'green')
      return
    }

    let pushed = 0
    let errors = 0

    for (const post of missingPosts) {
      try {
        // Create signature
        const signature = ANProto.sign(post.contentHash, this.anprotoKeypair)
        const pubkey = b4a.toString(this.anprotoKeypair.publicKey, 'base64')

        // Push to relay
        await this.relayClient.push(pubkey, post.yaml, signature, null)
        pushed++
        log('✅', `  Pushed post ${post.index}: ${post.yaml.substring(0, 40)}...`, 'gray')
      } catch (err) {
        errors++
        log('❌', `  Failed to push post ${post.index}: ${err.message}`, 'red')
      }
    }

    this.stats.pushed = pushed
    log('✅', `Push complete: ${pushed} pushed, ${errors} errors`, 'green')
  }

  async verifyRelayUpdated() {
    log('🔍', 'Step 5: Verifying relay was updated...', 'blue')

    // Wait a bit for relay to process
    await new Promise(resolve => setTimeout(resolve, 2000))

    // Pull again
    const posts = await this.relayClient.pull()
    const newTotal = posts.length

    const ourPubkey = b4a.toString(this.anprotoKeypair.publicKey, 'base64')
    const ourCount = posts.filter(p => p.author === ourPubkey).length

    const increase = newTotal - this.stats.relayTotal
    const ourIncrease = ourCount - this.stats.relayOurs

    log('✅', `Verification:`, 'green')
    log('  ', `  Relay total: ${this.stats.relayTotal} → ${newTotal} (+${increase})`, 'gray')
    log('  ', `  Our posts: ${this.stats.relayOurs} → ${ourCount} (+${ourIncrease})`, 'gray')

    if (ourIncrease === this.stats.pushed) {
      log('✅', '  Perfect! All pushed posts appeared on relay', 'green')
    } else if (ourIncrease > 0) {
      log('⚠️', `  Partial success: expected +${this.stats.pushed}, got +${ourIncrease}`, 'yellow')
    } else {
      log('❌', '  No new posts on relay - push may have failed', 'red')
    }
  }

  async testContentByHashRetrieval() {
    log('🧪', 'Step 6: Testing content-by-hash retrieval...', 'blue')

    // Pick a random local post
    if (this.myFeed.length === 0) {
      log('⚠️', '  No local posts to test', 'yellow')
      return
    }

    const randomIndex = Math.floor(Math.random() * this.myFeed.length)
    const block = await this.myFeed.get(randomIndex)
    const entry = JSON.parse(block.toString())

    if (!entry.yaml) {
      log('⚠️', '  Selected post has no YAML', 'yellow')
      return
    }

    const contentHash = entry.contentHash || ANProto.hash(entry.yaml)

    // Check contentCache
    const cached = await this.contentCache.get(contentHash)

    if (cached) {
      log('✅', `  Content found in cache: ${contentHash.substring(0, 20)}...`, 'green')
      log('  ', `  Preview: ${cached.value.substring(0, 50)}...`, 'gray')
    } else {
      log('❌', `  Content NOT in cache: ${contentHash.substring(0, 20)}...`, 'red')
    }
  }

  printSummary() {
    console.log('\n' + '='.repeat(60))
    log('📊', 'SYNC TEST SUMMARY', 'blue')
    console.log('='.repeat(60))

    console.log(`
${colors.gray}Relay State:${colors.reset}
  Total posts on relay:        ${this.stats.relayTotal}
  Our posts on relay:          ${this.stats.relayOurs}

${colors.gray}Local State:${colors.reset}
  Total local posts:           ${this.stats.localTotal}

${colors.gray}Sync Results:${colors.reset}
  Already on relay (dupes):    ${this.stats.duplicates}
  Missing from relay:          ${this.stats.localNotInRelay}
  Successfully pushed:         ${this.stats.pushed}

${colors.gray}Efficiency:${colors.reset}
  Deduplication rate:          ${this.stats.localTotal > 0 ? Math.round(this.stats.duplicates / this.stats.localTotal * 100) : 0}%
  Push success rate:           ${this.stats.localNotInRelay > 0 ? Math.round(this.stats.pushed / this.stats.localNotInRelay * 100) : 0}%
`)

    console.log('='.repeat(60))
  }

  async cleanup() {
    if (this.store) {
      await this.store.close()
    }
  }

  async run() {
    try {
      await this.init()

      const relayPosts = await this.pullAllFromRelay()
      const localPosts = await this.getLocalPosts()
      const { missing, duplicates } = await this.diffLocalVsRelay(localPosts, relayPosts)

      await this.pushMissingPosts(missing)
      await this.verifyRelayUpdated()
      await this.testContentByHashRetrieval()

      this.printSummary()

      log('✅', 'All tests complete!', 'green')
    } catch (err) {
      log('❌', `Test failed: ${err.message}`, 'red')
      console.error(err.stack)
      process.exit(1)
    } finally {
      await this.cleanup()
    }
  }
}

// Run the test
const test = new RelaySyncTest()
test.run().then(() => {
  process.exit(0)
}).catch(err => {
  console.error(err)
  process.exit(1)
})