local-lite 0.0.5 → 0.0.7

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.
@@ -16,9 +16,13 @@ export interface BetterSqliteDatabase {
16
16
  * Adapter for better-sqlite3 in Node.js, Electron, Next.js, and server environments.
17
17
  */
18
18
  export declare class BetterSQLite3Adapter implements LocalDBAdapter {
19
+ readonly supportsSqlPushdown = true;
19
20
  private db;
20
- private queue;
21
+ private txQueue;
22
+ private statementCache;
23
+ private readonly maxCacheSize;
21
24
  constructor(betterSqliteInstance: any);
25
+ private getStatement;
22
26
  execute(query: string, params?: any[]): Promise<any[]>;
23
27
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
24
28
  close(): void;
@@ -5,35 +5,56 @@ exports.BetterSQLite3Adapter = void 0;
5
5
  * Adapter for better-sqlite3 in Node.js, Electron, Next.js, and server environments.
6
6
  */
7
7
  class BetterSQLite3Adapter {
8
+ supportsSqlPushdown = true;
8
9
  db;
9
- queue = Promise.resolve();
10
+ txQueue = Promise.resolve();
11
+ statementCache = new Map();
12
+ maxCacheSize = 100;
10
13
  constructor(betterSqliteInstance) {
11
14
  this.db = betterSqliteInstance;
12
15
  }
16
+ getStatement(sql) {
17
+ let stmt = this.statementCache.get(sql);
18
+ if (!stmt) {
19
+ if (this.statementCache.size >= this.maxCacheSize) {
20
+ const oldestKey = this.statementCache.keys().next().value;
21
+ if (oldestKey)
22
+ this.statementCache.delete(oldestKey);
23
+ }
24
+ stmt = this.db.prepare(sql);
25
+ this.statementCache.set(sql, stmt);
26
+ }
27
+ return stmt;
28
+ }
13
29
  async execute(query, params = []) {
30
+ const trimmed = query.trim();
31
+ const isReader = /^SELECT|^PRAGMA/i.test(trimmed);
32
+ // Fast-path: Readers execute directly and synchronously without artificial promise queue delay
33
+ if (isReader) {
34
+ const stmt = this.getStatement(trimmed);
35
+ return stmt.all(...params);
36
+ }
14
37
  const run = async () => {
15
- const trimmed = query.trim();
16
- const stmt = this.db.prepare(trimmed);
17
- // If query returns rows (SELECT, PRAGMA) or is marked as reader
18
- if (stmt.reader || /^SELECT|^PRAGMA/i.test(trimmed)) {
38
+ const stmt = this.getStatement(trimmed);
39
+ if (stmt.reader) {
19
40
  return stmt.all(...params);
20
41
  }
21
42
  const info = stmt.run(...params);
22
43
  return [info];
23
44
  };
24
- const next = this.queue.then(run, run);
25
- this.queue = next.catch(() => { });
45
+ const next = this.txQueue.then(run, run);
46
+ this.txQueue = next.catch(() => { });
26
47
  return await next;
27
48
  }
28
49
  async transaction(action) {
29
50
  const run = async () => {
30
- // Explicit transaction control compatible with async actions
31
- this.db.prepare('BEGIN IMMEDIATE').run();
51
+ // Explicit transaction control reusing prepared statements
52
+ this.getStatement('BEGIN IMMEDIATE').run();
32
53
  try {
33
54
  const txWrapper = {
34
55
  execute: async (q, p = []) => {
35
56
  const trimmed = q.trim();
36
- const stmt = this.db.prepare(trimmed);
57
+ const stmt = this.getStatement(trimmed);
37
58
  if (stmt.reader || /^SELECT|^PRAGMA/i.test(trimmed)) {
38
59
  return stmt.all(...p);
39
60
  }
@@ -41,12 +62,12 @@ class BetterSQLite3Adapter {
41
62
  },
42
63
  };
43
64
  const result = await action(txWrapper);
44
- this.db.prepare('COMMIT').run();
65
+ this.getStatement('COMMIT').run();
45
66
  return result;
46
67
  }
47
68
  catch (err) {
48
69
  try {
49
- this.db.prepare('ROLLBACK').run();
70
+ this.getStatement('ROLLBACK').run();
50
71
  }
51
72
  catch {
52
73
  // Ignore secondary rollback error if connection closed
@@ -54,8 +75,8 @@ class BetterSQLite3Adapter {
54
75
  throw err;
55
76
  }
56
77
  };
57
- const next = this.queue.then(run, run);
58
- this.queue = next.catch(() => { });
78
+ const next = this.txQueue.then(run, run);
79
+ this.txQueue = next.catch(() => { });
59
80
  return await next;
60
81
  }
61
82
  close() {
@@ -1,12 +1,21 @@
1
1
  import { LocalDBAdapter, LocalTransaction } from './index.js';
2
+ export interface ExpoDatabase {
3
+ getAllAsync(query: string, params: any[]): Promise<any[]>;
4
+ runAsync?(query: string, params: any[]): Promise<any>;
5
+ execAsync?(query: string): Promise<void>;
6
+ withTransactionAsync(action: () => Promise<void>): Promise<void>;
7
+ closeAsync?(): Promise<void>;
8
+ closeSync?(): void;
9
+ }
2
10
  export declare class ExpoSQLiteAdapter implements LocalDBAdapter {
11
+ readonly supportsSqlPushdown = true;
3
12
  private db;
4
- private queue;
13
+ private writeQueue;
5
14
  constructor(expoDbInstance: any);
6
15
  execute(query: string, params?: any[]): Promise<any[]>;
7
16
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
8
17
  /**
9
- * Waits for pending queries/transactions to finish and closes the underlying SQLite database connection.
18
+ * Waits for pending writes/transactions to finish and closes the underlying SQLite connection.
10
19
  */
11
20
  close(): Promise<void>;
12
21
  }
@@ -1,26 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExpoSQLiteAdapter = void 0;
4
+ // Pre-compiled RegExp hoisted to module scope — 0 regex allocations per query
5
+ const READ_QUERY_REGEX = /^(?:SELECT|PRAGMA)\b/i;
4
6
  class ExpoSQLiteAdapter {
7
+ supportsSqlPushdown = true;
5
8
  db;
6
- queue = Promise.resolve();
9
+ writeQueue = Promise.resolve();
7
10
  constructor(expoDbInstance) {
8
11
  this.db = expoDbInstance;
9
12
  }
10
13
  async execute(query, params = []) {
14
+ const trimmed = query.trim();
15
+ const isRead = READ_QUERY_REGEX.test(trimmed);
16
+ // Fast-path: reads bypass the write queue entirely for zero-latency reactive queries
17
+ if (isRead) {
18
+ return this.db.getAllAsync(trimmed, params);
19
+ }
11
20
  const run = async () => {
12
- const trimmed = query.trim();
13
- const isSelect = /^SELECT\b/i.test(trimmed) || /^PRAGMA\b/i.test(trimmed);
14
- // Parameterless DDL/write statements can bypass statement preparation
15
- // to avoid NativeStatement SharedObject allocation race conditions on Android
16
- if (!isSelect && (!params || params.length === 0) && typeof this.db.execAsync === 'function') {
17
- await this.db.execAsync(query);
21
+ // Parameterless DDL statements bypass statement preparation
22
+ if ((!params || params.length === 0) && typeof this.db.execAsync === 'function') {
23
+ await this.db.execAsync(trimmed);
18
24
  return [];
19
25
  }
20
- return await this.db.getAllAsync(query, params);
26
+ // Modern Expo SQLite (SDK 51+): runAsync is optimized for writes without dummy row allocation
27
+ if (typeof this.db.runAsync === 'function') {
28
+ const res = await this.db.runAsync(trimmed, params);
29
+ return [res];
30
+ }
31
+ return this.db.getAllAsync(trimmed, params);
21
32
  };
22
- const next = this.queue.then(run, run);
23
- this.queue = next.catch(() => { });
33
+ const next = this.writeQueue.then(run, run);
34
+ this.writeQueue = next.catch(() => { });
24
35
  return await next;
25
36
  }
26
37
  async transaction(action) {
@@ -30,28 +41,34 @@ class ExpoSQLiteAdapter {
30
41
  const txWrapper = {
31
42
  execute: async (q, p = []) => {
32
43
  const trimmed = q.trim();
33
- const isSelect = /^SELECT\b/i.test(trimmed) || /^PRAGMA\b/i.test(trimmed);
34
- if (!isSelect && (!p || p.length === 0) && typeof this.db.execAsync === 'function') {
35
- await this.db.execAsync(q);
44
+ if (READ_QUERY_REGEX.test(trimmed)) {
45
+ return this.db.getAllAsync(trimmed, p);
46
+ }
47
+ if ((!p || p.length === 0) && typeof this.db.execAsync === 'function') {
48
+ await this.db.execAsync(trimmed);
36
49
  return [];
37
50
  }
38
- return await this.db.getAllAsync(q, p);
39
- }
51
+ if (typeof this.db.runAsync === 'function') {
52
+ const res = await this.db.runAsync(trimmed, p);
53
+ return [res];
54
+ }
55
+ return this.db.getAllAsync(trimmed, p);
56
+ },
40
57
  };
41
58
  result = await action(txWrapper);
42
59
  });
43
60
  return result;
44
61
  };
45
- // Serialize transactions and queries to prevent SQLite concurrency crashes
46
- const next = this.queue.then(run, run);
47
- this.queue = next.catch(() => { });
62
+ // Serialize transactions to prevent SQLite concurrency crashes
63
+ const next = this.writeQueue.then(run, run);
64
+ this.writeQueue = next.catch(() => { });
48
65
  return await next;
49
66
  }
50
67
  /**
51
- * Waits for pending queries/transactions to finish and closes the underlying SQLite database connection.
68
+ * Waits for pending writes/transactions to finish and closes the underlying SQLite connection.
52
69
  */
53
70
  async close() {
54
- await this.queue;
71
+ await this.writeQueue;
55
72
  if (typeof this.db.closeAsync === 'function') {
56
73
  await this.db.closeAsync();
57
74
  }
@@ -3,6 +3,8 @@ export interface LocalTransaction {
3
3
  }
4
4
  export interface LocalDBAdapter {
5
5
  execute(query: string, params?: any[]): Promise<any[]>;
6
+ /** True when execute runs real SQL with filtering, ordering, paging, and projection. */
7
+ readonly supportsSqlPushdown?: boolean;
6
8
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
7
9
  close?(): Promise<void> | void;
8
10
  }
@@ -17,6 +17,7 @@ export interface LibSQLClient {
17
17
  * Supports local SQLite files, embedded replicas, in-memory databases, and server/edge deployments.
18
18
  */
19
19
  export declare class LibSQLAdapter implements LocalDBAdapter {
20
+ readonly supportsSqlPushdown = true;
20
21
  private client;
21
22
  constructor(libsqlClient: any);
22
23
  execute(query: string, params?: any[]): Promise<any[]>;
@@ -28,6 +28,7 @@ function normalizeRows(res) {
28
28
  * Supports local SQLite files, embedded replicas, in-memory databases, and server/edge deployments.
29
29
  */
30
30
  class LibSQLAdapter {
31
+ supportsSqlPushdown = true;
31
32
  client;
32
33
  constructor(libsqlClient) {
33
34
  this.client = libsqlClient;
@@ -17,9 +17,13 @@ export interface NodeSqliteDatabaseSync {
17
17
  * Provides zero-dependency, native SQLite on modern Node.js runtimes.
18
18
  */
19
19
  export declare class NodeSQLiteAdapter implements LocalDBAdapter {
20
+ readonly supportsSqlPushdown = true;
20
21
  private db;
21
- private queue;
22
+ private txQueue;
23
+ private statementCache;
24
+ private readonly maxCacheSize;
22
25
  constructor(nodeSqliteDb: any);
26
+ private getStatement;
23
27
  execute(query: string, params?: any[]): Promise<any[]>;
24
28
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
25
29
  close(): void;
@@ -6,24 +6,42 @@ exports.NodeSQLiteAdapter = void 0;
6
6
  * Provides zero-dependency, native SQLite on modern Node.js runtimes.
7
7
  */
8
8
  class NodeSQLiteAdapter {
9
+ supportsSqlPushdown = true;
9
10
  db;
10
- queue = Promise.resolve();
11
+ txQueue = Promise.resolve();
12
+ statementCache = new Map();
13
+ maxCacheSize = 100;
11
14
  constructor(nodeSqliteDb) {
12
15
  this.db = nodeSqliteDb;
13
16
  }
17
+ getStatement(sql) {
18
+ let stmt = this.statementCache.get(sql);
19
+ if (!stmt) {
20
+ if (this.statementCache.size >= this.maxCacheSize) {
21
+ const oldestKey = this.statementCache.keys().next().value;
22
+ if (oldestKey)
23
+ this.statementCache.delete(oldestKey);
24
+ }
25
+ stmt = this.db.prepare(sql);
26
+ this.statementCache.set(sql, stmt);
27
+ }
28
+ return stmt;
29
+ }
14
30
  async execute(query, params = []) {
31
+ const trimmed = query.trim();
32
+ const isReader = /^SELECT|^PRAGMA/i.test(trimmed);
33
+ // Fast-path: Readers execute directly and synchronously without queuing delay
34
+ if (isReader) {
35
+ const stmt = this.getStatement(trimmed);
36
+ return stmt.all(...params);
37
+ }
15
38
  const run = async () => {
16
- const trimmed = query.trim();
17
- const stmt = this.db.prepare(trimmed);
18
- // Queries that return rows (SELECT, PRAGMA)
19
- if (/^SELECT|^PRAGMA/i.test(trimmed)) {
20
- return stmt.all(...params);
21
- }
39
+ const stmt = this.getStatement(trimmed);
22
40
  const res = stmt.run(...params);
23
41
  return [res];
24
42
  };
25
- const next = this.queue.then(run, run);
26
- this.queue = next.catch(() => { });
43
+ const next = this.txQueue.then(run, run);
44
+ this.txQueue = next.catch(() => { });
27
45
  return await next;
28
46
  }
29
47
  async transaction(action) {
@@ -33,7 +51,7 @@ class NodeSQLiteAdapter {
33
51
  const txWrapper = {
34
52
  execute: async (q, p = []) => {
35
53
  const trimmed = q.trim();
36
- const stmt = this.db.prepare(trimmed);
54
+ const stmt = this.getStatement(trimmed);
37
55
  if (/^SELECT|^PRAGMA/i.test(trimmed)) {
38
56
  return stmt.all(...p);
39
57
  }
@@ -54,8 +72,8 @@ class NodeSQLiteAdapter {
54
72
  throw err;
55
73
  }
56
74
  };
57
- const next = this.queue.then(run, run);
58
- this.queue = next.catch(() => { });
75
+ const next = this.txQueue.then(run, run);
76
+ this.txQueue = next.catch(() => { });
59
77
  return await next;
60
78
  }
61
79
  close() {
@@ -18,6 +18,7 @@ export interface OpSQLiteDatabase {
18
18
  * Enables ultra-fast C++ JSI SQLite in bare React Native.
19
19
  */
20
20
  export declare class OpSQLiteAdapter implements LocalDBAdapter {
21
+ readonly supportsSqlPushdown = true;
21
22
  private db;
22
23
  private txMutex;
23
24
  constructor(opSqliteInstance: any);
@@ -26,6 +26,7 @@ function extractRows(res) {
26
26
  * Enables ultra-fast C++ JSI SQLite in bare React Native.
27
27
  */
28
28
  class OpSQLiteAdapter {
29
+ supportsSqlPushdown = true;
29
30
  db;
30
31
  txMutex = Promise.resolve();
31
32
  constructor(opSqliteInstance) {
@@ -19,6 +19,7 @@ export interface SqlJsDatabase {
19
19
  * Adapter for sql.js (WebAssembly SQLite in Web Browsers, PWAs, and Expo Web).
20
20
  */
21
21
  export declare class SqlJsAdapter implements LocalDBAdapter {
22
+ readonly supportsSqlPushdown = true;
22
23
  private db;
23
24
  constructor(sqlJsDatabase: any);
24
25
  execute(query: string, params?: any[]): Promise<any[]>;
@@ -5,6 +5,7 @@ exports.SqlJsAdapter = void 0;
5
5
  * Adapter for sql.js (WebAssembly SQLite in Web Browsers, PWAs, and Expo Web).
6
6
  */
7
7
  class SqlJsAdapter {
8
+ supportsSqlPushdown = true;
8
9
  db;
9
10
  constructor(sqlJsDatabase) {
10
11
  this.db = sqlJsDatabase;
@@ -179,39 +179,46 @@ class FirestoreRemoteAdapter {
179
179
  const mod = isCompat ? null : await this.getModule();
180
180
  let maxServerTimestamp = sinceTimestamp;
181
181
  const changes = [];
182
- for (const table of this.tables) {
183
- let docs = [];
184
- if (isCompat) {
185
- const snapshot = await this.db
186
- .collection(table)
187
- .where(this.updatedAtColumn, '>', sinceTimestamp)
188
- .get();
189
- docs = snapshot.docs || [];
190
- }
191
- else if (mod && typeof mod.getDocs === 'function') {
192
- const q = mod.query(mod.collection(this.db, table), mod.where(this.updatedAtColumn, '>', sinceTimestamp));
193
- const snapshot = await mod.getDocs(q);
194
- docs = snapshot.docs || [];
195
- }
196
- for (const doc of docs) {
197
- const data = typeof doc.data === 'function' ? doc.data() : doc;
198
- const isSoftDeleted = Boolean(data[this.deletedColumn] === true ||
199
- data[this.deletedColumn] === 1 ||
200
- (data[this.deletedColumn] &&
201
- !['false', '0', 'null', 'undefined'].includes(String(data[this.deletedColumn]).toLowerCase())));
202
- const rowTime = extractTimestamp(data[this.updatedAtColumn]);
203
- if (rowTime > maxServerTimestamp) {
204
- maxServerTimestamp = rowTime;
182
+ const CHUNK_SIZE = 5;
183
+ for (let i = 0; i < this.tables.length; i += CHUNK_SIZE) {
184
+ const chunk = this.tables.slice(i, i + CHUNK_SIZE);
185
+ const results = await Promise.all(chunk.map(async (table) => {
186
+ let docs = [];
187
+ if (isCompat) {
188
+ const snapshot = await this.db
189
+ .collection(table)
190
+ .where(this.updatedAtColumn, '>', sinceTimestamp)
191
+ .get();
192
+ docs = snapshot.docs || [];
193
+ }
194
+ else if (mod && typeof mod.getDocs === 'function') {
195
+ const q = mod.query(mod.collection(this.db, table), mod.where(this.updatedAtColumn, '>', sinceTimestamp));
196
+ const snapshot = await mod.getDocs(q);
197
+ docs = snapshot.docs || [];
198
+ }
199
+ return { table, docs };
200
+ }));
201
+ for (const { table, docs } of results) {
202
+ for (const doc of docs) {
203
+ const data = typeof doc.data === 'function' ? doc.data() : doc;
204
+ const isSoftDeleted = Boolean(data[this.deletedColumn] === true ||
205
+ data[this.deletedColumn] === 1 ||
206
+ (data[this.deletedColumn] &&
207
+ !['false', '0', 'null', 'undefined'].includes(String(data[this.deletedColumn]).toLowerCase())));
208
+ const rowTime = extractTimestamp(data[this.updatedAtColumn]);
209
+ if (rowTime > maxServerTimestamp) {
210
+ maxServerTimestamp = rowTime;
211
+ }
212
+ const rowId = String(data[this.primaryKey] || doc.id || data.id);
213
+ const operation = isSoftDeleted ? 'DELETE' : 'UPDATE';
214
+ changes.push({
215
+ tableName: table,
216
+ operation,
217
+ rowId,
218
+ payload: isSoftDeleted ? null : data,
219
+ timestamp: rowTime,
220
+ });
205
221
  }
206
- const rowId = String(data[this.primaryKey] || doc.id || data.id);
207
- const operation = isSoftDeleted ? 'DELETE' : 'UPDATE';
208
- changes.push({
209
- tableName: table,
210
- operation,
211
- rowId,
212
- payload: isSoftDeleted ? null : data,
213
- timestamp: rowTime,
214
- });
215
222
  }
216
223
  }
217
224
  return {
@@ -110,12 +110,17 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
110
110
  * Reads updated records and checks `deleted: true` to propagate soft deletes.
111
111
  */
112
112
  fetchChanges(sinceTimestamp: number, scope?: Record<string, any>): Promise<DeltaChanges>;
113
+ /**
114
+ * Formats a row payload for remote Supabase storage (adds primaryKey, deleted: false, updated_at).
115
+ */
116
+ private buildRowData;
113
117
  /**
114
118
  * Helper to determine if a record represents a soft delete.
115
119
  */
116
120
  private isSoftDeleted;
117
121
  /**
118
122
  * Subscribes to live realtime postgres_changes from Supabase.
123
+ * Batches rapid burst events in the same microtask turn into a single SQLite sync transaction.
119
124
  */
120
125
  subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): (() => void) | void;
121
126
  /**