local-lite 0.0.3 → 0.0.4

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 (55) 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 +86 -3
  28. package/dist/core/client.js +423 -41
  29. package/dist/core/index.d.ts +14 -0
  30. package/dist/core/index.js +29 -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.d.ts +8 -0
  34. package/dist/core/query.js +37 -0
  35. package/dist/core/schema-manager.d.ts +9 -0
  36. package/dist/core/schema-manager.js +68 -0
  37. package/dist/core/sync-worker.d.ts +38 -1
  38. package/dist/core/sync-worker.js +351 -104
  39. package/dist/core/table-manager.js +26 -29
  40. package/dist/core/types.d.ts +8 -0
  41. package/dist/react/BenchmarkModal.d.ts +32 -0
  42. package/dist/react/BenchmarkModal.js +777 -0
  43. package/dist/react/SyncInspector.d.ts +31 -0
  44. package/dist/react/SyncInspector.js +655 -0
  45. package/dist/react/index.d.ts +51 -19
  46. package/dist/react/index.js +265 -105
  47. package/dist/react/runner.d.ts +7 -0
  48. package/dist/react/runner.js +42 -0
  49. package/dist/react/status.d.ts +24 -0
  50. package/dist/react/status.js +123 -0
  51. package/dist/react/theme.d.ts +18 -0
  52. package/dist/react/theme.js +46 -0
  53. package/dist/utils/logger.d.ts +4 -0
  54. package/dist/utils/logger.js +20 -0
  55. package/package.json +138 -133
@@ -1,6 +1,10 @@
1
- import { LocalFirstClient } from '../core/client.js';
2
- import { QueryOptions, SyncMetrics } from '../core/types.js';
1
+ import { LocalFirstClient, TransactionalClient } from '../core/client.js';
2
+ import { BatchMutation, MutateOptions, QueryOptions } from '../core/types.js';
3
3
  export type LiveQueryFn<T> = (client: LocalFirstClient) => Promise<T[]> | T[];
4
+ /**
5
+ * Clears the in-memory live query cache.
6
+ */
7
+ export declare function clearLiveQueryCache(): void;
4
8
  /**
5
9
  * Hook to execute a live reactive table query that automatically re-runs
6
10
  * whenever the table is mutated locally or via incoming remote delta.
@@ -24,25 +28,53 @@ export declare function useLiveQuery<T = any>(client: LocalFirstClient | null |
24
28
  refetch: () => Promise<void>;
25
29
  loading: boolean;
26
30
  };
27
- export interface UseSyncStatusResult {
28
- isOnline: boolean;
29
- isPushing: boolean;
30
- isPulling: boolean;
31
- isSyncing: boolean;
32
- pendingCount: number;
33
- pendingRecords: any[];
34
- lastSyncedAt: number | null;
35
- refresh: () => Promise<void>;
31
+ /**
32
+ * Reactive hook to query a single record by primary key with 0ms cache read-through.
33
+ * Automatically re-renders only when this specific row or table changes.
34
+ */
35
+ export declare function useLiveRow<T = any>(client: LocalFirstClient | null | undefined, tableName: string, rowId: string | null | undefined, options?: {
36
+ includeDeleted?: boolean;
37
+ }): {
38
+ data: T | null;
39
+ refetch: () => Promise<void>;
40
+ loading: boolean;
41
+ };
42
+ export interface InfiniteQueryOptions extends QueryOptions {
43
+ pageSize?: number;
44
+ }
45
+ export interface UseLiveInfiniteQueryResult<T> {
46
+ data: T[];
47
+ loading: boolean;
48
+ isFetchingNextPage: boolean;
49
+ hasNextPage: boolean;
50
+ fetchNextPage: () => Promise<void>;
51
+ refetch: () => Promise<void>;
36
52
  }
