@h-ai/reldb 0.1.0-alpha.49 → 0.1.0-alpha.51

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/README.md CHANGED
@@ -74,6 +74,24 @@ await reldb.init({
74
74
  })
75
75
  ```
76
76
 
77
+ ### SQLite 执行方式(worker 异步)
78
+
79
+ better-sqlite3 是同步驱动,查询会阻塞 Node 主线程事件循环。WAL 只能改善数据库锁并发,无法避免同步调用占用主线程。需要卸载这一阻塞时,可把 SQLite 切到专用 worker 线程执行(单连接,opt-in,默认 `sync`):
80
+
81
+ ```ts
82
+ await reldb.init({
83
+ type: 'sqlite',
84
+ database: './data.db',
85
+ sqlite: {
86
+ executor: 'worker', // 'sync'(默认)| 'worker'
87
+ walMode: true, // 默认 true
88
+ workerConnectTimeoutMs: 10000, // worker 打开连接超时,默认 10000
89
+ },
90
+ })
91
+ ```
92
+
93
+ worker 模式下查询、写入与事务均在 worker 线程串行执行;公共 API 与返回值与同步模式完全一致。
94
+
77
95
  ### 操作日志
78
96
 
79
97
  可在数据库配置中通过 `operationLog` 开启 SQL 操作日志。日志在 reldb provider-base 的 DML 操作层统一输出,不在 `reldb-main.ts` 或各 raw provider 中重复包装。
package/dist/index.d.ts CHANGED
@@ -93,14 +93,20 @@ type SslConfig = z.infer<typeof SslConfigSchema>;
93
93
  * @example
94
94
  * ```ts
95
95
  * sqlite: {
96
- * walMode: true, // 启用 WAL 模式,提高并发性能
97
- * readonly: false // 只读模式
96
+ * walMode: true, // 启用 WAL 模式,提高并发性能
97
+ * readonly: false, // 只读模式
98
+ * executor: 'worker' // 用 worker 线程执行,避免同步查询阻塞主线程事件循环
98
99
  * }
