OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore commits / 128176b

Fix bridge ELOCKED error by sharing Corestore instance
The subprocess approach failed because: 1. pear:// apps run from cache, not a local directory 2. Spawning `pear run . bridge` fails with ERR_INVALID_PROJECT_DIR 3. Opening same storage from multiple processes causes ELOCKED Solution: - Run bridge in-process instead of as subprocess - Share existing Corestore instance from web server to bridge - Bridge constructor now accepts existingStore parameter - Only close store if bridge created it (not when shared) - Added stop() method to clean up intervals and event listeners - getBridgeStatus() no longer tries to create temp bridge instance Testing results: ✅ Bridge starts without ELOCKED errors ✅ Sync pulled 633 messages from relay ✅ Bridge stops cleanly ✅ All API endpoints working (status, start, stop, sync) Released as v4195 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
128176b90b89ff3a263dffc5100a43827f327e1d
by plankatron <fleeky@gmail.com> · 2026-01-22T16:26:49.000Z

Changed files (3) +84 -55

pear-app/bridge/cli.js +2 -2
……@@ -65,8 +65,8 @@ async function main() {
6565 process.exit(0)
6666 }
6767
68 // Initialize bridge
69 const bridge = new WirecoreBridge(storagePath, relayUrl)
68 // Initialize bridge (CLI runs standalone, so no existing store)
69 const bridge = new WirecoreBridge(relayUrl, null, storagePath)
7070
7171 try {
7272 await bridge.init()
pear-app/bridge/wirecore-bridge.js +44 -13
……@@ -17,15 +17,18 @@ import { RelayClient } from '../lib/relay-client.js'
1717import { ANProto } from '../lib/anproto.js'
1818
1919export class WirecoreBridge {
20 constructor(storagePath, relayUrl) {
21 this.storagePath = storagePath
20 constructor(relayUrl, existingStore = null, storagePath = null) {
2221 this.relayUrl = relayUrl
23 this.store = null
22 this.storagePath = storagePath
23 this.store = existingStore // Reuse existing Corestore if provided
2424 this.metaBee = null
2525 this.syncBee = null // Track sync state
2626 this.relayClient = null
2727 this.myFeed = null
2828 this.anprotoKeypair = null
29 this._pullInterval = null // For cleanup
30 this._appendHandler = null // For cleanup
31 this._ownStore = false // Track if we created the store
2932 }
3033
3134 /**
……@@ -33,12 +36,20 @@ export class WirecoreBridge {
3336 */
3437 async init() {
3538 console.log('🌉 Initializing bridge...')
36 console.log(' Storage:', this.storagePath)
3739 console.log(' Relay:', this.relayUrl)
3840
39 // Open same Corestore as pear-app
40 this.store = new Corestore(this.storagePath)
41 await this.store.ready()
41 // Open Corestore if not provided
42 if (!this.store) {
43 if (!this.storagePath) {
44 throw new Error('Either existingStore or storagePath must be provided')
45 }
46 console.log(' Opening storage:', this.storagePath)
47 this.store = new Corestore(this.storagePath)
48 await this.store.ready()
49 this._ownStore = true
50 } else {
51 console.log(' Using existing Corestore')
52 }
4253
4354 // Load metadata (keypair, etc.)
4455 this.metaBee = new Hyperbee(this.store.get({ name: 'metadata' }), {
……@@ -257,17 +268,18 @@ export class WirecoreBridge {
257268 }
258269
259270 // Watch for new posts
260 this.myFeed.on('append', async () => {
271 this._appendHandler = async () => {
261272 console.log('🔔 New post detected!')
262273 try {
263274 await this.push()
264275 } catch (err) {
265276 console.error('Failed to push:', err.message)
266277 }
267 })
278 }
279 this.myFeed.on('append', this._appendHandler)
268280
269281 // Periodic pull from relay
270 setInterval(async () => {
282 this._pullInterval = setInterval(async () => {
271283 console.log('⏰ Periodic pull from relay...')
272284 try {
273285 await this.pull()
……@@ -277,9 +289,27 @@ export class WirecoreBridge {
277289 }, pullInterval)
278290
279291 console.log('✅ Bridge daemon running\n')
292 }
280293
281 // Keep alive
282 await new Promise(() => {})
294 /**
295 * Stop daemon mode (cleanup intervals and listeners)
296 */
297 async stop() {
298 console.log('🛑 Stopping bridge daemon...')
299
300 // Clear periodic pull interval
301 if (this._pullInterval) {
302 clearInterval(this._pullInterval)
303 this._pullInterval = null
304 }
305
306 // Remove append event listener
307 if (this._appendHandler) {
308 this.myFeed.removeListener('append', this._appendHandler)
309 this._appendHandler = null
310 }
311
312 console.log('✅ Bridge daemon stopped')
283313 }
284314
285315 /**
……@@ -376,7 +406,8 @@ export class WirecoreBridge {
376406 */
377407 async close() {
378408 console.log('Closing bridge...')
379 if (this.store) {
409 // Only close the store if we created it (not shared with web server)
410 if (this.store && this._ownStore) {
380411 await this.store.close()
381412 }
382413 console.log('✅ Bridge closed')
pear-app/index.js +38 -40
……@@ -58,7 +58,7 @@ let userDrive = null // Hyperdrive for user files (images, etc.)
5858let identity = null // Legacy IdentityKey (will be removed in Phase 2)
5959let anprotoKeypair = null // ANProto keypair { publicKey: Buffer, secretKey: Buffer }
6060let follows = new Map() // discoveryKey -> { feed, publicKey, name }
61let bridgeProcess = null // Bridge daemon child process (for web UI management)
61let bridgeInstance = null // Bridge daemon instance (WirecoreBridge, for web UI management)
6262let bridgeConfig = { relayUrl: 'https://7rnx.net/relay', autoStart: false } // Bridge configuration
6363
6464/**
……@@ -1557,24 +1557,31 @@ async function followUser(publicKeyInput, name) {
15571557 * Get bridge status (running, stats, config)
15581558 */
15591559async function getBridgeStatus() {
1560 const { WirecoreBridge } = await import('./bridge/wirecore-bridge.js')
1561 const bridge = new WirecoreBridge(STORAGE_PATH, bridgeConfig.relayUrl)
1562
15631560 try {
1564 await bridge.init()
1565 const status = await bridge.status()
1566 await bridge.close()
1561 // If bridge is running, get its status
1562 if (bridgeInstance) {
1563 const status = await bridgeInstance.status()
1564 return {
1565 running: true,
1566 config: bridgeConfig,
1567 status
1568 }
1569 }
15671570
1571 // Bridge not running - return basic status without accessing storage
1572 // (accessing storage would cause ELOCKED if web server already has it open)
15681573 return {
1569 running: bridgeProcess !== null && !bridgeProcess.killed,
1570 pid: bridgeProcess ? bridgeProcess.pid : null,
1574 running: false,
15711575 config: bridgeConfig,
1572 status
1576 status: {
1577 relay: { online: null }, // Unknown when bridge not running
1578 sync: { pendingPosts: 0 },
1579 stats: { totalPushed: 0, totalPulled: 0 }
1580 }
15731581 }
15741582 } catch (err) {
15751583 return {
15761584 running: false,
1577 pid: null,
15781585 config: bridgeConfig,
15791586 error: err.message
15801587 }
……@@ -1582,55 +1589,44 @@ async function getBridgeStatus() {
15821589}
15831590
15841591/**
1585 * Start bridge daemon as child process
1592 * Start bridge daemon in-process
15861593 */
15871594async function startBridge() {
1588 if (bridgeProcess && !bridgeProcess.killed) {
1595 if (bridgeInstance) {
15891596 throw new Error('Bridge already running')
15901597 }
15911598
1592 const { spawn } = await import('bare-subprocess')
1599 console.log('🌉 Starting bridge daemon...')
1600 console.log(' Relay:', bridgeConfig.relayUrl)
15931601
1594 // Start bridge daemon
1595 const pullInterval = bridgeConfig.pullInterval || 300 // 5 minutes default
1596 bridgeProcess = spawn('pear', ['run', '.', 'bridge', 'start', '--interval', pullInterval.toString()], {
1597 stdio: 'inherit',
1598 cwd: process.cwd()
1599 })
1602 const { WirecoreBridge } = await import('./bridge/wirecore-bridge.js')
16001603
1601 bridgeProcess.on('exit', (code) => {
1602 console.log(`Bridge process exited with code ${code}`)
1603 bridgeProcess = null
1604 })
1604 // Pass existing Corestore instance to avoid ELOCKED error
1605 bridgeInstance = new WirecoreBridge(bridgeConfig.relayUrl, store, STORAGE_PATH)
16051606
1606 // Wait a moment to ensure it started
1607 await new Promise(resolve => setTimeout(resolve, 1000))
1607 await bridgeInstance.init()
16081608
1609 if (bridgeProcess.killed) {
1610 throw new Error('Bridge failed to start')
1611 }
1609 // Start in daemon mode (watch + periodic pull)
1610 const pullInterval = (bridgeConfig.pullInterval || 300) * 1000 // Convert seconds to ms
1611 await bridgeInstance.start(pullInterval)
16121612
1613 console.log('✅ Bridge daemon started (PID:', bridgeProcess.pid, ')')
1613 console.log('✅ Bridge daemon started')
16141614}
16151615
16161616/**
16171617 * Stop bridge daemon
16181618 */
16191619async function stopBridge() {
1620 if (!bridgeProcess || bridgeProcess.killed) {
1620 if (!bridgeInstance) {
16211621 throw new Error('Bridge not running')
16221622 }
16231623
1624 bridgeProcess.kill('SIGTERM')
1624 console.log('🌉 Stopping bridge daemon...')
16251625
1626 // Wait for graceful shutdown
1627 await new Promise(resolve => setTimeout(resolve, 2000))
1626 await bridgeInstance.stop()
1627 await bridgeInstance.close()
1628 bridgeInstance = null
16281629
1629 if (!bridgeProcess.killed) {
1630 bridgeProcess.kill('SIGKILL')
1631 }
1632
1633 bridgeProcess = null
16341630 console.log('✅ Bridge daemon stopped')
16351631}
16361632
……@@ -1639,7 +1635,9 @@ async function stopBridge() {
16391635 */
16401636async function syncBridge() {
16411637 const { WirecoreBridge } = await import('./bridge/wirecore-bridge.js')
1642 const bridge = new WirecoreBridge(STORAGE_PATH, bridgeConfig.relayUrl)
1638
1639 // Pass existing Corestore instance to avoid ELOCKED error
1640 const bridge = new WirecoreBridge(bridgeConfig.relayUrl, store, STORAGE_PATH)
16431641
16441642 await bridge.init()
16451643 const result = await bridge.sync()