37
53
  /**
38
- * Consolidated hook that tracks real-time sync connectivity, sync worker activity
39
- * (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
54
+ * Reactive infinite query hook that progressively loads pages of records
55
+ * with live updates when underlying table rows are inserted, updated, or synced.
40
56
  */
41
- export declare function useSyncStatus(client: LocalFirstClient | null | undefined): UseSyncStatusResult;
57
+ export declare function useLiveInfiniteQuery<T = any>(client: LocalFirstClient | null | undefined, tableName: string, options?: InfiniteQueryOptions): UseLiveInfiniteQueryResult<T>;
58
+ export interface UseMutationsResult<TTable extends string = string> {
59
+ client: LocalFirstClient | null | undefined;
60
+ insert: <T extends Record<string, any> = Record<string, any>>(tableName: TTable | (string & {}), payload: T, options?: MutateOptions) => Promise<string | undefined>;
61
+ insertMany: <T extends Record<string, any> = Record<string, any>>(tableName: TTable | (string & {}), records: T[], options?: MutateOptions) => Promise<string[] | undefined>;
62
+ update: <T extends Record<string, any> = Record<string, any>>(tableName: TTable | (string & {}), rowId: string, payload: Partial<T>, options?: MutateOptions) => Promise<void>;
63
+ updateMany: <T extends Record<string, any> = Record<string, any>>(tableName: TTable | (string & {}), updates: Array<{
64
+ id: string;
65
+ payload: Partial<T>;
66
+ }>, options?: MutateOptions) => Promise<void>;
67
+ delete: (tableName: TTable | (string & {}), rowId: string, options?: MutateOptions) => Promise<void>;
68
+ batch: (mutations: BatchMutation[]) => Promise<string[] | undefined>;
69
+ transaction: <T>(action: (txClient: TransactionalClient) => Promise<T>) => Promise<T | undefined>;
70
+ }
42
71
  /**
43
- * Hook that provides real-time sync telemetry and engine performance metrics.
72
+ * Universal mutations hook providing memoized, null-safe helpers for database operations.
44
73
  */
45
- export declare function useSyncMetrics(client: LocalFirstClient | null | undefined, pollIntervalMs?: number): {
46
- metrics: SyncMetrics | null;
47
- refresh: () => Promise<void>;
48
- };
74
+ export declare function useMutations<TTable extends string = string>(client: LocalFirstClient | null | undefined): UseMutationsResult<TTable>;
75
+ export * from './runner.js';
76
+ export * from './status.js';
77
+ export * from './theme.js';
78
+ export * from './BenchmarkModal.js';
79
+ export { SyncInspector, SyncIndicator } from './SyncInspector.js';
80
+ export type { SyncInspectorProps, SyncIndicatorProps } from './SyncInspector.js';
@@ -1,9 +1,90 @@
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.useLiveRow = useLiveRow;
21
+ exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
22
+ exports.useMutations = useMutations;
6
23
  const react_1 = require("react");
