OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-store commits / 9f14f0a

0.0.22 (org #34): locate the head commit block, never synthesise it
Completes 0.0.21 — atproto-did-peer 0.2.11 already imports findCommitBlock, and it had not been committed. Caught by checking the tree instead of trusting the commit log. The obvious implementation hands dagCbor.encode(head.signed) to the assembler. That would also mean serving a CAR rooted at a commit the firehose does not carry — unwalkable even for a caller holding every other block. So the head block is LOCATED in the frames, newest first, and a head we cannot find is a REFUSAL rather than a block we invent. Two consequences: - unionDiffCar returns null instead of writing a CAR around bytes it was not given; repoDiffSince then locates or refuses. - a front needs no dag-cbor dependency to ask for a diff, which matters because atproto-did-peer has none and is a symlinked sibling (adding one is a deploy hazard there). Test: +3 tests / +8 asserts — the head found without being handed in, the EMPTY diff case (no frames to take it from, so it must scan), and a head no frame carries being refused. 79/79, 238 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9f14f0a2f736a95fe5e7b275b1fa14efb0dca18b
by plankatron <fleeky@gmail.com> · 2026-08-07T18:26:35.000Z

Changed files (3) +94 -10

lib/repo-diff.js +51 -9
……@@ -85,11 +85,10 @@ export async function collectFramesSince ({ getFrame, length, since }) {
8585
8686// Union the `blocks` CARs of the given #commit frames into ONE CAR rooted at `headCommit`.
8787//
88// Deduped by CID: a block touched by three commits appears once. The head commit block is added
89// explicitly rather than assumed present, because the front's VERIFIED head is the root we must serve
90// and it is not this function's job to trust that the last frame happens to match it — the caller
91// asserts that (see requireHead below).
92export async function unionDiffCar ({ frames, headCommitCid, headCommitBytes }) {
88// Deduped by CID: a block touched by three commits appears once. Returns null when the head block is
89// neither in the frames nor supplied — the caller must then LOCATE it (findCommitBlock) or refuse,
90// because a CAR rooted at a commit nobody carries cannot be walked by anyone.
91export async function unionDiffCar ({ frames, headCommitCid, headCommitBytes = null }) {
9392 const blocks = new Map() // cidStr -> { cid, bytes }
9493 for (const body of frames) {
9594 if (!body || !body.blocks) continue
……@@ -98,27 +97,62 @@ export async function unionDiffCar ({ frames, headCommitCid, headCommitBytes })
9897 for (const [cidStr, block] of car.blocks) if (!blocks.has(cidStr)) blocks.set(cidStr, block)
9998 }
10099 const headStr = headCommitCid.toString()
101 if (!blocks.has(headStr)) blocks.set(headStr, { cid: headCommitCid, bytes: headCommitBytes })
100 if (!blocks.has(headStr)) {
101 if (!headCommitBytes) return null // caller must locate it; see findCommitBlock
102 blocks.set(headStr, { cid: headCommitCid, bytes: headCommitBytes })
103 }
102104 const carBytes = await writeCar(headCommitCid, [...blocks.values()])
103105 return { carBytes, blocks: blocks.size }
104106}
105107
108// Find a commit block BY CID inside the firehose, newest frame first.
109//
110// ⛔ THE HEAD BLOCK IS NEVER SYNTHESISED. It would be easy to hand `dagCbor.encode(head.signed)` in from
111// the front and root the CAR at that — and it would also mean serving a CAR rooted at a commit the
112// firehose does not actually carry, which cannot be walked even by a caller holding everything else. So
113// the block is LOCATED in the frames instead. That the search can fail is the point: a head the firehose
114// never carried is a refusal, not a block we invent. It also removes any need for a dag-cbor dependency
115// in the front.
116export async function findCommitBlock ({ getFrame, length, cid, scanLimit = 64 }) {
117 const want = cid.toString()
118 const stop = Math.max(0, length - scanLimit)
119 for (let i = length - 1; i >= stop; i--) {
120 let d = null
121 try { d = decodeFrame(await getFrame(i)) } catch { continue }
122 if (!d || d.t !== '#commit' || !d.body || !d.body.blocks) continue
123 try {
124 const car = await readCar(d.body.blocks)
125 const hit = car.blocks.get(want)
126 if (hit) return hit
127 } catch { continue }
128 }
129 return null
130}
131
106132// The whole operation: firehose + a verified head -> a partial CAR, or an honest refusal.
107133//
108134// Returns { ok, carBytes, blocks, fromRev, toRev, reason }. `ok: false` means DO NOT SERVE A DIFF — the
109135// caller must fall back to the full CAR and report that `since` was not honoured. It never throws for a
110136// coverage problem, because "I cannot answer that" is a normal answer here, not an error.
111export async function repoDiffSince ({ getFrame, length, since, headCommitCid, headCommitBytes, headRev }) {
137export async function repoDiffSince ({ getFrame, length, since, headCommitCid, headCommitBytes = null, headRev }) {
112138 if (!since) return { ok: false, reason: 'no since given' }
113139 if (!length) return { ok: false, reason: 'firehose is empty — no history to diff from' }
114140 const { frames, covered, anchorRev } = await collectFramesSince({ getFrame, length, since })
115141 if (!covered) {
116142 return { ok: false, reason: 'firehose does not reach back to rev ' + since + ' — cannot prove the diff is complete' }
117143 }
144 // The head block, located in the firehose rather than supplied. `headCommitBytes` remains accepted so a
145 // caller that legitimately holds the bytes (a test, or a producer assembling its own commit) can pass
146 // them, but the normal path finds them.
147 const locate = async () => headCommitBytes
148 ? { cid: headCommitCid, bytes: headCommitBytes }
149 : await findCommitBlock({ getFrame, length, cid: headCommitCid })
118150 // `since` is at or ahead of our head: nothing has changed that the caller does not have. The correct
119151 // answer is a CAR carrying the commit alone — an EMPTY diff, which is a real answer and not an error.
120152 if (!frames.length) {
121 const car = await unionDiffCar({ frames: [], headCommitCid, headCommitBytes })
153 const head = await locate()
154 if (!head) return { ok: false, reason: 'the verified head commit is not carried by this firehose' }
155 const car = await unionDiffCar({ frames: [], headCommitCid, headCommitBytes: head.bytes })
122156 return { ok: true, ...car, fromRev: anchorRev, toRev: headRev || null, empty: true }
123157 }
124158 // The head we serve must be the head these frames build up to. If the front's verified head is NEWER
……@@ -129,6 +163,14 @@ export async function repoDiffSince ({ getFrame, length, since, headCommitCid, h
129163 if (headRev && newer(headRev, lastRev)) {
130164 return { ok: false, reason: 'verified head (' + headRev + ') is ahead of the firehose tail (' + lastRev + ')' }
131165 }
132 const car = await unionDiffCar({ frames, headCommitCid, headCommitBytes })
166 let car = await unionDiffCar({ frames, headCommitCid, headCommitBytes })
167 if (!car) {
168 // The frames did not carry the head block. Look for it before giving up — then refuse rather than
169 // synthesise one, because a CAR rooted at a commit this firehose never carried is unwalkable for
170 // everyone, including a caller holding every other block.
171 const head = await locate()
172 if (!head) return { ok: false, reason: 'the verified head commit is not carried by this firehose' }
173 car = await unionDiffCar({ frames, headCommitCid, headCommitBytes: head.bytes })
174 }
133175 return { ok: true, ...car, fromRev: anchorRev, toRev: lastRev, empty: false }
134176}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "atproto-store",
3 "version": "0.0.21",
3 "version": "0.0.22",
44 "description": "Holepunch-native atproto repo store — records in a classic Hyperbee (collection/rkey -> DAG-CBOR). The P2P home; the atproto-repo module is the bridge serializer over it.",
55 "type": "module",
66 "main": "lib/repo-store.js",
test/repo-diff.test.js +42 -0
……@@ -240,6 +240,48 @@ test('a head ahead of the firehose tail is REFUSED — the diff could not be wal
240240 t.ok(/ahead of the firehose tail/.test(d.reason), 'and says why: ' + d.reason)
241241})
242242
243// The head block is LOCATED in the firehose, never synthesised — with no headCommitBytes supplied.
244//
245// This is what lets a front ask for a diff without a dag-cbor dependency, and it is the stricter
246// behaviour: a head the firehose does not carry becomes a refusal rather than a CAR rooted at a commit
247// nobody can walk to.
248test('the head block is found in the firehose without being handed in', async (t) => {
249 const r = await newRepo(t)
250 const w1 = await r.write('a1', 'first')
251 const w2 = await r.write('a2', 'second')
252
253 const d = await repoDiffSince({ ...frameReader(r.seq), since: w1.rev, headCommitCid: w2.commitCid, headRev: w2.rev })
254 t.ok(d.ok, 'served with no headCommitBytes given')
255 const car = await readCar(d.carBytes)
256 t.ok(car.roots[0].equals(w2.commitCid), 'rooted at the head')
257 t.ok(car.blocks.has(w2.commitCid.toString()), 'and the head block is present, sourced from the frames')
258})
259
260test('an EMPTY diff also locates its head block (no frames to take it from)', async (t) => {
261 const r = await newRepo(t)
262 await r.write('a1', 'first')
263 const w2 = await r.write('a2', 'second')
264 // since == head: no frames qualify, so the head block cannot come from the union — it is found by
265 // scanning back through the firehose instead.
266 const d = await repoDiffSince({ ...frameReader(r.seq), since: w2.rev, headCommitCid: w2.commitCid, headRev: w2.rev })
267 t.ok(d.ok && d.empty, 'served as an empty diff')
268 const car = await readCar(d.carBytes)
269 t.is(car.blocks.size, 1, 'commit block only')
270 t.ok(car.roots[0].equals(w2.commitCid), 'rooted at the head it located')
271})
272
273test('a head the firehose never carried is refused, not synthesised', async (t) => {
274 const r = await newRepo(t)
275 const w1 = await r.write('a1', 'first')
276 const w2 = await r.write('a2', 'second')
277 // A commit that verifies as far as this function is concerned but whose block is in no frame. Its rev
278 // is NOT ahead of the tail, so it gets past that guard and must be caught by the locate step.
279 const orphan = { ...w2.signedCommit, rev: w1.rev }
280 const d = await repoDiffSince({ ...frameReader(r.seq), since: w1.rev, headCommitCid: commitCid(orphan), headRev: w1.rev })
281 t.absent(d.ok, 'refused')
282 t.ok(/not carried by this firehose/.test(d.reason), 'and says why: ' + d.reason)
283})
284
243285test('no since -> no diff (the caller must not get a partial CAR by accident)', async (t) => {
244286 const r = await newRepo(t)
245287 const w1 = await r.write('a1', 'first')