local-lite 0.0.0-stage → 0.0.1

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -2
  3. package/dist/adapters/local/better-sqlite3.d.ts +23 -0
  4. package/dist/adapters/local/better-sqlite3.js +46 -0
  5. package/dist/adapters/local/expo-sqlite.d.ts +8 -0
  6. package/dist/adapters/local/expo-sqlite.js +30 -0
  7. package/dist/adapters/local/index.d.ts +13 -0
  8. package/dist/adapters/local/index.js +22 -0
  9. package/dist/adapters/local/libsql.d.ts +24 -0
  10. package/dist/adapters/local/libsql.js +87 -0
  11. package/dist/adapters/local/node-sqlite.d.ts +24 -0
  12. package/dist/adapters/local/node-sqlite.js +44 -0
  13. package/dist/adapters/local/op-sqlite.d.ts +26 -0
  14. package/dist/adapters/local/op-sqlite.js +59 -0
  15. package/dist/adapters/local/sql-js.d.ts +26 -0
  16. package/dist/adapters/local/sql-js.js +63 -0
  17. package/dist/adapters/network/index.d.ts +11 -0
  18. package/dist/adapters/network/index.js +53 -0
  19. package/dist/adapters/remote/convex.d.ts +71 -0
  20. package/dist/adapters/remote/convex.js +185 -0
  21. package/dist/adapters/remote/demo.d.ts +21 -0
  22. package/dist/adapters/remote/demo.js +46 -0
  23. package/dist/adapters/remote/firestore.d.ts +64 -0
  24. package/dist/adapters/remote/firestore.js +302 -0
  25. package/dist/adapters/remote/index.d.ts +10 -0
  26. package/dist/adapters/remote/index.js +20 -0
  27. package/dist/adapters/remote/supabase.d.ts +97 -0
  28. package/dist/adapters/remote/supabase.js +276 -0
  29. package/dist/config/index.d.ts +24 -0
  30. package/dist/config/index.js +101 -0
  31. package/dist/config/resolve-local.d.ts +7 -0
  32. package/dist/config/resolve-local.js +68 -0
  33. package/dist/config/resolve-remote.d.ts +7 -0
  34. package/dist/config/resolve-remote.js +81 -0
  35. package/dist/config/types.d.ts +159 -0
  36. package/dist/config/types.js +2 -0
  37. package/dist/core/client.d.ts +100 -0
  38. package/dist/core/client.js +273 -0
  39. package/dist/core/conflict.d.ts +5 -0
  40. package/dist/core/conflict.js +38 -0
  41. package/dist/core/meta-store.d.ts +28 -0
  42. package/dist/core/meta-store.js +64 -0
  43. package/dist/core/outbox-store.d.ts +45 -0
  44. package/dist/core/outbox-store.js +141 -0
  45. package/dist/core/schema-manager.d.ts +39 -0
  46. package/dist/core/schema-manager.js +150 -0
  47. package/dist/core/sync-worker.d.ts +74 -0
  48. package/dist/core/sync-worker.js +335 -0
  49. package/dist/core/table-manager.d.ts +19 -0
  50. package/dist/core/table-manager.js +74 -0
  51. package/dist/core/types.d.ts +73 -0
  52. package/dist/core/types.js +2 -0
  53. package/dist/index.d.ts +17 -0
  54. package/dist/index.js +33 -0
  55. package/dist/react/index.d.ts +34 -0
  56. package/dist/react/index.js +103 -0
  57. package/dist/reactive/event-emitter.d.ts +7 -0
  58. package/dist/reactive/event-emitter.js +34 -0
  59. package/dist/utils/logger.d.ts +51 -0
  60. package/dist/utils/logger.js +169 -0
  61. package/dist/utils/sql.d.ts +4 -0
  62. package/dist/utils/sql.js +9 -0
  63. package/dist/utils/uuid.d.ts +7 -0
  64. package/dist/utils/uuid.js +27 -0
  65. package/package.json +130 -3
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SyncWorker = void 0;
4
+ const conflict_js_1 = require("./conflict.js");
5
+ const index_js_1 = require("../adapters/network/index.js");
6
+ const outbox_store_js_1 = require("./outbox-store.js");
7
+ const meta_store_js_1 = require("./meta-store.js");
8
+ const table_manager_js_1 = require("./table-manager.js");
9
+ const logger_js_1 = require("../utils/logger.js");
10
+ class SyncWorker {
11
+ db;
12
+ remote;
13
+ events;
14
+ network;
15
+ conflictResolver;
16
+ primaryKey;
17
+ autoSchema;
18
+ schemaManager;
19
+ logger;
20
+ retryConfig;
21
+ pushIntervalMs;
22
+ pullIntervalMs;
23
+ maxRetries;
24
+ pushIntervalTimer;
25
+ pullIntervalTimer;
26
+ pushDebounceTimer;
27
+ networkUnsubscribe;
28
+ isPushing = false;
29
+ isPulling = false;
30
+ lastPulledTimestamp = 0;
31
+ cursorLoaded = false;
32
+ deltaQueue = Promise.resolve();
33
+ constructor(db, remote, events, options = {}) {
34
+ this.db = db;
35
+ this.remote = remote;
36
+ this.events = events;
37
+ this.network = options.network ?? new index_js_1.DefaultNetworkStatusProvider();
38
+ this.conflictResolver = (0, conflict_js_1.getConflictResolver)(options.conflictResolver ?? 'last-write-wins');
39
+ this.primaryKey = options.primaryKey ?? 'id';
40
+ this.autoSchema = options.autoSchema ?? false;
41
+ this.schemaManager = options.schemaManager;
42
+ this.pushIntervalMs = options.pushIntervalMs ?? 5000;
43
+ this.pullIntervalMs = options.pullIntervalMs ?? 10000;
44
+ // Configure logger
45
+ if (options.logger instanceof logger_js_1.SyncLogger) {
46
+ this.logger = options.logger;
47
+ }
48
+ else if (typeof options.logger === 'boolean') {
49
+ this.logger = new logger_js_1.SyncLogger({ enabled: options.logger, level: options.logLevel });
50
+ }
51
+ else if (options.logger && typeof options.logger === 'object') {
52
+ this.logger = new logger_js_1.SyncLogger({ customLogger: options.logger, level: options.logLevel });
53
+ }
54
+ else {
55
+ this.logger = options.logLevel ? new logger_js_1.SyncLogger({ level: options.logLevel }) : logger_js_1.defaultLogger;
56
+ }
57
+ // Configure retries & backoff
58
+ const r = options.retry;
59
+ if (typeof r === 'number') {
60
+ this.retryConfig = {
61
+ maxRetries: r,
62
+ initialDelayMs: 1000,
63
+ backoffMultiplier: 1.5,
64
+ maxDelayMs: 30000,
65
+ };
66
+ }
67
+ else if (typeof r === 'object' && r !== null) {
68
+ this.retryConfig = {
69
+ maxRetries: r.maxRetries ?? options.maxRetries ?? 5,
70
+ initialDelayMs: r.initialDelayMs ?? 1000,
71
+ backoffMultiplier: r.backoffMultiplier ?? 1.5,
72
+ maxDelayMs: r.maxDelayMs ?? 30000,
73
+ shouldRetry: r.shouldRetry,
74
+ };
75
+ }
76
+ else {
77
+ this.retryConfig = {
78
+ maxRetries: options.maxRetries ?? 5,
79
+ initialDelayMs: 1000,
80
+ backoffMultiplier: 1.5,
81
+ maxDelayMs: 30000,
82
+ };
83
+ }
84
+ this.maxRetries = this.retryConfig.maxRetries ?? 5;
85
+ }
86
+ /**
87
+ * Starts background interval push/pull loops and listens for network & realtime updates.
88
+ */
89
+ start() {
90
+ this.stop();
91
+ this.pushIntervalTimer = setInterval(() => this.pushLocalChanges(), this.pushIntervalMs);
92
+ this.pullIntervalTimer = setInterval(() => this.pullRemoteChanges(), this.pullIntervalMs);
93
+ // Subscribe to live remote changes
94
+ this.remote.subscribeToLiveChanges((deltas) => {
95
+ this.applyDeltasLocally(deltas).catch((err) => {
96
+ this.logger.error('Error applying live remote deltas:', err);
97
+ });
98
+ });
99
+ // Subscribe to network connectivity changes
100
+ this.networkUnsubscribe = this.network.subscribe((online) => {
101
+ if (online) {
102
+ this.schedulePush(0);
103
+ this.pullRemoteChanges().catch((err) => {
104
+ this.logger.error('Error pulling changes on reconnect:', err);
105
+ });
106
+ }
107
+ });
108
+ // Initial pull
109
+ this.pullRemoteChanges().catch((err) => {
110
+ this.logger.error('Initial pull failed:', err);
111
+ });
112
+ }
113
+ /**
114
+ * Stops all active intervals, timers, and listeners.
115
+ */
116
+ stop() {
117
+ if (this.pushIntervalTimer) {
118
+ clearInterval(this.pushIntervalTimer);
119
+ this.pushIntervalTimer = undefined;
120
+ }
121
+ if (this.pullIntervalTimer) {
122
+ clearInterval(this.pullIntervalTimer);
123
+ this.pullIntervalTimer = undefined;
124
+ }
125
+ if (this.pushDebounceTimer) {
126
+ clearTimeout(this.pushDebounceTimer);
127
+ this.pushDebounceTimer = undefined;
128
+ }
129
+ if (this.networkUnsubscribe) {
130
+ this.networkUnsubscribe();
131
+ this.networkUnsubscribe = undefined;
132
+ }
133
+ }
134
+ /**
135
+ * Schedules an immediate push with a short debounce.
136
+ */
137
+ schedulePush(delayMs = 50) {
138
+ if (this.pushDebounceTimer) {
139
+ clearTimeout(this.pushDebounceTimer);
140
+ }
141
+ this.pushDebounceTimer = setTimeout(() => {
142
+ this.pushDebounceTimer = undefined;
143
+ this.pushLocalChanges().catch((err) => {
144
+ this.logger.error('Debounced push failed:', err);
145
+ });
146
+ }, delayMs);
147
+ }
148
+ async isOnline() {
149
+ try {
150
+ return await Promise.resolve(this.network.isOnline());
151
+ }
152
+ catch {
153
+ return true;
154
+ }
155
+ }
156
+ /**
157
+ * Pushes pending outbox records to the remote database.
158
+ */
159
+ async pushLocalChanges() {
160
+ if (this.isPushing)
161
+ return;
162
+ if (!(await this.isOnline()))
163
+ return;
164
+ this.isPushing = true;
165
+ let pendingRecords = [];
166
+ try {
167
+ pendingRecords = await outbox_store_js_1.OutboxStore.getPending(this.db, this.maxRetries);
168
+ if (pendingRecords.length === 0) {
169
+ return;
170
+ }
171
+ const operations = pendingRecords.map((r) => ({
172
+ id: r.id,
173
+ tableName: r.tableName,
174
+ rowId: r.rowId,
175
+ operation: r.operation,
176
+ payload: r.payload,
177
+ timestamp: r.timestamp,
178
+ }));
179
+ const recordIds = operations.map((o) => o.id);
180
+ // Mark records as PROCESSING
181
+ await outbox_store_js_1.OutboxStore.markProcessing(this.db, recordIds);
182
+ const success = await this.remote.pushChanges(operations);
183
+ if (success) {
184
+ // Successful push: delete rows from outbox
185
+ await outbox_store_js_1.OutboxStore.deleteCompleted(this.db, recordIds);
186
+ this.events.emit('sync_outbox', { operation: 'DELETE', rowId: 'batch' });
187
+ }
188
+ else {
189
+ await this.handlePushFailure(pendingRecords, 'Remote push returned false');
190
+ }
191
+ }
192
+ catch (error) {
193
+ this.logger.error('Sync Engine Outbox Push Error:', error);
194
+ if (pendingRecords.length > 0) {
195
+ await this.handlePushFailure(pendingRecords, error?.message || String(error), error);
196
+ }
197
+ }
198
+ finally {
199
+ this.isPushing = false;
200
+ }
201
+ }
202
+ async handlePushFailure(records, errorMessage, errorObj) {
203
+ try {
204
+ const recordIds = records.map((r) => r.id);
205
+ await outbox_store_js_1.OutboxStore.markFailed(this.db, recordIds, errorMessage);
206
+ this.events.emit('sync_outbox', { operation: 'UPDATE', rowId: 'batch' });
207
+ // If custom shouldRetry filter specifies an operation should NOT be retried, mark it FAILED permanently
208
+ if (this.retryConfig.shouldRetry && errorObj) {
209
+ for (const rec of records) {
210
+ if (!this.retryConfig.shouldRetry(errorObj, rec)) {
211
+ await outbox_store_js_1.OutboxStore.markFailed(this.db, [rec.id], `Non-retryable error: ${errorMessage}`);
212
+ }
213
+ }
214
+ }
215
+ // Schedule next retry with exponential backoff
216
+ const currentRetry = Math.max(...records.map((r) => r.retryCount || 0));
217
+ const isInfinite = !Number.isFinite(this.maxRetries) || this.maxRetries === -1;
218
+ if (isInfinite || currentRetry < this.maxRetries) {
219
+ const backoffDelay = Math.min((this.retryConfig.initialDelayMs || 1000) *
220
+ Math.pow(this.retryConfig.backoffMultiplier || 1.5, currentRetry), this.retryConfig.maxDelayMs || 30000);
221
+ this.schedulePush(backoffDelay);
222
+ }
223
+ }
224
+ catch (err) {
225
+ this.logger.error('Failed to update outbox failure status:', err);
226
+ }
227
+ }
228
+ /**
229
+ * Pulls delta changes from the remote backend since lastPulledTimestamp.
230
+ */
231
+ async pullRemoteChanges() {
232
+ if (this.isPulling)
233
+ return;
234
+ if (!(await this.isOnline()))
235
+ return;
236
+ this.isPulling = true;
237
+ try {
238
+ if (!this.cursorLoaded) {
239
+ this.lastPulledTimestamp = await meta_store_js_1.MetaStore.getLastSyncedAt(this.db);
240
+ this.cursorLoaded = true;
241
+ }
242
+ const deltas = await this.remote.fetchChanges(this.lastPulledTimestamp);
243
+ if (deltas) {
244
+ await this.applyDeltasLocally(deltas);
245
+ }
246
+ }
247
+ catch (error) {
248
+ this.logger.error('Sync Engine Delta Pull Error:', error);
249
+ }
250
+ finally {
251
+ this.isPulling = false;
252
+ }
253
+ }
254
+ /**
255
+ * Applies incoming server delta changes to local tables with conflict resolution.
256
+ * Serializes execution in a queue to prevent concurrent SQLite transactions.
257
+ */
258
+ async applyDeltasLocally(deltas) {
259
+ const task = async () => {
260
+ await this.processDeltasLocally(deltas);
261
+ };
262
+ const next = this.deltaQueue.then(task, task);
263
+ this.deltaQueue = next.catch(() => { });
264
+ return await next;
265
+ }
266
+ async processDeltasLocally(deltas) {
267
+ if (!deltas.changes || deltas.changes.length === 0) {
268
+ if (deltas.timestamp && deltas.timestamp > this.lastPulledTimestamp) {
269
+ this.lastPulledTimestamp = deltas.timestamp;
270
+ await meta_store_js_1.MetaStore.setLastSyncedAt(this.db, this.lastPulledTimestamp);
271
+ }
272
+ return;
273
+ }
274
+ await this.db.transaction(async (tx) => {
275
+ for (const change of deltas.changes) {
276
+ // 1. Check for pending local mutations for this (table_name, row_id)
277
+ const localPendingRecord = await outbox_store_js_1.OutboxStore.findPendingByRow(tx, change.tableName, change.rowId);
278
+ // 2. Query current local row if it exists
279
+ const localRow = await table_manager_js_1.TableManager.fetchRow(tx, change.tableName, change.rowId, this.primaryKey, { autoSchema: this.autoSchema, schemaManager: this.schemaManager });
280
+ // 3. Conflict resolution if there is a pending local change
281
+ if (localPendingRecord) {
282
+ const resolution = await Promise.resolve(this.conflictResolver({
283
+ tableName: change.tableName,
284
+ rowId: change.rowId,
285
+ remoteChange: change,
286
+ localOutboxRecord: localPendingRecord,
287
+ localRow,
288
+ }));
289
+ if (resolution === 'keep-local') {
290
+ // Local offline change takes precedence
291
+ continue;
292
+ }
293
+ if (resolution === 'apply-remote') {
294
+ // Remote change wins: remove the superseded local pending outbox mutation
295
+ await outbox_store_js_1.OutboxStore.deleteRecord(tx, localPendingRecord.id);
296
+ }
297
+ else if (typeof resolution === 'object' && resolution.mergePayload) {
298
+ // Merge scenario
299
+ change.payload = resolution.mergePayload;
300
+ await outbox_store_js_1.OutboxStore.updatePayload(tx, localPendingRecord.id, resolution.mergePayload);
301
+ }
302
+ }
303
+ // 4. Apply remote change to local database
304
+ await table_manager_js_1.TableManager.applyMutation(tx, change.tableName, change.rowId, change.operation, change.payload, this.primaryKey, { autoSchema: this.autoSchema, schemaManager: this.schemaManager });
305
+ // 5. Emit table change event to notify reactive listeners
306
+ this.events.emit(change.tableName, {
307
+ operation: change.operation,
308
+ rowId: change.rowId,
309
+ payload: change.payload,
310
+ source: 'remote',
311
+ });
312
+ }
313
+ // Persist sync cursor atomically within the same transaction as delta writes
314
+ if (deltas.timestamp && deltas.timestamp > this.lastPulledTimestamp) {
315
+ await meta_store_js_1.MetaStore.setLastSyncedAt(tx, deltas.timestamp);
316
+ }
317
+ });
318
+ if (deltas.timestamp) {
319
+ this.lastPulledTimestamp = Math.max(this.lastPulledTimestamp, deltas.timestamp);
320
+ }
321
+ }
322
+ async getLastPulledTimestamp() {
323
+ if (!this.cursorLoaded) {
324
+ this.lastPulledTimestamp = await meta_store_js_1.MetaStore.getLastSyncedAt(this.db);
325
+ this.cursorLoaded = true;
326
+ }
327
+ return this.lastPulledTimestamp;
328
+ }
329
+ async resetSyncCursor() {
330
+ await meta_store_js_1.MetaStore.clearLastSyncedAt(this.db);
331
+ this.lastPulledTimestamp = 0;
332
+ this.cursorLoaded = true;
333
+ }
334
+ }
335
+ exports.SyncWorker = SyncWorker;
@@ -0,0 +1,19 @@
1
+ import { LocalTransaction } from '../adapters/local/index.js';
2
+ import { SyncOp } from './types.js';
3
+ import { SchemaManager } from './schema-manager.js';
4
+ export interface TableMutationOptions {
5
+ autoSchema?: boolean;
6
+ schemaManager?: SchemaManager;
7
+ }
8
+ export declare class TableManager {
9
+ /**
10
+ * Applies an insert, update, or delete mutation on a local data table.
11
+ * If an INSERT is specified but the row already exists, it gracefully executes an UPDATE.
12
+ * If an UPDATE is specified but the row does not exist, it inserts it.
13
+ */
14
+ static applyMutation(tx: LocalTransaction, tableName: string, rowId: string, operation: SyncOp, payload: Record<string, any> | null, primaryKey?: string, options?: TableMutationOptions): Promise<void>;
15
+ /**
16
+ * Fetches an existing row from a local table by primary key.
17
+ */
18
+ static fetchRow(tx: LocalTransaction, tableName: string, rowId: string, primaryKey?: string, options?: TableMutationOptions): Promise<Record<string, any> | null>;
19
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TableManager = void 0;
4
+ const sql_js_1 = require("../utils/sql.js");
5
+ const schema_manager_js_1 = require("./schema-manager.js");
6
+ const defaultSchemaManager = new schema_manager_js_1.SchemaManager();
7
+ class TableManager {
8
+ /**
9
+ * Applies an insert, update, or delete mutation on a local data table.
10
+ * If an INSERT is specified but the row already exists, it gracefully executes an UPDATE.
11
+ * If an UPDATE is specified but the row does not exist, it inserts it.
12
+ */
13
+ static async applyMutation(tx, tableName, rowId, operation, payload, primaryKey = 'id', options) {
14
+ const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
15
+ const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
16
+ const sm = options?.schemaManager ?? defaultSchemaManager;
17
+ if (options?.autoSchema) {
18
+ await sm.ensureTable(tx, tableName, primaryKey);
19
+ }
20
+ if (operation === 'DELETE') {
21
+ await tx.execute(`DELETE FROM ${safeTable} WHERE ${safePk} = ?`, [rowId]);
22
+ return;
23
+ }
24
+ let rowData;
25
+ if (options?.autoSchema) {
26
+ const formatted = await sm.ensureColumns(tx, tableName, payload, primaryKey);
27
+ rowData = { ...formatted, [primaryKey]: rowId };
28
+ }
29
+ else {
30
+ const formatted = {};
31
+ if (payload && typeof payload === 'object') {
32
+ for (const k of Object.keys(payload)) {
33
+ formatted[k] = (0, schema_manager_js_1.formatValueForStorage)(payload[k]);
34
+ }
35
+ }
36
+ rowData = { ...formatted, [primaryKey]: rowId };
37
+ }
38
+ const keys = Object.keys(rowData);
39
+ if (keys.length === 0)
40
+ return;
41
+ const existing = await tx.execute(`SELECT 1 FROM ${safeTable} WHERE ${safePk} = ? LIMIT 1`, [rowId]);
42
+ if (existing && existing.length > 0) {
43
+ // Row exists -> perform UPDATE
44
+ const updateKeys = keys.filter((k) => k !== primaryKey);
45
+ if (updateKeys.length > 0) {
46
+ const setClause = updateKeys.map((k) => `${(0, sql_js_1.escapeIdentifier)(k)} = ?`).join(', ');
47
+ const values = updateKeys.map((k) => rowData[k]);
48
+ values.push(rowId);
49
+ await tx.execute(`UPDATE ${safeTable} SET ${setClause} WHERE ${safePk} = ?`, values);
50
+ }
51
+ }
52
+ else {
53
+ // Row does not exist -> perform INSERT
54
+ const cols = keys.map((k) => (0, sql_js_1.escapeIdentifier)(k)).join(', ');
55
+ const placeholders = keys.map(() => '?').join(', ');
56
+ const values = keys.map((k) => rowData[k]);
57
+ await tx.execute(`INSERT INTO ${safeTable} (${cols}) VALUES (${placeholders})`, values);
58
+ }
59
+ }
60
+ /**
61
+ * Fetches an existing row from a local table by primary key.
62
+ */
63
+ static async fetchRow(tx, tableName, rowId, primaryKey = 'id', options) {
64
+ const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
65
+ const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
66
+ const sm = options?.schemaManager ?? defaultSchemaManager;
67
+ if (options?.autoSchema) {
68
+ await sm.ensureTable(tx, tableName, primaryKey);
69
+ }
70
+ const rows = await tx.execute(`SELECT * FROM ${safeTable} WHERE ${safePk} = ? LIMIT 1`, [rowId]);
71
+ return rows && rows.length > 0 ? rows[0] : null;
72
+ }
73
+ }
74
+ exports.TableManager = TableManager;
@@ -0,0 +1,73 @@
1
+ export type SyncOp = 'INSERT' | 'UPDATE' | 'DELETE';
2
+ export type OutboxStatus = 'PENDING' | 'PROCESSING' | 'FAILED';
3
+ export interface SyncOperation {
4
+ id: string;
5
+ tableName: string;
6
+ rowId: string;
7
+ operation: SyncOp;
8
+ payload: Record<string, any> | null;
9
+ timestamp: number;
10
+ }
11
+ export interface OutboxRecord extends SyncOperation {
12
+ status: OutboxStatus;
13
+ retryCount: number;
14
+ lastError?: string | null;
15
+ }
16
+ export interface DeltaChange {
17
+ tableName: string;
18
+ operation: SyncOp;
19
+ rowId: string;
20
+ payload: Record<string, any> | null;
21
+ timestamp?: number;
22
+ }
23
+ export interface DeltaChanges {
24
+ timestamp: number;
25
+ changes: DeltaChange[];
26
+ }
27
+ export type ConflictResolution = 'apply-remote' | 'keep-local' | {
28
+ mergePayload: Record<string, any>;
29
+ };
30
+ export interface ConflictContext {
31
+ tableName: string;
32
+ rowId: string;
33
+ remoteChange: DeltaChange;
34
+ localOutboxRecord?: OutboxRecord;
35
+ localRow?: Record<string, any> | null;
36
+ }
37
+ export type ConflictResolver = (context: ConflictContext) => ConflictResolution | Promise<ConflictResolution>;
38
+ export type ConflictStrategy = 'last-write-wins' | 'server-wins' | 'client-wins';
39
+ export interface NetworkStatusProvider {
40
+ isOnline(): boolean | Promise<boolean>;
41
+ subscribe(callback: (isOnline: boolean) => void): () => void;
42
+ }
43
+ export interface RetryConfig {
44
+ /** Maximum retry attempts before marking outbox record as permanently FAILED. Set to `Infinity` or `-1` for infinite retries. Default: 5 */
45
+ maxRetries?: number;
46
+ /** Initial delay before retrying failed operations in milliseconds. Default: 1000 */
47
+ initialDelayMs?: number;
48
+ /** Backoff multiplier applied on consecutive failures (e.g. 2 for exponential). Default: 1.5 */
49
+ backoffMultiplier?: number;
50
+ /** Maximum capped delay between retries in milliseconds. Default: 30000 */
51
+ maxDelayMs?: number;
52
+ /** Custom predicate deciding whether an error is retryable. Return false for non-retryable errors like permanent RLS violations. */
53
+ shouldRetry?: (error: any, operation: SyncOperation) => boolean;
54
+ }
55
+ export interface MutateOptions {
56
+ primaryKey?: string;
57
+ tx?: any;
58
+ customTimestamp?: number;
59
+ skipOutbox?: boolean;
60
+ }
61
+ export interface TableChangeEvent {
62
+ operation: SyncOp;
63
+ rowId: string;
64
+ payload?: Record<string, any> | null;
65
+ source: 'local' | 'remote';
66
+ }
67
+ export interface QueryOptions {
68
+ where?: Record<string, any>;
69
+ orderBy?: string;
70
+ order?: 'asc' | 'desc' | 'ASC' | 'DESC';
71
+ limit?: number;
72
+ offset?: number;
73
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,17 @@
1
+ export * from './core/client.js';
2
+ export * from './core/types.js';
3
+ export * from './core/sync-worker.js';
4
+ export * from './core/conflict.js';
5
+ export * from './core/outbox-store.js';
6
+ export * from './core/meta-store.js';
7
+ export * from './core/table-manager.js';
8
+ export * from './core/schema-manager.js';
9
+ export * from './reactive/event-emitter.js';
10
+ export * from './adapters/local/index.js';
11
+ export * from './adapters/remote/index.js';
12
+ export * from './adapters/network/index.js';
13
+ export * from './config/index.js';
14
+ export * from './react/index.js';
15
+ export * from './utils/uuid.js';
16
+ export * from './utils/sql.js';
17
+ export * from './utils/logger.js';
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
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
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./core/client.js"), exports);
18
+ __exportStar(require("./core/types.js"), exports);
19
+ __exportStar(require("./core/sync-worker.js"), exports);
20
+ __exportStar(require("./core/conflict.js"), exports);
21
+ __exportStar(require("./core/outbox-store.js"), exports);
22
+ __exportStar(require("./core/meta-store.js"), exports);
23
+ __exportStar(require("./core/table-manager.js"), exports);
24
+ __exportStar(require("./core/schema-manager.js"), exports);
25
+ __exportStar(require("./reactive/event-emitter.js"), exports);
26
+ __exportStar(require("./adapters/local/index.js"), exports);
27
+ __exportStar(require("./adapters/remote/index.js"), exports);
28
+ __exportStar(require("./adapters/network/index.js"), exports);
29
+ __exportStar(require("./config/index.js"), exports);
30
+ __exportStar(require("./react/index.js"), exports);
31
+ __exportStar(require("./utils/uuid.js"), exports);
32
+ __exportStar(require("./utils/sql.js"), exports);
33
+ __exportStar(require("./utils/logger.js"), exports);
@@ -0,0 +1,34 @@
1
+ import { LocalFirstClient } from '../core/client.js';
2
+ import { QueryOptions } from '../core/types.js';
3
+ export type LiveQueryFn<T> = (client: LocalFirstClient) => Promise<T[]> | T[];
4
+ /**
5
+ * Hook to execute a live reactive table query that automatically re-runs
6
+ * whenever the table is mutated locally or via incoming remote delta.
7
+ *
8
+ * Supports 3 flexible usage patterns:
9
+ * 1. Zero-SQL (all rows):
10
+ * const { data: todos } = useLiveQuery<Todo>(client, 'todos');
11
+ *
12
+ * 2. Declarative options (filtering & sorting without SQL):
13
+ * const { data: active } = useLiveQuery<Todo>(client, 'todos', {
14
+ * where: { completed: 0 },
15
+ * orderBy: 'created_at',
16
+ * order: 'desc'
17
+ * });
18
+ *
19
+ * 3. Custom query callback (advanced raw SQL / joins):
20
+ * const { data } = useLiveQuery(client, 'todos', (c) => c.db.execute('SELECT ...'));
21
+ */
22
+ export declare function useLiveQuery<T = any>(client: LocalFirstClient | null | undefined, tableName: string, queryOrOptions?: LiveQueryFn<T> | QueryOptions): {
23
+ data: T[];
24
+ refetch: () => Promise<void>;
25
+ loading: boolean;
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): {
31
+ pendingCount: number;
32
+ pendingRecords: any[];
33
+ refresh: () => Promise<void>;
34
+ };