Files
cc-platform-core/server/db.js
2026-03-26 15:00:49 -04:00

153 lines
4.1 KiB
JavaScript

/**
* @cc-platform/server/db — SQLite database factory
*
* Extracted from the common setup patterns in both
* remoteturtle/server/database.js and Inventory-Manager-CC/web/server/db.js.
*
* Provides:
* - Database creation with WAL mode and production-ready pragmas
* - Directory validation and creation
* - Schema initialization
* - Debounced save support (batch writes on intervals)
* - Clean shutdown with flush
*
* Usage:
* const { createDatabase } = require('@cc-platform/server/db');
* const { db, debouncedSave, close } = createDatabase('/data/myservice.db', {
* schema: `CREATE TABLE IF NOT EXISTS items (...)`,
* debounceMs: 2000,
* });
*
* // Use db directly for queries
* const row = db.prepare('SELECT * FROM items WHERE id = ?').get(1);
*
* // Use debouncedSave to batch expensive saves
* debouncedSave(() => { ... bulk insert ... });
*
* // On shutdown
* close();
*/
'use strict';
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
/**
* @typedef {Object} DatabaseOptions
* @property {string} [schema] SQL to execute on init (CREATE TABLE IF NOT EXISTS ...)
* @property {number} [debounceMs] Debounce interval for batched saves (default: 2000)
* @property {number} [cacheSize] SQLite cache size pragma (default: -8000 = ~8MB)
* @property {string} [tempStore] SQLite temp_store pragma (default: 'MEMORY')
* @property {boolean} [foreignKeys] Enable foreign keys (default: true)
*/
/**
* Create and initialize a SQLite database with production-ready defaults.
*
* @param {string} dbPath - Path to the database file
* @param {DatabaseOptions} opts - Configuration options
* @returns {{ db, debouncedSave, flushPendingSave, close }}
*/
function createDatabase(dbPath, opts = {}) {
// Ensure directory exists and is writable
const dir = path.dirname(path.resolve(dbPath));
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Verify directory is writable
try {
fs.accessSync(dir, fs.constants.W_OK);
} catch (e) {
throw new Error(`Database directory is not writable: ${dir}`);
}
const db = new Database(dbPath);
// Production-ready pragmas
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
if (opts.foreignKeys !== false) {
db.pragma('foreign_keys = ON');
}
if (opts.cacheSize) {
db.pragma(`cache_size = ${opts.cacheSize}`);
}
if (opts.tempStore) {
db.pragma(`temp_store = ${opts.tempStore}`);
}
// Schema initialization
if (opts.schema) {
db.exec(opts.schema);
}
// --- Debounced save support ---
const debounceMs = opts.debounceMs || 2000;
let saveTimer = null;
let pendingSave = null;
/**
* Schedule a save function to run after the debounce interval.
* If called again before the timer fires, the latest save function replaces
* the previous one (only the most recent save runs).
*
* @param {Function} saveFn - The save operation to execute
*/
function debouncedSave(saveFn) {
pendingSave = saveFn;
if (!saveTimer) {
saveTimer = setTimeout(() => {
saveTimer = null;
if (pendingSave) {
try {
pendingSave();
} catch (e) {
console.error('[db] Debounced save error:', e.message);
}
pendingSave = null;
}
}, debounceMs);
}
}
/**
* Immediately execute any pending save and clear the timer.
* Call this during shutdown to avoid data loss.
*/
function flushPendingSave() {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
if (pendingSave) {
try {
pendingSave();
} catch (e) {
console.error('[db] Flush save error:', e.message);
}
pendingSave = null;
}
}
/**
* Flush pending saves and close the database connection.
* Safe to call multiple times.
*/
function close() {
flushPendingSave();
try {
db.close();
} catch (e) {
// Already closed — ignore
}
}
return { db, debouncedSave, flushPendingSave, close };
}
module.exports = { createDatabase };