local-lite 0.0.5 → 0.0.6

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.
@@ -17,8 +17,11 @@ export interface BetterSqliteDatabase {
17
17
  */
18
18
  export declare class BetterSQLite3Adapter implements LocalDBAdapter {
19
19
  private db;
20
- private queue;
20
+ private txQueue;
21
+ private statementCache;
22
+ private readonly maxCacheSize;
21
23
  constructor(betterSqliteInstance: any);
24
+ private getStatement;
22
25
  execute(query: string, params?: any[]): Promise<any[]>;
23
26
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
24
27
  close(): void;
@@ -6,34 +6,54 @@ exports.BetterSQLite3Adapter = void 0;
6
6
  */
7
7
  class BetterSQLite3Adapter {
8
8
  db;
9
- queue = Promise.resolve();
9
+ txQueue = Promise.resolve();
10
+ statementCache = new Map();
11
+ maxCacheSize = 100;
10
12
  constructor(betterSqliteInstance) {
11
13
  this.db = betterSqliteInstance;
12
14
  }
15
+ getStatement(sql) {
16
+ let stmt = this.statementCache.get(sql);
17
+ if (!stmt) {
18
+ if (this.statementCache.size >= this.maxCacheSize) {
19
+ const oldestKey = this.statementCache.keys().next().value;
20
+ if (oldestKey)
21
+ this.statementCache.delete(oldestKey);
22
+ }
23
+ stmt = this.db.prepare(sql);
24
+ this.statementCache.set(sql, stmt);
25
+ }
26
+ return stmt;
27
+ }
13
28
  async execute(query, params = []) {
29
+ const trimmed = query.trim();
30
+ const isReader = /^SELECT|^PRAGMA/i.test(trimmed);
31
+ // Fast-path: Readers execute directly and synchronously without artificial promise queue delay
32
+ if (isReader) {
33
+ const stmt = this.getStatement(trimmed);
34
+ return stmt.all(...params);
35
+ }
14
36
  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)) {
37
+ const stmt = this.getStatement(trimmed);
38
+ if (stmt.reader) {
19
39
  return stmt.all(...params);
20
40
  }
21
41
  const info = stmt.run(...params);
22
42
  return [info];
23
43
  };
24
- const next = this.queue.then(run, run);
25
- this.queue = next.catch(() => { });
44
+ const next = this.txQueue.then(run, run);
45
+ this.txQueue = next.catch(() => { });
26
46
  return await next;
27
47
  }
28
48
  async transaction(action) {
29
49
  const run = async () => {
30
- // Explicit transaction control compatible with async actions
31
- this.db.prepare('BEGIN IMMEDIATE').run();
50
+ // Explicit transaction control reusing prepared statements
51
+ this.getStatement('BEGIN IMMEDIATE').run();
32
52
  try {
33
53
  const txWrapper = {
34
54
  execute: async (q, p = []) => {
35
55
  const trimmed = q.trim();
36
- const stmt = this.db.prepare(trimmed);
56
+ const stmt = this.getStatement(trimmed);
37
57
  if (stmt.reader || /^SELECT|^PRAGMA/i.test(trimmed)) {
38
58
  return stmt.all(...p);
39
59
  }
@@ -41,12 +61,12 @@ class BetterSQLite3Adapter {
41
61
  },
42
62
  };
43
63
  const result = await action(txWrapper);
44
- this.db.prepare('COMMIT').run();
64
+ this.getStatement('COMMIT').run();
45
65
  return result;
46
66
  }
47
67
  catch (err) {
48
68
  try {
49
- this.db.prepare('ROLLBACK').run();
69
+ this.getStatement('ROLLBACK').run();
50
70
  }
51
71
  catch {
52
72
  // Ignore secondary rollback error if connection closed
@@ -54,8 +74,8 @@ class BetterSQLite3Adapter {
54
74
  throw err;
55
75
  }
56
76
  };
57
- const next = this.queue.then(run, run);
58
- this.queue = next.catch(() => { });
77
+ const next = this.txQueue.then(run, run);
78
+ this.txQueue = next.catch(() => { });
59
79
  return await next;
60
80
  }
61
81
  close() {
@@ -1,12 +1,20 @@
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 {
3
11
  private db;
4
- private queue;
12
+ private writeQueue;
5
13
  constructor(expoDbInstance: any);
6
14
  execute(query: string, params?: any[]): Promise<any[]>;
7
15
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
8
16
  /**
9
- * Waits for pending queries/transactions to finish and closes the underlying SQLite database connection.
17
+ * Waits for pending writes/transactions to finish and closes the underlying SQLite connection.
10
18
  */
11
19
  close(): Promise<void>;
12
20
  }
@@ -1,26 +1,36 @@
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 {
5
7
  db;
6
- queue = Promise.resolve();
8
+ writeQueue = Promise.resolve();
7
9
  constructor(expoDbInstance) {
8
10
  this.db = expoDbInstance;
9
11
  }
10
12
  async execute(query, params = []) {
13
+ const trimmed = query.trim();
14
+ const isRead = READ_QUERY_REGEX.test(trimmed);
15
+ // Fast-path: reads bypass the write queue entirely for zero-latency reactive queries
16
+ if (isRead) {
17
+ return this.db.getAllAsync(trimmed, params);
18
+ }
11
19
  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);
20
+ // Parameterless DDL statements bypass statement preparation
21
+ if ((!params || params.length === 0) && typeof this.db.execAsync === 'function') {
22
+ await this.db.execAsync(trimmed);
18
23
  return [];
19
24
  }
20
- return await this.db.getAllAsync(query, params);
25
+ // Modern Expo SQLite (SDK 51+): runAsync is optimized for writes without dummy row allocation
26
+ if (typeof this.db.runAsync === 'function') {
27
+ const res = await this.db.runAsync(trimmed, params);
28
+ return [res];
29
+ }
30
+ return this.db.getAllAsync(trimmed, params);
21
31
  };
22
- const next = this.queue.then(run, run);
23
- this.queue = next.catch(() => { });
32
+ const next = this.writeQueue.then(run, run);
33
+ this.writeQueue = next.catch(() => { });
24
34
  return await next;
25
35
  }
26
36
  async transaction(action) {
@@ -30,28 +40,34 @@ class ExpoSQLiteAdapter {
30
40
  const txWrapper = {
31
41
  execute: async (q, p = []) => {
32
42
  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);
43
+ if (READ_QUERY_REGEX.test(trimmed)) {
44
+ return this.db.getAllAsync(trimmed, p);
45
+ }
46
+ if ((!p || p.length === 0) && typeof this.db.execAsync === 'function') {
47
+ await this.db.execAsync(trimmed);
36
48
  return [];
37
49
  }
38
- return await this.db.getAllAsync(q, p);
39
- }
50
+ if (typeof this.db.runAsync === 'function') {
51
+ const res = await this.db.runAsync(trimmed, p);
52
+ return [res];
53
+ }
54
+ return this.db.getAllAsync(trimmed, p);
55
+ },
40
56
  };
41
57
  result = await action(txWrapper);
42
58
  });
43
59
  return result;
44
60
  };
45
- // Serialize transactions and queries to prevent SQLite concurrency crashes
46
- const next = this.queue.then(run, run);
47
- this.queue = next.catch(() => { });
61
+ // Serialize transactions to prevent SQLite concurrency crashes
62
+ const next = this.writeQueue.then(run, run);
63
+ this.writeQueue = next.catch(() => { });
48
64
  return await next;
49
65
  }
