/**
* Generic, reusable schema-migration runner — the standard upgrade path.
*
* Modeled on Keet (keet-core: @keet/system-digest.abi.version gate +
* numbered migrations/{N}): store a monotonically-increasing schema
* version, and on boot run every registered migration whose target
* version is newer than the store's current version, in order, bumping
* the version AFTER each succeeds. In-place (the migration reads legacy
* state and writes new state in the SAME store) — never copy the store
* (hypercore-storage's device-file guard rejects a moved corestore).
*
* Properties:
* - **version-gated**: a migration with version <= current is skipped.
* - **idempotent**: re-running at the latest version does nothing.
* - **resumable**: the version is bumped only after a migration's `up`
* resolves, so a crash mid-chain resumes from the last completed one.
* - **fail-stop**: a throwing `up` aborts the chain (the store is left
* at the last successfully-applied version, not a half-newer one).
*
* A migration is `{ version:number, name?:string, up(ctx):Promise }`
* where `version` is the version it brings the store TO. Each `up` should
* itself be idempotent (it may be retried after a crash before setVersion).
*
* opts:
* getVersion() -> Promise<number|null> current store schema version
* setVersion(v) -> Promise persist the new version
* migrations -> [{ version, name, up }]
* ctx -> opaque, passed to each up() (e.g. { corestore, orgs })
* log(msg) -> optional progress
*
* Returns { startVersion, version, ran:[versions] }.
*/
export async function runMigrations (opts = {}) {
const { getVersion, setVersion, migrations = [], ctx, log } = opts
if (typeof getVersion !== 'function' || typeof setVersion !== 'function') {
throw new Error('runMigrations requires { getVersion, setVersion }')
}
for (const m of migrations) {
if (!m || typeof m.version !== 'number' || typeof m.up !== 'function') {
throw new Error('each migration needs { version:number, up:function }')
}
}
// Reject duplicate target versions — ambiguous ordering.
const versions = migrations.map(m => m.version)
if (new Set(versions).size !== versions.length) {
throw new Error('duplicate migration versions: ' + versions.join(','))
}
const sorted = [...migrations].sort((a, b) => a.version - b.version)
let current = (await getVersion()) || 0
const startVersion = current
const ran = []
for (const m of sorted) {
if (m.version <= current) continue
if (log) log('migrate ' + current + ' -> ' + m.version + (m.name ? ' (' + m.name + ')' : ''))
await m.up(ctx) // in-place; a throw aborts the chain here
await setVersion(m.version) // bump only after success → resumable
current = m.version
ran.push(m.version)
}
return { startVersion, version: current, ran }
}