27 lines
1.7 KiB
JavaScript
27 lines
1.7 KiB
JavaScript
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import lockfile from 'proper-lockfile';
|
|
import { writeFileAtomic } from './secretary-layer1.mjs';
|
|
|
|
// Апсерт строки дела в оглавление (§D8). Ключ — <slug>/protocol.md.
|
|
export function upsertIndexEntry(indexMd, { slug, title, goal, status, date }) {
|
|
const line = `- [${title}](${slug}/protocol.md) — ${goal} · ${status} · ${date}`;
|
|
const key = `(${slug}/protocol.md)`;
|
|
const lines = String(indexMd || '').split('\n').filter((l) => l.length > 0);
|
|
const idx = lines.findIndex((l) => l.includes(key));
|
|
if (idx >= 0) lines[idx] = line;
|
|
else lines.push(line);
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/** Безопасная к параллели запись строки темы в оглавление: read-modify-write под замком файла.
|
|
* Две темы пишут одно оглавление — замок сериализует, upsert по ключу-теме не теряет строк.
|
|
* proper-lockfile стат-ит цель → файл должен существовать ДО взятия замка (создаём пустой). */
|
|
export async function writeIndexSafely(idxFile, entry, { lockImpl = lockfile } = {}) {
|
|
if (!existsSync(idxFile)) { try { writeFileSync(idxFile, ''); } catch { /* гонка создания — переживём */ } }
|
|
const release = await lockImpl.lock(idxFile, { stale: 15_000, retries: { retries: 10, minTimeout: 20, maxTimeout: 200 }, realpath: false });
|
|
try {
|
|
const cur = existsSync(idxFile) ? readFileSync(idxFile, 'utf-8') : '';
|
|
writeFileAtomic(idxFile, upsertIndexEntry(cur, entry));
|
|
} finally { await release(); }
|
|
}
|