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.
- package/README.md +421 -362
- package/dist/adapters/local/better-sqlite3.d.ts +2 -0
- package/dist/adapters/local/better-sqlite3.js +46 -25
- package/dist/adapters/local/expo-sqlite.d.ts +5 -1
- package/dist/adapters/local/expo-sqlite.js +40 -7
- package/dist/adapters/local/index.d.ts +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +2 -0
- package/dist/adapters/local/node-sqlite.js +45 -22
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +6 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +5 -0
- package/dist/adapters/network/index.js +33 -0
- package/dist/adapters/remote/convex.d.ts +2 -2
- package/dist/adapters/remote/convex.js +8 -12
- package/dist/adapters/remote/demo.d.ts +1 -1
- package/dist/adapters/remote/demo.js +3 -0
- package/dist/adapters/remote/firestore.d.ts +3 -2
- package/dist/adapters/remote/firestore.js +69 -46
- package/dist/adapters/remote/index.d.ts +6 -4
- package/dist/adapters/remote/supabase.d.ts +34 -6
- package/dist/adapters/remote/supabase.js +340 -79
- package/dist/config/index.js +7 -10
- package/dist/config/resolve-local.js +12 -1
- package/dist/config/resolve-remote.js +33 -28
- package/dist/config/types.d.ts +30 -5
- package/dist/core/client.d.ts +86 -3
- package/dist/core/client.js +423 -41
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +29 -0
- package/dist/core/outbox-store.d.ts +8 -4
- package/dist/core/outbox-store.js +84 -65
- package/dist/core/query.d.ts +8 -0
- package/dist/core/query.js +37 -0
- package/dist/core/schema-manager.d.ts +9 -0
- package/dist/core/schema-manager.js +68 -0
- package/dist/core/sync-worker.d.ts +38 -1
- package/dist/core/sync-worker.js +351 -104
- package/dist/core/table-manager.js +26 -29
- package/dist/core/types.d.ts +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/react/BenchmarkModal.d.ts +32 -0
- package/dist/react/BenchmarkModal.js +777 -0
- package/dist/react/SyncInspector.d.ts +31 -0
- package/dist/react/SyncInspector.js +655 -0
- package/dist/react/index.d.ts +49 -19
- package/dist/react/index.js +266 -95
- package/dist/react/runner.d.ts +7 -0
- package/dist/react/runner.js +42 -0
- package/dist/react/status.d.ts +24 -0
- package/dist/react/status.js +123 -0
- package/dist/react/theme.d.ts +18 -0
- package/dist/react/theme.js +46 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +20 -0
- package/package.json +138 -133
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { LocalFirstClient } from '../core/client.js';
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
*
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
*
|
|
72
|
+
* Universal mutations hook providing memoized, null-safe helpers for database operations.
|
|
46
73
|
*/
|
|
47
|
-
export declare function
|
|
48
|
-
|
|
49
|
-
|
|
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';
|
package/dist/react/index.js
CHANGED
|
@@ -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.
|
|
5
|
-
exports.
|
|
6
|
-
exports.
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
36
|
-
let
|
|
37
|
-
if (
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
79
|
-
const
|
|
80
|
-
const [
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
97
|
-
const unsubscribe = client.events.on(
|
|
98
|
-
|
|
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,
|
|
104
|
-
return {
|
|
237
|
+
}, [client, tableName, rowId, refetch, scheduleRefetch]);
|
|
238
|
+
return { data, refetch, loading };
|
|
105
239
|
}
|
|
106
240
|
/**
|
|
107
|
-
*
|
|
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
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
(0, react_1.
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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
|
-
*
|
|
325
|
+
* Universal mutations hook providing memoized, null-safe helpers for database operations.
|
|
139
326
|
*/
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
};
|