@twin.org/synchronised-storage-service 0.0.1-next.5 → 0.0.1-next.6

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.
@@ -47,6 +47,10 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
47
47
  * The flag to determine if this is a consolidated snapshot.
48
48
  */
49
49
  isConsolidated;
50
+ /**
51
+ * The epoch for the changeset.
52
+ */
53
+ epoch;
50
54
  /**
51
55
  * The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
52
56
  */
@@ -84,6 +88,10 @@ __decorate([
84
88
  entity.property({ type: "boolean" }),
85
89
  __metadata("design:type", Boolean)
86
90
  ], exports.SyncSnapshotEntry.prototype, "isConsolidated", void 0);
91
+ __decorate([
92
+ entity.property({ type: "number" }),
93
+ __metadata("design:type", Number)
94
+ ], exports.SyncSnapshotEntry.prototype, "epoch", void 0);
87
95
  __decorate([
88
96
  entity.property({ type: "array", itemType: "string", optional: true }),
89
97
  __metadata("design:type", Array)
@@ -867,6 +875,9 @@ class LocalSyncStateHelper {
867
875
  if (previousChangeIndex !== -1) {
868
876
  localChangeSnapshot.changes.splice(previousChangeIndex, 1);
869
877
  }
878
+ // If we already have changes from previous updates
879
+ // then make sure we update the dateModified, otherwise
880
+ // we assume this is the first change and setting modified is not necessary
870
881
  if (localChangeSnapshot.changes.length > 0) {
871
882
  localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
872
883
  }
@@ -932,7 +943,8 @@ class LocalSyncStateHelper {
932
943
  dateModified: now,
933
944
  changeSetStorageIds: [],
934
945
  isLocal,
935
- isConsolidated: false
946
+ isConsolidated: false,
947
+ epoch: 0
936
948
  }
937
949
  ];
938
950
  }
@@ -984,13 +996,22 @@ class LocalSyncStateHelper {
984
996
  }
985
997
  });
986
998
  // Get all the existing snapshots that we have processed previously
987
- const existingRemoteSnapshots = await this.getSnapshots(storageKey, false);
999
+ let existingSnapshots = await this.getSnapshots(storageKey, false);
988
1000
  // Sort from newest to oldest
