core/persistence/edit.persistence.ts

core/persistence/edit.persistence.ts is a file in GovLab Quality. 155 lines of code and 34 definitions.

import type {
    CommitContext,
    FixAction,
    FixableResult,
    SafeFixFs,
    SafeFixOptions,
    SafeFixOutcome,
} from "#types/edit.types";
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
import { isInsideRoot } from "#core/predicates/location.predicate";
import { outsideWorkspace } from "#configuration/strings/tool.strings";
import path from "node:path";
import { relativePath } from "@ssot/paths";
import { writeVerbatim } from "@govlab/canonical-write";

const TEMP_SUFFIX = ".govlab-fix-tmp";
const BACKUP_FOLDER = "fix-backup";
const TEMP_FOLDER = "fix-tmp";
const RETRY_1_MS = 40;
const RETRY_2_MS = 120;
const RETRY_3_MS = 360;
const NO_DELAY = 0;
const RETRY_DELAYS_MS = [NO_DELAY, RETRY_1_MS, RETRY_2_MS, RETRY_3_MS];
const INT32_BYTES = 4;
const TRANSIENT_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);

const NODE_FS: SafeFixFs = {
    ensureDir: (dir) => {
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true });
        }
    },
    read: (file) => readFileSync(file, "utf8"),
    remove: (file) => {
        if (existsSync(file)) {
            rmSync(file, { force: true });
        }
    },
    rename: (from, to) => {
        renameSync(from, to);
    },
    write: (file, data) => {
        writeVerbatim(file, data);
    },
};

const cacheFolder = function cacheFolder(root: string, folder: string): string {
    return path.join(root, relativePath("toolCache", folder));
};

const hasFatal = function hasFatal(result: FixableResult): boolean {
    return result.messages.some((message) => message.fatal === true);
};

const errorCodeOf = function errorCodeOf(error: unknown): unknown {
    return typeof error === "object" && error !== null && "code" in error ? error.code : null;
};

const isTransient = function isTransient(error: unknown): boolean {
    const code = errorCodeOf(error);
    return typeof code === "string" && TRANSIENT_CODES.has(code);
};

const sleepSync = function sleepSync(ms: number): void {
    Atomics.wait(new Int32Array(new SharedArrayBuffer(INT32_BYTES)), 0, 0, ms);
};

const attemptOnce = function attemptOnce(context: CommitContext): boolean {
    try {
        context.fs.write(context.temp, context.output);
        context.fs.rename(context.temp, context.file);
        return true;
    } catch (error) {
        context.fs.remove?.(context.temp);
        if (!isTransient(error)) {
            throw error;
        }
        return false;
    }
};

const fallbackWrite = function fallbackWrite(context: CommitContext): boolean {
    try {
        context.fs.write(context.file, context.output);
        return true;
    } catch (error) {
        if (!isTransient(error)) {
            throw error;
        }
        return false;
    }
};

const tempOf = function tempOf(fs: SafeFixFs, root: string, file: string): string {
    if (!isInsideRoot(root, file)) {
        throw new Error(outsideWorkspace(file, root));
    }
    const inside = path.relative(root, file);
    const temp = path.join(cacheFolder(root, TEMP_FOLDER), `${inside}${TEMP_SUFFIX}`);
    fs.ensureDir(path.dirname(temp));
    return temp;
};

const commitWrite = function commitWrite(fs: SafeFixFs, root: string, file: string, output: string): boolean {
    const context: CommitContext = { file, fs, output, temp: tempOf(fs, root, file) };
    for (const delay of RETRY_DELAYS_MS) {
        if (delay > NO_DELAY) {
            sleepSync(delay);
        }
        if (attemptOnce(context)) {
            return true;
        }
    }
    return fallbackWrite(context);
};

const writeBackup = function writeBackup(fs: SafeFixFs, entry: { file: string; original: string; root: string }): void {
    const backupPath = path.join(cacheFolder(entry.root, BACKUP_FOLDER), path.relative(entry.root, entry.file));
    fs.ensureDir(path.dirname(backupPath));
    fs.write(backupPath, entry.original);
};

const processFile = function processFile(fs: SafeFixFs, options: SafeFixOptions, result: FixableResult): FixAction {
    const file = result.filePath;
    if (hasFatal(result)) {
        return { file, kind: "skipped" };
    }
    if (options.dryRun === true) {
        return { file, kind: "pending" };
    }
    const original = fs.read(file);
    if (options.backup === true) {
        writeBackup(fs, { file, original, root: options.root });
    }
    return commitWrite(fs, options.root, file, result.output ?? original)
        ? { file, kind: "written", original }
        : { file, kind: "skipped" };
};

const filesOf = function filesOf(actions: readonly FixAction[], kind: FixAction["kind"]): string[] {
    return actions.filter((action) => action.kind === kind).map((action) => action.file);
};

const rollback = function rollback(fs: SafeFixFs, actions: readonly FixAction[]): void {
    for (const action of actions) {
        if (action.kind === "written" && typeof action.original === "string") {
            fs.write(action.file, action.original);
        }
    }
};

export const applyFixesSafely = function applyFixesSafely(
    results: readonly FixableResult[],
    options: SafeFixOptions,
): SafeFixOutcome {
    const fs = options.fs ?? NODE_FS;
    const actions: FixAction[] = [];
    try {
        for (const result of results.filter((entry) => typeof entry.output === "string")) {
            actions.push(processFile(fs, options, result));
        }
    } catch (error) {
        rollback(fs, actions);
        throw error;
    }
    return {
        pending: filesOf(actions, "pending"),
        skipped: filesOf(actions, "skipped"),
        written: filesOf(actions, "written"),
    };
};