var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
var __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Diagnostic } from "#log/Diagnostic.js";
import { Logger } from "#log/Logger.js";
import { ImplementationError, ReadOnlyError } from "#MatterError.js";
import { Time } from "#time/Time.js";
import { Timestamp } from "#time/Timestamp.js";
import { Millis } from "#time/TimeUnit.js";
import { asError } from "#util/Error.js";
import { Observable } from "#util/Observable.js";
import { MaybePromise } from "#util/Promises.js";
import { describeList } from "#util/String.js";
import { FinalizationError, TransactionDestroyedError, TransactionFlowError, UnsettledStateError } from "./errors.js";
import { ResourceSet } from "./ResourceSet.js";
import { Status } from "./Status.js";
const logger = Logger.get("Transaction");
const MAX_PRECOMMIT_CYCLES = 5;
const MAX_CHAINED_COMMITS = 5;
function open(via, lifetime, isolation = "rw") {
  return new Tx(via, lifetime, isolation);
}
class Tx {
  #isolation;
  #participants = /* @__PURE__ */ new Set();
  #roles = /* @__PURE__ */ new Map();
  #resources = /* @__PURE__ */ new Set();
  #status;
  #waitingOn;
  #via;
  #lifetime;
  #shared;
  #closed;
  #isAsync = false;
  #reportingLocks = false;
  constructor(via, lifetime, isolation) {
    this.#via = Diagnostic.via(via);
    this.#lifetime = lifetime.join("tx", via);
    Object.defineProperties(this.#lifetime.details, {
      status: {
        get: () => this.#status,
        enumerable: true
      },
      resources: {
        get: () => {
          if (!this.#resources.size) {
            return;
          }
          return [...this.#resources].join("+");
        }
      }
    });
    this.#isolation = isolation;
    if (isolation === "rw") {
      this.#status = Status.Shared;
    } else {
      this.#status = Status.ReadOnly;
    }
  }
  [Symbol.dispose]() {
    var _stack = [];
    try {
      switch (this.status) {
        case Status.ReadOnly:
        case Status.Shared:
        case Status.Destroyed:
          break;
        default:
          logger.error(this.via, "Disposed", this.via, "while", this.status);
          break;
      }
      const _closing = __using(_stack, this.#lifetime.closing());
      this.#reset("dropped");
      this.#status = Status.Destroyed;
      this.#closed?.emit();
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  get via() {
    return this.#via;
  }
  get isolation() {
    return this.#isolation;
  }
  get status() {
    return this.#status;
  }
  get participants() {
    return this.#participants;
  }
  get resources() {
    return this.#resources;
  }
  get waitingOn() {
    return this.#waitingOn;
  }
  get isAsync() {
    return this.#isAsync;
  }
  /**
   * We set this during async processing.  This enables the lock reporting when too much time ellapses.
   */
  set isAsync(isAsync) {
    if (!this.#isAsync) {
      this.#locksChanged(this.#resources);
    }
    this.#isAsync = isAsync;
  }
  onShared(listener, once) {
    if (this.status === Status.ReadOnly) {
      return;
    }
    if (this.#shared === void 0) {
      this.#shared = Observable();
    }
    this.#shared[once ? "once" : "on"](listener);
  }
  onClose(listener) {
    if (this.status === Status.ReadOnly) {
      return;
    }
    if (this.status === Status.Destroyed) {
      listener();
    }
    if (this.#closed === void 0) {
      this.#closed = Observable();
    }
    this.#closed.once(listener);
  }
  async addResources(...resources) {
    this.#assertAvailable();
    if (this.#status === Status.Exclusive) {
      const set = new ResourceSet(this, resources);
      const locked = await set.acquireLocks();
      this.#locksChanged(locked);
    }
    this.addResourcesSync(...resources);
  }
  addResourcesSync(...resources) {
    this.#assertAvailable();
    if (this.#status === Status.Exclusive) {
      const set = new ResourceSet(this, resources);
      const locked = set.acquireLocksSync();
      this.#locksChanged(locked);
    } else if (this.#status !== Status.Shared) {
      throw new TransactionFlowError(`Cannot add resources to transaction that is ${this.status}`);
    }
    for (const resource of resources) {
      this.#resources.add(resource);
    }
  }
  async begin() {
    this.#assertAvailable();
    if (this.status === Status.Exclusive) {
      return;
    }
    if (this.status !== Status.Shared) {
      throw new TransactionFlowError(`Cannot begin write transaction because transaction is ${this.#status}`);
    }
    this.#status = Status.Waiting;
    try {
      const resources = new ResourceSet(this, this.#resources);
      const locked = await resources.acquireLocks();
      this.#locksChanged(locked);
      this.#status = Status.Exclusive;
    } catch (e) {
      this.#status = Status.Shared;
      throw e;
    }
  }
  beginSync() {
    this.#assertAvailable();
    if (this.status === Status.Exclusive) {
      return;
    }
    if (this.status !== Status.Shared) {
      throw new TransactionFlowError(`Cannot begin write transaction because transaction is ${this.#status}`);
    }
    this.#status = Status.Exclusive;
    try {
      const resources = new ResourceSet(this, this.#resources);
      const locked = resources.acquireLocksSync();
      this.#locksChanged(locked);
    } catch (e) {
      this.#status = Status.Shared;
      throw e;
    }
  }
  addParticipants(...participants) {
    this.#assertAvailable();
    for (const participant of participants) {
      if (this.#participants.has(participant)) {
        continue;
      }
      if ([...this.#participants].findIndex((p) => p.toString() === participant.toString()) !== -1) {
        throw new ImplementationError(`Participant ${participant} identity is not unique`);
      }
      this.#participants.add(participant);
      if (participant.role !== void 0) {
        if (this.#roles.has(participant.role)) {
          throw new TransactionFlowError(`A participant is already registered for role ${participant.role}`);
        }
        this.#roles.set(participant.role, participant);
      }
    }
  }
  getParticipant(role) {
    this.#assertAvailable();
    return this.#roles.get(role);
  }
  commit() {
    if (this.status === Status.Shared) {
      return this.rollback();
    }
    this.#assertAvailable();
    const result = this.#executeCommitCycle(0);
    if (result) {
      this.isAsync = true;
    }
    return result;
  }
  resolve(result) {
    this.#lifetime.closing();
    if (MaybePromise.is(result)) {
      this.isAsync = true;
      return result.then(this.resolve.bind(this), this.reject.bind(this));
    }
    let promise;
    try {
      promise = this.commit();
    } catch (e) {
      return this.reject(e);
    }
    if (MaybePromise.is(promise)) {
      this.isAsync = true;
      return Promise.resolve(promise).then(() => {
        this[Symbol.dispose]();
        return result;
      }, this.reject.bind(this)).finally(this[Symbol.dispose].bind(this));
    }
    this[Symbol.dispose]();
    return result;
  }
  rollback() {
    this.#assertAvailable();
    return this.#finalize(Status.RollingBack, "rolled back", () => this.#executeRollback());
  }
  reject(cause) {
    this.#lifetime.closing();
    if (this.#status === Status.Shared) {
      try {
        this.#reset("released");
      } finally {
        this[Symbol.dispose]();
      }
      throw cause;
    }
    logger.error("Rolling back", this.via, "due to error:", Diagnostic.weak(asError(cause).message));
    try {
      const result = this.rollback();
      if (MaybePromise.is(result)) {
        return Promise.resolve(result).catch((cause2) => {
          if (cause2 === cause) {
            return;
          }
          logger.error("Secondary error in", this.via, "rollback:", cause2);
        }).finally(() => {
          this[Symbol.dispose]();
          throw cause;
        });
      }
    } catch (cause2) {
      if (cause2 !== cause) {
        logger.error("Secondary error in", this.via, "rollback:", cause2);
      }
    }
    this[Symbol.dispose]();
    throw cause;
  }
  join(...name) {
    if (this.#isolation === "ro") {
      throw new ReadOnlyError();
    }
    return this.#lifetime.join(...name);
  }
  /**
   * Execute commit logic for a single commit cycle.
   *
   * A "cycle" performs all commit logic and normally brings us back to shared state.  But we allow post-commit
   * handlers to re-enter exclusive state.  If that happens, we trigger another commit cycle.
   */
  #executeCommitCycle(count) {
    count++;
    if (count > MAX_CHAINED_COMMITS) {
      throw new TransactionFlowError(
        `Transaction commits have cascaded ${count} times which likely indicates an infinite loop`
      );
    }
    let result = this.#createPreCommitExecutor()();
    if (MaybePromise.is(result)) {
      result = result.then(this.#executeCommit.bind(this));
    } else {
      result = this.#executeCommit();
    }
    if (MaybePromise.is(result)) {
      return result.then(() => {
        if (this.#status === Status.Exclusive) {
          return this.#executeCommitCycle(count);
        }
      });
    } else if (this.#status === Status.Exclusive) {
      return this.#executeCommitCycle(count);
    }
  }
  waitFor(others) {
    this.#assertAvailable();
    if (this.waitingOn) {
      throw new TransactionFlowError("Attempted wait on a transaction that is already waiting");
    }
    logger.log(
      Status.slowLogLevel,
      "Tx",
      this.via,
      "waiting on",
      describeList("and", ...[...others].map((other) => other.via))
    );
    this.#waitingOn = others;
    return new Promise((resolve) => {
      for (const other of others) {
        other.onShared(() => {
          others.delete(other);
          if (!others.size) {
            this.#waitingOn = void 0;
            resolve();
          }
        }, true);
      }
    }).finally(() => this.#waitingOn = void 0);
  }
  toString() {
    return `transaction<${this.via}>`;
  }
  treatAsSlow() {
    Monitor.delete(this);
    if (this.#reportingLocks) {
      return;
    }
    this.#reportingLocks = true;
    this.#locksChanged(this.#resources);
  }
  /**
   * Shared implementation for commit and rollback.
   */
  #finalize(status, why, finalizer) {
    if (this.status !== Status.Shared && this.status !== Status.Exclusive) {
      throw new TransactionFlowError(
        `Illegal attempt to enter status ${status} when transaction is ${this.#status}`
      );
    }
    let isAsync = false;
    try {
      this.#status = status;
      const result = finalizer();
      if (MaybePromise.is(result)) {
        isAsync = true;
        return Promise.resolve(result).finally(() => this.#reset(why));
      }
    } finally {
      if (!isAsync) {
        this.#reset(why);
      }
    }
  }
  /**
   * Reset state to shared with no resources or participants.
   */
  #reset(why) {
    const set = new ResourceSet(this, this.#resources);
    const unlocked = set.releaseLocks();
    this.#locksChanged(unlocked, `${why} and unlocked`);
    this.#resources.clear();
    Monitor.delete(this);
    this.#reportingLocks = false;
    this.#participants.clear();
    this.#roles.clear();
    this.#status = Status.Shared;
    this.#shared?.emit();
  }
  /**
   * Iteratively execute pre-commit until all participants "settle" and report no possible mutation.
   */
  #createPreCommitExecutor() {
    let mayHaveMutated = false;
    let abortedDueToError = false;
    let iterator = this.participants[Symbol.iterator]();
    let cycles = 1;
    const errorRollback = (error) => {
      logger.error(
        "Rolling back",
        this.via,
        "due to pre-commit error:",
        Diagnostic.weak(error?.message || `${error}`)
      );
      const result = this.#finalize(Status.RollingBack, "rolled back", () => this.#executeRollback());
      if (MaybePromise.is(result)) {
        return result.then(() => {
          throw error;
        });
      }
      throw error;
    };
    const nextCycle = () => {
      cycles++;
      if (cycles > MAX_PRECOMMIT_CYCLES) {
        return errorRollback(
          new UnsettledStateError(
            `State has not settled after ${MAX_PRECOMMIT_CYCLES} pre-commit cycles which likely indicates an infinite loop`
          )
        );
      }
      mayHaveMutated = false;
      iterator = this.participants[Symbol.iterator]();
    };
    const executePreCommit = (previousResult) => {
      if (abortedDueToError) {
        return;
      }
      if (previousResult) {
        mayHaveMutated = true;
      }
      while (true) {
        const n = iterator.next();
        if (n.done) {
          if (mayHaveMutated) {
            const result = nextCycle();
            if (MaybePromise.is(result)) {
              return result;
            }
            continue;
          }
          break;
        }
        const participant = n.value;
        const handleError = (error) => {
          abortedDueToError = true;
          return errorRollback(error);
        };
        try {
          const result = participant.preCommit?.();
          if (MaybePromise.is(result)) {
            return Promise.resolve(result).catch(handleError).then(executePreCommit);
          }
          if (result) {
            mayHaveMutated = true;
          }
        } catch (e) {
          return handleError(e);
        }
      }
    };
    return executePreCommit;
  }
  /**
   * Handle actual commit and post-commit.
   */
  #executeCommit() {
    const participants = [...this.#participants];
    const executeCommit = () => {
      const result2 = this.#executeCommit1();
      if (MaybePromise.is(result2)) {
        return Promise.resolve(result2).then(this.#executeCommit2.bind(this));
      }
      return this.#executeCommit2();
    };
    const result = this.#finalize(Status.CommittingPhaseOne, "committed", executeCommit);
    const executePostCommit = this.#createPostCommitExecutor(participants);
    if (MaybePromise.is(result)) {
      return result.then(executePostCommit);
    }
    return executePostCommit();
  }
  #executeCommit1() {
    let needRollback = false;
    let asyncCommits;
    for (const participant of this.participants) {
      const handleParticipantError = (error) => {
        logger.error(`Error committing ${participant} (phase one):`, error);
        needRollback = true;
      };
      try {
        const result = participant.commit1?.();
        if (MaybePromise.is(result)) {
          if (!asyncCommits) {
            asyncCommits = [];
          }
          asyncCommits.push(Promise.resolve(result).catch(handleParticipantError));
        }
      } catch (e) {
        handleParticipantError(e);
        break;
      }
    }
    const abortIfFailed = () => {
      if (needRollback) {
        const result = this.#executeRollback();
        if (MaybePromise.is(result)) {
          return result.then(() => {
            throw new FinalizationError("Rolled back due to commit phase one error");
          });
        }
        throw new FinalizationError("Rolled back due to commit phase one error");
      }
    };
    if (asyncCommits) {
      return Promise.allSettled(asyncCommits).then(abortIfFailed);
    }
    return abortIfFailed();
  }
  #executeCommit2() {
    this.#status = Status.CommittingPhaseTwo;
    let errored;
    let ongoing;
    for (const participant of this.participants) {
      const promise = MaybePromise.then(
        () => participant.commit2?.(),
        void 0,
        (error) => {
          logger.error(`Error committing (phase two) ${participant}, state inconsistency possible:`, error);
          if (errored) {
            errored.push(participant);
          } else {
            errored = [participant];
          }
        }
      );
      if (MaybePromise.is(promise)) {
        if (ongoing) {
          ongoing.push(promise);
        } else {
          ongoing = [promise];
        }
      }
    }
    if (ongoing) {
      return Promise.allSettled(ongoing).then(() => throwIfErrored(errored, "in commit phase 2"));
    } else {
      throwIfErrored(errored, "in commit phase 2");
    }
  }
  /**
   * Execute post-commit phase.
   *
   * We notify each participant sequentially.  If a participant throws, we log the error and move on to the next
   * participant.
   */
  #createPostCommitExecutor(participants) {
    let i = 0;
    const executePostCommit = () => {
      for (; i < participants.length; i++) {
        let reportParticipantError2 = function(e) {
          logger.error(`Error post-commit of ${participant}:`, e);
        };
        var reportParticipantError = reportParticipantError2;
        const participant = participants[i];
        try {
          const promise = participant.postCommit?.();
          if (MaybePromise.is(promise)) {
            i++;
            return Promise.resolve(promise).then(executePostCommit, (e) => {
              reportParticipantError2(e);
              executePostCommit();
            });
          }
        } catch (e) {
          reportParticipantError2(e);
        }
      }
    };
    return executePostCommit;
  }
  /**
   * Rollback logic passed to #finish.
   */
  #executeRollback() {
    this.#status = Status.RollingBack;
    let errored;
    let ongoing;
    for (const participant of this.participants) {
      const promise = MaybePromise.then(
        () => participant.rollback?.(),
        void 0,
        (error) => {
          logger.error(`Error rolling back ${participant}, state inconsistency possible:`, error);
          if (errored) {
            errored.push(participant);
          } else {
            errored = [participant];
          }
        }
      );
      if (MaybePromise.is(promise)) {
        if (ongoing) {
          ongoing.push(promise);
        } else {
          ongoing = [promise];
        }
      }
    }
    const finished = () => {
      this.#status = Status.Shared;
      throwIfErrored(errored, "in commit phase 2");
    };
    if (ongoing) {
      return Promise.allSettled(ongoing).then(finished);
    } else {
      finished();
    }
  }
  #locksChanged(resources, how = "locked") {
    if (!resources.size || !this.isAsync) {
      return;
    }
    if (!this.#reportingLocks) {
      Monitor.add(this);
      return;
    }
    let resourceDescription;
    if (how === "locked") {
      resourceDescription = Diagnostic.strong(describeList("and", ...[...resources].map((r) => r.toString())));
    } else {
      resourceDescription = `${resources.size} resource${resources.size === 1 ? "" : "s"}`;
    }
    logger.log(Status.slowLogLevel, this.via, how, resourceDescription);
  }
  #assertAvailable() {
    if (this.#status === Status.Destroyed) {
      logger.warn(
        "You have accessed transaction",
        this.via,
        "outside of the context in which it was active.  Open a new context or ensure your operation completes before the context exits"
      );
      throw new TransactionDestroyedError(`Transaction ${this.#via} is destroyed`);
    }
    if (this.#status === Status.ReadOnly) {
      throw new ReadOnlyError();
    }
  }
}
function throwIfErrored(errored, when) {
  if (!errored?.length) {
    return;
  }
  const suffix = errored.length > 1 ? "s" : "";
  throw new FinalizationError(
    `Unhandled error${suffix} ${when} participant${suffix} ${describeList(
      "and",
      ...errored.map((p) => p.toString())
    )}`
  );
}
const Monitor = /* @__PURE__ */ (function() {
  const monitored = /* @__PURE__ */ new Map();
  let monitor;
  function check() {
    const now = Time.nowMs;
    for (const [tx, slowAt] of monitored) {
      if (now > slowAt) {
        tx.treatAsSlow();
      }
    }
  }
  return {
    add(tx) {
      const { slowTransactionTime } = Status;
      if (slowTransactionTime < 0) {
        return;
      }
      if (!slowTransactionTime) {
        tx.treatAsSlow();
        return;
      }
      if (monitored.has(tx)) {
        return;
      }
      monitored.set(tx, Timestamp(Time.nowMs + slowTransactionTime));
      if (monitor === void 0) {
        monitor = Time.getPeriodicTimer("tx-lock-monitor", Millis(slowTransactionTime / 10), check);
        monitor.start();
      }
    },
    delete(tx) {
      monitored.delete(tx);
      if (!monitored.size && monitor) {
        monitor.stop();
        monitor = void 0;
      }
    }
  };
})();
export {
  open
};
//# sourceMappingURL=Tx.js.map
