Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 2x 2x 2x 2x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 19x 19x 254x 254x 254x 254x 254x 254x 254x 254x 254x 3x 1x 1x 2x 2x 2x 3x 254x 254x 254x 254x 254x 254x 254x 254x 254x 14x 2x 2x 1x 1x 1x 1x 12x 12x 1x 1x 11x 11x 13x 1x 1x 10x 10x 10x 10x 11x 1x 1x 9x 9x 10x 9x 9x 14x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 24x 24x 4x 4x 20x 20x 20x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 26x 2x 2x 2x 2x 2x 2x 2x 2x 2x 26x 20x 20x 8x 8x 8x 8x 8x 8x 20x 20x 24x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 254x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x | /**
* @file store.js
* @description On-disk storage for recorded traces.
*
* Traces live in `.avenx/traces/` under the project root, one JSON file per
* recording. A plain directory of plain files is deliberate: a developer can
* read one, diff two, attach one to an issue, or delete the lot, without a
* database, an index file that can go stale, or a tool.
*
* Node-only, like everything else that touches the filesystem.
* @module lib/core/trace/store
*/
import fs from 'fs';
import path from 'path';
import { validateTrace } from './schema.js';
/**
* Where traces are kept, relative to the project root.
* @type {string}
*/
export const TRACE_DIR = path.join('.avenx', 'traces');
/**
* How many traces a project keeps before `prune` starts suggesting cleanup.
* @type {number}
*/
export const DEFAULT_KEEP = 20;
/**
* Resolves and creates the trace directory for a project.
* @param {string} rootDir - The project root.
* @returns {string} The absolute trace directory.
*/
export function traceDir(rootDir) {
const dir = path.join(rootDir, TRACE_DIR);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
/**
* Rejects a trace id that could escape the trace directory.
*
* Ids arrive from the CLI and from the dev server's ingest endpoint, so they
* are untrusted input on a path join.
* @param {string} id - The candidate id.
* @returns {boolean} Whether it is safe to use as a file name.
*/
export function isValidTraceId(id) {
return typeof id === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(id);
}
/**
* Writes a trace to disk.
* @param {string} rootDir - The project root.
* @param {object} trace - The trace to save.
* @returns {string} The path written.
* @throws {Error} When the trace has no usable id.
*/
export function saveTrace(rootDir, trace) {
if (!isValidTraceId(trace && trace.id)) {
throw new Error(`Refusing to save a trace with an unusable id: ${JSON.stringify(trace && trace.id)}`);
}
const filePath = path.join(traceDir(rootDir), `${trace.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(trace, null, 2));
return filePath;
}
/**
* Reads one trace.
* @param {string} rootDir - The project root.
* @param {string} id - The trace id, or `latest` for the newest.
* @returns {{trace: object, path: string}|null} The trace, or null when absent.
* @throws {Error} When the file exists but is not a readable trace.
*/
export function loadTrace(rootDir, id) {
if (id === 'latest') {
const all = listTraces(rootDir);
if (all.length === 0) {
return null;
}
return loadTrace(rootDir, all[0].id);
}
if (!isValidTraceId(id)) {
return null;
}
const filePath = path.join(rootDir, TRACE_DIR, `${id}.json`);
if (!fs.existsSync(filePath)) {
return null;
}
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (error) {
throw new Error(`${filePath} is not valid JSON: ${error.message}`, { cause: error });
}
const valid = validateTrace(parsed);
if (!valid.ok) {
throw new Error(`${filePath} cannot be read: ${valid.error}`);
}
return { trace: parsed, path: filePath };
}
/**
* Lists stored traces, newest first.
*
* A file that cannot be parsed is listed with a `broken` flag rather than
* skipped, so a corrupted recording is visible and can be pruned rather than
* silently disappearing from the listing.
* @param {string} rootDir - The project root.
* @returns {Array<object>} Summaries, newest first.
*/
export function listTraces(rootDir) {
const dir = path.join(rootDir, TRACE_DIR);
if (!fs.existsSync(dir)) {
return [];
}
const entries = [];
for (const name of fs.readdirSync(dir)) {
if (!name.endsWith('.json')) {
continue;
}
const filePath = path.join(dir, name);
const stat = fs.statSync(filePath);
try {
const trace = JSON.parse(fs.readFileSync(filePath, 'utf8'));
entries.push({
id: trace.id || path.basename(name, '.json'),
path: filePath,
mtime: stat.mtimeMs,
size: stat.size,
trace,
broken: !validateTrace(trace).ok,
});
} catch {
entries.push({
id: path.basename(name, '.json'),
path: filePath,
mtime: stat.mtimeMs,
size: stat.size,
trace: null,
broken: true,
});
}
}
entries.sort((a, b) => {
if (b.mtime !== a.mtime) {
return b.mtime - a.mtime;
}
const bCreated = b.trace && b.trace.createdAt ? Date.parse(b.trace.createdAt) || 0 : 0;
const aCreated = a.trace && a.trace.createdAt ? Date.parse(a.trace.createdAt) || 0 : 0;
if (bCreated !== aCreated) {
return bCreated - aCreated;
}
return b.id.localeCompare(a.id);
});
return entries;
}
/**
* Deletes stored traces.
* @param {string} rootDir - The project root.
* @param {object} [options] - What to remove.
* @param {number} [options.keep] - Keep this many of the newest.
* @param {boolean} [options.all] - Remove everything.
* @param {string} [options.id] - Remove one trace.
* @returns {string[]} The ids removed.
*/
export function pruneTraces(rootDir, options = {}) {
const entries = listTraces(rootDir);
let doomed;
if (options.id) {
doomed = entries.filter((entry) => entry.id === options.id);
} else if (options.all) {
doomed = entries;
} else {
const keep = typeof options.keep === 'number' ? options.keep : DEFAULT_KEEP;
doomed = entries.slice(keep);
}
const removed = [];
for (const entry of doomed) {
try {
fs.unlinkSync(entry.path);
removed.push(entry.id);
} catch {
// A trace that cannot be removed is not worth failing a cleanup over.
}
}
return removed;
}
|