local-lite 0.0.4 → 0.0.5

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.
@@ -0,0 +1,372 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryBuilder = void 0;
4
+ exports.formatColumnIdentifier = formatColumnIdentifier;
5
+ /**
6
+ * Escapes an identifier or compound identifier (e.g. 'table.column' or 'table.col as alias').
7
+ */
8
+ function formatColumnIdentifier(col) {
9
+ const trimmed = col.trim();
10
+ if (trimmed === '*')
11
+ return '*';
12
+ // Handle "expression AS alias"
13
+ const asMatch = trimmed.match(/^(.+?)\s+(?:AS|as)\s+([A-Za-z0-9_"]+)$/i);
14
+ if (asMatch) {
15
+ const [, expr, rawAlias] = asMatch;
16
+ const unquotedAlias = rawAlias.startsWith('"') && rawAlias.endsWith('"')
17
+ ? rawAlias.slice(1, -1)
18
+ : rawAlias;
19
+ const cleanAlias = `"${unquotedAlias.replace(/"/g, '""')}"`;
20
+ if (expr.includes('(') && expr.endsWith(')')) {
21
+ return `${expr} AS ${cleanAlias}`;
22
+ }
23
+ return `${formatColumnIdentifier(expr)} AS ${cleanAlias}`;
24
+ }
25
+ // Handle function calls without alias (e.g. COUNT(*))
26
+ if (trimmed.includes('(') && trimmed.endsWith(')')) {
27
+ return trimmed;
28
+ }
29
+ // Handle "table.column"
30
+ if (trimmed.includes('.')) {
31
+ return trimmed
32
+ .split('.')
33
+ .map((part) => `"${part.replace(/"/g, '""')}"`)
34
+ .join('.');
35
+ }
36
+ return `"${trimmed.replace(/"/g, '""')}"`;
37
+ }
38
+ /**
39
+ * Fluent, type-safe SQL Query Builder for local-first SQLite operations.
40
+ * Allows writing expressive JavaScript code that compiles and executes high-performance SQL under the hood.
41
+ */
42
+ class QueryBuilder {
43
+ client;
44
+ tableName;
45
+ selectCols = ['*'];
46
+ joins = [];
47
+ whereConditions = {};
48
+ orderColumn;
49
+ orderDirection = 'ASC';
50
+ limitCount;
51
+ offsetCount;
52
+ groupByCols = [];
53
+ includeDeleted = false;
54
+ constructor(client, tableName) {
55
+ this.client = client;
56
+ this.tableName = tableName;
57
+ }
58
+ /**
59
+ * Specifies which columns to select.
60
+ * Supports column aliases (e.g. ['id', 'contracts.rent_amount AS rent', 'houses.house_number']).
61
+ */
62
+ select(...columns) {
63
+ const flat = columns.flat();
64
+ if (flat.length > 0) {
65
+ this.selectCols = flat;
66
+ }
67
+ return this;
68
+ }
69
+ /**
70
+ * Inner joins another SQLite table on a matching column expression.
71
+ */
72
+ join(table, firstCol, secondCol) {
73
+ this.joins.push({ type: 'INNER', table, firstCol, secondCol });
74
+ return this;
75
+ }
76
+ /**
77
+ * Left joins another SQLite table on a matching column expression.
78
+ */
79
+ leftJoin(table, firstCol, secondCol) {
80
+ this.joins.push({ type: 'LEFT', table, firstCol, secondCol });
81
+ return this;
82
+ }
83
+ /**
84
+ * Adds WHERE criteria to filter results.
85
+ * Supports:
86
+ * - where({ status: 'ACTIVE' })
87
+ * - where('status', 'ACTIVE')
88
+ * - where('end_date', '>=', today)
89
+ */
90
+ where(columnOrFilter, operatorOrValue, value) {
91
+ if (typeof columnOrFilter === 'string') {
92
+ if (value !== undefined) {
93
+ const rawOp = String(operatorOrValue).toLowerCase().trim();
94
+ const opKey = rawOp === '>=' ? '$gte' :
95
+ rawOp === '<=' ? '$lte' :
96
+ rawOp === '>' ? '$gt' :
97
+ rawOp === '<' ? '$lt' :
98
+ rawOp === '!=' || rawOp === '<>' ? '$ne' :
99
+ rawOp === '=' || rawOp === '==' ? '$eq' :
100
+ rawOp === 'like' ? '$contains' : '$eq';
101
+ const existing = typeof this.whereConditions[columnOrFilter] === 'object' &&
102
+ this.whereConditions[columnOrFilter] !== null
103
+ ? this.whereConditions[columnOrFilter]
104
+ : {};
105
+ this.whereConditions[columnOrFilter] = { ...existing, [opKey]: value };
106
+ }
107
+ else {
108
+ this.whereConditions[columnOrFilter] = operatorOrValue;
109
+ }
110
+ }
111
+ else if (columnOrFilter && typeof columnOrFilter === 'object') {
112
+ this.whereConditions = { ...this.whereConditions, ...columnOrFilter };
113
+ }
114
+ return this;
115
+ }
116
+ /**
117
+ * Orders query results by column and direction.
118
+ */
119
+ orderBy(column, direction = 'ASC') {
120
+ this.orderColumn = column;
121
+ this.orderDirection = direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
122
+ return this;
123
+ }
124
+ /**
125
+ * Groups rows by one or more columns for aggregation.
126
+ */
127
+ groupBy(...columns) {
128
+ this.groupByCols = columns.flat();
129
+ return this;
130
+ }
131
+ /**
132
+ * Limits the number of returned records.
133
+ */
134
+ limit(count) {
135
+ this.limitCount = count;
136
+ return this;
137
+ }
138
+ /**
139
+ * Offsets the starting record position.
140
+ */
141
+ offset(count) {
142
+ this.offsetCount = count;
143
+ return this;
144
+ }
145
+ /**
146
+ * Includes soft-deleted rows in results.
147
+ */
148
+ withDeleted(include = true) {
149
+ this.includeDeleted = include;
150
+ return this;
151
+ }
152
+ /**
153
+ * Applies standard QueryOptions (select, where, orderBy, limit, offset, includeDeleted) to this query.
154
+ */
155
+ applyOptions(options) {
156
+ if (!options)
157
+ return this;
158
+ if (options.select)
159
+ this.select(options.select);
160
+ if (options.where && typeof options.where === 'object')
161
+ this.where(options.where);
162
+ if (options.orderBy)
163
+ this.orderBy(options.orderBy, options.order || 'ASC');
164
+ if (typeof options.limit === 'number')
165
+ this.limit(options.limit);
166
+ if (typeof options.offset === 'number')
167
+ this.offset(options.offset);
168
+ if (options.includeDeleted)
169
+ this.withDeleted(true);
170
+ return this;
171
+ }
172
+ /**
173
+ * Compiles the current builder state into an executable SQL string and parameter list.
174
+ */
175
+ toSQL() {
176
+ const params = [];
177
+ const projections = this.selectCols.map(formatColumnIdentifier).join(', ');
178
+ const fromTable = `"${this.tableName.replace(/"/g, '""')}"`;
179
+ let sql = `SELECT ${projections} FROM ${fromTable}`;
180
+ // Append JOINs
181
+ for (const j of this.joins) {
182
+ const joinTable = `"${j.table.replace(/"/g, '""')}"`;
183
+ const col1 = formatColumnIdentifier(j.firstCol);
184
+ const col2 = formatColumnIdentifier(j.secondCol);
185
+ sql += ` ${j.type} JOIN ${joinTable} ON ${col1} = ${col2}`;
186
+ }
187
+ // Build WHERE clauses
188
+ const whereClauses = [];
189
+ if (this.whereConditions && typeof this.whereConditions === 'object') {
190
+ for (const [col, val] of Object.entries(this.whereConditions)) {
191
+ if (col === '$or' || col === '$and')
192
+ continue;
193
+ const safeCol = formatColumnIdentifier(col);
194
+ if (val === null) {
195
+ whereClauses.push(`${safeCol} IS NULL`);
196
+ }
197
+ else if (typeof val === 'boolean') {
198
+ whereClauses.push(`${safeCol} = ?`);
199
+ params.push(val ? 1 : 0);
200
+ }
201
+ else if (typeof val === 'string' || typeof val === 'number') {
202
+ whereClauses.push(`${safeCol} = ?`);
203
+ params.push(val);
204
+ }
205
+ else if (typeof val === 'object' && !Array.isArray(val)) {
206
+ const ops = val;
207
+ const eqVal = ops.$eq !== undefined ? ops.$eq : ops.eq;
208
+ if (eqVal !== undefined) {
209
+ whereClauses.push(`${safeCol} = ?`);
210
+ params.push(typeof eqVal === 'boolean' ? (eqVal ? 1 : 0) : eqVal);
211
+ }
212
+ const neVal = ops.$ne !== undefined ? ops.$ne : ops.ne;
213
+ if (neVal !== undefined) {
214
+ whereClauses.push(`${safeCol} != ?`);
215
+ params.push(typeof neVal === 'boolean' ? (neVal ? 1 : 0) : neVal);
216
+ }
217
+ const gtVal = ops.$gt !== undefined ? ops.$gt : ops.gt;
218
+ if (gtVal !== undefined) {
219
+ whereClauses.push(`${safeCol} > ?`);
220
+ params.push(gtVal);
221
+ }
222
+ const gteVal = ops.$gte !== undefined ? ops.$gte : ops.gte;
223
+ if (gteVal !== undefined) {
224
+ whereClauses.push(`${safeCol} >= ?`);
225
+ params.push(gteVal);
226
+ }
227
+ const ltVal = ops.$lt !== undefined ? ops.$lt : ops.lt;
228
+ if (ltVal !== undefined) {
229
+ whereClauses.push(`${safeCol} < ?`);
230
+ params.push(ltVal);
231
+ }
232
+ const lteVal = ops.$lte !== undefined ? ops.$lte : ops.lte;
233
+ if (lteVal !== undefined) {
234
+ whereClauses.push(`${safeCol} <= ?`);
235
+ params.push(lteVal);
236
+ }
237
+ const inVal = ops.$in !== undefined ? ops.$in : ops.in;
238
+ if (Array.isArray(inVal) && inVal.length > 0) {
239
+ const placeholders = inVal.map(() => '?').join(', ');
240
+ whereClauses.push(`${safeCol} IN (${placeholders})`);
241
+ params.push(...inVal);
242
+ }
243
+ const ninVal = ops.$nin !== undefined ? ops.$nin : ops.nin;
244
+ if (Array.isArray(ninVal) && ninVal.length > 0) {
245
+ const placeholders = ninVal.map(() => '?').join(', ');
246
+ whereClauses.push(`${safeCol} NOT IN (${placeholders})`);
247
+ params.push(...ninVal);
248
+ }
249
+ const containsVal = ops.$contains !== undefined ? ops.$contains : ops.contains;
250
+ if (containsVal !== undefined) {
251
+ whereClauses.push(`${safeCol} LIKE ?`);
252
+ params.push(`%${containsVal}%`);
253
+ }
254
+ const nullVal = ops.$null !== undefined ? ops.$null : ops.null;
255
+ if (nullVal !== undefined) {
256
+ whereClauses.push(nullVal ? `${safeCol} IS NULL` : `${safeCol} IS NOT NULL`);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ // Push down soft-deletion check on primary table if deleted column exists and not explicitly overridden
262
+ const schemaManager = 'schemaManager' in this.client
263
+ ? this.client.schemaManager
264
+ : this.client.parent?.schemaManager;
265
+ if (!this.includeDeleted && schemaManager?.hasColumn(this.tableName, 'deleted')) {
266
+ const delCol = this.tableName.includes('.')
267
+ ? `${formatColumnIdentifier(this.tableName)}."deleted"`
268
+ : `"${this.tableName.replace(/"/g, '""')}"."deleted"`;
269
+ whereClauses.push(`(${delCol} IS NULL OR (${delCol} != 1 AND ${delCol} != 'true'))`);
270
+ }
271
+ if (whereClauses.length > 0) {
272
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
273
+ }
274
+ // GROUP BY
275
+ if (this.groupByCols.length > 0) {
276
+ sql += ` GROUP BY ${this.groupByCols.map(formatColumnIdentifier).join(', ')}`;
277
+ }
278
+ // ORDER BY
279
+ if (this.orderColumn) {
280
+ sql += ` ORDER BY ${formatColumnIdentifier(this.orderColumn)} ${this.orderDirection}`;
281
+ }
282
+ // LIMIT & OFFSET
283
+ if (typeof this.limitCount === 'number' && this.limitCount > 0) {
284
+ sql += ` LIMIT ?`;
285
+ params.push(this.limitCount);
286
+ if (typeof this.offsetCount === 'number' && this.offsetCount > 0) {
287
+ sql += ` OFFSET ?`;
288
+ params.push(this.offsetCount);
289
+ }
290
+ }
291
+ return { sql, params };
292
+ }
293
+ /**
294
+ * Executes the compiled SQL query against SQLite and returns rows.
295
+ */
296
+ async execute() {
297
+ const schemaManager = 'schemaManager' in this.client
298
+ ? this.client.schemaManager
299
+ : this.client.parent?.schemaManager;
300
+ const db = 'db' in this.client ? this.client.db : this.client.tx;
301
+ if (schemaManager && db) {
302
+ await schemaManager.ensureTableIntrospected(db, this.tableName);
303
+ for (const j of this.joins) {
304
+ await schemaManager.ensureTableIntrospected(db, j.table);
305
+ }
306
+ }
307
+ const { sql, params } = this.toSQL();
308
+ try {
309
+ const rawRows = await this.client.query(sql, params);
310
+ const hydrator = 'hydrateRow' in this.client
311
+ ? this.client.hydrateRow.bind(this.client)
312
+ : this.client.parent?.hydrateRow?.bind(this.client.parent);
313
+ if (hydrator && rawRows && rawRows.length > 0) {
314
+ return rawRows.map((r) => hydrator(this.tableName, r));
315
+ }
316
+ return rawRows;
317
+ }
318
+ catch (err) {
319
+ const msg = String(err?.message || err || '');
320
+ if (msg.includes('no such table') || msg.includes('no such column')) {
321
+ return [];
322
+ }
323
+ throw err;
324
+ }
325
+ }
326
+ /**
327
+ * Returns the first matching record or null if not found.
328
+ */
329
+ async first() {
330
+ const originalLimit = this.limitCount;
331
+ this.limitCount = 1;
332
+ const rows = await this.execute();
333
+ this.limitCount = originalLimit;
334
+ return rows.length > 0 ? rows[0] : null;
335
+ }
336
+ /**
337
+ * Executes a COUNT(*) in SQLite and returns the numeric count directly.
338
+ */
339
+ async count() {
340
+ const originalSelect = this.selectCols;
341
+ this.selectCols = ['COUNT(*) AS _count'];
342
+ const rows = await this.execute();
343
+ this.selectCols = originalSelect;
344
+ const countVal = rows[0]?._count;
345
+ return typeof countVal === 'number' ? countVal : Number(countVal) || 0;
346
+ }
347
+ /**
348
+ * Executes a SUM(column) in SQLite and returns the numeric total directly.
349
+ */
350
+ async sum(column) {
351
+ const originalSelect = this.selectCols;
352
+ const safeCol = formatColumnIdentifier(column);
353
+ this.selectCols = [`COALESCE(SUM(${safeCol}), 0) AS _sum`];
354
+ const rows = await this.execute();
355
+ this.selectCols = originalSelect;
356
+ const sumVal = rows[0]?._sum;
357
+ return typeof sumVal === 'number' ? sumVal : Number(sumVal) || 0;
358
+ }
359
+ /**
360
+ * Executes an AVG(column) in SQLite and returns the average directly.
361
+ */
362
+ async avg(column) {
363
+ const originalSelect = this.selectCols;
364
+ const safeCol = formatColumnIdentifier(column);
365
+ this.selectCols = [`AVG(${safeCol}) AS _avg`];
366
+ const rows = await this.execute();
367
+ this.selectCols = originalSelect;
368
+ const avgVal = rows[0]?._avg;
369
+ return avgVal === null || avgVal === undefined ? null : Number(avgVal) || 0;
370
+ }
371
+ }
372
+ exports.QueryBuilder = QueryBuilder;
@@ -91,6 +91,38 @@ class SchemaManager {
91
91
  }
92
92
  }
93
93
  }
94
+ // Auto-create high performance single and composite indexes for foreign keys (_id) and soft-deletes (deleted)
95
+ const hasDeleted = this.knownColumns.has(`${tableName}:deleted`);
96
+ for (const entry of this.knownColumns) {
97
+ if (!entry.startsWith(`${tableName}:`))
98
+ continue;
99
+ const colName = entry.slice(tableName.length + 1);
100
+ if (colName.endsWith('_id') && colName !== 'id') {
101
+ const fkIndexKey = `${tableName}:${colName}`;
102
+ if (!this.knownIndexes.has(fkIndexKey)) {
103
+ this.knownIndexes.add(fkIndexKey);
104
+ try {
105
+ const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${colName}`);
106
+ const safeFk = (0, sql_js_1.escapeIdentifier)(colName);
107
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk});`);
108
+ }
109
+ catch { }
110
+ }
111
+ if (hasDeleted) {
112
+ const compositeKey = `${tableName}:${colName}_deleted`;
113
+ if (!this.knownIndexes.has(compositeKey)) {
114
+ this.knownIndexes.add(compositeKey);
115
+ try {
116
+ const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${colName}_del`);
117
+ const safeFk = (0, sql_js_1.escapeIdentifier)(colName);
118
+ const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
119
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk}, ${safeDel});`);
120
+ }
121
+ catch { }
122
+ }
123
+ }
124
+ }
125
+ }
94
126
  }
95
127
  catch {
96
128
  // Ignore
@@ -171,10 +203,9 @@ class SchemaManager {
171
203
  // Auto-index foreign keys and common query columns
172
204
  const lowerKey = key.toLowerCase();
173
205
  if (lowerKey.endsWith('_id') ||
174
- lowerKey === 'business_id' ||
175
206
  lowerKey === 'deleted' ||
176
207
  lowerKey === 'created_at' ||
177
- lowerKey === 'payment_date') {
208
+ lowerKey === 'updated_at') {
178
209
  const indexKey = `${tableName}:${lowerKey}`;
179
210
  if (!this.knownIndexes.has(indexKey)) {
180
211
  this.knownIndexes.add(indexKey);
@@ -190,20 +221,24 @@ class SchemaManager {
190
221
  }
191
222
  }
192
223
  }
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
224
+ // Auto-create high performance composite index for foreign keys with soft-deletion
225
+ if (this.knownColumns.has(`${tableName}:deleted`)) {
226
+ for (const key of keys) {
227
+ const lowerKey = key.toLowerCase();
228
+ if (lowerKey.endsWith('_id') && lowerKey !== 'id') {
229
+ const compositeKey = `${tableName}:${lowerKey}_deleted`;
230
+ if (!this.knownIndexes.has(compositeKey)) {
231
+ this.knownIndexes.add(compositeKey);
232
+ try {
233
+ const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${lowerKey}_del`);
234
+ const safeFk = (0, sql_js_1.escapeIdentifier)(key);
235
+ const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
236
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk}, ${safeDel});`);
237
+ }
238
+ catch {
239
+ // Ignore
240
+ }
241
+ }
207
242
  }
208
243
  }
209
244
  }
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,
@@ -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.