@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.
- package/dist/cjs/index.cjs +422 -160
- package/dist/esm/index.mjs +423 -161
- package/dist/types/entities/syncSnapshotEntry.d.ts +15 -3
- package/dist/types/helpers/blobStorageHelper.d.ts +7 -1
- package/dist/types/helpers/changeSetHelper.d.ts +8 -1
- package/dist/types/helpers/localSyncStateHelper.d.ts +4 -3
- package/dist/types/helpers/remoteSyncStateHelper.d.ts +3 -2
- package/dist/types/models/ISyncSnapshot.d.ts +8 -0
- package/dist/types/models/ISyncState.d.ts +4 -0
- package/dist/types/models/ISynchronisedStorageServiceConfig.d.ts +5 -0
- package/docs/architecture.md +4 -1
- package/docs/changelog.md +28 -0
- package/docs/open-api/spec.json +2 -0
- package/docs/reference/classes/SyncSnapshotEntry.md +29 -5
- package/docs/reference/interfaces/ISyncSnapshot.md +16 -0
- package/docs/reference/interfaces/ISyncState.md +8 -0
- package/docs/reference/interfaces/ISynchronisedStorageServiceConfig.md +14 -0
- package/locales/en.json +16 -9
- package/package.json +2 -2
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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"
|
|
78
|
+
property({ type: "string" }),
|
|
63
79
|
__metadata("design:type", String)
|
|
64
80
|
], SyncSnapshotEntry.prototype, "dateModified", void 0);
|
|
65
81
|
__decorate([
|
|
66
|
-
property({ type: "boolean"
|
|
82
|
+
property({ type: "boolean" }),
|
|
67
83
|
__metadata("design:type", Boolean)
|
|
68
|
-
], SyncSnapshotEntry.prototype, "
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
localChangeSnapshot.changes.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
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
|
|
892
|
+
async getSnapshots(storageKey, isLocal) {
|
|
785
893
|
await this._logging?.log({
|
|
786
894
|
level: "info",
|
|
787
895
|
source: this.CLASS_NAME,
|
|
788
|
-
message: "
|
|
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: "
|
|
797
|
-
value:
|
|
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: "
|
|
919
|
+
message: "getSnapshotsExists",
|
|
812
920
|
data: {
|
|
813
921
|
storageKey
|
|
814
922
|
}
|
|
815
923
|
});
|
|
816
|
-
return queryResult.entities
|
|
924
|
+
return queryResult.entities;
|
|
817
925
|
}
|
|
818
926
|
await this._logging?.log({
|
|
819
927
|
level: "info",
|
|
820
928
|
source: this.CLASS_NAME,
|
|
821
|
-
message: "
|
|
929
|
+
message: "getSnapshotsDoesNotExist",
|
|
822
930
|
data: {
|
|
823
931
|
storageKey
|
|
824
932
|
}
|
|
825
933
|
});
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
|
|
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: "
|
|
1016
|
+
message: "applySnapshotNoExisting",
|
|
890
1017
|
data: {
|
|
891
|
-
|
|
892
|
-
dateCreated: new Date(snapshot.dateCreated).toISOString()
|
|
1018
|
+
storageKey
|
|
893
1019
|
}
|
|
894
1020
|
});
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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.
|
|
936
|
-
localModified: new Date(modifiedSnapshot.
|
|
937
|
-
modifiedSnapshot.
|
|
938
|
-
remoteModified: new Date(modifiedSnapshot.
|
|
939
|
-
modifiedSnapshot.
|
|
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.
|
|
943
|
-
const localChangeSetStorageIds = modifiedSnapshot.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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: "
|
|
1603
|
+
message: "syncStateRetrieving",
|
|
1362
1604
|
data: {
|
|
1363
1605
|
syncPointerId
|
|
1364
1606
|
}
|
|
1365
1607
|
});
|
|
1366
|
-
const syncState = await this._blobStorageHelper.
|
|
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: "
|
|
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: "
|
|
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.
|
|
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 ??= {
|
|
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
|
|
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) =>
|
|
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)
|
|
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.
|
|
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
|
|
1813
|
-
if (
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
|
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: "
|
|
2097
|
+
message: "builtStorageChangeSet",
|
|
1850
2098
|
data: {
|
|
1851
2099
|
storageKey,
|
|
1852
2100
|
changeSetStorageId
|
|
1853
2101
|
}
|
|
1854
2102
|
});
|
|
1855
|
-
|
|
1856
|
-
|
|
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
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
|
1882
|
-
//
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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,
|