import { resolver } from "@rocicorp/resolver";
import { BroadcastChannel as bc } from "../../../shared/src/broadcast-channel.js";
import { getBrowserGlobal } from "../../../shared/src/browser-env.js";
const keyPrefix = "zero-active";
function toLockName(clientGroupID, clientID) {
  return `${keyPrefix}/${clientGroupID}/${clientID}`;
}
function toBroadcastChannelName(clientGroupID) {
  return `${keyPrefix}/${clientGroupID}`;
}
function fromLockName(lockKey) {
  if (!lockKey || !lockKey.startsWith(keyPrefix)) {
    return void 0;
  }
  const parts = lockKey.slice(keyPrefix.length).split("/");
  if (parts.length !== 3) {
    return void 0;
  }
  return {
    clientGroupID: parts[1],
    clientID: parts[2]
  };
}
function ignoreAbortError(e) {
  if (e instanceof Error && e.name === "AbortError") {
    return;
  }
  throw e;
}
class ActiveClientsManager {
  clientGroupID;
  clientID;
  #resolver = resolver();
  #lockManager;
  #activeClients = /* @__PURE__ */ new Set();
  /**
   * A callback that is called when a client is added to the client group.
   * It receives the client ID of the added client.
   */
  onAdd;
  /**
   * A callback that is called when a client is deleted from the client group.
   * It receives the client ID of the deleted client.
   */
  onDelete;
  /**
   * Creates an instance of `ActiveClientsManager` for the specified client
   * group and client ID. It will return a promise that resolves when the
   * instance is ready to use, which means that it has successfully acquired the
   * exclusive lock for the client and has retrieved the list of active clients.
   */
  static async create(clientGroupID, clientID, signal) {
    const instance = new ActiveClientsManager(clientGroupID, clientID, signal);
    await instance.#init(signal);
    return instance;
  }
  constructor(clientGroupID, clientID, signal) {
    this.clientGroupID = clientGroupID;
    this.clientID = clientID;
    this.#lockManager = getClientLockManager(signal);
    this.#activeClients.add(clientID);
  }
  async #init(signal) {
    const { clientGroupID, clientID } = this;
    const name = toLockName(clientGroupID, clientID);
    const channel = new bc(toBroadcastChannelName(clientGroupID));
    channel.addEventListener(
      "message",
      (e) => {
        const client = fromLockName(e.data);
        if (client?.clientGroupID === this.clientGroupID) {
          this.#addClient(client.clientID);
        }
      },
      { signal }
    );
    this.#lockManager.request(name, "exclusive", () => this.#resolver.promise).catch(ignoreAbortError);
    signal.addEventListener(
      "abort",
      () => {
        this.#lockManager.release(name, () => this.#resolver.resolve());
        channel.close();
      },
      { once: true }
    );
    for (const clientID2 of await this.#getActiveClients()) {
      if (clientID2 !== this.clientID) {
        this.#addClient(clientID2);
      }
    }
    if (!signal.aborted) {
      channel.postMessage(name);
    }
  }
  get activeClients() {
    return this.#activeClients;
  }
  async #getActiveClients() {
    const activeClients = /* @__PURE__ */ new Set();
    for await (const lockName of this.#lockManager.queryExclusive()) {
      const client = fromLockName(lockName);
      if (client?.clientGroupID === this.clientGroupID) {
        activeClients.add(client.clientID);
      }
    }
    return activeClients;
  }
  /**
   * This gets called when a new client is added to the client group.
   *
   * It will request a shared lock for the client, and when the exclusive lock
   * is released, it will notify that the client has been deactivated.
   */
  #addSharedLockForOtherClient(clientID) {
    const name = toLockName(this.clientGroupID, clientID);
    this.#lockManager.request(name, "shared", () => this.#removeClient(clientID)).catch(ignoreAbortError);
  }
  #addClient(clientID) {
    if (!this.#activeClients.has(clientID)) {
      this.#activeClients.add(clientID);
      this.#addSharedLockForOtherClient(clientID);
      this.onAdd?.(clientID);
    }
  }
  #removeClient(clientID) {
    if (this.#activeClients.delete(clientID)) {
      this.onDelete?.(clientID, this.clientGroupID);
    }
  }
}
function getClientLockManager(signal) {
  const locks = getBrowserGlobal("navigator")?.locks;
  if (locks) {
    return new NativeClientLockManager(locks, signal);
  }
  return new MockClientLockManager();
}
class NativeClientLockManager {
  #locks;
  #signal;
  constructor(locks, signal) {
    this.#locks = locks;
    this.#signal = signal;
  }
  async request(name, mode, fn) {
    await this.#locks.request(name, { mode, signal: this.#signal }, fn);
  }
  release(_name, fn) {
    fn();
  }
  async *queryExclusive() {
    const snapshot = await this.#locks.query();
    for (const lock of [
      ...snapshot.held ?? [],
      ...snapshot.pending ?? []
    ]) {
      if (lock.mode === "exclusive" && lock.name) {
        yield lock.name;
      }
    }
  }
}
const mockLockNames = /* @__PURE__ */ new Set();
const mockListeners = /* @__PURE__ */ new Set();
class MockClientLockManager {
  #listeners = /* @__PURE__ */ new Set();
  request(name, mode, fn) {
    if (mode === "exclusive") {
      mockLockNames.add(name);
    } else {
      const listener = (removed) => {
        if (removed === name) {
          mockListeners.delete(listener);
          return fn();
        }
      };
      mockListeners.add(listener);
      this.#listeners.add(listener);
    }
    return Promise.resolve();
  }
  release(name, fn) {
    mockLockNames.delete(name);
    for (const listener of mockListeners) {
      listener(name);
    }
    for (const listener of this.#listeners) {
      mockListeners.delete(listener);
    }
    fn();
  }
  async *queryExclusive() {
    yield* mockLockNames;
  }
}
export {
  ActiveClientsManager
};
//# sourceMappingURL=active-clients-manager.js.map
