local-lite 0.0.6 → 0.0.7
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/dist/adapters/local/better-sqlite3.d.ts +1 -0
- package/dist/adapters/local/better-sqlite3.js +1 -0
- package/dist/adapters/local/expo-sqlite.d.ts +1 -0
- package/dist/adapters/local/expo-sqlite.js +1 -0
- package/dist/adapters/local/index.d.ts +2 -0
- package/dist/adapters/local/libsql.d.ts +1 -0
- package/dist/adapters/local/libsql.js +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +1 -0
- package/dist/adapters/local/node-sqlite.js +1 -0
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +1 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +1 -0
- package/dist/core/client.d.ts +1 -1
- package/dist/core/client.js +54 -20
- package/dist/react/index.js +61 -8
- package/package.json +5 -4
|
@@ -16,6 +16,7 @@ export interface BetterSqliteDatabase {
|
|
|
16
16
|
* Adapter for better-sqlite3 in Node.js, Electron, Next.js, and server environments.
|
|
17
17
|
*/
|
|
18
18
|
export declare class BetterSQLite3Adapter implements LocalDBAdapter {
|
|
19
|
+
readonly supportsSqlPushdown = true;
|
|
19
20
|
private db;
|
|
20
21
|
private txQueue;
|
|
21
22
|
private statementCache;
|
|
@@ -4,6 +4,7 @@ exports.ExpoSQLiteAdapter = void 0;
|
|
|
4
4
|
// Pre-compiled RegExp hoisted to module scope — 0 regex allocations per query
|
|
5
5
|
const READ_QUERY_REGEX = /^(?:SELECT|PRAGMA)\b/i;
|
|
6
6
|
class ExpoSQLiteAdapter {
|
|
7
|
+
supportsSqlPushdown = true;
|
|
7
8
|
db;
|
|
8
9
|
writeQueue = Promise.resolve();
|
|
9
10
|
constructor(expoDbInstance) {
|
|
@@ -3,6 +3,8 @@ export interface LocalTransaction {
|
|
|
3
3
|
}
|
|
4
4
|
export interface LocalDBAdapter {
|
|
5
5
|
execute(query: string, params?: any[]): Promise<any[]>;
|
|
6
|
+
/** True when execute runs real SQL with filtering, ordering, paging, and projection. */
|
|
7
|
+
readonly supportsSqlPushdown?: boolean;
|
|
6
8
|
transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
|
|
7
9
|
close?(): Promise<void> | void;
|
|
8
10
|
}
|
|
@@ -17,6 +17,7 @@ export interface LibSQLClient {
|
|
|
17
17
|
* Supports local SQLite files, embedded replicas, in-memory databases, and server/edge deployments.
|
|
18
18
|
*/
|
|
19
19
|
export declare class LibSQLAdapter implements LocalDBAdapter {
|
|
20
|
+
readonly supportsSqlPushdown = true;
|
|
20
21
|
private client;
|
|
21
22
|
constructor(libsqlClient: any);
|
|
22
23
|
execute(query: string, params?: any[]): Promise<any[]>;
|
|
@@ -28,6 +28,7 @@ function normalizeRows(res) {
|
|
|
28
28
|
* Supports local SQLite files, embedded replicas, in-memory databases, and server/edge deployments.
|
|
29
29
|
*/
|
|
30
30
|
class LibSQLAdapter {
|
|
31
|
+
supportsSqlPushdown = true;
|
|
31
32
|
client;
|
|
32
33
|
constructor(libsqlClient) {
|
|
33
34
|
this.client = libsqlClient;
|
|
@@ -17,6 +17,7 @@ export interface NodeSqliteDatabaseSync {
|
|
|
17
17
|
* Provides zero-dependency, native SQLite on modern Node.js runtimes.
|
|
18
18
|
*/
|
|
19
19
|
export declare class NodeSQLiteAdapter implements LocalDBAdapter {
|
|
20
|
+
readonly supportsSqlPushdown = true;
|
|
20
21
|
private db;
|
|
21
22
|
private txQueue;
|
|
22
23
|
private statementCache;
|
|
@@ -18,6 +18,7 @@ export interface OpSQLiteDatabase {
|
|
|
18
18
|
* Enables ultra-fast C++ JSI SQLite in bare React Native.
|
|
19
19
|
*/
|
|
20
20
|
export declare class OpSQLiteAdapter implements LocalDBAdapter {
|
|
21
|
+
readonly supportsSqlPushdown = true;
|
|
21
22
|
private db;
|
|
22
23
|
private txMutex;
|
|
23
24
|
constructor(opSqliteInstance: any);
|
|
@@ -19,6 +19,7 @@ export interface SqlJsDatabase {
|
|
|
19
19
|
* Adapter for sql.js (WebAssembly SQLite in Web Browsers, PWAs, and Expo Web).
|
|
20
20
|
*/
|
|
21
21
|
export declare class SqlJsAdapter implements LocalDBAdapter {
|
|
22
|
+
readonly supportsSqlPushdown = true;
|
|
22
23
|
private db;
|
|
23
24
|
constructor(sqlJsDatabase: any);
|
|
24
25
|
execute(query: string, params?: any[]): Promise<any[]>;
|
package/dist/core/client.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export declare class LocalFirstClient {
|
|
|
81
81
|
* Writes a mutation to the outbox table, optionally coalescing with existing PENDING records for the same row.
|
|
82
82
|
* Returns the affected outboxId, or undefined if an offline INSERT was deleted offline before pushing.
|
|
83
83
|
*/
|
|
84
|
-
recordOutboxMutation(tx: LocalTransaction, tableName: string, rowId: string, operation: SyncOp, payload: Record<string, any> | null, timestamp: number): Promise<string | undefined>;
|
|
84
|
+
recordOutboxMutation(tx: LocalTransaction, tableName: string, rowId: string, operation: SyncOp, payload: Record<string, any> | null, timestamp: number, pendingOutboxMap?: Map<string, OutboxRecord>): Promise<string | undefined>;
|
|
85
85
|
/**
|
|
86
86
|
* Initializes the client: creates sync_outbox schema, ensures autoSchema tables if enabled,
|
|
87
87
|
* applies migrations, and optionally starts automatic sync. Concurrent/repeated calls
|
package/dist/core/client.js
CHANGED
|
@@ -11,6 +11,18 @@ const uuid_js_1 = require("../utils/uuid.js");
|
|
|
11
11
|
const sql_js_1 = require("../utils/sql.js");
|
|
12
12
|
const query_js_1 = require("./query.js");
|
|
13
13
|
const query_builder_js_1 = require("./query-builder.js");
|
|
14
|
+
function hasFunctionFilter(value) {
|
|
15
|
+
if (typeof value === 'function')
|
|
16
|
+
return true;
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return value.some(hasFunctionFilter);
|
|
19
|
+
if (value && typeof value === 'object')
|
|
20
|
+
return Object.values(value).some(hasFunctionFilter);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function shouldUseClientSideQuery(options) {
|
|
24
|
+
return hasFunctionFilter(options?.where);
|
|
25
|
+
}
|
|
14
26
|
async function executeFindById(db, tableName, id, primaryKey, hydrateRow, includeDeleted) {
|
|
15
27
|
const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
|
|
16
28
|
const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
|
|
@@ -117,8 +129,11 @@ class TransactionalClient {
|
|
|
117
129
|
return executeFindByIds(this.tx, tableName, ids, this.parent.getPrimaryKey(), (t, r) => this.parent.hydrateRow(t, r), options?.includeDeleted);
|
|
118
130
|
}
|
|
119
131
|
async findMany(tableName, options) {
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
if (!this.parent.db.supportsSqlPushdown || shouldUseClientSideQuery(options)) {
|
|
133
|
+
const rows = await this.from(tableName).execute();
|
|
134
|
+
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
135
|
+
}
|
|
136
|
+
return this.from(tableName).applyOptions(options).execute();
|
|
122
137
|
}
|
|
123
138
|
async query(sql, params = []) {
|
|
124
139
|
return (await this.tx.execute(sql, params));
|
|
@@ -228,40 +243,52 @@ class LocalFirstClient {
|
|
|
228
243
|
* Writes a mutation to the outbox table, optionally coalescing with existing PENDING records for the same row.
|
|
229
244
|
* Returns the affected outboxId, or undefined if an offline INSERT was deleted offline before pushing.
|
|
230
245
|
*/
|
|
231
|
-
async recordOutboxMutation(tx, tableName, rowId, operation, payload, timestamp) {
|
|
246
|
+
async recordOutboxMutation(tx, tableName, rowId, operation, payload, timestamp, pendingOutboxMap) {
|
|
247
|
+
const pendingKey = tableName + ':' + rowId;
|
|
232
248
|
if (this.coalesceOutbox) {
|
|
233
|
-
const existing =
|
|
249
|
+
const existing = pendingOutboxMap
|
|
250
|
+
? pendingOutboxMap.get(pendingKey)
|
|
251
|
+
: await outbox_store_js_1.OutboxStore.findPendingByRow(tx, tableName, rowId);
|
|
234
252
|
if (existing && existing.status === 'PENDING') {
|
|
235
253
|
if (operation === 'DELETE') {
|
|
236
254
|
if (existing.operation === 'INSERT') {
|
|
237
|
-
// Created offline and deleted offline before ever being pushed to server: clean up outbox completely
|
|
238
255
|
await outbox_store_js_1.OutboxStore.deleteRecord(tx, existing.id);
|
|
256
|
+
pendingOutboxMap?.delete(pendingKey);
|
|
239
257
|
return undefined;
|
|
240
258
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
259
|
+
await outbox_store_js_1.OutboxStore.updateOperation(tx, existing.id, 'DELETE', null, timestamp);
|
|
260
|
+
if (pendingOutboxMap) {
|
|
261
|
+
existing.operation = 'DELETE';
|
|
262
|
+
existing.payload = null;
|
|
263
|
+
existing.timestamp = timestamp;
|
|
244
264
|
}
|
|
265
|
+
return existing.id;
|
|
245
266
|
}
|
|
246
|
-
|
|
267
|
+
if (operation === 'UPDATE') {
|
|
247
268
|
const merged = { ...(existing.payload || {}), ...(payload || {}) };
|
|
248
269
|
await outbox_store_js_1.OutboxStore.updatePayload(tx, existing.id, merged, timestamp);
|
|
270
|
+
if (pendingOutboxMap) {
|
|
271
|
+
existing.payload = merged;
|
|
272
|
+
existing.timestamp = timestamp;
|
|
273
|
+
}
|
|
249
274
|
return existing.id;
|
|
250
275
|
}
|
|
251
|
-
|
|
276
|
+
if (operation === 'INSERT') {
|
|
252
277
|
await outbox_store_js_1.OutboxStore.updateOperation(tx, existing.id, 'INSERT', payload, timestamp);
|
|
278
|
+
if (pendingOutboxMap) {
|
|
279
|
+
existing.operation = 'INSERT';
|
|
280
|
+
existing.payload = payload;
|
|
281
|
+
existing.timestamp = timestamp;
|
|
282
|
+
}
|
|
253
283
|
return existing.id;
|
|
254
284
|
}
|
|
255
285
|
}
|
|
256
286
|
}
|
|
257
287
|
const outboxId = (0, uuid_js_1.generateUUID)();
|
|
258
|
-
await outbox_store_js_1.OutboxStore.insertRecord(tx, {
|
|
259
|
-
|
|
260
|
-
tableName,
|
|
261
|
-
|
|
262
|
-
operation,
|
|
263
|
-
payload,
|
|
264
|
-
timestamp,
|
|
288
|
+
await outbox_store_js_1.OutboxStore.insertRecord(tx, { id: outboxId, tableName, rowId, operation, payload, timestamp });
|
|
289
|
+
pendingOutboxMap?.set(pendingKey, {
|
|
290
|
+
id: outboxId, tableName, rowId, operation, payload, timestamp,
|
|
291
|
+
status: 'PENDING', retryCount: 0, lastError: null,
|
|
265
292
|
});
|
|
266
293
|
return outboxId;
|
|
267
294
|
}
|
|
@@ -360,7 +387,11 @@ class LocalFirstClient {
|
|
|
360
387
|
const queuedEvents = [];
|
|
361
388
|
const queuedOutboxIds = [];
|
|
362
389
|
const pendingOutboxOps = [];
|
|
390
|
+
let pendingOutboxMap;
|
|
363
391
|
const executeInTx = async (tx) => {
|
|
392
|
+
if (this.coalesceOutbox && mutations.length > 1) {
|
|
393
|
+
pendingOutboxMap = await outbox_store_js_1.OutboxStore.getActivePendingMap(tx);
|
|
394
|
+
}
|
|
364
395
|
for (const m of mutations) {
|
|
365
396
|
const pk = m.primaryKey ?? options.primaryKey ?? this.primaryKey;
|
|
366
397
|
const payload = m.payload ?? null;
|
|
@@ -377,7 +408,7 @@ class LocalFirstClient {
|
|
|
377
408
|
if (!skipOutbox) {
|
|
378
409
|
const timestamp = m.customTimestamp ?? options.customTimestamp ?? Date.now();
|
|
379
410
|
if (this.coalesceOutbox && m.operation !== 'INSERT') {
|
|
380
|
-
const outboxId = await this.recordOutboxMutation(tx, m.tableName, rowId, m.operation, payload, timestamp);
|
|
411
|
+
const outboxId = await this.recordOutboxMutation(tx, m.tableName, rowId, m.operation, payload, timestamp, pendingOutboxMap);
|
|
381
412
|
if (outboxId) {
|
|
382
413
|
queuedOutboxIds.push(outboxId);
|
|
383
414
|
}
|
|
@@ -662,8 +693,11 @@ class LocalFirstClient {
|
|
|
662
693
|
return result;
|
|
663
694
|
}
|
|
664
695
|
async findMany(tableName, options) {
|
|
665
|
-
|
|
666
|
-
|
|
696
|
+
if (!this.db.supportsSqlPushdown || shouldUseClientSideQuery(options)) {
|
|
697
|
+
const rows = await this.from(tableName).execute();
|
|
698
|
+
return (0, query_js_1.applyQueryOptions)(rows, options);
|
|
699
|
+
}
|
|
700
|
+
return this.from(tableName).applyOptions(options).execute();
|
|
667
701
|
}
|
|
668
702
|
/**
|
|
669
703
|
* Finds a single record by primary key.
|
package/dist/react/index.js
CHANGED
|
@@ -116,6 +116,39 @@ class LRUQueryCache {
|
|
|
116
116
|
// In-memory query result cache capped with LRU eviction to prevent memory leaks
|
|
117
117
|
const queryCache = new LRUQueryCache();
|
|
118
118
|
const inFlightQueries = new Map();
|
|
119
|
+
const tableVersions = new WeakMap();
|
|
120
|
+
const clientIds = new WeakMap();
|
|
121
|
+
let nextClientId = 1;
|
|
122
|
+
function getClientScope(client) {
|
|
123
|
+
if (!client)
|
|
124
|
+
return 'no-client';
|
|
125
|
+
let id = clientIds.get(client);
|
|
126
|
+
if (!id) {
|
|
127
|
+
id = nextClientId++;
|
|
128
|
+
clientIds.set(client, id);
|
|
129
|
+
}
|
|
130
|
+
return 'client-' + id;
|
|
131
|
+
}
|
|
132
|
+
function getScopedCacheKey(client, key) {
|
|
133
|
+
return getClientScope(client) + ':' + key;
|
|
134
|
+
}
|
|
135
|
+
function getTableVersion(client, tableName) {
|
|
136
|
+
if (!client)
|
|
137
|
+
return 0;
|
|
138
|
+
return tableVersions.get(client)?.get(tableName) || 0;
|
|
139
|
+
}
|
|
140
|
+
function advanceTableVersion(client, tableName) {
|
|
141
|
+
if (!client)
|
|
142
|
+
return 0;
|
|
143
|
+
let versions = tableVersions.get(client);
|
|
144
|
+
if (!versions) {
|
|
145
|
+
versions = new Map();
|
|
146
|
+
tableVersions.set(client, versions);
|
|
147
|
+
}
|
|
148
|
+
const nextVersion = getTableVersion(client, tableName) + 1;
|
|
149
|
+
versions.set(tableName, nextVersion);
|
|
150
|
+
return nextVersion;
|
|
151
|
+
}
|
|
119
152
|
async function fetchDeduped(key, fn) {
|
|
120
153
|
let inFlight = inFlightQueries.get(key);
|
|
121
154
|
if (!inFlight) {
|
|
@@ -155,7 +188,7 @@ function useLiveQuery(client, tableName, queryOrOptions) {
|
|
|
155
188
|
: typeof queryOrOptions === 'function'
|
|
156
189
|
? 'custom'
|
|
157
190
|
: 'all';
|
|
158
|
-
const cacheKey = `${tableName}:${queryKey}
|
|
191
|
+
const cacheKey = getScopedCacheKey(client, `${tableName}:${queryKey}`);
|
|
159
192
|
// Read-through cache: return instant cached rows on mount (0ms latency, zero flicker)
|
|
160
193
|
const [data, setData] = (0, react_1.useState)(() => queryCache.get(cacheKey) || []);
|
|
161
194
|
const [loading, setLoading] = (0, react_1.useState)(() => !queryCache.has(cacheKey));
|
|
@@ -164,8 +197,9 @@ function useLiveQuery(client, tableName, queryOrOptions) {
|
|
|
164
197
|
const refetch = (0, react_1.useCallback)(async () => {
|
|
165
198
|
if (!client || !client.db)
|
|
166
199
|
return;
|
|
200
|
+
const versionAtStart = getTableVersion(client, tableName);
|
|
167
201
|
try {
|
|
168
|
-
const res = await fetchDeduped(cacheKey, async () => {
|
|
202
|
+
const res = await fetchDeduped(cacheKey + ':v' + versionAtStart, async () => {
|
|
169
203
|
const param = queryParamRef.current;
|
|
170
204
|
if (typeof param === 'function')
|
|
171
205
|
return (await param(client)) || [];
|
|
@@ -173,6 +207,8 @@ function useLiveQuery(client, tableName, queryOrOptions) {
|
|
|
173
207
|
return await client.findMany(tableName, param);
|
|
174
208
|
return await client.findMany(tableName);
|
|
175
209
|
});
|
|
210
|
+
if (versionAtStart !== getTableVersion(client, tableName))
|
|
211
|
+
return;
|
|
176
212
|
queryCache.set(cacheKey, res);
|
|
177
213
|
setData((prev) => (areRowArraysEqual(prev, res) ? prev : res));
|
|
178
214
|
}
|
|
@@ -196,6 +232,7 @@ function useLiveQuery(client, tableName, queryOrOptions) {
|
|
|
196
232
|
return;
|
|
197
233
|
refetch();
|
|
198
234
|
const unsubscribe = client.events.on(tableName, () => {
|
|
235
|
+
advanceTableVersion(client, tableName);
|
|
199
236
|
scheduleRefetch();
|
|
200
237
|
});
|
|
201
238
|
return () => {
|
|
@@ -297,10 +334,11 @@ function useLiveQueries(client, queries) {
|
|
|
297
334
|
return JSON.stringify(queries, (_, val) => (typeof val === 'function' ? 'fn' : val));
|
|
298
335
|
}, [queries]);
|
|
299
336
|
const items = (0, react_1.useMemo)(() => normalizeMultiQueries(queries), [signature]);
|
|
337
|
+
const clientScope = getClientScope(client);
|
|
300
338
|
const [data, setData] = (0, react_1.useState)(() => {
|
|
301
339
|
const initial = {};
|
|
302
340
|
for (const item of items) {
|
|
303
|
-
const cached = queryCache.get(item.cacheKey);
|
|
341
|
+
const cached = queryCache.get(clientScope + ':' + item.cacheKey);
|
|
304
342
|
initial[item.key] = cached ? cached : [];
|
|
305
343
|
}
|
|
306
344
|
return initial;
|
|
@@ -309,7 +347,7 @@ function useLiveQueries(client, queries) {
|
|
|
309
347
|
if (items.length === 0)
|
|
310
348
|
return false;
|
|
311
349
|
for (const item of items) {
|
|
312
|
-
if (!queryCache.has(item.cacheKey))
|
|
350
|
+
if (!queryCache.has(clientScope + ':' + item.cacheKey))
|
|
313
351
|
return true;
|
|
314
352
|
}
|
|
315
353
|
return false;
|
|
@@ -320,14 +358,18 @@ function useLiveQueries(client, queries) {
|
|
|
320
358
|
try {
|
|
321
359
|
const results = await Promise.all(items.map(async (item) => {
|
|
322
360
|
try {
|
|
323
|
-
const
|
|
361
|
+
const versionAtStart = getTableVersion(client, item.table);
|
|
362
|
+
const scopedCacheKey = clientScope + ':' + item.cacheKey;
|
|
363
|
+
const rows = await fetchDeduped(scopedCacheKey + ':v' + versionAtStart, async () => {
|
|
324
364
|
if (item.fn)
|
|
325
365
|
return (await item.fn(client)) || [];
|
|
326
366
|
if (item.options)
|
|
327
367
|
return await client.findMany(item.table, item.options);
|
|
328
368
|
return await client.findMany(item.table);
|
|
329
369
|
});
|
|
330
|
-
|
|
370
|
+
if (versionAtStart !== getTableVersion(client, item.table))
|
|
371
|
+
return null;
|
|
372
|
+
queryCache.set(scopedCacheKey, rows);
|
|
331
373
|
return { key: item.key, rows };
|
|
332
374
|
}
|
|
333
375
|
catch (err) {
|
|
@@ -343,6 +385,8 @@ function useLiveQueries(client, queries) {
|
|
|
343
385
|
let changed = false;
|
|
344
386
|
const next = { ...prev };
|
|
345
387
|
for (const res of results) {
|
|
388
|
+
if (!res)
|
|
389
|
+
continue;
|
|
346
390
|
if (!areRowArraysEqual(prev[res.key], res.rows)) {
|
|
347
391
|
next[res.key] = res.rows;
|
|
348
392
|
changed = true;
|
|
@@ -362,6 +406,7 @@ function useLiveQueries(client, queries) {
|
|
|
362
406
|
refetch();
|
|
363
407
|
const tables = Array.from(new Set(items.map((i) => i.table).filter(Boolean)));
|
|
364
408
|
const unsubs = tables.map((tbl) => client.events.on(tbl, () => {
|
|
409
|
+
advanceTableVersion(client, tbl);
|
|
365
410
|
scheduleRefetch();
|
|
366
411
|
}));
|
|
367
412
|
return () => {
|
|
@@ -377,7 +422,7 @@ function useLiveQueries(client, queries) {
|
|
|
377
422
|
* Automatically re-renders only when this specific row or table changes.
|
|
378
423
|
*/
|
|
379
424
|
function useLiveRow(client, tableName, rowId, options) {
|
|
380
|
-
const cacheKey = `${tableName}:row:${rowId || 'null'}:${Boolean(options?.includeDeleted)}
|
|
425
|
+
const cacheKey = getScopedCacheKey(client, `${tableName}:row:${rowId || 'null'}:${Boolean(options?.includeDeleted)}`);
|
|
381
426
|
const [data, setData] = (0, react_1.useState)(() => {
|
|
382
427
|
if (!rowId)
|
|
383
428
|
return null;
|
|
@@ -392,10 +437,13 @@ function useLiveRow(client, tableName, rowId, options) {
|
|
|
392
437
|
return;
|
|
393
438
|
}
|
|
394
439
|
try {
|
|
395
|
-
const
|
|
440
|
+
const versionAtStart = getTableVersion(client, tableName);
|
|
441
|
+
const res = await fetchDeduped(cacheKey + ':v' + versionAtStart, async () => {
|
|
396
442
|
const r = await client.findById(tableName, rowId, options);
|
|
397
443
|
return r ? [r] : [];
|
|
398
444
|
});
|
|
445
|
+
if (versionAtStart !== getTableVersion(client, tableName))
|
|
446
|
+
return;
|
|
399
447
|
const row = res.length > 0 ? res[0] : null;
|
|
400
448
|
if (row) {
|
|
401
449
|
queryCache.set(cacheKey, [row]);
|
|
@@ -424,6 +472,7 @@ function useLiveRow(client, tableName, rowId, options) {
|
|
|
424
472
|
return;
|
|
425
473
|
refetch();
|
|
426
474
|
const unsubscribe = client.events.on(tableName, (event) => {
|
|
475
|
+
advanceTableVersion(client, tableName);
|
|
427
476
|
// Precise row-level invalidation: only refetch if event affects this rowId or is a bulk event
|
|
428
477
|
if (!event || !event.rowId || event.rowId === rowId || event.rowId === 'batch') {
|
|
429
478
|
scheduleRefetch();
|
|
@@ -455,6 +504,7 @@ function useLiveInfiniteQuery(client, tableName, options = {}) {
|
|
|
455
504
|
return;
|
|
456
505
|
if (isNextPage)
|
|
457
506
|
setIsFetchingNextPage(true);
|
|
507
|
+
const versionAtStart = getTableVersion(client, tableName);
|
|
458
508
|
try {
|
|
459
509
|
const currentOpts = optionsRef.current;
|
|
460
510
|
const fetchLimit = pages * pageSize + 1; // +1 to check for hasNextPage
|
|
@@ -463,6 +513,8 @@ function useLiveInfiniteQuery(client, tableName, options = {}) {
|
|
|
463
513
|
limit: fetchLimit,
|
|
464
514
|
offset: currentOpts.offset || 0,
|
|
465
515
|
});
|
|
516
|
+
if (versionAtStart !== getTableVersion(client, tableName))
|
|
517
|
+
return;
|
|
466
518
|
if (rows.length > pages * pageSize) {
|
|
467
519
|
setHasNextPage(true);
|
|
468
520
|
setData(rows.slice(0, pages * pageSize));
|
|
@@ -504,6 +556,7 @@ function useLiveInfiniteQuery(client, tableName, options = {}) {
|
|
|
504
556
|
return;
|
|
505
557
|
loadData(1, false);
|
|
506
558
|
const unsubscribe = client.events.on(tableName, () => {
|
|
559
|
+
advanceTableVersion(client, tableName);
|
|
507
560
|
scheduleRefetch();
|
|
508
561
|
});
|
|
509
562
|
return () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-lite",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -129,8 +129,9 @@
|
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
"devDependencies": {
|
|
132
|
-
"@supabase/supabase-js": "^2.118.0",
|
|
133
|
-
"@types/
|
|
132
|
+
"@supabase/supabase-js": "^2.118.0",
|
|
133
|
+
"@types/node": "^26.6.5",
|
|
134
|
+
"@types/react": "^19.3.0",
|
|
134
135
|
"@types/uuid": "^11.0.0",
|
|
135
136
|
"expo-sqlite": "^58.0.13",
|
|
136
137
|
"react": "^19.3.1",
|
|
@@ -140,4 +141,4 @@
|
|
|
140
141
|
"dependencies": {
|
|
141
142
|
"uuid": "^14.0.3"
|
|
142
143
|
}
|
|
143
|
-
}
|
|
144
|
+
}
|