/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { asError, ImplementationError, InternalError, Logger, MatterAggregateError, MaybePromise } from "#general";
import { hasRemoteActor } from "#protocol";
import { Contextual } from "../context/Contextual.js";
import { NodeActivity } from "../context/NodeActivity.js";
import { LocalActorContext } from "../context/server/LocalActorContext.js";
const logger = Logger.get("Reactors");
class Reactors {
  #backing;
  #backings = /* @__PURE__ */ new Set();
  #destructionComplete;
  constructor(backing) {
    this.#backing = backing;
  }
  get backing() {
    return this.#backing;
  }
  async close() {
    for (const reactor of this.#backings) {
      await reactor.close();
    }
    if (this.#backings.size) {
      return new Promise((resolve) => this.#destructionComplete = resolve);
    }
  }
  add(observable, reactor, options) {
    for (const backing of this.#backings) {
      if (backing.is(observable, reactor)) {
        return;
      }
    }
    this.#backings.add(new ReactorBacking(this, observable, reactor, options ?? {}));
  }
  remove(selector) {
    const toRemove = Array();
    for (const backing of this.#backings) {
      if (backing.is(selector.observable, selector.reactor)) {
        toRemove.push(backing);
      }
    }
    return MatterAggregateError.allSettled(
      toRemove.map((backing) => backing.close()),
      "Error removing reactors"
    );
  }
  deleteClosedBacking(backing) {
    this.#backings.delete(backing);
    if (!this.#backings.size) {
      this.#destructionComplete?.();
    }
  }
  get resource() {
    return this.#backing.datasource;
  }
}
class ReactorBacking {
  #owner;
  #endpoint;
  #listener;
  #reactor;
  #observable;
  #offline;
  #closing;
  #lock;
  #deferred = Array();
  #trampoline;
  #resolveTrampoline;
  #nodeActivity;
  #activity;
  constructor(reactors, observable, reactor, { offline, once, lock }) {
    this.#owner = reactors;
    this.#endpoint = reactors.backing.endpoint;
    this.#observable = observable;
    this.#offline = offline;
    this.#reactor = reactor;
    if (lock) {
      if (lock === true) {
        lock = [reactors.resource];
      } else if (!Array.isArray(lock)) {
        lock = [lock];
      }
      this.#lock = lock;
    }
    const reactorListener = ((...args) => {
      let resolution;
      let rejection;
      let alreadyReacting = true;
      try {
        if (!this.#trampoline) {
          alreadyReacting = false;
          if (!this.#nodeActivity) {
            this.#nodeActivity = this.#endpoint.env.get(NodeActivity);
          }
          this.#activity = this.#nodeActivity.begin(this.toString());
          this.#trampoline = new Promise((resolve) => {
            this.#resolveTrampoline = () => {
              resolve();
              this.#trampoline = void 0;
            };
          });
        }
        if (once) {
          resolution = this.#reactOnce(args);
        } else if (alreadyReacting) {
          resolution = this.#reactDeferred(args);
        } else {
          resolution = this.#react(args);
        }
      } catch (e) {
        const error = this.#augmentError(e);
        if (this.#observable.isAsync) {
          resolution = Promise.reject(error);
        } else {
          rejection = error;
        }
      }
      if (!alreadyReacting) {
        if (MaybePromise.is(resolution)) {
          void Promise.resolve(resolution).finally(this.#afterReaction.bind(this)).catch(() => {
          });
        } else {
          this.#afterReaction();
        }
      }
      if (rejection) {
        throw rejection;
      }
      if (this.#observable.isAsync) {
        resolution = Promise.resolve(resolution);
      } else if (MaybePromise.is(resolution)) {
        resolution.then(void 0, this.#unhandledError.bind(this));
        resolution = void 0;
      }
      return resolution;
    });
    observable.on(this.#listener = reactorListener);
  }
  is(observable, reactor) {
    return (observable === void 0 || this.#observable === observable) && (reactor === void 0 || this.#reactor === reactor) && !this.#closing;
  }
  close() {
    if (!this.#closing) {
      this.#close();
    }
    return this.#trampoline;
  }
  toString() {
    const reactorName = this.#reactor.name ? `.${this.#reactor.name}` : "";
    return `reactor<${this.#owner.backing}${reactorName}>`;
  }
  #unhandledError(error) {
    logger.error("Unhandled", this.#augmentError(error));
  }
  #close() {
    this.#observable.off(this.#listener);
    this.#closing = true;
    if (this.#trampoline) {
      this.#trampoline = this.#trampoline.finally(() => this.#owner.deleteClosedBacking(this));
    } else {
      this.#owner.deleteClosedBacking(this);
    }
  }
  /**
   * Trampoline the next reaction or resolve the trampoline promise.
   *
   * Note - this method *cannot* throw because we invoke it within a finally that may be on a rejected promise.
   * Errors on that promise must be caught by the emitter.  But the finally also creates a promise and *that* promise
   * will throw if the original promise is rejected, which requires us to have a catch() on the finally promise or
   * the promise will crash with an unhandled error.
   */
  #afterReaction() {
    try {
      if (this.#deferred.length) {
        const next = this.#deferred.shift();
        void next().catch((e) => {
          logger.error("Internal error invoking next reactor", e);
        }).finally(this.#afterReaction.bind(this));
        return;
      }
      this.#resolveTrampoline?.();
      this.#activity?.[Symbol.dispose]();
      this.#activity = void 0;
    } catch (e) {
      logger.error("Internal error after final reaction", e);
    }
  }
  /**
   * Invoke a single reactor when no other reactor is active.
   *
   * If the reaction is asynchronous it is tracked via {@link #reactAsync}.
   */
  #react(args) {
    const originalContext = Contextual.contextOf(args[args.length - 1]);
    let context;
    if (!this.#offline) {
      context = originalContext;
    }
    if (context) {
      const result = this.#reactWithContext(context, this.#owner.backing, args);
      if (MaybePromise.is(result) && !this.#observable.isAsync) {
        throw new ImplementationError(
          `${this} returned a Promise but the observable is synchronous; you must set the "offline" option so this reactor runs with a dedicated transaction`
        );
      }
      return result;
    }
    const command = hasRemoteActor(originalContext) && originalContext.command;
    try {
      const reactor = (context2) => {
        return this.#reactWithContext(context2, this.#owner.backing, args);
      };
      let result = LocalActorContext.act(this.toString(), reactor, {
        command,
        lifetime: this.#endpoint.construction
      });
      if (MaybePromise.is(result)) {
        result = result.then(void 0, this.#unhandledError.bind(this));
      }
      return result;
    } catch (e) {
      this.#unhandledError(e);
    }
  }
  /**
   * Invoked by the listener when there are already ongoing reactions.
   *
   * Installs a trampoline function and returns a placeholder promise.  The trampolined functions are always async
   * regardless of the emitter and listener because by necessity they do not act immediately.
   */
  #reactDeferred(args) {
    return new Promise((resolve, reject) => {
      const deferredReaction = async () => {
        try {
          const result = await this.#react(args);
          resolve(result);
        } catch (e) {
          reject(asError(e));
        }
      };
      this.#deferred.push(deferredReaction);
    });
  }
  /**
   * React in "once" mode -- react then close.
   */
  #reactOnce(args) {
    this.#observable.off(this.#listener);
    let isAsync = false;
    try {
      const result = this.#react(args);
      if (MaybePromise.is(result)) {
        isAsync = true;
        return Promise.resolve(result).finally(() => this.#close());
      }
      return result;
    } finally {
      if (!isAsync) {
        void this.close();
      }
    }
  }
  /**
   * Invoked by #react once it obtains a context.
   */
  #reactWithContext(context, backing, args) {
    if (this.#lock) {
      return this.#lockThenReact(context, backing, args);
    }
    const reactor = this.#bindReactor(context, backing);
    return reactor(...args);
  }
  /**
   * Grab locks if so configured before performing reaction.
   */
  async #lockThenReact(context, backing, args) {
    if (!this.#lock) {
      throw new InternalError("No locks to acquire");
    }
    await context.transaction.addResources(...this.#lock);
    await context.transaction.begin();
    for (const resource of context.transaction.resources) {
      if (resource.lockedBy !== context.transaction) {
        throw new InternalError(`Lock of ${resource} should be held by ${this} but is not`);
      }
    }
    const reactor = this.#bindReactor(context, backing);
    return await reactor(...args);
  }
  /**
   * Bind the reactor to a behavior instance once locks are held.
   */
  #bindReactor(context, backing) {
    const agent = this.#endpoint.agentFor(context);
    const behavior = backing.createBehavior(agent, backing.type);
    return this.#reactor.bind(behavior);
  }
  /**
   * Detail the reactor in error messages for errors triggered during reaction.
   */
  #augmentError(cause) {
    const error = asError(cause);
    error.message = `Error in ${this}: ${error.message}`;
    return error;
  }
}
export {
  Reactors
};
//# sourceMappingURL=Reactors.js.map
