local-lite 0.0.2 → 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 (57) 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/index.d.ts +1 -1
  42. package/dist/index.js +0 -1
  43. package/dist/react/BenchmarkModal.d.ts +32 -0
  44. package/dist/react/BenchmarkModal.js +777 -0
  45. package/dist/react/SyncInspector.d.ts +31 -0
  46. package/dist/react/SyncInspector.js +655 -0
  47. package/dist/react/index.d.ts +49 -19
  48. package/dist/react/index.js +266 -95
  49. package/dist/react/runner.d.ts +7 -0
  50. package/dist/react/runner.js +42 -0
  51. package/dist/react/status.d.ts +24 -0
  52. package/dist/react/status.js +123 -0
  53. package/dist/react/theme.d.ts +18 -0
  54. package/dist/react/theme.js +46 -0
  55. package/dist/utils/logger.d.ts +4 -0
  56. package/dist/utils/logger.js +20 -0
  57. 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.
@@ -25,26 +29,52 @@ export declare function useLiveQuery<T = any>(client: LocalFirstClient | null |
25
29
  loading: boolean;
26
30
  };
27
31
  /**
28
- * Hook that tracks the status and pending items in sync_outbox in real time.
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.
29
34
  */
30
- export declare function useOutboxStatus(client: LocalFirstClient | null | undefined): {
31
- pendingCount: number;
32
- pendingRecords: any[];
33
- refresh: () => Promise<void>;
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;
34
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>;
52
+ }
35
53
  /**
36
- * Hook that tracks real-time sync connectivity and sync worker activity (pushing/pulling).
54
+ * Reactive infinite query hook that progressively loads pages of records
55
+ * with live updates when underlying table rows are inserted, updated, or synced.
37
56
  */
38
- export declare function useSyncStatus(client: LocalFirstClient | null | undefined): {
39
- isOnline: boolean;
40
- isPushing: boolean;
41
- isPulling: boolean;
42
- pendingCount: number;
43
- };
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
+ }
44
71
  /**
45
- * Hook that provides real-time sync telemetry and engine performance metrics.
72
+ * Universal mutations hook providing memoized, null-safe helpers for database operations.
46
73
  */
47
- export declare function useSyncMetrics(client: LocalFirstClient | null | undefined, pollIntervalMs?: number): {
48
- metrics: SyncMetrics | null;
49
- refresh: () => Promise<void>;
50
- };
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,10 +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.useOutboxStatus = useOutboxStatus;
5
- exports.useSyncStatus = useSyncStatus;
6
- exports.useSyncMetrics = useSyncMetrics;
20
+ exports.useLiveRow = useLiveRow;
21
+ exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
22
+ exports.useMutations = useMutations;
7
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
+ }
8
88
  /**
9
89
  * Hook to execute a live reactive table query that automatically re-runs
10
90
  * whenever the table is mutated locally or via incoming remote delta.
@@ -24,32 +104,50 @@ const react_1 = require("react");
24
104
  * const { data } = useLiveQuery(client, 'todos', (c) => c.db.execute('SELECT ...'));
25
105
  */
26
106
  function useLiveQuery(client, tableName, queryOrOptions) {
27
- const [data, setData] = (0, react_1.useState)([]);
28
- 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));
29
116
  const queryParamRef = (0, react_1.useRef)(queryOrOptions);
30
117
  queryParamRef.current = queryOrOptions;
31
118
  const refetch = (0, react_1.useCallback)(async () => {
32
119
  if (!client || !client.db)
33
120
  return;
34
121
  try {
35
- const param = queryParamRef.current;
36
- let res;
37
- if (typeof param === 'function') {
38
- 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
+ });
39
141
  }
40
- else if (typeof param === 'object' && param !== null) {
41
- res = await client.findMany(tableName, param);
42
- }
43
- else {
44
- res = await client.findMany(tableName);
45
- }
46
- setData(res || []);
142
+ const res = (await inFlight) || [];
143
+ queryCache.set(cacheKey, res);
144
+ setData((prev) => (areRowArraysEqual(prev, res) ? prev : res));
47
145
  }
48
146
  catch (err) {
49
147
  const msg = String(err?.message || err || '');
50
148
  if (msg.includes('no such column') || msg.includes('no such table')) {
51
149
  // Table or columns have not been populated yet on fresh database; default to empty list
52
- setData([]);
150
+ setData((prev) => (prev.length === 0 ? prev : []));
53
151
  }
54
152
  else {
55
153
  console.error(`Error in useLiveQuery("${tableName}"):`, err);
@@ -58,117 +156,190 @@ function useLiveQuery(client, tableName, queryOrOptions) {
58
156
  finally {
59
157
  setLoading(false);
60
158
  }
61
- }, [client, tableName]);
159
+ }, [client, tableName, cacheKey]);
160
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
62
161
  (0, react_1.useEffect)(() => {
63
162
  if (!client || !client.db)
64
163
  return;
65
164
  refetch();
66
165
  const unsubscribe = client.events.on(tableName, () => {
67
- refetch();
166
+ scheduleRefetch();
68
167
  });
69
168
  return () => {
70
169
  unsubscribe();
71
170
  };
72
- }, [client, tableName, refetch]);
171
+ }, [client, tableName, queryKey, refetch, scheduleRefetch]);
73
172
  return { data, refetch, loading };
