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.
- package/dist/adapters/local/better-sqlite3.d.ts +5 -1
- package/dist/adapters/local/better-sqlite3.js +35 -14
- package/dist/adapters/local/expo-sqlite.d.ts +11 -2
- package/dist/adapters/local/expo-sqlite.js +37 -20
- package/dist/adapters/local/index.d.ts +2 -0
- package/dist/adapters/local/libsql.d.ts +1 -0
- package/dist/adapters/local/libsql.js +1 -0
- package/dist/adapters/local/node-sqlite.d.ts +5 -1
- package/dist/adapters/local/node-sqlite.js +30 -12
- package/dist/adapters/local/op-sqlite.d.ts +1 -0
- package/dist/adapters/local/op-sqlite.js +1 -0
- package/dist/adapters/local/sql-js.d.ts +1 -0
- package/dist/adapters/local/sql-js.js +1 -0
- package/dist/adapters/remote/firestore.js +39 -32
- package/dist/adapters/remote/supabase.d.ts +5 -0
- package/dist/adapters/remote/supabase.js +192 -85
- package/dist/core/client.d.ts +1 -9
- package/dist/core/client.js +123 -105
- package/dist/core/outbox-store.d.ts +5 -0
- package/dist/core/outbox-store.js +29 -4
- package/dist/core/query-builder.d.ts +1 -0
- package/dist/core/query-builder.js +140 -71
- package/dist/core/query.js +28 -32
- package/dist/core/schema-manager.d.ts +4 -15
- package/dist/core/schema-manager.js +45 -135
- package/dist/core/sync-worker.js +7 -2
- package/dist/react/index.js +123 -57
- package/dist/react/status.d.ts +5 -0
- package/dist/react/status.js +20 -12
- package/dist/utils/uuid.d.ts +1 -1
- package/dist/utils/uuid.js +7 -16
- package/package.json +5 -4
|
@@ -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
|
-
//
|
|
201
|
+
// Batch consecutive DELETEs for the same table
|
|
213
202
|
if (op.operation === 'DELETE') {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
.
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
//
|
|
283
|
+
// Batch consecutive UPDATEs for the same table
|
|
245
284
|
if (op.operation === 'UPDATE') {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
.
|
|
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(
|
|
263
|
-
errors[
|
|
326
|
+
failedIds.push(item.id);
|
|
327
|
+
errors[item.id] = msg;
|
|
264
328
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/core/client.d.ts
CHANGED
|
@@ -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[]>;
|