local-lite 0.0.3 → 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.
Files changed (63) hide show
  1. package/README.md +421 -362
  2. package/dist/adapters/local/better-sqlite3.d.ts +2 -0
  3. package/dist/adapters/local/better-sqlite3.js +46 -25
  4. package/dist/adapters/local/expo-sqlite.d.ts +5 -1
  5. package/dist/adapters/local/expo-sqlite.js +40 -7
  6. package/dist/adapters/local/index.d.ts +1 -0
  7. package/dist/adapters/local/node-sqlite.d.ts +2 -0
  8. package/dist/adapters/local/node-sqlite.js +45 -22
  9. package/dist/adapters/local/op-sqlite.d.ts +1 -0
  10. package/dist/adapters/local/op-sqlite.js +6 -0
  11. package/dist/adapters/local/sql-js.d.ts +1 -0
  12. package/dist/adapters/local/sql-js.js +5 -0
  13. package/dist/adapters/network/index.js +33 -0
  14. package/dist/adapters/remote/convex.d.ts +2 -2
  15. package/dist/adapters/remote/convex.js +8 -12
  16. package/dist/adapters/remote/demo.d.ts +1 -1
  17. package/dist/adapters/remote/demo.js +3 -0
  18. package/dist/adapters/remote/firestore.d.ts +3 -2
  19. package/dist/adapters/remote/firestore.js +69 -46
  20. package/dist/adapters/remote/index.d.ts +6 -4
  21. package/dist/adapters/remote/supabase.d.ts +34 -6
  22. package/dist/adapters/remote/supabase.js +340 -79
  23. package/dist/config/index.js +7 -10
  24. package/dist/config/resolve-local.js +12 -1
  25. package/dist/config/resolve-remote.js +33 -28
  26. package/dist/config/types.d.ts +30 -5
  27. package/dist/core/client.d.ts +133 -5
  28. package/dist/core/client.js +431 -46
  29. package/dist/core/index.d.ts +15 -0
  30. package/dist/core/index.js +30 -0
  31. package/dist/core/outbox-store.d.ts +8 -4
  32. package/dist/core/outbox-store.js +84 -65
  33. package/dist/core/query-builder.d.ts +119 -0
  34. package/dist/core/query-builder.js +372 -0
  35. package/dist/core/query.d.ts +8 -0
  36. package/dist/core/query.js +37 -0
  37. package/dist/core/schema-manager.d.ts +9 -0
  38. package/dist/core/schema-manager.js +103 -0
  39. package/dist/core/sync-worker.d.ts +38 -1
  40. package/dist/core/sync-worker.js +351 -104
  41. package/dist/core/table-manager.js +26 -29
  42. package/dist/core/types.d.ts +8 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.js +2 -0
  45. package/dist/react/BenchmarkModal.d.ts +32 -0
  46. package/dist/react/BenchmarkModal.js +777 -0
  47. package/dist/react/SyncInspector.d.ts +31 -0
  48. package/dist/react/SyncInspector.js +585 -0
  49. package/dist/react/index.d.ts +89 -18
  50. package/dist/react/index.js +450 -105
  51. package/dist/react/runner.d.ts +7 -0
  52. package/dist/react/runner.js +42 -0
  53. package/dist/react/status.d.ts +24 -0
  54. package/dist/react/status.js +123 -0
  55. package/dist/react/theme.d.ts +18 -0
  56. package/dist/react/theme.js +46 -0
  57. package/dist/schema/generator.d.ts +49 -0
  58. package/dist/schema/generator.js +209 -0
  59. package/dist/schema/index.d.ts +1 -0
  60. package/dist/schema/index.js +17 -0
  61. package/dist/utils/logger.d.ts +4 -0
  62. package/dist/utils/logger.js +20 -0
  63. package/package.json +143 -133
