@twin.org/synchronised-storage-service 0.0.1-next.9 → 0.0.3-next.1
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/es/data/verifiableStorageKeys.json +5 -0
- package/dist/es/entities/syncSnapshotEntry.js +93 -0
- package/dist/es/entities/syncSnapshotEntry.js.map +1 -0
- package/dist/es/helpers/blobStorageHelper.js +185 -0
- package/dist/es/helpers/blobStorageHelper.js.map +1 -0
- package/dist/es/helpers/changeSetHelper.js +215 -0
- package/dist/es/helpers/changeSetHelper.js.map +1 -0
- package/dist/es/helpers/localSyncStateHelper.js +384 -0
- package/dist/es/helpers/localSyncStateHelper.js.map +1 -0
- package/dist/es/helpers/remoteSyncStateHelper.js +560 -0
- package/dist/es/helpers/remoteSyncStateHelper.js.map +1 -0
- package/dist/es/helpers/versions.js +6 -0
- package/dist/es/helpers/versions.js.map +1 -0
- package/dist/es/index.js +13 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/ISyncPointerStore.js +4 -0
- package/dist/es/models/ISyncPointerStore.js.map +1 -0
- package/dist/es/models/ISyncSnapshot.js +4 -0
- package/dist/es/models/ISyncSnapshot.js.map +1 -0
- package/dist/es/models/ISyncState.js +2 -0
- package/dist/es/models/ISyncState.js.map +1 -0
- package/dist/es/models/ISynchronisedStorageServiceConfig.js +4 -0
- package/dist/es/models/ISynchronisedStorageServiceConfig.js.map +1 -0
- package/dist/es/models/ISynchronisedStorageServiceConstructorOptions.js +2 -0
- package/dist/es/models/ISynchronisedStorageServiceConstructorOptions.js.map +1 -0
- package/dist/es/restEntryPoints.js +10 -0
- package/dist/es/restEntryPoints.js.map +1 -0
- package/dist/es/schema.js +11 -0
- package/dist/es/schema.js.map +1 -0
- package/dist/es/synchronisedStorageRoutes.js +153 -0
- package/dist/es/synchronisedStorageRoutes.js.map +1 -0
- package/dist/es/synchronisedStorageService.js +554 -0
- package/dist/es/synchronisedStorageService.js.map +1 -0
- package/dist/types/entities/syncSnapshotEntry.d.ts +3 -3
- package/dist/types/helpers/blobStorageHelper.d.ts +3 -3
- package/dist/types/helpers/changeSetHelper.d.ts +16 -32
- package/dist/types/helpers/localSyncStateHelper.d.ts +11 -11
- package/dist/types/helpers/remoteSyncStateHelper.d.ts +18 -14
- package/dist/types/index.d.ts +10 -10
- package/dist/types/models/ISyncState.d.ts +1 -1
- package/dist/types/models/ISynchronisedStorageServiceConfig.d.ts +3 -8
- package/dist/types/models/ISynchronisedStorageServiceConstructorOptions.d.ts +7 -6
- package/dist/types/synchronisedStorageRoutes.d.ts +1 -1
- package/dist/types/synchronisedStorageService.d.ts +18 -21
- package/docs/architecture.md +168 -12
- package/docs/changelog.md +135 -0
- package/docs/open-api/spec.json +62 -57
- package/docs/reference/classes/SyncSnapshotEntry.md +4 -10
- package/docs/reference/classes/SynchronisedStorageService.md +38 -50
- package/docs/reference/interfaces/ISynchronisedStorageServiceConfig.md +3 -17
- package/docs/reference/interfaces/ISynchronisedStorageServiceConstructorOptions.md +9 -8
- package/locales/en.json +11 -16
- package/package.json +26 -9
- package/dist/cjs/index.cjs +0 -2235
- package/dist/esm/index.mjs +0 -2227
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mainnet": "verifiable:iota:0x9c3dd66dcd05d86ac1b9485fdcc912dd3ef63447c86ad7af94709b77ba552909:0x66c6434514fb55cc0f18d253a42a37e50ba64d8011a6194bec427570d21fcfdc",
|
|
3
|
+
"testnet": "verifiable:iota:0xf0093c80eb9569e04022143517a100ac63f54fc1ba5996cb40e94a62c0e52fce:0x627128fe0e6906be7bb4536dbe6fb00defc4ed6a7f9c8c80a070f40931286f5c",
|
|
4
|
+
"devnet": "verifiable:iota:0x0000000000000000000000000000000000000000000000000000000000000000:0x0000000000000000000000000000000000000000000000000000000000000000"
|
|
5
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright 2024 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { entity, property } from "@twin.org/entity";
|
|
4
|
+
/**
|
|
5
|
+
* Class representing an entry for the sync snapshot.
|
|
6
|
+
*/
|
|
7
|
+
let SyncSnapshotEntry = class SyncSnapshotEntry {
|
|
8
|
+
/**
|
|
9
|
+
* The id for the snapshot.
|
|
10
|
+
*/
|
|
11
|
+
id;
|
|
12
|
+
/**
|
|
13
|
+
* The version for the snapshot.
|
|
14
|
+
*/
|
|
15
|
+
version;
|
|
16
|
+
/**
|
|
17
|
+
* The storage key for the snapshot i.e. which entity is being synchronized.
|
|
18
|
+
*/
|
|
19
|
+
storageKey;
|
|
20
|
+
/**
|
|
21
|
+
* The date the snapshot was created.
|
|
22
|
+
*/
|
|
23
|
+
dateCreated;
|
|
24
|
+
/**
|
|
25
|
+
* The date the snapshot was last modified.
|
|
26
|
+
*/
|
|
27
|
+
dateModified;
|
|
28
|
+
/**
|
|
29
|
+
* The flag to determine if this is the snapshot is the local one containing changes for this node.
|
|
30
|
+
*/
|
|
31
|
+
isLocal;
|
|
32
|
+
/**
|
|
33
|
+
* The flag to determine if this is a consolidated snapshot.
|
|
34
|
+
*/
|
|
35
|
+
isConsolidated;
|
|
36
|
+
/**
|
|
37
|
+
* The epoch for the changeset.
|
|
38
|
+
*/
|
|
39
|
+
epoch;
|
|
40
|
+
/**
|
|
41
|
+
* The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
|
|
42
|
+
*/
|
|
43
|
+
changeSetStorageIds;
|
|
44
|
+
/**
|
|
45
|
+
* The changes that were made in this snapshot, if this is a local snapshot.
|
|
46
|
+
*/
|
|
47
|
+
changes;
|
|
48
|
+
};
|
|
49
|
+
__decorate([
|
|
50
|
+
property({ type: "string", isPrimary: true }),
|
|
51
|
+
__metadata("design:type", String)
|
|
52
|
+
], SyncSnapshotEntry.prototype, "id", void 0);
|
|
53
|
+
__decorate([
|
|
54
|
+
property({ type: "string" }),
|
|
55
|
+
__metadata("design:type", String)
|
|
56
|
+
], SyncSnapshotEntry.prototype, "version", void 0);
|
|
57
|
+
__decorate([
|
|
58
|
+
property({ type: "string", isSecondary: true }),
|
|
59
|
+
__metadata("design:type", String)
|
|
60
|
+
], SyncSnapshotEntry.prototype, "storageKey", void 0);
|
|
61
|
+
__decorate([
|
|
62
|
+
property({ type: "string" }),
|
|
63
|
+
__metadata("design:type", String)
|
|
64
|
+
], SyncSnapshotEntry.prototype, "dateCreated", void 0);
|
|
65
|
+
__decorate([
|
|
66
|
+
property({ type: "string" }),
|
|
67
|
+
__metadata("design:type", String)
|
|
68
|
+
], SyncSnapshotEntry.prototype, "dateModified", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
property({ type: "boolean" }),
|
|
71
|
+
__metadata("design:type", Boolean)
|
|
72
|
+
], SyncSnapshotEntry.prototype, "isLocal", void 0);
|
|
73
|
+
__decorate([
|
|
74
|
+
property({ type: "boolean" }),
|
|
75
|
+
__metadata("design:type", Boolean)
|
|
76
|
+
], SyncSnapshotEntry.prototype, "isConsolidated", void 0);
|
|
77
|
+
__decorate([
|
|
78
|
+
property({ type: "number" }),
|
|
79
|
+
__metadata("design:type", Number)
|
|
80
|
+
], SyncSnapshotEntry.prototype, "epoch", void 0);
|
|
81
|
+
__decorate([
|
|
82
|
+
property({ type: "array", itemType: "string", optional: true }),
|
|
83
|
+
__metadata("design:type", Array)
|
|
84
|
+
], SyncSnapshotEntry.prototype, "changeSetStorageIds", void 0);
|
|
85
|
+
__decorate([
|
|
86
|
+
property({ type: "array", itemType: "object", optional: true }),
|
|
87
|
+
__metadata("design:type", Array)
|
|
88
|
+
], SyncSnapshotEntry.prototype, "changes", void 0);
|
|
89
|
+
SyncSnapshotEntry = __decorate([
|
|
90
|
+
entity()
|
|
91
|
+
], SyncSnapshotEntry);
|
|
92
|
+
export { SyncSnapshotEntry };
|
|
93
|
+
//# sourceMappingURL=syncSnapshotEntry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncSnapshotEntry.js","sourceRoot":"","sources":["../../../src/entities/syncSnapshotEntry.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGpD;;GAEG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC7B;;OAEG;IAEI,EAAE,CAAU;IAEnB;;OAEG;IAEI,OAAO,CAAU;IAExB;;OAEG;IAEI,UAAU,CAAU;IAE3B;;OAEG;IAEI,WAAW,CAAU;IAE5B;;OAEG;IAEI,YAAY,CAAU;IAE7B;;OAEG;IAEI,OAAO,CAAW;IAEzB;;OAEG;IAEI,cAAc,CAAW;IAEhC;;OAEG;IAEI,KAAK,CAAU;IAEtB;;OAEG;IAEI,mBAAmB,CAAY;IAEtC;;OAEG;IAEI,OAAO,CAAiB;CAC/B,CAAA;AAvDO;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;;6CAC3B;AAMZ;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;;kDACL;AAMjB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;;qDACrB;AAMpB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;;sDACD;AAMrB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;;uDACA;AAMtB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;kDACL;AAMlB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;yDACE;AAMzB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;;gDACP;AAMf;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;8DAC1B;AAM/B;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;kDACjC;AA3DnB,iBAAiB;IAD7B,MAAM,EAAE;GACI,iBAAiB,CA4D7B","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { entity, property } from \"@twin.org/entity\";\nimport type { ISyncChange } from \"@twin.org/synchronised-storage-models\";\n\n/**\n * Class representing an entry for the sync snapshot.\n */\n@entity()\nexport class SyncSnapshotEntry {\n\t/**\n\t * The id for the snapshot.\n\t */\n\t@property({ type: \"string\", isPrimary: true })\n\tpublic id!: string;\n\n\t/**\n\t * The version for the snapshot.\n\t */\n\t@property({ type: \"string\" })\n\tpublic version!: string;\n\n\t/**\n\t * The storage key for the snapshot i.e. which entity is being synchronized.\n\t */\n\t@property({ type: \"string\", isSecondary: true })\n\tpublic storageKey!: string;\n\n\t/**\n\t * The date the snapshot was created.\n\t */\n\t@property({ type: \"string\" })\n\tpublic dateCreated!: string;\n\n\t/**\n\t * The date the snapshot was last modified.\n\t */\n\t@property({ type: \"string\" })\n\tpublic dateModified!: string;\n\n\t/**\n\t * The flag to determine if this is the snapshot is the local one containing changes for this node.\n\t */\n\t@property({ type: \"boolean\" })\n\tpublic isLocal!: boolean;\n\n\t/**\n\t * The flag to determine if this is a consolidated snapshot.\n\t */\n\t@property({ type: \"boolean\" })\n\tpublic isConsolidated!: boolean;\n\n\t/**\n\t * The epoch for the changeset.\n\t */\n\t@property({ type: \"number\" })\n\tpublic epoch!: number;\n\n\t/**\n\t * The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.\n\t */\n\t@property({ type: \"array\", itemType: \"string\", optional: true })\n\tpublic changeSetStorageIds?: string[];\n\n\t/**\n\t * The changes that were made in this snapshot, if this is a local snapshot.\n\t */\n\t@property({ type: \"array\", itemType: \"object\", optional: true })\n\tpublic changes?: ISyncChange[];\n}\n"]}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { BaseError, Compression, CompressionType, GeneralError, Is, ObjectHelper } from "@twin.org/core";
|
|
2
|
+
import { VaultEncryptionType } from "@twin.org/vault-models";
|
|
3
|
+
/**
|
|
4
|
+
* Class for performing blob storage operations.
|
|
5
|
+
*/
|
|
6
|
+
export class BlobStorageHelper {
|
|
7
|
+
/**
|
|
8
|
+
* Runtime name for the class.
|
|
9
|
+
*/
|
|
10
|
+
static CLASS_NAME = "BlobStorageHelper";
|
|
11
|
+
/**
|
|
12
|
+
* The logging component to use for logging.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
_logging;
|
|
16
|
+
/**
|
|
17
|
+
* The vault connector.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
_vaultConnector;
|
|
21
|
+
/**
|
|
22
|
+
* The blob storage connector to use.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
_blobStorageConnector;
|
|
26
|
+
/**
|
|
27
|
+
* The id of the vault key to use for encrypting/decrypting blobs.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
_blobStorageEncryptionKeyId;
|
|
31
|
+
/**
|
|
32
|
+
* Is this a trusted node.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
_isTrustedNode;
|
|
36
|
+
/**
|
|
37
|
+
* Create a new instance of BlobStorageHelper.
|
|
38
|
+
* @param logging The logging component to use for logging.
|
|
39
|
+
* @param vaultConnector The vault connector to use for for the encryption key.
|
|
40
|
+
* @param blobStorageConnector The blob storage component to use.
|
|
41
|
+
* @param blobStorageEncryptionKeyId The id of the vault key to use for encrypting/decrypting blobs.
|
|
42
|
+
* @param isTrustedNode Is this a trusted node.
|
|
43
|
+
*/
|
|
44
|
+
constructor(logging, vaultConnector, blobStorageConnector, blobStorageEncryptionKeyId, isTrustedNode) {
|
|
45
|
+
this._logging = logging;
|
|
46
|
+
this._vaultConnector = vaultConnector;
|
|
47
|
+
this._blobStorageConnector = blobStorageConnector;
|
|
48
|
+
this._blobStorageEncryptionKeyId = blobStorageEncryptionKeyId;
|
|
49
|
+
this._isTrustedNode = isTrustedNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Load a blob from storage.
|
|
53
|
+
* @param blobId The id of the blob to apply.
|
|
54
|
+
* @returns The blob.
|
|
55
|
+
*/
|
|
56
|
+
async loadBlob(blobId) {
|
|
57
|
+
await this._logging?.log({
|
|
58
|
+
level: "info",
|
|
59
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
60
|
+
message: "loadBlob",
|
|
61
|
+
data: {
|
|
62
|
+
blobId
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
const encryptedBlob = await this._blobStorageConnector.get(blobId);
|
|
67
|
+
if (Is.uint8Array(encryptedBlob)) {
|
|
68
|
+
const compressedBlob = await this._vaultConnector.decrypt(this._blobStorageEncryptionKeyId, VaultEncryptionType.ChaCha20Poly1305, encryptedBlob);
|
|
69
|
+
const decompressedBlob = await Compression.decompress(compressedBlob, CompressionType.Gzip);
|
|
70
|
+
await this._logging?.log({
|
|
71
|
+
level: "info",
|
|
72
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
73
|
+
message: "loadedBlob",
|
|
74
|
+
data: {
|
|
75
|
+
blobId
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return ObjectHelper.fromBytes(decompressedBlob);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await this._logging?.log({
|
|
83
|
+
level: "error",
|
|
84
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
85
|
+
message: "loadBlobFailed",
|
|
86
|
+
data: {
|
|
87
|
+
blobId
|
|
88
|
+
},
|
|
89
|
+
error: BaseError.fromError(error)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
await this._logging?.log({
|
|
93
|
+
level: "info",
|
|
94
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
95
|
+
message: "loadBlobEmpty",
|
|
96
|
+
data: {
|
|
97
|
+
blobId
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Save a blob.
|
|
103
|
+
* @param blob The blob to save.
|
|
104
|
+
* @returns The id of the blob.
|
|
105
|
+
*/
|
|
106
|
+
async saveBlob(blob) {
|
|
107
|
+
await this._logging?.log({
|
|
108
|
+
level: "info",
|
|
109
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
110
|
+
message: "saveBlob"
|
|
111
|
+
});
|
|
112
|
+
if (!this._isTrustedNode) {
|
|
113
|
+
throw new GeneralError(BlobStorageHelper.CLASS_NAME, "notTrustedNode");
|
|
114
|
+
}
|
|
115
|
+
const compressedBlob = await Compression.compress(ObjectHelper.toBytes(blob), CompressionType.Gzip);
|
|
116
|
+
const encryptedBlob = await this._vaultConnector.encrypt(this._blobStorageEncryptionKeyId, VaultEncryptionType.ChaCha20Poly1305, compressedBlob);
|
|
117
|
+
try {
|
|
118
|
+
const blobId = await this._blobStorageConnector.set(encryptedBlob);
|
|
119
|
+
await this._logging?.log({
|
|
120
|
+
level: "info",
|
|
121
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
122
|
+
message: "savedBlob",
|
|
123
|
+
data: {
|
|
124
|
+
blobId
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return blobId;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
await this._logging?.log({
|
|
131
|
+
level: "error",
|
|
132
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
133
|
+
message: "saveBlobFailed",
|
|
134
|
+
error: BaseError.fromError(error)
|
|
135
|
+
});
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Remove a blob from storage.
|
|
141
|
+
* @param blobId The id of the blob to remove.
|
|
142
|
+
* @returns Nothing.
|
|
143
|
+
*/
|
|
144
|
+
async removeBlob(blobId) {
|
|
145
|
+
await this._logging?.log({
|
|
146
|
+
level: "info",
|
|
147
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
148
|
+
message: "removeBlob",
|
|
149
|
+
data: {
|
|
150
|
+
blobId
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
await this._blobStorageConnector.remove(blobId);
|
|
155
|
+
await this._logging?.log({
|
|
156
|
+
level: "info",
|
|
157
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
158
|
+
message: "removedBlob",
|
|
159
|
+
data: {
|
|
160
|
+
blobId
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
await this._logging?.log({
|
|
166
|
+
level: "error",
|
|
167
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
168
|
+
message: "removeBlobFailed",
|
|
169
|
+
data: {
|
|
170
|
+
blobId
|
|
171
|
+
},
|
|
172
|
+
error: BaseError.fromError(error)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
await this._logging?.log({
|
|
176
|
+
level: "info",
|
|
177
|
+
source: BlobStorageHelper.CLASS_NAME,
|
|
178
|
+
message: "removeBlobEmpty",
|
|
179
|
+
data: {
|
|
180
|
+
blobId
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=blobStorageHelper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blobStorageHelper.js","sourceRoot":"","sources":["../../../src/helpers/blobStorageHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IBlobStorageConnector } from \"@twin.org/blob-storage-models\";\nimport {\n\tBaseError,\n\tCompression,\n\tCompressionType,\n\tGeneralError,\n\tIs,\n\tObjectHelper\n} from \"@twin.org/core\";\nimport type { ILoggingComponent } from \"@twin.org/logging-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { type IVaultConnector, VaultEncryptionType } from \"@twin.org/vault-models\";\n\n/**\n * Class for performing blob storage operations.\n */\nexport class BlobStorageHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<BlobStorageHelper>();\n\n\t/**\n\t * The logging component to use for logging.\n\t * @internal\n\t */\n\tprivate readonly _logging?: ILoggingComponent;\n\n\t/**\n\t * The vault connector.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector: IVaultConnector;\n\n\t/**\n\t * The blob storage connector to use.\n\t * @internal\n\t */\n\tprivate readonly _blobStorageConnector: IBlobStorageConnector;\n\n\t/**\n\t * The id of the vault key to use for encrypting/decrypting blobs.\n\t * @internal\n\t */\n\tprivate readonly _blobStorageEncryptionKeyId: string;\n\n\t/**\n\t * Is this a trusted node.\n\t * @internal\n\t */\n\tprivate readonly _isTrustedNode: boolean;\n\n\t/**\n\t * Create a new instance of BlobStorageHelper.\n\t * @param logging The logging component to use for logging.\n\t * @param vaultConnector The vault connector to use for for the encryption key.\n\t * @param blobStorageConnector The blob storage component to use.\n\t * @param blobStorageEncryptionKeyId The id of the vault key to use for encrypting/decrypting blobs.\n\t * @param isTrustedNode Is this a trusted node.\n\t */\n\tconstructor(\n\t\tlogging: ILoggingComponent | undefined,\n\t\tvaultConnector: IVaultConnector,\n\t\tblobStorageConnector: IBlobStorageConnector,\n\t\tblobStorageEncryptionKeyId: string,\n\t\tisTrustedNode: boolean\n\t) {\n\t\tthis._logging = logging;\n\t\tthis._vaultConnector = vaultConnector;\n\t\tthis._blobStorageConnector = blobStorageConnector;\n\t\tthis._blobStorageEncryptionKeyId = blobStorageEncryptionKeyId;\n\t\tthis._isTrustedNode = isTrustedNode;\n\t}\n\n\t/**\n\t * Load a blob from storage.\n\t * @param blobId The id of the blob to apply.\n\t * @returns The blob.\n\t */\n\tpublic async loadBlob<T>(blobId: string): Promise<T | undefined> {\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\tmessage: \"loadBlob\",\n\t\t\tdata: {\n\t\t\t\tblobId\n\t\t\t}\n\t\t});\n\n\t\ttry {\n\t\t\tconst encryptedBlob = await this._blobStorageConnector.get(blobId);\n\n\t\t\tif (Is.uint8Array(encryptedBlob)) {\n\t\t\t\tconst compressedBlob = await this._vaultConnector.decrypt(\n\t\t\t\t\tthis._blobStorageEncryptionKeyId,\n\t\t\t\t\tVaultEncryptionType.ChaCha20Poly1305,\n\t\t\t\t\tencryptedBlob\n\t\t\t\t);\n\n\t\t\t\tconst decompressedBlob = await Compression.decompress(compressedBlob, CompressionType.Gzip);\n\t\t\t\tawait this._logging?.log({\n\t\t\t\t\tlevel: \"info\",\n\t\t\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\t\t\tmessage: \"loadedBlob\",\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tblobId\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\treturn ObjectHelper.fromBytes<T>(decompressedBlob);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tawait this._logging?.log({\n\t\t\t\tlevel: \"error\",\n\t\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\t\tmessage: \"loadBlobFailed\",\n\t\t\t\tdata: {\n\t\t\t\t\tblobId\n\t\t\t\t},\n\t\t\t\terror: BaseError.fromError(error)\n\t\t\t});\n\t\t}\n\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\tmessage: \"loadBlobEmpty\",\n\t\t\tdata: {\n\t\t\t\tblobId\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Save a blob.\n\t * @param blob The blob to save.\n\t * @returns The id of the blob.\n\t */\n\tpublic async saveBlob<T>(blob: T): Promise<string> {\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\tmessage: \"saveBlob\"\n\t\t});\n\n\t\tif (!this._isTrustedNode) {\n\t\t\tthrow new GeneralError(BlobStorageHelper.CLASS_NAME, \"notTrustedNode\");\n\t\t}\n\n\t\tconst compressedBlob = await Compression.compress(\n\t\t\tObjectHelper.toBytes(blob),\n\t\t\tCompressionType.Gzip\n\t\t);\n\n\t\tconst encryptedBlob = await this._vaultConnector.encrypt(\n\t\t\tthis._blobStorageEncryptionKeyId,\n\t\t\tVaultEncryptionType.ChaCha20Poly1305,\n\t\t\tcompressedBlob\n\t\t);\n\n\t\ttry {\n\t\t\tconst blobId = await this._blobStorageConnector.set(encryptedBlob);\n\n\t\t\tawait this._logging?.log({\n\t\t\t\tlevel: \"info\",\n\t\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\t\tmessage: \"savedBlob\",\n\t\t\t\tdata: {\n\t\t\t\t\tblobId\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn blobId;\n\t\t} catch (error) {\n\t\t\tawait this._logging?.log({\n\t\t\t\tlevel: \"error\",\n\t\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\t\tmessage: \"saveBlobFailed\",\n\t\t\t\terror: BaseError.fromError(error)\n\t\t\t});\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * Remove a blob from storage.\n\t * @param blobId The id of the blob to remove.\n\t * @returns Nothing.\n\t */\n\tpublic async removeBlob(blobId: string): Promise<void> {\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\tmessage: \"removeBlob\",\n\t\t\tdata: {\n\t\t\t\tblobId\n\t\t\t}\n\t\t});\n\n\t\ttry {\n\t\t\tawait this._blobStorageConnector.remove(blobId);\n\n\t\t\tawait this._logging?.log({\n\t\t\t\tlevel: \"info\",\n\t\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\t\tmessage: \"removedBlob\",\n\t\t\t\tdata: {\n\t\t\t\t\tblobId\n\t\t\t\t}\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tawait this._logging?.log({\n\t\t\t\tlevel: \"error\",\n\t\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\t\tmessage: \"removeBlobFailed\",\n\t\t\t\tdata: {\n\t\t\t\t\tblobId\n\t\t\t\t},\n\t\t\t\terror: BaseError.fromError(error)\n\t\t\t});\n\t\t}\n\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: BlobStorageHelper.CLASS_NAME,\n\t\t\tmessage: \"removeBlobEmpty\",\n\t\t\tdata: {\n\t\t\t\tblobId\n\t\t\t}\n\t\t});\n\t}\n}\n"]}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Copyright 2024 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { BaseError, Converter, Is, ObjectHelper, RandomHelper } from "@twin.org/core";
|
|
4
|
+
import { SyncChangeOperation, SynchronisedStorageTopics } from "@twin.org/synchronised-storage-models";
|
|
5
|
+
/**
|
|
6
|
+
* Class for performing change set operations.
|
|
7
|
+
*/
|
|
8
|
+
export class ChangeSetHelper {
|
|
9
|
+
/**
|
|
10
|
+
* Runtime name for the class.
|
|
11
|
+
*/
|
|
12
|
+
static CLASS_NAME = "ChangeSetHelper";
|
|
13
|
+
/**
|
|
14
|
+
* The logging component to use for logging.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
_logging;
|
|
18
|
+
/**
|
|
19
|
+
* The event bus component.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
_eventBusComponent;
|
|
23
|
+
/**
|
|
24
|
+
* The blob storage helper to use for remote sync states.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
_blobStorageHelper;
|
|
28
|
+
/**
|
|
29
|
+
* The identity of the node that is performing the update.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
_nodeId;
|
|
33
|
+
/**
|
|
34
|
+
* Create a new instance of ChangeSetHelper.
|
|
35
|
+
* @param logging The logging component to use for logging.
|
|
36
|
+
* @param eventBusComponent The event bus component to use for events.
|
|
37
|
+
* @param blobStorageHelper The blob storage component to use for remote sync states.
|
|
38
|
+
*/
|
|
39
|
+
constructor(logging, eventBusComponent, blobStorageHelper) {
|
|
40
|
+
this._logging = logging;
|
|
41
|
+
this._eventBusComponent = eventBusComponent;
|
|
42
|
+
this._blobStorageHelper = blobStorageHelper;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Set the node identity to use for signing changesets.
|
|
46
|
+
* @param nodeId The identity of the node that is performing the update.
|
|
47
|
+
*/
|
|
48
|
+
setNodeId(nodeId) {
|
|
49
|
+
this._nodeId = nodeId;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get a changeset.
|
|
53
|
+
* @param changeSetStorageId The id of the sync changeset to apply.
|
|
54
|
+
* @returns The changeset if it was verified.
|
|
55
|
+
*/
|
|
56
|
+
async getChangeset(changeSetStorageId) {
|
|
57
|
+
await this._logging?.log({
|
|
58
|
+
level: "info",
|
|
59
|
+
source: ChangeSetHelper.CLASS_NAME,
|
|
60
|
+
message: "getChangeSet",
|
|
61
|
+
data: {
|
|
62
|
+
changeSetStorageId
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
const syncChangeSet = await this._blobStorageHelper.loadBlob(changeSetStorageId);
|
|
67
|
+
return syncChangeSet;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
await this._logging?.log({
|
|
71
|
+
level: "warn",
|
|
72
|
+
source: ChangeSetHelper.CLASS_NAME,
|
|
73
|
+
message: "getChangeSetError",
|
|
74
|
+
data: {
|
|
75
|
+
changeSetStorageId
|
|
76
|
+
},
|
|
77
|
+
error: BaseError.fromError(error)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
await this._logging?.log({
|
|
81
|
+
level: "info",
|
|
82
|
+
source: ChangeSetHelper.CLASS_NAME,
|
|
83
|
+
message: "getChangeSetEmpty",
|
|
84
|
+
data: {
|
|
85
|
+
changeSetStorageId
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Apply a sync changeset.
|
|
91
|
+
* @param changeSetStorageId The id of the sync changeset to apply.
|
|
92
|
+
* @returns The changeset if it existed.
|
|
93
|
+
*/
|
|
94
|
+
async getAndApplyChangeset(changeSetStorageId) {
|
|
95
|
+
const syncChangeset = await this.getChangeset(changeSetStorageId);
|
|
96
|
+
// Only apply changesets from other nodes, we don't want to overwrite
|
|
97
|
+
// any changes we have made to local entity storage
|
|
98
|
+
if (!Is.empty(syncChangeset) && syncChangeset.nodeId !== this._nodeId) {
|
|
99
|
+
await this.applyChangeset(syncChangeset);
|
|
100
|
+
}
|
|
101
|
+
return syncChangeset;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Apply a sync changeset.
|
|
105
|
+
* @param syncChangeset The sync changeset to apply.
|
|
106
|
+
* @returns Nothing.
|
|
107
|
+
*/
|
|
108
|
+
async applyChangeset(syncChangeset) {
|
|
109
|
+
if (Is.arrayValue(syncChangeset.changes)) {
|
|
110
|
+
for (const change of syncChangeset.changes) {
|
|
111
|
+
await this._logging?.log({
|
|
112
|
+
level: "info",
|
|
113
|
+
source: ChangeSetHelper.CLASS_NAME,
|
|
114
|
+
message: "changeSetApplyingChange",
|
|
115
|
+
data: {
|
|
116
|
+
operation: change.operation,
|
|
117
|
+
id: change.id
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
switch (change.operation) {
|
|
121
|
+
case SyncChangeOperation.Set:
|
|
122
|
+
if (!Is.empty(change.entity)) {
|
|
123
|
+
// The id was stripped from the entity as it is part of the operation
|
|
124
|
+
// we make sure we reinstate it in the publish
|
|
125
|
+
// Also the node identity was stripped when stored in the changeset
|
|
126
|
+
// as the changeset is signed with the node identity.
|
|
127
|
+
// so we need to restore it here.
|
|
128
|
+
await this._eventBusComponent.publish(SynchronisedStorageTopics.RemoteItemSet, {
|
|
129
|
+
storageKey: syncChangeset.storageKey,
|
|
130
|
+
entity: {
|
|
131
|
+
...change.entity,
|
|
132
|
+
id: change.id,
|
|
133
|
+
nodeId: syncChangeset.nodeId
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case SyncChangeOperation.Delete:
|
|
139
|
+
if (!Is.empty(change.id)) {
|
|
140
|
+
await this._eventBusComponent.publish(SynchronisedStorageTopics.RemoteItemRemove, {
|
|
141
|
+
storageKey: syncChangeset.storageKey,
|
|
142
|
+
id: change.id,
|
|
143
|
+
nodeId: syncChangeset.nodeId
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Store the changeset.
|
|
153
|
+
* @param syncChangeSet The sync change set to store.
|
|
154
|
+
* @returns The id of the change set.
|
|
155
|
+
*/
|
|
156
|
+
async storeChangeSet(syncChangeSet) {
|
|
157
|
+
await this._logging?.log({
|
|
158
|
+
level: "info",
|
|
159
|
+
source: ChangeSetHelper.CLASS_NAME,
|
|
160
|
+
message: "changeSetStoring",
|
|
161
|
+
data: {
|
|
162
|
+
id: syncChangeSet.id
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return this._blobStorageHelper.saveBlob(syncChangeSet);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Copy a change set.
|
|
169
|
+
* @param syncChangeSet The sync changeset to copy.
|
|
170
|
+
* @returns The id of the updated change set.
|
|
171
|
+
*/
|
|
172
|
+
async copyChangeset(syncChangeSet) {
|
|
173
|
+
if (Is.stringValue(this._nodeId)) {
|
|
174
|
+
await this._logging?.log({
|
|
175
|
+
level: "info",
|
|
176
|
+
source: ChangeSetHelper.CLASS_NAME,
|
|
177
|
+
message: "copyChangeSet",
|
|
178
|
+
data: {
|
|
179
|
+
changeSetStorageId: syncChangeSet.id
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// Allocate a new id to the changeset copy
|
|
183
|
+
const copy = ObjectHelper.clone(syncChangeSet);
|
|
184
|
+
copy.id = Converter.bytesToHex(RandomHelper.generate(32));
|
|
185
|
+
// Store the copy
|
|
186
|
+
return {
|
|
187
|
+
syncChangeSet: copy,
|
|
188
|
+
changeSetStorageId: await this.storeChangeSet(copy)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reset the storage for a given storage key.
|
|
194
|
+
* @param storageKey The key of the storage to reset.
|
|
195
|
+
* @param resetMode The reset mode, this will use the nodeId in the entities to determine which are local/remote.
|
|
196
|
+
* @returns Nothing.
|
|
197
|
+
*/
|
|
198
|
+
async reset(storageKey, resetMode) {
|
|
199
|
+
// If we are applying a consolidation we need to reset the local db
|
|
200
|
+
// but keep any entries from the local node, as they might have been updated
|
|
201
|
+
await this._logging?.log({
|
|
202
|
+
level: "info",
|
|
203
|
+
source: ChangeSetHelper.CLASS_NAME,
|
|
204
|
+
message: "storageReset",
|
|
205
|
+
data: {
|
|
206
|
+
storageKey
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
await this._eventBusComponent.publish(SynchronisedStorageTopics.Reset, {
|
|
210
|
+
storageKey,
|
|
211
|
+
resetMode
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=changeSetHelper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changeSetHelper.js","sourceRoot":"","sources":["../../../src/helpers/changeSetHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { BaseError, Converter, Is, ObjectHelper, RandomHelper } from \"@twin.org/core\";\nimport type { IEventBusComponent } from \"@twin.org/event-bus-models\";\nimport type { ILoggingComponent } from \"@twin.org/logging-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport {\n\ttype ISyncChangeSet,\n\ttype ISyncItemRemove,\n\ttype ISyncItemSet,\n\ttype ISyncReset,\n\tSyncChangeOperation,\n\tSynchronisedStorageTopics,\n\ttype SyncNodeIdMode\n} from \"@twin.org/synchronised-storage-models\";\nimport type { BlobStorageHelper } from \"./blobStorageHelper.js\";\n\n/**\n * Class for performing change set operations.\n */\nexport class ChangeSetHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<ChangeSetHelper>();\n\n\t/**\n\t * The logging component to use for logging.\n\t * @internal\n\t */\n\tprivate readonly _logging?: ILoggingComponent;\n\n\t/**\n\t * The event bus component.\n\t * @internal\n\t */\n\tprivate readonly _eventBusComponent: IEventBusComponent;\n\n\t/**\n\t * The blob storage helper to use for remote sync states.\n\t * @internal\n\t */\n\tprivate readonly _blobStorageHelper: BlobStorageHelper;\n\n\t/**\n\t * The identity of the node that is performing the update.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * Create a new instance of ChangeSetHelper.\n\t * @param logging The logging component to use for logging.\n\t * @param eventBusComponent The event bus component to use for events.\n\t * @param blobStorageHelper The blob storage component to use for remote sync states.\n\t */\n\tconstructor(\n\t\tlogging: ILoggingComponent | undefined,\n\t\teventBusComponent: IEventBusComponent,\n\t\tblobStorageHelper: BlobStorageHelper\n\t) {\n\t\tthis._logging = logging;\n\t\tthis._eventBusComponent = eventBusComponent;\n\t\tthis._blobStorageHelper = blobStorageHelper;\n\t}\n\n\t/**\n\t * Set the node identity to use for signing changesets.\n\t * @param nodeId The identity of the node that is performing the update.\n\t */\n\tpublic setNodeId(nodeId: string): void {\n\t\tthis._nodeId = nodeId;\n\t}\n\n\t/**\n\t * Get a changeset.\n\t * @param changeSetStorageId The id of the sync changeset to apply.\n\t * @returns The changeset if it was verified.\n\t */\n\tpublic async getChangeset(changeSetStorageId: string): Promise<ISyncChangeSet | undefined> {\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: ChangeSetHelper.CLASS_NAME,\n\t\t\tmessage: \"getChangeSet\",\n\t\t\tdata: {\n\t\t\t\tchangeSetStorageId\n\t\t\t}\n\t\t});\n\n\t\ttry {\n\t\t\tconst syncChangeSet =\n\t\t\t\tawait this._blobStorageHelper.loadBlob<ISyncChangeSet>(changeSetStorageId);\n\n\t\t\treturn syncChangeSet;\n\t\t} catch (error) {\n\t\t\tawait this._logging?.log({\n\t\t\t\tlevel: \"warn\",\n\t\t\t\tsource: ChangeSetHelper.CLASS_NAME,\n\t\t\t\tmessage: \"getChangeSetError\",\n\t\t\t\tdata: {\n\t\t\t\t\tchangeSetStorageId\n\t\t\t\t},\n\t\t\t\terror: BaseError.fromError(error)\n\t\t\t});\n\t\t}\n\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: ChangeSetHelper.CLASS_NAME,\n\t\t\tmessage: \"getChangeSetEmpty\",\n\t\t\tdata: {\n\t\t\t\tchangeSetStorageId\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Apply a sync changeset.\n\t * @param changeSetStorageId The id of the sync changeset to apply.\n\t * @returns The changeset if it existed.\n\t */\n\tpublic async getAndApplyChangeset(\n\t\tchangeSetStorageId: string\n\t): Promise<ISyncChangeSet | undefined> {\n\t\tconst syncChangeset = await this.getChangeset(changeSetStorageId);\n\n\t\t// Only apply changesets from other nodes, we don't want to overwrite\n\t\t// any changes we have made to local entity storage\n\t\tif (!Is.empty(syncChangeset) && syncChangeset.nodeId !== this._nodeId) {\n\t\t\tawait this.applyChangeset(syncChangeset);\n\t\t}\n\n\t\treturn syncChangeset;\n\t}\n\n\t/**\n\t * Apply a sync changeset.\n\t * @param syncChangeset The sync changeset to apply.\n\t * @returns Nothing.\n\t */\n\tpublic async applyChangeset(syncChangeset: ISyncChangeSet): Promise<void> {\n\t\tif (Is.arrayValue(syncChangeset.changes)) {\n\t\t\tfor (const change of syncChangeset.changes) {\n\t\t\t\tawait this._logging?.log({\n\t\t\t\t\tlevel: \"info\",\n\t\t\t\t\tsource: ChangeSetHelper.CLASS_NAME,\n\t\t\t\t\tmessage: \"changeSetApplyingChange\",\n\t\t\t\t\tdata: {\n\t\t\t\t\t\toperation: change.operation,\n\t\t\t\t\t\tid: change.id\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t\tswitch (change.operation) {\n\t\t\t\t\tcase SyncChangeOperation.Set:\n\t\t\t\t\t\tif (!Is.empty(change.entity)) {\n\t\t\t\t\t\t\t// The id was stripped from the entity as it is part of the operation\n\t\t\t\t\t\t\t// we make sure we reinstate it in the publish\n\t\t\t\t\t\t\t// Also the node identity was stripped when stored in the changeset\n\t\t\t\t\t\t\t// as the changeset is signed with the node identity.\n\t\t\t\t\t\t\t// so we need to restore it here.\n\t\t\t\t\t\t\tawait this._eventBusComponent.publish<ISyncItemSet>(\n\t\t\t\t\t\t\t\tSynchronisedStorageTopics.RemoteItemSet,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tstorageKey: syncChangeset.storageKey,\n\t\t\t\t\t\t\t\t\tentity: {\n\t\t\t\t\t\t\t\t\t\t...change.entity,\n\t\t\t\t\t\t\t\t\t\tid: change.id,\n\t\t\t\t\t\t\t\t\t\tnodeId: syncChangeset.nodeId\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase SyncChangeOperation.Delete:\n\t\t\t\t\t\tif (!Is.empty(change.id)) {\n\t\t\t\t\t\t\tawait this._eventBusComponent.publish<ISyncItemRemove>(\n\t\t\t\t\t\t\t\tSynchronisedStorageTopics.RemoteItemRemove,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tstorageKey: syncChangeset.storageKey,\n\t\t\t\t\t\t\t\t\tid: change.id,\n\t\t\t\t\t\t\t\t\tnodeId: syncChangeset.nodeId\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Store the changeset.\n\t * @param syncChangeSet The sync change set to store.\n\t * @returns The id of the change set.\n\t */\n\tpublic async storeChangeSet(syncChangeSet: ISyncChangeSet): Promise<string> {\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: ChangeSetHelper.CLASS_NAME,\n\t\t\tmessage: \"changeSetStoring\",\n\t\t\tdata: {\n\t\t\t\tid: syncChangeSet.id\n\t\t\t}\n\t\t});\n\n\t\treturn this._blobStorageHelper.saveBlob(syncChangeSet);\n\t}\n\n\t/**\n\t * Copy a change set.\n\t * @param syncChangeSet The sync changeset to copy.\n\t * @returns The id of the updated change set.\n\t */\n\tpublic async copyChangeset(syncChangeSet: ISyncChangeSet): Promise<\n\t\t| {\n\t\t\t\tsyncChangeSet: ISyncChangeSet;\n\t\t\t\tchangeSetStorageId: string;\n\t\t }\n\t\t| undefined\n\t> {\n\t\tif (Is.stringValue(this._nodeId)) {\n\t\t\tawait this._logging?.log({\n\t\t\t\tlevel: \"info\",\n\t\t\t\tsource: ChangeSetHelper.CLASS_NAME,\n\t\t\t\tmessage: \"copyChangeSet\",\n\t\t\t\tdata: {\n\t\t\t\t\tchangeSetStorageId: syncChangeSet.id\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Allocate a new id to the changeset copy\n\t\t\tconst copy = ObjectHelper.clone(syncChangeSet);\n\t\t\tcopy.id = Converter.bytesToHex(RandomHelper.generate(32));\n\n\t\t\t// Store the copy\n\t\t\treturn {\n\t\t\t\tsyncChangeSet: copy,\n\t\t\t\tchangeSetStorageId: await this.storeChangeSet(copy)\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Reset the storage for a given storage key.\n\t * @param storageKey The key of the storage to reset.\n\t * @param resetMode The reset mode, this will use the nodeId in the entities to determine which are local/remote.\n\t * @returns Nothing.\n\t */\n\tpublic async reset(storageKey: string, resetMode: SyncNodeIdMode): Promise<void> {\n\t\t// If we are applying a consolidation we need to reset the local db\n\t\t// but keep any entries from the local node, as they might have been updated\n\t\tawait this._logging?.log({\n\t\t\tlevel: \"info\",\n\t\t\tsource: ChangeSetHelper.CLASS_NAME,\n\t\t\tmessage: \"storageReset\",\n\t\t\tdata: {\n\t\t\t\tstorageKey\n\t\t\t}\n\t\t});\n\t\tawait this._eventBusComponent.publish<ISyncReset>(SynchronisedStorageTopics.Reset, {\n\t\t\tstorageKey,\n\t\t\tresetMode\n\t\t});\n\t}\n}\n"]}
|