| … | … | @@ -131,6 +131,38 @@ export class BlobPointerStore { |
| 131 | 131 | // PULL-ONLY: it never writes a pointer, so it is safe on a NON-WRITABLE replica — exactly the peer |
| 132 | 132 | // that needs to catch up. Presence is tested with hasLocalFile (a stat), not get(), so a converged |
| 133 | 133 | // node does no reads at all. Bounded per pass with `more`, so a large backlog cannot stall the caller. |
| 134 | // Every blob CID this DID has PUBLISHED a pointer for — the inventory behind com.atproto.sync.listBlobs. |
| 135 | // |
| 136 | // ⛔ THIS IS THE ONLY WAY TO SEE AN ORPHAN. A blob referenced by some record can be found by walking |
| 137 | // records (favela-web/lib/blob-refs.js does exactly that, and needs no server support). A blob that is |
| 138 | // uploaded and referenced by NOTHING is invisible to any such walk, by construction — and that is the |
| 139 | // class that permanently lost the 2026-07-03 image. The pointer set is the authority's own inventory, |
| 140 | // so it is the only place the difference shows up. |
| 141 | // |
| 142 | // Reads the SHARED state, so it lists what the DID has published across all its co-owners, not merely |
| 143 | // what this node happens to hold bytes for. Sorted for a stable cursor; `since` is deliberately NOT |
| 144 | // supported (pointers carry no rev, so a rev-filter would silently return the wrong set — better to |
| 145 | // not offer it than to answer wrongly). |
| 146 | async list ({ limit = 500, cursor = null } = {}) { |
| 147 | let map |
| 148 | try { |
| 149 | map = await ((typeof this.sb.listNow === 'function') ? this.sb.listNow(BLOB_PREFIX) : this.sb.list(BLOB_PREFIX)) |
| 150 | } catch (e) { |
| 151 | return { cids: [], cursor: null, error: e.message } |
| 152 | } |
| 153 | const all = Object.keys(map || {}) |
| 154 | .filter((k) => k.startsWith(BLOB_PREFIX)) |
| 155 | .map((k) => k.slice(BLOB_PREFIX.length)) |
| 156 | .filter(Boolean) |
| 157 | .sort() |
| 158 | const start = cursor ? all.findIndex((c) => c > String(cursor)) : 0 |
| 159 | const from = start < 0 ? all.length : start |
| 160 | const n = Math.max(1, Math.min(Number(limit) || 500, 1000)) |
| 161 | const page = all.slice(from, from + n) |
| 162 | const next = (from + n) < all.length ? page[page.length - 1] : null |
| 163 | return { cids: page, cursor: next, total: all.length } |
| 164 | } |
| 165 | |
| 134 | 166 | async reconcile ({ limit = 8 } = {}) { |
| 135 | 167 | if (typeof this.blobs.hasLocalFile !== 'function') return { skipped: 'not-supported' } |
| 136 | 168 | let map |