989
- const sortedSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
990
- // If we have no existing snapshots we can't have yet synced
991
- // in this case we need to find the most recent consolidation
992
- // and use that to build a complete DB table
993
- if (existingRemoteSnapshots.length === 0) {
1001
+ existingSnapshots = existingSnapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
1002
+ // Sort from newest to oldest
1003
+ const syncStateSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
1004
+ // Get the newest epoch from the local storage
1005
+ const newestExistingEpoch = existingSnapshots[0]?.epoch ?? 0;
1006
+ // Get the oldest epoch from the remote storage
1007
+ const oldestSyncStateEpoch = syncStateSnapshots[syncStateSnapshots.length - 1]?.epoch ?? 0;
1008
+ // If there is a gap between the largest epoch we have locally
1009
+ // and the smallest epoch we have remotely then we have missed
1010
+ // data so we need to perform a full sync
1011
+ const hasEpochGap = newestExistingEpoch + 1 < oldestSyncStateEpoch;
1012
+ // If we have an epoch gap or no existing snapshots then we need to apply
1013
+ // a full sync from a consolidation
1014
+ if (!existingSnapshots.some(s => s.isConsolidated) || hasEpochGap) {
994
1015
  await this._logging?.log({
995
1016
  level: "info",
996
1017
  source: this.CLASS_NAME,
@@ -999,28 +1020,35 @@ class LocalSyncStateHelper {
999
1020
  storageKey
1000
1021
  }
1001
1022
  });
1002
- const firstConsolidated = sortedSnapshots.find(snapshot => snapshot.isConsolidated);
1003
- if (firstConsolidated) {
1004
- // We found a consolidated snapshot, we can use it
1023
+ const mostRecentConsolidation = syncStateSnapshots.findIndex(snapshot => snapshot.isConsolidated);
1024
+ if (mostRecentConsolidation !== -1) {
1025
+ // We found the most recent consolidated snapshot, we can use it
1005
1026
  await this._logging?.log({
1006
1027
  level: "info",
1007
1028
  source: this.CLASS_NAME,
1008
1029
  message: "applySnapshotFoundConsolidated",
1009
1030
  data: {
1010
1031
  storageKey,
1011
- snapshotId: firstConsolidated.id
1032
+ snapshotId: syncStateSnapshots[mostRecentConsolidation].id
1012
1033
  }
1013
1034
  });
1014
1035
  // We need to reset the entity storage and remove all the remote items
1015
- // so that we use just the ones from the consolidation
1036
+ // so that we use just the ones from the consolidation, since
1037
+ // we don't have any existing there shouldn't be any remote entries
1038
+ // but we reset nonetheless
1016
1039
  await this._changeSetHelper.reset(storageKey, synchronisedStorageModels.SyncNodeIdentityMode.Remote);
1017
- await this.processNewSnapshots([
1018
- {
1019
- ...firstConsolidated,
1020
- storageKey,
1021
- isLocal: false
1022
- }
1023
- ]);
1040
+ // We need to process the most recent consolidation and all changes
1041
+ // that were made since then, from newest to oldest (so newer changes override older ones)
1042
+ // Process snapshots from the consolidation point (most recent) back to the newest
1043
+ for (let i = mostRecentConsolidation; i >= 0; i--) {
1044
+ await this.processNewSnapshots([
1045
+ {
1046
+ ...syncStateSnapshots[i],
1047
+ storageKey,
1048
+ isLocal: false
1049
+ }
1050
+ ]);
1051
+ }
1024
1052
  }
1025
1053
  else {
1026
1054
  await this._logging?.log({
@@ -1034,15 +1062,20 @@ class LocalSyncStateHelper {
1034
1062
  }
1035
1063
  }
1036
1064
  else {
1065
+ // We have existing consolidated remote snapshots, so we can assume that we have
1066
+ // applied at least one consolidation snapshot, in this case we need to look at the changes since
1067
+ // then and apply them if we haven't already
1068
+ // We don't need to apply any additional consolidated snapshots, just the changesets
1037
1069
  // Create a lookup map for the existing snapshots
1038
- const existingSnapshots = {};
1039
- for (const snapshot of existingRemoteSnapshots) {
1040
- existingSnapshots[snapshot.id] = snapshot;
1070
+ const existingSnapshotsMap = {};
1071
+ for (const snapshot of existingSnapshots) {
1072
+ existingSnapshotsMap[snapshot.id] = snapshot;
1041
1073
  }
1042
1074
  const newSnapshots = [];
1043
1075
  const modifiedSnapshots = [];
1044
- const referencedExistingSnapshots = Object.keys(existingSnapshots);
1045
- for (const snapshot of sortedSnapshots) {
1076
+ const referencedExistingSnapshots = Object.keys(existingSnapshotsMap);
1077
+ let completedProcessing = false;
1078
+ for (const snapshot of syncStateSnapshots) {
1046
1079
  await this._logging?.log({
1047
1080
  level: "info",
1048
1081
  source: this.CLASS_NAME,
@@ -1052,34 +1085,37 @@ class LocalSyncStateHelper {
1052
1085
  dateCreated: new Date(snapshot.dateCreated).toISOString()
1053
1086
  }
1054
1087
  });
1055
- // See if we have the local snapshot
1056
- const currentSnapshot = existingSnapshots[snapshot.id];
1088
+ // See if we have the snapshot stored locally
1089
+ const currentSnapshot = existingSnapshotsMap[snapshot.id];
1057
1090
  // As we are referencing an existing snapshot, we need to remove it from the list
1058
1091
  // to allow us to cleanup any unreferenced snapshots later
1059
1092
  const idx = referencedExistingSnapshots.indexOf(snapshot.id);
1060
1093
  if (idx !== -1) {
1061
1094
  referencedExistingSnapshots.splice(idx, 1);
1062
1095
  }
1063
- const updatedSnapshot = {
1064
- ...snapshot,
1065
- storageKey,
1066
- isLocal: false
1067
- };
1068
- if (core.Is.empty(currentSnapshot)) {
1069
- // We don't have the snapshot locally, so we need to process it
1070
- newSnapshots.push(updatedSnapshot);
1071
- }
1072
- else if (currentSnapshot.dateModified !== snapshot.dateModified) {
1073
- // If the local snapshot has a different dateModified, we need to update it
1074
- modifiedSnapshots.push({
1075
- currentSnapshot,
1076
- updatedSnapshot
1077
- });
1078
- }
1079
- else {
1080
- // we sorted the snapshots from newest to oldest, so if we found a local snapshot
1081
- // with the same dateModified as the remote snapshot, we can stop processing further
1082
- break;
1096
+ // No need to apply consolidated snapshots
1097
+ if (!snapshot.isConsolidated && !completedProcessing) {
1098
+ const updatedSnapshot = {
1099
+ ...snapshot,
1100
+ storageKey,
1101
+ isLocal: false
1102
+ };
1103
+ if (core.Is.empty(currentSnapshot)) {
1104
+ // We don't have the snapshot locally, so we need to process all of it
1105
+ newSnapshots.push(updatedSnapshot);
1106
+ }
1107
+ else if (currentSnapshot.dateModified !== snapshot.dateModified) {
1108
+ // If the local snapshot has a different dateModified, we need to update it
1109
+ modifiedSnapshots.push({
1110
+ currentSnapshot,
1111
+ updatedSnapshot
1112
+ });
1113
+ }
1114
+ else {
1115
+ // we sorted the snapshots from newest to oldest, so if we found a local snapshot
1116
+ // with the same dateModified as the remote snapshot, we can stop processing further
1117
+ completedProcessing = true;
1118
+ }
1083
1119
  }
1084
1120
  }
1085
1121
  // We reverse the order of the snapshots to process them from oldest to newest
@@ -1404,6 +1440,7 @@ class RemoteSyncStateHelper {
1404
1440
  const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
1405
1441
  // Get the current snapshot, if it does not exist we create a new one
1406
1442
  let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
1443
+ const currentEpoch = currentSnapshot?.epoch ?? 0;
1407
1444
  const now = new Date(Date.now()).toISOString();
1408
1445
  // If there is no snapshot or the current one is a consolidation
1409
1446
  // we start a new snapshot
@@ -1414,6 +1451,7 @@ class RemoteSyncStateHelper {
1414
1451
  dateCreated: now,
1415
1452
  dateModified: now,
1416
1453
  isConsolidated: false,
1454
+ epoch: currentEpoch + 1,
1417
1455
  changeSetStorageIds: []
1418
1456
  };
1419
1457
  syncState.snapshots.push(currentSnapshot);
@@ -1544,7 +1582,12 @@ class RemoteSyncStateHelper {
1544
1582
  const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
1545
1583
  syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
1546
1584
  for (const snapshot of toRemove) {
1547
- await this._blobStorageHelper.removeBlob(snapshot.id);
1585
+ // We need to remove all the storage ids associated with the snapshot
1586
+ if (core.Is.arrayValue(snapshot.changeSetStorageIds)) {
1587
+ for (const storageId of snapshot.changeSetStorageIds) {
1588
+ await this._blobStorageHelper.removeBlob(storageId);
1589
+ }
1590
+ }
1548
1591
  }
1549
1592
  }
1550
1593
  return this._blobStorageHelper.saveBlob(syncState);
@@ -1639,12 +1682,17 @@ class RemoteSyncStateHelper {
1639
1682
  storageKey: response.storageKey,
1640
1683
  snapshots: []
1641
1684
  };
1685
+ // Sort the snapshots so the newest snapshot is last in the array
1686
+ const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
1687
+ const currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
1688
+ const currentEpoch = currentSnapshot?.epoch ?? 0;
1642
1689
  const batchSnapshot = {
1643
1690
  version: SYNC_SNAPSHOT_VERSION,
1644
1691
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1645
1692
  dateCreated: now,
1646
1693
  dateModified: now,
1647
1694
  isConsolidated: true,
1695
+ epoch: currentEpoch + 1,
1648
1696
  changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
1649
1697
  };
1650
1698
  syncState.snapshots.push(batchSnapshot);
@@ -2099,25 +2147,17 @@ class SynchronisedStorageService {
2099
2147
  * @internal
2100
2148
  */
2101
2149
  async startConsolidationSync(storageKey) {
2102
- let localChangeSnapshot;
2103
2150
  try {
2104
- // If we are performing a consolidation, we can remove the local change snapshot
2105
- // as we are going to create a complete changeset from the DB
2106
- const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
2107
- localChangeSnapshot = localChangeSnapshots[0];
2108
- if (!core.Is.empty(localChangeSnapshot)) {
2109
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2110
- }
2151
+ // If we are going to perform a consolidation first take any local updates
2152
+ // we have and create a changeset from them, so that anybody applying
2153
+ // just changes since a consolidation can use the changeset
2154
+ // and skip the consolidation
2155
+ await this.updateFromLocalSyncState(storageKey);
2156
+ // Now start the consolidation
2111
2157
  await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
2112
2158
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
2113
- // The consolidation was successful, so we can remove the local change snapshot permanently
2114
- localChangeSnapshot = undefined;
2115
2159
  }
2116
2160
  catch (error) {
2117
- if (localChangeSnapshot) {
2118
- // If the consolidation failed, we can keep the local change snapshot
2119
- await this._localSyncStateHelper.setLocalChangeSnapshot(localChangeSnapshot);
2120
- }
2121
2161
  await this._logging?.log({
2122
2162
  level: "error",
2123
2163
  source: this.CLASS_NAME,
@@ -45,6 +45,10 @@ let SyncSnapshotEntry = class SyncSnapshotEntry {
45
45
  * The flag to determine if this is a consolidated snapshot.
46
46
  */
47
47
  isConsolidated;
48
+ /**
49
+ * The epoch for the changeset.
50
+ */
51
+ epoch;
48
52
  /**
49
53
  * The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
50
54
  */
@@ -82,6 +86,10 @@ __decorate([
82
86
  property({ type: "boolean" }),
83
87
  __metadata("design:type", Boolean)
84
88
  ], SyncSnapshotEntry.prototype, "isConsolidated", void 0);
89
+ __decorate([
90
+ property({ type: "number" }),
91
+ __metadata("design:type", Number)
92
+ ], SyncSnapshotEntry.prototype, "epoch", void 0);
85
93
  __decorate([
86
94
  property({ type: "array", itemType: "string", optional: true }),
87
95
  __metadata("design:type", Array)
@@ -865,6 +873,9 @@ class LocalSyncStateHelper {
865
873
  if (previousChangeIndex !== -1) {
866
874
  localChangeSnapshot.changes.splice(previousChangeIndex, 1);
867
875
  }
876
+ // If we already have changes from previous updates
877
+ // then make sure we update the dateModified, otherwise
878
+ // we assume this is the first change and setting modified is not necessary
868
879
  if (localChangeSnapshot.changes.length > 0) {
869
880
  localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
870
881
  }
@@ -930,7 +941,8 @@ class LocalSyncStateHelper {
930
941
  dateModified: now,
931
942
  changeSetStorageIds: [],
932
943
  isLocal,
933
- isConsolidated: false
944
+ isConsolidated: false,
945
+ epoch: 0
934
946
  }
935
947
  ];
936
948
  }
@@ -982,13 +994,22 @@ class LocalSyncStateHelper {
982
994
  }
983
995
  });
984
996
  // Get all the existing snapshots that we have processed previously
985
- const existingRemoteSnapshots = await this.getSnapshots(storageKey, false);
997
+ let existingSnapshots = await this.getSnapshots(storageKey, false);
986
998
  // Sort from newest to oldest
987
- const sortedSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
988
- // If we have no existing snapshots we can't have yet synced
989
- // in this case we need to find the most recent consolidation
990
- // and use that to build a complete DB table
991
- if (existingRemoteSnapshots.length === 0) {
999
+ existingSnapshots = existingSnapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
1000
+ // Sort from newest to oldest
1001
+ const syncStateSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
1002
+ // Get the newest epoch from the local storage
1003
+ const newestExistingEpoch = existingSnapshots[0]?.epoch ?? 0;
1004
+ // Get the oldest epoch from the remote storage
1005
+ const oldestSyncStateEpoch = syncStateSnapshots[syncStateSnapshots.length - 1]?.epoch ?? 0;
1006
+ // If there is a gap between the largest epoch we have locally
1007
+ // and the smallest epoch we have remotely then we have missed
1008
+ // data so we need to perform a full sync
1009
+ const hasEpochGap = newestExistingEpoch + 1 < oldestSyncStateEpoch;
1010
+ // If we have an epoch gap or no existing snapshots then we need to apply
1011
+ // a full sync from a consolidation
1012
+ if (!existingSnapshots.some(s => s.isConsolidated) || hasEpochGap) {
992
1013
  await this._logging?.log({
993
1014
  level: "info",
994
1015
  source: this.CLASS_NAME,
@@ -997,28 +1018,35 @@ class LocalSyncStateHelper {
997
1018
  storageKey
998
1019
  }
999
1020
  });
1000
- const firstConsolidated = sortedSnapshots.find(snapshot => snapshot.isConsolidated);
1001
- if (firstConsolidated) {
1002
- // We found a consolidated snapshot, we can use it
1021
+ const mostRecentConsolidation = syncStateSnapshots.findIndex(snapshot => snapshot.isConsolidated);
1022
+ if (mostRecentConsolidation !== -1) {
1023
+ // We found the most recent consolidated snapshot, we can use it
1003
1024
  await this._logging?.log({
1004
1025
  level: "info",
1005
1026
  source: this.CLASS_NAME,
1006
1027
  message: "applySnapshotFoundConsolidated",
1007
1028
  data: {
1008
1029
  storageKey,
1009
- snapshotId: firstConsolidated.id
1030
+ snapshotId: syncStateSnapshots[mostRecentConsolidation].id
1010
1031
  }
1011
1032
  });
1012
1033
  // We need to reset the entity storage and remove all the remote items
1013
- // so that we use just the ones from the consolidation
1034
+ // so that we use just the ones from the consolidation, since
1035
+ // we don't have any existing there shouldn't be any remote entries
1036
+ // but we reset nonetheless
1014
1037
  await this._changeSetHelper.reset(storageKey, SyncNodeIdentityMode.Remote);
1015
- await this.processNewSnapshots([
1016
- {
1017
- ...firstConsolidated,
1018
- storageKey,
1019
- isLocal: false
1020
- }
1021
- ]);
1038
+ // We need to process the most recent consolidation and all changes
1039
+ // that were made since then, from newest to oldest (so newer changes override older ones)
1040
+ // Process snapshots from the consolidation point (most recent) back to the newest
1041
+ for (let i = mostRecentConsolidation; i >= 0; i--) {
1042
+ await this.processNewSnapshots([
1043
+ {
1044
+ ...syncStateSnapshots[i],
1045
+ storageKey,
1046
+ isLocal: false
1047
+ }
1048
+ ]);
1049
+ }
1022
1050
  }
1023
1051
  else {
1024
1052
  await this._logging?.log({
@@ -1032,15 +1060,20 @@ class LocalSyncStateHelper {
1032
1060
  }
1033
1061
  }
1034
1062
  else {
1063
+ // We have existing consolidated remote snapshots, so we can assume that we have
1064
+ // applied at least one consolidation snapshot, in this case we need to look at the changes since
1065
+ // then and apply them if we haven't already
1066
+ // We don't need to apply any additional consolidated snapshots, just the changesets
1035
1067
  // Create a lookup map for the existing snapshots
1036
- const existingSnapshots = {};
1037
- for (const snapshot of existingRemoteSnapshots) {
1038
- existingSnapshots[snapshot.id] = snapshot;
1068
+ const existingSnapshotsMap = {};
1069
+ for (const snapshot of existingSnapshots) {
1070
+ existingSnapshotsMap[snapshot.id] = snapshot;
1039
1071
  }
1040
1072
  const newSnapshots = [];
1041
1073
  const modifiedSnapshots = [];
1042
- const referencedExistingSnapshots = Object.keys(existingSnapshots);
1043
- for (const snapshot of sortedSnapshots) {
1074
+ const referencedExistingSnapshots = Object.keys(existingSnapshotsMap);
1075
+ let completedProcessing = false;
1076
+ for (const snapshot of syncStateSnapshots) {
1044
1077
  await this._logging?.log({
1045
1078
  level: "info",
1046
1079
  source: this.CLASS_NAME,
@@ -1050,34 +1083,37 @@ class LocalSyncStateHelper {
1050
1083
  dateCreated: new Date(snapshot.dateCreated).toISOString()
1051
1084
  }
1052
1085
  });
1053
- // See if we have the local snapshot
1054
- const currentSnapshot = existingSnapshots[snapshot.id];
1086
+ // See if we have the snapshot stored locally
1087
+ const currentSnapshot = existingSnapshotsMap[snapshot.id];
1055
1088
  // As we are referencing an existing snapshot, we need to remove it from the list
1056
1089
  // to allow us to cleanup any unreferenced snapshots later
1057
1090
  const idx = referencedExistingSnapshots.indexOf(snapshot.id);
1058
1091
  if (idx !== -1) {
1059
1092
  referencedExistingSnapshots.splice(idx, 1);
1060
1093
  }
1061
- const updatedSnapshot = {
1062
- ...snapshot,
1063
- storageKey,
1064
- isLocal: false
1065
- };
1066
- if (Is.empty(currentSnapshot)) {
1067
- // We don't have the snapshot locally, so we need to process it
1068
- newSnapshots.push(updatedSnapshot);
1069
- }
1070
- else if (currentSnapshot.dateModified !== snapshot.dateModified) {
1071
- // If the local snapshot has a different dateModified, we need to update it
1072
- modifiedSnapshots.push({
1073
- currentSnapshot,
1074
- updatedSnapshot
1075
- });
1076
- }
1077
- else {
1078
- // we sorted the snapshots from newest to oldest, so if we found a local snapshot
1079
- // with the same dateModified as the remote snapshot, we can stop processing further
1080
- break;
1094
+ // No need to apply consolidated snapshots
1095
+ if (!snapshot.isConsolidated && !completedProcessing) {
1096
+ const updatedSnapshot = {
1097
+ ...snapshot,
1098
+ storageKey,
1099
+ isLocal: false
1100
+ };
1101
+ if (Is.empty(currentSnapshot)) {
1102
+ // We don't have the snapshot locally, so we need to process all of it
1103
+ newSnapshots.push(updatedSnapshot);
1104
+ }
1105
+ else if (currentSnapshot.dateModified !== snapshot.dateModified) {
1106
+ // If the local snapshot has a different dateModified, we need to update it
1107
+ modifiedSnapshots.push({
1108
+ currentSnapshot,
1109
+ updatedSnapshot
1110
+ });
1111
+ }
1112
+ else {
1113
+ // we sorted the snapshots from newest to oldest, so if we found a local snapshot
1114
+ // with the same dateModified as the remote snapshot, we can stop processing further
1115
+ completedProcessing = true;
1116
+ }
1081
1117
  }
1082
1118
  }
1083
1119
  // We reverse the order of the snapshots to process them from oldest to newest
@@ -1402,6 +1438,7 @@ class RemoteSyncStateHelper {
1402
1438
  const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
1403
1439
  // Get the current snapshot, if it does not exist we create a new one
1404
1440
  let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
1441
+ const currentEpoch = currentSnapshot?.epoch ?? 0;
1405
1442
  const now = new Date(Date.now()).toISOString();
1406
1443
  // If there is no snapshot or the current one is a consolidation
1407
1444
  // we start a new snapshot
@@ -1412,6 +1449,7 @@ class RemoteSyncStateHelper {
1412
1449
  dateCreated: now,
1413
1450
  dateModified: now,
1414
1451
  isConsolidated: false,
1452
+ epoch: currentEpoch + 1,
1415
1453
  changeSetStorageIds: []
1416
1454
  };
1417
1455
  syncState.snapshots.push(currentSnapshot);
@@ -1542,7 +1580,12 @@ class RemoteSyncStateHelper {
1542
1580
  const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
1543
1581
  syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
1544
1582
  for (const snapshot of toRemove) {
1545
- await this._blobStorageHelper.removeBlob(snapshot.id);
1583
+ // We need to remove all the storage ids associated with the snapshot
1584
+ if (Is.arrayValue(snapshot.changeSetStorageIds)) {
1585
+ for (const storageId of snapshot.changeSetStorageIds) {
1586
+ await this._blobStorageHelper.removeBlob(storageId);
1587
+ }
1588
+ }
1546
1589
  }
1547
1590
  }
1548
1591
  return this._blobStorageHelper.saveBlob(syncState);
@@ -1637,12 +1680,17 @@ class RemoteSyncStateHelper {
1637
1680
  storageKey: response.storageKey,
1638
1681
  snapshots: []
1639
1682
  };
1683
+ // Sort the snapshots so the newest snapshot is last in the array
1684
+ const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
1685
+ const currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
1686
+ const currentEpoch = currentSnapshot?.epoch ?? 0;
1640
1687
  const batchSnapshot = {
1641
1688
  version: SYNC_SNAPSHOT_VERSION,
1642
1689
  id: Converter.bytesToHex(RandomHelper.generate(32)),
1643
1690
  dateCreated: now,
1644
1691
  dateModified: now,
1645
1692
  isConsolidated: true,
1693
+ epoch: currentEpoch + 1,
1646
1694
  changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
1647
1695
  };
1648
1696
  syncState.snapshots.push(batchSnapshot);
@@ -2097,25 +2145,17 @@ class SynchronisedStorageService {
2097
2145
  * @internal
2098
2146
  */
2099
2147
  async startConsolidationSync(storageKey) {
2100
- let localChangeSnapshot;
2101
2148
  try {
2102
- // If we are performing a consolidation, we can remove the local change snapshot
2103
- // as we are going to create a complete changeset from the DB
2104
- const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
2105
- localChangeSnapshot = localChangeSnapshots[0];
2106
- if (!Is.empty(localChangeSnapshot)) {
2107
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2108
- }
2149
+ // If we are going to perform a consolidation first take any local updates
2150
+ // we have and create a changeset from them, so that anybody applying
2151
+ // just changes since a consolidation can use the changeset
2152
+ // and skip the consolidation
2153
+ await this.updateFromLocalSyncState(storageKey);
2154
+ // Now start the consolidation
2109
2155
  await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
2110
2156
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
2111
- // The consolidation was successful, so we can remove the local change snapshot permanently
2112
- localChangeSnapshot = undefined;
2113
2157
  }
2114
2158
  catch (error) {
2115
- if (localChangeSnapshot) {
2116
- // If the consolidation failed, we can keep the local change snapshot
2117
- await this._localSyncStateHelper.setLocalChangeSnapshot(localChangeSnapshot);
2118
- }
2119
2159
  await this._logging?.log({
2120
2160
  level: "error",
2121
2161
  source: this.CLASS_NAME,
@@ -31,6 +31,10 @@ export declare class SyncSnapshotEntry<T extends ISynchronisedEntity = ISynchron
31
31
  * The flag to determine if this is a consolidated snapshot.
32
32
  */
33
33
  isConsolidated: boolean;
34
+ /**
35
+ * The epoch for the changeset.
36
+ */
37
+ epoch: number;
34
38
  /**
35
39
  * The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
36
40
  */
@@ -22,6 +22,10 @@ export interface ISyncSnapshot {
22
22
  * Is this a consolidated snapshot?
23
23
  */
24
24
  isConsolidated: boolean;
25
+ /**
26
+ * The epoch of the snapshot.
27
+ */
28
+ epoch: number;
25
29
  /**
26
30
  * The ids of the storage for the change sets in the snapshot.
27
31
  */
package/docs/changelog.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.1-next.6](https://github.com/twinfoundation/synchronised-storage/compare/synchronised-storage-service-v0.0.1-next.5...synchronised-storage-service-v0.0.1-next.6) (2025-08-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * improve consolidation logic ([698232f](https://github.com/twinfoundation/synchronised-storage/commit/698232f57640f87642ecd323cb1e4670eda33343))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/synchronised-storage-models bumped from 0.0.1-next.5 to 0.0.1-next.6
16
+
3
17
  ## [0.0.1-next.5](https://github.com/twinfoundation/synchronised-storage/compare/synchronised-storage-service-v0.0.1-next.4...synchronised-storage-service-v0.0.1-next.5) (2025-08-11)
4
18
 
5
19
 
@@ -76,6 +76,14 @@ The flag to determine if this is a consolidated snapshot.
76
76
 
77
77
  ***
78
78
 
79
+ ### epoch
80
+
81
+ > **epoch**: `number`
82
+
83
+ The epoch for the changeset.
84
+
85
+ ***
86
+
79
87
  ### changeSetStorageIds?
80
88
 
81
89
  > `optional` **changeSetStorageIds**: `string`[]
@@ -44,6 +44,14 @@ Is this a consolidated snapshot?
44
44
 
45
45
  ***
46
46
 
47
+ ### epoch
48
+
49
+ > **epoch**: `number`
50
+
51
+ The epoch of the snapshot.
52
+
53
+ ***
54
+
47
55
  ### changeSetStorageIds
48
56
 
49
57
  > **changeSetStorageIds**: `string`[]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/synchronised-storage-service",
3
- "version": "0.0.1-next.5",
3
+ "version": "0.0.1-next.6",
4
4
  "description": "Synchronised storage contract implementation and REST endpoint definitions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "@twin.org/logging-models": "next",
27
27
  "@twin.org/nameof": "next",
28
28
  "@twin.org/standards-w3c-did": "next",
29
- "@twin.org/synchronised-storage-models": "0.0.1-next.5",
29
+ "@twin.org/synchronised-storage-models": "0.0.1-next.6",
30
30
  "@twin.org/verifiable-storage-models": "next",
31
31
  "@twin.org/web": "next"
32
32
  },