import { RWLock } from "@rocicorp/lock";
import { joinIterables } from "../../../shared/src/iterables.js";
import { promiseVoid } from "../../../shared/src/resolved-promises.js";
import { getSizeOfValue } from "../../../shared/src/size-of-value.js";
import { createChunk } from "./chunk.js";
import { computeRefCountUpdates } from "./gc.js";
import { mustGetChunk } from "./store.js";
class LazyStore {
  /**
   * This lock is used to ensure correct isolation of Reads and Writes.
   * Multiple Reads are allowed in parallel but only a single Write.  Reads and
   * Writes see an isolated view of the store (corresponding to the Serializable
   * level of transaction isolation defined in the SQL standard).
   *
   * To ensure these semantics the read lock must be acquired when a Read is
   * created and held til it is closed, and a Write lock must be acquired when a
   * Write is created and held til it is committed or closed.
   *
   * Code must have a read or write lock to
   * - read `_heads`
   * - read `_memOnlyChunks`
   * - read `_sourceStore`
   * - read and write `_sourceChunksCache`
   * - read and write `_refCounts`
   * - read and write `_refs`
   * and must have a write lock to
   * - write `_heads`
   * - write `_memOnlyChunks`
   */
  #rwLock = new RWLock();
  #heads = /* @__PURE__ */ new Map();
  #sourceStore;
  #chunkHasher;
  #assertValidHash;
  /** The following are protected so testing subclass can access. */
  _memOnlyChunks = /* @__PURE__ */ new Map();
  _sourceChunksCache;
  /**
   * Ref counts are maintained so that chunks which are unreachable
   * from this stores heads can be eagerly and deterministically deleted from
   * `this._memOnlyChunks` and `this._sourceChunksCache`.
   *
   * These ref counts are independent from `this._sourceStore`'s ref counts.
   * These ref counts are based on reachability from `this._heads`.
   * A chunk is deleted from `this._memOnlyChunks` or
   * `this._sourceChunksCache` (which ever it is in) when its ref count becomes
   * zero.
   * These ref counts count the refs in `this._heads` and `this._refs`.
   *
   * Not all reachable chunk's refs are included in `this._refs`, because this
   * would require loading all chunks reachable in the source store in a
   * non-lazy manner. `this._refs` contains the refs of all currently reachable
   * chunks that were ever in `this._memOnlyChunks` or
   * `this._sourceChunksCache` (even if they have been evicted).  A
   * chunk's ref information is lazily discovered and stored in `this._refs` and
   * counted in `this._refCounts`. A chunk's entries in `this._refs` and
   * `this._refCounts` are only deleted when a chunk is deleted due to it
   * becoming unreachable (it is not deleted if the chunk is evicted from the
   * source-store cache).
   *
   * The major implication of this lazy discovery of source store refs, is that
   * a reachable source store chunk may not be cached when loaded, because it is
   * not known to be reachable because some of the pertinent refs have not been
   * discovered.  However, in practice chunks are read by traversing the graph
   * starting from a head, and all pertinent refs are discovered as part of the
   * traversal.
   *
   * These ref counts can be changed in two ways:
   * 1. A LazyRead has a cache miss and loads a chunk from the source store that
   * is reachable from this._heads.  If this chunk's refs are not currently
   * counted, it will not have an entry in `this._refs`.  In this case, the
   * chunks refs will be put in `this._refs` and `this._refCounts` will be
   * updated to count them.
   * 2. A LazyWrite commit updates a head (which can result in increasing or
   * decreasing ref count) or puts a reachable chunk (either a `memory-only` or
   * `source` chunk) that references this hash (increasing ref count).  The
   * computation of these ref count changes is delegated to the
   * `computeRefCountUpdates` shared with dag.StoreImpl.  In order to
   * delegate determining reachability to `computeRefCountUpdates` and defer
   * this determination until commit time, LazyWrite treats cache misses
   * as a 'put' of the lazily-loaded chunk.
   *
   * A chunk's hash may have an entry in `this._refCounts` without that
   * chunk have ever been in `this._memOnlyChunks` or `this._sourceChunksCache`.
   * This is the case when a head or a reachable chunk that was ever in
   * `this._memOnlyChunks` or `this._sourceChunksCache` references a chunk
   * which is not currently cached (either because it has not been read, or
   * because it has been evicted).
   */
  _refCounts = /* @__PURE__ */ new Map();
  _refs = /* @__PURE__ */ new Map();
  constructor(sourceStore, sourceCacheSizeLimit, chunkHasher, assertValidHash, getSizeOfChunk = getSizeOfValue) {
    this._sourceChunksCache = new ChunksCache(
      sourceCacheSizeLimit,
      getSizeOfChunk,
      this._refCounts,
      this._refs
    );
    this.#sourceStore = sourceStore;
    this.#chunkHasher = chunkHasher;
    this.#assertValidHash = assertValidHash;
  }
  async read(sourceRead) {
    const release = await this.#rwLock.read();
    return new LazyRead(
      this.#heads,
      this._memOnlyChunks,
      this._sourceChunksCache,
      this.#sourceStore,
      release,
      this.#assertValidHash,
      sourceRead
    );
  }
  async write() {
    const release = await this.#rwLock.write();
    return new LazyWrite(
      this.#heads,
      this._memOnlyChunks,
      this._sourceChunksCache,
      this.#sourceStore,
      this._refCounts,
      this._refs,
      release,
      this.#chunkHasher,
      this.#assertValidHash
    );
  }
  close() {
    return promiseVoid;
  }
  /**
   * Does not acquire any lock on the store.
   */
  isCached(chunkHash) {
    return this._sourceChunksCache.getWithoutUpdatingLRU(chunkHash) !== void 0;
  }
  withSuspendedSourceCacheEvictsAndDeletes(fn) {
    return this._sourceChunksCache.withSuspendedEvictsAndDeletes(fn);
  }
}
class LazyRead {
  _heads;
  _memOnlyChunks;
  _sourceChunksCache;
  _sourceStore;
  #sourceRead = void 0;
  #release;
  #closed = false;
  assertValidHash;
  #sourceReadOwnedByCaller;
  constructor(heads, memOnlyChunks, sourceChunksCache, sourceStore, release, assertValidHash, sourceRead) {
    this._heads = heads;
    this._memOnlyChunks = memOnlyChunks;
    this._sourceChunksCache = sourceChunksCache;
    this._sourceStore = sourceStore;
    this.#release = release;
    this.assertValidHash = assertValidHash;
    this.#sourceRead = sourceRead !== void 0 ? Promise.resolve(sourceRead) : void 0;
    this.#sourceReadOwnedByCaller = sourceRead !== void 0;
  }
  isMemOnlyChunkHash(hash) {
    return this._memOnlyChunks.has(hash);
  }
  async hasChunk(hash) {
    return await this.getChunk(hash) !== void 0;
  }
  async getChunk(hash) {
    const memOnlyChunk = this._memOnlyChunks.get(hash);
    if (memOnlyChunk !== void 0) {
      return memOnlyChunk;
    }
    let chunk = this._sourceChunksCache.get(hash);
    if (chunk === void 0) {
      chunk = await (await this._getSourceRead()).getChunk(hash);
      if (chunk !== void 0) {
        this._sourceChunksCache.put(chunk);
      }
    }
    return chunk;
  }
  mustGetChunk(hash) {
    return mustGetChunk(this, hash);
  }
  getHead(name) {
    return Promise.resolve(this._heads.get(name));
  }
  release() {
    if (!this.#closed) {
      this.#release();
      if (!this.#sourceReadOwnedByCaller) {
        this.#sourceRead?.then((read) => read.release()).catch((_) => {
        });
      }
      this.#closed = true;
    }
  }
  get closed() {
    return this.#closed;
  }
  _getSourceRead() {
    if (!this.#sourceRead) {
      this.#sourceRead = this._sourceStore.read();
    }
    return this.#sourceRead;
  }
}
class LazyWrite extends LazyRead {
  #refCounts;
  #refs;
  #chunkHasher;
  _pendingHeadChanges = /* @__PURE__ */ new Map();
  _pendingMemOnlyChunks = /* @__PURE__ */ new Map();
  _pendingCachedChunks = /* @__PURE__ */ new Map();
  #createdChunks = /* @__PURE__ */ new Set();
  constructor(heads, memOnlyChunks, sourceChunksCache, sourceStore, refCounts, refs, release, chunkHasher, assertValidHash) {
    super(
      heads,
      memOnlyChunks,
      sourceChunksCache,
      sourceStore,
      release,
      assertValidHash
    );
    this.#refCounts = refCounts;
    this.#refs = refs;
    this.#chunkHasher = chunkHasher;
  }
  createChunk = (data, refs) => {
    const chunk = createChunk(data, refs, this.#chunkHasher);
    this.#createdChunks.add(chunk.hash);
    return chunk;
  };
  putChunk(c, size) {
    const { hash, meta } = c;
    this.assertValidHash(hash);
    if (meta.length > 0) {
      for (const h of meta) {
        this.assertValidHash(h);
      }
    }
    if (this.#createdChunks.has(hash) || this.isMemOnlyChunkHash(hash)) {
      this._pendingMemOnlyChunks.set(hash, c);
    } else {
      this._pendingCachedChunks.set(hash, { chunk: c, size: size ?? -1 });
    }
    return promiseVoid;
  }
  async setHead(name, hash) {
    await this.#setHead(name, hash);
  }
  async removeHead(name) {
    await this.#setHead(name, void 0);
  }
  async #setHead(name, hash) {
    const oldHash = await this.getHead(name);
    const v = this._pendingHeadChanges.get(name);
    if (v === void 0) {
      this._pendingHeadChanges.set(name, { new: hash, old: oldHash });
    } else {
      v.new = hash;
    }
  }
  isMemOnlyChunkHash(hash) {
    return this._pendingMemOnlyChunks.has(hash) || super.isMemOnlyChunkHash(hash);
  }
  async getChunk(hash) {
    const pendingMemOnlyChunk = this._pendingMemOnlyChunks.get(hash);
    if (pendingMemOnlyChunk !== void 0) {
      return pendingMemOnlyChunk;
    }
    const memOnlyChunk = this._memOnlyChunks.get(hash);
    if (memOnlyChunk !== void 0) {
      return memOnlyChunk;
    }
    const pendingCachedChunk = this._pendingCachedChunks.get(hash);
    if (pendingCachedChunk !== void 0) {
      return pendingCachedChunk.chunk;
    }
    let chunk = this._sourceChunksCache.get(hash);
    if (chunk === void 0) {
      chunk = await (await this._getSourceRead()).getChunk(hash);
      if (chunk !== void 0) {
        this._pendingCachedChunks.set(chunk.hash, { chunk, size: -1 });
      }
    }
    return chunk;
  }
  getHead(name) {
    const headChange = this._pendingHeadChanges.get(name);
    if (headChange) {
      return Promise.resolve(headChange.new);
    }
    return super.getHead(name);
  }
  async commit() {
    const pendingChunks = new Set(
      joinIterables(
        this._pendingMemOnlyChunks.keys(),
        this._pendingCachedChunks.keys()
      )
    );
    const refCountUpdates = await computeRefCountUpdates(
      this._pendingHeadChanges.values(),
      pendingChunks,
      this
    );
    for (const [hash, count] of refCountUpdates) {
      if (this.isMemOnlyChunkHash(hash)) {
        if (count === 0) {
          this.#refCounts.delete(hash);
          this._memOnlyChunks.delete(hash);
          this.#refs.delete(hash);
        } else {
          this.#refCounts.set(hash, count);
          const chunk = this._pendingMemOnlyChunks.get(hash);
          if (chunk) {
            this.#refs.set(hash, chunk.meta);
            this._memOnlyChunks.set(hash, chunk);
          }
        }
        refCountUpdates.delete(hash);
      }
    }
    this._sourceChunksCache.updateForCommit(
      this._pendingCachedChunks,
      refCountUpdates
    );
    for (const [name, headChange] of this._pendingHeadChanges) {
      if (headChange.new) {
        this._heads.set(name, headChange.new);
      } else {
        this._heads.delete(name);
      }
    }
    this._pendingMemOnlyChunks.clear();
    this._pendingCachedChunks.clear();
    this._pendingHeadChanges.clear();
    this.release();
  }
  getRefCount(hash) {
    return this.#refCounts.get(hash);
  }
  getRefs(hash) {
    const pendingMemOnlyChunk = this._pendingMemOnlyChunks.get(hash);
    if (pendingMemOnlyChunk) {
      return pendingMemOnlyChunk.meta;
    }
    const memOnlyChunk = this._memOnlyChunks.get(hash);
    if (memOnlyChunk) {
      return memOnlyChunk.meta;
    }
    const pendingCachedChunk = this._pendingCachedChunks.get(hash);
    if (pendingCachedChunk !== void 0) {
      return pendingCachedChunk.chunk.meta;
    }
    return this.#refs.get(hash);
  }
  areRefsCounted(hash) {
    return this.#refs.has(hash);
  }
  chunksPersisted(chunkHashes) {
    const chunksToCache = [];
    for (const chunkHash of chunkHashes) {
      const chunk = this._memOnlyChunks.get(chunkHash);
      if (chunk) {
        this._memOnlyChunks.delete(chunkHash);
        chunksToCache.push(chunk);
      }
    }
    this._sourceChunksCache.persisted(chunksToCache);
  }
}
class ChunksCache {
  #cacheSizeLimit;
  #getSizeOfChunk;
  #refCounts;
  #refs;
  #size = 0;
  #evictsAndDeletesSuspended = false;
  #suspendedDeletes = [];
  /**
   * Iteration order is from least to most recently used.
   *
   * Public so that testing subclass can access.
   */
  cacheEntries = /* @__PURE__ */ new Map();
  constructor(cacheSizeLimit, getSizeOfChunk, refCounts, refs) {
    this.#cacheSizeLimit = cacheSizeLimit;
    this.#getSizeOfChunk = getSizeOfChunk;
    this.#refCounts = refCounts;
    this.#refs = refs;
  }
  get(hash) {
    const cacheEntry = this.cacheEntries.get(hash);
    if (cacheEntry) {
      this.cacheEntries.delete(hash);
      this.cacheEntries.set(hash, cacheEntry);
    }
    return cacheEntry?.chunk;
  }
  getWithoutUpdatingLRU(hash) {
    return this.cacheEntries.get(hash)?.chunk;
  }
  put(chunk) {
    const { hash } = chunk;
    const oldCacheEntry = this.cacheEntries.get(hash);
    if (oldCacheEntry) {
      this.cacheEntries.delete(hash);
      this.cacheEntries.set(hash, oldCacheEntry);
      return;
    }
    const refCount = this.#refCounts.get(hash);
    if (refCount === void 0 || refCount < 1) {
      return;
    }
    if (!this.#cacheChunk(chunk)) {
      return;
    }
    if (!this.#refs.has(hash)) {
      for (const refHash of chunk.meta) {
        this.#refCounts.set(refHash, (this.#refCounts.get(refHash) || 0) + 1);
      }
      this.#refs.set(hash, chunk.meta);
    }
    this.#ensureCacheSizeLimit();
  }
  #ensureCacheSizeLimit() {
    if (this.#evictsAndDeletesSuspended) {
      return;
    }
    for (const entry of this.cacheEntries.values()) {
      if (this.#size <= this.#cacheSizeLimit) {
        break;
      }
      this.#evict(entry);
    }
  }
  #cacheChunk(chunk, size) {
    const chunkSize = size ?? this.#getSizeOfChunk(chunk);
    if (chunkSize > this.#cacheSizeLimit) {
      return false;
    }
    this.#size += chunkSize;
    this.cacheEntries.set(chunk.hash, { chunk, size: chunkSize });
    return true;
  }
  #evict(cacheEntry) {
    const { hash } = cacheEntry.chunk;
    this.#size -= cacheEntry.size;
    this.cacheEntries.delete(hash);
  }
  #deleteEntryByHash(hash) {
    this.#refCounts.delete(hash);
    this.#refs.delete(hash);
    const cacheEntry = this.cacheEntries.get(hash);
    if (cacheEntry) {
      this.#size -= cacheEntry.size;
      this.cacheEntries.delete(hash);
    }
  }
  updateForCommit(chunksToPut, refCountUpdates) {
    for (const [hash, count] of refCountUpdates) {
      if (count === 0) {
        if (!this.#evictsAndDeletesSuspended) {
          this.#deleteEntryByHash(hash);
        } else {
          this.#refCounts.set(hash, 0);
          this.#suspendedDeletes.push(hash);
        }
      } else {
        this.#refCounts.set(hash, count);
        const chunkAndSize = chunksToPut.get(hash);
        if (chunkAndSize) {
          const { chunk, size } = chunkAndSize;
          const oldCacheEntry = this.cacheEntries.get(hash);
          if (oldCacheEntry) {
            this.cacheEntries.delete(hash);
            this.cacheEntries.set(hash, oldCacheEntry);
          } else {
            this.#cacheChunk(chunk, size !== -1 ? size : void 0);
            this.#refs.set(hash, chunk.meta);
          }
        }
      }
    }
    this.#ensureCacheSizeLimit();
  }
  persisted(chunks) {
    for (const chunk of chunks) {
      this.#cacheChunk(chunk);
    }
    this.#ensureCacheSizeLimit();
  }
  async withSuspendedEvictsAndDeletes(fn) {
    this.#evictsAndDeletesSuspended = true;
    try {
      return await fn();
    } finally {
      this.#evictsAndDeletesSuspended = false;
      for (const hash of this.#suspendedDeletes) {
        if (this.#refCounts.get(hash) === 0) {
          this.#deleteEntryByHash(hash);
        }
      }
      this.#ensureCacheSizeLimit();
    }
  }
}
export {
  LazyRead,
  LazyStore,
  LazyWrite
};
//# sourceMappingURL=lazy-store.js.map
