@twin.org/synchronised-storage-service 0.0.1-next.4 → 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.
@@ -23,6 +23,10 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
23
23
  * The id for the snapshot.
24
24
  */
25
25
  id;
26
+ /**
27
+ * The version for the snapshot.
28
+ */
29
+ version;
26
30
  /**
27
31
  * The storage key for the snapshot i.e. which entity is being synchronized.
28
32
  */
@@ -36,9 +40,17 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
36
40
  */
37
41
  dateModified;
38
42
  /**
39
- * The flag to determine if this is the current local snapshot containing changes for this node.
43
+ * The flag to determine if this is the snapshot is the local one containing changes for this node.
44
+ */
45
+ isLocal;
46
+ /**
47
+ * The flag to determine if this is a consolidated snapshot.
40
48
  */
41
- isLocalSnapshot;
49
+ isConsolidated;
50
+ /**
51
+ * The epoch for the changeset.
52
+ */
53
+ epoch;
42
54
  /**
43
55
  * The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
44
56
  */
@@ -52,6 +64,10 @@ __decorate([
52
64
  entity.property({ type: "string", isPrimary: true }),
53
65
  __metadata("design:type", String)
54
66
  ], exports.SyncSnapshotEntry.prototype, "id", void 0);
67
+ __decorate([
68
+ entity.property({ type: "string" }),
69
+ __metadata("design:type", String)
70
+ ], exports.SyncSnapshotEntry.prototype, "version", void 0);
55
71
  __decorate([
56
72
  entity.property({ type: "string", isSecondary: true }),
57
73
  __metadata("design:type", String)
@@ -61,13 +77,21 @@ __decorate([
61
77
  __metadata("design:type", String)
62
78
  ], exports.SyncSnapshotEntry.prototype, "dateCreated", void 0);
63
79
  __decorate([
64
- entity.property({ type: "string", optional: true }),
80
+ entity.property({ type: "string" }),
65
81
  __metadata("design:type", String)
66
82
  ], exports.SyncSnapshotEntry.prototype, "dateModified", void 0);
67
83
  __decorate([
68
- entity.property({ type: "boolean", optional: true }),
84
+ entity.property({ type: "boolean" }),
69
85
  __metadata("design:type", Boolean)
70
- ], exports.SyncSnapshotEntry.prototype, "isLocalSnapshot", void 0);
86
+ ], exports.SyncSnapshotEntry.prototype, "isLocal", void 0);
87
+ __decorate([
88
+ entity.property({ type: "boolean" }),
89
+ __metadata("design:type", Boolean)
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);
71
95
  __decorate([
72
96
  entity.property({ type: "array", itemType: "string", optional: true }),
73
97
  __metadata("design:type", Array)
@@ -116,6 +140,7 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
116
140
  body: {
117
141
  id: "0909090909090909090909090909090909090909090909090909090909090909",
118
142
  dateCreated: "2025-05-29T01:00:00.000Z",
143
+ dateModified: "2025-05-29T01:00:00.000Z",
119
144
  nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
120
145
  changes: [
121
146
  {
@@ -316,7 +341,7 @@ class BlobStorageHelper {
316
341
  * @param blobId The id of the blob to apply.
317
342
  * @returns The blob.
318
343
  */
319
- async load(blobId) {
344
+ async loadBlob(blobId) {
320
345
  await this._logging?.log({
321
346
  level: "info",
322
347
  source: this.CLASS_NAME,
@@ -409,6 +434,51 @@ class BlobStorageHelper {
409
434
  throw error;
410
435
  }
411
436
  }
437
+ /**
438
+ * Remove a blob from storage.
439
+ * @param blobId The id of the blob to remove.
440
+ * @returns Nothing.
441
+ */
442
+ async removeBlob(blobId) {
443
+ await this._logging?.log({
444
+ level: "info",
445
+ source: this.CLASS_NAME,
446
+ message: "removeBlob",
447
+ data: {
448
+ blobId
449
+ }
450
+ });
451
+ try {
452
+ await this._blobStorageConnector.remove(blobId);
453
+ await this._logging?.log({
454
+ level: "info",
455
+ source: this.CLASS_NAME,
456
+ message: "removedBlob",
457
+ data: {
458
+ blobId
459
+ }
460
+ });
461
+ }
462
+ catch (error) {
463
+ await this._logging?.log({
464
+ level: "error",
465
+ source: this.CLASS_NAME,
466
+ message: "removeBlobFailed",
467
+ data: {
468
+ blobId
469
+ },
470
+ error: core.BaseError.fromError(error)
471
+ });
472
+ }
473
+ await this._logging?.log({
474
+ level: "info",
475
+ source: this.CLASS_NAME,
476
+ message: "removeBlobEmpty",
477
+ data: {
478
+ blobId
479
+ }
480
+ });
481
+ }
412
482
  }
413
483
 
414
484
  // Copyright 2024 IOTA Stiftung.
@@ -488,7 +558,7 @@ class ChangeSetHelper {
488
558
  }
489
559
  });
490
560
  try {
491
- const syncChangeSet = await this._blobStorageHelper.load(changeSetStorageId);
561
+ const syncChangeSet = await this._blobStorageHelper.loadBlob(changeSetStorageId);
492
562
  if (core.Is.object(syncChangeSet)) {
493
563
  const verified = await this.verifyChangesetProof(syncChangeSet);
494
564
  return verified ? syncChangeSet : undefined;
@@ -521,7 +591,9 @@ class ChangeSetHelper {
521
591
  */
522
592
  async getAndApplyChangeset(changeSetStorageId) {
523
593
  const syncChangeset = await this.getAndVerifyChangeset(changeSetStorageId);
524
- if (!core.Is.empty(syncChangeset)) {
594
+ // Only apply changesets from other nodes, we don't want to overwrite
595
+ // any changes we have made to local entity storage
596
+ if (!core.Is.empty(syncChangeset) && syncChangeset.nodeIdentity !== this._nodeIdentity) {
525
597
  await this.applyChangeset(syncChangeset);
526
598
  }
527
599
  return syncChangeset;
@@ -565,7 +637,8 @@ class ChangeSetHelper {
565
637
  if (!core.Is.empty(change.id)) {
566
638
  await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.RemoteItemRemove, {
567
639
  storageKey: syncChangeset.storageKey,
568
- id: change.id
640
+ id: change.id,
641
+ nodeIdentity: syncChangeset.nodeIdentity
569
642
  });
570
643
  }
571
644
  break;
@@ -707,8 +780,36 @@ class ChangeSetHelper {
707
780
  }
708
781
  }
709
782
  }
783
+ /**
784
+ * Reset the storage for a given storage key.
785
+ * @param storageKey The key of the storage to reset.
786
+ * @param resetMode The reset mode, this will use the nodeIdentity in the entities to determine which are local/remote.
787
+ * @returns Nothing.
788
+ */
789
+ async reset(storageKey, resetMode) {
790
+ // If we are applying a consolidation we need to reset the local db
791
+ // but keep any entries from the local node, as they might have been updated
792
+ await this._logging?.log({
793
+ level: "info",
794
+ source: this.CLASS_NAME,
795
+ message: "storageReset",
796
+ data: {
797
+ storageKey
798
+ }
799
+ });
800
+ await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.Reset, {
801
+ storageKey,
802
+ resetMode
803
+ });
804
+ }
710
805
  }
711
806
 
807
+ // Copyright 2024 IOTA Stiftung.
808
+ // SPDX-License-Identifier: Apache-2.0.
809
+ const SYNC_STATE_VERSION = "1";
810
+ const SYNC_POINTER_STORE_VERSION = "1";
811
+ const SYNC_SNAPSHOT_VERSION = "1";
812
+
712
813
  // Copyright 2024 IOTA Stiftung.
713
814
  // SPDX-License-Identifier: Apache-2.0.
714
815
  /**
@@ -763,31 +864,38 @@ class LocalSyncStateHelper {
763
864
  id
764
865
  }
765
866
  });
766
- const localChangeSnapshot = await this.getLocalChangeSnapshot(storageKey);
767
- localChangeSnapshot.changes ??= [];
768
- // If we already have a change for this id we are
769
- // about to supersede it, we remove the previous change
770
- // to avoid having multiple changes for the same id
771
- const previousChangeIndex = localChangeSnapshot.changes.findIndex(change => change.id === id);
772
- if (previousChangeIndex !== -1) {
773
- localChangeSnapshot.changes.splice(previousChangeIndex, 1);
774
- }
775
- if (localChangeSnapshot.changes.length > 0) {
776
- localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
867
+ const localChangeSnapshots = await this.getSnapshots(storageKey, true);
868
+ if (localChangeSnapshots.length > 0) {
869
+ const localChangeSnapshot = localChangeSnapshots[0];
870
+ localChangeSnapshot.changes ??= [];
871
+ // If we already have a change for this id we are
872
+ // about to supersede it, we remove the previous change
873
+ // to avoid having multiple changes for the same id
874
+ const previousChangeIndex = localChangeSnapshot.changes.findIndex(change => change.id === id);
875
+ if (previousChangeIndex !== -1) {
876
+ localChangeSnapshot.changes.splice(previousChangeIndex, 1);
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
881
+ if (localChangeSnapshot.changes.length > 0) {
882
+ localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
883
+ }
884
+ localChangeSnapshot.changes.push({ operation, id });
885
+ await this.setLocalChangeSnapshot(localChangeSnapshot);
777
886
  }
778
- localChangeSnapshot.changes.push({ operation, id });
779
- await this.setLocalChangeSnapshot(localChangeSnapshot);
780
887
  }
781
888
  /**
782
- * Get the current local snapshot which contains just the changes for this node.
889
+ * Get the snapshot which contains just the changes for this node.
783
890
  * @param storageKey The storage key of the snapshot to get.
891
+ * @param isLocal Whether to get the local snapshot or not.
784
892
  * @returns The local snapshot entry.
785
893
  */
786
- async getLocalChangeSnapshot(storageKey) {
894
+ async getSnapshots(storageKey, isLocal) {
787
895
  await this._logging?.log({
788
896
  level: "info",
789
897
  source: this.CLASS_NAME,
790
- message: "getLocalChangeSnapshot",
898
+ message: "getSnapshots",
791
899
  data: {
792
900
  storageKey
793
901
  }
@@ -795,8 +903,8 @@ class LocalSyncStateHelper {
795
903
  const queryResult = await this._snapshotEntryEntityStorage.query({
796
904
  conditions: [
797
905
  {
798
- property: "isLocalSnapshot",
799
- value: true,
906
+ property: "isLocal",
907
+ value: isLocal,
800
908
  comparison: entity.ComparisonOperator.Equals
801
909
  },
802
910
  {
@@ -810,28 +918,35 @@ class LocalSyncStateHelper {
810
918
  await this._logging?.log({
811
919
  level: "info",
812
920
  source: this.CLASS_NAME,
813
- message: "localChangeSnapshotExists",
921
+ message: "getSnapshotsExists",
814
922
  data: {
815
923
  storageKey
816
924
  }
817
925
  });
818
- return queryResult.entities[0];
926
+ return queryResult.entities;
819
927
  }
820
928
  await this._logging?.log({
821
929
  level: "info",
822
930
  source: this.CLASS_NAME,
823
- message: "localChangeSnapshotDoesNotExist",
931
+ message: "getSnapshotsDoesNotExist",
824
932
  data: {
825
933
  storageKey
826
934
  }
827
935
  });
828
- return {
829
- id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
830
- storageKey,
831
- dateCreated: new Date(Date.now()).toISOString(),
832
- changeSetStorageIds: [],
833
- isLocalSnapshot: true
834
- };
936
+ const now = new Date(Date.now()).toISOString();
937
+ return [
938
+ {
939
+ version: SYNC_SNAPSHOT_VERSION,
940
+ id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
941
+ storageKey,
942
+ dateCreated: now,
943
+ dateModified: now,
944
+ changeSetStorageIds: [],
945
+ isLocal,
946
+ isConsolidated: false,
947
+ epoch: 0
948
+ }
949
+ ];
835
950
  }
836
951
  /**
837
952
  * Set the current local snapshot with changes for this node.
@@ -880,46 +995,139 @@ class LocalSyncStateHelper {
880
995
  snapshotCount: syncState.snapshots.length
881
996
  }
882
997
  });
998
+ // Get all the existing snapshots that we have processed previously
999
+ let existingSnapshots = await this.getSnapshots(storageKey, false);
883
1000
  // Sort from newest to oldest
884
- const sortedSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
885
- const newSnapshots = [];
886
- const modifiedSnapshots = [];
887
- for (const snapshot of sortedSnapshots) {
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) {
888
1015
  await this._logging?.log({
889
1016
  level: "info",
890
1017
  source: this.CLASS_NAME,
891
- message: "applySnapshot",
1018
+ message: "applySnapshotNoExisting",
892
1019
  data: {
893
- snapshotId: snapshot.id,
894
- dateCreated: new Date(snapshot.dateCreated).toISOString()
1020
+ storageKey
895
1021
  }
896
1022
  });
897
- const localSnapshot = await this._snapshotEntryEntityStorage.get(snapshot.id);
898
- const remoteSnapshotWithContext = {
899
- ...snapshot,
900
- storageKey
901
- };
902
- if (core.Is.empty(localSnapshot)) {
903
- // We don't have the snapshot locally, so we need to process it
904
- newSnapshots.push(remoteSnapshotWithContext);
905
- }
906
- else if (localSnapshot.dateModified !== snapshot.dateModified) {
907
- // If the local snapshot has a different dateModified, we need to update it
908
- modifiedSnapshots.push({
909
- localSnapshot,
910
- remoteSnapshot: remoteSnapshotWithContext
1023
+ const mostRecentConsolidation = syncStateSnapshots.findIndex(snapshot => snapshot.isConsolidated);
1024
+ if (mostRecentConsolidation !== -1) {
1025
+ // We found the most recent consolidated snapshot, we can use it
1026
+ await this._logging?.log({
1027
+ level: "info",
1028
+ source: this.CLASS_NAME,
1029
+ message: "applySnapshotFoundConsolidated",
1030
+ data: {
1031
+ storageKey,
1032
+ snapshotId: syncStateSnapshots[mostRecentConsolidation].id
1033
+ }
911
1034
  });
1035
+ // We need to reset the entity storage and remove all the remote items
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
1039
+ await this._changeSetHelper.reset(storageKey, synchronisedStorageModels.SyncNodeIdentityMode.Remote);
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
+ }
912
1052
  }
913
1053
  else {
914
- // we sorted the snapshots from newest to oldest, so if we found a local snapshot
915
- // with the same dateModified as the remote snapshot, we can stop processing further
916
- break;
1054
+ await this._logging?.log({
1055
+ level: "info",
1056
+ source: this.CLASS_NAME,
1057
+ message: "applySnapshotNoConsolidated",
1058
+ data: {
1059
+ storageKey
1060
+ }
1061
+ });
1062
+ }
1063
+ }
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
1069
+ // Create a lookup map for the existing snapshots
1070
+ const existingSnapshotsMap = {};
1071
+ for (const snapshot of existingSnapshots) {
1072
+ existingSnapshotsMap[snapshot.id] = snapshot;
1073
+ }
1074
+ const newSnapshots = [];
1075
+ const modifiedSnapshots = [];
1076
+ const referencedExistingSnapshots = Object.keys(existingSnapshotsMap);
1077
+ let completedProcessing = false;
1078
+ for (const snapshot of syncStateSnapshots) {
1079
+ await this._logging?.log({
1080
+ level: "info",
1081
+ source: this.CLASS_NAME,
1082
+ message: "applySnapshot",
1083
+ data: {
1084
+ snapshotId: snapshot.id,
1085
+ dateCreated: new Date(snapshot.dateCreated).toISOString()
1086
+ }
1087
+ });
1088
+ // See if we have the snapshot stored locally
1089
+ const currentSnapshot = existingSnapshotsMap[snapshot.id];
1090
+ // As we are referencing an existing snapshot, we need to remove it from the list
1091
+ // to allow us to cleanup any unreferenced snapshots later
1092
+ const idx = referencedExistingSnapshots.indexOf(snapshot.id);
1093
+ if (idx !== -1) {
1094
+ referencedExistingSnapshots.splice(idx, 1);
1095
+ }
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
+ }
1119
+ }
1120
+ }
1121
+ // We reverse the order of the snapshots to process them from oldest to newest
1122
+ // because we want to apply the changes in the order they were created
1123
+ await this.processModifiedSnapshots(modifiedSnapshots.reverse());
1124
+ await this.processNewSnapshots(newSnapshots.reverse());
1125
+ // Any ids remaining in this list are no longer referenced in the global state
1126
+ // so we should remove them from the local storage as they will never be updated again
1127
+ for (const referencedSnapshotId of referencedExistingSnapshots) {
1128
+ await this._snapshotEntryEntityStorage.remove(referencedSnapshotId);
917
1129
  }
918
1130
  }
919
- // We reverse the order of the snapshots to process them from oldest to newest
920
- // because we want to apply the changes in the order they were created
921
- await this.processModifiedSnapshots(modifiedSnapshots.reverse());
922
- await this.processNewSnapshots(newSnapshots.reverse());
923
1131
  }
924
1132
  /**
925
1133
  * Process the modified snapshots and store them in the local storage.
@@ -934,15 +1142,15 @@ class LocalSyncStateHelper {
934
1142
  source: this.CLASS_NAME,
935
1143
  message: "processModifiedSnapshot",
936
1144
  data: {
937
- snapshotId: modifiedSnapshot.remoteSnapshot.id,
938
- localModified: new Date(modifiedSnapshot.localSnapshot.dateModified ??
939
- modifiedSnapshot.localSnapshot.dateCreated).toISOString(),
940
- remoteModified: new Date(modifiedSnapshot.remoteSnapshot.dateModified ??
941
- modifiedSnapshot.remoteSnapshot.dateCreated).toISOString()
1145
+ snapshotId: modifiedSnapshot.updatedSnapshot.id,
1146
+ localModified: new Date(modifiedSnapshot.currentSnapshot.dateModified ??
1147
+ modifiedSnapshot.currentSnapshot.dateCreated).toISOString(),
1148
+ remoteModified: new Date(modifiedSnapshot.updatedSnapshot.dateModified ??
1149
+ modifiedSnapshot.updatedSnapshot.dateCreated).toISOString()
942
1150
  }
943
1151
  });
944
- const remoteChangeSetStorageIds = modifiedSnapshot.remoteSnapshot.changeSetStorageIds;
945
- const localChangeSetStorageIds = modifiedSnapshot.localSnapshot.changeSetStorageIds ?? [];
1152
+ const remoteChangeSetStorageIds = modifiedSnapshot.updatedSnapshot.changeSetStorageIds;
1153
+ const localChangeSetStorageIds = modifiedSnapshot.currentSnapshot.changeSetStorageIds ?? [];
946
1154
  if (core.Is.arrayValue(remoteChangeSetStorageIds)) {
947
1155
  for (const storageId of remoteChangeSetStorageIds) {
948
1156
  // Check if the local snapshot does not have the storageId
@@ -951,7 +1159,7 @@ class LocalSyncStateHelper {
951
1159
  }
952
1160
  }
953
1161
  }
954
- await this._snapshotEntryEntityStorage.set(modifiedSnapshot.remoteSnapshot);
1162
+ await this._snapshotEntryEntityStorage.set(modifiedSnapshot.updatedSnapshot);
955
1163
  }
956
1164
  }
957
1165
  /**
@@ -968,7 +1176,7 @@ class LocalSyncStateHelper {
968
1176
  message: "processNewSnapshot",
969
1177
  data: {
970
1178
  snapshotId: newSnapshot.id,
971
- localModified: new Date(newSnapshot.dateCreated).toISOString()
1179
+ dateCreated: newSnapshot.dateCreated
972
1180
  }
973
1181
  });
974
1182
  const newSnapshotChangeSetStorageIds = newSnapshot.changeSetStorageIds ?? [];
@@ -982,12 +1190,6 @@ class LocalSyncStateHelper {
982
1190
  }
983
1191
  }
984
1192
 
985
- // Copyright 2024 IOTA Stiftung.
986
- // SPDX-License-Identifier: Apache-2.0.
987
- const SYNC_STATE_VERSION = "1";
988
- const SYNC_POINTER_STORE_VERSION = "1";
989
- const SYNC_SNAPSHOT_VERSION = "1";
990
-
991
1193
  // Copyright 2024 IOTA Stiftung.
992
1194
  // SPDX-License-Identifier: Apache-2.0.
993
1195
  /**
@@ -1048,6 +1250,11 @@ class RemoteSyncStateHelper {
1048
1250
  * @internal
1049
1251
  */
1050
1252
  _isTrustedNode;
1253
+ /**
1254
+ * Maximum number of consolidations to keep in storage.
1255
+ * @internal
1256
+ */
1257
+ _maxConsolidations;
1051
1258
  /**
1052
1259
  * Create a new instance of DecentralisedEntityStorageConnector.
1053
1260
  * @param logging The logging connector to use for logging.
@@ -1056,14 +1263,16 @@ class RemoteSyncStateHelper {
1056
1263
  * @param blobStorageHelper The blob storage helper to use for remote sync states.
1057
1264
  * @param changeSetHelper The change set helper to use for managing changesets.
1058
1265
  * @param isTrustedNode Whether the node is trusted or not.
1266
+ * @param maxConsolidations The maximum number of consolidations to keep in storage.
1059
1267
  */
1060
- constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode) {
1268
+ constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode, maxConsolidations) {
1061
1269
  this._logging = logging;
1062
1270
  this._eventBusComponent = eventBusComponent;
1063
1271
  this._verifiableSyncPointerStorageConnector = verifiableSyncPointerStorageConnector;
1064
1272
  this._changeSetHelper = changeSetHelper;
1065
1273
  this._blobStorageHelper = blobStorageHelper;
1066
1274
  this._isTrustedNode = isTrustedNode;
1275
+ this._maxConsolidations = maxConsolidations;
1067
1276
  this._batchResponseStorageIds = {};
1068
1277
  this._populateFullChanges = {};
1069
1278
  this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.BatchResponse, async (response) => {
@@ -1165,9 +1374,11 @@ class RemoteSyncStateHelper {
1165
1374
  core.ObjectHelper.propertyDelete(change.entity, "nodeIdentity");
1166
1375
  }
1167
1376
  }
1377
+ const now = new Date(Date.now()).toISOString();
1168
1378
  const syncChangeSet = {
1169
1379
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1170
- dateCreated: new Date(Date.now()).toISOString(),
1380
+ dateCreated: now,
1381
+ dateModified: now,
1171
1382
  storageKey,
1172
1383
  changes,
1173
1384
  nodeIdentity: this._nodeIdentity
@@ -1219,23 +1430,28 @@ class RemoteSyncStateHelper {
1219
1430
  const syncPointerStore = await this.getVerifiableSyncPointerStore();
1220
1431
  let syncState;
1221
1432
  if (!core.Is.empty(syncPointerStore.syncPointers[storageKey])) {
1222
- syncState = await this.getRemoteSyncState(syncPointerStore.syncPointers[storageKey]);
1433
+ syncState = await this.getSyncState(syncPointerStore.syncPointers[storageKey]);
1223
1434
  }
1224
1435
  // No current sync state, so we create a new one
1225
1436
  if (core.Is.empty(syncState)) {
1226
- syncState = { version: SYNC_STATE_VERSION, snapshots: [] };
1437
+ syncState = { version: SYNC_STATE_VERSION, storageKey, snapshots: [] };
1227
1438
  }
1228
1439
  // Sort the snapshots so the newest snapshot is last in the array
1229
1440
  const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
1230
1441
  // Get the current snapshot, if it does not exist we create a new one
1231
1442
  let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
1443
+ const currentEpoch = currentSnapshot?.epoch ?? 0;
1232
1444
  const now = new Date(Date.now()).toISOString();
1233
- if (core.Is.empty(currentSnapshot)) {
1445
+ // If there is no snapshot or the current one is a consolidation
1446
+ // we start a new snapshot
1447
+ if (core.Is.empty(currentSnapshot) || currentSnapshot.isConsolidated) {
1234
1448
  currentSnapshot = {
1235
1449
  version: SYNC_SNAPSHOT_VERSION,
1236
1450
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1237
1451
  dateCreated: now,
1238
1452
  dateModified: now,
1453
+ isConsolidated: false,
1454
+ epoch: currentEpoch + 1,
1239
1455
  changeSetStorageIds: []
1240
1456
  };
1241
1457
  syncState.snapshots.push(currentSnapshot);
@@ -1264,7 +1480,7 @@ class RemoteSyncStateHelper {
1264
1480
  message: "consolidationStarting"
1265
1481
  });
1266
1482
  // Perform a batch request to start the consolidation
1267
- await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize });
1483
+ await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize, requestMode: synchronisedStorageModels.SyncNodeIdentityMode.All });
1268
1484
  }
1269
1485
  /**
1270
1486
  * Get the sync pointer store.
@@ -1343,11 +1559,37 @@ class RemoteSyncStateHelper {
1343
1559
  await this._logging?.log({
1344
1560
  level: "info",
1345
1561
  source: this.CLASS_NAME,
1346
- message: "remoteSyncStateStoring",
1562
+ message: "syncStateStoring",
1347
1563
  data: {
1348
1564
  snapshotCount: syncState.snapshots.length
1349
1565
  }
1350
1566
  });
1567
+ // Limits the number of consolidations in the list so that we can shrink decentralised
1568
+ // storage requirements, sort from newest to oldest so that we can easily find the
1569
+ // oldest snapshots to remove.
1570
+ const snapshots = syncState.snapshots.sort((a, b) => new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime());
1571
+ // Find all the consolidation indexes
1572
+ const consolidationIndexes = [];
1573
+ for (let i = 0; i < snapshots.length; i++) {
1574
+ const snapshot = snapshots[i];
1575
+ if (snapshot.isConsolidated) {
1576
+ consolidationIndexes.push(i);
1577
+ }
1578
+ }
1579
+ if (consolidationIndexes.length > this._maxConsolidations) {
1580
+ // Once we have reached the max for consolidations we need to remove
1581
+ // all the snapshots, including non consolidated ones, beyond this point
1582
+ const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
1583
+ syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
1584
+ for (const snapshot of toRemove) {
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
+ }
1591
+ }
1592
+ }
1351
1593
  return this._blobStorageHelper.saveBlob(syncState);
1352
1594
  }
1353
1595
  /**
@@ -1355,22 +1597,22 @@ class RemoteSyncStateHelper {
1355
1597
  * @param syncPointerId The id of the sync pointer to retrieve the state for.
1356
1598
  * @returns The remote sync state.
1357
1599
  */
1358
- async getRemoteSyncState(syncPointerId) {
1600
+ async getSyncState(syncPointerId) {
1359
1601
  try {
1360
1602
  await this._logging?.log({
1361
1603
  level: "info",
1362
1604
  source: this.CLASS_NAME,
1363
- message: "remoteSyncStateRetrieving",
1605
+ message: "syncStateRetrieving",
1364
1606
  data: {
1365
1607
  syncPointerId
1366
1608
  }
1367
1609
  });
1368
- const syncState = await this._blobStorageHelper.load(syncPointerId);
1610
+ const syncState = await this._blobStorageHelper.loadBlob(syncPointerId);
1369
1611
  if (core.Is.object(syncState)) {
1370
1612
  await this._logging?.log({
1371
1613
  level: "info",
1372
1614
  source: this.CLASS_NAME,
1373
- message: "remoteSyncStateRetrieved",
1615
+ message: "syncStateRetrieved",
1374
1616
  data: {
1375
1617
  syncPointerId,
1376
1618
  snapshotCount: syncState.snapshots.length
@@ -1393,7 +1635,7 @@ class RemoteSyncStateHelper {
1393
1635
  await this._logging?.log({
1394
1636
  level: "info",
1395
1637
  source: this.CLASS_NAME,
1396
- message: "remoteSyncStateNotFound",
1638
+ message: "syncStateNotFound",
1397
1639
  data: {
1398
1640
  syncPointerId
1399
1641
  }
@@ -1432,19 +1674,29 @@ class RemoteSyncStateHelper {
1432
1674
  let syncState;
1433
1675
  if (core.Is.stringValue(syncPointerStore.syncPointers[response.storageKey])) {
1434
1676
  // If the sync pointer exists, we load the current sync state
1435
- syncState = await this.getRemoteSyncState(syncPointerStore.syncPointers[response.storageKey]);
1677
+ syncState = await this.getSyncState(syncPointerStore.syncPointers[response.storageKey]);
1436
1678
  }
1437
1679
  // If the sync state does not exist, we create a new one
1438
- syncState ??= { version: SYNC_STATE_VERSION, snapshots: [] };
1680
+ syncState ??= {
1681
+ version: SYNC_STATE_VERSION,
1682
+ storageKey: response.storageKey,
1683
+ snapshots: []
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;
1439
1689
  const batchSnapshot = {
1440
1690
  version: SYNC_SNAPSHOT_VERSION,
1441
1691
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1442
1692
  dateCreated: now,
1443
1693
  dateModified: now,
1694
+ isConsolidated: true,
1695
+ epoch: currentEpoch + 1,
1444
1696
  changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
1445
1697
  };
1446
1698
  syncState.snapshots.push(batchSnapshot);
1447
- // Store the sync state in the blob storage
1699
+ // Store the updated sync state
1448
1700
  const syncStateId = await this.storeRemoteSyncState(syncState);
1449
1701
  syncPointerStore.syncPointers[response.storageKey] = syncStateId;
1450
1702
  // Store the verifiable sync pointer in the verifiable storage
@@ -1474,11 +1726,14 @@ class RemoteSyncStateHelper {
1474
1726
  id: response.id
1475
1727
  }
1476
1728
  });
1729
+ // We have received a response to an item request, find the right storage
1730
+ // for the request id
1477
1731
  if (!core.Is.empty(this._populateFullChanges[response.storageKey])) {
1478
1732
  const idx = this._populateFullChanges[response.storageKey].requestIds.indexOf(response.id);
1479
1733
  if (idx !== -1) {
1480
1734
  this._populateFullChanges[response.storageKey].requestIds.splice(idx, 1);
1481
1735
  this._populateFullChanges[response.storageKey].entities[response.id] = response.entity;
1736
+ // If there are no request ids remaining we can complete the population
1482
1737
  if (this._populateFullChanges[response.storageKey].requestIds.length === 0) {
1483
1738
  await this._populateFullChanges[response.storageKey].completeCallback();
1484
1739
  }
@@ -1506,6 +1761,11 @@ class SynchronisedStorageService {
1506
1761
  * @internal
1507
1762
  */
1508
1763
  static _DEFAULT_CONSOLIDATION_BATCH_SIZE = 100;
1764
+ /**
1765
+ * The default max number of consolidations to keep in storage.
1766
+ * @internal
1767
+ */
1768
+ static _DEFAULT_MAX_CONSOLIDATIONS = 5;
1509
1769
  /**
1510
1770
  * Runtime name for the class.
1511
1771
  */
@@ -1624,6 +1884,7 @@ class SynchronisedStorageService {
1624
1884
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_INTERVAL_MINUTES,
1625
1885
  consolidationBatchSize: options.config.consolidationBatchSize ??
1626
1886
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE,
1887
+ maxConsolidations: options.config.maxConsolidations ?? SynchronisedStorageService._DEFAULT_MAX_CONSOLIDATIONS,
1627
1888
  blobStorageEncryptionKeyId: options.config.blobStorageEncryptionKeyId ?? "synchronised-storage-blob-encryption-key",
1628
1889
  verifiableStorageKeyId: options.config.verifiableStorageKeyId
1629
1890
  };
@@ -1639,11 +1900,16 @@ class SynchronisedStorageService {
1639
1900
  this._blobStorageHelper = new BlobStorageHelper(this._logging, this._vaultConnector, this._blobStorageConnector, this._config.blobStorageEncryptionKeyId, this._config.isTrustedNode);
1640
1901
  this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._identityConnector, this._blobStorageHelper, this._config.synchronisedStorageMethodId);
1641
1902
  this._localSyncStateHelper = new LocalSyncStateHelper(this._logging, this._localSyncSnapshotEntryEntityStorage, this._changeSetHelper);
1642
- this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._verifiableSyncPointerStorageConnector, this._blobStorageHelper, this._changeSetHelper, this._config.isTrustedNode);
1903
+ this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._verifiableSyncPointerStorageConnector, this._blobStorageHelper, this._changeSetHelper, this._config.isTrustedNode, this._config.maxConsolidations);
1643
1904
  this._serviceStarted = false;
1644
1905
  this._activeStorageKeys = {};
1645
1906
  this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.registerStorageKey(event.data));
1646
- this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.LocalItemChange, async (event) => this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id));
1907
+ this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.LocalItemChange, async (event) => {
1908
+ // Make sure the change event is from this node
1909
+ if (core.Is.stringValue(this._nodeIdentity) && this._nodeIdentity === event.data.nodeIdentity) {
1910
+ await this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id);
1911
+ }
1912
+ });
1647
1913
  }
1648
1914
  /**
1649
1915
  * The component needs to be started when the node is initialized.
@@ -1733,7 +1999,7 @@ class SynchronisedStorageService {
1733
1999
  // to store the change set in the synchronised storage.
1734
2000
  // This will be performed using rights-management
1735
2001
  const copy = await this._changeSetHelper.copyChangeset(syncChangeSet);
1736
- if (!core.Is.empty(copy) && core.Is.stringValue(this._nodeIdentity)) {
2002
+ if (!core.Is.empty(copy)) {
1737
2003
  // Apply the changes to this node
1738
2004
  await this._changeSetHelper.applyChangeset(copy.syncChangeSet);
1739
2005
  // And update the sync state with the latest changes
@@ -1790,7 +2056,7 @@ class SynchronisedStorageService {
1790
2056
  if (!core.Is.empty(verifiableSyncPointerStore.syncPointers[storageKey])) {
1791
2057
  // Load the sync state from the remote blob storage using the sync pointer
1792
2058
  // to load the sync state
1793
- const remoteSyncState = await this._remoteSyncStateHelper.getRemoteSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
2059
+ const remoteSyncState = await this._remoteSyncStateHelper.getSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
1794
2060
  // If we got the sync state we can try and sync from it
1795
2061
  if (!core.Is.undefined(remoteSyncState)) {
1796
2062
  await this._localSyncStateHelper.applySyncState(storageKey, remoteSyncState);
@@ -1811,64 +2077,67 @@ class SynchronisedStorageService {
1811
2077
  storageKey
1812
2078
  }
1813
2079
  });
1814
- const localChangeSnapshot = await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
1815
- if (core.Is.arrayValue(localChangeSnapshot.changes)) {
1816
- await this._remoteSyncStateHelper.buildChangeSet(storageKey, localChangeSnapshot.changes, async (syncChangeSet, changeSetStorageId) => {
1817
- if (core.Is.empty(syncChangeSet) && core.Is.empty(changeSetStorageId)) {
1818
- await this._logging?.log({
1819
- level: "info",
1820
- source: this.CLASS_NAME,
1821
- message: "builtStorageChangeSetNone",
1822
- data: {
1823
- storageKey
1824
- }
1825
- });
1826
- }
1827
- else {
1828
- await this._logging?.log({
1829
- level: "info",
1830
- source: this.CLASS_NAME,
1831
- message: "builtStorageChangeSet",
1832
- data: {
1833
- storageKey,
1834
- changeSetStorageId
1835
- }
1836
- });
1837
- // Send the local changes to the remote storage if we are a trusted node
1838
- if (this._config.isTrustedNode && core.Is.stringValue(changeSetStorageId)) {
1839
- // If we are a trusted node, we can add the change set to the sync state
1840
- // and remove the local change snapshot
1841
- await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
1842
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2080
+ const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
2081
+ if (localChangeSnapshots.length > 0) {
2082
+ const localChangeSnapshot = localChangeSnapshots[0];
2083
+ if (core.Is.arrayValue(localChangeSnapshot.changes)) {
2084
+ await this._remoteSyncStateHelper.buildChangeSet(storageKey, localChangeSnapshot.changes, async (syncChangeSet, changeSetStorageId) => {
2085
+ if (core.Is.empty(syncChangeSet) && core.Is.empty(changeSetStorageId)) {
2086
+ await this._logging?.log({
2087
+ level: "info",
2088
+ source: this.CLASS_NAME,
2089
+ message: "builtStorageChangeSetNone",
2090
+ data: {
2091
+ storageKey
2092
+ }
2093
+ });
1843
2094
  }
1844
- else if (!core.Is.empty(this._trustedSynchronisedStorageComponent) &&
1845
- core.Is.object(syncChangeSet)) {
1846
- // If we are not a trusted node, we need to send the changes to the trusted node
1847
- // and then remove the local change snapshot
2095
+ else {
1848
2096
  await this._logging?.log({
1849
2097
  level: "info",
1850
2098
  source: this.CLASS_NAME,
1851
- message: "sendingChangeSetToTrustedNode",
2099
+ message: "builtStorageChangeSet",
1852
2100
  data: {
1853
2101
  storageKey,
1854
2102
  changeSetStorageId
1855
2103
  }
1856
2104
  });
1857
- await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
1858
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2105
+ // Send the local changes to the remote storage if we are a trusted node
2106
+ if (this._config.isTrustedNode && core.Is.stringValue(changeSetStorageId)) {
2107
+ // If we are a trusted node, we can add the change set to the sync state
2108
+ // and remove the local change snapshot
2109
+ await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
2110
+ await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2111
+ }
2112
+ else if (!core.Is.empty(this._trustedSynchronisedStorageComponent) &&
2113
+ core.Is.object(syncChangeSet)) {
2114
+ // If we are not a trusted node, we need to send the changes to the trusted node
2115
+ // and then remove the local change snapshot
2116
+ await this._logging?.log({
2117
+ level: "info",
2118
+ source: this.CLASS_NAME,
2119
+ message: "sendingChangeSetToTrustedNode",
2120
+ data: {
2121
+ storageKey,
2122
+ changeSetStorageId
2123
+ }
2124
+ });
2125
+ await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
2126
+ await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2127
+ }
1859
2128
  }
1860
- }
1861
- });
1862
- }
1863
- else {
1864
- await this._logging?.log({
1865
- level: "info",
1866
- source: this.CLASS_NAME,
1867
- message: "updateFromLocalSyncStateNoChanges",
1868
- data: {
1869
- storageKey
1870
- }
1871
- });
2129
+ });
2130
+ }
2131
+ else {
2132
+ await this._logging?.log({
2133
+ level: "info",
2134
+ source: this.CLASS_NAME,
2135
+ message: "updateFromLocalSyncStateNoChanges",
2136
+ data: {
2137
+ storageKey
2138
+ }
2139
+ });
2140
+ }
1872
2141
  }
1873
2142
  }
1874
2143
  /**
@@ -1878,24 +2147,17 @@ class SynchronisedStorageService {
1878
2147
  * @internal
1879
2148
  */
1880
2149
  async startConsolidationSync(storageKey) {
1881
- let localChangeSnapshot;
1882
2150
  try {
1883
- // If we are performing a consolidation, we can remove the local change snapshot
1884
- // as we are going to create a complete changeset from the DB
1885
- localChangeSnapshot = await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
1886
- if (!core.Is.empty(localChangeSnapshot)) {
1887
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1888
- }
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
1889
2157
  await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
1890
2158
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
1891
- // The consolidation was successful, so we can remove the local change snapshot permanently
1892
- localChangeSnapshot = undefined;
1893
2159
  }
1894
2160
  catch (error) {
1895
- if (localChangeSnapshot) {
1896
- // If the consolidation failed, we can keep the local change snapshot
1897
- await this._localSyncStateHelper.setLocalChangeSnapshot(localChangeSnapshot);
1898
- }
1899
2161
  await this._logging?.log({
1900
2162
  level: "error",
1901
2163
  source: this.CLASS_NAME,