@xnetjs/sqlite 0.0.3 → 0.1.0

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.
@@ -1,5 +1,5 @@
1
- import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-t2aqQ__n.js';
2
- import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult } from '../types-DMrj3K4o.js';
1
+ import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-imtxkmXZ.js';
2
+ import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult } from '../types-BTabr_VP.js';
3
3
 
4
4
  /**
5
5
  * @xnetjs/sqlite - Expo SQLite adapter using expo-sqlite
@@ -31,6 +31,10 @@ declare class ExpoSQLiteAdapter implements SQLiteAdapter {
31
31
  isOpen(): boolean;
32
32
  query<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T[]>;
33
33
  queryOne<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T | null>;
34
+ queryBatch(reads: Array<{
35
+ sql: string;
36
+ params?: SQLValue[];
37
+ }>): Promise<SQLRow[][]>;
34
38
  run(sql: string, params?: SQLValue[]): Promise<RunResult>;
35
39
  exec(sql: string): Promise<void>;
36
40
  transaction<T>(fn: () => Promise<T>): Promise<T>;
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  SCHEMA_DDL,
6
6
  SCHEMA_VERSION
7
- } from "../chunk-NVD7G7DZ.js";
7
+ } from "../chunk-W3L4FXU5.js";
8
8
 
9
9
  // src/adapters/expo.ts
10
10
  var ExpoSQLiteAdapter = class {
@@ -53,6 +53,13 @@ var ExpoSQLiteAdapter = class {
53
53
  const result = await this.db.getFirstAsync(sql, params ?? []);
54
54
  return result ?? null;
55
55
  }
56
+ async queryBatch(reads) {
57
+ const results = [];
58
+ for (const read of reads) {
59
+ results.push(await this.query(read.sql, read.params));
60
+ }
61
+ return results;
62
+ }
56
63
  async run(sql, params) {
57
64
  this.ensureOpen();
58
65
  const result = await this.db.runAsync(sql, params ?? []);
@@ -1,5 +1,5 @@
1
- import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-t2aqQ__n.js';
2
- import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult } from '../types-DMrj3K4o.js';
1
+ import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-imtxkmXZ.js';
2
+ import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult } from '../types-BTabr_VP.js';
3
3
 
4
4
  /**
5
5
  * @xnetjs/sqlite - In-memory SQLite adapter for testing
@@ -22,6 +22,10 @@ declare class MemorySQLiteAdapter implements SQLiteAdapter {
22
22
  isOpen(): boolean;
23
23
  query<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T[]>;
24
24
  queryOne<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T | null>;
25
+ queryBatch(reads: Array<{
26
+ sql: string;
27
+ params?: SQLValue[];
28
+ }>): Promise<SQLRow[][]>;
25
29
  run(sql: string, params?: SQLValue[]): Promise<RunResult>;
26
30
  exec(sql: string): Promise<void>;
27
31
  transaction<T>(fn: () => Promise<T>): Promise<T>;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SCHEMA_DDL_CORE,
3
3
  SCHEMA_VERSION
4
- } from "../chunk-NVD7G7DZ.js";
4
+ } from "../chunk-W3L4FXU5.js";
5
5
 
6
6
  // src/adapters/memory.ts
7
7
  var MemorySQLiteAdapter = class {
@@ -47,6 +47,13 @@ SQL: ${sql}`);
47
47
  const rows = await this.query(sql, params);
48
48
  return rows[0] ?? null;
49
49
  }
50
+ async queryBatch(reads) {
51
+ const results = [];
52
+ for (const read of reads) {
53
+ results.push(await this.query(read.sql, read.params));
54
+ }
55
+ return results;
56
+ }
50
57
  async run(sql, params) {
51
58
  this.ensureOpen();
52
59
  try {
@@ -0,0 +1,78 @@
1
+ import { S as SQLValue, a as SQLRow } from '../types-BTabr_VP.js';
2
+ import DatabaseType from 'better-sqlite3';
3
+
4
+ /**
5
+ * @xnetjs/sqlite - Read-only reader thread for the Electron reader pool
6
+ *
7
+ * Runs in a Node `worker_threads` worker spawned by {@link ReaderPool}
8
+ * (exploration 0230). It opens its **own** `better-sqlite3` connection to the
9
+ * same database file in **read-only** mode and serves SELECTs against the WAL
10
+ * snapshot. Because native SQLite + WAL allows one writer concurrent with many
11
+ * readers — each on its own connection — these threads run genuinely in parallel
12
+ * on other cores, unlike the browser (where the `opfs-sahpool` VFS holds an
13
+ * exclusive handle and a second connection is impossible, exploration 0228).
14
+ *
15
+ * Each reader uses one-shot autocommit selects (no long-lived read transaction),
16
+ * so every query observes the latest committed snapshot and nothing holds back
17
+ * the writer's WAL checkpoint.
18
+ *
19
+ * The request handler is exported so it can be unit-tested against an in-process
20
+ * connection without spawning a worker.
21
+ */
22
+
23
+ /** A read request sent from the pool to a reader thread. */
24
+ type ReaderRequest = {
25
+ id: number;
26
+ op: 'query';
27
+ sql: string;
28
+ params?: SQLValue[];
29
+ } | {
30
+ id: number;
31
+ op: 'queryOne';
32
+ sql: string;
33
+ params?: SQLValue[];
34
+ } | {
35
+ id: number;
36
+ op: 'ping';
37
+ };
38
+ /** A reader thread's response, correlated back to the request by `id`. */
39
+ type ReaderResponse = {
40
+ id: number;
41
+ ok: true;
42
+ rows: SQLRow[];
43
+ } | {
44
+ id: number;
45
+ ok: true;
46
+ row: SQLRow | null;
47
+ } | {
48
+ id: number;
49
+ ok: true;
50
+ pong: true;
51
+ } | {
52
+ id: number;
53
+ ok: false;
54
+ error: string;
55
+ };
56
+ /** Signal the pool the reader booted (or failed to). `id` is always 0. */
57
+ type ReaderReady = {
58
+ id: 0;
59
+ ready: true;
60
+ } | {
61
+ id: 0;
62
+ ready: false;
63
+ error: string;
64
+ };
65
+ /**
66
+ * Execute one reader request against `db`, caching prepared statements per
67
+ * connection. Pure and synchronous — `better-sqlite3` is synchronous, which is
68
+ * exactly why a reader thread fully occupies a core for the duration of a query.
69
+ */
70
+ declare function handleReaderRequest(db: DatabaseType.Database, cache: Map<string, DatabaseType.Statement>, req: ReaderRequest): ReaderResponse;
71
+ /**
72
+ * Open a read-only connection for a reader thread. Applies connection-local
73
+ * pragmas only — `journal_mode` is owned by the writer and cannot be set on a
74
+ * read-only handle, but the reader transparently observes the file's WAL.
75
+ */
76
+ declare function openReaderConnection(dbPath: string): Promise<DatabaseType.Database>;
77
+
78
+ export { type ReaderReady, type ReaderRequest, type ReaderResponse, handleReaderRequest, openReaderConnection };
@@ -0,0 +1,57 @@
1
+ // src/adapters/reader-thread.ts
2
+ function handleReaderRequest(db, cache, req) {
3
+ try {
4
+ if (req.op === "ping") {
5
+ return { id: req.id, ok: true, pong: true };
6
+ }
7
+ let stmt = cache.get(req.sql);
8
+ if (!stmt) {
9
+ stmt = db.prepare(req.sql);
10
+ cache.set(req.sql, stmt);
11
+ }
12
+ if (req.op === "query") {
13
+ const rows = req.params ? stmt.all(...req.params) : stmt.all();
14
+ return { id: req.id, ok: true, rows };
15
+ }
16
+ const row = req.params ? stmt.get(...req.params) : stmt.get();
17
+ return { id: req.id, ok: true, row: row ?? null };
18
+ } catch (err) {
19
+ return { id: req.id, ok: false, error: err instanceof Error ? err.message : String(err) };
20
+ }
21
+ }
22
+ async function openReaderConnection(dbPath) {
23
+ const { default: Database } = await import("better-sqlite3");
24
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
25
+ db.pragma("busy_timeout = 5000");
26
+ db.pragma("cache_size = -32000");
27
+ db.pragma("temp_store = MEMORY");
28
+ return db;
29
+ }
30
+ async function bootstrapReaderThread() {
31
+ const { parentPort, workerData, isMainThread } = await import("worker_threads");
32
+ if (isMainThread || !parentPort) return;
33
+ const dbPath = workerData?.dbPath;
34
+ if (!dbPath) {
35
+ parentPort.postMessage({ id: 0, ready: false, error: "reader thread: missing dbPath" });
36
+ return;
37
+ }
38
+ try {
39
+ const db = await openReaderConnection(dbPath);
40
+ const cache = /* @__PURE__ */ new Map();
41
+ parentPort.on("message", (req) => {
42
+ parentPort.postMessage(handleReaderRequest(db, cache, req));
43
+ });
44
+ parentPort.postMessage({ id: 0, ready: true });
45
+ } catch (err) {
46
+ parentPort.postMessage({
47
+ id: 0,
48
+ ready: false,
49
+ error: err instanceof Error ? err.message : String(err)
50
+ });
51
+ }
52
+ }
53
+ void bootstrapReaderThread();
54
+ export {
55
+ handleReaderRequest,
56
+ openReaderConnection
57
+ };
@@ -1,5 +1,8 @@
1
- import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-t2aqQ__n.js';
2
- import { b as SQLiteConfig, a as SQLRow, S as SQLValue, R as RunResult, k as SQLiteNodeBatchApplyInput, l as SQLiteNodeBatchApplyResult, d as SQLiteOperationStats } from '../types-DMrj3K4o.js';
1
+ import { S as SQLiteAdapter, P as PreparedStatement } from '../adapter-imtxkmXZ.js';
2
+ import { b as SQLiteConfig, a as SQLRow, S as SQLValue, c as SQLBatchRead, R as RunResult, l as SQLiteNodeBatchApplyInput, m as SQLiteNodeBatchApplyResult, e as SQLiteOperationStats } from '../types-BTabr_VP.js';
3
+ import { a as SchedulerOpStats } from '../worker-scheduler-D04DqMmR.js';
4
+
5
+ type TabRole = 'leader' | 'follower';
3
6
 
