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