24
+ const runner_js_1 = require("./runner.js");
25
+ function areRowsEqual(a, b) {
26
+ if (a === b)
27
+ return true;
28
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
29
+ return false;
30
+ const aObj = a;
31
+ const bObj = b;
32
+ if (aObj.id !== undefined && bObj.id !== undefined && aObj.id !== bObj.id) {
33
+ return false;
34
+ }
35
+ if (aObj.updated_at !== undefined &&
36
+ bObj.updated_at !== undefined &&
37
+ aObj.updated_at !== bObj.updated_at) {
38
+ return false;
39
+ }
40
+ const aKeys = Object.keys(aObj);
41
+ const bKeys = Object.keys(bObj);
42
+ if (aKeys.length !== bKeys.length)
43
+ return false;
44
+ for (let i = 0; i < aKeys.length; i++) {
45
+ const k = aKeys[i];
46
+ const aVal = aObj[k];
47
+ const bVal = bObj[k];
48
+ if (aVal === bVal)
49
+ continue;
50
+ if (aVal &&
51
+ bVal &&
52
+ typeof aVal === 'object' &&
53
+ typeof bVal === 'object') {
54
+ if (Array.isArray(aVal) !== Array.isArray(bVal))
55
+ return false;
56
+ if (JSON.stringify(aVal) !== JSON.stringify(bVal))
57
+ return false;
58
+ continue;
59
+ }
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ function areRowArraysEqual(a, b) {
65
+ if (a === b)
66
+ return true;
67
+ if (!a || !b)
68
+ return false;
69
+ if (a.length !== b.length)
70
+ return false;
71
+ for (let i = 0; i < a.length; i++) {
72
+ if (!areRowsEqual(a[i], b[i])) {
73
+ return false;
74
+ }
75
+ }
76
+ return true;
77
+ }
78
+ // In-memory query result cache to eliminate initial render latency and flickering
79
+ const queryCache = new Map();
80
+ const inFlightQueries = new Map();
81
+ /**
82
+ * Clears the in-memory live query cache.
83
+ */
84
+ function clearLiveQueryCache() {
85
+ queryCache.clear();
86
+ inFlightQueries.clear();
87
+ }
7
88
  /**
8
89
  * Hook to execute a live reactive table query that automatically re-runs
9
90
  * whenever the table is mutated locally or via incoming remote delta.
@@ -23,32 +104,50 @@ const react_1 = require("react");
23
104
  * const { data } = useLiveQuery(client, 'todos', (c) => c.db.execute('SELECT ...'));
24
105
  */
25
106
  function useLiveQuery(client, tableName, queryOrOptions) {
26
- const [data, setData] = (0, react_1.useState)([]);
27
- const [loading, setLoading] = (0, react_1.useState)(true);
107
+ const queryKey = typeof queryOrOptions === 'object' && queryOrOptions !== null
108
+ ? JSON.stringify(queryOrOptions)
109
+ : typeof queryOrOptions === 'function'
110
+ ? 'custom'
111
+ : 'all';
112
+ const cacheKey = `${tableName}:${queryKey}`;
113
+ // Read-through cache: return instant cached rows on mount (0ms latency, zero flicker)
114
+ const [data, setData] = (0, react_1.useState)(() => queryCache.get(cacheKey) || []);
115
+ const [loading, setLoading] = (0, react_1.useState)(() => !queryCache.has(cacheKey));
28
116
  const queryParamRef = (0, react_1.useRef)(queryOrOptions);
29
117
  queryParamRef.current = queryOrOptions;
30
118
  const refetch = (0, react_1.useCallback)(async () => {
31
119
  if (!client || !client.db)
32
120
  return;
33
121
  try {
34
- const param = queryParamRef.current;
35
- let res;
36
- if (typeof param === 'function') {
37
- res = (await param(client)) || [];
122
+ // De-duplicate simultaneous identical in-flight queries
123
+ let inFlight = inFlightQueries.get(cacheKey);
124
+ if (!inFlight) {
125
+ inFlight = (async () => {
126
+ const param = queryParamRef.current;
127
+ if (typeof param === 'function') {
128
+ return (await param(client)) || [];
129
+ }
130
+ else if (typeof param === 'object' && param !== null) {
131
+ return await client.findMany(tableName, param);
132
+ }
133
+ else {
134
+ return await client.findMany(tableName);
135
+ }
136
+ })();
137
+ inFlightQueries.set(cacheKey, inFlight);
138
+ inFlight.finally(() => {
139
+ inFlightQueries.delete(cacheKey);
140
+ });
38
141
  }
39
- else if (typeof param === 'object' && param !== null) {
40
- res = await client.findMany(tableName, param);
41
- }
42
- else {
43
- res = await client.findMany(tableName);
44
- }
45
- setData(res || []);
142
+ const res = (await inFlight) || [];
143
+ queryCache.set(cacheKey, res);
144
+ setData((prev) => (areRowArraysEqual(prev, res) ? prev : res));
46
145
  }
47
146
  catch (err) {
48
147
  const msg = String(err?.message || err || '');
49
148
  if (msg.includes('no such column') || msg.includes('no such table')) {
50
149
  // Table or columns have not been populated yet on fresh database; default to empty list
51
- setData([]);
150
+ setData((prev) => (prev.length === 0 ? prev : []));
52
151
  }
53
152
  else {
54
153
  console.error(`Error in useLiveQuery("${tableName}"):`, err);
@@ -57,129 +156,190 @@ function useLiveQuery(client, tableName, queryOrOptions) {
57
156
  finally {
58
157
  setLoading(false);
59
158
  }
60
- }, [client, tableName]);
159
+ }, [client, tableName, cacheKey]);
160
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
61
161
  (0, react_1.useEffect)(() => {
62
162
  if (!client || !client.db)
63
163
  return;
64
164
  refetch();
65
165
  const unsubscribe = client.events.on(tableName, () => {
66
- refetch();
166
+ scheduleRefetch();
67
167
  });
68
168
  return () => {
69
169
  unsubscribe();
70
170
  };
71
- }, [client, tableName, refetch]);
171
+ }, [client, tableName, queryKey, refetch, scheduleRefetch]);
72
172
  return { data, refetch, loading };
73
173
  }
74
174
  /**
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.
175
+ * Reactive hook to query a single record by primary key with 0ms cache read-through.
176
+ * Automatically re-renders only when this specific row or table changes.
77
177
  */
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)
178
+ function useLiveRow(client, tableName, rowId, options) {
179
+ const cacheKey = `${tableName}:row:${rowId || 'null'}:${Boolean(options?.includeDeleted)}`;
180
+ const [data, setData] = (0, react_1.useState)(() => {
181
+ if (!rowId)
182
+ return null;
183
+ const cached = queryCache.get(cacheKey);
184
+ return cached && cached.length > 0 ? cached[0] : null;
185
+ });
186
+ const [loading, setLoading] = (0, react_1.useState)(() => Boolean(rowId && !queryCache.has(cacheKey)));
187
+ const refetch = (0, react_1.useCallback)(async () => {
188
+ if (!client || !client.db || !rowId) {
189
+ setData(null);
190
+ setLoading(false);
87
191
  return;
192
+ }
88
193
  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);
194
+ let inFlight = inFlightQueries.get(cacheKey);
195
+ if (!inFlight) {
196
+ inFlight = client.findById(tableName, rowId, options).then((r) => (r ? [r] : []));
197
+ inFlightQueries.set(cacheKey, inFlight);
198
+ inFlight.finally(() => inFlightQueries.delete(cacheKey));
98
199
  }
99
- if (records) {
100
- setPendingRecords(records);
101
- setPendingCount(records.filter((r) => r.status === 'PENDING').length);
200
+ const res = await inFlight;
201
+ const row = res.length > 0 ? res[0] : null;
202
+ if (row) {
203
+ queryCache.set(cacheKey, [row]);
102
204
  }
103
- if (lastSynced !== null && lastSynced !== undefined) {
104
- setLastSyncedAt(lastSynced > 0 ? lastSynced : null);
205
+ else {
206
+ queryCache.delete(cacheKey);
207
+ }
208
+ setData((prev) => (areRowsEqual(prev, row) ? prev : row));
209
+ }
210
+ catch (err) {
211
+ const msg = String(err?.message || err || '');
212
+ if (msg.includes('no such column') || msg.includes('no such table')) {
213
+ setData(null);
214
+ }
215
+ else {
216
+ console.error(`Error in useLiveRow("${tableName}", "${rowId}"):`, err);
105
217
  }
106
218
  }
107
- catch {
108
- // Ignored if client is stopping or tables initializing
219
+ finally {
220
+ setLoading(false);
109
221
  }
110
- }, [client]);
222
+ }, [client, tableName, rowId, cacheKey]);
223
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
111
224
  (0, react_1.useEffect)(() => {
112
- if (!client || !client.db)
225
+ if (!client || !client.db || !rowId)
113
226
  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();
227
+ refetch();
228
+ const unsubscribe = client.events.on(tableName, (event) => {
229
+ // Precise row-level invalidation: only refetch if event affects this rowId or is a bulk event
230
+ if (!event || !event.rowId || event.rowId === rowId || event.rowId === 'batch') {
231
+ scheduleRefetch();
232
+ }
129
233
  });
130
234
  return () => {
131
- unsubStatus();
132
- unsubOutbox();
133
- unsubPush();
134
- unsubPull();
235
+ unsubscribe();
135
236
  };
136
- }, [client, refresh]);
137
- return {
138
- isOnline,
139
- isPushing,
140
- isPulling,
141
- isSyncing: isPushing || isPulling,
142
- pendingCount,
143
- pendingRecords,
144
- lastSyncedAt,
145
- refresh,
146
- };
237
+ }, [client, tableName, rowId, refetch, scheduleRefetch]);
238
+ return { data, refetch, loading };
147
239
  }
