4.6 KiB
parallel-puller.ts
(no submodules)
Purpose
ParallelPuller is the chunk-fetch scheduler that turns "I want chunk
N of file F" into a verified Uint8Array. The puller asks the
PeerRegistry for ranked candidates, issues a request to the best
peer, waits racingWindowMs, then issues parallel requests to the
runners-up. The first response whose server signature verifies wins;
the other in-flight attempts are aborted via a shared
AbortController.
The puller is the mesh's primary trust enforcement point. Every byte
returned from a transport flows through verifyChunkSig before the
puller acknowledges success. A signature failure moves the offending
peer to the registry's denylist with score = -Infinity and surfaces
DriveMeshError(SIG_VERIFY_FAILED) — the orchestrator can choose to
fall back to the server rather than retry on peers.
Features
- Top-K candidate selection (default 3).
- Racing-window staggered issuance (default 250 ms).
- Shared
AbortControllershort-circuits losing attempts. - Mandatory
verifyChunkSiggate against the configuredserverPubKey. - Bad-sig denylisting via
registry.recordBadSig. - Non-signature failures update success / failure counters so ranking adapts.
maxParallelRequestsglobal cap.
Usage: Basic Example
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
const puller = new ParallelPuller({
registry,
transports: [httpsTransport],
serverPubKey,
});
const bytes = await puller.pull("file-id-hex", 0);
Usage: Advanced Example
import { ParallelPuller } from "@suaveplan/drive-mesh/scheduler/parallel-puller";
const puller = new ParallelPuller({
registry,
transports: [webrtcTransport, httpsTransport],
serverPubKey,
racingWindowMs: 150,
topK: 4,
maxParallelRequests: 16,
clock: fakeClock,
onAttempt: (peerId, fileId, chunkId) =>
log.debug("attempt", peerId, fileId, chunkId),
onSuccess: (peerId, _f, _c, bytes, elapsedMs) =>
log.info("served", peerId, bytes, elapsedMs),
onBadSig: peerId => log.warn("denylisting", peerId),
});
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
try {
const bytes = await puller.pull(fileId, chunkId, controller.signal);
} catch (err) {
if (err instanceof DriveMeshError && err.code === DRIVE_MESH_CODES.ABORTED) {
// caller-driven cancellation
}
}
API Reference
class ParallelPuller:constructor(options: ParallelPullerOptions).pull(fileId, chunkId, signal?): Promise<Uint8Array>.inflight: number(read-only) — in-flight request count.
ParallelPullerOptions:registry,transports,serverPubKey(required).maxParallelRequests?(default8).racingWindowMs?(default250).topK?(default3).clock?.onAttempt?(peerId, fileId, chunkId).onSuccess?(peerId, fileId, chunkId, bytes, elapsedMs).onFailure?(peerId, fileId, chunkId, reason).onBadSig?(peerId, fileId, chunkId).
- Throws:
DRIVE_MESH.CHUNK_UNAVAILABLE— no candidate peers.DRIVE_MESH.SIG_VERIFY_FAILED— winning peer returned bad sig.DRIVE_MESH.TRANSPORT_FAILED— single-attempt transport failure.DRIVE_MESH.ALL_PEERS_FAILED— every candidate failed.DRIVE_MESH.ABORTED— caller-provided signal aborted.
Implementation Notes
firstSettled is hand-rolled rather than using Promise.any:
Promise.any resolves on first success but does NOT abort losing
attempts, leaving them in flight wastefully. The shared
AbortController is the cancellation primitive — controller.abort()
fires both when a winner appears and when the outer caller aborts.
SIG_VERIFY_FAILED short-circuits the "all peers failed" wrapping:
the orchestrator distinguishes "every peer failed transport" (try
server fallback) from "winning peer was Byzantine" (denylist + retry
on a different peer). Re-wrapping the bad-sig error inside
ALL_PEERS_FAILED would lose that distinction.
maxParallelRequests is enforced by a yielding micro-loop in
#awaitSlot. The loop is bounded by the number of concurrent pull
calls — drive-mesh issues pull from a small, controlled set of
async contexts so the loop never spins indefinitely. Under heavier
fan-in the orchestrator should queue requests in front of the puller
rather than issuing every chunk concurrently.
Bad-sig denylist is permanent within the registry's denylist scope:
session (default) clears on mesh.stop(), persistent survives.
The puller does not concern itself with scope — the registry is the
authority.