99
100
  * ```
100
101
  */
101
102
  declare const SqliteOptionsSchema: z.ZodObject<{
102
103
  walMode: z.ZodDefault<z.ZodBoolean>;
103
104
  readonly: z.ZodDefault<z.ZodBoolean>;
105
+ executor: z.ZodDefault<z.ZodEnum<{
106
+ sync: "sync";
107
+ worker: "worker";
108
+ }>>;
109
+ workerConnectTimeoutMs: z.ZodDefault<z.ZodNumber>;
104
110
  }, z.core.$strip>;
105
111
  /**
106
112
  * MySQL 特定选项 Schema
@@ -145,6 +151,11 @@ declare const SqliteConfigSchema: z.ZodObject<{
145
151
  sqlite: z.ZodOptional<z.ZodObject<{
146
152
  walMode: z.ZodDefault<z.ZodBoolean>;
147
153
  readonly: z.ZodDefault<z.ZodBoolean>;
154
+ executor: z.ZodDefault<z.ZodEnum<{
155
+ sync: "sync";
156
+ worker: "worker";
157
+ }>>;
158
+ workerConnectTimeoutMs: z.ZodDefault<z.ZodNumber>;
148
159
  }, z.core.$strip>>;
149
160
  }, z.core.$strip>;
150
161
  /**
@@ -274,6 +285,11 @@ declare const ReldbConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
274
285
  sqlite: z.ZodOptional<z.ZodObject<{
275
286
  walMode: z.ZodDefault<z.ZodBoolean>;
276
287
  readonly: z.ZodDefault<z.ZodBoolean>;
288
+ executor: z.ZodDefault<z.ZodEnum<{
289
+ sync: "sync";
290
+ worker: "worker";
291
+ }>>;
292
+ workerConnectTimeoutMs: z.ZodDefault<z.ZodNumber>;
277
293
  }, z.core.$strip>>;
278
294
  }, z.core.$strip>, z.ZodObject<{
279
295
  url: z.ZodOptional<z.ZodString>;
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { core, ok, err } from '@h-ai/core';
3
+ import { existsSync } from 'fs';
3
4
  import { createRequire } from 'module';
5
+ import { Worker } from 'worker_threads';
4
6
 
5
7
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
8
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -37,7 +39,17 @@ var SqliteOptionsSchema = z.object({
37
39
  /** 是否启用 WAL 模式(默认 true),可提高并发读写性能 */
38
40
  walMode: z.boolean().default(true),
39
41
  /** 是否只读模式(默认 false) */
40
- readonly: z.boolean().default(false)
42
+ readonly: z.boolean().default(false),
43
+ /**
44
+ * 执行方式(默认 sync)。
45
+ *
46
+ * - `sync`:主线程直接执行 better-sqlite3(同步阻塞主线程事件循环)。
47
+ * - `worker`:在专用 worker 线程持有唯一连接执行,把阻塞移出主线程。
48
+ * 注意 WAL 只改善数据库锁并发,无法避免 better-sqlite3 同步调用占用调用线程。
49
+ */
50
+ executor: z.enum(["sync", "worker"]).default("sync"),
51
+ /** worker 执行方式下等待连接打开的超时(毫秒,默认 10000) */
52
+ workerConnectTimeoutMs: z.number().int().positive().default(1e4)
41
53
  });
42
54
  var MysqlOptionsSchema = z.object({
43
55
  /** 字符集(默认 utf8mb4),推荐使用 utf8mb4 支持完整 Unicode */
@@ -2320,9 +2332,157 @@ function createPostgresProvider() {
2320
2332
  };
2321
2333
  }
2322
2334
  var require2 = createRequire(import.meta.url);
2335
+ function createSyncSqliteEngine(options) {
2336
+ const Database = require2("better-sqlite3");
2337
+ const db = new Database(options.database, { readonly: options.readonly });
2338
+ if (options.walMode !== false) {
2339
+ db.pragma("journal_mode = WAL");
2340
+ }
2341
+ let open = true;
2342
+ return {
2343
+ async all(sql, params) {
2344
+ const stmt = db.prepare(sql);
2345
+ return params ? stmt.all(...params) : stmt.all();
2346
+ },
2347
+ async get(sql, params) {
2348
+ const stmt = db.prepare(sql);
2349
+ return (params ? stmt.get(...params) : stmt.get()) ?? null;
2350
+ },
2351
+ async run(sql, params) {
2352
+ const stmt = db.prepare(sql);
2353
+ const result = params ? stmt.run(...params) : stmt.run();
2354
+ return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
2355
+ },
2356
+ async exec(sql) {
2357
+ db.exec(sql);
2358
+ },
2359
+ async batch(statements) {
2360
+ const tx = db.transaction((items) => {
2361
+ for (const item of items) {
2362
+ const stmt = db.prepare(item.sql);
2363
+ if (item.params)
2364
+ stmt.run(...item.params);
2365
+ else stmt.run();
2366
+ }
2367
+ });
2368
+ tx(statements);
2369
+ },
2370
+ async close() {
2371
+ if (open) {
2372
+ db.close();
2373
+ open = false;
2374
+ }
2375
+ },
2376
+ isOpen: () => open
2377
+ };
2378
+ }
2379
+ function resolveWorkerUrl() {
2380
+ const candidates = [
2381
+ // 源码 / 测试:引擎与 worker 同目录。
2382
+ new URL("./reldb-sqlite-worker.mjs", import.meta.url),
2383
+ // 打包:引擎被并入 dist/index.js,worker 复制到 dist/providers/sqlite/。
2384
+ new URL("./providers/sqlite/reldb-sqlite-worker.mjs", import.meta.url)
2385
+ ];
2386
+ for (const candidate of candidates) {
2387
+ if (existsSync(candidate))
2388
+ return candidate;
2389
+ }
2390
+ return candidates[0];
2391
+ }
2392
+ async function createWorkerSqliteEngine(options, connectTimeoutMs) {
2393
+ const worker = new Worker(resolveWorkerUrl(), {
2394
+ workerData: { database: options.database, readonly: options.readonly, walMode: options.walMode }
2395
+ });
2396
+ let seq = 0;
2397
+ let open = true;
2398
+ const pending = /* @__PURE__ */ new Map();
2399
+ function rejectAll(error) {
2400
+ for (const entry of pending.values())
2401
+ entry.reject(error);
2402
+ pending.clear();
2403
+ }
2404
+ worker.on("message", (msg) => {
2405
+ if (!("id" in msg))
2406
+ return;
2407
+ const entry = pending.get(msg.id);
2408
+ if (!entry)
2409
+ return;
2410
+ pending.delete(msg.id);
2411
+ if (msg.ok)
2412
+ entry.resolve(msg.result);
2413
+ else entry.reject(new Error(msg.error?.message ?? "sqlite worker error"));
2414
+ });
2415
+ worker.on("error", (error) => {
2416
+ open = false;
2417
+ rejectAll(error instanceof Error ? error : new Error(String(error)));
2418
+ });
2419
+ worker.on("exit", (code) => {
2420
+ open = false;
2421
+ if (pending.size > 0)
2422
+ rejectAll(new Error(`sqlite worker exited unexpectedly with code ${code}`));
2423
+ });
2424
+ await new Promise((resolve, reject) => {
2425
+ const timer = setTimeout(() => {
2426
+ worker.off("message", onHandshake);
2427
+ reject(new Error(`sqlite worker init timed out after ${connectTimeoutMs}ms`));
2428
+ }, connectTimeoutMs);
2429
+ function onHandshake(msg) {
2430
+ if (msg?.type === "ready") {
2431
+ clearTimeout(timer);
2432
+ worker.off("message", onHandshake);
2433
+ resolve();
2434
+ } else if (msg?.type === "init-error") {
2435
+ clearTimeout(timer);
2436
+ worker.off("message", onHandshake);
2437
+ reject(new Error(msg.error?.message ?? "sqlite worker init failed"));
2438
+ }
2439
+ }
2440
+ worker.on("message", onHandshake);
2441
+ });
2442
+ function post(op, payload) {
2443
+ if (!open)
2444
+ return Promise.reject(new Error("sqlite worker is not open"));
2445
+ const id = ++seq;
2446
+ return new Promise((resolve, reject) => {
2447
+ pending.set(id, { resolve, reject });
2448
+ worker.postMessage({ id, op, ...payload });
2449
+ });
2450
+ }
2451
+ return {
2452
+ // 边界反序列化:worker 通过结构化克隆回传,行/结果类型在此断言。
2453
+ async all(sql, params) {
2454
+ return await post("all", { sql, params });
2455
+ },
2456
+ async get(sql, params) {
2457
+ return await post("get", { sql, params });
2458
+ },
2459
+ async run(sql, params) {
2460
+ return await post("run", { sql, params });
2461
+ },
2462
+ async exec(sql) {
2463
+ await post("exec", { sql });
2464
+ },
2465
+ async batch(statements) {
2466
+ await post("batch", { statements });
2467
+ },
2468
+ async close() {
2469
+ if (!open)
2470
+ return;
2471
+ try {
2472
+ await post("close", {});
2473
+ } catch {
2474
+ }
2475
+ open = false;
2476
+ await worker.terminate();
2477
+ },
2478
+ isOpen: () => open
2479
+ };
2480
+ }
2481
+
2482
+ // src/providers/reldb-provider-sqlite.ts
2323
2483
  var logger4 = core.logger.child({ module: "reldb", scope: "sqlite" });
2324
2484
  function createSqliteProvider() {
2325
- let database = null;
2485
+ let engine = null;
2326
2486
  let currentConfig2 = null;
2327
2487
  let txChain = Promise.resolve();
2328
2488
  const TX_LOCK_TIMEOUT_MS = 3e4;
@@ -2350,7 +2510,7 @@ function createSqliteProvider() {
2350
2510
  return release;
2351
2511
  }
2352
2512
  const ctx = {
2353
- isConnected: () => database !== null && database.open,
2513
+ isConnected: () => engine !== null && engine.isOpen(),
2354
2514
  logger: logger4,
2355
2515
  operationLog: () => currentConfig2?.operationLog
2356
2516
  };
@@ -2393,71 +2553,59 @@ function createSqliteProvider() {
2393
2553
  async createTable(name, columns, ifNotExists = true) {
2394
2554
  const quotedTable = quoteIdentifier(name);
2395
2555
  const sql = buildDefaultCreateTableSql(sqliteBuildColumnSql, quotedTable, columns, ifNotExists);
2396
- database.exec(sql);
2556
+ await engine.exec(sql);
2397
2557
  return ok(void 0);
2398
2558
  },
2399
2559
  async dropTable(name, ifExists) {
2400
2560
  const ifExistsClause = ifExists ? "IF EXISTS " : "";
2401
- database.exec(`DROP TABLE ${ifExistsClause}${quoteIdentifier(name)}`);
2561
+ await engine.exec(`DROP TABLE ${ifExistsClause}${quoteIdentifier(name)}`);
2402
2562
  return ok(void 0);
2403
2563
  },
2404
2564
  async addColumn(table, column, def) {
2405
2565
  const colSql = sqliteBuildColumnSql(column, def);
2406
- database.exec(`ALTER TABLE ${quoteIdentifier(table)} ADD COLUMN ${colSql}`);
2566
+ await engine.exec(`ALTER TABLE ${quoteIdentifier(table)} ADD COLUMN ${colSql}`);
2407
2567
  return ok(void 0);
2408
2568
  },
2409
2569
  async dropColumn(table, column) {
2410
- database.exec(`ALTER TABLE ${quoteIdentifier(table)} DROP COLUMN ${quoteIdentifier(column)}`);
2570
+ await engine.exec(`ALTER TABLE ${quoteIdentifier(table)} DROP COLUMN ${quoteIdentifier(column)}`);
2411
2571
  return ok(void 0);
2412
2572
  },
2413
2573
  async renameTable(oldName, newName) {
2414
- database.exec(buildDefaultRenameTableSql(quoteIdentifier(oldName), quoteIdentifier(newName)));
2574
+ await engine.exec(buildDefaultRenameTableSql(quoteIdentifier(oldName), quoteIdentifier(newName)));
2415
2575
  return ok(void 0);
2416
2576
  },
2417
2577
  async createIndex(table, index, def) {
2418
2578
  const sql = buildDefaultCreateIndexSql(quoteIdentifier, quoteIdentifier(table), quoteIdentifier(index), def);
2419
- database.exec(sql);
2579
+ await engine.exec(sql);
2420
2580
  return ok(void 0);
2421
2581
  },
2422
2582
  async dropIndex(index, ifExists = true) {
2423
2583
  const sql = buildDefaultDropIndexSql(quoteIdentifier, index, ifExists);
2424
- database.exec(sql);
2584
+ await engine.exec(sql);
2425
2585
  return ok(void 0);
2426
2586
  },
2427
2587
  async raw(sql) {
2428
- database.exec(sql);
2588
+ await engine.exec(sql);
2429
2589
  return ok(void 0);
2430
2590
  }
2431
2591
  };
2432
2592
  const rawDml = {
2433
2593
  async query(sql, params) {
2434
- const stmt = database.prepare(sql);
2435
- const rows = params ? stmt.all(...params) : stmt.all();
2594
+ const rows = await engine.all(sql, params);
2436
2595
  return ok(rows);
2437
2596
  },
2438
2597
  async get(sql, params) {
2439
- const stmt = database.prepare(sql);
2440
- const row = params ? stmt.get(...params) : stmt.get();
2598
+ const row = await engine.get(sql, params);
2441
2599
  return ok(row ?? null);
2442
2600
  },
2443
2601
  async execute(sql, params) {
2444
- const stmt = database.prepare(sql);
2445
- const result = params ? stmt.run(...params) : stmt.run();
2446
- return ok({ changes: result.changes, lastInsertRowid: result.lastInsertRowid });
2602
+ const result = await engine.run(sql, params);
2603
+ return ok(result);
2447
2604
  },
2448
2605
  async batch(statements) {
2449
2606
  const releaseTxLock = await acquireTxLock();
2450
2607
  try {
2451
- const db = database;
2452
- const transaction = db.transaction(() => {
2453
- for (const { sql: s, params } of statements) {
2454
- const stmt = db.prepare(s);
2455
- if (params)
2456
- stmt.run(...params);
2457
- else stmt.run();
2458
- }
2459
- });
2460
- transaction();
2608
+ await engine.batch(statements);
2461
2609
  return ok(void 0);
2462
2610
  } catch (error) {
2463
2611
  return err(HaiReldbError.QUERY_FAILED, reldbM("reldb_sqliteBatchFailed", { params: { error: String(error) } }), error);
@@ -2467,45 +2615,32 @@ function createSqliteProvider() {
2467
2615
  },
2468
2616
  async queryPage(options) {
2469
2617
  const result = await queryPageAsync(
2470
- async (sqlStr, params) => {
2471
- const stmt = database.prepare(sqlStr);
2472
- return params ? stmt.all(...params) : stmt.all();
2473
- },
2618
+ (sqlStr, params) => engine.all(sqlStr, params),
2474
2619
  options
2475
2620
  );
2476
2621
  return ok(result);
2477
2622
  }
2478
2623
  };
2479
- function createSqliteTxDmlOps(db) {
2624
+ function createSqliteTxDmlOps(txEngine) {
2480
2625
  return {
2481
2626
  async query(sql, params) {
2482
- const stmt = db.prepare(sql);
2483
- return ok(params ? stmt.all(...params) : stmt.all());
2627
+ return ok(await txEngine.all(sql, params));
2484
2628
  },
2485
2629
  async get(sql, params) {
2486
- const stmt = db.prepare(sql);
2487
- return ok((params ? stmt.get(...params) : stmt.get()) ?? null);
2630
+ return ok(await txEngine.get(sql, params) ?? null);
2488
2631
  },
2489
2632
  async execute(sql, params) {
2490
- const stmt = db.prepare(sql);
2491
- const result = params ? stmt.run(...params) : stmt.run();
2492
- return ok({ changes: result.changes, lastInsertRowid: result.lastInsertRowid });
2633
+ return ok(await txEngine.run(sql, params));
2493
2634
  },
2494
2635
  async batch(statements) {
2495
2636
  for (const { sql: s, params } of statements) {
2496
- const stmt = db.prepare(s);
2497
- if (params)
2498
- stmt.run(...params);
2499
- else stmt.run();
2637
+ await txEngine.run(s, params);
2500
2638
  }
2501
2639
  return ok(void 0);
2502
2640
  },
2503
2641
  async queryPage(options) {
2504
2642
  const result = await queryPageAsync(
2505
- async (sqlStr, params) => {
2506
- const stmt = db.prepare(sqlStr);
2507
- return params ? stmt.all(...params) : stmt.all();
2508
- },
2643
+ (sqlStr, params) => txEngine.all(sqlStr, params),
2509
2644
  options
2510
2645
  );
2511
2646
  return ok(result);
@@ -2513,7 +2648,7 @@ function createSqliteProvider() {
2513
2648
  };
2514
2649
  }
2515
2650
  async function beginTx() {
2516
- const db = database;
2651
+ const txEngine = engine;
2517
2652
  let released = false;
2518
2653
  const releaseTxLock = await acquireTxLock();
2519
2654
  const finishTransaction = () => {
@@ -2523,19 +2658,19 @@ function createSqliteProvider() {
2523
2658
  }
2524
2659
  };
2525
2660
  try {
2526
- db.exec("BEGIN TRANSACTION");
2661
+ await txEngine.exec("BEGIN TRANSACTION");
2527
2662
  } catch (error) {
2528
2663
  finishTransaction();
2529
2664
  return err(HaiReldbError.TRANSACTION_FAILED, sqliteTxErrorMessage(String(error)), error);
2530
2665
  }
2531
- const txDmlOps = createSqliteTxDmlOps(db);
2666
+ const txDmlOps = createSqliteTxDmlOps(txEngine);
2532
2667
  const guardedTxDmlOps = createBaseDmlOps(ctx, txDmlOps);
2533
2668
  return ok(createTxHandle(guardedTxDmlOps, {
2534
2669
  commit: async () => {
2535
- db.exec("COMMIT");
2670
+ await txEngine.exec("COMMIT");
2536
2671
  },
2537
2672
  rollback: async () => {
2538
- db.exec("ROLLBACK");
2673
+ await txEngine.exec("ROLLBACK");
2539
2674
  },
2540
2675
  release: () => finishTransaction(),
2541
2676
  errorMessage: sqliteTxErrorMessage
@@ -2551,40 +2686,41 @@ function createSqliteProvider() {
2551
2686
  return err(HaiReldbError.CONFIG_ERROR, reldbM("reldb_sqliteNeedPath"));
2552
2687
  }
2553
2688
  try {
2554
- const Database = require2("better-sqlite3");
2555
2689
  const sqliteOptions = {
2556
2690
  walMode: true,
2557
2691
  readonly: false,
2692
+ executor: "sync",
2693
+ workerConnectTimeoutMs: 1e4,
2558
2694
  ...config.sqlite ?? {}
2559
2695
  };
2560
- database = new Database(config.database, {
2561
- readonly: sqliteOptions.readonly ?? false
2562
- });
2563
- if (sqliteOptions.walMode !== false) {
2564
- database.pragma("journal_mode = WAL");
2565
- }
2696
+ const engineOptions = {
2697
+ database: config.database,
2698
+ readonly: sqliteOptions.readonly ?? false,
2699
+ walMode: sqliteOptions.walMode !== false
2700
+ };
2701
+ engine = sqliteOptions.executor === "worker" ? await createWorkerSqliteEngine(engineOptions, sqliteOptions.workerConnectTimeoutMs ?? 1e4) : createSyncSqliteEngine(engineOptions);
2566
2702
  currentConfig2 = config;
2567
- logger4.info("Connected to SQLite", { database: config.database });
2703
+ logger4.info("Connected to SQLite", { database: config.database, executor: sqliteOptions.executor });
2568
2704
  return ok(void 0);
2569
2705
  } catch (error) {
2570
2706
  return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", { params: { error: String(error) } }), error);
2571
2707
  }
2572
2708
  },
2573
2709
  async close() {
2574
- if (database) {
2710
+ if (engine) {
2575
2711
  try {
2576
2712
  try {
2577
2713
  await txChain;
2578
2714
  } catch {
2579
2715
  }
2580
- database.close();
2716
+ await engine.close();
2581
2717
  } catch (error) {
2582
2718
  currentConfig2 = null;
2583
- database = null;
2719
+ engine = null;
2584
2720
  return err(HaiReldbError.CONNECTION_FAILED, reldbM("reldb_sqliteConnectionFailed", { params: { error: String(error) } }), error);
2585
2721
  }
2586
2722
  currentConfig2 = null;
2587
- database = null;
2723
+ engine = null;
2588
2724
  logger4.info("Disconnected from SQLite");
2589
2725
  }
2590
2726
  return ok(void 0);