@lotargo/memory_plugin 1.3.2 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp-server/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import readline from "readline";
3
3
  import { join } from "node:path";
4
4
  import { getConfig, updateConfig, resetConfig } from "./config/config_manager.js";
@@ -91,9 +91,11 @@ async function getQuickStats() {
91
91
  let docCount = 0;
92
92
  let chunkCount = 0;
93
93
  try {
94
- const db = getDatabase();
95
- docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
96
- chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
94
+ const db = await getDatabase();
95
+ const docRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
96
+ docCount = docRow ? docRow.cnt : 0;
97
+ const chunkRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
98
+ chunkCount = chunkRow ? chunkRow.cnt : 0;
97
99
  } catch (e) {}
98
100
 
99
101
  let factCount = 0;
@@ -658,6 +660,31 @@ export async function runCli() {
658
660
  return;
659
661
  }
660
662
 
663
+ if (cliArgs.includes("login")) {
664
+ console.log("\n [CLOUD] Starting Turso cloud authorization...");
665
+ const { loginToCloud } = await import("./admin/auth.js");
666
+ try {
667
+ const secrets = await loginToCloud();
668
+ console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Connected to endpoint: ${secrets.dbUrl}\x1b[0m\n`);
669
+ } catch (e) {
670
+ console.error(`\n \x1b[31m[ERROR] Authorization failed: ${e.message}\x1b[0m\n`);
671
+ process.exit(1);
672
+ }
673
+ return;
674
+ }
675
+
676
+ if (cliArgs.includes("logout")) {
677
+ console.log("\n [CLOUD] Signing out of the cloud...");
678
+ const { logoutFromCloud } = await import("./admin/auth.js");
679
+ const deleted = logoutFromCloud();
680
+ if (deleted) {
681
+ console.log(" \x1b[32m[OK] You have been signed out. Encrypted secrets removed. Mode reverted to only-local.\x1b[0m\n");
682
+ } else {
683
+ console.log(" [*] Mode reverted to only-local. No session tokens were found.\x1b[0m\n");
684
+ }
685
+ return;
686
+ }
687
+
661
688
  let running = true;
662
689
  let selectedIndex = 0;
663
690
 
@@ -760,6 +787,27 @@ export async function runCli() {
760
787
  },
761
788
  ],
762
789
  },
790
+ {
791
+ title: "Cloud Synchronization & Turso",
792
+ items: [
793
+ {
794
+ label: "[CLOUD] Login to Turso Cloud",
795
+ value: "cloud_login",
796
+ info: "Perform secure OAuth/Device login flow with loopback listener and local AES-256 key encryption",
797
+ },
798
+ {
799
+ label: "[CLOUD] Logout",
800
+ value: "cloud_logout",
801
+ info: "Sign out, purge encrypted secrets, and revert mode to only-local",
802
+ },
803
+ {
804
+ label: "Operational Mode",
805
+ badge: config.mode.toUpperCase(),
806
+ value: "cloud_mode",
807
+ info: "Choose Operational Mode: only-local | only-cloud | hybrid-sync",
808
+ },
809
+ ],
810
+ },
763
811
  {
764
812
  title: "Global Prompt & Integration Management",
765
813
  items: [
@@ -1103,10 +1151,11 @@ export async function runCli() {
1103
1151
  await writeMemory(key, updated);
1104
1152
  let links = 0;
1105
1153
  try {
1106
- const db = getDatabase();
1107
- links = db
1154
+ const db = await getDatabase();
1155
+ const runRes = await db
1108
1156
  .prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
1109
- .run(newText, key, factText(selectedEntry)).changes;
1157
+ .run(newText, key, factText(selectedEntry));
1158
+ links = runRes ? runRes.changes : 0;
1110
1159
  } catch (e) {}
1111
1160
  console.clear();
1112
1161
  console.log(`\n [OK] Fact updated successfully${links ? `, ${links} doc link(s) updated` : ""}.\n`);
@@ -1234,8 +1283,8 @@ export async function runCli() {
1234
1283
  case "rag_docs": {
1235
1284
  let docRunning = true;
1236
1285
  while (docRunning) {
1237
- const db = getDatabase();
1238
- const docs = db.prepare("SELECT id, title, path, created_at FROM documents ORDER BY created_at DESC").all();
1286
+ const db = await getDatabase();
1287
+ const docs = await db.prepare("SELECT id, title, path, created_at FROM documents ORDER BY created_at DESC").all();
1239
1288
 
1240
1289
  if (!docs || docs.length === 0) {
1241
1290
  console.clear();
@@ -1295,9 +1344,11 @@ export async function runCli() {
1295
1344
  });
1296
1345
 
1297
1346
  if (actionRes.action === "select" && actionRes.value === "info") {
1298
- const secCount = db.prepare("SELECT COUNT(*) as cnt FROM sections WHERE doc_id = ?").get(targetDoc.id).cnt;
1299
- const chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks WHERE doc_id = ?").get(targetDoc.id).cnt;
1300
- const sampleSections = db.prepare("SELECT heading FROM sections WHERE doc_id = ? LIMIT 5").all(targetDoc.id);
1347
+ const secCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM sections WHERE doc_id = ?").get(targetDoc.id);
1348
+ const secCount = secCountRow ? secCountRow.cnt : 0;
1349
+ const chunkCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks WHERE doc_id = ?").get(targetDoc.id);
1350
+ const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
1351
+ const sampleSections = await db.prepare("SELECT heading FROM sections WHERE doc_id = ? LIMIT 5").all(targetDoc.id);
1301
1352
 
1302
1353
  console.clear();
1303
1354
  const line = "─".repeat(PANEL_WIDTH - 2);
@@ -1776,6 +1827,51 @@ export async function runCli() {
1776
1827
  }
1777
1828
  break;
1778
1829
  }
1830
+ case "cloud_login": {
1831
+ console.clear();
1832
+ console.log("\n [CLOUD] Starting Turso cloud authorization...");
1833
+ const { loginToCloud } = await import("./admin/auth.js");
1834
+ try {
1835
+ const secrets = await loginToCloud();
1836
+ console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Connected to endpoint: ${secrets.dbUrl}\x1b[0m\n`);
1837
+ } catch (e) {
1838
+ console.error(`\n \x1b[31m[ERROR] Authorization failed: ${e.message}\x1b[0m\n`);
1839
+ }
1840
+ await waitForEnter();
1841
+ break;
1842
+ }
1843
+ case "cloud_logout": {
1844
+ console.clear();
1845
+ console.log("\n [CLOUD] Signing out of the cloud...");
1846
+ const { logoutFromCloud } = await import("./admin/auth.js");
1847
+ const deleted = logoutFromCloud();
1848
+ if (deleted) {
1849
+ console.log(" \x1b[32m[OK] You have been signed out. Encrypted secrets removed. Mode reverted to only-local.\x1b[0m\n");
1850
+ } else {
1851
+ console.log(" [*] Mode reverted to only-local. No session tokens were found.\x1b[0m\n");
1852
+ }
1853
+ await waitForEnter();
1854
+ break;
1855
+ }
1856
+ case "cloud_mode": {
1857
+ const modeItems = [
1858
+ { label: "only-local (Local only)", value: "only-local", info: "Fully private, offline-first mode (everything stored on disk)" },
1859
+ { label: "only-cloud (Cloud only)", value: "only-cloud", info: "Fully serverless cloud mode with no local caching" },
1860
+ { label: "hybrid-sync (Local with background sync)", value: "hybrid-sync", info: "Instant local operations with a background sync daemon" },
1861
+ ];
1862
+ const initialIdx = Math.max(0, modeItems.findIndex((i) => i.value === config.mode));
1863
+ const subRes = await selectSimpleMenu({
1864
+ title: "CHOOSE OPERATIONAL MODE",
1865
+ subtitle: "Configure database storage and cloud sync behavior",
1866
+ items: modeItems,
1867
+ initialIndex: initialIdx,
1868
+ });
1869
+
1870
+ if (subRes.action === "select") {
1871
+ updateConfig({ mode: subRes.value });
1872
+ }
1873
+ break;
1874
+ }
1779
1875
  case "enable_prompt": {
1780
1876
  const { enableGlobalPrompt } = await import("./prompt_manager.js");
1781
1877
  const results = await enableGlobalPrompt();
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import os from "node:os";
5
+ import { execSync } from "node:child_process";
6
+ import { MEMORY_DIR, ensureDirSync } from "../memory.js";
7
+
8
+ const SECRETS_FILE = path.join(MEMORY_DIR, "auth_secrets.enc");
9
+
10
+ // Stable per-machine identifier. Must NOT rely on volatile values (e.g.
11
+ // os.networkInterfaces() — VPN adapters, hotspot IPs and IPv6 privacy
12
+ // addresses rotate constantly and would silently change the AES key).
13
+ function getMachineId() {
14
+ try {
15
+ if (process.platform === "win32") {
16
+ const out = execSync("reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid", {
17
+ encoding: "utf8",
18
+ stdio: ["ignore", "pipe", "ignore"],
19
+ });
20
+ const m = out.match(/MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]{36})/i);
21
+ if (m) return m[1].toLowerCase();
22
+ } else if (process.platform === "linux") {
23
+ for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
24
+ try {
25
+ const v = fs.readFileSync(p, "utf8").trim();
26
+ if (v) return v;
27
+ } catch {}
28
+ }
29
+ } else if (process.platform === "darwin") {
30
+ const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice", {
31
+ encoding: "utf8",
32
+ stdio: ["ignore", "pipe", "ignore"],
33
+ });
34
+ const m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
35
+ if (m) return m[1];
36
+ }
37
+ } catch {}
38
+ return null;
39
+ }
40
+
41
+ // Generate a deterministic hardware + system fingerprint (stable across reboots,
42
+ // network changes and user sessions on the same machine).
43
+ function getSystemFingerprint() {
44
+ const parts = [
45
+ getMachineId() || "no-machine-id",
46
+ os.hostname() || "localhost",
47
+ os.userInfo()?.username || "default_user",
48
+ os.platform() || "unknown",
49
+ os.arch() || "unknown",
50
+ ];
51
+ return parts.join("|");
52
+ }
53
+
54
+ // Derive a 256-bit (32 bytes) key using PBKDF2 with salt derived from fingerprint
55
+ function deriveEncryptionKey() {
56
+ const fingerprint = getSystemFingerprint();
57
+ const salt = crypto.createHash("sha256").update(fingerprint).digest();
58
+ // PBKDF2 with 10,000 iterations to derive a secure 32-byte key
59
+ return crypto.pbkdf2Sync(fingerprint, salt, 10000, 32, "sha256");
60
+ }
61
+
62
+ // Encrypt data using AES-256-GCM
63
+ export function encryptData(plainText) {
64
+ const key = deriveEncryptionKey();
65
+ const iv = crypto.randomBytes(12); // 96-bit IV is standard for GCM
66
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
67
+
68
+ let encrypted = cipher.update(plainText, "utf8", "hex");
69
+ encrypted += cipher.final("hex");
70
+
71
+ const authTag = cipher.getAuthTag().toString("hex");
72
+
73
+ // Format as: iv_hex:auth_tag_hex:encrypted_hex
74
+ return `${iv.toString("hex")}:${authTag}:${encrypted}`;
75
+ }
76
+
77
+ // Decrypt data using AES-256-GCM
78
+ export function decryptData(encryptedStr) {
79
+ const parts = encryptedStr.split(":");
80
+ if (parts.length !== 3) {
81
+ throw new Error("Invalid encrypted format. Expected iv:tag:ciphertext");
82
+ }
83
+
84
+ const [ivHex, authTagHex, encryptedHex] = parts;
85
+ const key = deriveEncryptionKey();
86
+ const iv = Buffer.from(ivHex, "hex");
87
+ const authTag = Buffer.from(authTagHex, "hex");
88
+
89
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
90
+ decipher.setAuthTag(authTag);
91
+
92
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
93
+ decrypted += decipher.final("utf8");
94
+
95
+ return decrypted;
96
+ }
97
+
98
+ // Save secrets securely
99
+ export function saveSecrets(secrets) {
100
+ ensureDirSync();
101
+ const plainText = JSON.stringify(secrets);
102
+ const encrypted = encryptData(plainText);
103
+ fs.writeFileSync(SECRETS_FILE, encrypted, "utf-8");
104
+ }
105
+
106
+ // Load secrets securely
107
+ export function loadSecrets() {
108
+ if (!fs.existsSync(SECRETS_FILE)) {
109
+ return null;
110
+ }
111
+ try {
112
+ const encrypted = fs.readFileSync(SECRETS_FILE, "utf-8").trim();
113
+ if (!encrypted) return null;
114
+ const decrypted = decryptData(encrypted);
115
+ return JSON.parse(decrypted);
116
+ } catch (err) {
117
+ console.error(
118
+ "Failed to decrypt or load cloud secrets:",
119
+ err.message,
120
+ "— the file was encrypted with a different machine key. Re-run login to recreate it."
121
+ );
122
+ return null;
123
+ }
124
+ }
125
+
126
+ // Delete secrets from disk
127
+ export function deleteSecrets() {
128
+ if (fs.existsSync(SECRETS_FILE)) {
129
+ try {
130
+ fs.unlinkSync(SECRETS_FILE);
131
+ return true;
132
+ } catch (err) {
133
+ console.error("Failed to delete secrets file:", err.message);
134
+ }
135
+ }
136
+ return false;
137
+ }
@@ -14,6 +14,11 @@ export const DEFAULT_CONFIG = {
14
14
  gpuAttentionBudget: 2000000, // GPU micro-batch attention budget [1M - 16M] (default 2.0M ~1.5GB VRAM)
15
15
  onnxThreads: 0, // ONNX WASM threads: 0 = auto-detect CPU cores, or 1-16
16
16
  executionDevice: "cpu", // "cpu" | "webgpu"
17
+ mode: "only-local", // "only-local" | "only-cloud" | "hybrid-sync"
18
+ tursoUrl: "", // Connection endpoint URL for Turso DB
19
+ failoverUrl: "", // Failover connection endpoint URL (Fly.io + LiteFS)
20
+ authorized: false, // True once the user completed cloud login (token stored encrypted)
21
+ username: "", // Account username from the Turso OAuth profile
17
22
  };
