#!/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)
})