| … | … | @@ -85,11 +85,10 @@ export async function collectFramesSince ({ getFrame, length, since }) { |
| 85 | 85 | |
| 86 | 86 | // Union the `blocks` CARs of the given #commit frames into ONE CAR rooted at `headCommit`. |
| 87 | 87 | // |
| 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). |
| 92 | | export 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. |
| 91 | export async function unionDiffCar ({ frames, headCommitCid, headCommitBytes = null }) { |
| 93 | 92 | const blocks = new Map() // cidStr -> { cid, bytes } |
| 94 | 93 | for (const body of frames) { |
| 95 | 94 | if (!body || !body.blocks) continue |
| … | … | @@ -98,27 +97,62 @@ export async function unionDiffCar ({ frames, headCommitCid, headCommitBytes }) |
| 98 | 97 | for (const [cidStr, block] of car.blocks) if (!blocks.has(cidStr)) blocks.set(cidStr, block) |
| 99 | 98 | } |
| 100 | 99 | 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 | } |
| 102 | 104 | const carBytes = await writeCar(headCommitCid, [...blocks.values()]) |
| 103 | 105 | return { carBytes, blocks: blocks.size } |
| 104 | 106 | } |
| 105 | 107 | |
| 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. |
| 116 | export 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 | |
| 106 | 132 | // The whole operation: firehose + a verified head -> a partial CAR, or an honest refusal. |
| 107 | 133 | // |
| 108 | 134 | // Returns { ok, carBytes, blocks, fromRev, toRev, reason }. `ok: false` means DO NOT SERVE A DIFF — the |
| 109 | 135 | // caller must fall back to the full CAR and report that `since` was not honoured. It never throws for a |
| 110 | 136 | // coverage problem, because "I cannot answer that" is a normal answer here, not an error. |
| 111 | | export async function repoDiffSince ({ getFrame, length, since, headCommitCid, headCommitBytes, headRev }) { |
| 137 | export async function repoDiffSince ({ getFrame, length, since, headCommitCid, headCommitBytes = null, headRev }) { |
| 112 | 138 | if (!since) return { ok: false, reason: 'no since given' } |
| 113 | 139 | if (!length) return { ok: false, reason: 'firehose is empty — no history to diff from' } |
| 114 | 140 | const { frames, covered, anchorRev } = await collectFramesSince({ getFrame, length, since }) |
| 115 | 141 | if (!covered) { |
| 116 | 142 | return { ok: false, reason: 'firehose does not reach back to rev ' + since + ' — cannot prove the diff is complete' } |
| 117 | 143 | } |
| 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 }) |
| 118 | 150 | // `since` is at or ahead of our head: nothing has changed that the caller does not have. The correct |
| 119 | 151 | // answer is a CAR carrying the commit alone — an EMPTY diff, which is a real answer and not an error. |
| 120 | 152 | 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 }) |
| 122 | 156 | return { ok: true, ...car, fromRev: anchorRev, toRev: headRev || null, empty: true } |
| 123 | 157 | } |
| 124 | 158 | // 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 |
| 129 | 163 | if (headRev && newer(headRev, lastRev)) { |
| 130 | 164 | return { ok: false, reason: 'verified head (' + headRev + ') is ahead of the firehose tail (' + lastRev + ')' } |
| 131 | 165 | } |
| 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 | } |
| 133 | 175 | return { ok: true, ...car, fromRev: anchorRev, toRev: lastRev, empty: false } |
| 134 | 176 | } |