4
7
  /**
5
8
  * @xnetjs/sqlite - Main thread proxy for SQLite Web Worker
@@ -26,13 +29,50 @@ declare class WebSQLiteProxy implements SQLiteAdapter {
26
29
  private _config;
27
30
  private inTransaction;
28
31
  private operationStats;
32
+ /** 'single-tab' = the pre-0263 per-tab worker path (or multiTab: false). */
33
+ private role;
34
+ private roleHandle;
35
+ private router;
36
+ private unsubscribeLeaderServe;
37
+ private unsubscribeRouterEvents;
38
+ /** The Comlink port into the LEADER's SQLite worker (follower role only). */
39
+ private followerPort;
40
+ private readonly guard;
41
+ /** Resolvers waiting for the connection to come back after leader loss. */
42
+ private reconnectWaiters;
43
+ /** Serializes role transitions (promotion vs. reattach can race). */
44
+ private transition;
29
45
  private createWorkerProxy;
30
46
  open(config: SQLiteConfig): Promise<void>;
47
+ private openSingleTab;
48
+ private openMultiTab;
49
+ private becomeLeaderOnRouter;
50
+ private attachToLeader;
51
+ /** This tab won the leadership lock after the previous leader went away. */
52
+ private promoteToLeader;
53
+ /** Another tab became leader; drop the dead port and fetch a fresh one. */
54
+ private reattachFollower;
55
+ private enqueueTransition;
56
+ private notifyReconnected;
57
+ private whenReconnected;
58
+ private teardownMultiTab;
59
+ /**
60
+ * Run a worker RPC with follower protections: leader loss REJECTS in-flight
61
+ * calls immediately instead of hanging, and `retryable` (idempotent read)
62
+ * calls transparently re-issue once the connection is re-established —
63
+ * against this tab's own worker if it was promoted, or the new leader's.
64
+ */
65
+ private rpc;
31
66
  resetStorage(config: SQLiteConfig): Promise<void>;
