import { decodeSegment, YencCrcError, YencError, type DecodedSegment } from '../yenc/index.js' import type { VolumeReader } from './walk.js' /** Downloads one article body (on a pooled connection, or through the seed API's relay). */ export type BodyFetcher = (messageId: string) => Promise /** * Reads volumes (each a list of segment message ids, in order) a segment at * a time, finding the segment for an offset from the part size (yEnc * posters use one size per set) and correcting from the `=ypart` offsets * when a guess is off. Stops reading after `maxSegments` downloads. */ export class SegmentReader implements VolumeReader { fetched = 0 private partSize = 0 private readonly sizes = new Map() private readonly cache = new Map() constructor( private readonly files: { id: string }[][], private readonly maxSegments: number, private readonly fetchBody: BodyFetcher ) {} get volumes() { return this.files.length } size(v: number) { return this.sizes.get(v) ?? null } async read(v: number, offset: number, more: boolean): Promise { const count = this.files[v].length let index = this.partSize ? Math.min(count - 1, Math.floor(offset / this.partSize)) : 0 for (let tries = 0; tries < 4; tries++) { const segment = await this.segment(v, index) if (!segment) return null const end = segment.offset + segment.data.length if (offset >= segment.offset && offset < end) { const data = segment.data.subarray(offset - segment.offset) if (!more || index + 1 >= count) return data const next = await this.segment(v, index + 1) return next ? Buffer.concat([data, next.data]) : null } const step = Math.floor((offset - segment.offset) / Math.max(1, segment.data.length)) const moved = Math.max(0, Math.min(count - 1, index + (step === 0 ? (offset < segment.offset ? -1 : 1) : step))) if (moved === index) return null index = moved } return null } private async segment(v: number, index: number): Promise { const key = `${v}:${index}` const cached = this.cache.get(key) if (cached) return cached const part = this.files[v][index] if (!part?.id || this.fetched >= this.maxSegments) return null this.fetched++ let body: Buffer try { body = await this.fetchBody(part.id) } catch { // A missing article (or the relay failing) ends the walk with what was found. return null } let segment: DecodedSegment try { segment = decodeSegment(body) } catch (error) { if (!(error instanceof YencError) || error instanceof YencCrcError) return null segment = decodeSegment(body, { lenientSize: true }) } this.sizes.set(v, segment.meta.begin.size) if (!this.partSize && segment.meta.part) this.partSize = segment.meta.part.end - segment.meta.part.begin + 1 // Walks move forward: the last few segments are enough. if (this.cache.size >= 4) this.cache.delete(this.cache.keys().next().value!) this.cache.set(key, segment) return segment } }