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.
- package/dist/adapters/local/better-sqlite3.d.ts +4 -1
- package/dist/adapters/local/better-sqlite3.js +34 -14
- package/dist/adapters/local/expo-sqlite.d.ts +10 -2
- package/dist/adapters/local/expo-sqlite.js +36 -20
- package/dist/adapters/local/node-sqlite.d.ts +4 -1
- package/dist/adapters/local/node-sqlite.js +29 -12
- package/dist/adapters/remote/firestore.js +39 -32
- package/dist/adapters/remote/supabase.d.ts +5 -0
- package/dist/adapters/remote/supabase.js +192 -85
- package/dist/core/client.d.ts +0 -8
- package/dist/core/client.js +70 -86
- package/dist/core/outbox-store.d.ts +5 -0
- package/dist/core/outbox-store.js +29 -4
- package/dist/core/query-builder.d.ts +1 -0
- package/dist/core/query-builder.js +140 -71
- package/dist/core/query.js +28 -32
- package/dist/core/schema-manager.d.ts +4 -15
- package/dist/core/schema-manager.js +45 -135
- package/dist/core/sync-worker.js +7 -2
- package/dist/react/index.js +65 -52
- package/dist/react/status.d.ts +5 -0
- package/dist/react/status.js +20 -12
- package/dist/utils/uuid.d.ts +1 -1
- package/dist/utils/uuid.js +7 -16
- package/package.json +1 -1
|
@@ -17,8 +17,11 @@ export interface BetterSqliteDatabase {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare class BetterSQLite3Adapter implements LocalDBAdapter {
|
|
19
19
|
private db;
|
|
20
|
-
private
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
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.
|
|
25
|
-
this.
|
|
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
|
|
31
|
-
this.
|
|
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.
|
|
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.
|
|
64
|
+
this.getStatement('COMMIT').run();
|
|
45
65
|
return result;
|
|
46
66
|
}
|
|
47
67
|
catch (err) {
|
|
48
68
|
try {
|
|
49
|
-
this.
|
|
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.
|
|
58
|
-
this.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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.
|
|
23
|
-
this.
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
const next = this.
|
|
47
|
-
this.
|
|
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
|
|
67
|
+
* Waits for pending writes/transactions to finish and closes the underlying SQLite connection.
|
|
52
68
|
*/
|
|
53
69
|
async close() {
|
|
54
|
-
await this.
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
26
|
-
this.
|
|
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.
|
|
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.
|
|
58
|
-
this.
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
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
|
/**
|