local-lite 0.0.3 → 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 (55) 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/react/BenchmarkModal.d.ts +32 -0
  42. package/dist/react/BenchmarkModal.js +777 -0
  43. package/dist/react/SyncInspector.d.ts +31 -0
  44. package/dist/react/SyncInspector.js +655 -0
  45. package/dist/react/index.d.ts +51 -19
  46. package/dist/react/index.js +265 -105
  47. package/dist/react/runner.d.ts +7 -0
  48. package/dist/react/runner.js +42 -0
  49. package/dist/react/status.d.ts +24 -0
  50. package/dist/react/status.js +123 -0
  51. package/dist/react/theme.d.ts +18 -0
  52. package/dist/react/theme.js +46 -0
  53. package/dist/utils/logger.d.ts +4 -0
  54. package/dist/utils/logger.js +20 -0
  55. package/package.json +138 -133
@@ -1,6 +1,6 @@
1
1
  import type { SupabaseClient } from '@supabase/supabase-js';
2
2
  import { RemoteDBAdapter } from './index.js';
3
- import { DeltaChanges, SyncOperation } from '../../core/types.js';
3
+ import { DeltaChanges, PushResult, SyncOperation } from '../../core/types.js';
4
4
  import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
5
5
  export interface SupabaseAdapterOptions {
6
6
  /**
@@ -13,6 +13,12 @@ export interface SupabaseAdapterOptions {
13
13
  * Default: 'updated_at'
14
14
  */
15
15
  updatedAtColumn?: string;
16
+ /**
17
+ * Optional subset of tables to which dynamic `syncScope` filters should apply.
18
+ * If provided, scope filters (like `business_id`) will only be attached to queries for these tables.
19
+ * Tables not in this list (e.g. 'businesses', 'profiles') will be fetched without the scope filter.
20
+ */
21
+ scopedTables?: string[];
16
22
  /**
17
23
  * Primary key column name on Postgres tables.
18
24
  * Default: 'id'
@@ -55,10 +61,18 @@ export interface SupabaseAdapterOptions {
55
61
  * Minimum log level. Default: 'info'
56
62
  */
57
63
  logLevel?: LogLevel;
64
+ /**
65
+ * Maximum records pulled per page from Supabase. Default: 1000
66
+ */
67
+ fetchLimit?: number;
58
68
  }
69
+ export type AnySupabaseClient = SupabaseClient<any, any, any> | any;
70
+ export type SupabaseClientResolver = AnySupabaseClient | (() => AnySupabaseClient | null | undefined) | null | undefined;
59
71
  export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
60
- private client;
72
+ private clientResolver?;
73
+ private activeLiveCallback?;
61
74
  private tables;
75
+ private scopedTables?;
62
76
  private updatedAtColumn;
63
77
  private primaryKey;
64
78
  private deletedColumn;
@@ -67,8 +81,22 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
67
81
  private schema;
68
82
  private channelName;
69
83
  private activeChannel?;
84
+ private fetchLimit;
70
85
  logger: SyncLogger;
71
- constructor(client: SupabaseClient, options?: SupabaseAdapterOptions);
86
+ constructor(clientOrOptions?: SupabaseClientResolver | SupabaseAdapterOptions, maybeOptions?: SupabaseAdapterOptions);
87
+ /**
88
+ * Resolves the active Supabase client instance dynamically.
89
+ */
90
+ getClient(): AnySupabaseClient;
91
+ /**
92
+ * Property getter for backwards-compatibility.
93
+ */
94
+ get client(): AnySupabaseClient;
95
+ /**
96
+ * Dynamically sets or updates the Supabase client or getter function.
97
+ * Automatically cleans up the previous realtime channel and re-subscribes if active.
98
+ */
99
+ setClient(client: SupabaseClientResolver): void;
72
100
  /**
73
101
  * Set or update tracked tables dynamically.
74
102
  */
@@ -76,12 +104,12 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
76
104
  /**
77
105
  * Pushes outbox operations to Supabase.
78
106
  */
