/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Logger } from "#log/Logger.js";
import { describeList } from "#util/String.js";
import { SynchronousTransactionConflictError, TransactionDeadlockError, TransactionFlowError } from "./errors.js";
import { Resource } from "./Resource.js";
const logger = Logger.get("ResourceSet");
class ResourceSet {
  #transaction;
  #resources = /* @__PURE__ */ new Set();
  constructor(transaction, resources = transaction.resources) {
    this.#transaction = transaction;
    for (let resource of resources) {
      while (resource[Resource.reference]) {
        resource = resource[Resource.reference];
      }
      this.#resources.add(resource);
    }
  }
  /**
   * Wait until the resources have no exclusive transactions and then lock.
   */
  async acquireLocks() {
    while (true) {
      let blockedBy;
      for (const resource of this.#resources) {
        const lockedBy = resource.lockedBy;
        if (lockedBy && lockedBy !== this.#transaction) {
          if (!blockedBy) {
            blockedBy = /* @__PURE__ */ new Set();
          }
          blockedBy.add(lockedBy);
        }
      }
      if (!blockedBy) {
        break;
      }
      this.#detectDeadlock(blockedBy);
      await this.#transaction.waitFor(blockedBy);
    }
    return this.acquireLocksSync();
  }
  /**
   * Acquire locks synchronously.
   *
   * Throws an error if resources aren't lockable.
   */
  acquireLocksSync() {
    const toLock = /* @__PURE__ */ new Set();
    const blocked = /* @__PURE__ */ new Set();
    for (const resource of this.#resources) {
      if (resource.lockedBy) {
        if (resource.lockedBy === this.#transaction) {
          continue;
        }
        logger.warn("Transaction", this.#transaction.via, "blocked by", resource.lockedBy.via);
        blocked.add(resource);
      }
      toLock.add(resource);
    }
    if (blocked.size) {
      logger.warn("You may need to await transaction.begin() to acquire locks asynchronously");
      const names = [...blocked].map((s) => s.toString());
      throw new SynchronousTransactionConflictError(`Cannot lock ${describeList("and", ...names)} synchronously`);
    }
    for (const resource of toLock) {
      resource.lockedBy = this.#transaction;
    }
    return toLock;
  }
  /**
   * Release locks.
   */
  releaseLocks() {
    const unlocked = /* @__PURE__ */ new Set();
    for (const resource of this.#resources) {
      if (resource.lockedBy === this.#transaction) {
        delete resource.lockedBy;
        unlocked.add(resource);
      }
    }
    return unlocked;
  }
  /**
   * Ensure that a transaction that is committing or rolling back has all resources locked.
   *
   * This is just a sanity check.
   */
  assertResourcesAreLocked(transaction, why) {
    for (const resource of transaction.resources) {
      if (resource.lockedBy !== transaction) {
        throw new TransactionFlowError(`Transaction attempted ${why} but does not have all resources locked`);
      }
    }
  }
  /**
   * If two transactions would block each other then we would have a deadlock.
   *
   * This is unlikely but not impossible.  It can happen if an endpoint is added to an exclusive transaction but a
   * second transaction already has exclusivity on the new endpoint *and* is waiting on the first transaction.
   *
   * So... detect if the wait graph would have cycles if we an endpoint.  If so, throw an error.
   */
  #detectDeadlock(blockedBy) {
    const examined = /* @__PURE__ */ new Set();
    const examineBlocker = (transaction) => {
      examined.add(transaction);
      if (transaction === this.#transaction) {
        throw new TransactionDeadlockError(
          "Resource deadlock detected, write operation cannot proceed.  To prevent this you can await transaction.begin() before modifying state"
        );
      }
      if (transaction.waitingOn) {
        for (const blocker of transaction.waitingOn) {
          if (!examined.has(blocker)) {
            examineBlocker(blocker);
          }
        }
      }
    };
    for (const transaction of blockedBy) {
      examineBlocker(transaction);
    }
  }
  [Symbol.iterator]() {
    return this.#resources[Symbol.iterator]();
  }
}
export {
  ResourceSet
};
//# sourceMappingURL=ResourceSet.js.map
