VersionedWorkflowStore.java

package org.hammer.audio.workflow.store;

import java.util.List;
import java.util.Objects;

/**
 * Persistence facade for versioned workflow checkpoints.
 *
 * <p>This interface is the only acceptable boundary between Audio Analyzer workflow services and
 * any concrete storage back end (e.g. JGit/Hibernate). All JGit and Hibernate types must stay
 * behind implementations of this interface and must never be exposed to callers.
 *
 * <p><b>Allowed callers</b>: application services. Must not be called directly from editor
 * adapters, UI components or workflow domain objects.
 *
 * <p><b>Dependency rule</b>: implementations may depend on JGit and Hibernate. Callers must depend
 * only on this interface and the value types in this package.
 */
public interface VersionedWorkflowStore {

  /**
   * Commits a workflow snapshot on the given branch and returns the new commit identifier.
   *
   * @param branch branch name (non-blank)
   * @param snapshot canonical DSL snapshot to persist
   * @param metadata author, message and timestamp for this commit
   * @return new commit identifier
   */
  CommitId commit(String branch, WorkflowSnapshot snapshot, CommitMetadata metadata);

  /**
   * Conditionally commits a workflow snapshot only while the branch still has the expected HEAD.
   *
   * <p>Implementations backed by an atomic ref store should override this method so the comparison
   * and ref publication share one optimistic-concurrency boundary. The default implementation keeps
   * test and lightweight stores compatible, but cannot eliminate a race between the history check
   * and {@link #commit(String, WorkflowSnapshot, CommitMetadata)}.
   *
   * @param branch branch name
   * @param expectedHead commit the caller expects at HEAD, or {@code null} for a new branch
   * @param snapshot canonical DSL snapshot to persist
   * @param metadata author, message and timestamp
   * @return new commit identifier
   * @throws StaleWorkflowHeadException when the observed HEAD differs
   */
  default CommitId commitIfHead(
      String branch, CommitId expectedHead, WorkflowSnapshot snapshot, CommitMetadata metadata) {
    List<CommitInfo> current = history(branch, 1);
    CommitId actualHead = current.isEmpty() ? null : current.getFirst().commitId();
    if (!Objects.equals(expectedHead, actualHead)) {
      throw new StaleWorkflowHeadException(branch, expectedHead, actualHead);
    }
    return commit(branch, snapshot, metadata);
  }

  /**
   * Loads the workflow snapshot at the given commit.
   *
   * @param commitId commit identifier returned by a previous {@link #commit} call
   * @return snapshot at that commit
   * @throws java.util.NoSuchElementException if the commit does not exist
   */
  WorkflowSnapshot loadAtCommit(CommitId commitId);

  /**
   * Loads the workflow snapshot at the HEAD of the given branch.
   *
   * @param branch branch name
   * @return snapshot at HEAD
   * @throws java.util.NoSuchElementException if the branch does not exist
   */
  WorkflowSnapshot loadHead(String branch);

  /**
   * Performs an optimistic-concurrency ref update on the given ref.
   *
   * <p>The update succeeds only if the ref currently points to {@code expectedOldCommit}. If
   * another writer has advanced the ref in the meantime, {@link RefUpdateResult#STALE} is returned
   * and the caller must retry.
   *
   * @param refName ref name to update
   * @param expectedOldCommit the commit the caller believes to be current HEAD
   * @param newCommit the commit to set as the new HEAD
   * @return update result
   */
  RefUpdateResult updateRef(String refName, CommitId expectedOldCommit, CommitId newCommit);

  /**
   * Returns a reverse-chronological list of recent commits on the given ref.
   *
   * @param refName ref or branch name
   * @param limit maximum number of entries to return
   * @return commit history, most recent first
   */
  List<CommitInfo> history(String refName, int limit);
}