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.
- 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 +133 -5
- package/dist/core/client.js +431 -46
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +30 -0
- package/dist/core/outbox-store.d.ts +8 -4
- package/dist/core/outbox-store.js +84 -65
- package/dist/core/query-builder.d.ts +119 -0
- package/dist/core/query-builder.js +372 -0
- 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 +103 -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 +2 -0
- package/dist/index.js +2 -0
- 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 +585 -0
- package/dist/react/index.d.ts +89 -18
- package/dist/react/index.js +450 -105
- 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/schema/generator.d.ts +49 -0
- package/dist/schema/generator.js +209 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +17 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +20 -0
- package/package.json +143 -133
package/dist/react/index.js
CHANGED
|
@@ -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.
|
|
5
|
-
exports.
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
35
|
-
let
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
400
|
+
else {
|
|
401
|
+
console.error(`Error in useLiveRow("${tableName}", "${rowId}"):`, err);
|
|
105
402
|
}
|
|
106
403
|
}
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
132
|
-
unsubOutbox();
|
|
133
|
-
unsubPush();
|
|
134
|
-
unsubPull();
|
|
420
|
+
unsubscribe();
|
|
135
421
|
};
|
|
136
|
-
}, [client,
|
|
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
|
-
*
|
|
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
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
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
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
180
|
-
if (timer)
|
|
181
|
-
clearInterval(timer);
|
|
497
|
+
unsubscribe();
|
|
182
498
|
};
|
|
183
|
-
}, [client,
|
|
184
|
-
return {
|
|
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
|
+
}
|