import type { IngestSegment } from '../ingest/segment.js' import type { ReleaseIdentity } from '../release/index.js' import type { AggregatedFile } from '../aggregate/index.js' /** * A complete file as a seeding node reports it (CONCEPT.md D37): what the * file, its release and its binary need, once per file, and the segments * as [part number, message-id, bytes]. */ export interface SeedFile { fileKey: string name: string poster: string subject: string totalParts: number groups: string[] /** Earliest post date of its articles, ISO 8601. */ postedAt: string | null release: ReleaseIdentity | null segments: [number, string, number][] } /** A segment of a file that didn't complete on the node, for the regular ingest path. */ export type SeedFragment = Omit & { postedAt?: string; articleNumber?: number } /** What a node sends per batch: the article it has reached, and what it found up to there. */ export interface SeedBatch { /** First article number not covered yet (everything below it, from the range start, is in this or earlier batches). */ next: number headers: number obfuscated: number deferred: number files: SeedFile[] fragments: SeedFragment[] } export function toSeedFile(file: AggregatedFile): SeedFile { const first = file.segments[0] const times = file.segments.map((s) => s.postedAt?.getTime()).filter((t): t is number => Number.isFinite(t)) return { fileKey: file.fileKey, name: first.name, poster: first.poster, subject: first.subject, totalParts: first.totalParts, groups: first.groups, postedAt: times.length > 0 ? new Date(Math.min(...times)).toISOString() : null, release: first.release ?? null, segments: file.segments.map((s) => [s.number, s.messageId, s.bytes]), } } export function toFragment(segment: IngestSegment): SeedFragment { const { postedAt, articleNumber, ...rest } = segment const date = postedAt && !Number.isNaN(postedAt.getTime()) ? postedAt.toISOString() : undefined return { ...rest, postedAt: date, articleNumber: articleNumber === undefined ? undefined : Number(articleNumber) } } export function fromFragment(fragment: SeedFragment): IngestSegment { // The JSON body parser turns empty strings into null; the columns want ''. return { ...fragment, poster: fragment.poster ?? '', subject: fragment.subject ?? '', name: fragment.name ?? '', postedAt: fragment.postedAt ? new Date(fragment.postedAt) : undefined, } } /** A file from a node's JSON, with empty strings back where the body parser made them null. */ export function normalizeSeedFile(file: SeedFile): SeedFile { return { ...file, name: file.name ?? '', poster: file.poster ?? '', subject: file.subject ?? '' } }