lgq
3 天以前 081f12a52906abe6c2d139fdc144135978681009
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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;