// DualBlobStore — write-new / read-both migration wrapper for a DID's blob store.
//
// Writes go to the content-addressed OverlayBlobStore (real files, FastCDC-deduped, servable by oid, and —
// under a shared blob root — ONE physical copy shared with a co-located consumer like favela-web). Reads
// try the overlay store first, then FALL BACK to the LEGACY Hyperbee BlobStore for blobs uploaded BEFORE
// the overlay swap (e.g. existing avatars) — so nothing 404s during the transition. No bulk rewrite of live
// data: old blobs serve from the legacy store and age out naturally (a re-upload lands in the overlay).
//
// Drop-in: same public surface as BlobStore / OverlayBlobStore (ready / put / get / has / close).
export class DualBlobStore {
constructor (overlayStore, legacyStore) {
if (!overlayStore) throw new Error('DualBlobStore: overlayStore required')
this._new = overlayStore
this._old = legacyStore || null
}
async ready () { return this } // the caller readies both underlying stores before constructing this
// NEW blobs -> the content-addressed overlay store only (never the legacy balloon).
put (bytes, mimeType) { return this._new.put(bytes, mimeType) }
// Read the overlay store first; fall back to the legacy store for pre-migration blobs.
async get (cidStr) {
const r = await this._new.get(cidStr)
if (r) return r
return this._old ? this._old.get(cidStr) : null
}
async has (cidStr) {
if (await this._new.has(cidStr)) return true
return this._old ? this._old.has(cidStr) : false
}
// ── operator repair (pass-through to the overlay side; the legacy bee needs neither) ──────────────
// Re-register the blob FILES so peers can fetch them by oid. The legacy Hyperbee store keeps its bytes
// as bee values, which replicate as core blocks — it has no oid index to repair.
// Inventory for listBlobs. Delegates to the overlay/pointer store only: the LEGACY store predates
// pointers entirely, so it has no published set to enumerate and including it would report blobs the
// rest of the network cannot fetch.
async list (opts) {
if (typeof this._new.list !== 'function') return { cids: [], cursor: null, skipped: 'not-supported' }
return this._new.list(opts)
}
async reindexOverlay () {
if (typeof this._new.reindexOverlay !== 'function') return { skipped: 'not-supported' }
return this._new.reindexOverlay()
}
// Publish shared pointers for local blobs that were never announced. WRITABLE-ONLY — the caller gates
// on base.writable (sb.set appends to the autobase).
async backfillPointers () {
if (typeof this._new.backfill !== 'function') return { skipped: 'not-supported' }
return this._new.backfill()
}
// Pull blobs this node lacks but the swarm has announced (`__blob__/<cid>`). PULL-ONLY, so it is safe
// on a non-writable replica. The legacy store is untouched: it holds pre-migration blobs that have no
// shared pointer and no oid, so there is nothing there to converge.
async reconcileBlobs (opts) {
if (typeof this._new.reconcile !== 'function') return { skipped: 'not-supported' }
return this._new.reconcile(opts)
}
async close () {
try { await this._new.close() } catch {}
try { if (this._old) await this._old.close() } catch {}
}
}