OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / lib / dual-blob-store.js

70 lines · javascript ref:
// 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 {}
  }
}