@@ -1,9 +1,95 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.SyncIndicator = exports.SyncInspector = void 0;
18
+ exports.clearLiveQueryCache = clearLiveQueryCache;
3
19
  exports.useLiveQuery = useLiveQuery;
4
- exports.useSyncStatus = useSyncStatus;
5
- exports.useSyncMetrics = useSyncMetrics;
20
+ exports.useLiveQueries = useLiveQueries;
21
+ exports.useLiveRow = useLiveRow;
22
+ exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
23
+ exports.useMutations = useMutations;
6
24
  const react_1 = require("react");
25
+ const runner_js_1 = require("./runner.js");
26
+ function areRowsEqual(a, b) {
27
+ if (a === b)
28
+ return true;
29
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
30
+ return false;
31
+ const aObj = a;
32
+ const bObj = b;
33
+ if (aObj.id !== undefined && bObj.id !== undefined && aObj.id !== bObj.id) {
34
+ return false;
35
+ }
36
+ // Fast path: if both records have an identical updated_at timestamp, they are equal.
37
+ // This bypasses expensive Object.keys loops and JSON.stringify on large metadata columns.
38
+ if (aObj.id !== undefined &&
39
+ bObj.id !== undefined &&
40
+ aObj.id === bObj.id &&
41
+ aObj.updated_at !== undefined &&
42
+ bObj.updated_at !== undefined) {
43
+ return aObj.updated_at === bObj.updated_at;
44
+ }
45
+ const aKeys = Object.keys(aObj);
46
+ const bKeys = Object.keys(bObj);
47
+ if (aKeys.length !== bKeys.length)
48
+ return false;
49
+ for (let i = 0; i < aKeys.length; i++) {
50
+ const k = aKeys[i];
51
+ const aVal = aObj[k];
52
+ const bVal = bObj[k];
53
+ if (aVal === bVal)
54
+ continue;
55
+ if (aVal &&
56
+ bVal &&
57
+ typeof aVal === 'object' &&
58
+ typeof bVal === 'object') {
59
+ if (Array.isArray(aVal) !== Array.isArray(bVal))
60
+ return false;
61
+ if (JSON.stringify(aVal) !== JSON.stringify(bVal))
62
+ return false;
63
+ continue;
64
+ }
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ function areRowArraysEqual(a, b) {
70
+ if (a === b)
71
+ return true;
72
+ if (!a || !b)
73
+ return false;
74
+ if (a.length !== b.length)
75
+ return false;
76
+ for (let i = 0; i < a.length; i++) {
77
+ if (!areRowsEqual(a[i], b[i])) {
78
+ return false;
79
+ }
80
+ }
81
+ return true;
82
+ }
83
+ // In-memory query result cache to eliminate initial render latency and flickering
84
+ const queryCache = new Map();
85
+ const inFlightQueries = new Map();
86
+ /**
87
+ * Clears the in-memory live query cache.
88
+ */
89
+ function clearLiveQueryCache() {
90
+ queryCache.clear();
91
+ inFlightQueries.clear();
92
+ }
7
93
  /**
8
94
  * Hook to execute a live reactive table query that automatically re-runs
9
95
  * whenever the table is mutated locally or via incoming remote delta.
@@ -23,32 +109,50 @@ const react_1 = require("react");
23
109
  * const { data } = useLiveQuery(client, 'todos', (c) => c.db.execute('SELECT ...'));
24
110
  */
25
111
  function useLiveQuery(client, tableName, queryOrOptions) {
26
- const [data, setData] = (0, react_1.useState)([]);
27
- const [loading, setLoading] = (0, react_1.useState)(true);
112
+ const queryKey = typeof queryOrOptions === 'object' && queryOrOptions !== null
113
+ ? JSON.stringify(queryOrOptions)
114
+ : typeof queryOrOptions === 'function'
115
+ ? 'custom'
116
+ : 'all';
117
+ const cacheKey = `${tableName}:${queryKey}`;
118
+ // Read-through cache: return instant cached rows on mount (0ms latency, zero flicker)
119
+ const [data, setData] = (0, react_1.useState)(() => queryCache.get(cacheKey) || []);
120
+ const [loading, setLoading] = (0, react_1.useState)(() => !queryCache.has(cacheKey));
28
121
  const queryParamRef = (0, react_1.useRef)(queryOrOptions);
29
122
  queryParamRef.current = queryOrOptions;
30
123
  const refetch = (0, react_1.useCallback)(async () => {
31
124
  if (!client || !client.db)
32
125
  return;
33
126
  try {
34
- const param = queryParamRef.current;
35
- let res;
36
- if (typeof param === 'function') {
37
- res = (await param(client)) || [];
38
- }
39
- else if (typeof param === 'object' && param !== null) {
40
- res = await client.findMany(tableName, param);
41
- }
42
- else {
43
- res = await client.findMany(tableName);
127
+ // De-duplicate simultaneous identical in-flight queries
128
+ let inFlight = inFlightQueries.get(cacheKey);
129
+ if (!inFlight) {
130
+ inFlight = (async () => {
131
+ const param = queryParamRef.current;
132
+ if (typeof param === 'function') {
133
+ return (await param(client)) || [];
134
+ }
135
+ else if (typeof param === 'object' && param !== null) {
136
+ return await client.findMany(tableName, param);
137
+ }
138
+ else {
139
+ return await client.findMany(tableName);
140
+ }
141
+ })();
142
+ inFlightQueries.set(cacheKey, inFlight);
143
+ inFlight.finally(() => {
144
+ inFlightQueries.delete(cacheKey);
145
+ });
44
146
  }
45
- setData(res || []);
147
+ const res = (await inFlight) || [];
148
+ queryCache.set(cacheKey, res);
149
+ setData((prev) => (areRowArraysEqual(prev, res) ? prev : res));
46
150
  }
47
151
  catch (err) {
48
152
  const msg = String(err?.message || err || '');
49
153
  if (msg.includes('no such column') || msg.includes('no such table')) {
50
154
  // Table or columns have not been populated yet on fresh database; default to empty list
51
- setData([]);
155
+ setData((prev) => (prev.length === 0 ? prev : []));
52
156
  }
53
157
  else {
54
158
  console.error(`Error in useLiveQuery("${tableName}"):`, err);
@@ -57,129 +161,370 @@ function useLiveQuery(client, tableName, queryOrOptions) {
57
161
  finally {
58
162
  setLoading(false);
59
163
  }
60
- }, [client, tableName]);
164
+ }, [client, tableName, cacheKey]);
165
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
61
166
  (0, react_1.useEffect)(() => {
62
167
  if (!client || !client.db)
63
168
  return;
64
169
  refetch();
65
170
  const unsubscribe = client.events.on(tableName, () => {
66
- refetch();
171
+ scheduleRefetch();
67
172
  });
68
173
  return () => {
69
174
  unsubscribe();
70
175
  };
71
- }, [client, tableName, refetch]);
176
+ }, [client, tableName, queryKey, refetch, scheduleRefetch]);
72
177
  return { data, refetch, loading };
73
178
  }
179
+ function normalizeMultiQueries(input) {
180
+ const items = [];
181
+ if (Array.isArray(input)) {
182
+ for (const item of input) {
183
+ if (typeof item === 'string') {
184
+ items.push({
185
+ key: item,
186
+ table: item,
187
+ queryKey: 'all',
188
+ cacheKey: `${item}:all`,
189
+ });
190
+ }
191
+ else if (typeof item === 'object' && item !== null && item.table) {
192
+ const { table, ...options } = item;
193
+ const qKey = JSON.stringify(options);
194
+ items.push({
195
+ key: table,
196
+ table,
197
+ queryKey: qKey,
198
+ cacheKey: `${table}:${qKey}`,
199
+ options,
200
+ });
201
+ }
202
+ }
203
+ }
204
+ else if (typeof input === 'object' && input !== null) {
205
+ for (const [key, spec] of Object.entries(input)) {
206
+ if (typeof spec === 'string') {
207
+ items.push({
208
+ key,
209
+ table: spec,
210
+ queryKey: 'all',
211
+ cacheKey: `${spec}:all`,
212
+ });
213
+ }
214
+ else if (typeof spec === 'function') {
215
+ items.push({
216
+ key,
217
+ table: key,
218
+ queryKey: 'custom',
219
+ cacheKey: `${key}:custom`,
220
+ fn: spec,
221
+ });
222
+ }
223
+ else if (typeof spec === 'object' && spec !== null) {
224
+ const table = spec.table || key;
225
+ const options = { ...spec };
226
+ delete options.table;
227
+ const qKey = JSON.stringify(options);
228
+ items.push({
229
+ key,
230
+ table,
231
+ queryKey: qKey,
232
+ cacheKey: `${table}:${qKey}`,
233
+ options,
234
+ });
235
+ }
236
+ }
237
+ }
238
+ return items;
239
+ }
74
240
  /**
75
- * Consolidated hook that tracks real-time sync connectivity, sync worker activity
76
- * (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
241
+ * Reactive multi-table live query hook that loads data from multiple tables concurrently.
242
+ *
243
+ * Benefits:
244
+ * - Parallel fetching: queries SQLite tables concurrently via Promise.all.
245
+ * - Single atomic React commit: coalesces multiple table results into a single state update,
246
+ * preventing cascading re-renders and UI freezing.
247
+ * - Multi-table event coordination: listens to mutations across all requested tables,
248
+ * coalescing updates so downstream calculations only run once.
249
+ * - Zero-latency cache read-through: returns instant cached rows on mount.
250
+ *
251
+ * Usage with dictionary:
252
+ * ```ts
253
+ * const { data, loading } = useLiveQueries(client, {
254
+ * projects: 'projects',
255
+ * tasks: { where: { project_id: '123' }, orderBy: 'created_at' },
256
+ * tags: { table: 'labels' }
257
+ * });
258
+ * // data.projects, data.tasks, data.tags
259
+ * ```
260
+ *
261
+ * Usage with array:
262
+ * ```ts
263
+ * const { data, loading } = useLiveQueries(client, ['projects', 'tasks', 'tags']);
264
+ * // data.projects, data.tasks, data.tags
265
+ * ```
77
266
  */
78
- function useSyncStatus(client) {
79
- const [isOnline, setIsOnline] = (0, react_1.useState)(true);
80
- const [isPushing, setIsPushing] = (0, react_1.useState)(false);
81
- const [isPulling, setIsPulling] = (0, react_1.useState)(false);
82
- const [pendingCount, setPendingCount] = (0, react_1.useState)(0);
83
- const [pendingRecords, setPendingRecords] = (0, react_1.useState)([]);
84
- const [lastSyncedAt, setLastSyncedAt] = (0, react_1.useState)(null);
85
- const refresh = (0, react_1.useCallback)(async () => {
86
- if (!client || !client.db)
267
+ function useLiveQueries(client, queries) {
268
+ const signature = (0, react_1.useMemo)(() => {
269
+ return JSON.stringify(queries, (_, val) => (typeof val === 'function' ? 'fn' : val));
270
+ }, [queries]);
271
+ const items = (0, react_1.useMemo)(() => normalizeMultiQueries(queries), [signature]);
272
+ const [data, setData] = (0, react_1.useState)(() => {
273
+ const initial = {};
274
+ for (const item of items) {
275
+ const cached = queryCache.get(item.cacheKey);
276
+ initial[item.key] = cached ? cached : [];
277
+ }
278
+ return initial;
279
+ });
280
+ const [loading, setLoading] = (0, react_1.useState)(() => {
281
+ if (items.length === 0)
282
+ return false;
283
+ for (const item of items) {
284
+ if (!queryCache.has(item.cacheKey))
285
+ return true;
286
+ }
287
+ return false;
288
+ });
289
+ const refetch = (0, react_1.useCallback)(async () => {
290
+ if (!client || !client.db || items.length === 0)
87
291
  return;
88
292
  try {
89
- const [metrics, records, lastSynced] = await Promise.all([
90
- client.getMetrics().catch(() => null),
91
- client.getOutboxRecords().catch(() => []),
92
- client.getLastSyncedAt().catch(() => null),
93
- ]);
94
- if (metrics) {
95
- setIsOnline(metrics.isOnline);
96
- setIsPushing(metrics.isPushing);
97
- setIsPulling(metrics.isPulling);
293
+ const results = await Promise.all(items.map(async (item) => {
294
+ let inFlight = inFlightQueries.get(item.cacheKey);
295
+ if (!inFlight) {
296
+ inFlight = (async () => {
297
+ if (item.fn) {
298
+ return (await item.fn(client)) || [];
299
+ }
300
+ else if (item.options) {
301
+ return await client.findMany(item.table, item.options);
302
+ }
303
+ else {
304
+ return await client.findMany(item.table);
305
+ }
306
+ })();
307
+ inFlightQueries.set(item.cacheKey, inFlight);
308
+ inFlight.finally(() => {
309
+ inFlightQueries.delete(item.cacheKey);
310
+ });
311
+ }
312
+ try {
313
+ const rows = (await inFlight) || [];
314
+ queryCache.set(item.cacheKey, rows);
315
+ return { key: item.key, rows };
316
+ }
317
+ catch (err) {
318
+ const msg = String(err?.message || err || '');
319
+ if (msg.includes('no such column') || msg.includes('no such table')) {
320
+ return { key: item.key, rows: [] };
321
+ }
322
+ console.error(`Error in useLiveQueries("${item.table}"):`, err);
323
+ return { key: item.key, rows: [] };
324
+ }
325
+ }));
326
+ setData((prev) => {
327
+ let changed = false;
328
+ const next = { ...prev };
329
+ for (const res of results) {
330
+ if (!areRowArraysEqual(prev[res.key], res.rows)) {
331
+ next[res.key] = res.rows;
332
+ changed = true;
333
+ }
334
+ }
335
+ return changed ? next : prev;
336
+ });
337
+ }
338
+ finally {
339
+ setLoading(false);
340
+ }
341
+ }, [client, items]);
342
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
343
+ (0, react_1.useEffect)(() => {
344
+ if (!client || !client.db || items.length === 0)
345
+ return;
346
+ refetch();
347
+ const tables = Array.from(new Set(items.map((i) => i.table).filter(Boolean)));
348
+ const unsubs = tables.map((tbl) => client.events.on(tbl, () => {
349
+ scheduleRefetch();
350
+ }));
351
+ return () => {
352
+ for (const unsub of unsubs) {
353
+ unsub();
354
+ }
355
+ };
356
+ }, [client, signature, refetch, scheduleRefetch]);
357
+ return { data, loading, refetch };
358
+ }
359
+ /**
360
+ * Reactive hook to query a single record by primary key with 0ms cache read-through.
361
+ * Automatically re-renders only when this specific row or table changes.
362
+ */
363
+ function useLiveRow(client, tableName, rowId, options) {
364
+ const cacheKey = `${tableName}:row:${rowId || 'null'}:${Boolean(options?.includeDeleted)}`;
365
+ const [data, setData] = (0, react_1.useState)(() => {
366
+ if (!rowId)
367
+ return null;
368
+ const cached = queryCache.get(cacheKey);
369
+ return cached && cached.length > 0 ? cached[0] : null;
370
+ });
371
+ const [loading, setLoading] = (0, react_1.useState)(() => Boolean(rowId && !queryCache.has(cacheKey)));
372
+ const refetch = (0, react_1.useCallback)(async () => {
373
+ if (!client || !client.db || !rowId) {
374
+ setData(null);
375
+ setLoading(false);
376
+ return;
377
+ }
378
+ try {
379
+ let inFlight = inFlightQueries.get(cacheKey);
380
+ if (!inFlight) {
381
+ inFlight = client.findById(tableName, rowId, options).then((r) => (r ? [r] : []));
382
+ inFlightQueries.set(cacheKey, inFlight);
383
+ inFlight.finally(() => inFlightQueries.delete(cacheKey));
384
+ }
385
+ const res = await inFlight;
386
+ const row = res.length > 0 ? res[0] : null;
387
+ if (row) {
388
+ queryCache.set(cacheKey, [row]);
98
389
  }
99
- if (records) {
100
- setPendingRecords(records);
101
- setPendingCount(records.filter((r) => r.status === 'PENDING').length);
390
+ else {
391
+ queryCache.delete(cacheKey);
392
+ }
393
+ setData((prev) => (areRowsEqual(prev, row) ? prev : row));
394
+ }
395
+ catch (err) {
396
+ const msg = String(err?.message || err || '');
397
+ if (msg.includes('no such column') || msg.includes('no such table')) {
398
+ setData(null);
102
399
  }
103
- if (lastSynced !== null && lastSynced !== undefined) {
104
- setLastSyncedAt(lastSynced > 0 ? lastSynced : null);
400
+ else {
401
+ console.error(`Error in useLiveRow("${tableName}", "${rowId}"):`, err);
105
402
  }
106
403
  }
107
- catch {
108
- // Ignored if client is stopping or tables initializing
404
+ finally {
405
+ setLoading(false);
109
406
  }
110
- }, [client]);
407
+ }, [client, tableName, rowId, cacheKey]);
408
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
111
409
  (0, react_1.useEffect)(() => {
112
- if (!client || !client.db)
410
+ if (!client || !client.db || !rowId)
113
411
  return;
114
- refresh();
115
- const unsubStatus = client.events.on('sync:status', (e) => {
116
- setIsOnline(e.isOnline);
117
- setIsPushing(e.isPushing);
118
- setIsPulling(e.isPulling);
119
- setPendingCount(e.pendingCount);
120
- });
121
- const unsubOutbox = client.events.on('sync_outbox', () => {
122
- refresh();
123
- });
124
- const unsubPush = client.events.on('sync:push', () => {
125
- refresh();
126
- });
127
- const unsubPull = client.events.on('sync:pull', () => {
128
- refresh();
412
+ refetch();
413
+ const unsubscribe = client.events.on(tableName, (event) => {
414
+ // Precise row-level invalidation: only refetch if event affects this rowId or is a bulk event
415
+ if (!event || !event.rowId || event.rowId === rowId || event.rowId === 'batch') {
416
+ scheduleRefetch();
417
+ }
129
418
  });
130
419
  return () => {
131
- unsubStatus();
132
- unsubOutbox();
133
- unsubPush();
134
- unsubPull();
420
+ unsubscribe();
135
421
  };
136
- }, [client, refresh]);
137
- return {
138
- isOnline,
139
- isPushing,
140
- isPulling,
141
- isSyncing: isPushing || isPulling,
142
- pendingCount,
143
- pendingRecords,
144
- lastSyncedAt,
145
- refresh,
146
- };
422
+ }, [client, tableName, rowId, refetch, scheduleRefetch]);
423
+ return { data, refetch, loading };
147
424
  }
148
425
  /**
149
- * Hook that provides real-time sync telemetry and engine performance metrics.
426
+ * Reactive infinite query hook that progressively loads pages of records
427
+ * with live updates when underlying table rows are inserted, updated, or synced.
150
428
  */
151
- function useSyncMetrics(client, pollIntervalMs = 3000) {
152
- const [metrics, setMetrics] = (0, react_1.useState)(null);
153
- const refresh = (0, react_1.useCallback)(async () => {
154
- if (!client)
429
+ function useLiveInfiniteQuery(client, tableName, options = {}) {
430
+ const pageSize = options.pageSize || options.limit || 25;
431
+ const [pageCount, setPageCount] = (0, react_1.useState)(1);
432
+ const [data, setData] = (0, react_1.useState)([]);
433
+ const [loading, setLoading] = (0, react_1.useState)(true);
434
+ const [isFetchingNextPage, setIsFetchingNextPage] = (0, react_1.useState)(false);
435
+ const [hasNextPage, setHasNextPage] = (0, react_1.useState)(true);
436
+ const optionsRef = (0, react_1.useRef)(options);
437
+ optionsRef.current = options;
438
+ const pageCountRef = (0, react_1.useRef)(pageCount);
439
+ pageCountRef.current = pageCount;
440
+ const loadData = (0, react_1.useCallback)(async (pages, isNextPage = false) => {
441
+ if (!client || !client.db)
155
442
  return;
443
+ if (isNextPage)
444
+ setIsFetchingNextPage(true);
156
445
  try {
157
- const data = await client.getMetrics();
158
- setMetrics(data);
446
+ const currentOpts = optionsRef.current;
447
+ const fetchLimit = pages * pageSize + 1; // +1 to check for hasNextPage
448
+ const rows = await client.findMany(tableName, {
449
+ ...currentOpts,
450
+ limit: fetchLimit,
451
+ offset: currentOpts.offset || 0,
452
+ });
453
+ if (rows.length > pages * pageSize) {
454
+ setHasNextPage(true);
455
+ setData(rows.slice(0, pages * pageSize));
456
+ }
457
+ else {
458
+ setHasNextPage(false);
459
+ setData(rows);
460
+ }
461
+ }
462
+ catch (err) {
463
+ const msg = String(err?.message || err || '');
464
+ if (msg.includes('no such column') || msg.includes('no such table')) {
465
+ setData([]);
466
+ setHasNextPage(false);
467
+ }
468
+ else {
469
+ console.error(`Error in useLiveInfiniteQuery("${tableName}"):`, err);
470
+ }
159
471
  }
160
- catch {
161
- // Ignored
472
+ finally {
473
+ setLoading(false);
474
+ if (isNextPage)
475
+ setIsFetchingNextPage(false);
162
476
  }
163
- }, [client]);
477
+ }, [client, tableName, pageSize]);
478
+ const refetch = (0, react_1.useCallback)(async () => {
479
+ await loadData(pageCountRef.current, false);
480
+ }, [loadData]);
481
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
482
+ const fetchNextPage = (0, react_1.useCallback)(async () => {
483
+ if (!hasNextPage || isFetchingNextPage)
484
+ return;
485
+ const nextPages = pageCountRef.current + 1;
486
+ setPageCount(nextPages);
487
+ await loadData(nextPages, true);
488
+ }, [hasNextPage, isFetchingNextPage, loadData]);
164
489
  (0, react_1.useEffect)(() => {
165
- if (!client)
490
+ if (!client || !client.db)
166
491
  return;
167
- refresh();
168
- // Listen to sync events for immediate refresh
169
- const unsubs = [
170
- client.events.on('sync:push', () => refresh()),
171
- client.events.on('sync:pull', () => refresh()),
172
- client.events.on('sync:status', () => refresh()),
173
- ];
174
- let timer;
175
- if (pollIntervalMs > 0) {
176
- timer = setInterval(refresh, pollIntervalMs);
177
- }
492
+ loadData(1, false);
493
+ const unsubscribe = client.events.on(tableName, () => {
494
+ scheduleRefetch();
495
+ });
178
496
  return () => {
179
- unsubs.forEach((u) => u());
180
- if (timer)
181
- clearInterval(timer);
497
+ unsubscribe();
182
498
  };
183
- }, [client, refresh, pollIntervalMs]);
184
- return { metrics, refresh };
499
+ }, [client, tableName, loadData, scheduleRefetch]);
500
+ return {
501
+ data,
502
+ loading,
503
+ isFetchingNextPage,
504
+ hasNextPage,
505
+ fetchNextPage,
506
+ refetch,
507
+ };
508
+ }
509
+ /**
510
+ * Universal mutations hook providing memoized, null-safe helpers for database operations.
511
+ */
512
+ function useMutations(client) {
513
+ return (0, react_1.useMemo)(() => ({
514
+ client,
515
+ insert: (table, payload, opts) => client ? client.insert(table, payload, opts) : Promise.resolve(undefined),
516
+ insertMany: (table, records, opts) => client ? client.insertMany(table, records, opts) : Promise.resolve(undefined),
517
+ update: (table, id, payload, opts) => client ? client.update(table, id, payload, opts) : Promise.resolve(),
518
+ updateMany: (table, updates, opts) => client ? client.updateMany(table, updates, opts) : Promise.resolve(),
519
+ delete: (table, id, opts) => client ? client.delete(table, id, opts) : Promise.resolve(),
520
+ batch: (mutations) => client ? client.batch(mutations) : Promise.resolve(undefined),
521
+ transaction: (action) => client ? client.transaction(action) : Promise.resolve(undefined),
522
+ }), [client]);
185
523
  }
524
+ __exportStar(require("./runner.js"), exports);
525
+ __exportStar(require("./status.js"), exports);
526
+ __exportStar(require("./theme.js"), exports);
527
+ __exportStar(require("./BenchmarkModal.js"), exports);
528
+ var SyncInspector_js_1 = require("./SyncInspector.js");
529
+ Object.defineProperty(exports, "SyncInspector", { enumerable: true, get: function () { return SyncInspector_js_1.SyncInspector; } });
530
+ Object.defineProperty(exports, "SyncIndicator", { enumerable: true, get: function () { return SyncInspector_js_1.SyncIndicator; } });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns a stable trigger that coalesces bursts of calls into a single run of `fn`.
3
+ * - Calls in the same tick collapse into one run scheduled on the next macrotask.
4
+ * - Calls while a run is in flight collapse into exactly one trailing run.
5
+ * This keeps reactive hooks from re-querying SQLite once per emitted row event.
6
+ */
7
+ export declare function useCoalescedRunner(fn: () => Promise<void>): () => void;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useCoalescedRunner = useCoalescedRunner;
4
+ const react_1 = require("react");
5
+ /**
6
+ * Returns a stable trigger that coalesces bursts of calls into a single run of `fn`.
7
+ * - Calls in the same tick collapse into one run scheduled on the next macrotask.
8
+ * - Calls while a run is in flight collapse into exactly one trailing run.
9
+ * This keeps reactive hooks from re-querying SQLite once per emitted row event.
10
+ */
11
+ function useCoalescedRunner(fn) {
12
+ const fnRef = (0, react_1.useRef)(fn);
13
+ fnRef.current = fn;
14
+ const state = (0, react_1.useRef)({
15
+ scheduled: false,
16
+ running: false,
17
+ rerun: false
18
+ });
19
+ return (0, react_1.useCallback)(() => {
20
+ const s = state.current;
21
+ if (s.running) {
22
+ s.rerun = true;
23
+ return;
24
+ }
25
+ if (s.scheduled)
26
+ return;
27
+ s.scheduled = true;
28
+ setTimeout(async () => {
29
+ s.scheduled = false;
30
+ s.running = true;
31
+ try {
32
+ do {
33
+ s.rerun = false;
34
+ await fnRef.current();
35
+ } while (s.rerun);
36
+ }
37
+ finally {
38
+ s.running = false;
39
+ }
40
+ }, 0);
41
+ }, []);
42
+ }