Source: src/js/models/dataPackage/UploadResult.js

"use strict";

define([], () => {
  const STATUSES = Object.freeze({
    PENDING: "pending",
    RUNNING: "running",
    SUCCEEDED: "succeeded",
    FAILED: "failed",
    AMBIGUOUS: "ambiguous",
    CANCELLED: "cancelled",
    SKIPPED: "skipped",
  });

  const OUTCOMES = Object.freeze({
    SUCCESS: "success",
    PARTIAL_FAILURE: "partial_failure",
    CANCELLED: "cancelled",
    STALE_REMOTE: "stale_remote",
  });

  /**
   * Mutable per action state for one in memory upload attempt.
   * @class UploadResult
   * @classcategory Models/DataPackage
   * @since 2.39.0
   */
  class UploadResult {
    /**
     * Create state for one upload attempt.
     * @param {object[]} actions Actions prepared for the upload
     * @param {object} [options] Result context
     * @param {number} [options.draftRevision] Prepared package revision
     * @param {object} [options.dataPackage] Owning package instance
     */
    constructor(actions = [], { draftRevision = 0, dataPackage = null } = {}) {
      this.actions = actions;
      this.draftRevision = draftRevision;
      this.dataPackage = dataPackage;
      this.actionStatus = new Map();
      this.actionErrors = new Map();
      /**
       * Errors for members whose upload actions could not be prepared.
       * @type {Map<string, Error>}
       * @since 2.41.0
       */
      this.preparationErrors = new Map();
      actions.forEach((action) => {
        this.actionStatus.set(action.id, STATUSES.PENDING);
      });
      this.outcome = null;
      this.retryable = false;
      this.reloadRequired = false;
      this.staleRemote = false;
      this.wasCancelled = false;
    }

    /**
     * Set an action's status and optional error.
     * @param {string} actionId Upload action identifier
     * @param {string} status Action status
     * @param {Error|null} [error] Action error
     * @returns {UploadResult} This result
     */
    setStatus(actionId, status, error = null) {
      this.actionStatus.set(actionId, status);
      if (error) {
        this.actionErrors.set(actionId, error);
      } else {
        this.actionErrors.delete(actionId);
      }
      return this;
    }

    /**
     * Mark an action as running.
     * @param {string} actionId Upload action identifier
     * @returns {UploadResult} This result
     */
    markRunning(actionId) {
      return this.setStatus(actionId, STATUSES.RUNNING);
    }

    /**
     * Mark an action as succeeded.
     * @param {string} actionId Upload action identifier
     * @returns {UploadResult} This result
     */
    markSucceeded(actionId) {
      return this.setStatus(actionId, STATUSES.SUCCEEDED);
    }

    /**
     * Mark an action as failed.
     * @param {string} actionId Upload action identifier
     * @param {Error} error Action error
     * @returns {UploadResult} This result
     */
    markFailed(actionId, error) {
      return this.setStatus(actionId, STATUSES.FAILED, error);
    }

    /**
     * Mark an action as an ambiguous write.
     * @param {string} actionId Upload action identifier
     * @param {Error} error Action error
     * @returns {UploadResult} This result
     */
    markAmbiguous(actionId, error) {
      return this.setStatus(actionId, STATUSES.AMBIGUOUS, error);
    }

    /**
     * Mark an action as cancelled.
     * @param {string} actionId Upload action identifier
     * @returns {UploadResult} This result
     */
    markCancelled(actionId) {
      this.wasCancelled = true;
      return this.setStatus(actionId, STATUSES.CANCELLED);
    }

    /**
     * Mark an action as skipped.
     * @param {string} actionId Upload action identifier
     * @returns {UploadResult} This result
     */
    markSkipped(actionId) {
      return this.setStatus(actionId, STATUSES.SKIPPED);
    }

    /**
     * Return an action's status.
     * @param {string} actionId Upload action identifier
     * @returns {string|null} Action status, or null
     */
    getStatus(actionId) {
      return this.actionStatus.get(actionId) ?? null;
    }

    /**
     * Return an action's error.
     * @param {string} actionId Upload action identifier
     * @returns {Error|null} Action error, or null
     */
    getError(actionId) {
      return this.actionErrors.get(actionId) ?? null;
    }

    /**
     * Collect per action and member level errors so callers can show the actual
     * failure instead of a generic "save failed" message.
     * @returns {string[]} Unique, non empty error messages
     */
    getErrorMessages() {
      const messages = new Set();
      const add = (error) => {
        let message = null;
        if (typeof error === "string") message = error;
        else if (error && typeof error.message === "string")
          message = error.message;
        if (message) messages.add(message);
      };
      this.actionErrors.forEach((error) => add(error));
      this.preparationErrors.forEach((error) => add(error));
      const members = this.dataPackage?.members?.toArray?.() || [];
      members.forEach((member) => add(member?.lastUploadError));
      return [...messages];
    }

    /**
     * Mark the package as stale against remote state.
     * @param {string|null} actionId Related upload action identifier
     * @param {Error} error Stale remote error
     * @returns {UploadResult} This result
     */
    markStaleRemote(actionId, error) {
      if (actionId) this.markFailed(actionId, error);
      this.staleRemote = true;
      return this;
    }

    /**
     * Derive the upload outcome from action statuses.
     * @returns {UploadResult} This result
     */
    finalize() {
      this.retryable = false;
      this.reloadRequired = false;
      const statuses = [...this.actionStatus.values()];
      const anyFailed =
        this.preparationErrors.size > 0 ||
        statuses.some(
          (status) =>
            status === STATUSES.FAILED || status === STATUSES.AMBIGUOUS,
        );
      const anySkipped = statuses.includes(STATUSES.SKIPPED);
      const allCommitted =
        this.preparationErrors.size === 0 &&
        statuses.every((status) => status === STATUSES.SUCCEEDED);

      if (this.staleRemote) {
        this.outcome = OUTCOMES.STALE_REMOTE;
        this.reloadRequired = true;
      } else if (this.wasCancelled && !allCommitted) {
        this.outcome = OUTCOMES.CANCELLED;
        this.reloadRequired = statuses.includes(STATUSES.AMBIGUOUS);
        this.retryable = !this.reloadRequired;
      } else if (anyFailed || anySkipped) {
        this.outcome = OUTCOMES.PARTIAL_FAILURE;
        this.retryable = true;
      } else {
        this.outcome = OUTCOMES.SUCCESS;
      }
      return this;
    }
  }

  UploadResult.Statuses = STATUSES;
  UploadResult.Outcomes = OUTCOMES;

  return UploadResult;
});