79
- pushChanges(operations: SyncOperation[]): Promise<boolean>;
107
+ pushChanges(operations: SyncOperation[]): Promise<boolean | PushResult>;
80
108
  /**
81
109
  * Fetches delta changes from Supabase since a given timestamp.
82
110
  * Reads updated records and checks `deleted: true` to propagate soft deletes.
83
111
  */
84
- fetchChanges(sinceTimestamp: number): Promise<DeltaChanges>;
112
+ fetchChanges(sinceTimestamp: number, scope?: Record<string, any>): Promise<DeltaChanges>;
85
113
  /**
86
114
  * Helper to determine if a record represents a soft delete.
87
115
  */
@@ -89,7 +117,7 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
89
117
  /**
90
118
  * Subscribes to live realtime postgres_changes from Supabase.
91
119
  */
92
- subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): void;
120
+ subscribeToLiveChanges(callback: (changes: DeltaChanges) => void): (() => void) | void;
93
121
  /**
94
122
  * Clean up active realtime channels.
95
123
  */
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SupabaseRemoteAdapter = void 0;
4
4
  const logger_js_1 = require("../../utils/logger.js");
5
5
  class SupabaseRemoteAdapter {
6
- client;
6
+ clientResolver;
7
+ activeLiveCallback;
7
8
  tables;
9
+ scopedTables;
8
10
  updatedAtColumn;
9
11
  primaryKey;
10
12
  deletedColumn;
@@ -13,10 +15,27 @@ class SupabaseRemoteAdapter {
13
15
  schema;
14
16
  channelName;
15
17
  activeChannel;
18
+ fetchLimit;
16
19
  logger;
17
- constructor(client, options = {}) {
18
- this.client = client;
20
+ constructor(clientOrOptions, maybeOptions = {}) {
21
+ let client;
22
+ let options;
23
+ // Allow new SupabaseRemoteAdapter(options) shorthand when no client is passed initially
24
+ if (clientOrOptions &&
25
+ typeof clientOrOptions === 'object' &&
26
+ !('channel' in clientOrOptions) &&
27
+ !('from' in clientOrOptions) &&
28
+ !('auth' in clientOrOptions)) {
29
+ client = null;
30
+ options = clientOrOptions;
31
+ }
32
+ else {
33
+ client = clientOrOptions;
34
+ options = maybeOptions;
35
+ }
36
+ this.clientResolver = client;
19
37
  this.tables = options.tables ?? [];
38
+ this.scopedTables = options.scopedTables;
20
39
  this.updatedAtColumn = options.updatedAtColumn ?? 'updated_at';
21
40
  this.primaryKey = options.primaryKey ?? 'id';
22
41
  this.deletedColumn = options.deletedColumn ?? options.deletedAtColumn ?? 'deleted';
@@ -24,17 +43,46 @@ class SupabaseRemoteAdapter {
24
43
  this.pushRpcName = options.pushRpcName;
25
44
  this.schema = options.schema ?? 'public';
26
45
  this.channelName = options.channelName ?? 'local_first_sync';
27
- if (options.logger instanceof logger_js_1.SyncLogger) {
28
- this.logger = options.logger;
29
- }
30
- else if (typeof options.logger === 'boolean') {
31
- this.logger = new logger_js_1.SyncLogger({ enabled: options.logger, level: options.logLevel });
46
+ this.fetchLimit = options.fetchLimit ?? 1000;
47
+ this.logger = (0, logger_js_1.createSyncLogger)(options.logger, options.logLevel);
48
+ }
49
+ /**
50
+ * Resolves the active Supabase client instance dynamically.
51
+ */
52
+ getClient() {
53
+ if (typeof this.clientResolver === 'function') {
54
+ try {
55
+ return this.clientResolver() ?? null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
32
60
  }
33
- else if (options.logger && typeof options.logger === 'object') {
34
- this.logger = new logger_js_1.SyncLogger({ customLogger: options.logger, level: options.logLevel });
61
+ return this.clientResolver ?? null;
62
+ }
63
+ /**
64
+ * Property getter for backwards-compatibility.
65
+ */
66
+ get client() {
67
+ return this.getClient();
68
+ }
69
+ /**
70
+ * Dynamically sets or updates the Supabase client or getter function.
71
+ * Automatically cleans up the previous realtime channel and re-subscribes if active.
72
+ */
73
+ setClient(client) {
74
+ const previousClient = this.getClient();
75
+ this.clientResolver = client;
76
+ const newClient = this.getClient();
77
+ if (this.activeChannel && previousClient?.removeChannel && previousClient !== newClient) {
78
+ try {
79
+ previousClient.removeChannel(this.activeChannel);
80
+ }
81
+ catch { }
82
+ this.activeChannel = undefined;
35
83
  }
36
- else {
37
- this.logger = options.logLevel ? new logger_js_1.SyncLogger({ level: options.logLevel }) : logger_js_1.defaultLogger;
84
+ if (this.activeLiveCallback && newClient && typeof newClient.channel === 'function') {
85
+ this.subscribeToLiveChanges(this.activeLiveCallback);
38
86
  }
39
87
  }
40
88
  /**
@@ -50,9 +98,14 @@ class SupabaseRemoteAdapter {
50
98
  if (!operations || operations.length === 0) {
51
99
  return true;
52
100
  }
101
+ const client = this.getClient();
102
+ if (!client) {
103
+ this.logger.debug('Supabase client is not available; skipping remote push.');
104
+ return false;
105
+ }
53
106
  // 1. If custom RPC is configured, perform atomic batch RPC
54
107
  if (this.pushRpcName) {
55
- const { error } = await this.client.rpc(this.pushRpcName, {
108
+ const { error } = await client.rpc(this.pushRpcName, {
56
109
  operations: operations.map((op) => ({
57
110
  id: op.id,
58
111
  table_name: op.tableName,
@@ -68,28 +121,129 @@ class SupabaseRemoteAdapter {
68
121
  }
69
122
  return true;
70
123
  }
71
- // 2. Direct table operations
72
- for (const op of operations) {
73
- try {
74
- if (op.operation === 'DELETE') {
75
- // Soft delete: sets deleted = true and stamps updated_at
124
+ // 2. Direct table operations with consecutive batching and per-operation tracking
125
+ const successfulIds = [];
126
+ const failedIds = [];
127
+ const errors = {};
128
+ let i = 0;
129
+ while (i < operations.length) {
130
+ const op = operations[i];
131
+ // Batch consecutive INSERTs for the same table
132
+ if (op.operation === 'INSERT') {
133
+ const batch = [];
134
+ while (i < operations.length &&
135
+ operations[i].operation === 'INSERT' &&
136
+ operations[i].tableName === op.tableName) {
137
+ batch.push(operations[i]);
138
+ i++;
139
+ }
140
+ const buildRowData = (item) => {
141
+ const row = {
142
+ ...(item.payload || {}),
143
+ [this.primaryKey]: item.rowId,
144
+ [this.deletedColumn]: false,
145
+ };
146
+ if (this.updatedAtColumn && !row[this.updatedAtColumn]) {
147
+ row[this.updatedAtColumn] = new Date(item.timestamp).toISOString();
148
+ }
149
+ return row;
150
+ };
151
+ if (batch.length === 1) {
152
+ try {
153
+ const rowData = buildRowData(batch[0]);
154
+ const { error } = await client.from(op.tableName).upsert(rowData);
155
+ if (error) {
156
+ const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'INSERT' });
157
+ this.logger.error(msg);
158
+ failedIds.push(batch[0].id);
159
+ errors[batch[0].id] = msg;
160
+ }
161
+ else {
162
+ successfulIds.push(batch[0].id);
163
+ }
164
+ }
165
+ catch (err) {
166
+ const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'INSERT' });
167
+ this.logger.error(msg);
168
+ failedIds.push(batch[0].id);
169
+ errors[batch[0].id] = msg;
170
+ }
171
+ }
172
+ else {
173
+ // Bulk upsert for multiple consecutive inserts
174
+ const retryIndividually = async () => {
175
+ for (const item of batch) {
176
+ try {
177
+ const singleRow = buildRowData(item);
178
+ const { error: singleErr } = await client.from(op.tableName).upsert(singleRow);
179
+ if (singleErr) {
180
+ const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'INSERT' });
181
+ failedIds.push(item.id);
182
+ errors[item.id] = msg;
183
+ }
184
+ else {
185
+ successfulIds.push(item.id);
186
+ }
187
+ }
188
+ catch (singleErr) {
189
+ const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'INSERT' });
190
+ failedIds.push(item.id);
191
+ errors[item.id] = msg;
192
+ }
193
+ }
194
+ };
195
+ try {
196
+ const rows = batch.map(buildRowData);
197
+ const { error } = await client.from(op.tableName).upsert(rows);
198
+ if (!error) {
199
+ for (const item of batch)
200
+ successfulIds.push(item.id);
201
+ }
202
+ else {
203
+ await retryIndividually();
204
+ }
205
+ }
206
+ catch {
207
+ await retryIndividually();
208
+ }
209
+ }
210
+ continue;
211
+ }
212
+ // Handle DELETE
213
+ if (op.operation === 'DELETE') {
214
+ try {
76
215
  const patch = {
77
216
  [this.deletedColumn]: this.deletedValue,
78
217
  };
79
218
  if (this.updatedAtColumn) {
80
219
  patch[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
81
220
  }
82
- const { error } = await this.client
221
+ const { error } = await client
83
222
  .from(op.tableName)
84
223
  .update(patch)
85
224
  .eq(this.primaryKey, op.rowId);
86
225
  if (error) {
87
- this.logger.error((0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'DELETE' }));
88
- return false;
226
+ const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'DELETE' });
227
+ this.logger.error(msg);
228
+ failedIds.push(op.id);
229
+ errors[op.id] = msg;
89
230
  }
231
+ else {
232
+ successfulIds.push(op.id);
233
+ }
234
+ }
235
+ catch (err) {
236
+ const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'DELETE' });
237
+ this.logger.error(msg);
238
+ failedIds.push(op.id);
239
+ errors[op.id] = msg;
90
240
  }
91
- else if (op.operation === 'UPDATE') {
92
- // Partial UPDATE on existing record: only updates the modified columns to respect NOT NULL constraints
241
+ i++;
242
+ continue;
243
+ }
244
+ // Handle UPDATE
245
+ if (op.operation === 'UPDATE') {
246
+ try {
93
247
  const patch = {
94
248
  ...(op.payload || {}),
95
249
  [this.deletedColumn]: false,
@@ -97,91 +251,130 @@ class SupabaseRemoteAdapter {
97
251
  if (this.updatedAtColumn) {
98
252
  patch[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
99
253
  }
100
- const { error, data } = await this.client
254
+ const { error, data } = await client
101
255
  .from(op.tableName)
102
256
  .update(patch)
103
257
  .eq(this.primaryKey, op.rowId)
104
258
  .select(this.primaryKey);
105
259
  if (error) {
106
- this.logger.error((0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'UPDATE' }));
107
- return false;
260
+ const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'UPDATE' });
261
+ this.logger.error(msg);
262
+ failedIds.push(op.id);
263
+ errors[op.id] = msg;
108
264
  }
109
- // If row did not exist remotely (e.g. created & modified offline), fallback to upsert
110
- if (!data || data.length === 0) {
265
+ else if (!data || data.length === 0) {
266
+ // If row did not exist remotely, fallback to upsert
111
267
  const rowData = {
112
268
  ...patch,
113
269
  [this.primaryKey]: op.rowId,
114
270
  };
115
- const { error: upsertErr } = await this.client
116
- .from(op.tableName)
117
- .upsert(rowData);
271
+ const { error: upsertErr } = await client.from(op.tableName).upsert(rowData);
118
272
  if (upsertErr) {
119
- this.logger.error((0, logger_js_1.formatSupabaseError)(upsertErr, { table: op.tableName, operation: 'UPSERT_FALLBACK' }));
120
- return false;
273
+ const msg = (0, logger_js_1.formatSupabaseError)(upsertErr, { table: op.tableName, operation: 'UPSERT_FALLBACK' });
274
+ this.logger.error(msg);
275
+ failedIds.push(op.id);
276
+ errors[op.id] = msg;
277
+ }
278
+ else {
279
+ successfulIds.push(op.id);
121
280
  }
122
281
  }
123
- }
124
- else {
125
- // INSERT -> upsert into table with deleted: false
126
- const rowData = {
127
- ...(op.payload || {}),
128
- [this.primaryKey]: op.rowId,
129
- [this.deletedColumn]: false,
130
- };
131
- if (this.updatedAtColumn && !rowData[this.updatedAtColumn]) {
132
- rowData[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
133
- }
134
- const { error } = await this.client
135
- .from(op.tableName)
136
- .upsert(rowData);
137
- if (error) {
138
- this.logger.error((0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'INSERT' }));
139
- return false;
282
+ else {
283
+ successfulIds.push(op.id);
140
284
  }
141
285
  }
286
+ catch (err) {
287
+ const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'UPDATE' });
288
+ this.logger.error(msg);
289
+ failedIds.push(op.id);
290
+ errors[op.id] = msg;
291
+ }
292
+ i++;
293
+ continue;
142
294
  }
143
- catch (err) {
144
- this.logger.error((0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: op.operation }));
145
- return false;
146
- }
295
+ i++;
296
+ }
297
+ if (failedIds.length === 0) {
298
+ return true;
147
299
  }
148
- return true;
300
+ return {
301
+ success: false,
302
+ successfulIds,
303
+ failedIds,
304
+ errors,
305
+ };
149
306
  }
150
307
  /**
151
308
  * Fetches delta changes from Supabase since a given timestamp.
152
309
  * Reads updated records and checks `deleted: true` to propagate soft deletes.
153
310
  */
154
- async fetchChanges(sinceTimestamp) {
311
+ async fetchChanges(sinceTimestamp, scope) {
312
+ const client = this.getClient();
313
+ if (!client) {
314
+ this.logger.debug('Supabase client is not available; skipping fetchChanges.');
315
+ return {
316
+ timestamp: sinceTimestamp,
317
+ changes: [],
318
+ };
319
+ }
155
320
  const changes = [];
156
321
  const sinceIso = sinceTimestamp > 0 ? new Date(sinceTimestamp).toISOString() : null;
157
322
  let maxServerTimestamp = sinceTimestamp;
158
- for (const table of this.tables) {
159
- try {
160
- let query = this.client
161
- .from(table)
162
- .select('*');
163
- if (sinceIso) {
164
- query = query.gt(this.updatedAtColumn, sinceIso);
323
+ const fetchTableDeltas = async (table) => {
324
+ const isTableScoped = !this.scopedTables || this.scopedTables.includes(table);
325
+ const tableChanges = [];
326
+ let hasMore = true;
327
+ let currentSinceIso = sinceIso;
328
+ while (hasMore) {
329
+ let query = client.from(table).select('*');
330
+ if (isTableScoped && scope && typeof scope === 'object') {
331
+ for (const [col, val] of Object.entries(scope)) {
332
+ if (val !== undefined && val !== null && typeof query.eq === 'function') {
333
+ query = query.eq(col, val);
334
+ }
335
+ }
336
+ }
337
+ if (currentSinceIso) {
338
+ query = query.gt(this.updatedAtColumn, currentSinceIso);
165
339
  }
166
340
  if (typeof query.order === 'function') {
167
341
  query = query.order(this.updatedAtColumn, { ascending: true });
168
342
  }
169
- const { data, error } = await query;
343
+ if (typeof query.limit === 'function') {
344
+ query = query.limit(this.fetchLimit);
345
+ }
346
+ let { data, error } = await query;
347
+ // If PostgreSQL returns 42703 (undefined column) because a scoped column does not exist on this table,
348
+ // gracefully fallback to querying without the scope filter so synchronization doesn't halt.
349
+ if (error &&
350
+ (error.code === '42703' || String(error.message || '').includes('does not exist')) &&
351
+ isTableScoped) {
352
+ this.logger.debug(`[local-lite] Column in scope does not exist on table "${table}". Retrying without scope filter.`);
353
+ let fallbackQuery = client.from(table).select('*');
354
+ if (currentSinceIso) {
355
+ fallbackQuery = fallbackQuery.gt(this.updatedAtColumn, currentSinceIso);
356
+ }
357
+ if (typeof fallbackQuery.order === 'function') {
358
+ fallbackQuery = fallbackQuery.order(this.updatedAtColumn, { ascending: true });
359
+ }
360
+ if (typeof fallbackQuery.limit === 'function') {
361
+ fallbackQuery = fallbackQuery.limit(this.fetchLimit);
362
+ }
363
+ const fallbackResult = await fallbackQuery;
364
+ data = fallbackResult.data;
365
+ error = fallbackResult.error;
366
+ }
170
367
  if (error) {
171
- this.logger.error((0, logger_js_1.formatSupabaseError)(error, { table, operation: 'PULL' }));
172
- continue;
368
+ throw error;
173
369
  }
174
- if (data && Array.isArray(data)) {
370
+ if (data && Array.isArray(data) && data.length > 0) {
175
371
  for (const row of data) {
176
372
  const isSoftDeleted = this.isSoftDeleted(row);
177
373
  const operation = isSoftDeleted ? 'DELETE' : 'UPDATE';
178
374
  const rowTime = row[this.updatedAtColumn]
179
375
  ? new Date(row[this.updatedAtColumn]).getTime()
180
376
  : Date.now();
181
- if (rowTime > maxServerTimestamp) {
182
- maxServerTimestamp = rowTime;
183
- }
184
- changes.push({
377
+ tableChanges.push({
185
378
  tableName: table,
186
379
  operation,
187
380
  rowId: String(row[this.primaryKey]),
@@ -189,10 +382,55 @@ class SupabaseRemoteAdapter {
189
382
  timestamp: rowTime,
190
383
  });
191
384
  }
385
+ // Stop paging if unpaginated query or fewer rows than limit returned
386
+ if (typeof query.limit !== 'function' || data.length < this.fetchLimit) {
387
+ hasMore = false;
388
+ }
389
+ else {
390
+ const lastRow = data[data.length - 1];
391
+ const nextIso = lastRow[this.updatedAtColumn]
392
+ ? new Date(lastRow[this.updatedAtColumn]).toISOString()
393
+ : null;
394
+ if (nextIso && nextIso !== currentSinceIso) {
395
+ currentSinceIso = nextIso;
396
+ }
397
+ else {
398
+ hasMore = false;
399
+ }
400
+ }
401
+ }
402
+ else {
403
+ hasMore = false;
192
404
  }
193
405
  }
194
- catch (err) {
195
- this.logger.error((0, logger_js_1.formatSupabaseError)(err, { table, operation: 'PULL' }));
406
+ return tableChanges;
407
+ };
408
+ // Query tables in concurrent chunks of 5 to avoid 100+ second serial roundtrips
409
+ const CHUNK_SIZE = 5;
410
+ for (let i = 0; i < this.tables.length; i += CHUNK_SIZE) {
411
+ const chunk = this.tables.slice(i, i + CHUNK_SIZE);
412
+ const results = await Promise.all(chunk.map(async (table) => {
413
+ try {
414
+ return await fetchTableDeltas(table);
415
+ }
416
+ catch (err) {
417
+ const msg = (0, logger_js_1.formatSupabaseError)(err, { table, operation: 'PULL' });
418
+ if (msg.includes('Network Offline') || msg.includes('Failed to reach')) {
419
+ this.logger.warn(msg);
420
+ }
421
+ else {
422
+ this.logger.error(msg);
423
+ }
424
+ throw err;
425
+ }
426
+ }));
427
+ for (const tableChanges of results) {
428
+ for (const change of tableChanges) {
429
+ if (change.timestamp !== undefined && change.timestamp > maxServerTimestamp) {
430
+ maxServerTimestamp = change.timestamp;
431
+ }
432
+ changes.push(change);
433
+ }
196
434
  }
197
435
  }
198
436
  return {
@@ -218,14 +456,29 @@ class SupabaseRemoteAdapter {
218
456
  * Subscribes to live realtime postgres_changes from Supabase.
219
457
  */
220
458
  subscribeToLiveChanges(callback) {
221
- if (this.activeChannel) {
222
- this.client.removeChannel(this.activeChannel);
459
+ this.activeLiveCallback = callback;
460
+ const client = this.getClient();
461
+ if (this.activeChannel && client?.removeChannel) {
462
+ try {
463
+ client.removeChannel(this.activeChannel);
464
+ }
465
+ catch { }
223
466
  this.activeChannel = undefined;
224
467
  }
468
+ if (!client || typeof client.channel !== 'function') {
469
+ this.logger.debug('Supabase client is not available; skipping live changes subscription.');
470
+ return () => {
471
+ this.activeLiveCallback = undefined;
472
+ this.destroy();
473
+ };
474
+ }
225
475
  if (this.tables.length === 0) {
226
- return;
476
+ return () => {
477
+ this.activeLiveCallback = undefined;
478
+ this.destroy();
479
+ };
227
480
  }
228
- const channel = this.client.channel(this.channelName);
481
+ const channel = client.channel(this.channelName);
229
482
  for (const table of this.tables) {
230
483
  channel.on('postgres_changes', { event: '*', schema: this.schema, table }, (payload) => {
231
484
  let op = 'UPDATE';
@@ -262,13 +515,21 @@ class SupabaseRemoteAdapter {
262
515
  }
263
516
  });
264
517
  this.activeChannel = channel;
518
+ return () => {
519
+ this.activeLiveCallback = undefined;
520
+ this.destroy();
521
+ };
265
522
  }
266
523
  /**
267
524
  * Clean up active realtime channels.
268
525
  */
269
526
  destroy() {
270
- if (this.activeChannel) {
271
- this.client.removeChannel(this.activeChannel);
527
+ const client = this.getClient();
528
+ if (this.activeChannel && client?.removeChannel) {
529
+ try {
530
+ client.removeChannel(this.activeChannel);
531
+ }
532
+ catch { }
272
533
  this.activeChannel = undefined;
273
534
  }
274
535
  }
@@ -67,9 +67,7 @@ async function createSyncClient(config) {
67
67
  const remoteAdapter = await (0, resolve_remote_js_1.resolveRemoteAdapter)(config);
68
68
  // 3. Resolve Network Provider
69
69
  const network = config.network ?? new index_js_1.DefaultNetworkStatusProvider(true);
70
- const autoSchema = config.autoSchema !== undefined
71
- ? config.autoSchema
72
- : !config.migrations || config.migrations.length === 0;
70
+ const autoSchema = config.autoSchema ?? false;
73
71
  // 4. Instantiate Local-First Client
74
72
  const client = new client_js_1.LocalFirstClient({
75
73
  local: localAdapter,
@@ -85,17 +83,16 @@ async function createSyncClient(config) {
85
83
  logLevel: config.logLevel,
86
84
  autoSchema,
87
85
  tables: config.tables ?? [],
86
+ migrations: config.migrations,
87
+ autoStart: config.autoStart,
88
+ jsonColumns: config.jsonColumns,
89
+ autoHydrateJSON: config.autoHydrateJSON,
90
+ syncScope: config.syncScope,
91
+ autoPauseOnBackground: config.autoPauseOnBackground,
88
92
  });
89
93
  // 5. Execute Migrations & Auto-initialize
90
94
  if (config.autoInitialize !== false) {
91
95
  await client.initialize();
92
- if (config.migrations && config.migrations.length > 0) {
93
- for (const query of config.migrations) {
94
- if (query && query.trim()) {
95
- await client.db.execute(query);
96
- }
97
- }
98
- }
99
96
  }
100
97
  return client;
101
98
  }