74
173
  }
75
174
  /**
76
- * Hook that tracks the status and pending items in sync_outbox in real time.
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 useOutboxStatus(client) {
79
- const [pendingCount, setPendingCount] = (0, react_1.useState)(0);
80
- const [pendingRecords, setPendingRecords] = (0, react_1.useState)([]);
81
- const refresh = (0, react_1.useCallback)(async () => {
82
- 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);
83
191
  return;
192
+ }
84
193
  try {
85
- const records = await client.getOutboxRecords();
86
- setPendingRecords(records || []);
87
- setPendingCount(records.filter((r) => r.status === 'PENDING').length);
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));
199
+ }
200
+ const res = await inFlight;
201
+ const row = res.length > 0 ? res[0] : null;
202
+ if (row) {
203
+ queryCache.set(cacheKey, [row]);
204
+ }
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);
217
+ }
88
218
  }
89
- catch {
90
- // Ignored if table doesn't exist yet
219
+ finally {
220
+ setLoading(false);
91
221
  }
92
- }, [client]);
222
+ }, [client, tableName, rowId, cacheKey]);
223
+ const scheduleRefetch = (0, runner_js_1.useCoalescedRunner)(refetch);
93
224
  (0, react_1.useEffect)(() => {
94
- if (!client || !client.db)
225
+ if (!client || !client.db || !rowId)
95
226
  return;
96
- refresh();
97
- const unsubscribe = client.events.on('sync_outbox', () => {
98
- 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
+ }
99
233
  });
100
234
  return () => {
101
235
  unsubscribe();
102
236
  };
103
- }, [client, refresh]);
104
- return { pendingCount, pendingRecords, refresh };
237
+ }, [client, tableName, rowId, refetch, scheduleRefetch]);
238
+ return { data, refetch, loading };
105
239
  }
106
240
  /**
107
- * Hook that tracks real-time sync connectivity and sync worker activity (pushing/pulling).
241
+ * Reactive infinite query hook that progressively loads pages of records
242
+ * with live updates when underlying table rows are inserted, updated, or synced.
108
243
  */
109
- function useSyncStatus(client) {
110
- const [status, setStatus] = (0, react_1.useState)({
111
- isOnline: true,
112
- isPushing: false,
113
- isPulling: false,
114
- pendingCount: 0,
115
- });
116
- (0, react_1.useEffect)(() => {
117
- 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)
118
257
  return;
119
- // Fetch initial metrics
120
- client.getMetrics().then((m) => {
121
- setStatus({
122
- isOnline: m.isOnline,
123
- isPushing: m.isPushing,
124
- isPulling: m.isPulling,
125
- pendingCount: m.outboxPendingCount,
258
+ if (isNextPage)
259
+ setIsFetchingNextPage(true);
260
+ try {
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,
126
267
  });
127
- }).catch(() => { });
128
- const unsubscribe = client.events.on('sync:status', (e) => {
129
- setStatus(e);
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
+ }
286
+ }
287
+ finally {
288
+ setLoading(false);
289
+ if (isNextPage)
290
+ setIsFetchingNextPage(false);
291
+ }
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]);
304
+ (0, react_1.useEffect)(() => {
305
+ if (!client || !client.db)
306
+ return;
307
+ loadData(1, false);
308
+ const unsubscribe = client.events.on(tableName, () => {
309
+ scheduleRefetch();
130
310
  });
131
311
  return () => {
132
312
  unsubscribe();
133
313
  };
134
- }, [client]);
135
- return status;
314
+ }, [client, tableName, loadData, scheduleRefetch]);
315
+ return {
316
+ data,
317
+ loading,
318
+ isFetchingNextPage,
319
+ hasNextPage,
320
+ fetchNextPage,
321
+ refetch,
322
+ };
136
323
  }
137
324
  /**
138
- * Hook that provides real-time sync telemetry and engine performance metrics.
325
+ * Universal mutations hook providing memoized, null-safe helpers for database operations.
139
326
  */
140
- function useSyncMetrics(client, pollIntervalMs = 3000) {
141
- const [metrics, setMetrics] = (0, react_1.useState)(null);
142
- const refresh = (0, react_1.useCallback)(async () => {
143
- if (!client)
144
- return;
145
- try {
146
- const data = await client.getMetrics();
147
- setMetrics(data);
148
- }
149
- catch {
150
- // Ignored
151
- }
152
- }, [client]);
153
- (0, react_1.useEffect)(() => {
154
- if (!client)
155
- return;
156
- refresh();
157
- // Listen to sync events for immediate refresh
158
- const unsubs = [
159
- client.events.on('sync:push', () => refresh()),
160
- client.events.on('sync:pull', () => refresh()),
161
- client.events.on('sync:status', () => refresh()),
162
- ];
163
- let timer;
164
- if (pollIntervalMs > 0) {
165
- timer = setInterval(refresh, pollIntervalMs);
166
- }
167
- return () => {
168
- unsubs.forEach((u) => u());
169
- if (timer)
170
- clearInterval(timer);
171
- };
172
- }, [client, refresh, pollIntervalMs]);
173
- return { metrics, refresh };
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]);
174
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
+ };