local-lite 0.0.4 → 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 +7 -2
- package/dist/adapters/remote/supabase.js +192 -85
- package/dist/config/types.d.ts +1 -1
- package/dist/core/client.d.ts +52 -15
- package/dist/core/client.js +195 -208
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/outbox-store.d.ts +5 -0
- package/dist/core/outbox-store.js +29 -4
- package/dist/core/query-builder.d.ts +120 -0
- package/dist/core/query-builder.js +441 -0
- package/dist/core/query.js +28 -32
- package/dist/core/schema-manager.d.ts +4 -15
- package/dist/core/schema-manager.js +48 -103
- package/dist/core/sync-worker.js +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/SyncInspector.js +0 -70
- package/dist/react/index.d.ts +39 -0
- package/dist/react/index.js +232 -34
- package/dist/react/status.d.ts +5 -0
- package/dist/react/status.js +20 -12
- package/dist/schema/generator.d.ts +49 -0
- package/dist/schema/generator.js +209 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +17 -0
- package/dist/utils/uuid.d.ts +1 -1
- package/dist/utils/uuid.js +7 -16
- package/package.json +7 -2
|
@@ -3,15 +3,6 @@ export interface DBExecutable {
|
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
5
|
* Detects the appropriate SQLite column type affinity from a JavaScript runtime value.
|
|
6
|
-
*
|
|
7
|
-
* Mapping:
|
|
8
|
-
* - boolean -> INTEGER (0 or 1)
|
|
9
|
-
* - integer -> INTEGER
|
|
10
|
-
* - float/double -> REAL
|
|
11
|
-
* - string -> TEXT
|
|
12
|
-
* - object/array -> TEXT (JSON formatted)
|
|
13
|
-
* - Uint8Array -> BLOB
|
|
14
|
-
* - null/undefined -> TEXT (flexible SQLite type affinity)
|
|
15
6
|
*/
|
|
16
7
|
export declare function detectSQLiteType(value: any): 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB';
|
|
17
8
|
/**
|
|
@@ -24,18 +15,16 @@ export declare class SchemaManager {
|
|
|
24
15
|
private knownTables;
|
|
25
16
|
private knownColumns;
|
|
26
17
|
private knownIndexes;
|
|
27
|
-
/**
|
|
28
|
-
* Resets the schema cache (useful for testing or full database resets).
|
|
29
|
-
*/
|
|
30
18
|
clearCache(): void;
|
|
31
|
-
/**
|
|
32
|
-
* Checks whether a column is known to exist on the specified table.
|
|
33
|
-
*/
|
|
34
19
|
hasColumn(tableName: string, column: string): boolean;
|
|
35
20
|
/**
|
|
36
21
|
* Introspects table columns via PRAGMA table_info if not previously cached.
|
|
37
22
|
*/
|
|
38
23
|
ensureTableIntrospected(db: DBExecutable, tableName: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Automatically creates single-column and composite FK+deleted indexes for given columns.
|
|
26
|
+
*/
|
|
27
|
+
private autoIndexColumns;
|
|
39
28
|
/**
|
|
40
29
|
* Ensures that the target table exists in SQLite with the specified primary key.
|
|
41
30
|
*/
|
|
@@ -6,36 +6,14 @@ exports.formatValueForStorage = formatValueForStorage;
|
|
|
6
6
|
const sql_js_1 = require("../utils/sql.js");
|
|
7
7
|
/**
|
|
8
8
|
* Detects the appropriate SQLite column type affinity from a JavaScript runtime value.
|
|
9
|
-
*
|
|
10
|
-
* Mapping:
|
|
11
|
-
* - boolean -> INTEGER (0 or 1)
|
|
12
|
-
* - integer -> INTEGER
|
|
13
|
-
* - float/double -> REAL
|
|
14
|
-
* - string -> TEXT
|
|
15
|
-
* - object/array -> TEXT (JSON formatted)
|
|
16
|
-
* - Uint8Array -> BLOB
|
|
17
|
-
* - null/undefined -> TEXT (flexible SQLite type affinity)
|
|
18
9
|
*/
|
|
19
10
|
function detectSQLiteType(value) {
|
|
20
|
-
if (
|
|
21
|
-
return 'TEXT';
|
|
22
|
-
}
|
|
23
|
-
const type = typeof value;
|
|
24
|
-
if (type === 'boolean') {
|
|
11
|
+
if (typeof value === 'boolean')
|
|
25
12
|
return 'INTEGER';
|
|
26
|
-
|
|
27
|
-
if (type === 'number') {
|
|
13
|
+
if (typeof value === 'number')
|
|
28
14
|
return Number.isInteger(value) ? 'INTEGER' : 'REAL';
|
|
29
|
-
|
|
30
|
-
if (type === 'string') {
|
|
31
|
-
return 'TEXT';
|
|
32
|
-
}
|
|
33
|
-
if (value instanceof Uint8Array) {
|
|
15
|
+
if (value instanceof Uint8Array)
|
|
34
16
|
return 'BLOB';
|
|
35
|
-
}
|
|
36
|
-
if (type === 'object') {
|
|
37
|
-
return 'TEXT';
|
|
38
|
-
}
|
|
39
17
|
return 'TEXT';
|
|
40
18
|
}
|
|
41
19
|
/**
|
|
@@ -44,32 +22,23 @@ function detectSQLiteType(value) {
|
|
|
44
22
|
* - Plain objects and arrays are JSON serialized
|
|
45
23
|
*/
|
|
46
24
|
function formatValueForStorage(value) {
|
|
47
|
-
if (value === null || value === undefined)
|
|
25
|
+
if (value === null || value === undefined)
|
|
48
26
|
return null;
|
|
49
|
-
|
|
50
|
-
if (typeof value === 'boolean') {
|
|
27
|
+
if (typeof value === 'boolean')
|
|
51
28
|
return value ? 1 : 0;
|
|
52
|
-
|
|
53
|
-
if (typeof value === 'object' && !(value instanceof Uint8Array)) {
|
|
29
|
+
if (typeof value === 'object' && !(value instanceof Uint8Array))
|
|
54
30
|
return JSON.stringify(value);
|
|
55
|
-
}
|
|
56
31
|
return value;
|
|
57
32
|
}
|
|
58
33
|
class SchemaManager {
|
|
59
34
|
knownTables = new Set();
|
|
60
35
|
knownColumns = new Set();
|
|
61
36
|
knownIndexes = new Set();
|
|
62
|
-
/**
|
|
63
|
-
* Resets the schema cache (useful for testing or full database resets).
|
|
64
|
-
*/
|
|
65
37
|
clearCache() {
|
|
66
38
|
this.knownTables.clear();
|
|
67
39
|
this.knownColumns.clear();
|
|
68
40
|
this.knownIndexes.clear();
|
|
69
41
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Checks whether a column is known to exist on the specified table.
|
|
72
|
-
*/
|
|
73
42
|
hasColumn(tableName, column) {
|
|
74
43
|
return this.knownColumns.has(`${tableName}:${column.toLowerCase()}`);
|
|
75
44
|
}
|
|
@@ -84,25 +53,54 @@ class SchemaManager {
|
|
|
84
53
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
85
54
|
try {
|
|
86
55
|
const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
|
|
56
|
+
const cols = [];
|
|
87
57
|
if (Array.isArray(tableInfo)) {
|
|
88
58
|
for (const col of tableInfo) {
|
|
89
|
-
if (col
|
|
59
|
+
if (col?.name) {
|
|
90
60
|
this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
|
|
61
|
+
cols.push(String(col.name));
|
|
91
62
|
}
|
|
92
63
|
}
|
|
93
64
|
}
|
|
65
|
+
await this.autoIndexColumns(db, tableName, safeTable, cols);
|
|
94
66
|
}
|
|
95
|
-
catch {
|
|
96
|
-
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Automatically creates single-column and composite FK+deleted indexes for given columns.
|
|
71
|
+
*/
|
|
72
|
+
async autoIndexColumns(db, tableName, safeTable, columns) {
|
|
73
|
+
const hasDeleted = this.knownColumns.has(`${tableName}:deleted`);
|
|
74
|
+
for (const key of columns) {
|
|
75
|
+
const lower = key.toLowerCase();
|
|
76
|
+
if (lower.endsWith('_id') || lower === 'deleted' || lower === 'created_at' || lower === 'updated_at') {
|
|
77
|
+
const indexKey = `${tableName}:${lower}`;
|
|
78
|
+
if (!this.knownIndexes.has(indexKey)) {
|
|
79
|
+
this.knownIndexes.add(indexKey);
|
|
80
|
+
try {
|
|
81
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS ${(0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`)} ON ${safeTable} (${(0, sql_js_1.escapeIdentifier)(key)});`);
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (hasDeleted && lower.endsWith('_id') && lower !== 'id') {
|
|
87
|
+
const compositeKey = `${tableName}:${lower}_deleted`;
|
|
88
|
+
if (!this.knownIndexes.has(compositeKey)) {
|
|
89
|
+
this.knownIndexes.add(compositeKey);
|
|
90
|
+
try {
|
|
91
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS ${(0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${lower}_del`)} ON ${safeTable} (${(0, sql_js_1.escapeIdentifier)(key)}, "deleted");`);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
/**
|
|
100
99
|
* Ensures that the target table exists in SQLite with the specified primary key.
|
|
101
100
|
*/
|
|
102
101
|
async ensureTable(db, tableName, primaryKey = 'id') {
|
|
103
|
-
if (this.knownTables.has(tableName))
|
|
102
|
+
if (this.knownTables.has(tableName))
|
|
104
103
|
return;
|
|
105
|
-
}
|
|
106
104
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
107
105
|
const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
|
|
108
106
|
await db.execute(`CREATE TABLE IF NOT EXISTS ${safeTable} (${safePk} TEXT PRIMARY KEY);`);
|
|
@@ -115,99 +113,46 @@ class SchemaManager {
|
|
|
115
113
|
*/
|
|
116
114
|
async ensureColumns(db, tableName, payload, primaryKey = 'id') {
|
|
117
115
|
await this.ensureTable(db, tableName, primaryKey);
|
|
118
|
-
if (!payload || typeof payload !== 'object')
|
|
116
|
+
if (!payload || typeof payload !== 'object')
|
|
119
117
|
return {};
|
|
120
|
-
}
|
|
121
118
|
const keys = Object.keys(payload);
|
|
122
|
-
if (keys.length === 0)
|
|
119
|
+
if (keys.length === 0)
|
|
123
120
|
return {};
|
|
124
|
-
}
|
|
125
121
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
126
122
|
let cacheMiss = false;
|
|
127
123
|
for (const key of keys) {
|
|
128
|
-
if (key
|
|
129
|
-
continue;
|
|
130
|
-
const cacheKey = `${tableName}:${key.toLowerCase()}`;
|
|
131
|
-
if (!this.knownColumns.has(cacheKey)) {
|
|
124
|
+
if (key !== primaryKey && !this.knownColumns.has(`${tableName}:${key.toLowerCase()}`)) {
|
|
132
125
|
cacheMiss = true;
|
|
133
126
|
break;
|
|
134
127
|
}
|
|
135
128
|
}
|
|
136
129
|
if (cacheMiss) {
|
|
137
|
-
// Query PRAGMA table_info to discover existing columns from SQLite
|
|
138
130
|
try {
|
|
139
131
|
const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
|
|
140
132
|
if (Array.isArray(tableInfo)) {
|
|
141
133
|
for (const col of tableInfo) {
|
|
142
|
-
if (col
|
|
134
|
+
if (col?.name) {
|
|
143
135
|
this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
|
|
144
136
|
}
|
|
145
137
|
}
|
|
146
138
|
}
|
|
147
139
|
}
|
|
148
|
-
catch
|
|
149
|
-
// Fallback for mocks or custom drivers where PRAGMA table_info might not be supported
|
|
150
|
-
}
|
|
151
|
-
// Check for columns that still do not exist, and alter table
|
|
140
|
+
catch { }
|
|
152
141
|
for (const key of keys) {
|
|
153
142
|
if (key === primaryKey)
|
|
154
143
|
continue;
|
|
155
144
|
const cacheKey = `${tableName}:${key.toLowerCase()}`;
|
|
156
145
|
if (!this.knownColumns.has(cacheKey)) {
|
|
157
146
|
const detectedType = detectSQLiteType(payload[key]);
|
|
158
|
-
const safeCol = (0, sql_js_1.escapeIdentifier)(key);
|
|
159
147
|
try {
|
|
160
|
-
await db.execute(`ALTER TABLE ${safeTable} ADD COLUMN ${
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
// Ignore if column already exists (e.g. concurrent race)
|
|
164
|
-
const msg = String(err?.message || err || '');
|
|
165
|
-
if (!msg.toLowerCase().includes('duplicate column')) {
|
|
166
|
-
// Ignore or rethrow
|
|
167
|
-
}
|
|
148
|
+
await db.execute(`ALTER TABLE ${safeTable} ADD COLUMN ${(0, sql_js_1.escapeIdentifier)(key)} ${detectedType};`);
|
|
168
149
|
}
|
|
150
|
+
catch { }
|
|
169
151
|
this.knownColumns.add(cacheKey);
|
|
170
152
|
}
|
|
171
|
-
// Auto-index foreign keys and common query columns
|
|
172
|
-
const lowerKey = key.toLowerCase();
|
|
173
|
-
if (lowerKey.endsWith('_id') ||
|
|
174
|
-
lowerKey === 'business_id' ||
|
|
175
|
-
lowerKey === 'deleted' ||
|
|
176
|
-
lowerKey === 'created_at' ||
|
|
177
|
-
lowerKey === 'payment_date') {
|
|
178
|
-
const indexKey = `${tableName}:${lowerKey}`;
|
|
179
|
-
if (!this.knownIndexes.has(indexKey)) {
|
|
180
|
-
this.knownIndexes.add(indexKey);
|
|
181
|
-
try {
|
|
182
|
-
const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`);
|
|
183
|
-
const safeCol = (0, sql_js_1.escapeIdentifier)(key);
|
|
184
|
-
await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeCol});`);
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
// Ignore if indexes are not supported by adapter or already exist
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
// Auto-create high performance composite index for multi-tenant soft-deleted queries
|
|
194
|
-
if (this.knownColumns.has(`${tableName}:business_id`) &&
|
|
195
|
-
this.knownColumns.has(`${tableName}:deleted`)) {
|
|
196
|
-
const compositeKey = `${tableName}:business_id_deleted`;
|
|
197
|
-
if (!this.knownIndexes.has(compositeKey)) {
|
|
198
|
-
this.knownIndexes.add(compositeKey);
|
|
199
|
-
try {
|
|
200
|
-
const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_biz_del`);
|
|
201
|
-
const safeBiz = (0, sql_js_1.escapeIdentifier)('business_id');
|
|
202
|
-
const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
|
|
203
|
-
await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeBiz}, ${safeDel});`);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// Ignore
|
|
207
|
-
}
|
|
208
153
|
}
|
|
154
|
+
await this.autoIndexColumns(db, tableName, safeTable, keys);
|
|
209
155
|
}
|
|
210
|
-
// Format all values for SQLite storage
|
|
211
156
|
const formatted = {};
|
|
212
157
|
for (const key of keys) {
|
|
213
158
|
formatted[key] = formatValueForStorage(payload[key]);
|
package/dist/core/sync-worker.js
CHANGED
|
@@ -611,9 +611,12 @@ class SyncWorker {
|
|
|
611
611
|
const isLastChunk = i + DELTA_CHUNK_SIZE >= totalChanges;
|
|
612
612
|
const queuedEvents = [];
|
|
613
613
|
await this.db.transaction(async (tx) => {
|
|
614
|
+
// Pre-fetch active pending outbox records in 1 query for this chunk to eliminate N+1 bridge roundtrips
|
|
615
|
+
const pendingOutboxMap = await outbox_store_js_1.OutboxStore.getActivePendingMap(tx);
|
|
614
616
|
for (const change of chunk) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
+
const outboxKey = `${change.tableName}:${change.rowId}`;
|
|
618
|
+
// 1. Check in-memory map for pending local mutations for this (table_name, row_id)
|
|
619
|
+
const localPendingRecord = pendingOutboxMap.get(outboxKey);
|
|
617
620
|
// 2. Conflict resolution if there is a pending local change
|
|
618
621
|
if (localPendingRecord) {
|
|
619
622
|
// Only load the current local row when a resolver actually needs it.
|
|
@@ -640,11 +643,13 @@ class SyncWorker {
|
|
|
640
643
|
if (resolution === 'apply-remote') {
|
|
641
644
|
// Remote change wins: remove the superseded local pending outbox mutation
|
|
642
645
|
await outbox_store_js_1.OutboxStore.deleteRecord(tx, localPendingRecord.id);
|
|
646
|
+
pendingOutboxMap.delete(outboxKey);
|
|
643
647
|
}
|
|
644
648
|
else if (typeof resolution === 'object' && resolution.mergePayload) {
|
|
645
649
|
// Merge scenario
|
|
646
650
|
change.payload = resolution.mergePayload;
|
|
647
651
|
await outbox_store_js_1.OutboxStore.updatePayload(tx, localPendingRecord.id, resolution.mergePayload);
|
|
652
|
+
localPendingRecord.payload = resolution.mergePayload;
|
|
648
653
|
}
|
|
649
654
|
}
|
|
650
655
|
// 3. Apply remote change to local database
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './core/outbox-store.js';
|
|
|
7
7
|
export * from './core/meta-store.js';
|
|
8
8
|
export * from './core/table-manager.js';
|
|
9
9
|
export * from './core/schema-manager.js';
|
|
10
|
+
export * from './core/query-builder.js';
|
|
10
11
|
export * from './reactive/event-emitter.js';
|
|
11
12
|
export * from './adapters/local/index.js';
|
|
12
13
|
export * from './adapters/remote/index.js';
|
|
@@ -16,3 +17,4 @@ export type * from './react/index.js';
|
|
|
16
17
|
export * from './utils/uuid.js';
|
|
17
18
|
export * from './utils/sql.js';
|
|
18
19
|
export * from './utils/logger.js';
|
|
20
|
+
export * from './schema/index.js';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ __exportStar(require("./core/outbox-store.js"), exports);
|
|
|
23
23
|
__exportStar(require("./core/meta-store.js"), exports);
|
|
24
24
|
__exportStar(require("./core/table-manager.js"), exports);
|
|
25
25
|
__exportStar(require("./core/schema-manager.js"), exports);
|
|
26
|
+
__exportStar(require("./core/query-builder.js"), exports);
|
|
26
27
|
__exportStar(require("./reactive/event-emitter.js"), exports);
|
|
27
28
|
__exportStar(require("./adapters/local/index.js"), exports);
|
|
28
29
|
__exportStar(require("./adapters/remote/index.js"), exports);
|
|
@@ -31,3 +32,4 @@ __exportStar(require("./config/index.js"), exports);
|
|
|
31
32
|
__exportStar(require("./utils/uuid.js"), exports);
|
|
32
33
|
__exportStar(require("./utils/sql.js"), exports);
|
|
33
34
|
__exportStar(require("./utils/logger.js"), exports);
|
|
35
|
+
__exportStar(require("./schema/index.js"), exports);
|
|
@@ -92,37 +92,6 @@ function SyncInspector({ client, position = 'bottom-right', variant = 'pill', in
|
|
|
92
92
|
numColor: metrics?.outboxFailedCount ? '#EF4444' : '#10B981',
|
|
93
93
|
},
|
|
94
94
|
], [status.pendingCount, metrics?.outboxFailedCount, currentTheme.text]);
|
|
95
|
-
// Shared Connection & Telemetry Rows
|
|
96
|
-
const telemetryItems = (0, react_1.useMemo)(() => [
|
|
97
|
-
{
|
|
98
|
-
id: 'status',
|
|
99
|
-
label: 'Connection Status',
|
|
100
|
-
value: status.isOnline ? 'Online (Connected)' : 'Offline',
|
|
101
|
-
color: statusColor,
|
|
102
|
-
hasDot: true,
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
id: 'sync-state',
|
|
106
|
-
label: 'Sync State',
|
|
107
|
-
value: status.isSyncing ? 'Syncing...' : 'Idle',
|
|
108
|
-
color: status.isSyncing ? '#F59E0B' : currentTheme.textMuted,
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: 'last-synced',
|
|
112
|
-
label: 'Last Synchronized',
|
|
113
|
-
value: status.lastSyncedAt ? new Date(status.lastSyncedAt).toLocaleTimeString() : 'Never',
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
id: 'push-duration',
|
|
117
|
-
label: 'Last Push Duration',
|
|
118
|
-
value: `${metrics?.lastPushDurationMs || 0} ms`,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
id: 'pull-duration',
|
|
122
|
-
label: 'Last Pull Duration',
|
|
123
|
-
value: `${metrics?.lastPullDurationMs || 0} ms`,
|
|
124
|
-
},
|
|
125
|
-
], [status.isOnline, status.isSyncing, status.lastSyncedAt, statusColor, metrics?.lastPushDurationMs, metrics?.lastPullDurationMs, currentTheme.textMuted]);
|
|
126
95
|
// Shared Action Buttons
|
|
127
96
|
const actionButtons = (0, react_1.useMemo)(() => [
|
|
128
97
|
{ id: 'push', label: '🚀 Push Changes', onPress: handlePush, bg: '#3B82F6', color: '#FFFFFF' },
|
|
@@ -185,19 +154,6 @@ function SyncInspector({ client, position = 'bottom-right', variant = 'pill', in
|
|
|
185
154
|
backgroundColor: currentTheme.card,
|
|
186
155
|
borderColor: currentTheme.border,
|
|
187
156
|
},
|
|
188
|
-
], children: [(0, jsx_runtime_1.jsx)(Text, { style: [rnStyles.infoCardTitle, { color: currentTheme.textMuted }], children: "CONNECTION & TELEMETRY" }), telemetryItems.map((item, idx) => ((0, jsx_runtime_1.jsxs)(View, { style: [
|
|
189
|
-
rnStyles.infoRow,
|
|
190
|
-
{ borderBottomColor: currentTheme.border },
|
|
191
|
-
idx === telemetryItems.length - 1 && { borderBottomWidth: 0 },
|
|
192
|
-
], children: [(0, jsx_runtime_1.jsx)(Text, { style: [rnStyles.infoKey, { color: currentTheme.textMuted }], children: item.label }), (0, jsx_runtime_1.jsxs)(View, { style: { flexDirection: 'row', alignItems: 'center', gap: 6 }, children: [item.hasDot && (0, jsx_runtime_1.jsx)(View, { style: [rnStyles.dotSmall, { backgroundColor: item.color }] }), (0, jsx_runtime_1.jsx)(Text, { style: [
|
|
193
|
-
rnStyles.infoVal,
|
|
194
|
-
item.color ? { color: item.color, fontWeight: '700' } : { color: currentTheme.text },
|
|
195
|
-
], children: item.value })] })] }, item.id)))] }), (0, jsx_runtime_1.jsxs)(View, { style: [
|
|
196
|
-
rnStyles.infoCard,
|
|
197
|
-
{
|
|
198
|
-
backgroundColor: currentTheme.card,
|
|
199
|
-
borderColor: currentTheme.border,
|
|
200
|
-
},
|
|
201
157
|
], children: [(0, jsx_runtime_1.jsx)(Text, { style: [rnStyles.infoCardTitle, { color: currentTheme.textMuted }], children: "ENGINE ACTIONS" }), (0, jsx_runtime_1.jsx)(View, { style: rnStyles.btnRow, children: actionButtons.slice(0, 2).map((btn) => ((0, jsx_runtime_1.jsx)(TouchableOpacity, { onPress: btn.onPress, style: [rnStyles.actionBtn, { backgroundColor: btn.bg }], activeOpacity: 0.8, children: (0, jsx_runtime_1.jsx)(Text, { style: [rnStyles.actionBtnText, { color: btn.color }], children: btn.label }) }, btn.id))) }), (0, jsx_runtime_1.jsx)(View, { style: [rnStyles.btnRow, { marginTop: 8 }], children: actionButtons.slice(2, 4).map((btn) => ((0, jsx_runtime_1.jsx)(TouchableOpacity, { onPress: btn.onPress, style: [rnStyles.actionBtn, { backgroundColor: btn.bg }], activeOpacity: 0.8, children: (0, jsx_runtime_1.jsx)(Text, { style: [rnStyles.actionBtnText, { color: btn.color }], children: btn.label }) }, btn.id))) })] }), (metrics?.lastPushError || metrics?.lastPullError) && ((0, jsx_runtime_1.jsxs)(View, { style: [rnStyles.infoCard, { borderColor: '#EF444480', backgroundColor: '#7F1D1D20' }], children: [(0, jsx_runtime_1.jsx)(Text, { style: [rnStyles.infoCardTitle, { color: '#F87171' }], children: "SYNC ENGINE ERRORS" }), metrics?.lastPushError && ((0, jsx_runtime_1.jsxs)(View, { style: { marginBottom: 6 }, children: [(0, jsx_runtime_1.jsx)(Text, { style: { fontSize: 11, color: '#FCA5A5', fontWeight: '700' }, children: "Last Push Error:" }), (0, jsx_runtime_1.jsx)(Text, { style: { fontSize: 12, color: '#FECACA' }, children: String(metrics.lastPushError) })] })), metrics?.lastPullError && ((0, jsx_runtime_1.jsxs)(View, { children: [(0, jsx_runtime_1.jsx)(Text, { style: { fontSize: 11, color: '#FCA5A5', fontWeight: '700' }, children: "Last Pull Error:" }), (0, jsx_runtime_1.jsx)(Text, { style: { fontSize: 12, color: '#FECACA' }, children: String(metrics.lastPullError) })] }))] })), status.pendingRecords && status.pendingRecords.length > 0 && ((0, jsx_runtime_1.jsxs)(View, { style: [
|
|
202
158
|
rnStyles.infoCard,
|
|
203
159
|
{
|
|
@@ -324,32 +280,6 @@ function SyncInspector({ client, position = 'bottom-right', variant = 'pill', in
|
|
|
324
280
|
border: `1px solid ${currentTheme.border}`,
|
|
325
281
|
borderRadius: 12,
|
|
326
282
|
padding: 16,
|
|
327
|
-
}, children: [(0, jsx_runtime_1.jsx)("div", { style: {
|
|
328
|
-
fontSize: 11,
|
|
329
|
-
fontWeight: 700,
|
|
330
|
-
color: currentTheme.textMuted,
|
|
331
|
-
letterSpacing: '0.8px',
|
|
332
|
-
marginBottom: 12,
|
|
333
|
-
}, children: "CONNECTION & TELEMETRY" }), telemetryItems.map((item, idx) => ((0, jsx_runtime_1.jsxs)("div", { style: {
|
|
334
|
-
display: 'flex',
|
|
335
|
-
justifyContent: 'space-between',
|
|
336
|
-
alignItems: 'center',
|
|
337
|
-
padding: '8px 0',
|
|
338
|
-
borderBottom: idx === telemetryItems.length - 1 ? 'none' : `1px solid ${currentTheme.border}`,
|
|
339
|
-
}, children: [(0, jsx_runtime_1.jsx)("span", { style: { color: currentTheme.textMuted, fontSize: 13 }, children: item.label }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [item.hasDot && ((0, jsx_runtime_1.jsx)("span", { style: {
|
|
340
|
-
width: 8,
|
|
341
|
-
height: 8,
|
|
342
|
-
borderRadius: 4,
|
|
343
|
-
backgroundColor: item.color,
|
|
344
|
-
} })), (0, jsx_runtime_1.jsx)("span", { style: {
|
|
345
|
-
color: item.color || currentTheme.text,
|
|
346
|
-
fontSize: 13,
|
|
347
|
-
fontWeight: item.color ? 700 : 600,
|
|
348
|
-
}, children: item.value })] })] }, item.id)))] }), (0, jsx_runtime_1.jsxs)("div", { style: {
|
|
349
|
-
backgroundColor: currentTheme.card,
|
|
350
|
-
border: `1px solid ${currentTheme.border}`,
|
|
351
|
-
borderRadius: 12,
|
|
352
|
-
padding: 16,
|
|
353
283
|
}, children: [(0, jsx_runtime_1.jsx)("div", { style: {
|
|
354
284
|
fontSize: 11,
|
|
355
285
|
fontWeight: 700,
|
package/dist/react/index.d.ts
CHANGED
|
@@ -28,6 +28,45 @@ export declare function useLiveQuery<T = any>(client: LocalFirstClient | null |
|
|
|
28
28
|
refetch: () => Promise<void>;
|
|
29
29
|
loading: boolean;
|
|
30
30
|
};
|
|
31
|
+
export type MultiQuerySpec<T = any> = string | (QueryOptions & {
|
|
32
|
+
table?: string;
|
|
33
|
+
}) | LiveQueryFn<T>;
|
|
34
|
+
export type MultiQueryInput = Array<string | (QueryOptions & {
|
|
35
|
+
table: string;
|
|
36
|
+
})> | Record<string, MultiQuerySpec>;
|
|
37
|
+
export interface UseLiveQueriesResult<T extends Record<string, any[]>> {
|
|
38
|
+
data: T;
|
|
39
|
+
loading: boolean;
|
|
40
|
+
refetch: () => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reactive multi-table live query hook that loads data from multiple tables concurrently.
|
|
44
|
+
*
|
|
45
|
+
* Benefits:
|
|
46
|
+
* - Parallel fetching: queries SQLite tables concurrently via Promise.all.
|
|
47
|
+
* - Single atomic React commit: coalesces multiple table results into a single state update,
|
|
48
|
+
* preventing cascading re-renders and UI freezing.
|
|
49
|
+
* - Multi-table event coordination: listens to mutations across all requested tables,
|
|
50
|
+
* coalescing updates so downstream calculations only run once.
|
|
51
|
+
* - Zero-latency cache read-through: returns instant cached rows on mount.
|
|
52
|
+
*
|
|
53
|
+
* Usage with dictionary:
|
|
54
|
+
* ```ts
|
|
55
|
+
* const { data, loading } = useLiveQueries(client, {
|
|
56
|
+
* projects: 'projects',
|
|
57
|
+
* tasks: { where: { project_id: '123' }, orderBy: 'created_at' },
|
|
58
|
+
* tags: { table: 'labels' }
|
|
59
|
+
* });
|
|
60
|
+
* // data.projects, data.tasks, data.tags
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* Usage with array:
|
|
64
|
+
* ```ts
|
|
65
|
+
* const { data, loading } = useLiveQueries(client, ['projects', 'tasks', 'tags']);
|
|
66
|
+
* // data.projects, data.tasks, data.tags
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function useLiveQueries<T extends Record<string, any[]> = Record<string, any[]>>(client: LocalFirstClient | null | undefined, queries: MultiQueryInput): UseLiveQueriesResult<T>;
|
|
31
70
|
/**
|
|
32
71
|
* Reactive hook to query a single record by primary key with 0ms cache read-through.
|
|
33
72
|
* Automatically re-renders only when this specific row or table changes.
|