148
240
  /**
149
- * Hook that provides real-time sync telemetry and engine performance metrics.
241
+ * Reactive infinite query hook that progressively loads pages of records
242
+ * with live updates when underlying table rows are inserted, updated, or synced.
150
243
  */
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)
244
+ function useLiveInfiniteQuery(client, tableName, options = {}) {
245
+ const pageSize = options.pageSize || options.limit || 25;
246
+ const [pageCount, setPageCount] = (0, react_1.useState)(1);
247
+ const [data, setData] = (0, react_1.useState)([]);
248
+ const [loading, setLoading] = (0, react_1.useState)(true);
249
+ const [isFetchingNextPage, setIsFetchingNextPage] = (0, react_1.useState)(false);
250
+ const [hasNextPage, setHasNextPage] = (0, react_1.useState)(true);
251
+ const optionsRef = (0, react_1.useRef)(options);
252
+ optionsRef.current = options;
253
+ const pageCountRef = (0, react_1.useRef)(pageCount);
254
+ pageCountRef.current = pageCount;
255
+ const loadData = (0, react_1.useCallback)(async (pages, isNextPage = false) => {
256
+ if (!client || !client.db)
155
257
  return;
258
+ if (isNextPage)
259
+ setIsFetchingNextPage(true);
156
260
  try {
157
- const data = await client.getMetrics();
158
- setMetrics(data);
261
+ const currentOpts = optionsRef.current;
262
+ const fetchLimit = pages * pageSize + 1; // +1 to check for hasNextPage
263
+ const rows = await client.findMany(tableName, {
264
+ ...currentOpts,
265
+ limit: fetchLimit,
266
+ offset: currentOpts.offset || 0,
267
+ });
268
+ if (rows.length > pages * pageSize) {
269
+ setHasNextPage(true);
270
+ setData(rows.slice(0, pages * pageSize));
271
+ }
272
+ else {
273
+ setHasNextPage(false);
274
+ setData(rows);
275
+ }
276
+ }
277
+ catch (err) {
278
+ const msg = String(err?.message || err || '');
279
+ if (msg.includes('no such column') || msg.includes('no such table')) {
280
+ setData([]);
281
+ setHasNextPage(false);
282
+ }
283
+ else {
284
+ console.error(`Error in useLiveInfiniteQuery("${tableName}"):`, err);
285
+ }
159
286
  }
160
- catch {
161
- // Ignored
287
+ finally {
288
+ setLoading(false);
289
+ if (isNextPage)
290
+ setIsFetchingNextPage(false);
162
291
  }
163
- }, [client]);
292
+ }, [client, tableName, pageSize]);
293
+ const refetch = (0, react_1.useCallback)(async () => {
294
+ await loadData(pageCountRef.current, false);
295
+ }, [loadData]);
296
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
297
+ const fetchNextPage = (0, react_1.useCallback)(async () => {
298
+ if (!hasNextPage || isFetchingNextPage)
299
+ return;
300
+ const nextPages = pageCountRef.current + 1;
301
+ setPageCount(nextPages);
302
+ await loadData(nextPages, true);
303
+ }, [hasNextPage, isFetchingNextPage, loadData]);
164
304
  (0, react_1.useEffect)(() => {
165
- if (!client)
305
+ if (!client || !client.db)
166
306
  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
- }
307
+ loadData(1, false);
308
+ const unsubscribe = client.events.on(tableName, () => {
309
+ scheduleRefetch();
310
+ });
178
311
  return () => {
179
- unsubs.forEach((u) => u());
180
- if (timer)
181
- clearInterval(timer);
312
+ unsubscribe();
182
313
  };
183
- }, [client, refresh, pollIntervalMs]);
184
- return { metrics, refresh };
314
+ }, [client, tableName, loadData, scheduleRefetch]);
315
+ return {
316
+ data,
317
+ loading,
318
+ isFetchingNextPage,
319
+ hasNextPage,
320
+ fetchNextPage,
321
+ refetch,
322
+ };
323
+ }
324
+ /**
325
+ * Universal mutations hook providing memoized, null-safe helpers for database operations.
326
+ */
327
+ function useMutations(client) {
328
+ return (0, react_1.useMemo)(() => ({
329
+ client,
330
+ insert: (table, payload, opts) => client ? client.insert(table, payload, opts) : Promise.resolve(undefined),
331
+ insertMany: (table, records, opts) => client ? client.insertMany(table, records, opts) : Promise.resolve(undefined),
332
+ update: (table, id, payload, opts) => client ? client.update(table, id, payload, opts) : Promise.resolve(),
333
+ updateMany: (table, updates, opts) => client ? client.updateMany(table, updates, opts) : Promise.resolve(),
334
+ delete: (table, id, opts) => client ? client.delete(table, id, opts) : Promise.resolve(),
335
+ batch: (mutations) => client ? client.batch(mutations) : Promise.resolve(undefined),
336
+ transaction: (action) => client ? client.transaction(action) : Promise.resolve(undefined),
337
+ }), [client]);
185
338
  }
