OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / dc8941a

port(0.3.0): backpressure P1 single-flight serve + revert #fs to pure Ok mirror
P1 (single-flight): Oks inline _onChunkNeed started a rival serve loop per concurrent chunkNeed frame from the same peer, interleaving sends + admitting N chunks/drain instead of honouring backpressure once (the discipline our 0.2.13 serialized pump had). Restored: per-peer single-flight queue (peer._serving/_serveQueue) draining one _serveJob at a time; _serveJob keeps the exact proven send+_waitForDrain logic. Verified: 2MB two-peer registerFile->fetchFile hash-verified under bare. Revert #fs delta: it did NOT achieve dual-runtime (transfer.js file I/O uses bare-fs-specific APIs node fs rejects), and pgh/daemons run under bare in prod anyway. lib/v3 stays a pure Ok mirror + only real deltas (getIndexMeta, P1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dc8941ad3aed0fafb64b8bc9ad88773ef4dc96c4
by plankatron <fleeky@gmail.com> · 2026-07-11T16:32:47.000Z

Changed files (3) +43 -20

lib/v3/overlay.js +3 -3
……@@ -21,9 +21,9 @@
2121 */
2222
2323import ReadyResource from 'ready-resource'
24import fs from '#fs'
25import path from '#path'
26import os from '#os'
24import fs from 'bare-fs'
25import path from 'bare-path'
26import os from 'bare-os'
2727import crypto from 'hypercore-crypto'
2828
2929import { FileIndex } from './file-index.js'
lib/v3/protocol.js +37 -14
……@@ -12,11 +12,11 @@
1212 * 4. Sender chunks on demand → streams chunks → receiver writes to disk
1313 */
1414
15import path from '#path'
15import path from 'bare-path'
1616import * as messages from './messages.js'
1717import { hashChunk, selectTier } from './chunker.js'
1818import { ChunkScheduler } from './chunk-scheduler.js'
19import fs from '#fs'
19import fs from 'bare-fs'
2020
2121const PROTOCOL = 'hyper-overlay/v2'
2222const VERSION = 2
……@@ -601,26 +601,49 @@ export class OverlayProtocol {
601601 : await fileIndex.getChunkMap(msg.path)
602602 if (!chunkMap) return
603603
604 for (let i = 0; i < msg.indices.length; i++) {
605 const index = msg.indices[i]
604 // Single-flight serve pump per peer (mirrors hypercore's `receiverBusy`).
605 // Enqueue this request; if no pump is running for this peer, drain the queue
606 // one job at a time. protomux dispatches every frame of a socket read
607 // synchronously, so two chunkNeed frames from the same peer would otherwise
608 // start rival serve loops — interleaving sends and admitting N chunks per
609 // drain instead of honouring backpressure once (the discipline our 0.2.13
610 // serialized pump had, which Ok's inline loop dropped).
611 if (!peer._serveQueue) peer._serveQueue = []
612 peer._serveQueue.push({ path: msg.path, diskPath, chunkMap, indices: msg.indices })
613 if (peer._serving) return
614 peer._serving = true
615 try {
616 while (peer._serveQueue.length) {
617 if (peer.channel && peer.channel.closed) { peer._serveQueue.length = 0; break }
618 try { await this._serveJob(peer, peer._serveQueue.shift()) } catch {}
619 }
620 } finally {
621 peer._serving = false
622 }
623 }
624
625 // Serve one chunkNeed job: stream the requested chunk indices, honouring
626 // stream backpressure — stop producing when send() reports the write buffer is
627 // full and resume on drain, so sibling protocols multiplexed on the same Noise
628 // stream (Corestore replication, app handshakes; protomux MAX_BACKLOG is
629 // Infinity) aren't starved. Runs under the per-peer single-flight guard in
630 // _onChunkNeed, so exactly one job streams at a time per peer.
631 async _serveJob (peer, job) {
632 const { path, diskPath, chunkMap, indices } = job
633 for (let i = 0; i < indices.length; i++) {
634 const index = indices[i]
606635 if (index >= chunkMap.length) continue
607636 const c = chunkMap[index]
608637 const data = this._transferManager.readChunk(diskPath, c.offset, c.length)
609638 if (!data) continue
610 const flushed = peer.msgs.chunkData.send({ path: msg.path, index, data })
639 const flushed = peer.msgs.chunkData.send({ path, index, data })
611640 if (this._chunkServeCb) {
612 try { this._chunkServeCb({ path: msg.path, index, bytes: data.length, peer, from: peer.authorizedServe.get(msg.path) || null }) } catch {}
641 try { this._chunkServeCb({ path, index, bytes: data.length, peer, from: peer.authorizedServe.get(path) || null }) } catch {}
613642 }
614 // Head-of-line blocking — chunkData and every other protocol the host
615 // multiplexes over the SAME Noise stream (Corestore replication, app
616 // handshakes) share one socket, and protomux's MAX_BACKLOG is Infinity.
617 // send() returns the stream's writable state; on backpressure, stop
618 // producing content and let the queue drain, else sibling protocols on
619 // the stream are starved for the rest of the download.
620 if (flushed === false && i < msg.indices.length - 1) {
643 if (flushed === false && i < indices.length - 1) {
621644 const alive = await this._waitForDrain(peer)
622645 if (!alive) return
623 if (this._serveAuthorizer && !peer.authorizedServe.has(msg.path)) return
646 if (this._serveAuthorizer && !peer.authorizedServe.has(path)) return
624647 }
625648 }
626649 }
lib/v3/transfer.js +3 -3
……@@ -16,9 +16,9 @@
1616 * No blob storage. Chunks exist in memory during transfer only.
1717 */
1818
19import fs from '#fs'
20import path from '#path'
21import os from '#os'
19import fs from 'bare-fs'
20import path from 'bare-path'
21import os from 'bare-os'
2222import c from 'compact-encoding'
2323import sodium from 'sodium-universal'
2424import { chunk as chunkBuffer, chunkStream, hashChunk, createStreamingHasher, selectTier, getTierParams } from './chunker.js'