OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git commits / 053b898

Fix federation sync: remove hard timeout, prep for Protomux data path
Root cause of failed hyper-tps sync: 120s timeout killed the git fetch mid-transfer. A 1.4 GB release takes ~7 min over LAN P2P. The timeout was killing it at ~400 MB, causing "unexpected disconnect while reading sideband packet". Changes: - Remove hard 120s timeout from _syncRepo. The P2P transport (SecretStream) has its own 45s inactivity timeout for hung connections. The sync just needs to wait for the transfer to complete. - Remove force-update (+refs/*) from refspec (prevents stale peers from reverting fresh refs) - Pass syncServer to handleRefUpdate for future Protomux data channel sync - Add sendSyncRequest to GitSyncServer (bidirectional capability) Future: replace git fetch subprocess with Protomux data channel piping (git-fetch-pack ↔ git-upload-pack through existing driveSwarm connection). The plumbing is in place, just needs the git-fetch-pack integration. Tagged pre-protomux-sync as rollback point. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
053b898fbab507b3bddca3b0a5a488e8ce999e4a
by plankatron <fleeky@gmail.com> · 2026-04-07T15:46:19.000Z

Changed files (3) +29 -19

lib/p2p/sync/git-sync-channel.js +11 -0
……@@ -198,6 +198,17 @@ export class GitSyncServer extends GitSyncBase {
198198 })
199199 }
200200
201 // Server can also request sync from the remote (bidirectional).
202 // Used when the client manager wants to pull data through the
203 // existing Protomux connection instead of spawning git fetch.
204 sendSyncRequest (repo, localRefs) {
205 this._controlMessage.send({
206 type: 'sync-request',
207 repo,
208 localRefs
209 })
210 }
211
201212 sendSyncError (repo, code, message) {
202213 this._controlMessage.send({
203214 type: 'sync-error',
lib/p2p/sync/git-sync-client-manager.js +15 -18
……@@ -63,7 +63,7 @@ export class GitSyncClientManager {
6363 * @param {object} remoteRefs — remote ref map { refName: sha, ... }
6464 * @param {string} peerKey — hex public key of the peer that sent the update
6565 */
66 async handleRefUpdate (repoName, remoteRefs, peerKey) {
66 async handleRefUpdate (repoName, remoteRefs, peerKey, syncServer) {
6767 if (this._destroyed) return
6868 if (this.serverKey && peerKey === this.serverKey) return // self-sync guard
6969
……@@ -106,7 +106,7 @@ export class GitSyncClientManager {
106106 if (diff.removed.length > 0) summary.push(`${diff.removed.length} removed`)
107107 console.log(`[GitSyncClient] Syncing ${repoName} from ${peerKey.slice(0, 8)} (${summary.join(', ')})`)
108108
109 const syncPromise = this._syncRepo(repoName, repoPath, peerKey)
109 const syncPromise = this._syncRepo(repoName, repoPath, peerKey, syncServer)
110110 .then(() => {
111111 console.log(`[GitSyncClient] Synced ${repoName} from ${peerKey.slice(0, 8)}`)
112112 })
……@@ -313,41 +313,38 @@ export class GitSyncClientManager {
313313 * @param {string} peerKey — hex public key of the peer to fetch from
314314 * @returns {Promise<void>}
315315 */
316 async _syncRepo (repoName, repoPath, peerKey) {
316 async _syncRepo (repoName, repoPath, peerKey, syncServer) {
317317 const fetchUrl = `punch://${peerKey}/${repoName}`
318318 console.log(`[GitSync] Fetching ${repoName} from ${peerKey.slice(0, 8)}...`)
319319
320320 await new Promise((resolve, reject) => {
321321 let done = false
322 // detached: true creates a new process group so we can kill all children
323322 const child = spawn('git', [
324323 '-C', repoPath, 'fetch', fetchUrl,
325 '+refs/*:refs/*', '--no-tags', '--prune'
324 'refs/*:refs/*', '--no-tags'
326325 ], { detached: true })
327326
328 const timer = setTimeout(() => {
329 if (done) return
330 done = true
331 // Kill the entire process group (git + git-remote-punch grandchild)
332 try { process.kill(-child.pid, 'SIGKILL') } catch {}
333 reject(new Error(`git fetch timeout after 120s for ${repoName}`))
334 }, 120000)
335
336 let stderr = ''
337 child.stderr.on('data', (d) => { stderr += d.toString() })
327 // No hard timeout — large repos (1+ GB) can take minutes.
328 // The P2P transport (SecretStream) has its own inactivity timeout
329 // (45s) which handles hung connections. We just need to wait for
330 // the transfer to complete, however long it takes.
338331 child.on('exit', (code) => {
339332 if (done) return
340333 done = true
341 clearTimeout(timer)
342334 if (code === 0) resolve()
343 else reject(new Error(`git fetch exit ${code}: ${stderr.trim()}`))
335 else {
336 const stderr = stderrBuf.join('')
337 reject(new Error(`git fetch exit ${code}: ${stderr.trim()}`))
338 }
344339 })
345340 child.on('error', (err) => {
346341 if (done) return
347342 done = true
348 clearTimeout(timer)
349343 reject(err)
350344 })
345
346 const stderrBuf = []
347 child.stderr.on('data', (d) => { stderrBuf.push(d.toString()) })
351348 })
352349
353350 // Refresh repo metadata (commits, branches, lastCommit) so the UI stays current
lib/p2p/sync/git-sync-manager.js +3 -1
……@@ -92,7 +92,9 @@ export class GitSyncManager {
9292 if (this.serverKey && syncKey === this.serverKey) return // self-sync guard
9393 console.log(`[GitSync] Received ref-update from ${syncKey.slice(0, 8)} for ${repo}`)
9494 if (this.clientManager) {
95 this.clientManager.handleRefUpdate(repo, refs, syncKey).catch(err => {
95 // Pass the Protomux server so sync can use the existing connection
96 // instead of spawning a git fetch subprocess
97 this.clientManager.handleRefUpdate(repo, refs, syncKey, server).catch(err => {
9698 console.error(`[GitSync] Client sync error for ${repo}: ${err.message}`)
9799 })
98100 }