339
+ __exportStar(require("./runner.js"), exports);
340
+ __exportStar(require("./status.js"), exports);
341
+ __exportStar(require("./theme.js"), exports);
342
+ __exportStar(require("./BenchmarkModal.js"), exports);
343
+ var SyncInspector_js_1 = require("./SyncInspector.js");
344
+ Object.defineProperty(exports, "SyncInspector", { enumerable: true, get: function () { return SyncInspector_js_1.SyncInspector; } });
345
+ 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
+ }
@@ -0,0 +1,24 @@
1
+ import { LocalFirstClient } from '../core/client.js';
2
+ import { SyncMetrics } from '../core/types.js';
3
+ export interface UseSyncStatusResult {
4
+ isOnline: boolean;
5
+ isPushing: boolean;
6
+ isPulling: boolean;
7
+ isSyncing: boolean;
8
+ pendingCount: number;
9
+ pendingRecords: any[];
10
+ lastSyncedAt: number | null;
11
+ refresh: () => Promise<void>;
12
+ }
13
+ /**
14
+ * Consolidated hook that tracks real-time sync connectivity, sync worker activity
15
+ * (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
16
+ */
17
+ export declare function useSyncStatus(client: LocalFirstClient | null | undefined): UseSyncStatusResult;
18
+ /**
19
+ * Hook that provides real-time sync telemetry and engine performance metrics.
20
+ */
21
+ export declare function useSyncMetrics(client: LocalFirstClient | null | undefined, pollIntervalMs?: number): {
22
+ metrics: SyncMetrics | null;
23
+ refresh: () => Promise<void>;
24
+ };