← Files ForgeMindARCHIVED FILE

src/io.mjs

2.17 KB · Oct 2, 2026 · 00:31 UTC

↓ Download file

import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
import path from 'node:path';

export function canonicalJson(value) {
  return `${JSON.stringify(sortValue(value), null, 2)}\n`;
}

export async function writeJsonAtomic(target, value) {
  return writeTextAtomic(target, canonicalJson(value));
}

export async function writeTextAtomic(target, content, options = {}) {
  const resolved = path.resolve(target);
  const directory = path.dirname(resolved);
  const temporary = path.join(directory, `.${path.basename(resolved)}.${randomUUID()}.tmp`);
  await mkdir(directory, { recursive: true });
  try {
    await writeFile(temporary, content, { encoding: 'utf8', flag: 'wx' });
    if (options.ifAbsent) {
      try {
        await writeFile(resolved, content, { encoding: 'utf8', flag: 'wx' });
        return resolved;
      } finally {
        await rm(temporary, { force: true });
      }
    }
    await replaceAtomically(temporary, resolved);
  } finally {
    await rm(temporary, { force: true });
  }
  return resolved;
}

async function replaceAtomically(temporary, resolved) {
  // Windows can briefly hold a just-written file open (for example through
  // Defender or an indexer). Retrying the final same-volume rename preserves
  // atomic replacement and avoids turning that transient lock into a failed
  // workflow run.
  for (let attempt = 0; ; attempt += 1) {
    try {
      await rename(temporary, resolved);
      return;
    } catch (error) {
      const retryable = error?.code === 'EPERM' || error?.code === 'EACCES' || error?.code === 'EBUSY';
      if (!retryable || attempt >= 4) throw error;
      await new Promise((resolve) => setTimeout(resolve, 25 * (attempt + 1)));
    }
  }
}

function sortValue(value) {
  if (Array.isArray(value)) {
    return value.map(sortValue);
  }
  if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
    return Object.fromEntries(
      Object.keys(value)
        .sort((left, right) => left.localeCompare(right))
        .map((key) => [key, sortValue(value[key])]),
    );
  }
  return value;
}

SHA-256: b231500489a4f5a0ef281d4878dc3fdc7836f6d03b092ed3a794ec8ada0b85dc