import { constants, zstdCompressSync, zstdDecompressSync } from 'node:zlib' /** A file of a release chunk: what an NZB needs, once per file, and its segments as [part, message-id, bytes]. */ export interface ChunkFile { name: string poster: string subject: string /** Earliest post date of its articles, ms since 1970 (null when no article had a valid date). */ postedAt: number | null groups: string[] segments: [number, string, number][] } const VERSION = 1 /** * A release chunk's payload (REBUILD.md, chunk store): a version byte, then * zstd of a JSON line with the files (name, poster, subject, date, groups, * sizes, and part numbers unless they are 1..N) followed by one message-id * per line, in file order. Message-ids are mostly random, so this is close * to what plain compression gets (LOG.md 2026-10-08 (3)). */ export function encodeChunk(files: ChunkFile[]): Buffer { const head = files.map((f) => { const sequential = f.segments.every(([n], i) => n === i + 1) return { n: f.name, p: f.poster, j: f.subject, d: f.postedAt, g: f.groups, s: f.segments.map(([, , b]) => b), ...(sequential ? {} : { k: f.segments.map(([n]) => n) }) } }) const ids = files.flatMap((f) => f.segments.map(([, id]) => id)) const text = Buffer.from([JSON.stringify(head), ...ids].join('\n'), 'utf8') return Buffer.concat([Buffer.from([VERSION]), zstdCompressSync(text, { params: { [constants.ZSTD_c_compressionLevel]: 3 } })]) } export function decodeChunk(data: Buffer): ChunkFile[] { if (data[0] !== VERSION) throw new Error(`Unknown chunk version ${data[0]}`) const [head, ...ids] = zstdDecompressSync(data.subarray(1)).toString('utf8').split('\n') let at = 0 return (JSON.parse(head) as any[]).map((f) => ({ name: f.n, poster: f.p, subject: f.j, postedAt: f.d, groups: f.g, segments: (f.s as number[]).map((bytes, i) => [f.k?.[i] ?? i + 1, ids[at++], bytes] as [number, string, number]), })) }