UNPKG

signalk-parquet

Version:

Vessel data Parquet file archive with automated value and geospatial triggers. History API compliant with cloud backups and queries.

841 lines 32.1 kB
"use strict"; /** * SQLite Write-Ahead Buffer for crash-safe data ingestion * * Per-path table architecture: each SignalK path gets its own table in buffer.db. * Scalar paths have a `value` column; object paths have `value_json` + flattened `value_*` columns. * This eliminates column pollution from ALTER TABLE ADD COLUMN on a shared table. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SQLiteBuffer = void 0; exports.pathToTableName = pathToTableName; const path = __importStar(require("path")); const fs = __importStar(require("fs-extra")); // Lazy-loaded: node:sqlite requires Node 22.5+ let DatabaseSync; try { // eslint-disable-next-line @typescript-eslint/no-require-imports ({ DatabaseSync } = require('node:sqlite')); } catch { // node:sqlite not available — SQLiteBuffer constructor will throw a clear error } /** * Convert a SignalK path to a SQLite table name. * Dots become underscores, any non-alphanumeric/underscore chars are stripped, * prefixed with `buffer_`. */ function pathToTableName(signalkPath) { return `buffer_${signalkPath.replace(/\./g, '_').replace(/[^a-zA-Z0-9_]/g, '_')}`; } class SQLiteBuffer { constructor(config) { if (!DatabaseSync) { throw new Error('node:sqlite is not available (requires Node.js 22.5+). SQLite buffer disabled — falling back to in-memory LRU.'); } this.dbPath = config.dbPath; this.retentionHours = config.retentionHours || 24; // Ensure directory exists fs.ensureDirSync(path.dirname(this.dbPath)); // Open database with WAL mode for crash safety and better concurrency this.db = new DatabaseSync(this.dbPath); this._open = true; // Configure for performance and crash safety this.db.exec('PRAGMA journal_mode = WAL'); this.db.exec('PRAGMA synchronous = NORMAL'); this.db.exec('PRAGMA cache_size = -64000'); // 64MB cache this.db.exec('PRAGMA temp_store = MEMORY'); this.db.exec('PRAGMA mmap_size = 268435456'); // 256MB memory-mapped I/O // Create metadata table this.createMetadataSchema(); // Migrate from old single-table layout if needed this.migrateFromLegacy(); // Rebuild tableMap from buffer_tables metadata this.tableMap = new Map(); this.loadExistingTables(); } createMetadataSchema() { this.db.exec(` CREATE TABLE IF NOT EXISTS buffer_tables ( path TEXT PRIMARY KEY, table_name TEXT NOT NULL, is_object INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); `); } /** * Migrate from the legacy single buffer_records table to per-path tables. * Runs once automatically if buffer_records exists but buffer_tables is empty. */ migrateFromLegacy() { // Check if old table exists const oldTableExists = this.db .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='buffer_records'`) .get(); if (!oldTableExists) return; // Check if we already migrated (buffer_tables has entries) const tableCount = this.db.prepare(`SELECT COUNT(*) as cnt FROM buffer_tables`).get().cnt; if (tableCount > 0) { // Already migrated — drop legacy table if it still exists this.db.exec(`DROP TABLE IF EXISTS buffer_records`); return; } // Get all columns from the old table const oldColumns = this.db.prepare('PRAGMA table_info(buffer_records)').all().map(c => c.name); // Discover all dynamic value_* columns (beyond base schema) const dynamicValueCols = oldColumns.filter(c => c.startsWith('value_') && c !== 'value_json'); // Get distinct paths const paths = this.db .prepare(`SELECT DISTINCT path FROM buffer_records ORDER BY path`) .all().map(r => r.path); if (paths.length === 0) { // No data — just drop the old table this.db.exec(`DROP TABLE IF EXISTS buffer_records`); return; } this.db.exec('BEGIN'); try { for (const signalkPath of paths) { // Determine if this path is an object path const hasJson = this.db .prepare(`SELECT COUNT(*) as cnt FROM buffer_records WHERE path = ? AND value_json IS NOT NULL`) .get(signalkPath).cnt; const isObject = hasJson > 0; // For object paths, discover which value_* columns have data for this path const pathValueCols = []; if (isObject && dynamicValueCols.length > 0) { // Check which dynamic columns have non-NULL data for this path for (const col of dynamicValueCols) { const hasData = this.db .prepare(`SELECT COUNT(*) as cnt FROM buffer_records WHERE path = ? AND ${col} IS NOT NULL`) .get(signalkPath).cnt; if (hasData > 0) { pathValueCols.push(col); } } } const tableName = pathToTableName(signalkPath); // Build CREATE TABLE const columns = [ 'id INTEGER PRIMARY KEY AUTOINCREMENT', 'context TEXT NOT NULL', 'received_timestamp TEXT NOT NULL', 'signalk_timestamp TEXT NOT NULL', ]; if (isObject) { columns.push('value_json TEXT'); for (const col of pathValueCols) { columns.push(`${col} REAL`); } } else { columns.push('value TEXT'); } columns.push('source TEXT', 'source_label TEXT', 'source_type TEXT', 'source_pgn INTEGER', 'source_src TEXT', 'meta TEXT', 'exported INTEGER NOT NULL DEFAULT 0', 'export_batch_id TEXT', `created_at TEXT NOT NULL DEFAULT (datetime('now'))`); this.db.exec(`CREATE TABLE ${tableName} (${columns.join(', ')})`); this.db.exec(`CREATE INDEX idx_${tableName}_ctx_exp ON ${tableName} (context, exported)`); this.db.exec(`CREATE INDEX idx_${tableName}_received ON ${tableName} (received_timestamp)`); // Copy data const selectCols = [ 'context', 'received_timestamp', 'signalk_timestamp', ]; if (isObject) { selectCols.push('value_json'); selectCols.push(...pathValueCols); } else { selectCols.push('value'); } selectCols.push('source', 'source_label', 'source_type', 'source_pgn', 'source_src', 'meta', 'exported', 'export_batch_id', 'created_at'); this.db.exec(`INSERT INTO ${tableName} (${selectCols.join(', ')}) SELECT ${selectCols.join(', ')} FROM buffer_records WHERE path = '${signalkPath.replace(/'/g, "''")}'`); // Register in metadata this.db .prepare(`INSERT INTO buffer_tables (path, table_name, is_object) VALUES (?, ?, ?)`) .run(signalkPath, tableName, isObject ? 1 : 0); } // Drop legacy table this.db.exec(`DROP TABLE buffer_records`); this.db.exec('COMMIT'); } catch (e) { this.db.exec('ROLLBACK'); throw e; } } /** * Load existing per-path tables from buffer_tables metadata and prepare INSERT statements. */ loadExistingTables() { const rows = this.db .prepare(`SELECT path, table_name, is_object FROM buffer_tables`) .all(); for (const row of rows) { const columns = new Set(); const tableInfo = this.db .prepare(`PRAGMA table_info(${row.table_name})`) .all(); for (const col of tableInfo) { columns.add(col.name); } const insertStmt = this.buildInsertStmt(row.table_name, columns, row.is_object === 1); this.tableMap.set(row.path, { tableName: row.table_name, isObject: row.is_object === 1, columns, insertStmt, }); } } /** * Build an INSERT statement for a per-path table. */ buildInsertStmt(tableName, columns, isObject) { const insertCols = []; const placeholders = []; // Order: context, received_timestamp, signalk_timestamp, value/value_json+value_*, source*, meta, exported, export_batch_id, created_at const orderedCols = ['context', 'received_timestamp', 'signalk_timestamp']; if (isObject) { orderedCols.push('value_json'); // Add any dynamic value_* columns for (const col of columns) { if (col.startsWith('value_') && col !== 'value_json') { orderedCols.push(col); } } } else { orderedCols.push('value'); } orderedCols.push('source', 'source_label', 'source_type', 'source_pgn', 'source_src', 'meta'); for (const col of orderedCols) { if (columns.has(col)) { insertCols.push(col); placeholders.push(`@${col}`); } } // Automatic columns insertCols.push('exported', 'export_batch_id', 'created_at'); placeholders.push('0', 'NULL', "datetime('now')"); return this.db.prepare(`INSERT INTO ${tableName} (${insertCols.join(', ')}) VALUES (${placeholders.join(', ')})`); } /** * Ensure a per-path table exists. Creates it on first insert for a new path. */ ensureTable(signalkPath, record) { const existing = this.tableMap.get(signalkPath); if (existing) return existing; const tableName = pathToTableName(signalkPath); // Detect object vs scalar from the record const valueKeys = Object.keys(record).filter(k => k.startsWith('value_') && k !== 'value_json' && record[k] !== undefined && record[k] !== null); const isObject = valueKeys.length > 0 || (record.value_json !== undefined && record.value_json !== null); const columnDefs = [ 'id INTEGER PRIMARY KEY AUTOINCREMENT', 'context TEXT NOT NULL', 'received_timestamp TEXT NOT NULL', 'signalk_timestamp TEXT NOT NULL', ]; if (isObject) { columnDefs.push('value_json TEXT'); for (const key of valueKeys) { const val = record[key]; const colType = typeof val === 'number' ? 'REAL' : 'TEXT'; columnDefs.push(`${key} ${colType}`); } } else { columnDefs.push('value TEXT'); } columnDefs.push('source TEXT', 'source_label TEXT', 'source_type TEXT', 'source_pgn INTEGER', 'source_src TEXT', 'meta TEXT', 'exported INTEGER NOT NULL DEFAULT 0', 'export_batch_id TEXT', `created_at TEXT NOT NULL DEFAULT (datetime('now'))`); this.db.exec(`CREATE TABLE ${tableName} (${columnDefs.join(', ')})`); this.db.exec(`CREATE INDEX idx_${tableName}_ctx_exp ON ${tableName} (context, exported)`); this.db.exec(`CREATE INDEX idx_${tableName}_received ON ${tableName} (received_timestamp)`); // Register in metadata this.db .prepare(`INSERT INTO buffer_tables (path, table_name, is_object) VALUES (?, ?, ?)`) .run(signalkPath, tableName, isObject ? 1 : 0); const columns = new Set(); const tableInfoRows = this.db .prepare(`PRAGMA table_info(${tableName})`) .all(); for (const col of tableInfoRows) { columns.add(col.name); } const insertStmt = this.buildInsertStmt(tableName, columns, isObject); const info = { tableName, isObject, columns, insertStmt }; this.tableMap.set(signalkPath, info); return info; } /** * Ensure a dynamic value_* column exists on a per-path table. * Only affects the single table for that path. */ ensureColumn(tableInfo, columnName, value) { if (tableInfo.columns.has(columnName)) return; const colType = typeof value === 'number' ? 'REAL' : 'TEXT'; this.db.exec(`ALTER TABLE ${tableInfo.tableName} ADD COLUMN ${columnName} ${colType}`); tableInfo.columns.add(columnName); tableInfo.insertStmt = this.buildInsertStmt(tableInfo.tableName, tableInfo.columns, tableInfo.isObject); } /** * Check if the database connection is open */ isOpen() { return this._open; } /** * Insert a single record into the buffer */ insert(record) { if (!this._open) { throw new Error('SQLite buffer is closed'); } const tableInfo = this.ensureTable(record.path, record); const params = this.prepareRecord(record, tableInfo); tableInfo.insertStmt.run(params); } /** * Insert multiple records in a single transaction (much faster) */ insertBatch(records) { if (records.length === 0) return; this.db.exec('BEGIN'); try { for (const record of records) { const tableInfo = this.ensureTable(record.path, record); const params = this.prepareRecord(record, tableInfo); tableInfo.insertStmt.run(params); } this.db.exec('COMMIT'); } catch (e) { this.db.exec('ROLLBACK'); throw e; } } prepareRecord(record, tableInfo) { const params = { context: record.context, received_timestamp: record.received_timestamp, signalk_timestamp: record.signalk_timestamp, }; if (tableInfo.isObject) { // Object path: value_json + flattened value_* columns let valueJson = null; if (record.value !== null && record.value !== undefined && typeof record.value === 'object') { valueJson = JSON.stringify(record.value); } if (record.value_json !== undefined && record.value_json !== null) { valueJson = typeof record.value_json === 'string' ? record.value_json : JSON.stringify(record.value_json); } params.value_json = valueJson; // Extract dynamic value_* columns for (const key of Object.keys(record)) { if (key.startsWith('value_') && key !== 'value_json' && record[key] !== undefined && record[key] !== null) { this.ensureColumn(tableInfo, key, record[key]); const val = record[key]; if (typeof val === 'number') { params[key] = val; } else if (typeof val === 'boolean') { params[key] = val ? 1 : 0; } else { params[key] = String(val); } } } // NULL-fill any known value_* columns not in this record for (const col of tableInfo.columns) { if (col.startsWith('value_') && col !== 'value_json' && !(col in params)) { params[col] = null; } } } else { // Scalar path: value column let valueStr = null; if (record.value !== null && record.value !== undefined) { valueStr = String(record.value); } params.value = valueStr; } // Serialize source params.source = record.source ? typeof record.source === 'object' ? JSON.stringify(record.source) : String(record.source) : null; params.source_label = record.source_label || null; params.source_type = record.source_type || null; params.source_pgn = record.source_pgn || null; params.source_src = record.source_src || null; // Serialize meta params.meta = record.meta ? typeof record.meta === 'object' ? JSON.stringify(record.meta) : String(record.meta) : null; return params; } /** * Convert a BufferRecord back to a DataRecord. * Path must be passed in since per-path tables have no path column. */ bufferRecordToDataRecord(record, signalkPath) { let value = record.value; let valueJson = undefined; // Parse value_json if present if (record.value_json) { try { valueJson = JSON.parse(record.value_json); } catch { valueJson = record.value_json; } } // Parse numeric values if (value !== null && value !== undefined && !isNaN(Number(value))) { value = Number(value); } else if (value === 'true') { value = true; } else if (value === 'false') { value = false; } // Parse source if JSON let source = record.source; if (record.source) { try { source = JSON.parse(record.source); } catch { source = record.source; } } // Parse meta if JSON let meta = record.meta; if (record.meta) { try { meta = JSON.parse(record.meta); } catch { meta = record.meta; } } const dataRecord = { received_timestamp: record.received_timestamp, signalk_timestamp: record.signalk_timestamp, context: record.context, path: signalkPath, value: value, value_json: valueJson, source: source, source_label: record.source_label || undefined, source_type: record.source_type || undefined, source_pgn: record.source_pgn || undefined, source_src: record.source_src || undefined, meta: meta, }; // Restore dynamic value_* columns from the record const rec = record; for (const key of Object.keys(rec)) { if (key.startsWith('value_') && key !== 'value_json' && rec[key] !== null && rec[key] !== undefined) { dataRecord[key] = rec[key]; } } return dataRecord; } /** * Clean up old exported records from all per-path tables */ cleanup() { let totalCleaned = 0; for (const [, info] of this.tableMap) { const result = this.db .prepare(`DELETE FROM ${info.tableName} WHERE exported = 1 AND created_at < datetime('now', '-' || ? || ' hours')`) .run(this.retentionHours); totalCleaned += Number(result.changes); } return totalCleaned; } /** * Get buffer statistics aggregated across all per-path tables */ getStats() { let totalRecords = 0; let pendingRecords = 0; let exportedRecords = 0; let oldestPendingTimestamp = null; let newestRecordTimestamp = null; for (const [, info] of this.tableMap) { const row = this.db .prepare(` SELECT COUNT(*) as totalRecords, SUM(CASE WHEN exported = 0 THEN 1 ELSE 0 END) as pendingRecords, SUM(CASE WHEN exported = 1 THEN 1 ELSE 0 END) as exportedRecords, MIN(CASE WHEN exported = 0 THEN received_timestamp END) as oldestPendingTimestamp, MAX(received_timestamp) as newestRecordTimestamp FROM ${info.tableName} `) .get(); totalRecords += row.totalRecords || 0; pendingRecords += row.pendingRecords || 0; exportedRecords += row.exportedRecords || 0; if (row.oldestPendingTimestamp) { if (!oldestPendingTimestamp || row.oldestPendingTimestamp < oldestPendingTimestamp) { oldestPendingTimestamp = row.oldestPendingTimestamp; } } if (row.newestRecordTimestamp) { if (!newestRecordTimestamp || row.newestRecordTimestamp > newestRecordTimestamp) { newestRecordTimestamp = row.newestRecordTimestamp; } } } // Get file sizes let dbSizeBytes = 0; let walSizeBytes = 0; try { dbSizeBytes = fs.statSync(this.dbPath).size; } catch { // File may not exist yet } try { const walPath = this.dbPath + '-wal'; if (fs.existsSync(walPath)) { walSizeBytes = fs.statSync(walPath).size; } } catch { // WAL file may not exist } return { totalRecords, pendingRecords, exportedRecords, oldestPendingTimestamp, newestRecordTimestamp, dbSizeBytes, walSizeBytes, }; } /** * Get count of pending records (faster than full stats) */ getPendingCount() { if (!this._open) { return 0; } let total = 0; for (const [, info] of this.tableMap) { const row = this.db .prepare(`SELECT COUNT(*) as count FROM ${info.tableName} WHERE exported = 0`) .get(); total += row.count; } return total; } /** * Checkpoint WAL file (useful before backup or to reduce WAL size) */ checkpoint() { this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } /** * Get distinct dates that have unexported records, excluding today. * Scans all per-path tables. */ getDatesWithUnexportedRecords(excludeToday = true) { if (!this._open) { return []; } const allDates = new Set(); for (const [, info] of this.tableMap) { let query = ` SELECT DISTINCT date(received_timestamp) as record_date FROM ${info.tableName} WHERE exported = 0 `; if (excludeToday) { query += ` AND date(received_timestamp) < date('now')`; } const rows = this.db.prepare(query).all(); for (const r of rows) { allDates.add(r.record_date); } } return Array.from(allDates).sort(); } /** * Get distinct context/path combinations for a specific date (UTC). * Scans all per-path tables. */ getPathsForDate(date) { if (!this._open) { return []; } const dateStr = date.toISOString().slice(0, 10); const startOfDay = `${dateStr}T00:00:00.000Z`; const endOfDay = `${dateStr}T23:59:59.999Z`; const results = []; for (const [signalkPath, info] of this.tableMap) { const rows = this.db .prepare(` SELECT DISTINCT context FROM ${info.tableName} WHERE received_timestamp >= ? AND received_timestamp <= ? AND exported = 0 `) .all(startOfDay, endOfDay); for (const row of rows) { results.push({ context: row.context, path: signalkPath }); } } return results.sort((a, b) => a.context < b.context ? -1 : a.context > b.context ? 1 : a.path < b.path ? -1 : a.path > b.path ? 1 : 0); } /** * Get all records for a specific context, path, and date (UTC). * Returns just DataRecord[] — no IDs needed since markDateExported() handles marking. */ getRecordsForPathAndDate(context, signalkPath, date) { if (!this._open) { return []; } const tableInfo = this.tableMap.get(signalkPath); if (!tableInfo) return []; const dateStr = date.toISOString().slice(0, 10); const startOfDay = `${dateStr}T00:00:00.000Z`; const endOfDay = `${dateStr}T23:59:59.999Z`; const bufferRecords = this.db .prepare(` SELECT * FROM ${tableInfo.tableName} WHERE context = ? AND received_timestamp >= ? AND received_timestamp <= ? AND exported = 0 ORDER BY received_timestamp ASC `) .all(context, startOfDay, endOfDay); return bufferRecords.map(r => this.bufferRecordToDataRecord(r, signalkPath)); } /** * Count unexported records for a specific context, path, and date (UTC). */ getRecordCountForPathAndDate(context, signalkPath, date) { if (!this._open) return 0; const tableInfo = this.tableMap.get(signalkPath); if (!tableInfo) return 0; const dateStr = date.toISOString().slice(0, 10); const startOfDay = `${dateStr}T00:00:00.000Z`; const endOfDay = `${dateStr}T23:59:59.999Z`; const row = this.db .prepare(` SELECT COUNT(*) as cnt FROM ${tableInfo.tableName} WHERE context = ? AND received_timestamp >= ? AND received_timestamp <= ? AND exported = 0 `) .get(context, startOfDay, endOfDay); return row?.cnt ?? 0; } /** * Get a batch of unexported records for a specific context, path, and date (UTC). * Uses LIMIT/OFFSET to avoid materializing all rows at once. */ getRecordsForPathAndDateBatched(context, signalkPath, date, limit, offset) { if (!this._open) return []; const tableInfo = this.tableMap.get(signalkPath); if (!tableInfo) return []; const dateStr = date.toISOString().slice(0, 10); const startOfDay = `${dateStr}T00:00:00.000Z`; const endOfDay = `${dateStr}T23:59:59.999Z`; const bufferRecords = this.db .prepare(` SELECT * FROM ${tableInfo.tableName} WHERE context = ? AND received_timestamp >= ? AND received_timestamp <= ? AND exported = 0 ORDER BY received_timestamp ASC LIMIT ? OFFSET ? `) .all(context, startOfDay, endOfDay, limit, offset); return bufferRecords.map(r => this.bufferRecordToDataRecord(r, signalkPath)); } /** * Mark records for a specific date as exported. * Queries the specific path's table. */ markDateExported(context, signalkPath, date, batchId) { if (!this._open) return; const tableInfo = this.tableMap.get(signalkPath); if (!tableInfo) return; const dateStr = date.toISOString().slice(0, 10); const startOfDay = `${dateStr}T00:00:00.000Z`; const endOfDay = `${dateStr}T23:59:59.999Z`; this.db .prepare(` UPDATE ${tableInfo.tableName} SET exported = 1, export_batch_id = ? WHERE context = ? AND received_timestamp >= ? AND received_timestamp <= ? AND exported = 0 `) .run(batchId, context, startOfDay, endOfDay); } /** * Get the set of known SignalK paths that have buffer tables. * Used by SQL builders to check if a buffer table exists for federation. */ getKnownPaths() { return new Set(this.tableMap.keys()); } /** * Check if a buffer table exists for a given SignalK path. */ hasTable(signalkPath) { return this.tableMap.has(signalkPath); } /** * Get the set of columns for a given path's buffer table. * Returns undefined if no table exists for this path. */ getTableColumns(signalkPath) { const info = this.tableMap.get(signalkPath); return info?.columns; } /** * Get the column schema (name + declared SQLite type) for a path's buffer table. * Returns undefined if no table exists for this path. */ getTableSchema(signalkPath) { if (!this._open) return undefined; const info = this.tableMap.get(signalkPath); if (!info) return undefined; const rows = this.db .prepare(`PRAGMA table_info(${info.tableName})`) .all(); return rows.map(r => ({ name: r.name, type: r.type })); } /** * Read a batch of unexported rows for federated history queries, keyset-paginated * by id so callers can stream large windows without materializing them all. * Rows are raw table rows (all columns), matching the table schema. */ getRowsForFederation(signalkPath, context, fromIso, toIso, afterId, limit) { if (!this._open) return []; const tableInfo = this.tableMap.get(signalkPath); if (!tableInfo) return []; return this.db .prepare(` SELECT * FROM ${tableInfo.tableName} WHERE context = ? AND signalk_timestamp >= ? AND signalk_timestamp < ? AND exported = 0 AND id > ? ORDER BY id ASC LIMIT ? `) .all(context, fromIso, toIso, afterId, limit); } /** * Get the database path */ getDbPath() { return this.dbPath; } /** * Close the database connection */ close() { try { this.checkpoint(); } catch { // Ignore checkpoint errors during shutdown } this.db.close(); this._open = false; } } exports.SQLiteBuffer = SQLiteBuffer; //# sourceMappingURL=sqlite-buffer.js.map