cry-synced-db-client 0.1.37 → 0.1.38

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/index.js CHANGED
@@ -16,7 +16,7 @@ function resolveConflict(local, external) {
16
16
  function mergeObjects(local, external) {
17
17
  const result = { ...local };
18
18
  for (const key of Object.keys(external)) {
19
- if (key === "_id" || key === "_dirty" || key === "_localChangedAt") {
19
+ if (key === "_id" || key === "_dirty") {
20
20
  continue;
21
21
  }
22
22
  const localValue = local[key];
@@ -669,7 +669,9 @@ class SyncedDb {
669
669
  }
670
670
  async findByIds(collection, ids) {
671
671
  this.assertCollection(collection);
672
- const items = await Promise.all(ids.map((id) => this.dexieDb.getById(collection, id)));
672
+ if (ids.length === 0)
673
+ return [];
674
+ const items = await this.dexieDb.getByIds(collection, ids);
673
675
  const results = [];
674
676
  for (const item of items) {
675
677
  if (item && !item._deleted) {
@@ -707,10 +709,9 @@ class SyncedDb {
707
709
  if (!existing) {
708
710
  console.warn(`SyncedDb.save: Object ${String(id)} not found in ${collection}, creating new`);
709
711
  }
712
+ await this.dexieDb.addDirtyChange(collection, id, { ...update, _lastUpdaterId: this.updaterId }, { _ts: existing?._ts, _rev: existing?._rev });
710
713
  const newData = {
711
714
  ...update,
712
- _dirty: true,
713
- _localChangedAt: new Date,
714
715
  _lastUpdaterId: this.updaterId
715
716
  };
716
717
  this.schedulePendingChange(collection, id, newData, 0, "save");
@@ -738,11 +739,15 @@ class SyncedDb {
738
739
  if (existing && !existing._deleted) {
739
740
  console.warn(`SyncedDb.insert: Object ${String(id)} already exists in ${collection}, overwriting`);
740
741
  }
742
+ const insertChanges = { ...data, _lastUpdaterId: this.updaterId };
743
+ delete insertChanges._id;
744
+ await this.dexieDb.addDirtyChange(collection, id, insertChanges, {
745
+ _ts: undefined,
746
+ _rev: undefined
747
+ });
741
748
  const newData = {
742
749
  _id: id,
743
750
  ...data,
744
- _dirty: true,
745
- _localChangedAt: new Date,
746
751
  _lastUpdaterId: this.updaterId
747
752
  };
748
753
  this.schedulePendingChange(collection, id, newData, 0, "insert");
@@ -755,10 +760,9 @@ class SyncedDb {
755
760
  if (!existing || existing._deleted) {
756
761
  return null;
757
762
  }
763
+ await this.dexieDb.addDirtyChange(collection, id, { _deleted: new Date, _lastUpdaterId: this.updaterId }, { _ts: existing._ts, _rev: existing._rev });
758
764
  const deleteUpdate = {
759
765
  _deleted: new Date,
760
- _dirty: true,
761
- _localChangedAt: new Date,
762
766
  _lastUpdaterId: this.updaterId
763
767
  };
764
768
  this.schedulePendingChange(collection, id, deleteUpdate, 0, "deleteOne");
@@ -770,25 +774,35 @@ class SyncedDb {
770
774
  const items = await this.find(collection, query);
771
775
  if (items.length === 0)
772
776
  return 0;
773
- const existingItems = await Promise.all(items.map((item) => this.dexieDb.getById(collection, item._id)));
777
+ const ids = items.map((item) => item._id);
778
+ const existingItems = await this.dexieDb.getByIds(collection, ids);
774
779
  const now = new Date;
775
- let count = 0;
780
+ const dirtyChangesBatch = [];
781
+ const idsToDelete = [];
776
782
  for (let i = 0;i < items.length; i++) {
777
783
  const item = items[i];
778
784
  const existing = existingItems[i];
779
785
  if (!item || !existing || existing._deleted)
780
786
  continue;
787
+ dirtyChangesBatch.push({
788
+ id: item._id,
789
+ changes: { _deleted: now, _lastUpdaterId: this.updaterId },
790
+ baseMeta: { _ts: existing._ts, _rev: existing._rev }
791
+ });
781
792
  const deleteUpdate = {
782
793
  _deleted: now,
783
- _dirty: true,
784
- _localChangedAt: now,
785
794
  _lastUpdaterId: this.updaterId
786
795
  };
787
796
  this.schedulePendingChange(collection, item._id, deleteUpdate, 0, "deleteMany");
788
- this.inMemDb.deleteOne(collection, item._id);
789
- count++;
797
+ idsToDelete.push(item._id);
798
+ }
799
+ if (dirtyChangesBatch.length > 0) {
800
+ await this.dexieDb.addDirtyChangesBatch(collection, dirtyChangesBatch);
801
+ }
802
+ if (idsToDelete.length > 0) {
803
+ this.inMemDb.deleteManyByIds(collection, idsToDelete);
790
804
  }
791
- return count;
805
+ return idsToDelete.length;
792
806
  }
793
807
  async hardDeleteOne(collection, id) {
794
808
  this.assertCollection(collection);
@@ -1092,8 +1106,7 @@ class SyncedDb {
1092
1106
  }
1093
1107
  }
1094
1108
  stripLocalFields(item) {
1095
- const { _dirty, _localChangedAt, ...rest } = item;
1096
- return rest;
1109
+ return item;
1097
1110
  }
1098
1111
  getPendingKey(collection, id) {
1099
1112
  return `${collection}:${String(id)}`;
@@ -1257,13 +1270,17 @@ class SyncedDb {
1257
1270
  if (dirtyItems.length === 0) {
1258
1271
  return { sentCount: 0 };
1259
1272
  }
1273
+ const ids = dirtyItems.map((item) => item._id);
1274
+ const fullItems = await this.dexieDb.getByIds(collection, ids);
1260
1275
  const updates = [];
1261
1276
  const deletes = [];
1262
- for (const item of dirtyItems) {
1263
- if (item._deleted) {
1264
- deletes.push(item);
1277
+ for (const fullItem of fullItems) {
1278
+ if (!fullItem)
1279
+ continue;
1280
+ if (fullItem._deleted) {
1281
+ deletes.push(fullItem);
1265
1282
  } else {
1266
- updates.push(item);
1283
+ updates.push(fullItem);
1267
1284
  }
1268
1285
  }
1269
1286
  const collectionBatches = [[{
@@ -1280,86 +1297,129 @@ class SyncedDb {
1280
1297
  let sentCount = 0;
1281
1298
  for (const result of results) {
1282
1299
  const { results: { inserted, updated, deleted } } = result;
1283
- for (const entity of inserted) {
1284
- await this.dexieDb.save(collection, entity._id, {
1285
- _rev: entity._rev,
1286
- _ts: entity._ts,
1287
- _dirty: false
1288
- });
1289
- this.inMemDb.save(collection, entity._id, {
1290
- _rev: entity._rev,
1291
- _ts: entity._ts
1292
- });
1293
- sentCount++;
1294
- }
1295
- for (const entity of updated) {
1296
- await this.dexieDb.save(collection, entity._id, {
1297
- _rev: entity._rev,
1298
- _ts: entity._ts,
1299
- _dirty: false
1300
- });
1301
- this.inMemDb.save(collection, entity._id, {
1302
- _rev: entity._rev,
1303
- _ts: entity._ts
1304
- });
1305
- sentCount++;
1300
+ const allSuccessIds = [
1301
+ ...inserted.map((e) => e._id),
1302
+ ...updated.map((e) => e._id),
1303
+ ...deleted.map((e) => e._id)
1304
+ ];
1305
+ if (allSuccessIds.length > 0) {
1306
+ await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
1307
+ }
1308
+ const insertedAndUpdated = [...inserted, ...updated];
1309
+ if (insertedAndUpdated.length > 0) {
1310
+ const idsToUpdate = insertedAndUpdated.map((e) => e._id);
1311
+ const existingItems = await this.dexieDb.getByIds(collection, idsToUpdate);
1312
+ const dexieSaveBatch = [];
1313
+ for (let i = 0;i < insertedAndUpdated.length; i++) {
1314
+ const entity = insertedAndUpdated[i];
1315
+ const existing = existingItems[i];
1316
+ if (existing) {
1317
+ dexieSaveBatch.push({
1318
+ ...existing,
1319
+ _rev: entity._rev,
1320
+ _ts: entity._ts
1321
+ });
1322
+ }
1323
+ this.inMemDb.save(collection, entity._id, {
1324
+ _rev: entity._rev,
1325
+ _ts: entity._ts
1326
+ });
1327
+ }
1328
+ if (dexieSaveBatch.length > 0) {
1329
+ await this.dexieDb.saveMany(collection, dexieSaveBatch);
1330
+ }
1331
+ sentCount += insertedAndUpdated.length;
1306
1332
  }
1307
- for (const entity of deleted) {
1308
- await this.dexieDb.deleteOne(collection, entity._id);
1309
- this.inMemDb.deleteOne(collection, entity._id);
1310
- sentCount++;
1333
+ if (deleted.length > 0) {
1334
+ const deleteIds = deleted.map((e) => e._id);
1335
+ await this.dexieDb.deleteMany(collection, deleteIds);
1336
+ this.inMemDb.deleteManyByIds(collection, deleteIds);
1337
+ sentCount += deleted.length;
1311
1338
  }
1312
1339
  }
1313
1340
  return { sentCount };
1314
1341
  }
1315
1342
  async recoverPendingWrites() {
1316
1343
  const pending = getPendingWrites(this.tenant);
1344
+ if (pending.length === 0)
1345
+ return;
1346
+ const byCollection = new Map;
1317
1347
  for (const write of pending) {
1348
+ const list = byCollection.get(write.collection) || [];
1349
+ list.push(write);
1350
+ byCollection.set(write.collection, list);
1351
+ }
1352
+ for (const [collection, writes] of byCollection) {
1318
1353
  try {
1319
- const existing = await this.dexieDb.getById(write.collection, write.id);
1320
- if (existing) {
1321
- await this.dexieDb.save(write.collection, write.id, write.data);
1322
- } else {
1323
- await this.dexieDb.insert(write.collection, write.data);
1354
+ const ids = writes.map((w) => w.id);
1355
+ const existingItems = await this.dexieDb.getByIds(collection, ids);
1356
+ const dirtyChangesBatch = [];
1357
+ const saveBatch = [];
1358
+ const insertBatch = [];
1359
+ for (let i = 0;i < writes.length; i++) {
1360
+ const write = writes[i];
1361
+ const existing = existingItems[i];
1362
+ const changes = { ...write.data };
1363
+ delete changes._id;
1364
+ dirtyChangesBatch.push({
1365
+ id: write.id,
1366
+ changes,
1367
+ baseMeta: { _ts: existing?._ts, _rev: existing?._rev }
1368
+ });
1369
+ if (existing) {
1370
+ saveBatch.push({ ...existing, ...write.data });
1371
+ } else {
1372
+ insertBatch.push(write.data);
1373
+ }
1374
+ }
1375
+ if (dirtyChangesBatch.length > 0) {
1376
+ await this.dexieDb.addDirtyChangesBatch(collection, dirtyChangesBatch);
1377
+ }
1378
+ const allToSave = [...saveBatch, ...insertBatch];
1379
+ if (allToSave.length > 0) {
1380
+ await this.dexieDb.saveMany(collection, allToSave);
1381
+ }
1382
+ for (const write of writes) {
1383
+ clearPendingWrite(this.tenant, write.collection, write.id);
1324
1384
  }
1325
- clearPendingWrite(this.tenant, write.collection, write.id);
1326
1385
  } catch (err) {
1327
- console.error("Failed to recover pending write:", err);
1386
+ console.error(`Failed to recover pending writes for ${collection}:`, err);
1328
1387
  }
1329
1388
  }
1330
1389
  }
1331
1390
  async processIncomingServerData(collectionName, config, serverData) {
1391
+ if (serverData.length === 0) {
1392
+ return { conflictsResolved: 0, maxTs: undefined };
1393
+ }
1332
1394
  let maxTs;
1333
1395
  let conflictsResolved = 0;
1396
+ const serverIds = serverData.map((item) => item._id);
1397
+ const localItems = await this.dexieDb.getByIds(collectionName, serverIds);
1398
+ const dirtyChangesMap = await this.dexieDb.getDirtyChangesBatch(collectionName, serverIds);
1334
1399
  const dexieBatch = [];
1335
1400
  const inMemSaveBatch = [];
1336
1401
  const inMemDeleteIds = [];
1337
- for (const serverItem of serverData) {
1338
- const localItem = await this.dexieDb.getById(collectionName, serverItem._id);
1402
+ for (let i = 0;i < serverData.length; i++) {
1403
+ const serverItem = serverData[i];
1404
+ const localItem = localItems[i];
1405
+ const dirtyChange = dirtyChangesMap.get(String(serverItem._id));
1339
1406
  if (serverItem._ts) {
1340
1407
  if (!maxTs || this.compareTimestamps(serverItem._ts, maxTs) > 0) {
1341
1408
  maxTs = serverItem._ts;
1342
1409
  }
1343
1410
  }
1344
1411
  if (localItem) {
1345
- if (localItem._dirty) {
1412
+ if (dirtyChange) {
1346
1413
  conflictsResolved++;
1347
1414
  const resolved = this.resolveCollectionConflict(collectionName, config, localItem, serverItem, "sync");
1348
- dexieBatch.push({
1349
- ...resolved,
1350
- _dirty: true,
1351
- _localChangedAt: localItem._localChangedAt
1352
- });
1415
+ dexieBatch.push(resolved);
1353
1416
  if (!resolved._deleted) {
1354
1417
  inMemSaveBatch.push(this.stripLocalFields(resolved));
1355
1418
  } else {
1356
1419
  inMemDeleteIds.push(serverItem._id);
1357
1420
  }
1358
1421
  } else {
1359
- dexieBatch.push({
1360
- ...serverItem,
1361
- _dirty: false
1362
- });
1422
+ dexieBatch.push(serverItem);
1363
1423
  if (!serverItem._deleted) {
1364
1424
  inMemSaveBatch.push(this.stripLocalFields(serverItem));
1365
1425
  } else {
@@ -1367,10 +1427,7 @@ class SyncedDb {
1367
1427
  }
1368
1428
  }
1369
1429
  } else {
1370
- dexieBatch.push({
1371
- ...serverItem,
1372
- _dirty: false
1373
- });
1430
+ dexieBatch.push(serverItem);
1374
1431
  if (!serverItem._deleted) {
1375
1432
  inMemSaveBatch.push(this.stripLocalFields(serverItem));
1376
1433
  }
@@ -1382,8 +1439,8 @@ class SyncedDb {
1382
1439
  if (inMemSaveBatch.length > 0) {
1383
1440
  this.inMemDb.saveMany(collectionName, inMemSaveBatch);
1384
1441
  }
1385
- for (const id of inMemDeleteIds) {
1386
- this.inMemDb.deleteOne(collectionName, id);
1442
+ if (inMemDeleteIds.length > 0) {
1443
+ this.inMemDb.deleteManyByIds(collectionName, inMemDeleteIds);
1387
1444
  }
1388
1445
  if (maxTs) {
1389
1446
  await this.dexieDb.setSyncMeta(collectionName, maxTs);
@@ -1399,16 +1456,20 @@ class SyncedDb {
1399
1456
  const collectionBatches = [];
1400
1457
  const dirtyItemsMap = new Map;
1401
1458
  for (const [collectionName] of this.collections) {
1402
- const dirtyItems = await this.dexieDb.getDirty(collectionName);
1403
- if (dirtyItems.length === 0)
1459
+ const dirtyChanges = await this.dexieDb.getDirty(collectionName);
1460
+ if (dirtyChanges.length === 0)
1404
1461
  continue;
1405
1462
  const updates = [];
1406
1463
  const deletes = [];
1407
- for (const item of dirtyItems) {
1408
- if (item._deleted) {
1409
- deletes.push(item);
1464
+ const ids = dirtyChanges.map((dc) => dc._id);
1465
+ const fullItems = await this.dexieDb.getByIds(collectionName, ids);
1466
+ for (const fullItem of fullItems) {
1467
+ if (!fullItem)
1468
+ continue;
1469
+ if (fullItem._deleted) {
1470
+ deletes.push(fullItem);
1410
1471
  } else {
1411
- updates.push(item);
1472
+ updates.push(fullItem);
1412
1473
  }
1413
1474
  }
1414
1475
  dirtyItemsMap.set(collectionName, { updates, deletes });
@@ -1472,34 +1533,50 @@ class SyncedDb {
1472
1533
  let sentCount = 0;
1473
1534
  for (const result of results) {
1474
1535
  const { collection, results: { inserted, updated, deleted, errors } } = result;
1475
- for (const entity of inserted) {
1476
- await this.dexieDb.save(collection, entity._id, {
1477
- _rev: entity._rev,
1478
- _ts: entity._ts,
1479
- _dirty: false
1480
- });
1481
- this.inMemDb.save(collection, entity._id, {
1482
- _rev: entity._rev,
1483
- _ts: entity._ts
1484
- });
1485
- sentCount++;
1486
- }
1487
- for (const entity of updated) {
1488
- await this.dexieDb.save(collection, entity._id, {
1489
- _rev: entity._rev,
1490
- _ts: entity._ts,
1491
- _dirty: false
1492
- });
1493
- this.inMemDb.save(collection, entity._id, {
1494
- _rev: entity._rev,
1495
- _ts: entity._ts
1496
- });
1497
- sentCount++;
1536
+ const allSuccessIds = [
1537
+ ...inserted.map((e) => e._id),
1538
+ ...updated.map((e) => e._id),
1539
+ ...deleted.map((e) => e._id)
1540
+ ];
1541
+ if (allSuccessIds.length > 0) {
1542
+ await this.dexieDb.clearDirtyChangesBatch(collection, allSuccessIds);
1543
+ }
1544
+ const insertedAndUpdated = [...inserted, ...updated];
1545
+ if (insertedAndUpdated.length > 0) {
1546
+ const idsToCheck = insertedAndUpdated.map((e) => e._id);
1547
+ const dexieItems = await this.dexieDb.getByIds(collection, idsToCheck);
1548
+ const dexieSaveBatch = [];
1549
+ const inMemSaveIds = [];
1550
+ for (let i = 0;i < insertedAndUpdated.length; i++) {
1551
+ const entity = insertedAndUpdated[i];
1552
+ const dexieItem = dexieItems[i];
1553
+ if (dexieItem) {
1554
+ dexieSaveBatch.push({
1555
+ ...dexieItem,
1556
+ _rev: entity._rev,
1557
+ _ts: entity._ts
1558
+ });
1559
+ if (!dexieItem._deleted) {
1560
+ inMemSaveIds.push({ id: entity._id, rev: entity._rev, ts: entity._ts });
1561
+ }
1562
+ }
1563
+ }
1564
+ if (dexieSaveBatch.length > 0) {
1565
+ await this.dexieDb.saveMany(collection, dexieSaveBatch);
1566
+ }
1567
+ for (const item of inMemSaveIds) {
1568
+ this.inMemDb.save(collection, item.id, {
1569
+ _rev: item.rev,
1570
+ _ts: item.ts
1571
+ });
1572
+ }
1573
+ sentCount += insertedAndUpdated.length;
1498
1574
  }
1499
- for (const entity of deleted) {
1500
- await this.dexieDb.deleteOne(collection, entity._id);
1501
- this.inMemDb.deleteOne(collection, entity._id);
1502
- sentCount++;
1575
+ if (deleted.length > 0) {
1576
+ const deleteIds = deleted.map((e) => e._id);
1577
+ await this.dexieDb.deleteMany(collection, deleteIds);
1578
+ this.inMemDb.deleteManyByIds(collection, deleteIds);
1579
+ sentCount += deleted.length;
1503
1580
  }
1504
1581
  if (errors) {
1505
1582
  console.error(`Sync errors for ${collection}:`, errors);
@@ -1669,32 +1746,40 @@ class SyncedDb {
1669
1746
  break;
1670
1747
  }
1671
1748
  case "batch": {
1749
+ const inserts = [];
1750
+ const updates = [];
1751
+ const deletes = [];
1672
1752
  for (const item of payload.data) {
1673
- if (item.operation === "insert") {
1674
- const serverItem = item.data;
1675
- if (serverItem) {
1676
- await this.handleServerItemInsert(collectionName, serverItem);
1677
- }
1678
- } else if (item.operation === "update") {
1679
- const deltaData = item.data;
1680
- if (deltaData && deltaData._id) {
1681
- const localItem = await this.dexieDb.getById(collectionName, deltaData._id);
1682
- if (localItem) {
1683
- await this.handleServerItemUpdate(collectionName, localItem, deltaData);
1684
- } else {
1685
- const fullItem = await this.restInterface.findById(collectionName, deltaData._id);
1686
- if (fullItem) {
1687
- await this.handleServerItemInsert(collectionName, fullItem);
1688
- }
1753
+ if (item.operation === "insert" && item.data) {
1754
+ inserts.push(item.data);
1755
+ } else if (item.operation === "update" && item.data && item.data._id) {
1756
+ updates.push(item.data);
1757
+ } else if (item.operation === "delete" && item.data && item.data._id) {
1758
+ deletes.push(item.data);
1759
+ }
1760
+ }
1761
+ for (const serverItem of inserts) {
1762
+ await this.handleServerItemInsert(collectionName, serverItem);
1763
+ }
1764
+ if (updates.length > 0) {
1765
+ const updateIds = updates.map((u) => u._id);
1766
+ const localItems = await this.dexieDb.getByIds(collectionName, updateIds);
1767
+ for (let i = 0;i < updates.length; i++) {
1768
+ const deltaData = updates[i];
1769
+ const localItem = localItems[i];
1770
+ if (localItem) {
1771
+ await this.handleServerItemUpdate(collectionName, localItem, deltaData);
1772
+ } else {
1773
+ const fullItem = await this.restInterface.findById(collectionName, deltaData._id);
1774
+ if (fullItem) {
1775
+ await this.handleServerItemInsert(collectionName, fullItem);
1689
1776
  }
1690
1777
  }
1691
- } else if (item.operation === "delete") {
1692
- const deleteData = item.data;
1693
- if (deleteData && deleteData._id) {
1694
- await this.handleServerItemDelete(collectionName, deleteData._id);
1695
- }
1696
1778
  }
1697
1779
  }
1780
+ for (const deleteData of deletes) {
1781
+ await this.handleServerItemDelete(collectionName, deleteData._id);
1782
+ }
1698
1783
  await this.broadcastMetaUpdate(collectionName, Date.now());
1699
1784
  break;
1700
1785
  }
@@ -1703,20 +1788,19 @@ class SyncedDb {
1703
1788
  async handleServerItemInsert(collectionName, serverItem) {
1704
1789
  const localItem = await this.dexieDb.getById(collectionName, serverItem._id);
1705
1790
  if (localItem) {
1791
+ const dirtyChange = await this.dexieDb.getDirtyChange(collectionName, serverItem._id);
1706
1792
  const metaChanged = localItem._rev !== serverItem._rev || !this.timestampsEqual(localItem._ts, serverItem._ts);
1707
- const dirtyNeedsClearing = localItem._dirty === true;
1708
- if (metaChanged || dirtyNeedsClearing) {
1793
+ if (metaChanged) {
1709
1794
  await this.dexieDb.save(collectionName, serverItem._id, {
1710
1795
  _rev: serverItem._rev,
1711
- _ts: serverItem._ts,
1712
- _dirty: false
1796
+ _ts: serverItem._ts
1713
1797
  });
1714
1798
  }
1799
+ if (dirtyChange && !metaChanged) {
1800
+ await this.dexieDb.clearDirtyChange(collectionName, serverItem._id);
1801
+ }
1715
1802
  } else {
1716
- await this.dexieDb.insert(collectionName, {
1717
- ...serverItem,
1718
- _dirty: false
1719
- });
1803
+ await this.dexieDb.insert(collectionName, serverItem);
1720
1804
  if (!serverItem._deleted) {
1721
1805
  this.inMemDb.insert(collectionName, this.stripLocalFields(serverItem));
1722
1806
  }
@@ -1726,14 +1810,13 @@ class SyncedDb {
1726
1810
  const isLoopback = serverDelta._lastUpdaterId === this.updaterId && serverDelta._rev !== undefined && localItem._rev !== undefined && serverDelta._rev === localItem._rev + 1;
1727
1811
  if (isLoopback) {
1728
1812
  const metaChanged2 = localItem._rev !== serverDelta._rev || !this.timestampsEqual(localItem._ts, serverDelta._ts);
1729
- const dirtyNeedsClearing = localItem._dirty === true;
1730
- if (metaChanged2 || dirtyNeedsClearing) {
1813
+ if (metaChanged2) {
1731
1814
  await this.dexieDb.save(collectionName, serverDelta._id, {
1732
1815
  _rev: serverDelta._rev,
1733
- _ts: serverDelta._ts,
1734
- _dirty: false
1816
+ _ts: serverDelta._ts
1735
1817
  });
1736
1818
  }
1819
+ await this.dexieDb.clearDirtyChange(collectionName, serverDelta._id);
1737
1820
  return;
1738
1821
  }
1739
1822
  const pendingKey = this.getPendingKey(collectionName, serverDelta._id);
@@ -1752,14 +1835,12 @@ class SyncedDb {
1752
1835
  }
1753
1836
  return;
1754
1837
  }
1838
+ const dirtyChange = await this.dexieDb.getDirtyChange(collectionName, serverDelta._id);
1755
1839
  const metaChanged = localItem._rev !== serverDelta._rev || !this.timestampsEqual(localItem._ts, serverDelta._ts);
1756
- if (localItem._dirty) {
1840
+ if (dirtyChange) {
1757
1841
  const merged = this.mergeLocalWithDelta(localItem, serverDelta);
1758
1842
  if (metaChanged) {
1759
- await this.dexieDb.save(collectionName, serverDelta._id, {
1760
- ...merged,
1761
- _dirty: true
1762
- });
1843
+ await this.dexieDb.save(collectionName, serverDelta._id, merged);
1763
1844
  }
1764
1845
  if (!merged._deleted) {
1765
1846
  this.inMemDb.save(collectionName, serverDelta._id, this.stripLocalFields(merged));
@@ -1769,10 +1850,7 @@ class SyncedDb {
1769
1850
  return;
1770
1851
  }
1771
1852
  const merged = this.mergeLocalWithDelta(localItem, serverDelta);
1772
- await this.dexieDb.save(collectionName, serverDelta._id, {
1773
- ...merged,
1774
- _dirty: false
1775
- });
1853
+ await this.dexieDb.save(collectionName, serverDelta._id, merged);
1776
1854
  if (!merged._deleted) {
1777
1855
  this.inMemDb.save(collectionName, serverDelta._id, this.stripLocalFields(merged));
1778
1856
  } else {
@@ -1783,7 +1861,7 @@ class SyncedDb {
1783
1861
  getNewFieldsFromServer(local, server) {
1784
1862
  const newFields = {};
1785
1863
  for (const key of Object.keys(server)) {
1786
- if (key !== "_id" && key !== "_dirty" && key !== "_localChangedAt" && local[key] === undefined) {
1864
+ if (key !== "_id" && key !== "_dirty" && local[key] === undefined) {
1787
1865
  newFields[key] = server[key];
1788
1866
  }
1789
1867
  }
@@ -1792,7 +1870,7 @@ class SyncedDb {
1792
1870
  mergeLocalWithDelta(local, delta) {
1793
1871
  const result = { ...local };
1794
1872
  for (const key of Object.keys(delta)) {
1795
- if (key === "_id" || key === "_dirty" || key === "_localChangedAt") {
1873
+ if (key === "_id" || key === "_dirty") {
1796
1874
  continue;
1797
1875
  }
1798
1876
  result[key] = delta[key];
@@ -1803,7 +1881,8 @@ class SyncedDb {
1803
1881
  const localItem = await this.dexieDb.getById(collectionName, id);
1804
1882
  if (!localItem)
1805
1883
  return;
1806
- if (localItem._dirty) {
1884
+ const dirtyChange = await this.dexieDb.getDirtyChange(collectionName, id);
1885
+ if (dirtyChange) {
1807
1886
  await this.dexieDb.save(collectionName, id, {
1808
1887
  _deleted: new Date
1809
1888
  });
@@ -1816,6 +1895,7 @@ class SyncedDb {
1816
1895
  // src/db/DexieDb.ts
1817
1896
  import Dexie from "dexie";
1818
1897
  var SYNC_META_TABLE = "_sync_meta";
1898
+ var DIRTY_CHANGES_TABLE = "_dirty_changes";
1819
1899
 
1820
1900
  class DexieDb extends Dexie {
1821
1901
  tenant;
@@ -1823,6 +1903,7 @@ class DexieDb extends Dexie {
1823
1903
  crossTabSyncDebounceMs;
1824
1904
  collections = new Map;
1825
1905
  syncMeta;
1906
+ dirtyChanges;
1826
1907
  broadcastChannel = null;
1827
1908
  pendingBroadcasts = new Map;
1828
1909
  debounceTimer = null;
@@ -1834,13 +1915,15 @@ class DexieDb extends Dexie {
1834
1915
  this.crossTabSyncDebounceMs = options?.crossTabSyncDebounceMs ?? 100;
1835
1916
  const schema = {};
1836
1917
  schema[SYNC_META_TABLE] = "[tenant+collection]";
1918
+ schema[DIRTY_CHANGES_TABLE] = "[collection+id]";
1837
1919
  for (const config of collectionConfigs) {
1838
1920
  const additionalIndexes = config.indexes || [];
1839
- const indexes = ["_id", "_dirty", "_rev", ...additionalIndexes.map(String)];
1921
+ const indexes = ["_id", "_rev", ...additionalIndexes.map(String)];
1840
1922
  schema[config.name] = indexes.join(", ");
1841
1923
  }
1842
1924
  this.version(1).stores(schema);
1843
1925
  this.syncMeta = this.table(SYNC_META_TABLE);
1926
+ this.dirtyChanges = this.table(DIRTY_CHANGES_TABLE);
1844
1927
  for (const config of collectionConfigs) {
1845
1928
  this.collections.set(config.name, this.table(config.name));
1846
1929
  }
@@ -1902,27 +1985,17 @@ class DexieDb extends Dexie {
1902
1985
  const key = this.idToString(id);
1903
1986
  const existing = await table.get(key);
1904
1987
  if (existing) {
1905
- await table.update(key, {
1906
- ...data,
1907
- _dirty: data._dirty ?? true,
1908
- _localChangedAt: new Date
1909
- });
1988
+ await table.update(key, data);
1910
1989
  } else {
1911
1990
  await table.put({
1912
1991
  _id: id,
1913
- ...data,
1914
- _dirty: data._dirty ?? true,
1915
- _localChangedAt: new Date
1992
+ ...data
1916
1993
  });
1917
1994
  }
1918
1995
  }
1919
1996
  async insert(collection, data) {
1920
1997
  const table = this.getTable(collection);
1921
- await table.put({
1922
- ...data,
1923
- _dirty: true,
1924
- _localChangedAt: new Date
1925
- });
1998
+ await table.put(data);
1926
1999
  }
1927
2000
  async saveMany(collection, items) {
1928
2001
  if (items.length === 0)
@@ -1935,6 +2008,13 @@ class DexieDb extends Dexie {
1935
2008
  const key = this.idToString(id);
1936
2009
  await table.delete(key);
1937
2010
  }
2011
+ async deleteMany(collection, ids) {
2012
+ if (ids.length === 0)
2013
+ return;
2014
+ const table = this.getTable(collection);
2015
+ const keys = ids.map((id) => this.idToString(id));
2016
+ await table.bulkDelete(keys);
2017
+ }
1938
2018
  async saveCollection(collection, data) {
1939
2019
  const table = this.getTable(collection);
1940
2020
  await table.clear();
@@ -1951,6 +2031,13 @@ class DexieDb extends Dexie {
1951
2031
  const key = this.idToString(id);
1952
2032
  return await table.get(key);
1953
2033
  }
2034
+ async getByIds(collection, ids) {
2035
+ if (ids.length === 0)
2036
+ return [];
2037
+ const table = this.getTable(collection);
2038
+ const keys = ids.map((id) => this.idToString(id));
2039
+ return await table.bulkGet(keys);
2040
+ }
1954
2041
  async getAll(collection) {
1955
2042
  const table = this.getTable(collection);
1956
2043
  return await table.toArray();
@@ -1960,23 +2047,97 @@ class DexieDb extends Dexie {
1960
2047
  return await table.count();
1961
2048
  }
1962
2049
  async getDirty(collection) {
1963
- const table = this.getTable(collection);
1964
- return await table.filter((item) => item._dirty === true).toArray();
2050
+ const dirtyEntries = await this.dirtyChanges.where("[collection+id]").between([collection, Dexie.minKey], [collection, Dexie.maxKey]).toArray();
2051
+ return dirtyEntries.map((entry) => ({
2052
+ _id: entry.id,
2053
+ ...entry.changes,
2054
+ _ts: entry.baseTs,
2055
+ _rev: entry.baseRev
2056
+ }));
1965
2057
  }
1966
- async markDirty(collection, id) {
1967
- const table = this.getTable(collection);
1968
- const key = this.idToString(id);
1969
- await table.update(key, {
1970
- _dirty: true,
1971
- _localChangedAt: new Date
1972
- });
2058
+ async addDirtyChange(collection, id, changes, baseMeta) {
2059
+ const stringId = this.idToString(id);
2060
+ const existing = await this.dirtyChanges.get([collection, stringId]);
2061
+ const now = Date.now();
2062
+ if (existing) {
2063
+ await this.dirtyChanges.put({
2064
+ ...existing,
2065
+ changes: { ...existing.changes, ...changes },
2066
+ updatedAt: now
2067
+ });
2068
+ } else {
2069
+ await this.dirtyChanges.put({
2070
+ collection,
2071
+ id: stringId,
2072
+ changes,
2073
+ baseTs: baseMeta?._ts,
2074
+ baseRev: baseMeta?._rev,
2075
+ createdAt: now,
2076
+ updatedAt: now
2077
+ });
2078
+ }
1973
2079
  }
1974
- async markClean(collection, id) {
1975
- const table = this.getTable(collection);
1976
- const key = this.idToString(id);
1977
- await table.update(key, {
1978
- _dirty: false
1979
- });
2080
+ async addDirtyChangesBatch(collection, changesList) {
2081
+ if (changesList.length === 0)
2082
+ return;
2083
+ const now = Date.now();
2084
+ const keys = changesList.map((c) => [collection, this.idToString(c.id)]);
2085
+ const existingEntries = await this.dirtyChanges.bulkGet(keys);
2086
+ const toWrite = [];
2087
+ for (let i = 0;i < changesList.length; i++) {
2088
+ const changeItem = changesList[i];
2089
+ const stringId = this.idToString(changeItem.id);
2090
+ const existing = existingEntries[i];
2091
+ if (existing) {
2092
+ toWrite.push({
2093
+ ...existing,
2094
+ changes: { ...existing.changes, ...changeItem.changes },
2095
+ updatedAt: now
2096
+ });
2097
+ } else {
2098
+ toWrite.push({
2099
+ collection,
2100
+ id: stringId,
2101
+ changes: changeItem.changes,
2102
+ baseTs: changeItem.baseMeta?._ts,
2103
+ baseRev: changeItem.baseMeta?._rev,
2104
+ createdAt: now,
2105
+ updatedAt: now
2106
+ });
2107
+ }
2108
+ }
2109
+ await this.dirtyChanges.bulkPut(toWrite);
2110
+ }
2111
+ async getDirtyChange(collection, id) {
2112
+ const stringId = this.idToString(id);
2113
+ return this.dirtyChanges.get([collection, stringId]);
2114
+ }
2115
+ async getDirtyChangesBatch(collection, ids) {
2116
+ const result = new Map;
2117
+ if (ids.length === 0)
2118
+ return result;
2119
+ const keys = ids.map((id) => [collection, this.idToString(id)]);
2120
+ const entries = await this.dirtyChanges.bulkGet(keys);
2121
+ for (let i = 0;i < ids.length; i++) {
2122
+ const entry = entries[i];
2123
+ if (entry) {
2124
+ result.set(this.idToString(ids[i]), entry);
2125
+ }
2126
+ }
2127
+ return result;
2128
+ }
2129
+ async clearDirtyChange(collection, id) {
2130
+ const stringId = this.idToString(id);
2131
+ await this.dirtyChanges.delete([collection, stringId]);
2132
+ }
2133
+ async clearDirtyChangesBatch(collection, ids) {
2134
+ if (ids.length === 0)
2135
+ return;
2136
+ const keys = ids.map((id) => [collection, this.idToString(id)]);
2137
+ await this.dirtyChanges.bulkDelete(keys);
2138
+ }
2139
+ async clearDirtyChanges(collection) {
2140
+ await this.dirtyChanges.where("[collection+id]").between([collection, Dexie.minKey], [collection, Dexie.maxKey]).delete();
1980
2141
  }
1981
2142
  async getSyncMeta(collection) {
1982
2143
  return await this.syncMeta.get([this.tenant, collection]);
@@ -1,5 +1,5 @@
1
1
  import Dexie from "dexie";
2
- import type { DexieDbOptions, I_DexieDb, MetaUpdateBroadcast, SyncMeta } from "../types/I_DexieDb";
2
+ import type { DexieDbOptions, DirtyChange, I_DexieDb, MetaUpdateBroadcast, SyncMeta } from "../types/I_DexieDb";
3
3
  import type { CollectionConfig } from "../types/CollectionConfig";
4
4
  import type { Id, LocalDbEntity } from "../types/DbEntity";
5
5
  /**
@@ -12,6 +12,7 @@ export declare class DexieDb extends Dexie implements I_DexieDb {
12
12
  private crossTabSyncDebounceMs;
13
13
  private collections;
14
14
  private syncMeta;
15
+ private dirtyChanges;
15
16
  private broadcastChannel;
16
17
  private pendingBroadcasts;
17
18
  private debounceTimer;
@@ -26,14 +27,31 @@ export declare class DexieDb extends Dexie implements I_DexieDb {
26
27
  insert<T extends LocalDbEntity>(collection: string, data: T): Promise<void>;
27
28
  saveMany<T extends LocalDbEntity>(collection: string, items: T[]): Promise<void>;
28
29
  deleteOne(collection: string, id: Id): Promise<void>;
30
+ deleteMany(collection: string, ids: Id[]): Promise<void>;
29
31
  saveCollection<T extends LocalDbEntity>(collection: string, data: T[]): Promise<void>;
30
32
  deleteCollection(collection: string): Promise<void>;
31
33
  getById<T extends LocalDbEntity>(collection: string, id: Id): Promise<T | undefined>;
34
+ getByIds<T extends LocalDbEntity>(collection: string, ids: Id[]): Promise<(T | undefined)[]>;
32
35
  getAll<T extends LocalDbEntity>(collection: string): Promise<T[]>;
33
36
  count(collection: string): Promise<number>;
34
- getDirty<T extends LocalDbEntity>(collection: string): Promise<T[]>;
35
- markDirty(collection: string, id: Id): Promise<void>;
36
- markClean(collection: string, id: Id): Promise<void>;
37
+ getDirty<T extends LocalDbEntity>(collection: string): Promise<Partial<T>[]>;
38
+ addDirtyChange(collection: string, id: Id, changes: Record<string, any>, baseMeta?: {
39
+ _ts?: any;
40
+ _rev?: number;
41
+ }): Promise<void>;
42
+ addDirtyChangesBatch(collection: string, changesList: Array<{
43
+ id: Id;
44
+ changes: Record<string, any>;
45
+ baseMeta?: {
46
+ _ts?: any;
47
+ _rev?: number;
48
+ };
49
+ }>): Promise<void>;
50
+ getDirtyChange(collection: string, id: Id): Promise<DirtyChange | undefined>;
51
+ getDirtyChangesBatch(collection: string, ids: Id[]): Promise<Map<string, DirtyChange>>;
52
+ clearDirtyChange(collection: string, id: Id): Promise<void>;
53
+ clearDirtyChangesBatch(collection: string, ids: Id[]): Promise<void>;
54
+ clearDirtyChanges(collection: string): Promise<void>;
37
55
  getSyncMeta(collection: string): Promise<SyncMeta | undefined>;
38
56
  setSyncMeta(collection: string, lastSyncTs: any): Promise<void>;
39
57
  deleteSyncMeta(collection: string): Promise<void>;
@@ -202,14 +202,14 @@ export declare class SyncedDb implements I_SyncedDb {
202
202
  /**
203
203
  * Handle server insert notification.
204
204
  * Insert notifications contain full object data.
205
- * Only writes to Dexie if metadata changed or _dirty needs clearing.
205
+ * Only writes to Dexie if metadata changed. Clears dirty change if loopback confirmed.
206
206
  */
207
207
  private handleServerItemInsert;
208
208
  /**
209
209
  * Handle server update notification (delta).
210
210
  * Update notifications contain only changed fields.
211
211
  * Caller must provide the existing local item.
212
- * Only writes to Dexie if metadata changed or _dirty needs updating.
212
+ * Only writes to Dexie if metadata changed.
213
213
  */
214
214
  private handleServerItemUpdate;
215
215
  private getNewFieldsFromServer;
@@ -21,10 +21,9 @@ export interface DbEntity {
21
21
  /**
22
22
  * Razširitev DbEntity z lokalnimi polji za sync tracking
23
23
  * Uporablja se v dexie bazi
24
+ *
25
+ * Note: _dirty field was removed. Dirty tracking is now handled via
26
+ * the _dirty_changes table in DexieDb with field-level granularity.
24
27
  */
25
28
  export interface LocalDbEntity extends DbEntity {
26
- /** Označuje, da ima objekt lokalne spremembe, ki čakajo na sync */
27
- _dirty?: boolean;
28
- /** Čas zadnje lokalne spremembe */
29
- _localChangedAt?: Date;
30
29
  }
@@ -7,6 +7,26 @@ export interface SyncMeta {
7
7
  collection: string;
8
8
  lastSyncTs?: any;
9
9
  }
10
+ /**
11
+ * Dirty change entry - tracks accumulated changes for a record
12
+ * Stored in _dirty_changes table with composite key [collection, id]
13
+ */
14
+ export interface DirtyChange {
15
+ /** Collection name */
16
+ collection: string;
17
+ /** Stringified Id */
18
+ id: string;
19
+ /** Accumulated changed fields */
20
+ changes: Record<string, any>;
21
+ /** _ts from base record when change was made */
22
+ baseTs?: any;
23
+ /** _rev from base record when change was made */
24
+ baseRev?: number;
25
+ /** When first change was recorded */
26
+ createdAt: number;
27
+ /** When last change was accumulated */
28
+ updatedAt: number;
29
+ }
10
30
  /**
11
31
  * Payload for cross-tab meta update broadcasts
12
32
  */
@@ -37,22 +57,46 @@ export interface I_DexieDb {
37
57
  insert<T extends LocalDbEntity>(collection: string, data: T): Promise<void>;
38
58
  /** Izbriše objekt iz kolekcije */
39
59
  deleteOne(collection: string, id: Id): Promise<void>;
60
+ /** Izbriše več objektov iz kolekcije (batch) */
61
+ deleteMany(collection: string, ids: Id[]): Promise<void>;
40
62
  /** Shrani celotno kolekcijo (nadomesti obstoječo) */
41
63
  saveCollection<T extends LocalDbEntity>(collection: string, data: T[]): Promise<void>;
42
64
  /** Izbriše celotno kolekcijo */
43
65
  deleteCollection(collection: string): Promise<void>;
44
66
  /** Vrne objekt po ID-ju */
45
67
  getById<T extends LocalDbEntity>(collection: string, id: Id): Promise<T | undefined>;
68
+ /** Vrne več objektov po ID-jih (batch) */
69
+ getByIds<T extends LocalDbEntity>(collection: string, ids: Id[]): Promise<(T | undefined)[]>;
46
70
  /** Vrne vse objekte v kolekciji */
47
71
  getAll<T extends LocalDbEntity>(collection: string): Promise<T[]>;
48
72
  /** Vrne število objektov v kolekciji */
49
73
  count(collection: string): Promise<number>;
50
- /** Vrne vse dirty objekte (z lokalnimi spremembami) */
51
- getDirty<T extends LocalDbEntity>(collection: string): Promise<T[]>;
52
- /** Označi objekt kot umazan (ima lokalne spremembe) */
53
- markDirty(collection: string, id: Id): Promise<void>;
54
- /** Označi objekt kot čist (sinhroniziran s serverjem) */
55
- markClean(collection: string, id: Id): Promise<void>;
74
+ /** Vrne vse dirty objekte (z lokalnimi spremembami) - returns only changed fields + _id + metadata */
75
+ getDirty<T extends LocalDbEntity>(collection: string): Promise<Partial<T>[]>;
76
+ /** Add or accumulate changes for a record */
77
+ addDirtyChange(collection: string, id: Id, changes: Record<string, any>, baseMeta?: {
78
+ _ts?: any;
79
+ _rev?: number;
80
+ }): Promise<void>;
81
+ /** Add or accumulate changes for multiple records (batch) */
82
+ addDirtyChangesBatch(collection: string, changes: Array<{
83
+ id: Id;
84
+ changes: Record<string, any>;
85
+ baseMeta?: {
86
+ _ts?: any;
87
+ _rev?: number;
88
+ };
89
+ }>): Promise<void>;
90
+ /** Get dirty change entry for a specific record */
91
+ getDirtyChange(collection: string, id: Id): Promise<DirtyChange | undefined>;
92
+ /** Get dirty change entries for multiple records (batch) */
93
+ getDirtyChangesBatch(collection: string, ids: Id[]): Promise<Map<string, DirtyChange>>;
94
+ /** Clear dirty change for a record (after successful sync) */
95
+ clearDirtyChange(collection: string, id: Id): Promise<void>;
96
+ /** Clear dirty changes for multiple records (batch) */
97
+ clearDirtyChangesBatch(collection: string, ids: Id[]): Promise<void>;
98
+ /** Clear all dirty changes for a collection */
99
+ clearDirtyChanges(collection: string): Promise<void>;
56
100
  /** Vrne sync metapodatke za kolekcijo */
57
101
  getSyncMeta(collection: string): Promise<SyncMeta | undefined>;
58
102
  /** Nastavi sync metapodatke za kolekcijo */
@@ -12,6 +12,8 @@ export interface I_InMemDb {
12
12
  insert<T extends DbEntity>(collection: string, data: T): void;
13
13
  /** Izbriše objekt iz kolekcije */
14
14
  deleteOne(collection: string, id: Id): void;
15
+ /** Izbriše več objektov iz kolekcije po ID-jih */
16
+ deleteManyByIds(collection: string, ids: Id[]): void;
15
17
  /** Shrani celotno kolekcijo (nadomesti obstoječo) */
16
18
  saveCollection<T extends DbEntity>(collection: string, data: T[]): void;
17
19
  /** Izbriše celotno kolekcijo */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",