feat(versionresolver): support skip-forward resume for orphan ledger versions
This commit is contained in:
@@ -110,11 +110,27 @@ export class VersionResolver {
|
||||
*
|
||||
* Behavior:
|
||||
* - If currentVersion === targetVersion → returns []
|
||||
* - Otherwise, finds the step whose `fromVersion === currentVersion`
|
||||
* and returns it plus all subsequent steps up to (and including)
|
||||
* the one whose `toVersion === targetVersion`.
|
||||
* - If no step starts at currentVersion → throws CHAIN_NOT_AT_CURRENT.
|
||||
* - If walking past targetVersion never matches → throws TARGET_NOT_REACHABLE.
|
||||
* - Otherwise, finds the first step whose `toVersion > currentVersion`
|
||||
* (i.e. the first step that hasn't been fully applied yet) and returns
|
||||
* it plus all subsequent steps up to (and including) the one whose
|
||||
* `toVersion === targetVersion`.
|
||||
* - Supports two resume modes:
|
||||
* 1. **Exact resume**: currentVersion === step.fromVersion — the normal
|
||||
* case, where the ledger sits exactly at a step's starting point.
|
||||
* 2. **Skip-forward resume**: currentVersion > step.fromVersion but
|
||||
* currentVersion < step.toVersion — the orphan case, where the
|
||||
* ledger was stamped to an intermediate version that no registered
|
||||
* step starts at (e.g. fresh installs using
|
||||
* `freshInstallVersion: targetVersion` across releases that didn't
|
||||
* add migrations). Skip-forward assumes step handlers are
|
||||
* idempotent (safe to re-run against data already partially in the
|
||||
* target shape). This is the documented contract for all step
|
||||
* handlers.
|
||||
* - If a step's `toVersion` overshoots `targetVersion` → throws
|
||||
* TARGET_NOT_REACHABLE.
|
||||
* - If no step can advance `currentVersion` toward `targetVersion`
|
||||
* (currentVersion is past the end of the chain) → throws
|
||||
* TARGET_NOT_REACHABLE.
|
||||
* - If currentVersion > targetVersion → throws DOWNGRADE_NOT_SUPPORTED.
|
||||
*/
|
||||
public static computePlan(
|
||||
@@ -137,12 +153,14 @@ export class VersionResolver {
|
||||
);
|
||||
}
|
||||
|
||||
const startIndex = steps.findIndex((s) => this.equals(s.fromVersion, currentVersion));
|
||||
// Find the first step that hasn't been fully applied yet.
|
||||
// A step is "not yet applied" iff its toVersion > currentVersion.
|
||||
const startIndex = steps.findIndex((s) => this.greaterThan(s.toVersion, currentVersion));
|
||||
if (startIndex === -1) {
|
||||
throw new SmartMigrationError(
|
||||
'CHAIN_NOT_AT_CURRENT',
|
||||
`No registered migration step starts at the current data version "${currentVersion}". Registered chain: ${this.describeChain(steps)}`,
|
||||
{ currentVersion, registeredChain: steps.map((s) => `${s.fromVersion}→${s.toVersion}`) },
|
||||
'TARGET_NOT_REACHABLE',
|
||||
`Current data version "${currentVersion}" is past the end of the registered chain, but target "${targetVersion}" has not been reached. No step can advance. Registered chain: ${this.describeChain(steps)}`,
|
||||
{ currentVersion, targetVersion, registeredChain: steps.map((s) => `${s.fromVersion}→${s.toVersion}`) },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user