/** * @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 };