OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / services / migrate / boot.js

82 lines · javascript ref:
/**
 * Boot-time, version-gated store migration (the in-place upgrade entry point).
 *
 * On daemon boot, AFTER the v2 per-org HyperOrgs is constructed + ready and
 * BEFORE the daemon starts serving, call migrateStore(). If the store's schema
 * version is behind TARGET_VERSION it opens the v1 sources in-place (old
 * single-orgs autobase + old per-org issue trackers), runs the registered
 * migrations through the generic runner (which bumps the version after each
 * succeeds → resumable / fail-stop), and closes the sources.
 *
 * Pure w.r.t. persistence: the caller injects getVersion/setVersion (see
 * version-file.js for the daemon's file-backed pair). This keeps migrateStore
 * unit-testable with an in-memory version cell.
 */

import { runMigrations } from './runner.js'
import { MIGRATIONS, TARGET_VERSION } from './migrations.js'
import { openOldOrgsBase, makeOpenOldOrgTracker } from './open-old-sources.js'

/**
 * @param {object} opts
 * @param {Corestore} opts.corestore   the daemon's corestore (NOT pre-namespaced)
 * @param {HyperOrgs} opts.newOrgs     the v2 per-org HyperOrgs (ready), the target
 * @param {() => Promise<number|null>} opts.getVersion
 * @param {(v:number) => Promise<void>} opts.setVersion
 * @param {string} [opts.orgsNamespace='orgs']   v1 orgs autobase namespace
 * @param {(name:string)=>(Buffer|string|null)} [opts.trackerBootstrapKeyFor]
 * @param {(msg:string)=>void} [opts.log]
 * @returns {Promise<{ skipped:boolean, startVersion?:number, version?:number, ran?:number[], result?:any }>}
 */
export async function migrateStore (opts = {}) {
  const { corestore, newOrgs, getVersion, setVersion, log, isMirror } = opts
  if (!corestore || !newOrgs) throw new Error('migrateStore requires { corestore, newOrgs }')
  if (typeof getVersion !== 'function' || typeof setVersion !== 'function') {
    throw new Error('migrateStore requires { getVersion, setVersion }')
  }

  // ORIGIN-ONLY (#52 Phase 4.4). The orgs migration (#37) SPLITS the single
  // orgs autobase into one autobase PER org — minting NEW cores with NEW keys.
  // If a MIRROR ran this, it would mint its OWN divergent per-org autobases
  // instead of mounting the origin's → split-brain. The origin owns the new
  // cores and conveys their keys through the replicated OrgEventLog
  // (reference-by-key, the Keet pattern); the mirror's OrgPuller mounts them
  // via mountOrg. So a mirror NEVER self-migrates — it converges by replication.
  if (isMirror) {
    if (log) log('schema migration skipped: mirror (orgs arrive via OrgPuller/mountOrg from origin)')
    return { skipped: true, reason: 'mirror', version: (await getVersion()) || 0, ran: [] }
  }

  const current = (await getVersion()) || 0
  if (current >= TARGET_VERSION) {
    return { skipped: true, startVersion: current, version: current, ran: [] }
  }

  if (log) log('schema migration: store at v' + current + ', target v' + TARGET_VERSION)

  const oldOrgsBase = await openOldOrgsBase(corestore, { namespace: opts.orgsNamespace || 'orgs' })
  const trackerOpener = makeOpenOldOrgTracker(corestore, { bootstrapKeyFor: opts.trackerBootstrapKeyFor })

  let result
  try {
    result = await runMigrations({
      getVersion,
      setVersion,
      migrations: MIGRATIONS,
      ctx: {
        oldOrgsBase,
        newOrgs,
        openOldOrgTracker: (name) => trackerOpener.open(name),
        log
      },
      log
    })
  } finally {
    await trackerOpener.closeAll()
    try { await oldOrgsBase.close() } catch {}
  }

  if (log) log('schema migration done: v' + result.startVersion + ' -> v' + result.version + ' (ran ' + result.ran.join(',') + ')')
  return { skipped: false, ...result, result }
}