"use strict";
// ─── Atomic file write + in-process file lock ──────────────────────────
//
// Two primitives that together close the read-modify-write race class
// the system audit flagged on .harness/automations.json and friends.
// Both are pure-stdlib; no new dependencies.
//
// Threat model: single Node process. Two async paths (e.g. the automation
// scheduler's tick and a UI route handler) can interleave their
// read-mutate-write of the same JSON file and lose data.
//
// Out of scope: cross-process locking. The harness assumes a single
// server process; if that ever changes, swap `withFileLock` for a
// real flock-style implementation (or `proper-lockfile`).
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.withFileLock = withFileLock;
exports.atomicWriteFile = atomicWriteFile;
exports._resetFileLocksForTest = _resetFileLocksForTest;
const fs_1 = require("fs");
const path = __importStar(require("path"));
// ─── In-process mutex per file path ─────────────────────────────────────
const chains = new Map();
/**
 * Serialize async work on a file path within this process. Returns the
 * result of `fn`. While `fn` is in flight, any other call with the same
 * absolute path queues and runs after `fn` settles.
 *
 * The internal Promise chain always resolves (never rejects), so one
 * caller's failure does NOT poison subsequent callers. The original
 * rejection is rethrown to the failing caller only.
 *
 * Memory: one Promise reference per unique path that's ever been locked.
 * Bounded by the number of distinct files the harness uses — small.
 *
 * Caller MUST pass an absolute path. Relative paths can alias across
 * cwd changes and break the contract.
 */
async function withFileLock(absolutePath, fn) {
    if (!path.isAbsolute(absolutePath)) {
        throw new Error(`withFileLock requires an absolute path, got: ${absolutePath}`);
    }
    const prev = (chains.get(absolutePath) ?? Promise.resolve());
    let outcome;
    const step = prev.then(async () => {
        try {
            outcome = { ok: true, value: await fn() };
        }
        catch (error) {
            outcome = { ok: false, error };
        }
    });
    chains.set(absolutePath, step);
    await step;
    if (!outcome.ok)
        throw outcome.error;
    return outcome.value;
}
// ─── Atomic write ───────────────────────────────────────────────────────
const RENAME_RETRY_DELAYS_MS = [25, 75, 150, 300, 600];
/**
 * Crash-safe write: writes to a unique sibling temp file then renames
 * over the destination. The temp file lives in the same directory so
 * `rename` is atomic on POSIX and atomic-ish on Windows (with retry on
 * the transient EPERM/EBUSY/EACCES that the OS surfaces when antivirus
 * or another reader briefly holds the destination).
 *
 * On rename failure the temp file is cleaned up best-effort so the
 * directory does not accumulate orphan `.tmp.*` files.
 */
async function atomicWriteFile(absolutePath, data, options = {}) {
    if (!path.isAbsolute(absolutePath)) {
        throw new Error(`atomicWriteFile requires an absolute path, got: ${absolutePath}`);
    }
    const dir = path.dirname(absolutePath);
    const base = path.basename(absolutePath);
    await fs_1.promises.mkdir(dir, { recursive: true });
    // Suffix includes pid + random to avoid collision when multiple
    // processes accidentally share a target (shouldn't happen, but
    // cheap insurance).
    const suffix = `${process.pid}.${Math.random().toString(36).slice(2, 10)}`;
    const tmpPath = path.join(dir, `.${base}.tmp.${suffix}`);
    await fs_1.promises.writeFile(tmpPath, data, { encoding: options.encoding ?? 'utf-8', mode: options.mode });
    try {
        await renameWithRetry(tmpPath, absolutePath);
    }
    catch (error) {
        // Clean up the orphan temp on rename failure. Best-effort.
        try {
            await fs_1.promises.unlink(tmpPath);
        }
        catch { /* ignore */ }
        throw error;
    }
}
async function renameWithRetry(tmpPath, targetPath) {
    for (let attempt = 0; attempt <= RENAME_RETRY_DELAYS_MS.length; attempt += 1) {
        try {
            await fs_1.promises.rename(tmpPath, targetPath);
            return;
        }
        catch (error) {
            if (!isTransientWindowsRenameError(error) || attempt === RENAME_RETRY_DELAYS_MS.length)
                throw error;
            await delay(RENAME_RETRY_DELAYS_MS[attempt]);
        }
    }
}
function isTransientWindowsRenameError(error) {
    if (process.platform !== 'win32')
        return false;
    const code = error.code;
    return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES';
}
function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/** Test-only: clear the lock chain map. Do not use in production code. */
function _resetFileLocksForTest() {
    chains.clear();
}
//# sourceMappingURL=atomicFile.js.map