local-lite 0.0.2 → 0.0.4

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 (57) hide show
  1. package/README.md +421 -362
  2. package/dist/adapters/local/better-sqlite3.d.ts +2 -0
  3. package/dist/adapters/local/better-sqlite3.js +46 -25
  4. package/dist/adapters/local/expo-sqlite.d.ts +5 -1
  5. package/dist/adapters/local/expo-sqlite.js +40 -7
  6. package/dist/adapters/local/index.d.ts +1 -0
  7. package/dist/adapters/local/node-sqlite.d.ts +2 -0
  8. package/dist/adapters/local/node-sqlite.js +45 -22
  9. package/dist/adapters/local/op-sqlite.d.ts +1 -0
  10. package/dist/adapters/local/op-sqlite.js +6 -0
  11. package/dist/adapters/local/sql-js.d.ts +1 -0
  12. package/dist/adapters/local/sql-js.js +5 -0
  13. package/dist/adapters/network/index.js +33 -0
  14. package/dist/adapters/remote/convex.d.ts +2 -2
  15. package/dist/adapters/remote/convex.js +8 -12
  16. package/dist/adapters/remote/demo.d.ts +1 -1
  17. package/dist/adapters/remote/demo.js +3 -0
  18. package/dist/adapters/remote/firestore.d.ts +3 -2
  19. package/dist/adapters/remote/firestore.js +69 -46
  20. package/dist/adapters/remote/index.d.ts +6 -4
  21. package/dist/adapters/remote/supabase.d.ts +34 -6
  22. package/dist/adapters/remote/supabase.js +340 -79
  23. package/dist/config/index.js +7 -10
  24. package/dist/config/resolve-local.js +12 -1
  25. package/dist/config/resolve-remote.js +33 -28
  26. package/dist/config/types.d.ts +30 -5
  27. package/dist/core/client.d.ts +86 -3
  28. package/dist/core/client.js +423 -41
  29. package/dist/core/index.d.ts +14 -0
  30. package/dist/core/index.js +29 -0
  31. package/dist/core/outbox-store.d.ts +8 -4
  32. package/dist/core/outbox-store.js +84 -65
  33. package/dist/core/query.d.ts +8 -0
  34. package/dist/core/query.js +37 -0
  35. package/dist/core/schema-manager.d.ts +9 -0
  36. package/dist/core/schema-manager.js +68 -0
  37. package/dist/core/sync-worker.d.ts +38 -1
  38. package/dist/core/sync-worker.js +351 -104
  39. package/dist/core/table-manager.js +26 -29
  40. package/dist/core/types.d.ts +8 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +0 -1
  43. package/dist/react/BenchmarkModal.d.ts +32 -0
  44. package/dist/react/BenchmarkModal.js +777 -0
  45. package/dist/react/SyncInspector.d.ts +31 -0
  46. package/dist/react/SyncInspector.js +655 -0
  47. package/dist/react/index.d.ts +49 -19
  48. package/dist/react/index.js +266 -95
  49. package/dist/react/runner.d.ts +7 -0
  50. package/dist/react/runner.js +42 -0
  51. package/dist/react/status.d.ts +24 -0
  52. package/dist/react/status.js +123 -0
  53. package/dist/react/theme.d.ts +18 -0
  54. package/dist/react/theme.js +46 -0
  55. package/dist/utils/logger.d.ts +4 -0
  56. package/dist/utils/logger.js +20 -0
  57. package/package.json +138 -133
