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.
Files changed (36) hide show
  1. package/dist/adapters/local/better-sqlite3.d.ts +4 -1
  2. package/dist/adapters/local/better-sqlite3.js +34 -14
  3. package/dist/adapters/local/expo-sqlite.d.ts +10 -2
  4. package/dist/adapters/local/expo-sqlite.js +36 -20
  5. package/dist/adapters/local/node-sqlite.d.ts +4 -1
  6. package/dist/adapters/local/node-sqlite.js +29 -12
  7. package/dist/adapters/remote/firestore.js +39 -32
  8. package/dist/adapters/remote/supabase.d.ts +7 -2
  9. package/dist/adapters/remote/supabase.js +192 -85
  10. package/dist/config/types.d.ts +1 -1
  11. package/dist/core/client.d.ts +52 -15
  12. package/dist/core/client.js +195 -208
  13. package/dist/core/index.d.ts +1 -0
  14. package/dist/core/index.js +1 -0
  15. package/dist/core/outbox-store.d.ts +5 -0
  16. package/dist/core/outbox-store.js +29 -4
  17. package/dist/core/query-builder.d.ts +120 -0
  18. package/dist/core/query-builder.js +441 -0
  19. package/dist/core/query.js +28 -32
  20. package/dist/core/schema-manager.d.ts +4 -15
  21. package/dist/core/schema-manager.js +48 -103
  22. package/dist/core/sync-worker.js +7 -2
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/react/SyncInspector.js +0 -70
  26. package/dist/react/index.d.ts +39 -0
  27. package/dist/react/index.js +232 -34
  28. package/dist/react/status.d.ts +5 -0
  29. package/dist/react/status.js +20 -12
  30. package/dist/schema/generator.d.ts +49 -0
  31. package/dist/schema/generator.js +209 -0
  32. package/dist/schema/index.d.ts +1 -0
  33. package/dist/schema/index.js +17 -0
  34. package/dist/utils/uuid.d.ts +1 -1
  35. package/dist/utils/uuid.js +7 -16
  36. 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 (value === null || value === undefined) {
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 && col.name) {
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
- // Ignore
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 === primaryKey)
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 && col.name) {
134
+ if (col?.name) {
143
135
  this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
144
136
  }
145
137
  }
146
138
  }
147
139
  }
148
- catch (err) {
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 ${safeCol} ${detectedType};`);
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]);
@@ -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
- // 1. Check for pending local mutations for this (table_name, row_id)
616
- const localPendingRecord = await outbox_store_js_1.OutboxStore.findPendingByRow(tx, change.tableName, change.rowId);
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,
@@ -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.