32
67
  close(): Promise<void>;
33
68
  isOpen(): boolean;
34
69
  query<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T[]>;
35
70
  queryOne<T extends SQLRow = SQLRow>(sql: string, params?: SQLValue[]): Promise<T | null>;
71
+ /**
72
+ * Execute several reads in ONE worker round-trip. N queries previously cost
73
+ * N postMessage round-trips; a batch costs one (exploration 0263).
74
+ */
75
+ queryBatch(reads: SQLBatchRead[]): Promise<SQLRow[][]>;
36
76
  run(sql: string, params?: SQLValue[]): Promise<RunResult>;
37
77
  exec(sql: string): Promise<void>;
38
78
  transaction<T>(_fn: () => Promise<T>): Promise<T>;
@@ -62,8 +102,11 @@ declare class WebSQLiteProxy implements SQLiteAdapter {
62
102
  applySchema(version: number, sql: string): Promise<boolean>;
63
103
  getDatabaseSize(): Promise<number>;
64
104
  vacuum(): Promise<void>;
105
+ incrementalVacuum(maxPages?: number): Promise<number>;
65
106
  checkpoint(): Promise<number>;
66
107
  getStorageMode(): Promise<'opfs' | 'memory'>;
108
+ /** This tab's multi-tab role: 'leader', 'follower', or 'single-tab'. */
109
+ getTabRole(): TabRole | 'single-tab';
67
110
  /**
68
111
  * Create a MessagePort connected directly to the SQLite worker.
69
112
  *
@@ -72,6 +115,15 @@ declare class WebSQLiteProxy implements SQLiteAdapter {
72
115
  * (e.g. the data worker) so its storage calls skip the main thread.
73
116
  */
74
117
  createMessagePort(): Promise<MessagePort>;
118
+ /**
119
+ * Worker-side scheduler stats: per-lane p50/p95 queue+exec latency and
120
+ * coalesce hits (exploration 0263). Complements getOperationStats(), which
121
+ * counts main-thread RPCs — together they give "how many round-trips" and
122
+ * "how long each op spent in the worker".
123
+ */
124
+ getSchedulerOpStats(): Promise<SchedulerOpStats>;
125
+ /** Zero the worker-side scheduler stats for a focused measurement. */
126
+ resetSchedulerOpStats(): Promise<void>;
75
127
  getOperationStats(): SQLiteOperationStats;
76
128
  resetOperationStats(): void;
77
129
  }