@@ -17,7 +17,9 @@ export interface BetterSqliteDatabase {
17
17
  */
18
18
  export declare class BetterSQLite3Adapter implements LocalDBAdapter {
19
19
  private db;
20
+ private queue;
20
21
  constructor(betterSqliteInstance: any);
21
22
  execute(query: string, params?: any[]): Promise<any[]>;
22
23
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
24
+ close(): void;
23
25
  }
@@ -6,40 +6,61 @@ exports.BetterSQLite3Adapter = void 0;
6
6
  */
7
7
  class BetterSQLite3Adapter {
8
8
  db;
9
+ queue = Promise.resolve();
9
10
  constructor(betterSqliteInstance) {
10
11
  this.db = betterSqliteInstance;
11
12
  }
12
13
  async execute(query, params = []) {
13
- const trimmed = query.trim();
14
- const stmt = this.db.prepare(trimmed);
15
- // If query returns rows (SELECT, PRAGMA) or is marked as reader
16
- if (stmt.reader || /^SELECT|^PRAGMA/i.test(trimmed)) {
17
- return stmt.all(...params);
18
- }
19
- const info = stmt.run(...params);
20
- return [info];
14
+ const run = async () => {
15
+ const trimmed = query.trim();
16
+ const stmt = this.db.prepare(trimmed);
17
+ // If query returns rows (SELECT, PRAGMA) or is marked as reader
18
+ if (stmt.reader || /^SELECT|^PRAGMA/i.test(trimmed)) {
19
+ return stmt.all(...params);
20
+ }
21
+ const info = stmt.run(...params);
22
+ return [info];
23
+ };
24
+ const next = this.queue.then(run, run);
25
+ this.queue = next.catch(() => { });
26
+ return await next;
21
27
  }
22
28
  async transaction(action) {
23
- // Explicit transaction control compatible with async actions
24
- this.db.prepare('BEGIN IMMEDIATE').run();
25
- try {
26
- const txWrapper = {
27
- execute: async (q, p = []) => {
28
- return await this.execute(q, p);
29
- },
30
- };
31
- const result = await action(txWrapper);
32
- this.db.prepare('COMMIT').run();
33
- return result;
34
- }
35
- catch (err) {
29
+ const run = async () => {
30
+ // Explicit transaction control compatible with async actions
31
+ this.db.prepare('BEGIN IMMEDIATE').run();
36
32
  try {
37
- this.db.prepare('ROLLBACK').run();
33
+ const txWrapper = {
34
+ execute: async (q, p = []) => {
35
+ const trimmed = q.trim();
36
+ const stmt = this.db.prepare(trimmed);
37
+ if (stmt.reader || /^SELECT|^PRAGMA/i.test(trimmed)) {
38
+ return stmt.all(...p);
39
+ }
40
+ return [stmt.run(...p)];
41
+ },
42
+ };
43
+ const result = await action(txWrapper);
44
+ this.db.prepare('COMMIT').run();
45
+ return result;
38
46
  }
39
- catch {
40
- // Ignore secondary rollback error if connection closed
47
+ catch (err) {
48
+ try {
49
+ this.db.prepare('ROLLBACK').run();
50
+ }
51
+ catch {
52
+ // Ignore secondary rollback error if connection closed
53
+ }
54
+ throw err;
41
55
  }
42
- throw err;
56
+ };
57
+ const next = this.queue.then(run, run);
58
+ this.queue = next.catch(() => { });
59
+ return await next;
60
+ }
61
+ close() {
62
+ if (typeof this.db.close === 'function') {
63
+ this.db.close();
43
64
  }
44
65
  }
45
66
  }
@@ -1,8 +1,12 @@
1
1
  import { LocalDBAdapter, LocalTransaction } from './index.js';
2
2
  export declare class ExpoSQLiteAdapter implements LocalDBAdapter {
3
3
  private db;
4
- private txMutex;
4
+ private queue;
5
5
  constructor(expoDbInstance: any);
6
6
  execute(query: string, params?: any[]): Promise<any[]>;
7
7
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
8
+ /**
9
+ * Waits for pending queries/transactions to finish and closes the underlying SQLite database connection.
10
+ */
11
+ close(): Promise<void>;
8
12
  }
@@ -3,28 +3,61 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExpoSQLiteAdapter = void 0;
4
4
  class ExpoSQLiteAdapter {
5
5
  db;
6
- txMutex = Promise.resolve();
6
+ queue = Promise.resolve();
7
7
  constructor(expoDbInstance) {
8
8
  this.db = expoDbInstance;
9
9
  }
10
10
  async execute(query, params = []) {
11
- return await this.db.getAllAsync(query, params);
11
+ const run = async () => {
12
+ const trimmed = query.trim();
13
+ const isSelect = /^SELECT\b/i.test(trimmed) || /^PRAGMA\b/i.test(trimmed);
14
+ // Parameterless DDL/write statements can bypass statement preparation
15
+ // to avoid NativeStatement SharedObject allocation race conditions on Android
16
+ if (!isSelect && (!params || params.length === 0) && typeof this.db.execAsync === 'function') {
17
+ await this.db.execAsync(query);
18
+ return [];
19
+ }
20
+ return await this.db.getAllAsync(query, params);
21
+ };
22
+ const next = this.queue.then(run, run);
23
+ this.queue = next.catch(() => { });
24
+ return await next;
12
25
  }
13
26
  async transaction(action) {
14
27
  const run = async () => {
15
28
  let result;
16
29
  await this.db.withTransactionAsync(async () => {
17
30
  const txWrapper = {
18
- execute: async (q, p = []) => await this.db.getAllAsync(q, p)
31
+ execute: async (q, p = []) => {
32
+ const trimmed = q.trim();
33
+ const isSelect = /^SELECT\b/i.test(trimmed) || /^PRAGMA\b/i.test(trimmed);
34
+ if (!isSelect && (!p || p.length === 0) && typeof this.db.execAsync === 'function') {
35
+ await this.db.execAsync(q);
36
+ return [];
37
+ }
38
+ return await this.db.getAllAsync(q, p);
39
+ }
19
40
  };
20
41
  result = await action(txWrapper);
21
42
  });
22
43
  return result;
23
44
  };
24
- // Serialize transactions to prevent SQLite "cannot start a transaction within a transaction"
25
- const currentTx = this.txMutex.then(run, run);
26
- this.txMutex = currentTx.catch(() => { });
27
- return await currentTx;
45
+ // Serialize transactions and queries to prevent SQLite concurrency crashes
46
+ const next = this.queue.then(run, run);
47
+ this.queue = next.catch(() => { });
48
+ return await next;
49
+ }
50
+ /**
51
+ * Waits for pending queries/transactions to finish and closes the underlying SQLite database connection.
52
+ */
53
+ async close() {
54
+ await this.queue;
55
+ if (typeof this.db.closeAsync === 'function') {
56
+ await this.db.closeAsync();
57
+ }
58
+ else if (typeof this.db.closeSync === 'function') {
59
+ this.db.closeSync();
60
+ }
28
61
  }
29
62
  }
30
63
  exports.ExpoSQLiteAdapter = ExpoSQLiteAdapter;
@@ -4,6 +4,7 @@ export interface LocalTransaction {
4
4
  export interface LocalDBAdapter {
5
5
  execute(query: string, params?: any[]): Promise<any[]>;
6
6
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
7
+ close?(): Promise<void> | void;
7
8
  }
8
9
  export * from './expo-sqlite.js';
9
10
  export * from './op-sqlite.js';
@@ -18,7 +18,9 @@ export interface NodeSqliteDatabaseSync {
18
18
  */
19
19
  export declare class NodeSQLiteAdapter implements LocalDBAdapter {
20
20
  private db;
21
+ private queue;
21
22
  constructor(nodeSqliteDb: any);
22
23
  execute(query: string, params?: any[]): Promise<any[]>;
23
24
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
25
+ close(): void;
24
26
  }
@@ -7,37 +7,60 @@ exports.NodeSQLiteAdapter = void 0;
7
7
  */
8
8
  class NodeSQLiteAdapter {
9
9
  db;
10
+ queue = Promise.resolve();
10
11
  constructor(nodeSqliteDb) {
11
12
  this.db = nodeSqliteDb;
12
13
  }
13
14
  async execute(query, params = []) {
14
- const trimmed = query.trim();
15
- const stmt = this.db.prepare(trimmed);
16
- // Queries that return rows (SELECT, PRAGMA)
17
- if (/^SELECT|^PRAGMA/i.test(trimmed)) {
18
- return stmt.all(...params);
19
- }
20
- const res = stmt.run(...params);
21
- return [res];
15
+ const run = async () => {
16
+ const trimmed = query.trim();
17
+ const stmt = this.db.prepare(trimmed);
18
+ // Queries that return rows (SELECT, PRAGMA)
19
+ if (/^SELECT|^PRAGMA/i.test(trimmed)) {
20
+ return stmt.all(...params);
21
+ }
22
+ const res = stmt.run(...params);
23
+ return [res];
24
+ };
25
+ const next = this.queue.then(run, run);
26
+ this.queue = next.catch(() => { });
27
+ return await next;
22
28
  }
23
29
  async transaction(action) {
24
- this.db.exec('BEGIN IMMEDIATE');
25
- try {
26
- const txWrapper = {
27
- execute: async (q, p = []) => this.execute(q, p),
28
- };
29
- const result = await action(txWrapper);
30
- this.db.exec('COMMIT');
31
- return result;
32
- }
33
- catch (err) {
30
+ const run = async () => {
31
+ this.db.exec('BEGIN IMMEDIATE');
34
32
  try {
35
- this.db.exec('ROLLBACK');
33
+ const txWrapper = {
34
+ execute: async (q, p = []) => {
35
+ const trimmed = q.trim();
36
+ const stmt = this.db.prepare(trimmed);
37
+ if (/^SELECT|^PRAGMA/i.test(trimmed)) {
38
+ return stmt.all(...p);
39
+ }
40
+ return [stmt.run(...p)];
41
+ },
42
+ };
43
+ const result = await action(txWrapper);
44
+ this.db.exec('COMMIT');
45
+ return result;
36
46
  }
37
- catch {
38
- // ignore rollback errors
47
+ catch (err) {
48
+ try {
49
+ this.db.exec('ROLLBACK');
50
+ }
51
+ catch {
52
+ // ignore rollback errors
53
+ }
54
+ throw err;
39
55
  }
40
- throw err;
56
+ };
57
+ const next = this.queue.then(run, run);
58
+ this.queue = next.catch(() => { });
59
+ return await next;
60
+ }
61
+ close() {
62
+ if (typeof this.db.close === 'function') {
63
+ this.db.close();
41
64
  }
42
65
  }
43
66
  }
@@ -23,4 +23,5 @@ export declare class OpSQLiteAdapter implements LocalDBAdapter {
23
23
  constructor(opSqliteInstance: any);
24
24
  execute(query: string, params?: any[]): Promise<any[]>;
25
25
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
26
+ close(): Promise<void>;
26
27
  }
@@ -55,5 +55,11 @@ class OpSQLiteAdapter {
55
55
  this.txMutex = currentTx.catch(() => { });
56
56
  return await currentTx;
57
57
  }
58
+ async close() {
59
+ await this.txMutex;
60
+ if (typeof this.db.close === 'function') {
61
+ this.db.close();
62
+ }
63
+ }
58
64
  }
59
65
  exports.OpSQLiteAdapter = OpSQLiteAdapter;
@@ -23,4 +23,5 @@ export declare class SqlJsAdapter implements LocalDBAdapter {
23
23
  constructor(sqlJsDatabase: any);
24
24
  execute(query: string, params?: any[]): Promise<any[]>;
25
25
  transaction<T>(action: (tx: LocalTransaction) => Promise<T>): Promise<T>;
26
+ close(): void;
26
27
  }
@@ -59,5 +59,10 @@ class SqlJsAdapter {
59
59
  throw err;
60
60
  }
61
61
  }
62
+ close() {
63
+ if (typeof this.db.close === 'function') {
64
+ this.db.close();
65
+ }
66
+ }
62
67
  }
63
68
  exports.SqlJsAdapter = SqlJsAdapter;
@@ -7,6 +7,39 @@ class DefaultNetworkStatusProvider {
7
7
  cleanupBrowserListeners;
8
8
  constructor(initialOnline = true) {
9
9
  this._isOnline = initialOnline;
10
+ // 1. Try React Native NetInfo if available in environment
11
+ try {
12
+ const NetInfo = require('@react-native-community/netinfo');
13
+ const netInfoInstance = NetInfo?.default || NetInfo;
14
+ if (netInfoInstance && typeof netInfoInstance.addEventListener === 'function') {
15
+ netInfoInstance
16
+ .fetch()
17
+ .then((state) => {
18
+ const online = state.isConnected === true &&
19
+ (state.isInternetReachable === true || state.isInternetReachable === null);
20
+ this.setOnline(online);
21
+ })
22
+ .catch(() => { });
23
+ const unsub = netInfoInstance.addEventListener((state) => {
24
+ const online = state.isConnected === true &&
25
+ (state.isInternetReachable === true || state.isInternetReachable === null);
26
+ this.setOnline(online);
27
+ });
28
+ this.cleanupBrowserListeners = () => {
29
+ if (typeof unsub === 'function') {
30
+ unsub();
31
+ }
32
+ else if (unsub && typeof unsub.remove === 'function') {
33
+ unsub.remove();
34
+ }
35
+ };
36
+ return;
37
+ }
38
+ }
39
+ catch {
40
+ // NetInfo not installed or running in node/web
41
+ }
42
+ // 2. Web DOM environment fallback
10
43
  if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
11
44
  if (typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean') {
12
45
  this._isOnline = navigator.onLine;
@@ -1,5 +1,5 @@
1
1
  import { RemoteDBAdapter } from './index.js';
2
- import { DeltaChanges, SyncOperation } from '../../core/types.js';
2
+ import { DeltaChanges, SyncOperation, PushResult } from '../../core/types.js';
3
3
  import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
4
4
  export interface ConvexRemoteAdapterOptions {
5
5
  /**
@@ -55,7 +55,7 @@ export declare class ConvexRemoteAdapter implements RemoteDBAdapter {
55
55
  /**
56
56
  * Pushes outbox operations as a batch mutation to Convex.
57
57
  */
58
- pushChanges(operations: SyncOperation[]): Promise<boolean>;
58
+ pushChanges(operations: SyncOperation[]): Promise<boolean | PushResult>;
59
59
  /**
60
60
  * Fetches delta changes from Convex since a given timestamp.
61
61
  */
@@ -23,18 +23,7 @@ class ConvexRemoteAdapter {
23
23
  this.pullQuery = options.pullQuery ?? 'sync:pullDeltas';
24
24
  this.deletedColumn = options.deletedColumn ?? 'deleted';
25
25
  this.updatedAtColumn = options.updatedAtColumn ?? 'updatedAt';
26
- if (options.logger instanceof logger_js_1.SyncLogger) {
27
- this.logger = options.logger;
28
- }
29
- else if (typeof options.logger === 'boolean') {
30
- this.logger = new logger_js_1.SyncLogger({ enabled: options.logger, level: options.logLevel });
31
- }
32
- else if (options.logger && typeof options.logger === 'object') {
33
- this.logger = new logger_js_1.SyncLogger({ customLogger: options.logger, level: options.logLevel });
34
- }
35
- else {
36
- this.logger = options.logLevel ? new logger_js_1.SyncLogger({ level: options.logLevel }) : logger_js_1.defaultLogger;
37
- }
26
+ this.logger = (0, logger_js_1.createSyncLogger)(options.logger, options.logLevel);
38
27
  }
39
28
  /**
40
29
  * Dynamically update tracked tables.
@@ -61,6 +50,13 @@ class ConvexRemoteAdapter {
61
50
  const res = await this.client.mutation(this.pushMutation, {
62
51
  operations: payload,
63
52
  });
53
+ if (res && typeof res === 'object' && Array.isArray(res.failedIds)) {
54
+ return {
55
+ success: res.failedIds.length === 0,
56
+ failedIds: res.failedIds,
57
+ errors: res.errors,
58
+ };
59
+ }
64
60
  return res !== false;
65
61
  }
66
62
  catch (error) {
@@ -12,7 +12,7 @@ export declare class MemoryDemoRemoteAdapter implements RemoteDBAdapter {
12
12
  constructor(simulatedLatencyMs?: number);
13
13
  pushChanges(operations: SyncOperation[]): Promise<boolean>;
14
14
  fetchChanges(_sinceTimestamp: number): Promise<DeltaChanges>;
15
- subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): void;
15
+ subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): () => void;
16
16
  /**
17
17
  * Manually emit simulated server deltas to subscribers.
18
18
  */
@@ -29,6 +29,9 @@ class MemoryDemoRemoteAdapter {
29
29
  }
30
30
  subscribeToLiveChanges(callback) {
31
31
  this.liveSubscribers.push(callback);
32
+ return () => {
33
+ this.liveSubscribers = this.liveSubscribers.filter((subscriber) => subscriber !== callback);
34
+ };
32
35
  }
33
36
  /**
34
37
  * Manually emit simulated server deltas to subscribers.
@@ -1,5 +1,5 @@
1
1
  import { RemoteDBAdapter } from './index.js';
2
- import { DeltaChanges, SyncOperation } from '../../core/types.js';
2
+ import { DeltaChanges, SyncOperation, PushResult } from '../../core/types.js';
3
3
  import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
4
4
  export interface FirestoreAdapterOptions {
5
5
  /**
@@ -47,8 +47,9 @@ export declare class FirestoreRemoteAdapter implements RemoteDBAdapter {
47
47
  /**
48
48
  * Pushes outbox operations as batch writes to Firestore.
49
49
  * Chunks operations into batches of 450 (below Firestore 500-write limit).
50
+ * Falls back to individual writes if a batch fails, returning partial push results.
50
51
  */
51
- pushChanges(operations: SyncOperation[]): Promise<boolean>;
52
+ pushChanges(operations: SyncOperation[]): Promise<boolean | PushResult>;
52
53
  /**
53
54
  * Fetches delta changes across tracked collections where updatedAt > sinceTimestamp.
54
55
  */
@@ -43,18 +43,7 @@ class FirestoreRemoteAdapter {
43
43
  this.deletedColumn = options.deletedColumn ?? 'deleted';
44
44
  this.updatedAtColumn = options.updatedAtColumn ?? 'updatedAt';
45
45
  this.firestoreModule = options.firestoreModule;
46
- if (options.logger instanceof logger_js_1.SyncLogger) {
47
- this.logger = options.logger;
48
- }
49
- else if (typeof options.logger === 'boolean') {
50
- this.logger = new logger_js_1.SyncLogger({ enabled: options.logger, level: options.logLevel });
51
- }
52
- else if (options.logger && typeof options.logger === 'object') {
53
- this.logger = new logger_js_1.SyncLogger({ customLogger: options.logger, level: options.logLevel });
54
- }
55
- else {
56
- this.logger = options.logLevel ? new logger_js_1.SyncLogger({ level: options.logLevel }) : logger_js_1.defaultLogger;
57
- }
46
+ this.logger = (0, logger_js_1.createSyncLogger)(options.logger, options.logLevel);
58
47
  }
59
48
  setTables(tables) {
60
49
  this.tables = tables;
@@ -75,11 +64,14 @@ class FirestoreRemoteAdapter {
75
64
  /**
76
65
  * Pushes outbox operations as batch writes to Firestore.
77
66
  * Chunks operations into batches of 450 (below Firestore 500-write limit).
67
+ * Falls back to individual writes if a batch fails, returning partial push results.
78
68
  */
79
69
  async pushChanges(operations) {
80
70
  if (!operations || operations.length === 0) {
81
71
  return true;
82
72
  }
73
+ const failedIds = [];
74
+ const errors = {};
83
75
  try {
84
76
  const isCompat = typeof this.db.batch === 'function' && typeof this.db.collection === 'function';
85
77
  const mod = isCompat ? null : await this.getModule();
@@ -87,56 +79,87 @@ class FirestoreRemoteAdapter {
87
79
  for (let i = 0; i < operations.length; i += CHUNK_SIZE) {
88
80
  const chunk = operations.slice(i, i + CHUNK_SIZE);
89
81
  const now = Date.now();
82
+ const buildDocData = (op) => {
83
+ if (op.operation === 'DELETE') {
84
+ return {
85
+ [this.primaryKey]: op.rowId,
86
+ [this.deletedColumn]: true,
87
+ [this.updatedAtColumn]: now,
88
+ };
89
+ }
90
+ else {
91
+ return {
92
+ ...(op.payload || {}),
93
+ [this.primaryKey]: op.rowId,
94
+ [this.deletedColumn]: false,
95
+ [this.updatedAtColumn]: now,
96
+ };
97
+ }
98
+ };
90
99
  if (isCompat) {
91
100
  // React Native Firebase / Compat SDK
92
- const batch = this.db.batch();
93
- for (const op of chunk) {
94
- const docRef = this.db.collection(op.tableName).doc(op.rowId);
95
- if (op.operation === 'DELETE') {
96
- batch.set(docRef, {
97
- [this.primaryKey]: op.rowId,
98
- [this.deletedColumn]: true,
99
- [this.updatedAtColumn]: now,
100
- }, { merge: true });
101
+ try {
102
+ const batch = this.db.batch();
103
+ for (const op of chunk) {
104
+ const docRef = this.db.collection(op.tableName).doc(op.rowId);
105
+ batch.set(docRef, buildDocData(op), { merge: true });
101
106
  }
102
- else {
103
- batch.set(docRef, {
104
- ...(op.payload || {}),
105
- [this.primaryKey]: op.rowId,
106
- [this.deletedColumn]: false,
107
- [this.updatedAtColumn]: now,
108
- }, { merge: true });
107
+ await batch.commit();
108
+ }
109
+ catch (batchError) {
110
+ this.logger.warn(`[Firestore] Batch write failed, falling back to per-document writes: ${batchError?.message || batchError}`);
111
+ for (const op of chunk) {
112
+ try {
113
+ const docRef = this.db.collection(op.tableName).doc(op.rowId);
114
+ await docRef.set(buildDocData(op), { merge: true });
115
+ }
116
+ catch (opErr) {
117
+ failedIds.push(op.id);
118
+ errors[op.id] = opErr?.message || String(opErr);
119
+ }
109
120
  }
110
121
  }
111
- await batch.commit();
112
122
  }
113
123
  else if (mod && typeof mod.writeBatch === 'function') {
114
124
  // Modular Firebase SDK
115
- const batch = mod.writeBatch(this.db);
116
- for (const op of chunk) {
117
- const docRef = mod.doc(mod.collection(this.db, op.tableName), op.rowId);
118
- if (op.operation === 'DELETE') {
119
- batch.set(docRef, {
120
- [this.primaryKey]: op.rowId,
121
- [this.deletedColumn]: true,
122
- [this.updatedAtColumn]: now,
123
- }, { merge: true });
125
+ try {
126
+ const batch = mod.writeBatch(this.db);
127
+ for (const op of chunk) {
128
+ const docRef = mod.doc(mod.collection(this.db, op.tableName), op.rowId);
129
+ batch.set(docRef, buildDocData(op), { merge: true });
124
130
  }
125
- else {
126
- batch.set(docRef, {
127
- ...(op.payload || {}),
128
- [this.primaryKey]: op.rowId,
129
- [this.deletedColumn]: false,
130
- [this.updatedAtColumn]: now,
131
- }, { merge: true });
131
+ await batch.commit();
132
+ }
133
+ catch (batchError) {
134
+ this.logger.warn(`[Firestore] Batch write failed, falling back to per-document writes: ${batchError?.message || batchError}`);
135
+ for (const op of chunk) {
136
+ try {
137
+ const docRef = mod.doc(mod.collection(this.db, op.tableName), op.rowId);
138
+ if (typeof mod.setDoc === 'function') {
139
+ await mod.setDoc(docRef, buildDocData(op), { merge: true });
140
+ }
141
+ else {
142
+ throw batchError;
143
+ }
144
+ }
145
+ catch (opErr) {
146
+ failedIds.push(op.id);
147
+ errors[op.id] = opErr?.message || String(opErr);
148
+ }
132
149
  }
133
150
  }
134
- await batch.commit();
135
151
  }
136
152
  else {
137
153
  throw new Error('Firestore client does not have .batch() and modular firebase/firestore writeBatch could not be loaded.');
138
154
  }
139
155
  }
156
+ if (failedIds.length > 0) {
157
+ return {
158
+ success: false,
159
+ failedIds,
160
+ errors,
161
+ };
162
+ }
140
163
  return true;
141
164
  }
142
165
  catch (error) {
@@ -1,8 +1,10 @@
1
- import { SyncOperation, DeltaChanges } from '../../core/types.js';
1
+ import { SyncOperation, DeltaChanges, PushResult } from '../../core/types.js';
2
2
  export interface RemoteDBAdapter {
3
- pushChanges(operations: SyncOperation[]): Promise<boolean>;
4
- fetchChanges(sinceTimestamp: number): Promise<DeltaChanges>;
5
- subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): void;
3
+ pushChanges(operations: SyncOperation[]): Promise<boolean | PushResult>;
4
+ /** Return a complete batch through timestamp; reject on partial failure. Supports optional scope for data filtering. */
5
+ fetchChanges(sinceTimestamp: number, scope?: Record<string, any>): Promise<DeltaChanges>;
6
+ /** Live events do not advance the durable pull checkpoint. Return cleanup when supported. */
7
+ subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): void | (() => void);
6
8
  }
7
9
  export * from './supabase.js';
8
10
  export * from './demo.js';