// src/access-idempotency.ts
import { mkdir, open, readFile, rename, stat, unlink, utimes, writeFile } from "fs/promises";
import path from "path";
import { createHash } from "crypto";
var testHooks = null;
function stableStringify(value) {
  return JSON.stringify(value, (_key, candidate) => {
    if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
      return candidate;
    }
    return Object.keys(candidate).sort().reduce((acc, key) => {
      acc[key] = candidate[key];
      return acc;
    }, {});
  });
}
function hashAccessIdempotencyPayload(payload) {
  return createHash("sha256").update(stableStringify(payload)).digest("hex");
}
function setAccessIdempotencyTestHooks(hooks) {
  testHooks = hooks;
}
var AccessIdempotencyStore = class {
  statePath;
  lockPath;
  keyLockDir;
  loadedMtimeMs = 0;
  state = {};
  writeQueue = Promise.resolve();
  constructor(memoryDir) {
    this.statePath = path.join(memoryDir, "state", "access-idempotency.json");
    this.lockPath = `${this.statePath}.lock`;
    this.keyLockDir = path.join(memoryDir, "state", "access-idempotency-locks");
  }
  async get(key, requestHash) {
    await this.writeQueue.catch(() => {
    });
    await this.reload({ forceRefresh: true });
    const entry = this.state[key];
    if (!entry) return { conflict: false };
    if (entry.requestHash !== requestHash) {
      return { conflict: true };
    }
    return {
      conflict: false,
      response: JSON.parse(JSON.stringify(entry.response))
    };
  }
  async put(key, requestHash, response) {
    await this.withWriteQueue(async () => {
      await this.reload({ forceRefresh: true });
      this.state[key] = {
        recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
        requestHash,
        response: JSON.parse(JSON.stringify(response))
      };
      await this.prune();
      await this.flush();
    });
  }
  async withKeyLock(key, callback) {
    const keyHash = createHash("sha256").update(key).digest("hex");
    const keyLockPath = path.join(this.keyLockDir, `${keyHash}.lock`);
    return this.withExclusiveFileLock(keyLockPath, callback);
  }
  async reload(options = {}) {
    if (options.forceRefresh === true) {
      try {
        const fileStat = await stat(this.statePath);
        if (fileStat.mtimeMs < this.loadedMtimeMs) {
          return;
        }
      } catch {
        this.state = {};
        this.loadedMtimeMs = 0;
        return;
      }
    }
    try {
      const raw = await readFile(this.statePath, "utf-8");
      const parsed = JSON.parse(raw);
      if (parsed && typeof parsed === "object") {
        this.state = parsed;
        this.loadedMtimeMs = await this.readMtimeMs();
        return;
      }
    } catch {
    }
    this.state = {};
    this.loadedMtimeMs = 0;
  }
  async prune() {
    const entries = Object.entries(this.state);
    if (entries.length <= 512) return;
    entries.sort((left, right) => right[1].recordedAt.localeCompare(left[1].recordedAt)).slice(512).forEach(([key]) => {
      delete this.state[key];
    });
  }
  async flush() {
    await mkdir(path.dirname(this.statePath), { recursive: true });
    await this.withExclusiveFileLock(this.lockPath, async () => {
      try {
        const raw = await readFile(this.statePath, "utf-8");
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === "object") {
          this.state = {
            ...parsed,
            ...this.state
          };
          await this.prune();
        }
      } catch {
      }
      await testHooks?.beforeFlushWrite?.();
      const tempPath = `${this.statePath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
      try {
        await writeFile(tempPath, JSON.stringify(this.state, null, 2), "utf-8");
        await rename(tempPath, this.statePath);
      } finally {
        await unlink(tempPath).catch(() => void 0);
      }
      this.loadedMtimeMs = await this.readMtimeMs();
    });
  }
  async readMtimeMs() {
    try {
      return (await stat(this.statePath)).mtimeMs;
    } catch {
      return 0;
    }
  }
  async withExclusiveFileLock(lockPath, callback) {
    await mkdir(path.dirname(lockPath), { recursive: true });
    const timeoutMs = testHooks?.lockTimeoutMs ?? 5e3;
    const staleLockMs = testHooks?.staleLockMs ?? 3e4;
    const lockHeartbeatMs = testHooks?.lockHeartbeatMs ?? Math.max(1e3, Math.floor(staleLockMs / 3));
    const startedAt = Date.now();
    while (true) {
      try {
        const handle = await open(lockPath, "wx");
        let heartbeat = null;
        if (lockHeartbeatMs > 0) {
          heartbeat = setInterval(() => {
            void utimes(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
          }, lockHeartbeatMs);
          heartbeat.unref?.();
        }
        try {
          return await callback();
        } finally {
          if (heartbeat) {
            clearInterval(heartbeat);
          }
          await handle.close().catch(() => void 0);
          await unlink(lockPath).catch(() => void 0);
        }
      } catch (error) {
        if (!isAlreadyExistsError(error)) throw error;
        try {
          const lockStat = await stat(lockPath);
          if (Date.now() - lockStat.mtimeMs > staleLockMs) {
            await unlink(lockPath).catch(() => void 0);
            continue;
          }
        } catch {
          continue;
        }
        if (Date.now() - startedAt > timeoutMs) {
          throw new Error("timed out acquiring access idempotency lock");
        }
        await sleep(10);
      }
    }
  }
  async withWriteQueue(callback) {
    const previous = this.writeQueue;
    let release;
    this.writeQueue = new Promise((resolve) => {
      release = resolve;
    });
    await previous.catch(() => {
    });
    try {
      return await callback();
    } finally {
      release();
    }
  }
};
function isAlreadyExistsError(error) {
  return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
}
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export {
  hashAccessIdempotencyPayload,
  setAccessIdempotencyTestHooks,
  AccessIdempotencyStore
};
//# sourceMappingURL=chunk-XKECPATV.js.map