0
0
Fork 0

tui: add atomic Selection JSON writer

writeSelection() emits the v1 state envelope (version, updated_at,
project_dir, device, selection{kind,name,path,abs_path}, viewer_line)
to <file>.<pid>.tmp then atomic-renames into place. mkdir -p the
parent, no .tmp left behind on success.
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-28 21:52:07 +02:00
parent 461bd10a73
commit ba40f10e6e
2 changed files with 87 additions and 0 deletions

View file

@ -0,0 +1,29 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { Selection } from './types.js';
export async function writeSelection(
filePath: string,
projectDir: string,
selection: Selection
): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true });
const payload = {
version: 1,
updated_at: new Date().toISOString(),
project_dir: projectDir,
device: selection.device,
selection: {
kind: selection.pou.kind,
name: selection.pou.name,
path: selection.pou.relPath,
abs_path: selection.pou.absPath,
},
viewer_line: selection.viewerLine,
};
const tmp = `${filePath}.${process.pid}.tmp`;
await fs.writeFile(tmp, JSON.stringify(payload, null, 2), 'utf8');
await fs.rename(tmp, filePath);
}

View file

@ -0,0 +1,58 @@
import { describe, it, expect } from 'vitest';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import { writeSelection } from '../../src/tui/shared/state-write.ts';
import { Selection } from '../../src/tui/shared/types.ts';
async function tmpDir(): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), 'phobics-tui-'));
}
const sampleSelection: Selection = {
device: 'CodesysRpi',
pou: {
name: 'FB_Test',
kind: 'FB',
relPath: 'Plc Logic/Application/FB_Test.st',
absPath: '/abs/Plc Logic/Application/FB_Test.st',
loc: 87,
mtimeMs: 0,
},
viewerLine: 12,
};
describe('writeSelection', () => {
it('writes a JSON file atomically with v1 envelope', async () => {
const dir = await tmpDir();
const target = path.join(dir, 'tui-state.json');
await writeSelection(target, '/abs/project', sampleSelection);
const text = await fs.readFile(target, 'utf8');
const parsed = JSON.parse(text);
expect(parsed.version).toBe(1);
expect(parsed.project_dir).toBe('/abs/project');
expect(parsed.device).toBe('CodesysRpi');
expect(parsed.selection.name).toBe('FB_Test');
expect(parsed.selection.kind).toBe('FB');
expect(parsed.viewer_line).toBe(12);
expect(typeof parsed.updated_at).toBe('string');
expect(() => new Date(parsed.updated_at)).not.toThrow();
});
it('creates parent directories as needed', async () => {
const dir = await tmpDir();
const target = path.join(dir, 'a', 'b', 'tui-state.json');
await writeSelection(target, '/abs/project', sampleSelection);
const stat = await fs.stat(target);
expect(stat.isFile()).toBe(true);
});
it('does not leave the .tmp file behind on success', async () => {
const dir = await tmpDir();
const target = path.join(dir, 'tui-state.json');
await writeSelection(target, '/abs/project', sampleSelection);
const entries = await fs.readdir(dir);
expect(entries.filter((e) => e.endsWith('.tmp'))).toEqual([]);
});
});