The HistoryStore provides a minimal file-based persistence layer for IntervalRecord objects. It writes JSON arrays to disk, supports atomic append operations, and handles graceful startup when the file doesn’t exist yet.
Design Philosophy: The store uses plain JSON files rather than a database to keep the system simple, portable, and easy to debug. Each interval record is self-contained and human-readable.
import { HistoryStore } from './tracker/history.js'const store = new HistoryStore({ filePath: 'data/history.json' // Optional, defaults to 'data/history.json'})
Parameters:
filePath (string): Path to the JSON file where interval records are persisted. Defaults to 'data/history.json'.
const records = await store.load()// Returns: Array<IntervalRecord>
Returns:
Promise<Array>: Array of IntervalRecord objects
Returns an empty array [] when the file does not exist yet
Errors:
Re-throws any filesystem error that is notENOENT (file not found)
/** * Reads the persisted history from disk. * * @returns {Promise<Array>} Array of IntervalRecord objects. * Returns an empty array when the file does not exist yet. * @throws {Error} Re-throws any filesystem error that is **not** ENOENT. */async load() { try { const raw = await readFile(this._filePath, 'utf-8') return JSON.parse(raw) } catch (err) { if (err.code === 'ENOENT') return [] throw err }}
Overwrites the history file with the given records array.
await store.save(records)
Parameters:
records (array): Full array of IntervalRecord objects to persist
Behavior:
Creates the parent directory tree when it does not exist (using mkdir -p semantics)
Writes JSON with 2-space indentation for human readability
Atomic replacement: Writes succeed or fail completely (no partial writes)
/** * Overwrites the history file with the given records array. * Creates the parent directory tree when it does not exist. * * @param {Array} records Full array of IntervalRecord objects to persist. */async save(records) { await mkdir(dirname(this._filePath), { recursive: true }) await writeFile(this._filePath, JSON.stringify(records, null, 2), 'utf-8')}
Convenience method: loads existing records, appends one, and saves.
await store.append(newRecord)
Parameters:
record (object): A single IntervalRecord to append
Behavior:
Loads the existing history
Appends the new record to the array
Saves the full array back to disk
Not concurrent-safe: If multiple processes write simultaneously, the last write wins. For production use with multiple writers, consider using a database or file locking.
/** * Convenience method: loads existing records, appends one, and saves. * * @param {Object} record A single IntervalRecord to append. */async append(record) { const records = await this.load() records.push(record) await this.save(records)}
import { HistoryStore } from './tracker/history.js'const store = new HistoryStore({ filePath: 'data/history.json' })// Load all recordsconst records = await store.load()console.log(`Loaded ${records.length} records`)// Add a new record manuallyrecords.push({ index: records.length + 1, epochTimestamp: Math.floor(Date.now() / 1000 / 300) * 300, result: 'UP', // ... other fields})// Save entire arrayawait store.save(records)console.log('History saved')
The store automatically creates parent directories when saving:
// This works even if 'data/archive/2024/' doesn't exist yetconst store = new HistoryStore({ filePath: 'data/archive/2024/march-history.json'})await store.append(record) // Creates data/archive/2024/ automatically
import { readFile, writeFile, mkdir } from 'fs/promises'import { dirname } from 'path'export class HistoryStore { /** * @param {Object} opts * @param {string} [opts.filePath='data/history.json'] Path to the JSON file * where interval records are persisted. */ constructor({ filePath = 'data/history.json' } = {}) { this._filePath = filePath } /** * Reads the persisted history from disk. * * @returns {Promise<Array>} Array of IntervalRecord objects. * Returns an empty array when the file does not exist yet. * @throws {Error} Re-throws any filesystem error that is **not** ENOENT. */ async load() { try { const raw = await readFile(this._filePath, 'utf-8') return JSON.parse(raw) } catch (err) { if (err.code === 'ENOENT') return [] throw err } } /** * Overwrites the history file with the given records array. * Creates the parent directory tree when it does not exist. * * @param {Array} records Full array of IntervalRecord objects to persist. */ async save(records) { await mkdir(dirname(this._filePath), { recursive: true }) await writeFile(this._filePath, JSON.stringify(records, null, 2), 'utf-8') } /** * Convenience method: loads existing records, appends one, and saves. * * @param {Object} record A single IntervalRecord to append. */ async append(record) { const records = await this.load() records.push(record) await this.save(records) }}