diff --git a/src/pds.js b/src/pds.js index 8b5ebf9..826870d 100644 --- a/src/pds.js +++ b/src/pds.js @@ -558,35 +558,45 @@ class MST { return null } - // Build entries with pre-computed depths + // Build entries with pre-computed depths (heights) + // In ATProto MST, "height" determines which layer a key belongs to + // Layer 0 is at the BOTTOM, root is at the highest layer const entries = [] + let maxDepth = 0 for (const r of records) { const key = `${r.collection}/${r.rkey}` + const depth = await getKeyDepth(key) + maxDepth = Math.max(maxDepth, depth) entries.push({ key, keyBytes: new TextEncoder().encode(key), cid: r.cid, - depth: await getKeyDepth(key) + depth }) } - return this.buildTree(entries, 0) + // Start building from the root (highest layer) going down to layer 0 + return this.buildTree(entries, maxDepth) } async buildTree(entries, layer) { if (entries.length === 0) return null - // Separate entries for this layer vs deeper layers + // Separate entries for this layer vs lower layers (subtrees) + // Keys with depth == layer stay at this node + // Keys with depth < layer go into subtrees (going down toward layer 0) const thisLayer = [] let leftSubtree = [] for (const entry of entries) { - if (entry.depth > layer) { + if (entry.depth < layer) { + // This entry belongs to a lower layer - accumulate for subtree leftSubtree.push(entry) } else { - // Process accumulated left subtree + // This entry belongs at current layer (depth == layer) + // Process accumulated left subtree first if (leftSubtree.length > 0) { - const leftCid = await this.buildTree(leftSubtree, layer + 1) + const leftCid = await this.buildTree(leftSubtree, layer - 1) thisLayer.push({ type: 'subtree', cid: leftCid }) leftSubtree = [] } @@ -596,7 +606,7 @@ class MST { // Handle remaining left subtree if (leftSubtree.length > 0) { - const leftCid = await this.buildTree(leftSubtree, layer + 1) + const leftCid = await this.buildTree(leftSubtree, layer - 1) thisLayer.push({ type: 'subtree', cid: leftCid }) } @@ -621,11 +631,12 @@ class MST { const prefixLen = commonPrefixLen(prevKeyBytes, keyBytes) const keySuffix = keyBytes.slice(prefixLen) + // ATProto requires t field to be present (can be null) const e = { p: prefixLen, k: keySuffix, v: new CID(cidToBytes(item.entry.cid)), - t: null // Always include t field (set later if subtree exists) + t: null // Will be updated if there's a subtree } node.e.push(e) @@ -633,7 +644,7 @@ class MST { } } - // Always include left pointer (can be null) + // ATProto requires l field to be present (can be null) node.l = leftCid ? new CID(cidToBytes(leftCid)) : null // Encode node with proper MST CBOR format @@ -675,7 +686,7 @@ function cborEncodeMstNode(node) { for (const item of val) encode(item) } else if (typeof val === 'object') { // Sort keys for deterministic encoding (DAG-CBOR style) - // Include null values, only exclude undefined + // Include null values (ATProto MST requires l and t fields even when null) const keys = Object.keys(val).filter(k => val[k] !== undefined) keys.sort((a, b) => { // DAG-CBOR: sort by length first, then lexicographically @@ -1420,12 +1431,65 @@ export class PersonalDataServer { if (commits.length === 0) { return Response.json({ error: 'repo not found' }, { status: 404 }) } - const blocks = this.sql.exec(`SELECT cid, data FROM blocks`).toArray() - const blocksForCar = blocks.map(b => ({ - cid: b.cid, - data: new Uint8Array(b.data) - })) - const car = buildCarFile(commits[0].cid, blocksForCar) + + // Only include blocks reachable from the current commit + const commitCid = commits[0].cid + const neededCids = new Set() + + // Helper to get block data + const getBlock = (cid) => { + const rows = this.sql.exec(`SELECT data FROM blocks WHERE cid = ?`, cid).toArray() + return rows.length > 0 ? new Uint8Array(rows[0].data) : null + } + + // Collect all reachable blocks starting from commit + const collectBlocks = (cid) => { + if (neededCids.has(cid)) return + neededCids.add(cid) + + const data = getBlock(cid) + if (!data) return + + // Decode CBOR to find CID references + try { + const decoded = cborDecode(data) + if (decoded && typeof decoded === 'object') { + // Commit object - follow 'data' (MST root) + if (decoded.data instanceof Uint8Array) { + collectBlocks(cidToString(decoded.data)) + } + // MST node - follow 'l' and entries' 'v' and 't' + if (decoded.l instanceof Uint8Array) { + collectBlocks(cidToString(decoded.l)) + } + if (Array.isArray(decoded.e)) { + for (const entry of decoded.e) { + if (entry.v instanceof Uint8Array) { + collectBlocks(cidToString(entry.v)) + } + if (entry.t instanceof Uint8Array) { + collectBlocks(cidToString(entry.t)) + } + } + } + } + } catch (e) { + // Not a structured block, that's fine + } + } + + collectBlocks(commitCid) + + // Build CAR with only needed blocks + const blocksForCar = [] + for (const cid of neededCids) { + const data = getBlock(cid) + if (data) { + blocksForCar.push({ cid, data }) + } + } + + const car = buildCarFile(commitCid, blocksForCar) return new Response(car, { headers: { 'content-type': 'application/vnd.ipld.car' } })