OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git commits / 8422871

Autobase-driven ref sync — deploy events replicate via Hypercore
Replaces unreliable Protomux broadcast + subprocess git fetch with Autobase-replicated ref-update events. Like Hypercore replication but for git refs — append-only, conflict-free, automatic. Flow: 1. Deploy completes → append ref-update to MetadataBase Autobase 2. Autobase replicates to all peers via driveSwarm (automatic, tiny) 3. Peer receives ref-update event → targeted delta fetch via punch:// 4. Monotonic version numbers prevent stale reverts (higher version wins) Changes: - deploy.js: add ref-update operation type with version monotonicity - deploy-hook.js: append ref-update event after successful deploy - pear-git-server.js: watch for ref-update events, trigger delta fetch The Protomux broadcast is kept as a fast-path notification alongside the authoritative Autobase event. Tagged pre-protomux-sync as rollback point. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8422871f6acfd349bbb32d891bacfc1adf989791
by plankatron <fleeky@gmail.com> · 2026-04-07T17:01:08.000Z

Changed files (3) +72 -2

lib/apply/deploy.js +22 -0
……@@ -54,6 +54,28 @@ export async function applyDeployOp (op, batch, base) {
5454 break
5555 }
5656
57 case 'ref-update': {
58 // Store latest ref value per repo+ref path.
59 // Autobase linearization ensures last-writer-wins semantics.
60 // Federation peers watch this key to know when to sync.
61 const refKey = `ref:${op.repoName}:${op.ref}`
62 const existing = await batch.get(refKey)
63 const existingVersion = existing?.value?.version || 0
64
65 // Only apply if version is higher (monotonic — prevents stale reverts)
66 if (op.version > existingVersion) {
67 await batch.put(refKey, {
68 repoName: op.repoName,
69 ref: op.ref,
70 sha: op.sha,
71 version: op.version,
72 serverKey: op.serverKey || null,
73 timestamp: op.timestamp || 0
74 })
75 }
76 break
77 }
78
5779 case 'add-writer': {
5880 if (op.writerKey) {
5981 try {
lib/ci/deploy-hook.js +22 -2
……@@ -252,10 +252,30 @@ async function _executeGitDeploy (server, repo, repoName, config, timestamp, opt
252252
253253 console.log(`[Deploy] Complete: ${repoName} ${commitHash.slice(0, 8)} in ${duration}ms`)
254254
255 // Broadcast ref-update over existing Protomux channels (instant, no new connection)
255 // Append ref-update to MetadataBase Autobase — replicated automatically
256 // to all federation peers. Monotonic version prevents stale reverts.
257 if (server.metadataBase && server.metadataBase.writable) {
258 try {
259 await server.metadataBase.append({
260 ns: 'deploy',
261 type: 'ref-update',
262 repoName,
263 ref: 'refs/heads/release',
264 sha: commitHash,
265 version,
266 serverKey: server.serverIdentity ? server.serverIdentity.getPublicKeyHex() : null,
267 timestamp
268 })
269 console.log(`[Deploy] Ref-update v${version} appended to Autobase`)
270 } catch (err) {
271 console.error(`[Deploy] Autobase ref-update failed: ${err.message}`)
272 }
273 }
274
275 // Broadcast ref-update over Protomux (fast-path notification)
256276 _broadcastRefs(server, repoName)
257277
258 // Also notify via browse protocol (for peers not on driveSwarm)
278 // Also notify via browse protocol (fallback for peers not on driveSwarm)
259279 notifyPeersOfDeploy(server, repoName).catch(err => {
260280 console.error(`[Deploy] Peer notification error: ${err.message}`)
261281 })
pear-git-server.js +28 -0
……@@ -336,6 +336,34 @@ class PearGitServer {
336336 })
337337 console.log('[Facades] Identity, Deploy, Groups initialized on unified MetadataBase')
338338
339 // Watch for ref-update events from federation peers.
340 // When a remote server deploys, the ref-update propagates via Autobase
341 // replication (automatic, reliable, tiny). We then fetch just the delta.
342 const selfKey = this.serverIdentity ? this.serverIdentity.getPublicKeyHex() : null
343 this.metadataBase.on('deploy', async (op) => {
344 if (op.type !== 'ref-update') return
345 if (op.serverKey === selfKey) return // self-deploy
346 const repoPath = path.join(this.repoRegistry.gitReposPath, op.repoName + '.git')
347 try { await fs.promises.stat(repoPath) } catch { return }
348 console.log(`[RefSync] ${op.repoName} v${op.version} from ${(op.serverKey || '').slice(0, 8)}`)
349 const fetchUrl = `punch://${op.serverKey}/${op.repoName}`
350 try {
351 const child = subprocess.spawn('git', [
352 '-C', repoPath, 'fetch', '--filter=blob:none', fetchUrl, `${op.ref}:${op.ref}`
353 ], { stdio: ['ignore', 'pipe', 'pipe'] })
354 const result = await new Promise((resolve) => {
355 let stderr = ''
356 child.stderr.on('data', (d) => { stderr += d.toString() })
357 child.on('exit', (code) => resolve({ code, stderr }))
358 child.on('error', () => resolve({ code: -1, stderr: 'spawn error' }))
359 })
360 if (result.code === 0) console.log(`[RefSync] ${op.repoName}: synced to ${op.sha.slice(0, 12)}`)
361 else console.error(`[RefSync] ${op.repoName}: failed — ${result.stderr.split('\n').pop()}`)
362 } catch (err) {
363 console.error(`[RefSync] ${op.repoName}: ${err.message}`)
364 }
365 })
366
339367 // Now that identity facade is ready, initialize auth
340368 this.authManager = new AuthManager({
341369 registry: this.registry,