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

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,13 @@ 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;
42
50
  /**
43
51
  * The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
44
52
  */
@@ -52,6 +60,10 @@ __decorate([
52
60
  entity.property({ type: "string", isPrimary: true }),
53
61
  __metadata("design:type", String)
54
62
  ], exports.SyncSnapshotEntry.prototype, "id", void 0);
63
+ __decorate([
64
+ entity.property({ type: "string" }),
65
+ __metadata("design:type", String)
66
+ ], exports.SyncSnapshotEntry.prototype, "version", void 0);
55
67
  __decorate([
56
68
  entity.property({ type: "string", isSecondary: true }),
57
69
  __metadata("design:type", String)
@@ -61,13 +73,17 @@ __decorate([
61
73
  __metadata("design:type", String)
62
74
  ], exports.SyncSnapshotEntry.prototype, "dateCreated", void 0);
63
75
  __decorate([
64
- entity.property({ type: "string", optional: true }),
76
+ entity.property({ type: "string" }),
65
77
  __metadata("design:type", String)
66
78
  ], exports.SyncSnapshotEntry.prototype, "dateModified", void 0);
67
79
  __decorate([
68
- entity.property({ type: "boolean", optional: true }),
80
+ entity.property({ type: "boolean" }),
81
+ __metadata("design:type", Boolean)
82
+ ], exports.SyncSnapshotEntry.prototype, "isLocal", void 0);
83
+ __decorate([
84
+ entity.property({ type: "boolean" }),
69
85
  __metadata("design:type", Boolean)
70
- ], exports.SyncSnapshotEntry.prototype, "isLocalSnapshot", void 0);
86
+ ], exports.SyncSnapshotEntry.prototype, "isConsolidated", void 0);
71
87
  __decorate([
72
88
  entity.property({ type: "array", itemType: "string", optional: true }),
73
89
  __metadata("design:type", Array)
@@ -116,6 +132,7 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
116
132
  body: {
117
133
  id: "0909090909090909090909090909090909090909090909090909090909090909",
118
134
  dateCreated: "2025-05-29T01:00:00.000Z",
135
+ dateModified: "2025-05-29T01:00:00.000Z",
119
136
  nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
120
137
  changes: [
121
138
  {
@@ -316,7 +333,7 @@ class BlobStorageHelper {
316
333
  * @param blobId The id of the blob to apply.
317
334
  * @returns The blob.
318
335
  */
319
- async load(blobId) {
336
+ async loadBlob(blobId) {
320
337
  await this._logging?.log({
321
338
  level: "info",
322
339
  source: this.CLASS_NAME,
@@ -409,6 +426,51 @@ class BlobStorageHelper {
409
426
  throw error;
410
427
  }
411
428
  }
429
+ /**
430
+ * Remove a blob from storage.
431
+ * @param blobId The id of the blob to remove.
432
+ * @returns Nothing.
433
+ */
434
+ async removeBlob(blobId) {
435
+ await this._logging?.log({
436
+ level: "info",
437
+ source: this.CLASS_NAME,
438
+ message: "removeBlob",
439
+ data: {
440
+ blobId
441
+ }
442
+ });
443
+ try {
444
+ await this._blobStorageConnector.remove(blobId);
445
+ await this._logging?.log({
446
+ level: "info",
447
+ source: this.CLASS_NAME,
448
+ message: "removedBlob",
449
+ data: {
450
+ blobId
451
+ }
452
+ });
453
+ }
454
+ catch (error) {
455
+ await this._logging?.log({
456
+ level: "error",
457
+ source: this.CLASS_NAME,
458
+ message: "removeBlobFailed",
459
+ data: {
460
+ blobId
461
+ },
462
+ error: core.BaseError.fromError(error)
463
+ });
464
+ }
465
+ await this._logging?.log({
466
+ level: "info",
467
+ source: this.CLASS_NAME,
468
+ message: "removeBlobEmpty",
469
+ data: {
470
+ blobId
471
+ }
472
+ });
473
+ }
412
474
  }
413
475
 
414
476
  // Copyright 2024 IOTA Stiftung.
@@ -488,7 +550,7 @@ class ChangeSetHelper {
488
550
  }
489
551
  });
490
552
  try {
491
- const syncChangeSet = await this._blobStorageHelper.load(changeSetStorageId);
553
+ const syncChangeSet = await this._blobStorageHelper.loadBlob(changeSetStorageId);
492
554
  if (core.Is.object(syncChangeSet)) {
493
555
  const verified = await this.verifyChangesetProof(syncChangeSet);
494
556
  return verified ? syncChangeSet : undefined;
@@ -521,7 +583,9 @@ class ChangeSetHelper {
521
583
  */
522
584
  async getAndApplyChangeset(changeSetStorageId) {
523
585
  const syncChangeset = await this.getAndVerifyChangeset(changeSetStorageId);
524
- if (!core.Is.empty(syncChangeset)) {
586
+ // Only apply changesets from other nodes, we don't want to overwrite
587
+ // any changes we have made to local entity storage
588
+ if (!core.Is.empty(syncChangeset) && syncChangeset.nodeIdentity !== this._nodeIdentity) {
525
589
  await this.applyChangeset(syncChangeset);
526
590
  }
527
591
  return syncChangeset;
@@ -565,7 +629,8 @@ class ChangeSetHelper {
565
629
  if (!core.Is.empty(change.id)) {
566
630
  await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.RemoteItemRemove, {
567
631
  storageKey: syncChangeset.storageKey,
568
- id: change.id
632
+ id: change.id,
633
+ nodeIdentity: syncChangeset.nodeIdentity
569
634
  });
570
635
  }
571
636
  break;
@@ -707,8 +772,36 @@ class ChangeSetHelper {
707
772
  }
708
773
  }
709
774
  }
775
+ /**
776
+ * Reset the storage for a given storage key.
777
+ * @param storageKey The key of the storage to reset.
778
+ * @param resetMode The reset mode, this will use the nodeIdentity in the entities to determine which are local/remote.
779
+ * @returns Nothing.
780
+ */
781
+ async reset(storageKey, resetMode) {
782
+ // If we are applying a consolidation we need to reset the local db
783
+ // but keep any entries from the local node, as they might have been updated
784
+ await this._logging?.log({
785
+ level: "info",
786
+ source: this.CLASS_NAME,
787
+ message: "storageReset",
788
+ data: {
789
+ storageKey
790
+ }
791
+ });
792
+ await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.Reset, {
793
+ storageKey,
794
+ resetMode
795
+ });
796
+ }
710
797
  }
711
798
 
799
+ // Copyright 2024 IOTA Stiftung.
800
+ // SPDX-License-Identifier: Apache-2.0.
801
+ const SYNC_STATE_VERSION = "1";
802
+ const SYNC_POINTER_STORE_VERSION = "1";
803
+ const SYNC_SNAPSHOT_VERSION = "1";
804
+
712
805
  // Copyright 2024 IOTA Stiftung.
713
806
  // SPDX-License-Identifier: Apache-2.0.
714
807
  /**
@@ -763,31 +856,35 @@ class LocalSyncStateHelper {
763
856
  id
764
857
  }
765
858
  });
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();
859
+ const localChangeSnapshots = await this.getSnapshots(storageKey, true);
860
+ if (localChangeSnapshots.length > 0) {
861
+ const localChangeSnapshot = localChangeSnapshots[0];
862
+ localChangeSnapshot.changes ??= [];
863
+ // If we already have a change for this id we are
864
+ // about to supersede it, we remove the previous change
865
+ // to avoid having multiple changes for the same id
866
+ const previousChangeIndex = localChangeSnapshot.changes.findIndex(change => change.id === id);
867
+ if (previousChangeIndex !== -1) {
868
+ localChangeSnapshot.changes.splice(previousChangeIndex, 1);
869
+ }
870
+ if (localChangeSnapshot.changes.length > 0) {
871
+ localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
872
+ }
873
+ localChangeSnapshot.changes.push({ operation, id });
874
+ await this.setLocalChangeSnapshot(localChangeSnapshot);
777
875
  }
778
- localChangeSnapshot.changes.push({ operation, id });
779
- await this.setLocalChangeSnapshot(localChangeSnapshot);
780
876
  }
781
877
  /**
782
- * Get the current local snapshot which contains just the changes for this node.
878
+ * Get the snapshot which contains just the changes for this node.
783
879
  * @param storageKey The storage key of the snapshot to get.
880
+ * @param isLocal Whether to get the local snapshot or not.
784
881
  * @returns The local snapshot entry.
785
882
  */
786
- async getLocalChangeSnapshot(storageKey) {
883
+ async getSnapshots(storageKey, isLocal) {
787
884
  await this._logging?.log({
788
885
  level: "info",
789
886
  source: this.CLASS_NAME,
790
- message: "getLocalChangeSnapshot",
887
+ message: "getSnapshots",
791
888
  data: {
792
889
  storageKey
793
890
  }
@@ -795,8 +892,8 @@ class LocalSyncStateHelper {
795
892
  const queryResult = await this._snapshotEntryEntityStorage.query({
796
893
  conditions: [
797
894
  {
798
- property: "isLocalSnapshot",
799
- value: true,
895
+ property: "isLocal",
896
+ value: isLocal,
800
897
  comparison: entity.ComparisonOperator.Equals
801
898
  },
802
899
  {
@@ -810,28 +907,34 @@ class LocalSyncStateHelper {
810
907
  await this._logging?.log({
811
908
  level: "info",
812
909
  source: this.CLASS_NAME,
813
- message: "localChangeSnapshotExists",
910
+ message: "getSnapshotsExists",
814
911
  data: {
815
912
  storageKey
816
913
  }
817
914
  });
818
- return queryResult.entities[0];
915
+ return queryResult.entities;
819
916
  }
820
917
  await this._logging?.log({
821
918
  level: "info",
822
919
  source: this.CLASS_NAME,
823
- message: "localChangeSnapshotDoesNotExist",
920
+ message: "getSnapshotsDoesNotExist",
824
921
  data: {
825
922
  storageKey
826
923
  }
827
924
  });
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
- };
925
+ const now = new Date(Date.now()).toISOString();
926
+ return [
927
+ {
928
+ version: SYNC_SNAPSHOT_VERSION,
929
+ id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
930
+ storageKey,
931
+ dateCreated: now,
932
+ dateModified: now,
933
+ changeSetStorageIds: [],
934
+ isLocal,
935
+ isConsolidated: false
936
+ }
937
+ ];
835
938
  }
836
939
  /**
837
940
  * Set the current local snapshot with changes for this node.
@@ -880,46 +983,115 @@ class LocalSyncStateHelper {
880
983
  snapshotCount: syncState.snapshots.length
881
984
  }
882
985
  });
986
+ // Get all the existing snapshots that we have processed previously
987
+ const existingRemoteSnapshots = await this.getSnapshots(storageKey, false);
883
988
  // Sort from newest to oldest
884
989
  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) {
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) {
888
994
  await this._logging?.log({
889
995
  level: "info",
890
996
  source: this.CLASS_NAME,
891
- message: "applySnapshot",
997
+ message: "applySnapshotNoExisting",
892
998
  data: {
893
- snapshotId: snapshot.id,
894
- dateCreated: new Date(snapshot.dateCreated).toISOString()
999
+ storageKey
895
1000
  }
896
1001
  });
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
1002
+ const firstConsolidated = sortedSnapshots.find(snapshot => snapshot.isConsolidated);
1003
+ if (firstConsolidated) {
1004
+ // We found a consolidated snapshot, we can use it
1005
+ await this._logging?.log({
1006
+ level: "info",
1007
+ source: this.CLASS_NAME,
1008
+ message: "applySnapshotFoundConsolidated",
1009
+ data: {
1010
+ storageKey,
1011
+ snapshotId: firstConsolidated.id
1012
+ }
911
1013
  });
1014
+ // We need to reset the entity storage and remove all the remote items
1015
+ // so that we use just the ones from the consolidation
1016
+ await this._changeSetHelper.reset(storageKey, synchronisedStorageModels.SyncNodeIdentityMode.Remote);
1017
+ await this.processNewSnapshots([
1018
+ {
1019
+ ...firstConsolidated,
1020
+ storageKey,
1021
+ isLocal: false
1022
+ }
1023
+ ]);
912
1024
  }
913
1025
  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;
1026
+ await this._logging?.log({
1027
+ level: "info",
1028
+ source: this.CLASS_NAME,
1029
+ message: "applySnapshotNoConsolidated",
1030
+ data: {
1031
+ storageKey
1032
+ }
1033
+ });
1034
+ }
1035
+ }
1036
+ else {
1037
+ // Create a lookup map for the existing snapshots
1038
+ const existingSnapshots = {};
1039
+ for (const snapshot of existingRemoteSnapshots) {
1040
+ existingSnapshots[snapshot.id] = snapshot;
1041
+ }
1042
+ const newSnapshots = [];
1043
+ const modifiedSnapshots = [];
1044
+ const referencedExistingSnapshots = Object.keys(existingSnapshots);
1045
+ for (const snapshot of sortedSnapshots) {
1046
+ await this._logging?.log({
1047
+ level: "info",
1048
+ source: this.CLASS_NAME,
1049
+ message: "applySnapshot",
1050
+ data: {
1051
+ snapshotId: snapshot.id,
1052
+ dateCreated: new Date(snapshot.dateCreated).toISOString()
1053
+ }
1054
+ });
1055
+ // See if we have the local snapshot
1056
+ const currentSnapshot = existingSnapshots[snapshot.id];
1057
+ // As we are referencing an existing snapshot, we need to remove it from the list
1058
+ // to allow us to cleanup any unreferenced snapshots later
1059
+ const idx = referencedExistingSnapshots.indexOf(snapshot.id);
1060
+ if (idx !== -1) {
1061
+ referencedExistingSnapshots.splice(idx, 1);
1062
+ }
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;
1083
+ }
1084
+ }
1085
+ // We reverse the order of the snapshots to process them from oldest to newest
1086
+ // because we want to apply the changes in the order they were created
1087
+ await this.processModifiedSnapshots(modifiedSnapshots.reverse());
1088
+ await this.processNewSnapshots(newSnapshots.reverse());
1089
+ // Any ids remaining in this list are no longer referenced in the global state
1090
+ // so we should remove them from the local storage as they will never be updated again
1091
+ for (const referencedSnapshotId of referencedExistingSnapshots) {
1092
+ await this._snapshotEntryEntityStorage.remove(referencedSnapshotId);
917
1093
  }
918
1094
  }
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
1095
  }
924
1096
  /**
925
1097
  * Process the modified snapshots and store them in the local storage.
@@ -934,15 +1106,15 @@ class LocalSyncStateHelper {
934
1106
  source: this.CLASS_NAME,
935
1107
  message: "processModifiedSnapshot",
936
1108
  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()
1109
+ snapshotId: modifiedSnapshot.updatedSnapshot.id,
1110
+ localModified: new Date(modifiedSnapshot.currentSnapshot.dateModified ??
1111
+ modifiedSnapshot.currentSnapshot.dateCreated).toISOString(),
1112
+ remoteModified: new Date(modifiedSnapshot.updatedSnapshot.dateModified ??
1113
+ modifiedSnapshot.updatedSnapshot.dateCreated).toISOString()
942
1114
  }
943
1115
  });
944
- const remoteChangeSetStorageIds = modifiedSnapshot.remoteSnapshot.changeSetStorageIds;
945
- const localChangeSetStorageIds = modifiedSnapshot.localSnapshot.changeSetStorageIds ?? [];
1116
+ const remoteChangeSetStorageIds = modifiedSnapshot.updatedSnapshot.changeSetStorageIds;
1117
+ const localChangeSetStorageIds = modifiedSnapshot.currentSnapshot.changeSetStorageIds ?? [];
946
1118
  if (core.Is.arrayValue(remoteChangeSetStorageIds)) {
947
1119
  for (const storageId of remoteChangeSetStorageIds) {
948
1120
  // Check if the local snapshot does not have the storageId
@@ -951,7 +1123,7 @@ class LocalSyncStateHelper {
951
1123
  }
952
1124
  }
953
1125
  }
954
- await this._snapshotEntryEntityStorage.set(modifiedSnapshot.remoteSnapshot);
1126
+ await this._snapshotEntryEntityStorage.set(modifiedSnapshot.updatedSnapshot);
955
1127
  }
956
1128
  }
957
1129
  /**
@@ -968,7 +1140,7 @@ class LocalSyncStateHelper {
968
1140
  message: "processNewSnapshot",
969
1141
  data: {
970
1142
  snapshotId: newSnapshot.id,
971
- localModified: new Date(newSnapshot.dateCreated).toISOString()
1143
+ dateCreated: newSnapshot.dateCreated
972
1144
  }
973
1145
  });
974
1146
  const newSnapshotChangeSetStorageIds = newSnapshot.changeSetStorageIds ?? [];
@@ -982,12 +1154,6 @@ class LocalSyncStateHelper {
982
1154
  }
983
1155
  }
984
1156
 
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
1157
  // Copyright 2024 IOTA Stiftung.
992
1158
  // SPDX-License-Identifier: Apache-2.0.
993
1159
  /**
@@ -1048,6 +1214,11 @@ class RemoteSyncStateHelper {
1048
1214
  * @internal
1049
1215
  */
1050
1216
  _isTrustedNode;
1217
+ /**
1218
+ * Maximum number of consolidations to keep in storage.
1219
+ * @internal
1220
+ */
1221
+ _maxConsolidations;
1051
1222
  /**
1052
1223
  * Create a new instance of DecentralisedEntityStorageConnector.
1053
1224
  * @param logging The logging connector to use for logging.
@@ -1056,14 +1227,16 @@ class RemoteSyncStateHelper {
1056
1227
  * @param blobStorageHelper The blob storage helper to use for remote sync states.
1057
1228
  * @param changeSetHelper The change set helper to use for managing changesets.
1058
1229
  * @param isTrustedNode Whether the node is trusted or not.
1230
+ * @param maxConsolidations The maximum number of consolidations to keep in storage.
1059
1231
  */
1060
- constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode) {
1232
+ constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode, maxConsolidations) {
1061
1233
  this._logging = logging;
1062
1234
  this._eventBusComponent = eventBusComponent;
1063
1235
  this._verifiableSyncPointerStorageConnector = verifiableSyncPointerStorageConnector;
1064
1236
  this._changeSetHelper = changeSetHelper;
1065
1237
  this._blobStorageHelper = blobStorageHelper;
1066
1238
  this._isTrustedNode = isTrustedNode;
1239
+ this._maxConsolidations = maxConsolidations;
1067
1240
  this._batchResponseStorageIds = {};
1068
1241
  this._populateFullChanges = {};
1069
1242
  this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.BatchResponse, async (response) => {
@@ -1165,9 +1338,11 @@ class RemoteSyncStateHelper {
1165
1338
  core.ObjectHelper.propertyDelete(change.entity, "nodeIdentity");
1166
1339
  }
1167
1340
  }
1341
+ const now = new Date(Date.now()).toISOString();
1168
1342
  const syncChangeSet = {
1169
1343
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1170
- dateCreated: new Date(Date.now()).toISOString(),
1344
+ dateCreated: now,
1345
+ dateModified: now,
1171
1346
  storageKey,
1172
1347
  changes,
1173
1348
  nodeIdentity: this._nodeIdentity
@@ -1219,23 +1394,26 @@ class RemoteSyncStateHelper {
1219
1394
  const syncPointerStore = await this.getVerifiableSyncPointerStore();
1220
1395
  let syncState;
1221
1396
  if (!core.Is.empty(syncPointerStore.syncPointers[storageKey])) {
1222
- syncState = await this.getRemoteSyncState(syncPointerStore.syncPointers[storageKey]);
1397
+ syncState = await this.getSyncState(syncPointerStore.syncPointers[storageKey]);
1223
1398
  }
1224
1399
  // No current sync state, so we create a new one
1225
1400
  if (core.Is.empty(syncState)) {
1226
- syncState = { version: SYNC_STATE_VERSION, snapshots: [] };
1401
+ syncState = { version: SYNC_STATE_VERSION, storageKey, snapshots: [] };
1227
1402
  }
1228
1403
  // Sort the snapshots so the newest snapshot is last in the array
1229
1404
  const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
1230
1405
  // Get the current snapshot, if it does not exist we create a new one
1231
1406
  let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
1232
1407
  const now = new Date(Date.now()).toISOString();
1233
- if (core.Is.empty(currentSnapshot)) {
1408
+ // If there is no snapshot or the current one is a consolidation
1409
+ // we start a new snapshot
1410
+ if (core.Is.empty(currentSnapshot) || currentSnapshot.isConsolidated) {
1234
1411
  currentSnapshot = {
1235
1412
  version: SYNC_SNAPSHOT_VERSION,
1236
1413
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1237
1414
  dateCreated: now,
1238
1415
  dateModified: now,
1416
+ isConsolidated: false,
1239
1417
  changeSetStorageIds: []
1240
1418
  };
1241
1419
  syncState.snapshots.push(currentSnapshot);
@@ -1264,7 +1442,7 @@ class RemoteSyncStateHelper {
1264
1442
  message: "consolidationStarting"
1265
1443
  });
1266
1444
  // Perform a batch request to start the consolidation
1267
- await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize });
1445
+ await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize, requestMode: synchronisedStorageModels.SyncNodeIdentityMode.All });
1268
1446
  }
1269
1447
  /**
1270
1448
  * Get the sync pointer store.
@@ -1343,11 +1521,32 @@ class RemoteSyncStateHelper {
1343
1521
  await this._logging?.log({
1344
1522
  level: "info",
1345
1523
  source: this.CLASS_NAME,
1346
- message: "remoteSyncStateStoring",
1524
+ message: "syncStateStoring",
1347
1525
  data: {
1348
1526
  snapshotCount: syncState.snapshots.length
1349
1527
  }
1350
1528
  });
1529
+ // Limits the number of consolidations in the list so that we can shrink decentralised
1530
+ // storage requirements, sort from newest to oldest so that we can easily find the
1531
+ // oldest snapshots to remove.
1532
+ const snapshots = syncState.snapshots.sort((a, b) => new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime());
1533
+ // Find all the consolidation indexes
1534
+ const consolidationIndexes = [];
1535
+ for (let i = 0; i < snapshots.length; i++) {
1536
+ const snapshot = snapshots[i];
1537
+ if (snapshot.isConsolidated) {
1538
+ consolidationIndexes.push(i);
1539
+ }
1540
+ }
1541
+ if (consolidationIndexes.length > this._maxConsolidations) {
1542
+ // Once we have reached the max for consolidations we need to remove
1543
+ // all the snapshots, including non consolidated ones, beyond this point
1544
+ const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
1545
+ syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
1546
+ for (const snapshot of toRemove) {
1547
+ await this._blobStorageHelper.removeBlob(snapshot.id);
1548
+ }
1549
+ }
1351
1550
  return this._blobStorageHelper.saveBlob(syncState);
1352
1551
  }
1353
1552
  /**
@@ -1355,22 +1554,22 @@ class RemoteSyncStateHelper {
1355
1554
  * @param syncPointerId The id of the sync pointer to retrieve the state for.
1356
1555
  * @returns The remote sync state.
1357
1556
  */
1358
- async getRemoteSyncState(syncPointerId) {
1557
+ async getSyncState(syncPointerId) {
1359
1558
  try {
1360
1559
  await this._logging?.log({
1361
1560
  level: "info",
1362
1561
  source: this.CLASS_NAME,
1363
- message: "remoteSyncStateRetrieving",
1562
+ message: "syncStateRetrieving",
1364
1563
  data: {
1365
1564
  syncPointerId
1366
1565
  }
1367
1566
  });
1368
- const syncState = await this._blobStorageHelper.load(syncPointerId);
1567
+ const syncState = await this._blobStorageHelper.loadBlob(syncPointerId);
1369
1568
  if (core.Is.object(syncState)) {
1370
1569
  await this._logging?.log({
1371
1570
  level: "info",
1372
1571
  source: this.CLASS_NAME,
1373
- message: "remoteSyncStateRetrieved",
1572
+ message: "syncStateRetrieved",
1374
1573
  data: {
1375
1574
  syncPointerId,
1376
1575
  snapshotCount: syncState.snapshots.length
@@ -1393,7 +1592,7 @@ class RemoteSyncStateHelper {
1393
1592
  await this._logging?.log({
1394
1593
  level: "info",
1395
1594
  source: this.CLASS_NAME,
1396
- message: "remoteSyncStateNotFound",
1595
+ message: "syncStateNotFound",
1397
1596
  data: {
1398
1597
  syncPointerId
1399
1598
  }
@@ -1432,19 +1631,24 @@ class RemoteSyncStateHelper {
1432
1631
  let syncState;
1433
1632
  if (core.Is.stringValue(syncPointerStore.syncPointers[response.storageKey])) {
1434
1633
  // If the sync pointer exists, we load the current sync state
1435
- syncState = await this.getRemoteSyncState(syncPointerStore.syncPointers[response.storageKey]);
1634
+ syncState = await this.getSyncState(syncPointerStore.syncPointers[response.storageKey]);
1436
1635
  }
1437
1636
  // If the sync state does not exist, we create a new one
1438
- syncState ??= { version: SYNC_STATE_VERSION, snapshots: [] };
1637
+ syncState ??= {
1638
+ version: SYNC_STATE_VERSION,
1639
+ storageKey: response.storageKey,
1640
+ snapshots: []
1641
+ };
1439
1642
  const batchSnapshot = {
1440
1643
  version: SYNC_SNAPSHOT_VERSION,
1441
1644
  id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
1442
1645
  dateCreated: now,
1443
1646
  dateModified: now,
1647
+ isConsolidated: true,
1444
1648
  changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
1445
1649
  };
1446
1650
  syncState.snapshots.push(batchSnapshot);
1447
- // Store the sync state in the blob storage
1651
+ // Store the updated sync state
1448
1652
  const syncStateId = await this.storeRemoteSyncState(syncState);
1449
1653
  syncPointerStore.syncPointers[response.storageKey] = syncStateId;
1450
1654
  // Store the verifiable sync pointer in the verifiable storage
@@ -1474,11 +1678,14 @@ class RemoteSyncStateHelper {
1474
1678
  id: response.id
1475
1679
  }
1476
1680
  });
1681
+ // We have received a response to an item request, find the right storage
1682
+ // for the request id
1477
1683
  if (!core.Is.empty(this._populateFullChanges[response.storageKey])) {
1478
1684
  const idx = this._populateFullChanges[response.storageKey].requestIds.indexOf(response.id);
1479
1685
  if (idx !== -1) {
1480
1686
  this._populateFullChanges[response.storageKey].requestIds.splice(idx, 1);
1481
1687
  this._populateFullChanges[response.storageKey].entities[response.id] = response.entity;
1688
+ // If there are no request ids remaining we can complete the population
1482
1689
  if (this._populateFullChanges[response.storageKey].requestIds.length === 0) {
1483
1690
  await this._populateFullChanges[response.storageKey].completeCallback();
1484
1691
  }
@@ -1506,6 +1713,11 @@ class SynchronisedStorageService {
1506
1713
  * @internal
1507
1714
  */
1508
1715
  static _DEFAULT_CONSOLIDATION_BATCH_SIZE = 100;
1716
+ /**
1717
+ * The default max number of consolidations to keep in storage.
1718
+ * @internal
1719
+ */
1720
+ static _DEFAULT_MAX_CONSOLIDATIONS = 5;
1509
1721
  /**
1510
1722
  * Runtime name for the class.
1511
1723
  */
@@ -1624,6 +1836,7 @@ class SynchronisedStorageService {
1624
1836
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_INTERVAL_MINUTES,
1625
1837
  consolidationBatchSize: options.config.consolidationBatchSize ??
1626
1838
  SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE,
1839
+ maxConsolidations: options.config.maxConsolidations ?? SynchronisedStorageService._DEFAULT_MAX_CONSOLIDATIONS,
1627
1840
  blobStorageEncryptionKeyId: options.config.blobStorageEncryptionKeyId ?? "synchronised-storage-blob-encryption-key",
1628
1841
  verifiableStorageKeyId: options.config.verifiableStorageKeyId
1629
1842
  };
@@ -1639,11 +1852,16 @@ class SynchronisedStorageService {
1639
1852
  this._blobStorageHelper = new BlobStorageHelper(this._logging, this._vaultConnector, this._blobStorageConnector, this._config.blobStorageEncryptionKeyId, this._config.isTrustedNode);
1640
1853
  this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._identityConnector, this._blobStorageHelper, this._config.synchronisedStorageMethodId);
1641
1854
  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);
1855
+ this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._verifiableSyncPointerStorageConnector, this._blobStorageHelper, this._changeSetHelper, this._config.isTrustedNode, this._config.maxConsolidations);
1643
1856
  this._serviceStarted = false;
1644
1857
  this._activeStorageKeys = {};
1645
1858
  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));
1859
+ this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.LocalItemChange, async (event) => {
1860
+ // Make sure the change event is from this node
1861
+ if (core.Is.stringValue(this._nodeIdentity) && this._nodeIdentity === event.data.nodeIdentity) {
1862
+ await this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id);
1863
+ }
1864
+ });
1647
1865
  }
1648
1866
  /**
1649
1867
  * The component needs to be started when the node is initialized.
@@ -1733,7 +1951,7 @@ class SynchronisedStorageService {
1733
1951
  // to store the change set in the synchronised storage.
1734
1952
  // This will be performed using rights-management
1735
1953
  const copy = await this._changeSetHelper.copyChangeset(syncChangeSet);
1736
- if (!core.Is.empty(copy) && core.Is.stringValue(this._nodeIdentity)) {
1954
+ if (!core.Is.empty(copy)) {
1737
1955
  // Apply the changes to this node
1738
1956
  await this._changeSetHelper.applyChangeset(copy.syncChangeSet);
1739
1957
  // And update the sync state with the latest changes
@@ -1790,7 +2008,7 @@ class SynchronisedStorageService {
1790
2008
  if (!core.Is.empty(verifiableSyncPointerStore.syncPointers[storageKey])) {
1791
2009
  // Load the sync state from the remote blob storage using the sync pointer
1792
2010
  // to load the sync state
1793
- const remoteSyncState = await this._remoteSyncStateHelper.getRemoteSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
2011
+ const remoteSyncState = await this._remoteSyncStateHelper.getSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
1794
2012
  // If we got the sync state we can try and sync from it
1795
2013
  if (!core.Is.undefined(remoteSyncState)) {
1796
2014
  await this._localSyncStateHelper.applySyncState(storageKey, remoteSyncState);
@@ -1811,64 +2029,67 @@ class SynchronisedStorageService {
1811
2029
  storageKey
1812
2030
  }
1813
2031
  });
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);
2032
+ const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
2033
+ if (localChangeSnapshots.length > 0) {
2034
+ const localChangeSnapshot = localChangeSnapshots[0];
2035
+ if (core.Is.arrayValue(localChangeSnapshot.changes)) {
2036
+ await this._remoteSyncStateHelper.buildChangeSet(storageKey, localChangeSnapshot.changes, async (syncChangeSet, changeSetStorageId) => {
2037
+ if (core.Is.empty(syncChangeSet) && core.Is.empty(changeSetStorageId)) {
2038
+ await this._logging?.log({
2039
+ level: "info",
2040
+ source: this.CLASS_NAME,
2041
+ message: "builtStorageChangeSetNone",
2042
+ data: {
2043
+ storageKey
2044
+ }
2045
+ });
1843
2046
  }
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
2047
+ else {
1848
2048
  await this._logging?.log({
1849
2049
  level: "info",
1850
2050
  source: this.CLASS_NAME,
1851
- message: "sendingChangeSetToTrustedNode",
2051
+ message: "builtStorageChangeSet",
1852
2052
  data: {
1853
2053
  storageKey,
1854
2054
  changeSetStorageId
1855
2055
  }
1856
2056
  });
1857
- await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
1858
- await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2057
+ // Send the local changes to the remote storage if we are a trusted node
2058
+ if (this._config.isTrustedNode && core.Is.stringValue(changeSetStorageId)) {
2059
+ // If we are a trusted node, we can add the change set to the sync state
2060
+ // and remove the local change snapshot
2061
+ await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
2062
+ await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2063
+ }
2064
+ else if (!core.Is.empty(this._trustedSynchronisedStorageComponent) &&
2065
+ core.Is.object(syncChangeSet)) {
2066
+ // If we are not a trusted node, we need to send the changes to the trusted node
2067
+ // and then remove the local change snapshot
2068
+ await this._logging?.log({
2069
+ level: "info",
2070
+ source: this.CLASS_NAME,
2071
+ message: "sendingChangeSetToTrustedNode",
2072
+ data: {
2073
+ storageKey,
2074
+ changeSetStorageId
2075
+ }
2076
+ });
2077
+ await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
2078
+ await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
2079
+ }
1859
2080
  }
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
- });
2081
+ });
2082
+ }
2083
+ else {
2084
+ await this._logging?.log({
2085
+ level: "info",
2086
+ source: this.CLASS_NAME,
2087
+ message: "updateFromLocalSyncStateNoChanges",
2088
+ data: {
2089
+ storageKey
2090
+ }
2091
+ });
2092
+ }
1872
2093
  }
1873
2094
  }
1874
2095
  /**
@@ -1882,7 +2103,8 @@ class SynchronisedStorageService {
1882
2103
  try {
1883
2104
  // If we are performing a consolidation, we can remove the local change snapshot
1884
2105
  // as we are going to create a complete changeset from the DB
1885
- localChangeSnapshot = await this._localSyncStateHelper.getLocalChangeSnapshot(storageKey);
2106
+ const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
2107
+ localChangeSnapshot = localChangeSnapshots[0];
1886
2108
  if (!core.Is.empty(localChangeSnapshot)) {
1887
2109
  await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
1888
2110
  }