var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
var __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Logger } from "#log/Logger.js";
import { Hours } from "#time/TimeUnit.js";
import { Abort } from "#util/Abort.js";
import { Gzip } from "#util/Gzip.js";
import { BasicMultiplex } from "#util/Multiplex.js";
import { FilesystemStorageDriver, StorageError } from "../StorageDriver.js";
import { WalCleaner } from "./WalCleaner.js";
import {
  compareCommitIds,
  compressedSegmentFilename,
  parseSegmentFilename,
  segmentFilename
} from "./WalCommit.js";
import { WalReader } from "./WalReader.js";
import { WalSnapshot } from "./WalSnapshot.js";
import { WalTransaction, applyCommit } from "./WalTransaction.js";
import { WalWriter } from "./WalWriter.js";
const logger = Logger.get("WalStorageDriver");
class WalStorageDriver extends FilesystemStorageDriver {
  static id = "wal";
  static async create(namespace, descriptor) {
    const storage = new WalStorageDriver(namespace, {
      maxSegmentSize: descriptor.maxSegmentSize,
      fsync: descriptor.fsync,
      compressSnapshot: descriptor.compressSnapshot,
      compressLog: descriptor.compressLog,
      headSnapshot: descriptor.headSnapshot
    });
    try {
      await storage.initialize();
    } catch (error) {
      await storage.close().catch(() => {
      });
      throw error;
    }
    return storage;
  }
  #storageDir;
  #options;
  #cache;
  #cacheLoading;
  #pendingOps;
  #abort = new Abort();
  #workers = new BasicMultiplex();
  #initialized = false;
  #lastCommitId;
  #lastCommitTs;
  #lastSnapshotCommitId;
  #writer;
  #reader;
  #cleaner;
  #compressSnapshot = true;
  constructor(namespace, options) {
    super(namespace);
    this.#storageDir = options?.storageDir ?? this.root.directory;
    this.#options = options ?? {};
  }
  get initialized() {
    return this.#initialized;
  }
  async initialize() {
    if (this.#initialized) {
      throw new StorageError("WalStorageDriver already initialized");
    }
    await super.initialize();
    this.#abort = new Abort();
    this.#workers = new BasicMultiplex();
    this.#cache = void 0;
    this.#pendingOps = void 0;
    this.#lastCommitId = void 0;
    this.#lastCommitTs = void 0;
    this.#lastSnapshotCommitId = void 0;
    await this.#storageDir.mkdir();
    const walDir = this.#storageDir.directory("wal");
    await walDir.mkdir();
    const compressLog = this.#options.compressLog ?? WalStorageDriver.defaults.compressLog;
    this.#reader = new WalReader(walDir);
    this.#writer = new WalWriter(walDir, {
      name: this.root?.namespace,
      maxSegmentSize: this.#options.maxSegmentSize,
      fsync: this.#options.fsync,
      onRotate: compressLog ? () => {
        this.#workers.add(this.#compressRotatedSegments(), "wal-compress");
      } : void 0
    });
    this.#compressSnapshot = this.#options.compressSnapshot ?? WalStorageDriver.defaults.compressSnapshot;
    const headSnapshot = this.#options.headSnapshot ?? WalStorageDriver.defaults.headSnapshot;
    this.#cleaner = new WalCleaner(
      walDir,
      headSnapshot ? {
        dir: this.#storageDir,
        compress: this.#compressSnapshot,
        reader: this.#reader
      } : void 0
    );
    const snapshotInterval = this.#options.snapshotInterval ?? WalStorageDriver.defaults.snapshotInterval;
    this.#workers.add(
      this.#runWorker("wal-snapshot", snapshotInterval, () => this.#runSnapshot()),
      "wal-snapshot"
    );
    const cleanInterval = this.#options.cleanInterval ?? WalStorageDriver.defaults.cleanInterval;
    if (cleanInterval !== void 0) {
      this.#workers.add(
        this.#runWorker("wal-clean", cleanInterval, () => this.#runClean()),
        "wal-clean"
      );
    }
    this.#initialized = true;
  }
  async close() {
    this.#abort();
    await this.#workers;
    if (this.#lastCommitId) {
      await this.#runSnapshot();
      await this.#runClean();
    }
    await this.#writer?.close();
    this.#cache = void 0;
    this.#initialized = false;
    await super.close();
  }
  async clone() {
    this.#assertInitialized();
    await this.#runSnapshot();
    const tempDir = this.#storageDir.fs.tempDirectory();
    await this.#storageDir.fs.copy(this.#storageDir, tempDir);
    const clone = new WalStorageDriver(void 0, { ...this.#options, storageDir: tempDir });
    await clone.initialize();
    return clone;
  }
  // --- Read operations (loaded from cache) ---
  async get(contexts, key) {
    this.#assertInitialized();
    const store = await this.#loadCache();
    const contextKey = this.#contextKey(contexts);
    return store[contextKey]?.[key];
  }
  async keys(contexts) {
    this.#assertInitialized();
    const store = await this.#loadCache();
    const contextKey = this.#contextKey(contexts);
    return Object.keys(store[contextKey] ?? {});
  }
  async values(contexts) {
    this.#assertInitialized();
    const store = await this.#loadCache();
    const contextKey = this.#contextKey(contexts);
    return { ...store[contextKey] ?? {} };
  }
  async contexts(contexts) {
    this.#assertInitialized();
    const store = await this.#loadCache();
    const contextKey = contexts.length ? contexts.join(".") : "";
    const prefix = contextKey.length ? `${contextKey}.` : "";
    const found = /* @__PURE__ */ new Set();
    for (const key of Object.keys(store)) {
      if (key.startsWith(prefix)) {
        const sub = key.substring(prefix.length).split(".");
        if (sub.length >= 1 && sub[0].length > 0) {
          found.add(sub[0]);
        }
      }
    }
    return [...found];
  }
  async set(contexts, keyOrValues, value) {
    var _stack = [];
    try {
      this.#assertInitialized();
      const tx = __using(_stack, await this.begin(), true);
      if (typeof keyOrValues === "string") {
        await tx.set(contexts, keyOrValues, value);
      } else {
        await tx.set(contexts, keyOrValues);
      }
      await tx.commit();
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      var _promise = __callDispose(_stack, _error, _hasError);
      _promise && await _promise;
    }
  }
  async delete(contexts, key) {
    var _stack = [];
    try {
      this.#assertInitialized();
      const tx = __using(_stack, await this.begin(), true);
      await tx.delete(contexts, key);
      await tx.commit();
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      var _promise = __callDispose(_stack, _error, _hasError);
      _promise && await _promise;
    }
  }
  async clearAll(contexts) {
    var _stack = [];
    try {
      this.#assertInitialized();
      const tx = __using(_stack, await this.begin(), true);
      await tx.clearAll(contexts);
      await tx.commit();
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      var _promise = __callDispose(_stack, _error, _hasError);
      _promise && await _promise;
    }
  }
  // --- Transactions ---
  async begin() {
    this.#assertInitialized();
    return new WalTransaction(this, this.#writer, (id, ts, ops) => {
      this.#lastCommitId = id;
      this.#lastCommitTs = ts;
      if (this.#cache !== void 0) {
        applyCommit(this.#cache, { ts, ops });
      } else if (this.#cacheLoading !== void 0 || this.#pendingOps !== void 0) {
        (this.#pendingOps ??= []).push({ ts, ops });
      }
      this.#cacheLoading = void 0;
    });
  }
  // --- Snapshot reconstruction ---
  /**
   * Reconstruct a snapshot as of the given timestamp.
   *
   * If `asOf` is omitted, returns the full current state.
   */
  async snapshotAtTime(asOf) {
    this.#assertInitialized();
    if (asOf !== void 0 && asOf > Date.now()) {
      throw new StorageError("Timestamp is in the future");
    }
    return this.#reconstruct(
      asOf !== void 0 ? (_id, commitTs) => commitTs > asOf : void 0,
      asOf !== void 0 ? (baseTs) => {
        if (baseTs > 0 && asOf < baseTs) {
          throw new StorageError("Timestamp predates available logs");
        }
      } : void 0
    );
  }
  /**
   * Reconstruct a snapshot at the given commit ID.
   *
   * If `commitId` is omitted, returns the full current state.
   */
  async snapshotAtCommit(commitId) {
    this.#assertInitialized();
    return this.#reconstruct(
      commitId !== void 0 ? (id, _commitTs) => compareCommitIds(id, commitId) > 0 : void 0
    );
  }
  // --- Internal ---
  async #loadCache() {
    if (this.#cache) {
      return this.#cache;
    }
    if (this.#cacheLoading) {
      return this.#cacheLoading;
    }
    this.#cacheLoading = this.#doLoadCache();
    try {
      return await this.#cacheLoading;
    } finally {
      this.#cacheLoading = void 0;
    }
  }
  async #doLoadCache() {
    const store = {};
    let afterCommitId;
    const snap = await WalSnapshot.load(this.#storageDir);
    if (snap) {
      Object.assign(store, snap.data);
      afterCommitId = snap.commitId;
      this.#lastSnapshotCommitId = snap.commitId;
      this.#lastCommitTs = snap.ts || void 0;
      logger.debug("Loaded snapshot at commit", snap.commitId);
    }
    let replayCount = 0;
    for await (const { id, commit } of this.#reader.read(afterCommitId)) {
      applyCommit(store, commit);
      this.#lastCommitId = id;
      this.#lastCommitTs = commit.ts || this.#lastCommitTs;
      replayCount++;
    }
    if (replayCount > 0) {
      logger.debug(`Replayed ${replayCount} WAL commits`);
    } else if (afterCommitId) {
      this.#lastCommitId = afterCommitId;
    }
    const pending = this.#pendingOps;
    if (pending !== void 0) {
      this.#pendingOps = void 0;
      for (const commit of pending) {
        applyCommit(store, commit);
      }
    }
    this.#cache = store;
    return store;
  }
  /**
   * Shared reconstruction logic for snapshotAtTime/snapshotAtCommit.
   *
   * @param shouldStop - if provided, called for each commit; return true to stop before applying that commit
   * @param validateBase - if provided, called with the base snapshot ts for pre-condition checks
   */
  async #reconstruct(shouldStop, validateBase) {
    const store = {};
    let afterCommitId;
    let lastId;
    let lastTs = 0;
    const snap = await WalSnapshot.load(this.#storageDir);
    if (snap) {
      Object.assign(store, snap.data);
      afterCommitId = snap.commitId;
      lastId = snap.commitId;
      lastTs = snap.ts;
    }
    validateBase?.(lastTs);
    for await (const { id, commit } of this.#reader.read(afterCommitId)) {
      if (shouldStop?.(id, commit.ts)) {
        break;
      }
      applyCommit(store, commit);
      lastId = id;
      lastTs = commit.ts || lastTs;
    }
    if (!lastId) {
      throw new StorageError("No data available");
    }
    return new WalSnapshot(lastId, lastTs, store);
  }
  async #runWorker(name, interval, task) {
    while (!this.#abort.aborted) {
      await this.#abort.sleep(name, interval);
      if (this.#abort.aborted) return;
      try {
        await task();
      } catch (e) {
        logger.warn(`${name} worker error:`, e);
      }
    }
  }
  async #runSnapshot() {
    if (!this.#lastCommitId) return;
    try {
      const store = await this.#loadCache();
      const snapshot = new WalSnapshot(this.#lastCommitId, this.#lastCommitTs ?? 0, store);
      await snapshot.save(this.#storageDir, { compress: this.#compressSnapshot });
      this.#lastSnapshotCommitId = this.#lastCommitId;
    } catch (e) {
      logger.warn("Snapshot failed:", e);
    }
  }
  async #runClean() {
    if (!this.#lastSnapshotCommitId) return;
    try {
      await this.#cleaner.run(this.#lastSnapshotCommitId);
    } catch (e) {
      logger.warn("WAL cleanup failed:", e);
    }
  }
  async #compressRotatedSegments() {
    const walDir = this.#storageDir.directory("wal");
    if (!await walDir.exists()) return;
    const activeSegment = this.#writer.currentSegment;
    for await (const entry of walDir.entries()) {
      if (entry.kind !== "file") continue;
      if (!entry.name.endsWith(".jsonl")) continue;
      const seg = parseSegmentFilename(entry.name);
      if (seg === void 0 || seg === activeSegment) continue;
      try {
        const gzName = compressedSegmentFilename(seg);
        const gzFile = walDir.file(gzName);
        if (await gzFile.exists()) {
          await entry.delete();
          continue;
        }
        const srcFile = walDir.file(segmentFilename(seg));
        const tmpName = gzName + ".tmp";
        const tmpFile = walDir.file(tmpName);
        await tmpFile.write(Gzip.compress(srcFile.readBytes()));
        await tmpFile.rename(gzName);
        await srcFile.delete();
      } catch (e) {
        logger.warn(`Failed to compress WAL segment ${seg}:`, e);
      }
    }
  }
  #contextKey(contexts) {
    for (const ctx of contexts) {
      if (!ctx.length || ctx.includes(".")) {
        throw new StorageError("Context must not contain empty segments or leading or trailing dots.");
      }
    }
    return contexts.join(".");
  }
  #assertInitialized() {
    if (!this.#initialized) {
      throw new StorageError("WalStorageDriver is not initialized");
    }
  }
}
((WalStorageDriver2) => {
  WalStorageDriver2.defaults = {
    maxSegmentSize: 16 * 1024 * 1024,
    fsync: true,
    snapshotInterval: Hours(6),
    cleanInterval: void 0,
    compressSnapshot: Gzip.isAvailable,
    compressLog: Gzip.isAvailable,
    headSnapshot: true
  };
})(WalStorageDriver || (WalStorageDriver = {}));
export {
  WalStorageDriver
};
//# sourceMappingURL=WalStorageDriver.js.map
