/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Datasource } from "#behavior/state/managed/Datasource.js";
import { InternalError } from "@matter/general";
import { RemoteWriteParticipant } from "./RemoteWriteParticipant.js";
class DatasourceCache {
  #writer;
  #endpointNumber;
  #behaviorId;
  #localWriter;
  #buffer;
  #version;
  #dirtyKeys = /* @__PURE__ */ new Set();
  #erased = false;
  #reclaimed = false;
  #consumer;
  initialValues;
  constructor(options) {
    this.#writer = options.writer;
    this.#endpointNumber = options.endpointNumber;
    this.#behaviorId = options.behaviorId;
    this.#localWriter = options.localWriter;
    this.#buffer = options.buffer;
    this.initialValues = options.initialValues;
    const version = options.initialValues?.[DatasourceCache.VERSION_KEY];
    this.#version = typeof version === "number" ? version : Datasource.UNKNOWN_VERSION;
  }
  get consumer() {
    return this.#consumer;
  }
  set consumer(consumer) {
    this.#consumer = consumer;
    if (consumer !== void 0) {
      this.#reclaimed = false;
      this.initialValues = void 0;
    }
  }
  /**
   * Current values, preferring the live consumer over the seed snapshot, so cluster structure can rebuild from
   * up-to-date data while a behavior is active.
   */
  get currentValues() {
    return this.#consumer && !this.#reclaimed ? this.#consumer.snapshot() : this.initialValues;
  }
  async set(transaction, values) {
    let participant = transaction.getParticipant(this.#writer);
    if (participant === void 0) {
      participant = new RemoteWriteParticipant(this.#writer);
      transaction.addParticipants(participant);
    }
    const previousValues = this.#consumer?.readValues(new Set(Object.keys(values))) ?? {};
    participant.set(this.#endpointNumber, this.#behaviorId, values, {
      compensator: this,
      previousValues
    });
  }
  /**
   * Restores local cache values whose remote write was declined.
   *
   * Only restores keys that:
   *   - had a captured pre-write value (skip if the snapshot doesn't know the prior state), AND
   *   - still hold the value we attempted to write (skip if a concurrent subscription update already moved on).
   */
  async compensate(failedAttributeIds, previousValues, writtenValues) {
    if (this.#erased || this.#reclaimed || !this.#consumer) {
      return;
    }
    const restore = /* @__PURE__ */ new Map();
    const current = this.#consumer.readValues(failedAttributeIds);
    for (const id of failedAttributeIds) {
      if (!(id in previousValues)) {
        continue;
      }
      if (current[id] !== writtenValues[id]) {
        continue;
      }
      restore.set(id, previousValues[id]);
    }
    if (restore.size) {
      await this.externalSet(restore);
    }
  }
  async externalSet(values) {
    if (this.#erased || this.#reclaimed) {
      return;
    }
    const versionVal = values.get(DatasourceCache.VERSION_KEY);
    if (typeof versionVal === "number") {
      this.#version = versionVal;
    }
    if (this.#buffer) {
      for (const key of values.keys()) {
        this.#dirtyKeys.add(String(key));
      }
      this.#buffer.markDirty(this);
    } else {
      const valuesStruct = Object.fromEntries(values);
      await this.#localWriter?.persist(this.#endpointNumber, this.#behaviorId, valuesStruct);
    }
    if (this.consumer) {
      await this.consumer.integrateExternalChange(values);
    } else {
      if (!this.initialValues) {
        this.initialValues = {};
      }
      Object.assign(this.initialValues, Object.fromEntries(values));
    }
  }
  /**
   * Reclaim {@link initialValues} from the active datasource so the cache can be detached.  Blocks
   * {@link externalSet} until a new {@link consumer} is assigned, preventing writes to the released datasource.
   */
  reclaimValues() {
    if (this.consumer) {
      this.initialValues = this.consumer.releaseValues();
    }
    this.#reclaimed = true;
  }
  get version() {
    return this.#version;
  }
  set version(_version) {
    throw new InternalError("Datasource version must be set via externalSet");
  }
  get erased() {
    return this.#erased;
  }
  /**
   * Disable this cache in memory without touching persisted storage.
   */
  discard() {
    this.#erased = true;
    this.#dirtyKeys.clear();
    this.#buffer?.removeDirty(this);
  }
  /**
   * Erase values just for this datasource.  After this call, {@link externalSet} is permanently disabled.
   */
  async erase() {
    this.discard();
    await this.#localWriter?.erase(this.#endpointNumber, this.#behaviorId);
  }
  /**
   * Re-add keys that were cleared during a successful {@link flush} but whose transaction subsequently failed to
   * commit.  Without this, a commit failure would silently lose buffered writes.
   */
  restoreDirtyKeys(keys) {
    if (this.#erased) {
      return;
    }
    for (const key of keys) {
      this.#dirtyKeys.add(key);
    }
  }
  /**
   * Flush dirty values to storage.  When {@link tx} is provided, writes go through the shared transaction.
   *
   * Returns the set of keys that were flushed, so the caller can restore them via {@link restoreDirtyKeys} if
   * the enclosing transaction fails to commit.
   */
  async flush(tx) {
    if (this.#erased || !this.#dirtyKeys.size) {
      return;
    }
    const flushing = new Set(this.#dirtyKeys);
    for (const key of flushing) {
      this.#dirtyKeys.delete(key);
    }
    let values = this.consumer?.readValues(flushing);
    if (values === void 0 || !Object.keys(values).length) {
      if (this.initialValues !== void 0) {
        values = {};
        for (const key of flushing) {
          if (key in this.initialValues) {
            values[key] = this.initialValues[key];
          }
        }
      }
    }
    if (values === void 0 || !Object.keys(values).length) {
      for (const key of flushing) {
        this.#dirtyKeys.add(key);
      }
      this.#buffer?.markDirty(this);
      return;
    }
    values[DatasourceCache.VERSION_KEY] = this.#version;
    try {
      if (tx && this.#localWriter) {
        await this.#localWriter.persistInTransaction(tx, this.#endpointNumber, this.#behaviorId, values);
      } else {
        await this.#localWriter?.persist(this.#endpointNumber, this.#behaviorId, values);
      }
    } catch (e) {
      for (const key of flushing) {
        this.#dirtyKeys.add(key);
      }
      throw e;
    }
    return flushing;
  }
}
((DatasourceCache2) => {
  DatasourceCache2.VERSION_KEY = "__version__";
})(DatasourceCache || (DatasourceCache = {}));
export {
  DatasourceCache
};
//# sourceMappingURL=DatasourceCache.js.map
