local-lite 0.0.1 → 0.0.3
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 +14 -2
- package/dist/core/client.d.ts +56 -3
- package/dist/core/client.js +212 -40
- package/dist/core/outbox-store.d.ts +32 -1
- package/dist/core/outbox-store.js +126 -2
- package/dist/core/query.d.ts +13 -0
- package/dist/core/query.js +150 -0
- package/dist/core/sync-worker.d.ts +16 -1
- package/dist/core/sync-worker.js +142 -4
- package/dist/core/types.d.ts +84 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +21 -7
- package/dist/react/index.js +94 -12
- package/package.json +3 -3
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchesField = matchesField;
|
|
4
|
+
exports.matchesWhere = matchesWhere;
|
|
5
|
+
exports.applyQueryOptions = applyQueryOptions;
|
|
6
|
+
/**
|
|
7
|
+
* Checks whether a single field value matches a field filter condition.
|
|
8
|
+
*/
|
|
9
|
+
function matchesField(fieldValue, filter) {
|
|
10
|
+
if (filter === undefined)
|
|
11
|
+
return true;
|
|
12
|
+
if (typeof filter === 'function') {
|
|
13
|
+
return filter(fieldValue);
|
|
14
|
+
}
|
|
15
|
+
// Direct primitive / reference equality
|
|
16
|
+
if (filter === null || typeof filter !== 'object') {
|
|
17
|
+
return fieldValue === filter;
|
|
18
|
+
}
|
|
19
|
+
// Operator object: $eq, $ne, $in, $nin, $gt, $gte, $lt, $lte, $contains, etc.
|
|
20
|
+
const ops = filter;
|
|
21
|
+
if (ops.$eq !== undefined && fieldValue !== ops.$eq) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (ops.$ne !== undefined && fieldValue === ops.$ne) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (ops.$in !== undefined) {
|
|
28
|
+
if (!Array.isArray(ops.$in) || !ops.$in.includes(fieldValue)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (ops.$nin !== undefined) {
|
|
33
|
+
if (Array.isArray(ops.$nin) && ops.$nin.includes(fieldValue)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (ops.$gt !== undefined && !(fieldValue > ops.$gt)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (ops.$gte !== undefined && !(fieldValue >= ops.$gte)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (ops.$lt !== undefined && !(fieldValue < ops.$lt)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (ops.$lte !== undefined && !(fieldValue <= ops.$lte)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (ops.$contains !== undefined) {
|
|
50
|
+
const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
|
|
51
|
+
const search = String(ops.$contains).toLowerCase();
|
|
52
|
+
if (!str.includes(search)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (ops.$startsWith !== undefined) {
|
|
57
|
+
const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
|
|
58
|
+
const prefix = String(ops.$startsWith).toLowerCase();
|
|
59
|
+
if (!str.startsWith(prefix)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (ops.$endsWith !== undefined) {
|
|
64
|
+
const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
|
|
65
|
+
const suffix = String(ops.$endsWith).toLowerCase();
|
|
66
|
+
if (!str.endsWith(suffix)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (ops.$null !== undefined) {
|
|
71
|
+
const isNull = fieldValue === null || fieldValue === undefined;
|
|
72
|
+
if (ops.$null !== isNull) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Evaluates whether a given row matches a WhereFilter.
|
|
80
|
+
*/
|
|
81
|
+
function matchesWhere(row, where) {
|
|
82
|
+
if (!where)
|
|
83
|
+
return true;
|
|
84
|
+
if (typeof where === 'function') {
|
|
85
|
+
return where(row);
|
|
86
|
+
}
|
|
87
|
+
// Handle $or combinator
|
|
88
|
+
if ('$or' in where && Array.isArray(where.$or)) {
|
|
89
|
+
const orMatches = where.$or.some((subWhere) => matchesWhere(row, subWhere));
|
|
90
|
+
if (!orMatches)
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Handle $and combinator
|
|
94
|
+
if ('$and' in where && Array.isArray(where.$and)) {
|
|
95
|
+
const andMatches = where.$and.every((subWhere) => matchesWhere(row, subWhere));
|
|
96
|
+
if (!andMatches)
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
// Field-level conditions
|
|
100
|
+
for (const [key, filter] of Object.entries(where)) {
|
|
101
|
+
if (key === '$or' || key === '$and')
|
|
102
|
+
continue;
|
|
103
|
+
if (!matchesField(row[key], filter)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Applies in-memory filtering, sorting, offset, and limit to an array of rows.
|
|
111
|
+
*/
|
|
112
|
+
function applyQueryOptions(rows, options) {
|
|
113
|
+
if (!rows || rows.length === 0)
|
|
114
|
+
return [];
|
|
115
|
+
let result = [...rows];
|
|
116
|
+
// 1. Where filtering
|
|
117
|
+
if (options?.where) {
|
|
118
|
+
result = result.filter((row) => matchesWhere(row, options.where));
|
|
119
|
+
}
|
|
120
|
+
// 2. Order by
|
|
121
|
+
if (options?.orderBy) {
|
|
122
|
+
const key = options.orderBy;
|
|
123
|
+
const isAsc = (options.order || 'asc').toLowerCase() === 'asc';
|
|
124
|
+
result.sort((a, b) => {
|
|
125
|
+
const valA = a[key];
|
|
126
|
+
const valB = b[key];
|
|
127
|
+
if (valA === valB)
|
|
128
|
+
return 0;
|
|
129
|
+
if (valA === null || valA === undefined)
|
|
130
|
+
return isAsc ? 1 : -1;
|
|
131
|
+
if (valB === null || valB === undefined)
|
|
132
|
+
return isAsc ? -1 : 1;
|
|
133
|
+
if (typeof valA === 'string' && typeof valB === 'string') {
|
|
134
|
+
return isAsc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
135
|
+
}
|
|
136
|
+
if (typeof valA === 'number' && typeof valB === 'number') {
|
|
137
|
+
return isAsc ? valA - valB : valB - valA;
|
|
138
|
+
}
|
|
139
|
+
return isAsc ? (valA > valB ? 1 : -1) : (valA < valB ? 1 : -1);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
// 3. Offset and Limit
|
|
143
|
+
if (options?.offset !== undefined && options.offset > 0) {
|
|
144
|
+
result = result.slice(options.offset);
|
|
145
|
+
}
|
|
146
|
+
if (options?.limit !== undefined && options.limit > 0) {
|
|
147
|
+
result = result.slice(0, options.limit);
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LocalDBAdapter } from '../adapters/local/index.js';
|
|
2
2
|
import { RemoteDBAdapter } from '../adapters/remote/index.js';
|
|
3
3
|
import { EventEmitter } from '../reactive/event-emitter.js';
|
|
4
|
-
import { ConflictResolver, ConflictStrategy, DeltaChanges, NetworkStatusProvider, RetryConfig } from './types.js';
|
|
4
|
+
import { ConflictResolver, ConflictStrategy, DeltaChanges, NetworkStatusProvider, RetryConfig, SyncMetrics } from './types.js';
|
|
5
5
|
import { SchemaManager } from './schema-manager.js';
|
|
6
6
|
import { SyncLogger, CustomLogger, LogLevel } from '../utils/logger.js';
|
|
7
7
|
export interface SyncWorkerOptions {
|
|
@@ -40,6 +40,16 @@ export declare class SyncWorker {
|
|
|
40
40
|
private lastPulledTimestamp;
|
|
41
41
|
private cursorLoaded;
|
|
42
42
|
private deltaQueue;
|
|
43
|
+
private totalPushedOperations;
|
|
44
|
+
private totalPulledDeltas;
|
|
45
|
+
private totalPushCycles;
|
|
46
|
+
private totalPullCycles;
|
|
47
|
+
private lastPushDurationMs;
|
|
48
|
+
private lastPullDurationMs;
|
|
49
|
+
private lastPushTimestamp;
|
|
50
|
+
private lastPullTimestamp;
|
|
51
|
+
private lastPushError;
|
|
52
|
+
private lastPullError;
|
|
43
53
|
constructor(db: LocalDBAdapter, remote: RemoteDBAdapter, events: EventEmitter, options?: SyncWorkerOptions);
|
|
44
54
|
/**
|
|
45
55
|
* Starts background interval push/pull loops and listens for network & realtime updates.
|
|
@@ -54,6 +64,7 @@ export declare class SyncWorker {
|
|
|
54
64
|
*/
|
|
55
65
|
schedulePush(delayMs?: number): void;
|
|
56
66
|
private isOnline;
|
|
67
|
+
private emitStatus;
|
|
57
68
|
/**
|
|
58
69
|
* Pushes pending outbox records to the remote database.
|
|
59
70
|
*/
|
|
@@ -71,4 +82,8 @@ export declare class SyncWorker {
|
|
|
71
82
|
private processDeltasLocally;
|
|
72
83
|
getLastPulledTimestamp(): Promise<number>;
|
|
73
84
|
resetSyncCursor(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Collects real-time engine telemetry snapshot.
|
|
87
|
+
*/
|
|
88
|
+
getMetrics(): Promise<SyncMetrics>;
|
|
74
89
|
}
|
package/dist/core/sync-worker.js
CHANGED
|
@@ -30,6 +30,17 @@ class SyncWorker {
|
|
|
30
30
|
lastPulledTimestamp = 0;
|
|
31
31
|
cursorLoaded = false;
|
|
32
32
|
deltaQueue = Promise.resolve();
|
|
33
|
+
// Telemetry metrics
|
|
34
|
+
totalPushedOperations = 0;
|
|
35
|
+
totalPulledDeltas = 0;
|
|
36
|
+
totalPushCycles = 0;
|
|
37
|
+
totalPullCycles = 0;
|
|
38
|
+
lastPushDurationMs = 0;
|
|
39
|
+
lastPullDurationMs = 0;
|
|
40
|
+
lastPushTimestamp = 0;
|
|
41
|
+
lastPullTimestamp = 0;
|
|
42
|
+
lastPushError = null;
|
|
43
|
+
lastPullError = null;
|
|
33
44
|
constructor(db, remote, events, options = {}) {
|
|
34
45
|
this.db = db;
|
|
35
46
|
this.remote = remote;
|
|
@@ -62,6 +73,7 @@ class SyncWorker {
|
|
|
62
73
|
initialDelayMs: 1000,
|
|
63
74
|
backoffMultiplier: 1.5,
|
|
64
75
|
maxDelayMs: 30000,
|
|
76
|
+
jitter: true,
|
|
65
77
|
};
|
|
66
78
|
}
|
|
67
79
|
else if (typeof r === 'object' && r !== null) {
|
|
@@ -70,6 +82,7 @@ class SyncWorker {
|
|
|
70
82
|
initialDelayMs: r.initialDelayMs ?? 1000,
|
|
71
83
|
backoffMultiplier: r.backoffMultiplier ?? 1.5,
|
|
72
84
|
maxDelayMs: r.maxDelayMs ?? 30000,
|
|
85
|
+
jitter: r.jitter ?? true,
|
|
73
86
|
shouldRetry: r.shouldRetry,
|
|
74
87
|
};
|
|
75
88
|
}
|
|
@@ -79,6 +92,7 @@ class SyncWorker {
|
|
|
79
92
|
initialDelayMs: 1000,
|
|
80
93
|
backoffMultiplier: 1.5,
|
|
81
94
|
maxDelayMs: 30000,
|
|
95
|
+
jitter: true,
|
|
82
96
|
};
|
|
83
97
|
}
|
|
84
98
|
this.maxRetries = this.retryConfig.maxRetries ?? 5;
|
|
@@ -98,6 +112,7 @@ class SyncWorker {
|
|
|
98
112
|
});
|
|
99
113
|
// Subscribe to network connectivity changes
|
|
100
114
|
this.networkUnsubscribe = this.network.subscribe((online) => {
|
|
115
|
+
this.emitStatus();
|
|
101
116
|
if (online) {
|
|
102
117
|
this.schedulePush(0);
|
|
103
118
|
this.pullRemoteChanges().catch((err) => {
|
|
@@ -109,6 +124,7 @@ class SyncWorker {
|
|
|
109
124
|
this.pullRemoteChanges().catch((err) => {
|
|
110
125
|
this.logger.error('Initial pull failed:', err);
|
|
111
126
|
});
|
|
127
|
+
this.emitStatus();
|
|
112
128
|
}
|
|
113
129
|
/**
|
|
114
130
|
* Stops all active intervals, timers, and listeners.
|
|
@@ -153,6 +169,22 @@ class SyncWorker {
|
|
|
153
169
|
return true;
|
|
154
170
|
}
|
|
155
171
|
}
|
|
172
|
+
async emitStatus() {
|
|
173
|
+
try {
|
|
174
|
+
const online = await this.isOnline();
|
|
175
|
+
const counts = await outbox_store_js_1.OutboxStore.getCounts(this.db);
|
|
176
|
+
const statusEvent = {
|
|
177
|
+
isOnline: online,
|
|
178
|
+
isPushing: this.isPushing,
|
|
179
|
+
isPulling: this.isPulling,
|
|
180
|
+
pendingCount: counts.pending,
|
|
181
|
+
};
|
|
182
|
+
this.events.emit('sync:status', statusEvent);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Ignored
|
|
186
|
+
}
|
|
187
|
+
}
|
|
156
188
|
/**
|
|
157
189
|
* Pushes pending outbox records to the remote database.
|
|
158
190
|
*/
|
|
@@ -162,8 +194,11 @@ class SyncWorker {
|
|
|
162
194
|
if (!(await this.isOnline()))
|
|
163
195
|
return;
|
|
164
196
|
this.isPushing = true;
|
|
197
|
+
this.emitStatus();
|
|
198
|
+
const startTime = Date.now();
|
|
165
199
|
let pendingRecords = [];
|
|
166
200
|
try {
|
|
201
|
+
this.totalPushCycles++;
|
|
167
202
|
pendingRecords = await outbox_store_js_1.OutboxStore.getPending(this.db, this.maxRetries);
|
|
168
203
|
if (pendingRecords.length === 0) {
|
|
169
204
|
return;
|
|
@@ -180,23 +215,57 @@ class SyncWorker {
|
|
|
180
215
|
// Mark records as PROCESSING
|
|
181
216
|
await outbox_store_js_1.OutboxStore.markProcessing(this.db, recordIds);
|
|
182
217
|
const success = await this.remote.pushChanges(operations);
|
|
218
|
+
const durationMs = Date.now() - startTime;
|
|
219
|
+
this.lastPushDurationMs = durationMs;
|
|
220
|
+
this.lastPushTimestamp = Date.now();
|
|
183
221
|
if (success) {
|
|
184
222
|
// Successful push: delete rows from outbox
|
|
185
223
|
await outbox_store_js_1.OutboxStore.deleteCompleted(this.db, recordIds);
|
|
224
|
+
this.totalPushedOperations += operations.length;
|
|
225
|
+
this.lastPushError = null;
|
|
226
|
+
const pushEvent = {
|
|
227
|
+
success: true,
|
|
228
|
+
count: operations.length,
|
|
229
|
+
durationMs,
|
|
230
|
+
error: null,
|
|
231
|
+
};
|
|
232
|
+
this.events.emit('sync:push', pushEvent);
|
|
186
233
|
this.events.emit('sync_outbox', { operation: 'DELETE', rowId: 'batch' });
|
|
187
234
|
}
|
|
188
235
|
else {
|
|
189
|
-
|
|
236
|
+
const errMsg = 'Remote push returned false';
|
|
237
|
+
this.lastPushError = errMsg;
|
|
238
|
+
const pushEvent = {
|
|
239
|
+
success: false,
|
|
240
|
+
count: operations.length,
|
|
241
|
+
durationMs,
|
|
242
|
+
error: errMsg,
|
|
243
|
+
};
|
|
244
|
+
this.events.emit('sync:push', pushEvent);
|
|
245
|
+
await this.handlePushFailure(pendingRecords, errMsg);
|
|
190
246
|
}
|
|
191
247
|
}
|
|
192
248
|
catch (error) {
|
|
249
|
+
const durationMs = Date.now() - startTime;
|
|
250
|
+
this.lastPushDurationMs = durationMs;
|
|
251
|
+
this.lastPushTimestamp = Date.now();
|
|
252
|
+
const errMsg = error?.message || String(error);
|
|
253
|
+
this.lastPushError = errMsg;
|
|
193
254
|
this.logger.error('Sync Engine Outbox Push Error:', error);
|
|
255
|
+
const pushEvent = {
|
|
256
|
+
success: false,
|
|
257
|
+
count: pendingRecords.length,
|
|
258
|
+
durationMs,
|
|
259
|
+
error: errMsg,
|
|
260
|
+
};
|
|
261
|
+
this.events.emit('sync:push', pushEvent);
|
|
194
262
|
if (pendingRecords.length > 0) {
|
|
195
|
-
await this.handlePushFailure(pendingRecords,
|
|
263
|
+
await this.handlePushFailure(pendingRecords, errMsg, error);
|
|
196
264
|
}
|
|
197
265
|
}
|
|
198
266
|
finally {
|
|
199
267
|
this.isPushing = false;
|
|
268
|
+
this.emitStatus();
|
|
200
269
|
}
|
|
201
270
|
}
|
|
202
271
|
async handlePushFailure(records, errorMessage, errorObj) {
|
|
@@ -212,12 +281,17 @@ class SyncWorker {
|
|
|
212
281
|
}
|
|
213
282
|
}
|
|
214
283
|
}
|
|
215
|
-
// Schedule next retry with exponential backoff
|
|
284
|
+
// Schedule next retry with exponential backoff & optional jitter
|
|
216
285
|
const currentRetry = Math.max(...records.map((r) => r.retryCount || 0));
|
|
217
286
|
const isInfinite = !Number.isFinite(this.maxRetries) || this.maxRetries === -1;
|
|
218
287
|
if (isInfinite || currentRetry < this.maxRetries) {
|
|
219
|
-
|
|
288
|
+
let backoffDelay = Math.min((this.retryConfig.initialDelayMs || 1000) *
|
|
220
289
|
Math.pow(this.retryConfig.backoffMultiplier || 1.5, currentRetry), this.retryConfig.maxDelayMs || 30000);
|
|
290
|
+
// Apply ±15% jitter if enabled to avoid synchronized retry stampedes
|
|
291
|
+
if (this.retryConfig.jitter !== false) {
|
|
292
|
+
const jitterFactor = 0.85 + Math.random() * 0.3;
|
|
293
|
+
backoffDelay = Math.round(backoffDelay * jitterFactor);
|
|
294
|
+
}
|
|
221
295
|
this.schedulePush(backoffDelay);
|
|
222
296
|
}
|
|
223
297
|
}
|
|
@@ -234,21 +308,52 @@ class SyncWorker {
|
|
|
234
308
|
if (!(await this.isOnline()))
|
|
235
309
|
return;
|
|
236
310
|
this.isPulling = true;
|
|
311
|
+
this.emitStatus();
|
|
312
|
+
const startTime = Date.now();
|
|
237
313
|
try {
|
|
314
|
+
this.totalPullCycles++;
|
|
238
315
|
if (!this.cursorLoaded) {
|
|
239
316
|
this.lastPulledTimestamp = await meta_store_js_1.MetaStore.getLastSyncedAt(this.db);
|
|
240
317
|
this.cursorLoaded = true;
|
|
241
318
|
}
|
|
242
319
|
const deltas = await this.remote.fetchChanges(this.lastPulledTimestamp);
|
|
320
|
+
const durationMs = Date.now() - startTime;
|
|
321
|
+
this.lastPullDurationMs = durationMs;
|
|
322
|
+
this.lastPullTimestamp = Date.now();
|
|
323
|
+
this.lastPullError = null;
|
|
324
|
+
const changesCount = deltas?.changes?.length || 0;
|
|
243
325
|
if (deltas) {
|
|
326
|
+
this.totalPulledDeltas += changesCount;
|
|
244
327
|
await this.applyDeltasLocally(deltas);
|
|
245
328
|
}
|
|
329
|
+
const pullEvent = {
|
|
330
|
+
success: true,
|
|
331
|
+
changesCount,
|
|
332
|
+
durationMs,
|
|
333
|
+
timestamp: this.lastPulledTimestamp,
|
|
334
|
+
error: null,
|
|
335
|
+
};
|
|
336
|
+
this.events.emit('sync:pull', pullEvent);
|
|
246
337
|
}
|
|
247
338
|
catch (error) {
|
|
339
|
+
const durationMs = Date.now() - startTime;
|
|
340
|
+
this.lastPullDurationMs = durationMs;
|
|
341
|
+
this.lastPullTimestamp = Date.now();
|
|
342
|
+
const errMsg = error?.message || String(error);
|
|
343
|
+
this.lastPullError = errMsg;
|
|
248
344
|
this.logger.error('Sync Engine Delta Pull Error:', error);
|
|
345
|
+
const pullEvent = {
|
|
346
|
+
success: false,
|
|
347
|
+
changesCount: 0,
|
|
348
|
+
durationMs,
|
|
349
|
+
timestamp: this.lastPulledTimestamp,
|
|
350
|
+
error: errMsg,
|
|
351
|
+
};
|
|
352
|
+
this.events.emit('sync:pull', pullEvent);
|
|
249
353
|
}
|
|
250
354
|
finally {
|
|
251
355
|
this.isPulling = false;
|
|
356
|
+
this.emitStatus();
|
|
252
357
|
}
|
|
253
358
|
}
|
|
254
359
|
/**
|
|
@@ -286,6 +391,14 @@ class SyncWorker {
|
|
|
286
391
|
localOutboxRecord: localPendingRecord,
|
|
287
392
|
localRow,
|
|
288
393
|
}));
|
|
394
|
+
const resolutionStr = typeof resolution === 'string' ? resolution : 'merge';
|
|
395
|
+
const conflictEvent = {
|
|
396
|
+
tableName: change.tableName,
|
|
397
|
+
rowId: change.rowId,
|
|
398
|
+
resolution: resolutionStr,
|
|
399
|
+
timestamp: Date.now(),
|
|
400
|
+
};
|
|
401
|
+
this.events.emit('sync:conflict', conflictEvent);
|
|
289
402
|
if (resolution === 'keep-local') {
|
|
290
403
|
// Local offline change takes precedence
|
|
291
404
|
continue;
|
|
@@ -331,5 +444,30 @@ class SyncWorker {
|
|
|
331
444
|
this.lastPulledTimestamp = 0;
|
|
332
445
|
this.cursorLoaded = true;
|
|
333
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Collects real-time engine telemetry snapshot.
|
|
449
|
+
*/
|
|
450
|
+
async getMetrics() {
|
|
451
|
+
const counts = await outbox_store_js_1.OutboxStore.getCounts(this.db);
|
|
452
|
+
const online = await this.isOnline();
|
|
453
|
+
return {
|
|
454
|
+
outboxPendingCount: counts.pending,
|
|
455
|
+
outboxFailedCount: counts.failed,
|
|
456
|
+
outboxProcessingCount: counts.processing,
|
|
457
|
+
totalPushedOperations: this.totalPushedOperations,
|
|
458
|
+
totalPulledDeltas: this.totalPulledDeltas,
|
|
459
|
+
totalPushCycles: this.totalPushCycles,
|
|
460
|
+
totalPullCycles: this.totalPullCycles,
|
|
461
|
+
lastPushDurationMs: this.lastPushDurationMs,
|
|
462
|
+
lastPullDurationMs: this.lastPullDurationMs,
|
|
463
|
+
lastPushTimestamp: this.lastPushTimestamp,
|
|
464
|
+
lastPullTimestamp: this.lastPullTimestamp,
|
|
465
|
+
lastPushError: this.lastPushError,
|
|
466
|
+
lastPullError: this.lastPullError,
|
|
467
|
+
isOnline: online,
|
|
468
|
+
isPushing: this.isPushing,
|
|
469
|
+
isPulling: this.isPulling,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
334
472
|
}
|
|
335
473
|
exports.SyncWorker = SyncWorker;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface RetryConfig {
|
|
|
49
49
|
backoffMultiplier?: number;
|
|
50
50
|
/** Maximum capped delay between retries in milliseconds. Default: 30000 */
|
|
51
51
|
maxDelayMs?: number;
|
|
52
|
+
/** Whether to apply randomized jitter (±20%) to backoff delays to prevent thundering herd. Default: true */
|
|
53
|
+
jitter?: boolean;
|
|
52
54
|
/** Custom predicate deciding whether an error is retryable. Return false for non-retryable errors like permanent RLS violations. */
|
|
53
55
|
shouldRetry?: (error: any, operation: SyncOperation) => boolean;
|
|
54
56
|
}
|
|
@@ -64,10 +66,91 @@ export interface TableChangeEvent {
|
|
|
64
66
|
payload?: Record<string, any> | null;
|
|
65
67
|
source: 'local' | 'remote';
|
|
66
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Filter operator comparison options for query predicates.
|
|
71
|
+
*/
|
|
72
|
+
export interface ComparisonOperator<T = any> {
|
|
73
|
+
$eq?: T;
|
|
74
|
+
$ne?: T;
|
|
75
|
+
$gt?: T;
|
|
76
|
+
$gte?: T;
|
|
77
|
+
$lt?: T;
|
|
78
|
+
$lte?: T;
|
|
79
|
+
$in?: T[];
|
|
80
|
+
$nin?: T[];
|
|
81
|
+
$contains?: string;
|
|
82
|
+
$startsWith?: string;
|
|
83
|
+
$endsWith?: string;
|
|
84
|
+
$null?: boolean;
|
|
85
|
+
}
|
|
86
|
+
export type FieldFilter<T = any> = T | ComparisonOperator<T> | ((value: T) => boolean);
|
|
87
|
+
export type WhereFilter = Record<string, FieldFilter> | ((row: Record<string, any>) => boolean) | {
|
|
88
|
+
$or?: (Record<string, FieldFilter> | ((row: Record<string, any>) => boolean))[];
|
|
89
|
+
$and?: (Record<string, FieldFilter> | ((row: Record<string, any>) => boolean))[];
|
|
90
|
+
[key: string]: any;
|
|
91
|
+
};
|
|
67
92
|
export interface QueryOptions {
|
|
68
|
-
where?:
|
|
93
|
+
where?: WhereFilter;
|
|
69
94
|
orderBy?: string;
|
|
70
95
|
order?: 'asc' | 'desc' | 'ASC' | 'DESC';
|
|
71
96
|
limit?: number;
|
|
72
97
|
offset?: number;
|
|
73
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Individual mutation descriptor for atomic batch execution.
|
|
101
|
+
*/
|
|
102
|
+
export interface BatchMutation {
|
|
103
|
+
tableName: string;
|
|
104
|
+
rowId?: string;
|
|
105
|
+
operation: SyncOp;
|
|
106
|
+
payload?: Record<string, any> | null;
|
|
107
|
+
primaryKey?: string;
|
|
108
|
+
skipOutbox?: boolean;
|
|
109
|
+
customTimestamp?: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Real-time operational telemetry metrics captured by the sync engine.
|
|
113
|
+
*/
|
|
114
|
+
export interface SyncMetrics {
|
|
115
|
+
outboxPendingCount: number;
|
|
116
|
+
outboxFailedCount: number;
|
|
117
|
+
outboxProcessingCount: number;
|
|
118
|
+
totalPushedOperations: number;
|
|
119
|
+
totalPulledDeltas: number;
|
|
120
|
+
totalPushCycles: number;
|
|
121
|
+
totalPullCycles: number;
|
|
122
|
+
lastPushDurationMs: number;
|
|
123
|
+
lastPullDurationMs: number;
|
|
124
|
+
lastPushTimestamp: number;
|
|
125
|
+
lastPullTimestamp: number;
|
|
126
|
+
lastPushError?: string | null;
|
|
127
|
+
lastPullError?: string | null;
|
|
128
|
+
isOnline: boolean;
|
|
129
|
+
isPushing: boolean;
|
|
130
|
+
isPulling: boolean;
|
|
131
|
+
}
|
|
132
|
+
export interface SyncStatusEvent {
|
|
133
|
+
isOnline: boolean;
|
|
134
|
+
isPushing: boolean;
|
|
135
|
+
isPulling: boolean;
|
|
136
|
+
pendingCount: number;
|
|
137
|
+
}
|
|
138
|
+
export interface PushEvent {
|
|
139
|
+
success: boolean;
|
|
140
|
+
count: number;
|
|
141
|
+
durationMs: number;
|
|
142
|
+
error?: string | null;
|
|
143
|
+
}
|
|
144
|
+
export interface PullEvent {
|
|
145
|
+
success: boolean;
|
|
146
|
+
changesCount: number;
|
|
147
|
+
durationMs: number;
|
|
148
|
+
timestamp: number;
|
|
149
|
+
error?: string | null;
|
|
150
|
+
}
|
|
151
|
+
export interface ConflictEvent {
|
|
152
|
+
tableName: string;
|
|
153
|
+
rowId: string;
|
|
154
|
+
resolution: string;
|
|
155
|
+
timestamp: number;
|
|
156
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './core/client.js';
|
|
2
2
|
export * from './core/types.js';
|
|
3
|
+
export * from './core/query.js';
|
|
3
4
|
export * from './core/sync-worker.js';
|
|
4
5
|
export * from './core/conflict.js';
|
|
5
6
|
export * from './core/outbox-store.js';
|
|
@@ -11,7 +12,7 @@ export * from './adapters/local/index.js';
|
|
|
11
12
|
export * from './adapters/remote/index.js';
|
|
12
13
|
export * from './adapters/network/index.js';
|
|
13
14
|
export * from './config/index.js';
|
|
14
|
-
export * from './react/index.js';
|
|
15
|
+
export type * from './react/index.js';
|
|
15
16
|
export * from './utils/uuid.js';
|
|
16
17
|
export * from './utils/sql.js';
|
|
17
18
|
export * from './utils/logger.js';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./core/client.js"), exports);
|
|
18
18
|
__exportStar(require("./core/types.js"), exports);
|
|
19
|
+
__exportStar(require("./core/query.js"), exports);
|
|
19
20
|
__exportStar(require("./core/sync-worker.js"), exports);
|
|
20
21
|
__exportStar(require("./core/conflict.js"), exports);
|
|
21
22
|
__exportStar(require("./core/outbox-store.js"), exports);
|
|
@@ -27,7 +28,6 @@ __exportStar(require("./adapters/local/index.js"), exports);
|
|
|
27
28
|
__exportStar(require("./adapters/remote/index.js"), exports);
|
|
28
29
|
__exportStar(require("./adapters/network/index.js"), exports);
|
|
29
30
|
__exportStar(require("./config/index.js"), exports);
|
|
30
|
-
__exportStar(require("./react/index.js"), exports);
|
|
31
31
|
__exportStar(require("./utils/uuid.js"), exports);
|
|
32
32
|
__exportStar(require("./utils/sql.js"), exports);
|
|
33
33
|
__exportStar(require("./utils/logger.js"), exports);
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LocalFirstClient } from '../core/client.js';
|
|
2
|
-
import { QueryOptions } from '../core/types.js';
|
|
2
|
+
import { QueryOptions, SyncMetrics } from '../core/types.js';
|
|
3
3
|
export type LiveQueryFn<T> = (client: LocalFirstClient) => Promise<T[]> | T[];
|
|
4
4
|
/**
|
|
5
5
|
* Hook to execute a live reactive table query that automatically re-runs
|
|
@@ -9,9 +9,9 @@ export type LiveQueryFn<T> = (client: LocalFirstClient) => Promise<T[]> | T[];
|
|
|
9
9
|
* 1. Zero-SQL (all rows):
|
|
10
10
|
* const { data: todos } = useLiveQuery<Todo>(client, 'todos');
|
|
11
11
|
*
|
|
12
|
-
* 2. Declarative options (filtering & sorting without SQL):
|
|
12
|
+
* 2. Declarative options (filtering with rich operators & sorting without SQL):
|
|
13
13
|
* const { data: active } = useLiveQuery<Todo>(client, 'todos', {
|
|
14
|
-
* where: { completed: 0 },
|
|
14
|
+
* where: { completed: 0, priority: { $gte: 2 } },
|
|
15
15
|
* orderBy: 'created_at',
|
|
16
16
|
* order: 'desc'
|
|
17
17
|
* });
|
|
@@ -24,11 +24,25 @@ export declare function useLiveQuery<T = any>(client: LocalFirstClient | null |
|
|
|
24
24
|
refetch: () => Promise<void>;
|
|
25
25
|
loading: boolean;
|
|
26
26
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
export interface UseSyncStatusResult {
|
|
28
|
+
isOnline: boolean;
|
|
29
|
+
isPushing: boolean;
|
|
30
|
+
isPulling: boolean;
|
|
31
|
+
isSyncing: boolean;
|
|
31
32
|
pendingCount: number;
|
|
32
33
|
pendingRecords: any[];
|
|
34
|
+
lastSyncedAt: number | null;
|
|
35
|
+
refresh: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Consolidated hook that tracks real-time sync connectivity, sync worker activity
|
|
39
|
+
* (pushing/pulling/syncing), and pending items in the local SQLite outbox queue.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useSyncStatus(client: LocalFirstClient | null | undefined): UseSyncStatusResult;
|
|
42
|
+
/**
|
|
43
|
+
* Hook that provides real-time sync telemetry and engine performance metrics.
|
|
44
|
+
*/
|
|
45
|
+
export declare function useSyncMetrics(client: LocalFirstClient | null | undefined, pollIntervalMs?: number): {
|
|
46
|
+
metrics: SyncMetrics | null;
|
|
33
47
|
refresh: () => Promise<void>;
|
|
34
48
|
};
|