velesdb-memory 0.14.1

VelesDB-memory: local-first MCP memory server for AI agents (remember/recall/relate/forget/why + deterministic context compiler).
Documentation
use std::path::Path;

use super::{
    activate_destination, advance, archive_source, entry_state, journal_workspace, late_activation,
    outcome, query_error, rename_durably, require_journalled_fingerprint, unrecognised_disk,
    MigrationLock, MigrationState, Phase, Slots, SwitchOutcome, SwitchState,
};

pub(crate) fn stage_live_switch(
    store: &Path,
    destination: &Path,
) -> Result<SwitchOutcome, crate::MemoryError> {
    run_live_action(store, destination, stage_locked, false)
}

pub(crate) fn rollback_staged_live_switch(
    store: &Path,
    destination: &Path,
) -> Result<SwitchOutcome, crate::MemoryError> {
    run_live_action(store, destination, rollback_locked, false)
}

pub(crate) fn finalize_staged_live_switch(
    store: &Path,
    destination: &Path,
) -> Result<SwitchOutcome, crate::MemoryError> {
    run_live_action(store, destination, finalize_locked, true)
}

type LiveAction =
    fn(&Slots, &mut MigrationState, &Path, &MigrationLock) -> Result<(), crate::MemoryError>;

fn run_live_action(
    store: &Path,
    destination: &Path,
    action: LiveAction,
    allow_completed: bool,
) -> Result<SwitchOutcome, crate::MemoryError> {
    let workspace = journal_workspace(destination)?;
    let lock = MigrationLock::acquire(&workspace, "migrate-live-switch").map_err(query_error)?;
    let result = live_action_locked(
        store,
        destination,
        &workspace,
        &lock,
        action,
        allow_completed,
    );
    crate::migration::execute::reconcile(result, lock.release())
}

fn live_action_locked(
    store: &Path,
    destination: &Path,
    workspace: &Path,
    lock: &MigrationLock,
    action: LiveAction,
    allow_completed: bool,
) -> Result<SwitchOutcome, crate::MemoryError> {
    let mut state = entry_state(workspace, allow_completed)?;
    let slots = Slots::resolve(store, &state, destination)?;
    action(&slots, &mut state, workspace, lock)?;
    Ok(outcome(&slots))
}

fn stage_locked(
    slots: &Slots,
    state: &mut MigrationState,
    _workspace: &Path,
    _lock: &MigrationLock,
) -> Result<(), crate::MemoryError> {
    require_phase(state.phase, Phase::DestinationValidated, "stage")?;
    archive_source(slots, state)?;
    activate_destination(slots, state)
}

fn rollback_locked(
    slots: &Slots,
    state: &mut MigrationState,
    _workspace: &Path,
    _lock: &MigrationLock,
) -> Result<(), crate::MemoryError> {
    require_phase(state.phase, Phase::DestinationValidated, "roll back")?;
    match slots.on_disk() {
        SwitchState {
            source: true,
            archive: false,
            destination: true,
        } => Ok(()),
        SwitchState {
            source: false,
            archive: true,
            destination: true,
        } => restore_first_rename(slots, state),
        SwitchState {
            source: true,
            archive: true,
            destination: false,
        } => restore_both_renames(slots, state),
        other => Err(unrecognised_disk(other, Phase::DestinationValidated)),
    }
}

fn restore_first_rename(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
    require_journalled_fingerprint(&slots.archive, state, "archive")?;
    rename_durably(&slots.archive, &slots.source)
}

fn restore_both_renames(slots: &Slots, state: &MigrationState) -> Result<(), crate::MemoryError> {
    late_activation(slots, state)?;
    rename_durably(&slots.source, &slots.destination)?;
    rename_durably(&slots.archive, &slots.source)
}

fn finalize_locked(
    slots: &Slots,
    state: &mut MigrationState,
    workspace: &Path,
    lock: &MigrationLock,
) -> Result<(), crate::MemoryError> {
    if state.phase == Phase::Committed {
        return Ok(());
    }
    late_activation(slots, state)?;
    if state.phase == Phase::DestinationValidated {
        advance(state, Phase::SourceArchived, workspace, lock)?;
    }
    require_phase(state.phase, Phase::SourceArchived, "finalize")?;
    advance(state, Phase::DestinationActivated, workspace, lock)
}

fn require_phase(
    actual: Phase,
    expected: Phase,
    operation: &str,
) -> Result<(), crate::MemoryError> {
    if actual == expected {
        return Ok(());
    }
    Err(query_error(format!(
        "cannot {operation} live switch from journal phase {actual:?}; expected {expected:?}"
    )))
}