@twin.org/synchronised-storage-service 0.0.1-next.5 → 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 +101 -61
- package/dist/esm/index.mjs +101 -61
- package/dist/types/entities/syncSnapshotEntry.d.ts +4 -0
- package/dist/types/models/ISyncSnapshot.d.ts +4 -0
- package/docs/changelog.md +14 -0
- package/docs/reference/classes/SyncSnapshotEntry.md +8 -0
- package/docs/reference/interfaces/ISyncSnapshot.md +8 -0
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -47,6 +47,10 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
|
|
|
47
47
|
* The flag to determine if this is a consolidated snapshot.
|
|
48
48
|
*/
|
|
49
49
|
isConsolidated;
|
|
50
|
+
/**
|
|
51
|
+
* The epoch for the changeset.
|
|
52
|
+
*/
|
|
53
|
+
epoch;
|
|
50
54
|
/**
|
|
51
55
|
* The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
|
|
52
56
|
*/
|
|
@@ -84,6 +88,10 @@ __decorate([
|
|
|
84
88
|
entity.property({ type: "boolean" }),
|
|
85
89
|
__metadata("design:type", Boolean)
|
|
86
90
|
], exports.SyncSnapshotEntry.prototype, "isConsolidated", void 0);
|
|
91
|
+
__decorate([
|
|
92
|
+
entity.property({ type: "number" }),
|
|
93
|
+
__metadata("design:type", Number)
|
|
94
|
+
], exports.SyncSnapshotEntry.prototype, "epoch", void 0);
|
|
87
95
|
__decorate([
|
|
88
96
|
entity.property({ type: "array", itemType: "string", optional: true }),
|
|
89
97
|
__metadata("design:type", Array)
|
|
@@ -867,6 +875,9 @@ class LocalSyncStateHelper {
|
|
|
867
875
|
if (previousChangeIndex !== -1) {
|
|
868
876
|
localChangeSnapshot.changes.splice(previousChangeIndex, 1);
|
|
869
877
|
}
|
|
878
|
+
// If we already have changes from previous updates
|
|
879
|
+
// then make sure we update the dateModified, otherwise
|
|
880
|
+
// we assume this is the first change and setting modified is not necessary
|
|
870
881
|
if (localChangeSnapshot.changes.length > 0) {
|
|
871
882
|
localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
|
|
872
883
|
}
|
|
@@ -932,7 +943,8 @@ class LocalSyncStateHelper {
|
|
|
932
943
|
dateModified: now,
|
|
933
944
|
changeSetStorageIds: [],
|
|
934
945
|
isLocal,
|
|
935
|
-
isConsolidated: false
|
|
946
|
+
isConsolidated: false,
|
|
947
|
+
epoch: 0
|
|
936
948
|
}
|
|
937
949
|
];
|
|
938
950
|
}
|
|
@@ -984,13 +996,22 @@ class LocalSyncStateHelper {
|
|
|
984
996
|
}
|
|
985
997
|
});
|
|
986
998
|
// Get all the existing snapshots that we have processed previously
|
|
987
|
-
|
|
999
|
+
let existingSnapshots = await this.getSnapshots(storageKey, false);
|
|
988
1000
|
// Sort from newest to oldest
|
|
989
|
-
|
|
990
|
-
//
|
|
991
|
-
|
|
992
|
-
//
|
|
993
|
-
|
|
1001
|
+
existingSnapshots = existingSnapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
|
|
1002
|
+
// Sort from newest to oldest
|
|
1003
|
+
const syncStateSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
|
|
1004
|
+
// Get the newest epoch from the local storage
|
|
1005
|
+
const newestExistingEpoch = existingSnapshots[0]?.epoch ?? 0;
|
|
1006
|
+
// Get the oldest epoch from the remote storage
|
|
1007
|
+
const oldestSyncStateEpoch = syncStateSnapshots[syncStateSnapshots.length - 1]?.epoch ?? 0;
|
|
1008
|
+
// If there is a gap between the largest epoch we have locally
|
|
1009
|
+
// and the smallest epoch we have remotely then we have missed
|
|
1010
|
+
// data so we need to perform a full sync
|
|
1011
|
+
const hasEpochGap = newestExistingEpoch + 1 < oldestSyncStateEpoch;
|
|
1012
|
+
// If we have an epoch gap or no existing snapshots then we need to apply
|
|
1013
|
+
// a full sync from a consolidation
|
|
1014
|
+
if (!existingSnapshots.some(s => s.isConsolidated) || hasEpochGap) {
|
|
994
1015
|
await this._logging?.log({
|
|
995
1016
|
level: "info",
|
|
996
1017
|
source: this.CLASS_NAME,
|
|
@@ -999,28 +1020,35 @@ class LocalSyncStateHelper {
|
|
|
999
1020
|
storageKey
|
|
1000
1021
|
}
|
|
1001
1022
|
});
|
|
1002
|
-
const
|
|
1003
|
-
if (
|
|
1004
|
-
// We found
|
|
1023
|
+
const mostRecentConsolidation = syncStateSnapshots.findIndex(snapshot => snapshot.isConsolidated);
|
|
1024
|
+
if (mostRecentConsolidation !== -1) {
|
|
1025
|
+
// We found the most recent consolidated snapshot, we can use it
|
|
1005
1026
|
await this._logging?.log({
|
|
1006
1027
|
level: "info",
|
|
1007
1028
|
source: this.CLASS_NAME,
|
|
1008
1029
|
message: "applySnapshotFoundConsolidated",
|
|
1009
1030
|
data: {
|
|
1010
1031
|
storageKey,
|
|
1011
|
-
snapshotId:
|
|
1032
|
+
snapshotId: syncStateSnapshots[mostRecentConsolidation].id
|
|
1012
1033
|
}
|
|
1013
1034
|
});
|
|
1014
1035
|
// We need to reset the entity storage and remove all the remote items
|
|
1015
|
-
// so that we use just the ones from the consolidation
|
|
1036
|
+
// so that we use just the ones from the consolidation, since
|
|
1037
|
+
// we don't have any existing there shouldn't be any remote entries
|
|
1038
|
+
// but we reset nonetheless
|
|
1016
1039
|
await this._changeSetHelper.reset(storageKey, synchronisedStorageModels.SyncNodeIdentityMode.Remote);
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1040
|
+
// We need to process the most recent consolidation and all changes
|
|
1041
|
+
// that were made since then, from newest to oldest (so newer changes override older ones)
|
|
1042
|
+
// Process snapshots from the consolidation point (most recent) back to the newest
|
|
1043
|
+
for (let i = mostRecentConsolidation; i >= 0; i--) {
|
|
1044
|
+
await this.processNewSnapshots([
|
|
1045
|
+
{
|
|
1046
|
+
...syncStateSnapshots[i],
|
|
1047
|
+
storageKey,
|
|
1048
|
+
isLocal: false
|
|
1049
|
+
}
|
|
1050
|
+
]);
|
|
1051
|
+
}
|
|
1024
1052
|
}
|
|
1025
1053
|
else {
|
|
1026
1054
|
await this._logging?.log({
|
|
@@ -1034,15 +1062,20 @@ class LocalSyncStateHelper {
|
|
|
1034
1062
|
}
|
|
1035
1063
|
}
|
|
1036
1064
|
else {
|
|
1065
|
+
// We have existing consolidated remote snapshots, so we can assume that we have
|
|
1066
|
+
// applied at least one consolidation snapshot, in this case we need to look at the changes since
|
|
1067
|
+
// then and apply them if we haven't already
|
|
1068
|
+
// We don't need to apply any additional consolidated snapshots, just the changesets
|
|
1037
1069
|
// Create a lookup map for the existing snapshots
|
|
1038
|
-
const
|
|
1039
|
-
for (const snapshot of
|
|
1040
|
-
|
|
1070
|
+
const existingSnapshotsMap = {};
|
|
1071
|
+
for (const snapshot of existingSnapshots) {
|
|
1072
|
+
existingSnapshotsMap[snapshot.id] = snapshot;
|
|
1041
1073
|
}
|
|
1042
1074
|
const newSnapshots = [];
|
|
1043
1075
|
const modifiedSnapshots = [];
|
|
1044
|
-
const referencedExistingSnapshots = Object.keys(
|
|
1045
|
-
|
|
1076
|
+
const referencedExistingSnapshots = Object.keys(existingSnapshotsMap);
|
|
1077
|
+
let completedProcessing = false;
|
|
1078
|
+
for (const snapshot of syncStateSnapshots) {
|
|
1046
1079
|
await this._logging?.log({
|
|
1047
1080
|
level: "info",
|
|
1048
1081
|
source: this.CLASS_NAME,
|
|
@@ -1052,34 +1085,37 @@ class LocalSyncStateHelper {
|
|
|
1052
1085
|
dateCreated: new Date(snapshot.dateCreated).toISOString()
|
|
1053
1086
|
}
|
|
1054
1087
|
});
|
|
1055
|
-
// See if we have the
|
|
1056
|
-
const currentSnapshot =
|
|
1088
|
+
// See if we have the snapshot stored locally
|
|
1089
|
+
const currentSnapshot = existingSnapshotsMap[snapshot.id];
|
|
1057
1090
|
// As we are referencing an existing snapshot, we need to remove it from the list
|
|
1058
1091
|
// to allow us to cleanup any unreferenced snapshots later
|
|
1059
1092
|
const idx = referencedExistingSnapshots.indexOf(snapshot.id);
|
|
1060
1093
|
if (idx !== -1) {
|
|
1061
1094
|
referencedExistingSnapshots.splice(idx, 1);
|
|
1062
1095
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1096
|
+
// No need to apply consolidated snapshots
|
|
1097
|
+
if (!snapshot.isConsolidated && !completedProcessing) {
|
|
1098
|
+
const updatedSnapshot = {
|
|
1099
|
+
...snapshot,
|
|
1100
|
+
storageKey,
|
|
1101
|
+
isLocal: false
|
|
1102
|
+
};
|
|
1103
|
+
if (core.Is.empty(currentSnapshot)) {
|
|
1104
|
+
// We don't have the snapshot locally, so we need to process all of it
|
|
1105
|
+
newSnapshots.push(updatedSnapshot);
|
|
1106
|
+
}
|
|
1107
|
+
else if (currentSnapshot.dateModified !== snapshot.dateModified) {
|
|
1108
|
+
// If the local snapshot has a different dateModified, we need to update it
|
|
1109
|
+
modifiedSnapshots.push({
|
|
1110
|
+
currentSnapshot,
|
|
1111
|
+
updatedSnapshot
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
// we sorted the snapshots from newest to oldest, so if we found a local snapshot
|
|
1116
|
+
// with the same dateModified as the remote snapshot, we can stop processing further
|
|
1117
|
+
completedProcessing = true;
|
|
1118
|
+
}
|
|
1083
1119
|
}
|
|
1084
1120
|
}
|
|
1085
1121
|
// We reverse the order of the snapshots to process them from oldest to newest
|
|
@@ -1404,6 +1440,7 @@ class RemoteSyncStateHelper {
|
|
|
1404
1440
|
const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
|
|
1405
1441
|
// Get the current snapshot, if it does not exist we create a new one
|
|
1406
1442
|
let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
|
|
1443
|
+
const currentEpoch = currentSnapshot?.epoch ?? 0;
|
|
1407
1444
|
const now = new Date(Date.now()).toISOString();
|
|
1408
1445
|
// If there is no snapshot or the current one is a consolidation
|
|
1409
1446
|
// we start a new snapshot
|
|
@@ -1414,6 +1451,7 @@ class RemoteSyncStateHelper {
|
|
|
1414
1451
|
dateCreated: now,
|
|
1415
1452
|
dateModified: now,
|
|
1416
1453
|
isConsolidated: false,
|
|
1454
|
+
epoch: currentEpoch + 1,
|
|
1417
1455
|
changeSetStorageIds: []
|
|
1418
1456
|
};
|
|
1419
1457
|
syncState.snapshots.push(currentSnapshot);
|
|
@@ -1544,7 +1582,12 @@ class RemoteSyncStateHelper {
|
|
|
1544
1582
|
const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1545
1583
|
syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1546
1584
|
for (const snapshot of toRemove) {
|
|
1547
|
-
|
|
1585
|
+
// We need to remove all the storage ids associated with the snapshot
|
|
1586
|
+
if (core.Is.arrayValue(snapshot.changeSetStorageIds)) {
|
|
1587
|
+
for (const storageId of snapshot.changeSetStorageIds) {
|
|
1588
|
+
await this._blobStorageHelper.removeBlob(storageId);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1548
1591
|
}
|
|
1549
1592
|
}
|
|
1550
1593
|
return this._blobStorageHelper.saveBlob(syncState);
|
|
@@ -1639,12 +1682,17 @@ class RemoteSyncStateHelper {
|
|
|
1639
1682
|
storageKey: response.storageKey,
|
|
1640
1683
|
snapshots: []
|
|
1641
1684
|
};
|
|
1685
|
+
// Sort the snapshots so the newest snapshot is last in the array
|
|
1686
|
+
const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
|
|
1687
|
+
const currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
|
|
1688
|
+
const currentEpoch = currentSnapshot?.epoch ?? 0;
|
|
1642
1689
|
const batchSnapshot = {
|
|
1643
1690
|
version: SYNC_SNAPSHOT_VERSION,
|
|
1644
1691
|
id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
|
|
1645
1692
|
dateCreated: now,
|
|
1646
1693
|
dateModified: now,
|
|
1647
1694
|
isConsolidated: true,
|
|
1695
|
+
epoch: currentEpoch + 1,
|
|
1648
1696
|
changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
|
|
1649
1697
|
};
|
|
1650
1698
|
syncState.snapshots.push(batchSnapshot);
|
|
@@ -2099,25 +2147,17 @@ class SynchronisedStorageService {
|
|
|
2099
2147
|
* @internal
|
|
2100
2148
|
*/
|
|
2101
2149
|
async startConsolidationSync(storageKey) {
|
|
2102
|
-
let localChangeSnapshot;
|
|
2103
2150
|
try {
|
|
2104
|
-
// If we are
|
|
2105
|
-
//
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
}
|
|
2151
|
+
// If we are going to perform a consolidation first take any local updates
|
|
2152
|
+
// we have and create a changeset from them, so that anybody applying
|
|
2153
|
+
// just changes since a consolidation can use the changeset
|
|
2154
|
+
// and skip the consolidation
|
|
2155
|
+
await this.updateFromLocalSyncState(storageKey);
|
|
2156
|
+
// Now start the consolidation
|
|
2111
2157
|
await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
|
|
2112
2158
|
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
|
|
2113
|
-
// The consolidation was successful, so we can remove the local change snapshot permanently
|
|
2114
|
-
localChangeSnapshot = undefined;
|
|
2115
2159
|
}
|
|
2116
2160
|
catch (error) {
|
|
2117
|
-
if (localChangeSnapshot) {
|
|
2118
|
-
// If the consolidation failed, we can keep the local change snapshot
|
|
2119
|
-
await this._localSyncStateHelper.setLocalChangeSnapshot(localChangeSnapshot);
|
|
2120
|
-
}
|
|
2121
2161
|
await this._logging?.log({
|
|
2122
2162
|
level: "error",
|
|
2123
2163
|
source: this.CLASS_NAME,
|
package/dist/esm/index.mjs
CHANGED
|
@@ -45,6 +45,10 @@ let SyncSnapshotEntry = class SyncSnapshotEntry {
|
|
|
45
45
|
* The flag to determine if this is a consolidated snapshot.
|
|
46
46
|
*/
|
|
47
47
|
isConsolidated;
|
|
48
|
+
/**
|
|
49
|
+
* The epoch for the changeset.
|
|
50
|
+
*/
|
|
51
|
+
epoch;
|
|
48
52
|
/**
|
|
49
53
|
* The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
|
|
50
54
|
*/
|
|
@@ -82,6 +86,10 @@ __decorate([
|
|
|
82
86
|
property({ type: "boolean" }),
|
|
83
87
|
__metadata("design:type", Boolean)
|
|
84
88
|
], SyncSnapshotEntry.prototype, "isConsolidated", void 0);
|
|
89
|
+
__decorate([
|
|
90
|
+
property({ type: "number" }),
|
|
91
|
+
__metadata("design:type", Number)
|
|
92
|
+
], SyncSnapshotEntry.prototype, "epoch", void 0);
|
|
85
93
|
__decorate([
|
|
86
94
|
property({ type: "array", itemType: "string", optional: true }),
|
|
87
95
|
__metadata("design:type", Array)
|
|
@@ -865,6 +873,9 @@ class LocalSyncStateHelper {
|
|
|
865
873
|
if (previousChangeIndex !== -1) {
|
|
866
874
|
localChangeSnapshot.changes.splice(previousChangeIndex, 1);
|
|
867
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
|
|
868
879
|
if (localChangeSnapshot.changes.length > 0) {
|
|
869
880
|
localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
|
|
870
881
|
}
|
|
@@ -930,7 +941,8 @@ class LocalSyncStateHelper {
|
|
|
930
941
|
dateModified: now,
|
|
931
942
|
changeSetStorageIds: [],
|
|
932
943
|
isLocal,
|
|
933
|
-
isConsolidated: false
|
|
944
|
+
isConsolidated: false,
|
|
945
|
+
epoch: 0
|
|
934
946
|
}
|
|
935
947
|
];
|
|
936
948
|
}
|
|
@@ -982,13 +994,22 @@ class LocalSyncStateHelper {
|
|
|
982
994
|
}
|
|
983
995
|
});
|
|
984
996
|
// Get all the existing snapshots that we have processed previously
|
|
985
|
-
|
|
997
|
+
let existingSnapshots = await this.getSnapshots(storageKey, false);
|
|
986
998
|
// Sort from newest to oldest
|
|
987
|
-
|
|
988
|
-
//
|
|
989
|
-
|
|
990
|
-
//
|
|
991
|
-
|
|
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) {
|
|
992
1013
|
await this._logging?.log({
|
|
993
1014
|
level: "info",
|
|
994
1015
|
source: this.CLASS_NAME,
|
|
@@ -997,28 +1018,35 @@ class LocalSyncStateHelper {
|
|
|
997
1018
|
storageKey
|
|
998
1019
|
}
|
|
999
1020
|
});
|
|
1000
|
-
const
|
|
1001
|
-
if (
|
|
1002
|
-
// We found
|
|
1021
|
+
const mostRecentConsolidation = syncStateSnapshots.findIndex(snapshot => snapshot.isConsolidated);
|
|
1022
|
+
if (mostRecentConsolidation !== -1) {
|
|
1023
|
+
// We found the most recent consolidated snapshot, we can use it
|
|
1003
1024
|
await this._logging?.log({
|
|
1004
1025
|
level: "info",
|
|
1005
1026
|
source: this.CLASS_NAME,
|
|
1006
1027
|
message: "applySnapshotFoundConsolidated",
|
|
1007
1028
|
data: {
|
|
1008
1029
|
storageKey,
|
|
1009
|
-
snapshotId:
|
|
1030
|
+
snapshotId: syncStateSnapshots[mostRecentConsolidation].id
|
|
1010
1031
|
}
|
|
1011
1032
|
});
|
|
1012
1033
|
// We need to reset the entity storage and remove all the remote items
|
|
1013
|
-
// so that we use just the ones from the consolidation
|
|
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
|
|
1014
1037
|
await this._changeSetHelper.reset(storageKey, SyncNodeIdentityMode.Remote);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
+
}
|
|
1022
1050
|
}
|
|
1023
1051
|
else {
|
|
1024
1052
|
await this._logging?.log({
|
|
@@ -1032,15 +1060,20 @@ class LocalSyncStateHelper {
|
|
|
1032
1060
|
}
|
|
1033
1061
|
}
|
|
1034
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
|
|
1035
1067
|
// Create a lookup map for the existing snapshots
|
|
1036
|
-
const
|
|
1037
|
-
for (const snapshot of
|
|
1038
|
-
|
|
1068
|
+
const existingSnapshotsMap = {};
|
|
1069
|
+
for (const snapshot of existingSnapshots) {
|
|
1070
|
+
existingSnapshotsMap[snapshot.id] = snapshot;
|
|
1039
1071
|
}
|
|
1040
1072
|
const newSnapshots = [];
|
|
1041
1073
|
const modifiedSnapshots = [];
|
|
1042
|
-
const referencedExistingSnapshots = Object.keys(
|
|
1043
|
-
|
|
1074
|
+
const referencedExistingSnapshots = Object.keys(existingSnapshotsMap);
|
|
1075
|
+
let completedProcessing = false;
|
|
1076
|
+
for (const snapshot of syncStateSnapshots) {
|
|
1044
1077
|
await this._logging?.log({
|
|
1045
1078
|
level: "info",
|
|
1046
1079
|
source: this.CLASS_NAME,
|
|
@@ -1050,34 +1083,37 @@ class LocalSyncStateHelper {
|
|
|
1050
1083
|
dateCreated: new Date(snapshot.dateCreated).toISOString()
|
|
1051
1084
|
}
|
|
1052
1085
|
});
|
|
1053
|
-
// See if we have the
|
|
1054
|
-
const currentSnapshot =
|
|
1086
|
+
// See if we have the snapshot stored locally
|
|
1087
|
+
const currentSnapshot = existingSnapshotsMap[snapshot.id];
|
|
1055
1088
|
// As we are referencing an existing snapshot, we need to remove it from the list
|
|
1056
1089
|
// to allow us to cleanup any unreferenced snapshots later
|
|
1057
1090
|
const idx = referencedExistingSnapshots.indexOf(snapshot.id);
|
|
1058
1091
|
if (idx !== -1) {
|
|
1059
1092
|
referencedExistingSnapshots.splice(idx, 1);
|
|
1060
1093
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
+
}
|
|
1081
1117
|
}
|
|
1082
1118
|
}
|
|
1083
1119
|
// We reverse the order of the snapshots to process them from oldest to newest
|
|
@@ -1402,6 +1438,7 @@ class RemoteSyncStateHelper {
|
|
|
1402
1438
|
const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
|
|
1403
1439
|
// Get the current snapshot, if it does not exist we create a new one
|
|
1404
1440
|
let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
|
|
1441
|
+
const currentEpoch = currentSnapshot?.epoch ?? 0;
|
|
1405
1442
|
const now = new Date(Date.now()).toISOString();
|
|
1406
1443
|
// If there is no snapshot or the current one is a consolidation
|
|
1407
1444
|
// we start a new snapshot
|
|
@@ -1412,6 +1449,7 @@ class RemoteSyncStateHelper {
|
|
|
1412
1449
|
dateCreated: now,
|
|
1413
1450
|
dateModified: now,
|
|
1414
1451
|
isConsolidated: false,
|
|
1452
|
+
epoch: currentEpoch + 1,
|
|
1415
1453
|
changeSetStorageIds: []
|
|
1416
1454
|
};
|
|
1417
1455
|
syncState.snapshots.push(currentSnapshot);
|
|
@@ -1542,7 +1580,12 @@ class RemoteSyncStateHelper {
|
|
|
1542
1580
|
const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1543
1581
|
syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1544
1582
|
for (const snapshot of toRemove) {
|
|
1545
|
-
|
|
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
|
+
}
|
|
1546
1589
|
}
|
|
1547
1590
|
}
|
|
1548
1591
|
return this._blobStorageHelper.saveBlob(syncState);
|
|
@@ -1637,12 +1680,17 @@ class RemoteSyncStateHelper {
|
|
|
1637
1680
|
storageKey: response.storageKey,
|
|
1638
1681
|
snapshots: []
|
|
1639
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;
|
|
1640
1687
|
const batchSnapshot = {
|
|
1641
1688
|
version: SYNC_SNAPSHOT_VERSION,
|
|
1642
1689
|
id: Converter.bytesToHex(RandomHelper.generate(32)),
|
|
1643
1690
|
dateCreated: now,
|
|
1644
1691
|
dateModified: now,
|
|
1645
1692
|
isConsolidated: true,
|
|
1693
|
+
epoch: currentEpoch + 1,
|
|
1646
1694
|
changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
|
|
1647
1695
|
};
|
|
1648
1696
|
syncState.snapshots.push(batchSnapshot);
|
|
@@ -2097,25 +2145,17 @@ class SynchronisedStorageService {
|
|
|
2097
2145
|
* @internal
|
|
2098
2146
|
*/
|
|
2099
2147
|
async startConsolidationSync(storageKey) {
|
|
2100
|
-
let localChangeSnapshot;
|
|
2101
2148
|
try {
|
|
2102
|
-
// If we are
|
|
2103
|
-
//
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
}
|
|
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
|
|
2109
2155
|
await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
|
|
2110
2156
|
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
|
|
2111
|
-
// The consolidation was successful, so we can remove the local change snapshot permanently
|
|
2112
|
-
localChangeSnapshot = undefined;
|
|
2113
2157
|
}
|
|
2114
2158
|
catch (error) {
|
|
2115
|
-
if (localChangeSnapshot) {
|
|
2116
|
-
// If the consolidation failed, we can keep the local change snapshot
|
|
2117
|
-
await this._localSyncStateHelper.setLocalChangeSnapshot(localChangeSnapshot);
|
|
2118
|
-
}
|
|
2119
2159
|
await this._logging?.log({
|
|
2120
2160
|
level: "error",
|
|
2121
2161
|
source: this.CLASS_NAME,
|
|
@@ -31,6 +31,10 @@ export declare class SyncSnapshotEntry<T extends ISynchronisedEntity = ISynchron
|
|
|
31
31
|
* The flag to determine if this is a consolidated snapshot.
|
|
32
32
|
*/
|
|
33
33
|
isConsolidated: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The epoch for the changeset.
|
|
36
|
+
*/
|
|
37
|
+
epoch: number;
|
|
34
38
|
/**
|
|
35
39
|
* The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
|
|
36
40
|
*/
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.1-next.6](https://github.com/twinfoundation/synchronised-storage/compare/synchronised-storage-service-v0.0.1-next.5...synchronised-storage-service-v0.0.1-next.6) (2025-08-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* improve consolidation logic ([698232f](https://github.com/twinfoundation/synchronised-storage/commit/698232f57640f87642ecd323cb1e4670eda33343))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/synchronised-storage-models bumped from 0.0.1-next.5 to 0.0.1-next.6
|
|
16
|
+
|
|
3
17
|
## [0.0.1-next.5](https://github.com/twinfoundation/synchronised-storage/compare/synchronised-storage-service-v0.0.1-next.4...synchronised-storage-service-v0.0.1-next.5) (2025-08-11)
|
|
4
18
|
|
|
5
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/synchronised-storage-service",
|
|
3
|
-
"version": "0.0.1-next.
|
|
3
|
+
"version": "0.0.1-next.6",
|
|
4
4
|
"description": "Synchronised storage contract implementation and REST endpoint definitions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@twin.org/logging-models": "next",
|
|
27
27
|
"@twin.org/nameof": "next",
|
|
28
28
|
"@twin.org/standards-w3c-did": "next",
|
|
29
|
-
"@twin.org/synchronised-storage-models": "0.0.1-next.
|
|
29
|
+
"@twin.org/synchronised-storage-models": "0.0.1-next.6",
|
|
30
30
|
"@twin.org/verifiable-storage-models": "next",
|
|
31
31
|
"@twin.org/web": "next"
|
|
32
32
|
},
|