OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git commits / f77dce7

Add backpressure to P2P git transport, fix undefined repo registration
Backpressure: all three bidirectional proxy paths (git-p2p-handler, git-remote-punch, git-sync-pipe) ignored write() return values. For large repos (1+ GB like hyper-tps), data arrived faster than the P2P connection could consume, overflowing buffers and causing "unexpected disconnect while reading sideband packet" / "bad pack header" errors. Fix: check write() return value, pause source on false, resume on drain. Remove 45s/60s connection timeouts that killed large transfers mid-flight. Undefined repo: _watchMetadataRepos called register(string, object) but register() takes a single object with .name — created repo:undefined in Hyperbee, announced as "undefined" on DHT. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
f77dce75a709bf462f509547b4940a724f945c4f
by plankatron <fleeky@gmail.com> · 2026-04-07T21:07:12.000Z

Changed files (4) +41 -10

bin/git-remote-punch +17 -5
……@@ -160,9 +160,9 @@ async function handleConnect(service, serverKey, repoName, httpsUrl, stdout, std
160160 process.stdin.removeAllListeners('data')
161161 process.stdin.removeAllListeners('end')
162162
163 // SecretStream has built-in timeout that auto-refreshes on incoming data.
164 // If no data arrives within 45s, it calls destroy() which fires 'close'.
165 conn.setTimeout(45000)
163 // No connection timeout — large repos (1+ GB) can transfer for minutes.
164 // Backpressure keeps data flowing; stale connections are detected by
165 // the stream's error/close events.
166166
167167 // Tell git we're ready FIRST (before any data forwarding)
168168 // This ensures '\n' precedes ref advertisement on stdout
……@@ -182,9 +182,18 @@ async function handleConnect(service, serverKey, repoName, httpsUrl, stdout, std
182182 // streamx pipe() unconditionally calls dest.end() when source ends,
183183 // and SecretStream._final() closes both directions of the noise stream.
184184 // This kills push (server can't send status after client sends pack).
185 //
186 // Backpressure: when write() returns false, pause the source.
187 // Resume on 'drain'. Without this, large repos overflow the send
188 // buffer and cause "unexpected disconnect" / "bad pack header".
185189 let stdinEnded = false
186190 process.stdin.on('data', (chunk) => {
187 conn.write(chunk)
191 if (!conn.write(chunk)) {
192 process.stdin.pause()
193 }
194 })
195 conn.on('drain', () => {
196 if (!stdinEnded) process.stdin.resume()
188197 })
189198
190199 process.stdin.on('end', () => {
……@@ -196,8 +205,11 @@ async function handleConnect(service, serverKey, repoName, httpsUrl, stdout, std
196205 })
197206
198207 conn.on('data', (chunk) => {
199 stdout.write(chunk)
208 if (!stdout.write(chunk)) {
209 conn.pause()
210 }
200211 })
212 stdout.on('drain', () => conn.resume())
201213
202214 conn.on('close', () => cleanup(0))
203215
lib/p2p/protocol/git-p2p-handler.js +15 -4
……@@ -54,8 +54,9 @@ export class GitP2PHandler {
5454 const connId = peerInfo.publicKey.toString('hex').slice(0, 8)
5555 console.log(`[GitP2P] Connection from ${connId}`)
5656
57 // Auto-destroy stale connections — SecretStream refreshes on incoming data
58 if (typeof conn.setTimeout === 'function') conn.setTimeout(60000)
57 // No connection-level timeout — git transfers can run for minutes on large
58 // repos. Backpressure keeps data flowing; connection drops are detected by
59 // the stream's built-in error/close events.
5960
6061 let headerReceived = false
6162 let buffer = Buffer.alloc(0)
……@@ -789,10 +790,17 @@ export class GitP2PHandler {
789790 // Manual bidirectional proxy instead of .pipe().
790791 // Explicit resume() after adding data listener — removing the
791792 // headerHandler may leave the noise stream paused in streamx.
793 //
794 // Backpressure: when write() returns false, pause the source.
795 // Resume on 'drain'. Without this, large repos (1+ GB) overflow
796 // the send buffer and cause "unexpected disconnect" / "bad pack header".
792797 conn.on('data', (chunk) => {
793798 console.log(`[GitP2P] conn->git: ${chunk.length} bytes`)
794 gitProcess.stdin.write(chunk)
799 if (!gitProcess.stdin.write(chunk)) {
800 conn.pause()
801 }
795802 })
803 gitProcess.stdin.on('drain', () => conn.resume())
796804
797805 let firstChunk = true
798806 gitProcess.stdout.on('data', (chunk) => {
……@@ -803,8 +811,11 @@ export class GitP2PHandler {
803811 console.log(`[GitP2P] REFS: ${preview}`)
804812 firstChunk = false
805813 }
806 conn.write(chunk)
814 if (!conn.write(chunk)) {
815 gitProcess.stdout.pause()
816 }
807817 })
818 conn.on('drain', () => gitProcess.stdout.resume())
808819
809820 // Ensure conn is in flowing mode after headerHandler was removed
810821 if (typeof conn.resume === 'function') conn.resume()
lib/p2p/sync/git-sync-pipe.js +7 -0
……@@ -37,11 +37,18 @@ export function createGitPipe (service, repoPath, dataEntry, opts = {}) {
3737 const stderrChunks = []
3838
3939 // -- git stdout → channel.message.send() --
40 // NOTE: Protomux message.send() does not return a backpressure signal.
41 // The underlying mux stream handles buffering. If the remote can't keep
42 // up, data accumulates in the mux write buffer. For very large transfers,
43 // the direct punch:// path (git-p2p-handler) should be preferred over
44 // this Protomux path.
4045 child.stdout.on('data', (chunk) => {
4146 dataEntry.message.send(chunk)
4247 })
4348
4449 // -- channel data → git stdin (wire up onmessage) --
50 // Protomux delivers messages synchronously — we can't pause delivery,
51 // but we can let Node/Bare buffer in git's stdin pipe.
4552 dataEntry.message.onmessage = (data) => {
4653 if (child.stdin.writable !== false) {
4754 child.stdin.write(data)
pear-git-server.js +2 -1
……@@ -583,7 +583,8 @@ class PearGitServer {
583583 console.log(`[Metadata] Created new repo from peer: ${remote.id}`)
584584
585585 // Register it
586 await this.repoRegistry.register(remote.id, {
586 await this.repoRegistry.register({
587 name: remote.id,
587588 description: remote.description,
588589 visibility: remote.visibility
589590 })