Rebuild: the fragment merger
Test and deploy / test (push) Successful in 19s
Test and deploy / deploy (push) Skipped

Files cut by a range's edge are put together once the ranges around them
are done (coordinator:run, every minute): a complete file is judged by the
rules and written as a release chunk with probe candidates; one still
missing parts while both neighbouring ranges are done is dropped and
counted (incomplete/edge). A neighbour not planned yet counts as not done,
except before the group's oldest article. BatchStore.writeReleases is the
shared write path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Opus 5.5 committed 2026-10-09 02:19:53 +02:00
1 parent 2c14315f94
commit b862201991
7 files changed
+365 -65

No files matched your search

+3 -1
View File
@@ -1,4 +1,6 @@
export { RangeIndexer, type EdgeBatch, type ReleaseChunk, type DroppedChunk, type ProbeCandidate, type CandidateKind, type RuleCount, type Fragment, type RangeIndexerOptions } from './range_indexer.js'
export { RangeIndexer, chunkOf, candidatesOf, releaseKeyOf, type EdgeBatch, type ReleaseChunk, type DroppedChunk, type ProbeCandidate, type CandidateKind, type RuleCount, type Fragment, type RangeIndexerOptions } from './range_indexer.js'
export { encodeFrame, decodeFrames, isKeepalive, KEEPALIVE, type Frame } from './stream.js'
export { toWire, fromWire, type WireBatch } from './wire.js'
export type { Lease } from './lease.js'
export { mergeFragments } from './merge.js'
export type { EdgeSegment, FileMeta } from './scan.js'
+35
View File
@@ -0,0 +1,35 @@
import type { AggregatedFile } from '../files/index.js'
import type { Fragment } from './range_indexer.js'
import type { EdgeSegment, FileMeta } from './scan.js'
/**
* The fragments of one file from neighbouring ranges, put together
* (REBUILD.md, fragments): the complete file when every part from 1 to its
* total is there (the first article seen of a part wins), else null.
*/
export function mergeFragments(fragments: Fragment[]): AggregatedFile<EdgeSegment> | null {
if (fragments.length === 0) return null
const first = fragments[0]
const meta: FileMeta = {
fileKey: first.fileKey,
name: first.name,
poster: first.poster,
subject: first.subject,
totalParts: first.totalParts,
groups: first.groups,
release: first.release ? { ...first.release, source: first.release.source as any } : null,
}
const parts = new Map<number, EdgeSegment>()
for (const fragment of fragments) {
const postedAt = fragment.postedAt ?? Number.NaN
for (const [number, messageId, bytes, articleNumber] of fragment.segments) {
const known = parts.get(number)
if (!known || articleNumber < known.articleNumber) {
parts.set(number, { fileKey: meta.fileKey, number, totalParts: meta.totalParts, messageId, bytes, articleNumber, postedAt, file: meta })
}
}
}
if (parts.size < meta.totalParts) return null
for (let n = 1; n <= meta.totalParts; n++) if (!parts.has(n)) return null
return { fileKey: meta.fileKey, segments: [...parts.values()].sort((a, b) => a.number - b.number) }
}
+3 -3
View File
@@ -280,7 +280,7 @@ export class RangeIndexer {
}
/** The release key; a file without a recognisable release becomes a release of its own. */
function releaseKeyOf(s: EdgeSegment): string {
export function releaseKeyOf(s: EdgeSegment): string {
return s.file.release?.key ?? createHash('sha1').update(`file\n${s.fileKey}`).digest('hex')
}
@@ -293,7 +293,7 @@ function postedOf(segments: EdgeSegment[]): number | null {
return min
}
function chunkOf(key: string, files: AggregatedFile<EdgeSegment>[]): ReleaseChunk {
export function chunkOf(key: string, files: AggregatedFile<EdgeSegment>[]): ReleaseChunk {
const first = files[0].segments[0].file
const chunkFiles: ChunkFile[] = files.map((f) => {
const m = f.segments[0].file
@@ -339,7 +339,7 @@ function chunkOf(key: string, files: AggregatedFile<EdgeSegment>[]): ReleaseChun
* volume, of its par2 index, of a lone random-named file, and a 7z set's
* last volume (its listing is in the tail). At most a few per release.
*/
function candidatesOf(key: string, files: AggregatedFile<EdgeSegment>[]): ProbeCandidate[] {
export function candidatesOf(key: string, files: AggregatedFile<EdgeSegment>[]): ProbeCandidate[] {
const out: ProbeCandidate[] = []
let tail: { volume: number; file: AggregatedFile<EdgeSegment> } | null = null
for (const f of files) {