local-lite 0.0.5 → 0.0.6
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 +4 -1
- package/dist/adapters/local/better-sqlite3.js +34 -14
- package/dist/adapters/local/expo-sqlite.d.ts +10 -2
- package/dist/adapters/local/expo-sqlite.js +36 -20
- package/dist/adapters/local/node-sqlite.d.ts +4 -1
- package/dist/adapters/local/node-sqlite.js +29 -12
- package/dist/adapters/remote/firestore.js +39 -32
- package/dist/adapters/remote/supabase.d.ts +5 -0
- package/dist/adapters/remote/supabase.js +192 -85
- package/dist/core/client.d.ts +0 -8
- package/dist/core/client.js +70 -86
- package/dist/core/outbox-store.d.ts +5 -0
- package/dist/core/outbox-store.js +29 -4
- package/dist/core/query-builder.d.ts +1 -0
- package/dist/core/query-builder.js +140 -71
- package/dist/core/query.js +28 -32
- package/dist/core/schema-manager.d.ts +4 -15
- package/dist/core/schema-manager.js +45 -135
- package/dist/core/sync-worker.js +7 -2
- package/dist/react/index.js +65 -52
- package/dist/react/status.d.ts +5 -0
- package/dist/react/status.js +20 -12
- package/dist/utils/uuid.d.ts +1 -1
- package/dist/utils/uuid.js +7 -16
- package/package.json +1 -1
package/dist/core/sync-worker.js
CHANGED
|
@@ -611,9 +611,12 @@ class SyncWorker {
|
|
|
611
611
|
const isLastChunk = i + DELTA_CHUNK_SIZE >= totalChanges;
|
|
612
612
|
const queuedEvents = [];
|
|
613
613
|
await this.db.transaction(async (tx) => {
|
|
614
|
+
// Pre-fetch active pending outbox records in 1 query for this chunk to eliminate N+1 bridge roundtrips
|
|
615
|
+
const pendingOutboxMap = await outbox_store_js_1.OutboxStore.getActivePendingMap(tx);
|
|
614
616
|
for (const change of chunk) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
+
const outboxKey = `${change.tableName}:${change.rowId}`;
|
|
618
|
+
// 1. Check in-memory map for pending local mutations for this (table_name, row_id)
|
|
619
|
+
const localPendingRecord = pendingOutboxMap.get(outboxKey);
|
|
617
620
|
// 2. Conflict resolution if there is a pending local change
|
|
618
621
|
if (localPendingRecord) {
|
|
619
622
|
// Only load the current local row when a resolver actually needs it.
|
|
@@ -640,11 +643,13 @@ class SyncWorker {
|
|
|
640
643
|
if (resolution === 'apply-remote') {
|
|
641
644
|
// Remote change wins: remove the superseded local pending outbox mutation
|
|
642
645
|
await outbox_store_js_1.OutboxStore.deleteRecord(tx, localPendingRecord.id);
|
|
646
|
+
pendingOutboxMap.delete(outboxKey);
|
|
643
647
|
}
|
|
644
648
|
else if (typeof resolution === 'object' && resolution.mergePayload) {
|
|
645
649
|
// Merge scenario
|
|
646
650
|
change.payload = resolution.mergePayload;
|
|
647
651
|
await outbox_store_js_1.OutboxStore.updatePayload(tx, localPendingRecord.id, resolution.mergePayload);
|
|
652
|
+
localPendingRecord.payload = resolution.mergePayload;
|
|
648
653
|
}
|
|
649
654
|
}
|
|
650
655
|
// 3. Apply remote change to local database
|
package/dist/react/index.js
CHANGED
|
@@ -33,14 +33,14 @@ function areRowsEqual(a, b) {
|
|
|
33
33
|
if (aObj.id !== undefined && bObj.id !== undefined && aObj.id !== bObj.id) {
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const aTs = aObj.updated_at ?? aObj.updatedAt;
|
|
37
|
+
const bTs = bObj.updated_at ?? bObj.updatedAt;
|
|
38
38
|
if (aObj.id !== undefined &&
|
|
39
39
|
bObj.id !== undefined &&
|
|
40
40
|
aObj.id === bObj.id &&
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
41
|
+
aTs !== undefined &&
|
|
42
|
+
bTs !== undefined) {
|
|
43
|
+
return aTs === bTs;
|
|
44
44
|
}
|
|
45
45
|
const aKeys = Object.keys(aObj);
|
|
46
46
|
const bKeys = Object.keys(bObj);
|
|
@@ -80,9 +80,50 @@ function areRowArraysEqual(a, b) {
|
|
|
80
80
|
}
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const MAX_QUERY_CACHE_SIZE = 100;
|
|
84
|
+
class LRUQueryCache {
|
|
85
|
+
map = new Map();
|
|
86
|
+
get(key) {
|
|
87
|
+
const val = this.map.get(key);
|
|
88
|
+
if (val !== undefined) {
|
|
89
|
+
this.map.delete(key);
|
|
90
|
+
this.map.set(key, val);
|
|
91
|
+
}
|
|
92
|
+
return val;
|
|
93
|
+
}
|
|
94
|
+
set(key, val) {
|
|
95
|
+
if (this.map.has(key)) {
|
|
96
|
+
this.map.delete(key);
|
|
97
|
+
}
|
|
98
|
+
else if (this.map.size >= MAX_QUERY_CACHE_SIZE) {
|
|
99
|
+
const oldestKey = this.map.keys().next().value;
|
|
100
|
+
if (oldestKey !== undefined) {
|
|
101
|
+
this.map.delete(oldestKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.map.set(key, val);
|
|
105
|
+
}
|
|
106
|
+
has(key) {
|
|
107
|
+
return this.map.has(key);
|
|
108
|
+
}
|
|
109
|
+
delete(key) {
|
|
110
|
+
return this.map.delete(key);
|
|
111
|
+
}
|
|
112
|
+
clear() {
|
|
113
|
+
this.map.clear();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// In-memory query result cache capped with LRU eviction to prevent memory leaks
|
|
117
|
+
const queryCache = new LRUQueryCache();
|
|
85
118
|
const inFlightQueries = new Map();
|
|
119
|
+
async function fetchDeduped(key, fn) {
|
|
120
|
+
let inFlight = inFlightQueries.get(key);
|
|
121
|
+
if (!inFlight) {
|
|
122
|
+
inFlight = fn().finally(() => inFlightQueries.delete(key));
|
|
123
|
+
inFlightQueries.set(key, inFlight);
|
|
124
|
+
}
|
|
125
|
+
return inFlight;
|
|
126
|
+
}
|
|
86
127
|
/**
|
|
87
128
|
* Clears the in-memory live query cache.
|
|
88
129
|
*/
|
|
@@ -124,27 +165,14 @@ function useLiveQuery(client, tableName, queryOrOptions) {
|
|
|
124
165
|
if (!client || !client.db)
|
|
125
166
|
return;
|
|
126
167
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
});
|
|
146
|
-
}
|
|
147
|
-
const res = (await inFlight) || [];
|
|
168
|
+
const res = await fetchDeduped(cacheKey, async () => {
|
|
169
|
+
const param = queryParamRef.current;
|
|
170
|
+
if (typeof param === 'function')
|
|
171
|
+
return (await param(client)) || [];
|
|
172
|
+
if (typeof param === 'object' && param !== null)
|
|
173
|
+
return await client.findMany(tableName, param);
|
|
174
|
+
return await client.findMany(tableName);
|
|
175
|
+
});
|
|
148
176
|
queryCache.set(cacheKey, res);
|
|
149
177
|
setData((prev) => (areRowArraysEqual(prev, res) ? prev : res));
|
|
150
178
|
}
|
|
@@ -291,26 +319,14 @@ function useLiveQueries(client, queries) {
|
|
|
291
319
|
return;
|
|
292
320
|
try {
|
|
293
321
|
const results = await Promise.all(items.map(async (item) => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (item.fn) {
|
|
322
|
+
try {
|
|
323
|
+
const rows = await fetchDeduped(item.cacheKey, async () => {
|
|
324
|
+
if (item.fn)
|
|
298
325
|
return (await item.fn(client)) || [];
|
|
299
|
-
|
|
300
|
-
else if (item.options) {
|
|
326
|
+
if (item.options)
|
|
301
327
|
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);
|
|
328
|
+
return await client.findMany(item.table);
|
|
310
329
|
});
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
const rows = (await inFlight) || [];
|
|
314
330
|
queryCache.set(item.cacheKey, rows);
|
|
315
331
|
return { key: item.key, rows };
|
|
316
332
|
}
|
|
@@ -376,13 +392,10 @@ function useLiveRow(client, tableName, rowId, options) {
|
|
|
376
392
|
return;
|
|
377
393
|
}
|
|
378
394
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
inFlight.finally(() => inFlightQueries.delete(cacheKey));
|
|
384
|
-
}
|
|
385
|
-
const res = await inFlight;
|
|
395
|
+
const res = await fetchDeduped(cacheKey, async () => {
|
|
396
|
+
const r = await client.findById(tableName, rowId, options);
|
|
397
|
+
return r ? [r] : [];
|
|
398
|
+
});
|
|
386
399
|
const row = res.length > 0 ? res[0] : null;
|
|
387
400
|
if (row) {
|
|
388
401
|
queryCache.set(cacheKey, [row]);
|
package/dist/react/status.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface UseSyncStatusResult {
|
|
|
13
13
|
/**
|
|
14
14
|
* Consolidated hook that tracks real-time sync connectivity, sync worker activity
|
|
15
15
|
* (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
|
|
16
|
+
*
|
|
17
|
+
* Performance notes:
|
|
18
|
+
* - sync:status events update isOnline/isPushing/isPulling/pendingCount in-place (no DB query).
|
|
19
|
+
* - The 50-record outbox fetch (pendingRecords) only runs on mount and on explicit refresh().
|
|
20
|
+
* - sync:push / sync:pull only trigger a lightweight pendingCount update via sync:status.
|
|
16
21
|
*/
|
|
17
22
|
export declare function useSyncStatus(client: LocalFirstClient | null | undefined): UseSyncStatusResult;
|
|
18
23
|
/**
|
package/dist/react/status.js
CHANGED
|
@@ -7,6 +7,11 @@ const runner_js_1 = require("./runner.js");
|
|
|
7
7
|
/**
|
|
8
8
|
* Consolidated hook that tracks real-time sync connectivity, sync worker activity
|
|
9
9
|
* (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
|
|
10
|
+
*
|
|
11
|
+
* Performance notes:
|
|
12
|
+
* - sync:status events update isOnline/isPushing/isPulling/pendingCount in-place (no DB query).
|
|
13
|
+
* - The 50-record outbox fetch (pendingRecords) only runs on mount and on explicit refresh().
|
|
14
|
+
* - sync:push / sync:pull only trigger a lightweight pendingCount update via sync:status.
|
|
10
15
|
*/
|
|
11
16
|
function useSyncStatus(client) {
|
|
12
17
|
const [isOnline, setIsOnline] = (0, react_1.useState)(true);
|
|
@@ -44,33 +49,36 @@ function useSyncStatus(client) {
|
|
|
44
49
|
// Ignored if client is stopping or tables initializing
|
|
45
50
|
}
|
|
46
51
|
}, [client]);
|
|
47
|
-
const scheduleRefresh = (0, runner_js_1.useCoalescedRunner)(refresh);
|
|
48
52
|
(0, react_1.useEffect)(() => {
|
|
49
53
|
if (!client || !client.db)
|
|
50
54
|
return;
|
|
55
|
+
// Initial full fetch on mount
|
|
51
56
|
refresh();
|
|
57
|
+
// sync:status fires after every push/pull cycle — update scalar fields directly,
|
|
58
|
+
// no DB round-trip needed. pendingCount comes from the event payload.
|
|
52
59
|
const unsubStatus = client.events.on('sync:status', (e) => {
|
|
53
60
|
setIsOnline(e.isOnline);
|
|
54
61
|
setIsPushing(e.isPushing);
|
|
55
62
|
setIsPulling(e.isPulling);
|
|
56
63
|
setPendingCount(e.pendingCount);
|
|
57
64
|
});
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// Only refresh lastSyncedAt after a completed pull (lightweight meta query only)
|
|
66
|
+
const unsubPull = client.events.on('sync:pull', async (e) => {
|
|
67
|
+
if (e?.success) {
|
|
68
|
+
try {
|
|
69
|
+
const ts = await client.getLastSyncedAt().catch(() => null);
|
|
70
|
+
if (ts !== null && ts !== undefined) {
|
|
71
|
+
setLastSyncedAt(ts > 0 ? ts : null);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { /* ignored */ }
|
|
75
|
+
}
|
|
66
76
|
});
|
|
67
77
|
return () => {
|
|
68
78
|
unsubStatus();
|
|
69
|
-
unsubOutbox();
|
|
70
|
-
unsubPush();
|
|
71
79
|
unsubPull();
|
|
72
80
|
};
|
|
73
|
-
}, [client, refresh
|
|
81
|
+
}, [client, refresh]);
|
|
74
82
|
return {
|
|
75
83
|
isOnline,
|
|
76
84
|
isPushing,
|
package/dist/utils/uuid.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from 'uuid';
|
|
2
2
|
/**
|
|
3
3
|
* Generates an RFC4122 v4 compliant UUID.
|
|
4
|
-
* Uses native crypto.randomUUID() when
|
|
4
|
+
* Uses native crypto.randomUUID() when available (detected once at import), with uuid.v4 fallback.
|
|
5
5
|
*/
|
|
6
6
|
export declare function generateUUID(): string;
|
|
7
7
|
export { uuidv4 };
|
package/dist/utils/uuid.js
CHANGED
|
@@ -4,24 +4,15 @@ exports.uuidv4 = void 0;
|
|
|
4
4
|
exports.generateUUID = generateUUID;
|
|
5
5
|
const uuid_1 = require("uuid");
|
|
6
6
|
Object.defineProperty(exports, "uuidv4", { enumerable: true, get: function () { return uuid_1.v4; } });
|
|
7
|
+
// Detect once at module load — avoids 3 typeof checks per UUID call
|
|
8
|
+
const nativeRandomUUID = typeof globalThis !== 'undefined' &&
|
|
9
|
+
typeof globalThis.crypto?.randomUUID === 'function'
|
|
10
|
+
? () => globalThis.crypto.randomUUID()
|
|
11
|
+
: null;
|
|
7
12
|
/**
|
|
8
13
|
* Generates an RFC4122 v4 compliant UUID.
|
|
9
|
-
* Uses native crypto.randomUUID() when
|
|
14
|
+
* Uses native crypto.randomUUID() when available (detected once at import), with uuid.v4 fallback.
|
|
10
15
|
*/
|
|
11
16
|
function generateUUID() {
|
|
12
|
-
|
|
13
|
-
if (typeof globalThis !== 'undefined' &&
|
|
14
|
-
globalThis.crypto &&
|
|
15
|
-
typeof globalThis.crypto.randomUUID === 'function') {
|
|
16
|
-
return globalThis.crypto.randomUUID();
|
|
17
|
-
}
|
|
18
|
-
return (0, uuid_1.v4)();
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
22
|
-
const r = (Math.random() * 16) | 0;
|
|
23
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
24
|
-
return v.toString(16);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
17
|
+
return nativeRandomUUID ? nativeRandomUUID() : (0, uuid_1.v4)();
|
|
27
18
|
}
|
package/package.json
CHANGED