50
66
  /**
51
- * Waits for pending queries/transactions to finish and closes the underlying SQLite database connection.
67
+ * Waits for pending writes/transactions to finish and closes the underlying SQLite connection.
52
68
  */
53
69
  async close() {
54
- await this.queue;
70
+ await this.writeQueue;
55
71
  if (typeof this.db.closeAsync === 'function') {
56
72
  await this.db.closeAsync();
57
73
  }
@@ -18,8 +18,11 @@ export interface NodeSqliteDatabaseSync {
18
18
  */
19
19
  export declare class NodeSQLiteAdapter implements LocalDBAdapter {
20
20
  private db;
21
- private queue;
21
+ private txQueue;
22
+ private statementCache;
23
+ private readonly maxCacheSize;
22
24
  constructor(nodeSqliteDb: any);
25
+ private getStatement;
23
26
  execute(query: string, params?: any[]): Promise<any[]>;
24
27
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
25
28
  close(): void;
@@ -7,23 +7,40 @@ exports.NodeSQLiteAdapter = void 0;
7
7
  */
8
8
  class NodeSQLiteAdapter {
9
9
  db;
10
- queue = Promise.resolve();
10
+ txQueue = Promise.resolve();
11
+ statementCache = new Map();
12
+ maxCacheSize = 100;
11
13
  constructor(nodeSqliteDb) {
12
14
  this.db = nodeSqliteDb;
13
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
+ }
14
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 queuing delay
33
+ if (isReader) {
34
+ const stmt = this.getStatement(trimmed);
35
+ return stmt.all(...params);
36
+ }
15
37
  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
- }
38
+ const stmt = this.getStatement(trimmed);
22
39
  const res = stmt.run(...params);
23
40
  return [res];
24
41
  };
25
- const next = this.queue.then(run, run);
26
- this.queue = next.catch(() => { });
42
+ const next = this.txQueue.then(run, run);
43
+ this.txQueue = next.catch(() => { });
27
44
  return await next;
28
45
  }
29
46
  async transaction(action) {
@@ -33,7 +50,7 @@ class NodeSQLiteAdapter {
33
50
  const txWrapper = {
34
51
  execute: async (q, p = []) => {
35
52
  const trimmed = q.trim();
36
- const stmt = this.db.prepare(trimmed);
53
+ const stmt = this.getStatement(trimmed);
37
54
  if (/^SELECT|^PRAGMA/i.test(trimmed)) {
38
55
  return stmt.all(...p);
39
56
  }
@@ -54,8 +71,8 @@ class NodeSQLiteAdapter {
54
71
  throw err;
55
72
  }
56
73
  };
57
- const next = this.queue.then(run, run);
58
- this.queue = next.catch(() => { });
74
+ const next = this.txQueue.then(run, run);
75
+ this.txQueue = next.catch(() => { });
59
76
  return await next;
60
77
  }
61
78
  close() {
@@ -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
  /**