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