# `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 `AbortController` short-circuits losing attempts. - Mandatory `verifyChunkSig` gate against the configured `serverPubKey`. - Bad-sig denylisting via `registry.recordBadSig`. - Non-signature failures update success / failure counters so ranking adapts. - `maxParallelRequests` global cap. ## Usage: Basic Example ```ts 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 ```ts 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`. - `inflight: number` (read-only) — in-flight request count. - `ParallelPullerOptions`: - `registry`, `transports`, `serverPubKey` (required). - `maxParallelRequests?` (default `8`). - `racingWindowMs?` (default `250`). - `topK?` (default `3`). - `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.