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.
@@ -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
  }
@@ -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
- await this.handlePushFailure(pendingRecords, 'Remote push returned false');
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, error?.message || String(error), error);
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
- const backoffDelay = Math.min((this.retryConfig.initialDelayMs || 1000) *
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;
@@ -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?: Record<string, any>;
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);
@@ -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
- * Hook that tracks the status and pending items in sync_outbox in real time.
29
- */
30
- export declare function useOutboxStatus(client: LocalFirstClient | null | undefined): {
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
  };