18
23
 
19
24
  let cachedConfig = null;
@@ -1,41 +1,225 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import { join } from "path";
3
3
  import { existsSync, mkdirSync } from "fs";
4
- import { MEMORY_DIR, ensureDir } from "../memory.js";
4
+ import { MEMORY_DIR } from "../memory.js";
5
5
  import { runMigrations } from "./migrations.js";
6
+ import { getConfig } from "../config/config_manager.js";
7
+ import { loadSecrets } from "../config/auth_store.js";
8
+ import { createClient } from "@libsql/client";
6
9
 
7
10
  let dbInstance = null;
11
+ let dbInitPromise = null;
8
12
 
9
13
  export const STORAGE_DIR = join(MEMORY_DIR, "storage");
10
14
  export const BLOBS_DIR = join(STORAGE_DIR, "blobs");
11
15
  export const MODELS_DIR = join(STORAGE_DIR, "models");
12
16
  export const DB_PATH = join(STORAGE_DIR, "memory.sqlite");
13
17
 
14
- export function getDatabase(customPath = null) {
15
- if (dbInstance && !customPath) {
16
- return dbInstance;
18
+ class DatabaseWrapper {
19
+ constructor(localDb, cloudClient, mode, failoverClient = null) {
20
+ this.localDb = localDb;
21
+ this.cloudClient = cloudClient;
22
+ this.mode = mode;
23
+ this.failoverClient = failoverClient;
24
+ this.usingFailover = false;
25
+ this.consecutiveFailures = 0;
17
26
  }
18
27
 
19
- const dbPath = customPath || DB_PATH;
20
- const parentDir = join(dbPath, "..");
21
- if (!existsSync(parentDir)) {
22
- mkdirSync(parentDir, { recursive: true });
28
+ async runWithRetry(fn) {
29
+ let attempts = 0;
30
+ const maxAttempts = 3;
31
+ const timeoutMs = 10000;
32
+
33
+ while (attempts < maxAttempts) {
34
+ attempts++;
35
+ const controller = new AbortController();
36
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
37
+
38
+ try {
39
+ const client = (this.usingFailover && this.failoverClient) ? this.failoverClient : this.cloudClient;
40
+ const result = await Promise.race([
41
+ fn(client),
42
+ new Promise((_, reject) => {
43
+ controller.signal.addEventListener("abort", () => {
44
+ reject(new Error("Database operation timed out after 10 seconds"));
45
+ });
46
+ })
47
+ ]);
48
+ clearTimeout(timeoutId);
49
+ // Successful operation, reset consecutive failures
50
+ this.consecutiveFailures = 0;
51
+ return result;
52
+ } catch (err) {
53
+ clearTimeout(timeoutId);
54
+ if (attempts >= maxAttempts) {
55
+ this.consecutiveFailures++;
56
+ if (this.consecutiveFailures >= 3 && this.failoverClient && !this.usingFailover) {
57
+ console.warn("[WARN] Turso is temporarily unreachable. Switching to LiteFS failover replica...");
58
+ this.usingFailover = true;
59
+ // Retry the operation on the failover client
60
+ return this.runWithRetry(fn);
61
+ }
62
+ throw err;
63
+ }
64
+ // Small delay before retrying (exponential backoff / fixed delay)
65
+ await new Promise((resolve) => setTimeout(resolve, 200));
66
+ }
67
+ }
68
+ }
69
+
70
+ async exec(sql) {
71
+ if (this.mode === "only-cloud" && (this.cloudClient || this.failoverClient)) {
72
+ const trimmed = sql.trim().replace(/;$/, "").toUpperCase();
73
+ if (trimmed === "BEGIN" || trimmed === "BEGIN IMMEDIATE" || trimmed === "COMMIT" || trimmed === "ROLLBACK") {
74
+ return;
75
+ }
76
+ return await this.runWithRetry(async (client) => {
77
+ return await client.executeMultiple(sql);
78
+ });
79
+ } else {
80
+ return this.localDb.exec(sql);
81
+ }
82
+ }
83
+
84
+ prepare(sql) {
85
+ const self = this;
86
+ return {
87
+ async run(...args) {
88
+ if (self.mode === "only-cloud" && (self.cloudClient || self.failoverClient)) {
89
+ const res = await self.runWithRetry(async (client) => {
90
+ return await client.execute({ sql, args });
91
+ });
92
+ return {
93
+ changes: res.rowsAffected || 0,
94
+ lastInsertRowid: res.lastInsertRowid !== undefined ? Number(res.lastInsertRowid) : undefined,
95
+ };
96
+ } else {
97
+ return self.localDb.prepare(sql).run(...args);
98
+ }
99
+ },
100
+ async get(...args) {
101
+ if (self.mode === "only-cloud" && (self.cloudClient || self.failoverClient)) {
102
+ const res = await self.runWithRetry(async (client) => {
103
+ return await client.execute({ sql, args });
104
+ });
105
+ return res.rows[0];
106
+ } else {
107
+ return self.localDb.prepare(sql).get(...args);
108
+ }
109
+ },
110
+ async all(...args) {
111
+ if (self.mode === "only-cloud" && (self.cloudClient || self.failoverClient)) {
112
+ const res = await self.runWithRetry(async (client) => {
113
+ return await client.execute({ sql, args });
114
+ });
115
+ return res.rows;
116
+ } else {
117
+ return self.localDb.prepare(sql).all(...args);
118
+ }
119
+ },
120
+ };
121
+ }
122
+
123
+ close() {
124
+ if (this.localDb) {
125
+ try {
126
+ this.localDb.close();
127
+ } catch (e) {}
128
+ this.localDb = null;
129
+ }
130
+ if (this.cloudClient) {
131
+ try {
132
+ this.cloudClient.close();
133
+ } catch (e) {}
134
+ this.cloudClient = null;
135
+ }
136
+ if (this.failoverClient) {
137
+ try {
138
+ this.failoverClient.close();
139
+ } catch (e) {}
140
+ this.failoverClient = null;
141
+ }
142
+ }
143
+ }
144
+
145
+ async function openDatabase(customPath, mode) {
146
+ const config = getConfig();
147
+ let localDb = null;
148
+ if (mode !== "only-cloud") {
149
+ const dbPath = customPath || DB_PATH;
150
+ const parentDir = join(dbPath, "..");
151
+ if (!existsSync(parentDir)) {
152
+ mkdirSync(parentDir, { recursive: true });
153
+ }
154
+ localDb = new DatabaseSync(dbPath);
155
+ localDb.exec("PRAGMA foreign_keys = ON;");
156
+ localDb.exec("PRAGMA journal_mode = WAL;");
157
+ }
158
+
159
+ let cloudClient = null;
160
+ let failoverClient = null;
161
+ if (mode === "only-cloud" || mode === "hybrid-sync") {
162
+ const secrets = loadSecrets();
163
+ const tursoUrl = customPath && customPath.startsWith("libsql:") ? customPath : (secrets?.dbUrl || config.tursoUrl);
164
+ const failoverUrl = config.failoverUrl || "";
165
+ const token = secrets?.token;
166
+
167
+ if (tursoUrl) {
168
+ cloudClient = createClient({
169
+ url: tursoUrl,
170
+ authToken: token || undefined,
171
+ });
172
+ if (failoverUrl) {
173
+ failoverClient = createClient({
174
+ url: failoverUrl,
175
+ authToken: token || undefined,
176
+ });
177
+ }
178
+ // In hybrid-sync mode, ensure remote schema is also fully migrated and up to date
179
+ if (mode === "hybrid-sync") {
180
+ const cloudDbWrapper = new DatabaseWrapper(null, cloudClient, "only-cloud", failoverClient);
181
+ await runMigrations(cloudDbWrapper);
182
+ }
183
+ } else if (mode === "only-cloud") {
184
+ throw new Error("Turso URL is required for only-cloud mode. Please login first.");
185
+ }
186
+ }
187
+
188
+ const wrappedDb = new DatabaseWrapper(localDb, cloudClient, mode, failoverClient);
189
+
190
+ // Initialize/run migrations
191
+ await runMigrations(wrappedDb);
192
+
193
+ if (!customPath) {
194
+ dbInstance = wrappedDb;
23
195
  }
24
196
 
25
- const db = new DatabaseSync(dbPath);
26
- db.exec("PRAGMA foreign_keys = ON;");
27
- db.exec("PRAGMA journal_mode = WAL;");
197
+ return wrappedDb;
198
+ }
28
199
 
29
- runMigrations(db);
200
+ export async function getDatabase(customPath = null, forceMode = null) {
201
+ const config = getConfig();
202
+ const mode = forceMode || config.mode || "only-local";
30
203
 
31
204
  if (!customPath) {
32
- dbInstance = db;
205
+ if (dbInstance && dbInstance.mode === mode) {
206
+ return dbInstance;
207
+ }
208
+ // Deduplicate concurrent default-DB initialization so migrations never run
209
+ // on multiple connections at once (avoids "database is locked" crashes).
210
+ if (!dbInitPromise) {
211
+ dbInitPromise = openDatabase(null, mode).finally(() => {
212
+ dbInitPromise = null;
213
+ });
214
+ }
215
+ return await dbInitPromise;
33
216
  }
34
217
 
35
- return db;
218
+ return openDatabase(customPath, mode);
36
219
  }
37
220
 
38
221
  export function closeDatabase() {
222
+ dbInitPromise = null;
39
223
  if (dbInstance) {
40
224
  dbInstance.close();
41
225
  dbInstance = null;