local-lite 0.0.5 → 0.0.7

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.
@@ -137,20 +137,9 @@ class SupabaseRemoteAdapter {
137
137
  batch.push(operations[i]);
138
138
  i++;
139
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
140
  if (batch.length === 1) {
152
141
  try {
153
- const rowData = buildRowData(batch[0]);
142
+ const rowData = this.buildRowData(batch[0]);
154
143
  const { error } = await client.from(op.tableName).upsert(rowData);
155
144
  if (error) {
156
145
  const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'INSERT' });
@@ -174,7 +163,7 @@ class SupabaseRemoteAdapter {
174
163
  const retryIndividually = async () => {
175
164
  for (const item of batch) {
176
165
  try {
177
- const singleRow = buildRowData(item);
166
+ const singleRow = this.buildRowData(item);
178
167
  const { error: singleErr } = await client.from(op.tableName).upsert(singleRow);
179
168
  if (singleErr) {
180
169
  const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'INSERT' });
@@ -193,7 +182,7 @@ class SupabaseRemoteAdapter {
193
182
  }
194
183
  };
195
184
  try {
196
- const rows = batch.map(buildRowData);
185
+ const rows = batch.map((item) => this.buildRowData(item));
197
186
  const { error } = await client.from(op.tableName).upsert(rows);
198
187
  if (!error) {
199
188
  for (const item of batch)
@@ -209,87 +198,173 @@ class SupabaseRemoteAdapter {
209
198
  }
210
199
  continue;
211
200
  }
212
- // Handle DELETE
201
+ // Batch consecutive DELETEs for the same table
213
202
  if (op.operation === 'DELETE') {
214
- try {
215
- const patch = {
216
- [this.deletedColumn]: this.deletedValue,
217
- };
218
- if (this.updatedAtColumn) {
219
- patch[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
203
+ const batch = [];
204
+ while (i < operations.length &&
205
+ operations[i].operation === 'DELETE' &&
206
+ operations[i].tableName === op.tableName) {
207
+ batch.push(operations[i]);
208
+ i++;
209
+ }
210
+ const patch = {
211
+ [this.deletedColumn]: this.deletedValue,
212
+ };
213
+ if (this.updatedAtColumn) {
214
+ patch[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
215
+ }
216
+ if (batch.length === 1) {
217
+ try {
218
+ const { error } = await client
219
+ .from(op.tableName)
220
+ .update(patch)
221
+ .eq(this.primaryKey, op.rowId);
222
+ if (error) {
223
+ const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'DELETE' });
224
+ this.logger.error(msg);
225
+ failedIds.push(op.id);
226
+ errors[op.id] = msg;
227
+ }
228
+ else {
229
+ successfulIds.push(op.id);
230
+ }
220
231
  }
221
- const { error } = await client
222
- .from(op.tableName)
223
- .update(patch)
224
- .eq(this.primaryKey, op.rowId);
225
- if (error) {
226
- const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'DELETE' });
232
+ catch (err) {
233
+ const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'DELETE' });
227
234
  this.logger.error(msg);
228
235
  failedIds.push(op.id);
229
236
  errors[op.id] = msg;
230
237
  }
231
- else {
232
- successfulIds.push(op.id);
233
- }
234
238
  }
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;
239
+ else {
240
+ // Bulk soft-delete using .in() to collapse N round-trips into 1
241
+ const rowIds = batch.map((item) => item.rowId);
242
+ try {
243
+ const { error } = await client
244
+ .from(op.tableName)
245
+ .update(patch)
246
+ .in(this.primaryKey, rowIds);
247
+ if (!error) {
248
+ for (const item of batch)
249
+ successfulIds.push(item.id);
250
+ }
251
+ else {
252
+ // Fallback to individual updates if .in() fails
253
+ for (const item of batch) {
254
+ try {
255
+ const { error: singleErr } = await client
256
+ .from(op.tableName)
257
+ .update(patch)
258
+ .eq(this.primaryKey, item.rowId);
259
+ if (singleErr) {
260
+ const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'DELETE' });
261
+ failedIds.push(item.id);
262
+ errors[item.id] = msg;
263
+ }
264
+ else {
265
+ successfulIds.push(item.id);
266
+ }
267
+ }
268
+ catch (singleErr) {
269
+ const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'DELETE' });
270
+ failedIds.push(item.id);
271
+ errors[item.id] = msg;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ catch {
277
+ for (const item of batch)
278
+ failedIds.push(item.id);
279
+ }
240
280
  }
241
- i++;
242
281
  continue;
243
282
  }
244
- // Handle UPDATE
283
+ // Batch consecutive UPDATEs for the same table
245
284
  if (op.operation === 'UPDATE') {
246
- try {
247
- const patch = {
248
- ...(op.payload || {}),
249
- [this.deletedColumn]: false,
250
- };
251
- if (this.updatedAtColumn) {
252
- patch[this.updatedAtColumn] = new Date(op.timestamp).toISOString();
285
+ const batch = [];
286
+ while (i < operations.length &&
287
+ operations[i].operation === 'UPDATE' &&
288
+ operations[i].tableName === op.tableName) {
289
+ batch.push(operations[i]);
290
+ i++;
291
+ }
292
+ if (batch.length === 1) {
293
+ const item = batch[0];
294
+ try {
295
+ const patch = this.buildRowData(item);
296
+ const { error, data } = await client
297
+ .from(op.tableName)
298
+ .update(patch)
299
+ .eq(this.primaryKey, item.rowId)
300
+ .select(this.primaryKey);
301
+ if (error) {
302
+ const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'UPDATE' });
303
+ this.logger.error(msg);
304
+ failedIds.push(item.id);
305
+ errors[item.id] = msg;
306
+ }
307
+ else if (!data || data.length === 0) {
308
+ const { error: upsertErr } = await client.from(op.tableName).upsert(patch);
309
+ if (upsertErr) {
310
+ const msg = (0, logger_js_1.formatSupabaseError)(upsertErr, { table: op.tableName, operation: 'UPSERT_FALLBACK' });
311
+ this.logger.error(msg);
312
+ failedIds.push(item.id);
313
+ errors[item.id] = msg;
314
+ }
315
+ else {
316
+ successfulIds.push(item.id);
317
+ }
318
+ }
319
+ else {
320
+ successfulIds.push(item.id);
321
+ }
253
322
  }
254
- const { error, data } = await client
255
- .from(op.tableName)
256
- .update(patch)
257
- .eq(this.primaryKey, op.rowId)
258
- .select(this.primaryKey);
259
- if (error) {
260
- const msg = (0, logger_js_1.formatSupabaseError)(error, { table: op.tableName, operation: 'UPDATE' });
323
+ catch (err) {
324
+ const msg = (0, logger_js_1.formatSupabaseError)(err, { table: op.tableName, operation: 'UPDATE' });
261
325
  this.logger.error(msg);
262
- failedIds.push(op.id);
263
- errors[op.id] = msg;
326
+ failedIds.push(item.id);
327
+ errors[item.id] = msg;
264
328
  }
265
- else if (!data || data.length === 0) {
266
- // If row did not exist remotely, fallback to upsert
267
- const rowData = {
268
- ...patch,
269
- [this.primaryKey]: op.rowId,
270
- };
271
- const { error: upsertErr } = await client.from(op.tableName).upsert(rowData);
272
- if (upsertErr) {
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;
329
+ }
330
+ else {
331
+ // Bulk upsert for multiple consecutive updates (merges fields by primaryKey in 1 HTTP call)
332
+ const rows = batch.map((item) => this.buildRowData(item));
333
+ try {
334
+ const { error } = await client.from(op.tableName).upsert(rows);
335
+ if (!error) {
336
+ for (const item of batch)
337
+ successfulIds.push(item.id);
277
338
  }
278
339
  else {
279
- successfulIds.push(op.id);
340
+ // Fallback to individual updates if bulk upsert has a schema conflict
341
+ for (const item of batch) {
342
+ try {
343
+ const singleRow = this.buildRowData(item);
344
+ const { error: singleErr } = await client.from(op.tableName).upsert(singleRow);
345
+ if (singleErr) {
346
+ const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'UPDATE' });
347
+ failedIds.push(item.id);
348
+ errors[item.id] = msg;
349
+ }
350
+ else {
351
+ successfulIds.push(item.id);
352
+ }
353
+ }
354
+ catch (singleErr) {
355
+ const msg = (0, logger_js_1.formatSupabaseError)(singleErr, { table: op.tableName, operation: 'UPDATE' });
356
+ failedIds.push(item.id);
357
+ errors[item.id] = msg;
358
+ }
359
+ }
280
360
  }
281
361
  }
282
- else {
283
- successfulIds.push(op.id);
362
+ catch {
363
+ for (const item of batch) {
364
+ failedIds.push(item.id);
365
+ }
284
366
  }
285
367
  }
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
368
  continue;
294
369
  }
295
370
  i++;
@@ -438,6 +513,20 @@ class SupabaseRemoteAdapter {
438
513
  changes,
439
514
  };
440
515
  }
516
+ /**
517
+ * Formats a row payload for remote Supabase storage (adds primaryKey, deleted: false, updated_at).
518
+ */
519
+ buildRowData(item) {
520
+ const row = {
521
+ ...(item.payload || {}),
522
+ [this.primaryKey]: item.rowId,
523
+ [this.deletedColumn]: false,
524
+ };
525
+ if (this.updatedAtColumn && !row[this.updatedAtColumn]) {
526
+ row[this.updatedAtColumn] = new Date(item.timestamp).toISOString();
527
+ }
528
+ return row;
529
+ }
441
530
  /**
442
531
  * Helper to determine if a record represents a soft delete.
443
532
  */
@@ -454,6 +543,7 @@ class SupabaseRemoteAdapter {
454
543
  }
455
544
  /**
456
545
  * Subscribes to live realtime postgres_changes from Supabase.
546
+ * Batches rapid burst events in the same microtask turn into a single SQLite sync transaction.
457
547
  */
458
548
  subscribeToLiveChanges(callback) {
459
549
  this.activeLiveCallback = callback;
@@ -479,6 +569,23 @@ class SupabaseRemoteAdapter {
479
569
  };
480
570
  }
481
571
  const channel = client.channel(this.channelName);
572
+ let pendingLiveChanges = [];
573
+ let isScheduled = false;
574
+ const flushLiveChanges = () => {
575
+ isScheduled = false;
576
+ if (pendingLiveChanges.length === 0)
577
+ return;
578
+ const toSend = pendingLiveChanges;
579
+ pendingLiveChanges = [];
580
+ const maxTimestamp = Math.max(...toSend.map((c) => c.timestamp || 0), Date.now());
581
+ callback({
582
+ timestamp: maxTimestamp,
583
+ changes: toSend,
584
+ });
585
+ };
586
+ const scheduleFlush = typeof queueMicrotask === 'function'
587
+ ? queueMicrotask
588
+ : (fn) => setTimeout(fn, 0);
482
589
  for (const table of this.tables) {
483
590
  channel.on('postgres_changes', { event: '*', schema: this.schema, table }, (payload) => {
484
591
  let op = 'UPDATE';
@@ -495,18 +602,17 @@ class SupabaseRemoteAdapter {
495
602
  const timestamp = record[this.updatedAtColumn]
496
603
  ? new Date(record[this.updatedAtColumn]).getTime()
497
604
  : Date.now();
498
- callback({
605
+ pendingLiveChanges.push({
606
+ tableName: table,
607
+ operation: finalOp,
608
+ rowId,
609
+ payload: finalOp === 'DELETE' ? null : record,
499
610
  timestamp,
500
- changes: [
501
- {
502
- tableName: table,
503
- operation: finalOp,
504
- rowId,
505
- payload: finalOp === 'DELETE' ? null : record,
506
- timestamp,
507
- },
508
- ],
509
611
  });
612
+ if (!isScheduled) {
613
+ isScheduled = true;
614
+ scheduleFlush(flushLiveChanges);
615
+ }
510
616
  });
511
617
  }
512
618
  channel.subscribe((status) => {
@@ -516,6 +622,7 @@ class SupabaseRemoteAdapter {
516
622
  });
517
623
  this.activeChannel = channel;
518
624
  return () => {
625
+ flushLiveChanges();
519
626
  this.activeLiveCallback = undefined;
520
627
  this.destroy();
521
628
  };
@@ -81,7 +81,7 @@ export declare class LocalFirstClient {
81
81
  * Writes a mutation to the outbox table, optionally coalescing with existing PENDING records for the same row.
82
82
  * Returns the affected outboxId, or undefined if an offline INSERT was deleted offline before pushing.
83
83
  */
84
- recordOutboxMutation(tx: LocalTransaction, tableName: string, rowId: string, operation: SyncOp, payload: Record<string, any> | null, timestamp: number): Promise<string | undefined>;
84
+ recordOutboxMutation(tx: LocalTransaction, tableName: string, rowId: string, operation: SyncOp, payload: Record<string, any> | null, timestamp: number, pendingOutboxMap?: Map<string, OutboxRecord>): Promise<string | undefined>;
85
85
  /**
86
86
  * Initializes the client: creates sync_outbox schema, ensures autoSchema tables if enabled,
87
87
  * applies migrations, and optionally starts automatic sync. Concurrent/repeated calls
@@ -212,14 +212,6 @@ export declare class LocalFirstClient {
212
212
  findById<T = any>(tableName: string, id: string, options?: {
213
213
  includeDeleted?: boolean;
214
214
  }): Promise<T | null>;
215
- /**
216
- * Finds multiple records by primary keys in a single batched SQL query.
217
- * Eliminates the N-roundtrip bridge overhead of multiple findById calls.
218
- * Automatically omits soft-deleted records unless includeDeleted: true.
219
- *
220
- * Example:
221
- * const tenants = await client.findByIds('tenants', ['ten-1', 'ten-2', 'ten-3']);
222
- */
223
215
  findByIds<T = any>(tableName: string, ids: string[], options?: {
224
216
  includeDeleted?: boolean;
225
217
  }): Promise<T[]>;