import { dxSqliteDB as SqliteDB_base_class } from './libvbar-m-dxsqlitedb.so'; import dxMap from './dxMap.js'; import log from './dxLogger.js'; import std from './dxStd.js'; /** * SQLite Database Module (dxSqliteDB) * * @description * Provides a multi-instance, thread-safe interface to SQLite databases. It manages database * connections by their file path, allowing different parts of an application, including * different threads, to safely access and manipulate the same database file. * * @feature * - **Multi-Instance**: Manage multiple database files simultaneously based on their paths. * - **Thread-Safe**: Uses a C++ layer with a serialized transaction model, allowing safe * database access from multiple JavaScript threads without manual locking. * - **Cross-Thread Coordination**: Leverages `dxMap` as a global registry to ensure that * once a database is initialized in any thread, other threads can get a connection to it * simply by calling `init()` with the same path. * * @threadingModel * Each JS thread (main or worker) has its own memory heap, so JS objects cannot be * directly shared. This module handles this by: * 1. Using a shared `dxMap` (`__dxSqliteDB_instances__`) to store the `path` of each * initialized database, which acts as a global registry. * 2. Each thread maintains its own local cache of `dxSqliteDB` instances, keyed by path. * 3. A call to `init(path)` will first check the local cache, then the global registry, * ensuring that each thread gets a valid local instance pointing to the shared * database file. * * @example * // --- Main Thread --- * import dxSqliteDB from 'dxSqliteDB'; * const dbPath = '/data/app.db'; * * try { * const db = dxSqliteDB.init(dbPath); * db.exec(` * CREATE TABLE IF NOT EXISTS events ( * id INTEGER PRIMARY KEY AUTOINCREMENT, * type TEXT NOT NULL, * timestamp INTEGER * ) * `); * db.exec(`INSERT INTO events (type, timestamp) VALUES ('start', ${Date.now()})`); * } catch (e) { * log.error('Main thread DB error:', e.message); * } * * // --- Worker Thread --- * import dxSqliteDB from 'dxSqliteDB'; * const dbPath = '/data/app.db'; * * try { * // Calling init() again with the same path is safe. It will return a cached * // local instance or create a new one connected to the same shared database file. * const db = dxSqliteDB.init(dbPath); * if (db) { * const events = db.select('SELECT * FROM events ORDER BY id DESC LIMIT 1'); * // events would be: [{ id: 1, type: 'start', timestamp: 1678886400000 }] * } * } catch (e) { * log.error('Worker thread DB error:', e.message); * } */ // A thread-local cache for database instances, keyed by path. This map is NOT shared between threads. const localInstances = new Map(); // A shared, cross-thread registry using dxMap to store registered paths. const globalRegistry = dxMap.get('__dxSqliteDB_instances__'); const dxSqliteDB = { /** * Initializes and returns a database instance for a given path. * * @description * This function is the single entry point for getting a database connection. It's safe * to call this function multiple times with the same path from any thread. * * - If an instance for this path already exists in the current thread's cache, it's returned immediately. * - If not, it checks a global registry. If another thread has already initialized this path, * a new local instance is created for the current thread, connected to the same database file. * - If it's the first time this path is initialized by any thread, it will be registered globally. * * @param {string} path - The full, absolute path to the database file. This path is used as the unique identifier. * @returns {SqliteDB_base_class} The database instance object. This object has the following methods: * - **.exec(sql: string): void** - Executes a non-query SQL statement (e.g., CREATE, INSERT, UPDATE, DELETE). Throws an error on failure. * - **.select(sql: string): object[]** - Executes a query SQL statement (SELECT) and returns an array of result objects. * - **.begin(): void** - Begins a new transaction. * - **.commit(): void** - Commits the current transaction. * - **.rollback(): void** - Rolls back the current transaction. * @throws {Error} Throws an error if the 'path' is missing or empty, or if the underlying database connection fails. */ init: function (path) { if (!path) { throw new Error("dxSqliteDB.init requires a 'path'"); } // 1. Check thread-local cache first. if (localInstances.has(path)) { return localInstances.get(path); } // 2. Check if the path is in the global registry (initialized by any thread). const isGloballyRegistered = globalRegistry.has(path); try { // Create a new local instance regardless. const dbInstance = new SqliteDB_base_class(); std.ensurePathExists(path); dbInstance.init(path); // Store in the thread-local cache. localInstances.set(path, dbInstance); // If it wasn't globally registered before, register it now. if (!isGloballyRegistered) { globalRegistry.put(path, "1"); // Value indicates presence. } return dbInstance; } catch (e) { log.error(`Failed to initialize dxSqliteDB instance for path '${path}':`, e.message); // In case of failure, only clean up the global registry if this was the first attempt. if (!isGloballyRegistered) { globalRegistry.del(path); } throw e; } }, /** * Closes the database connection for the current thread. * * @description * This function should be called when a database is no longer needed in the current thread. * It closes the database connection and cleans up resources for the local instance. * NOTE: This is a thread-local operation and does not affect the global registry. * * @param {string} path - The path of the database instance to deinitialize. */ deinit: function (path) { if (!path) return; // Clean up the local instance if it exists in this thread. if (localInstances.has(path)) { const dbInstance = localInstances.get(path); try { dbInstance.deinit(); } catch (e) { log.error(`Error during deinit of local dxSqliteDB instance for path '${path}':`, e.message); } finally { localInstances.delete(path); } } } }; export default dxSqliteDB;