@twin.org/synchronised-storage-service 0.0.1-next.3 → 0.0.1-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +963 -276
- package/dist/esm/index.mjs +965 -279
- package/dist/types/entities/syncSnapshotEntry.d.ts +12 -5
- package/dist/types/helpers/blobStorageHelper.d.ts +39 -0
- package/dist/types/helpers/changeSetHelper.d.ts +26 -7
- package/dist/types/helpers/localSyncStateHelper.d.ts +11 -25
- package/dist/types/helpers/remoteSyncStateHelper.d.ts +17 -12
- package/dist/types/helpers/versions.d.ts +3 -0
- package/dist/types/index.d.ts +0 -2
- package/dist/types/models/ISyncPointerStore.d.ts +4 -0
- package/dist/types/models/ISyncSnapshot.d.ts +9 -1
- package/dist/types/models/ISyncState.d.ts +8 -0
- package/dist/types/models/ISynchronisedStorageServiceConfig.d.ts +17 -6
- package/dist/types/models/ISynchronisedStorageServiceConstructorOptions.d.ts +6 -2
- package/dist/types/synchronisedStorageRoutes.d.ts +9 -1
- package/dist/types/synchronisedStorageService.d.ts +13 -4
- package/docs/architecture.md +128 -0
- package/docs/changelog.md +29 -0
- package/docs/open-api/spec.json +246 -18
- package/docs/reference/classes/SyncSnapshotEntry.md +22 -6
- package/docs/reference/classes/SynchronisedStorageService.md +38 -5
- package/docs/reference/functions/synchronisedStorageGetDecryptionKeyRequest.md +31 -0
- package/docs/reference/index.md +1 -2
- package/docs/reference/interfaces/ISyncPointerStore.md +8 -0
- package/docs/reference/interfaces/ISyncSnapshot.md +18 -2
- package/docs/reference/interfaces/ISyncState.md +16 -0
- package/docs/reference/interfaces/ISynchronisedStorageServiceConfig.md +44 -10
- package/docs/reference/interfaces/ISynchronisedStorageServiceConstructorOptions.md +11 -3
- package/locales/en.json +60 -25
- package/package.json +3 -2
- package/dist/types/models/ISyncChange.d.ts +0 -18
- package/dist/types/models/ISyncChangeSet.d.ts +0 -36
- package/docs/reference/interfaces/ISyncChange.md +0 -33
- package/docs/reference/interfaces/ISyncChangeSet.md +0 -65
package/dist/cjs/index.cjs
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
var entity = require('@twin.org/entity');
|
|
4
4
|
var core = require('@twin.org/core');
|
|
5
5
|
var web = require('@twin.org/web');
|
|
6
|
+
var blobStorageModels = require('@twin.org/blob-storage-models');
|
|
6
7
|
var entityStorageModels = require('@twin.org/entity-storage-models');
|
|
7
8
|
var identityModels = require('@twin.org/identity-models');
|
|
8
9
|
var loggingModels = require('@twin.org/logging-models');
|
|
10
|
+
var standardsW3cDid = require('@twin.org/standards-w3c-did');
|
|
9
11
|
var synchronisedStorageModels = require('@twin.org/synchronised-storage-models');
|
|
12
|
+
var vaultModels = require('@twin.org/vault-models');
|
|
10
13
|
var verifiableStorageModels = require('@twin.org/verifiable-storage-models');
|
|
11
|
-
var
|
|
12
|
-
var standardsW3cDid = require('@twin.org/standards-w3c-did');
|
|
14
|
+
var crypto = require('@twin.org/crypto');
|
|
13
15
|
|
|
14
16
|
// Copyright 2024 IOTA Stiftung.
|
|
15
17
|
// SPDX-License-Identifier: Apache-2.0.
|
|
@@ -21,6 +23,10 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
|
|
|
21
23
|
* The id for the snapshot.
|
|
22
24
|
*/
|
|
23
25
|
id;
|
|
26
|
+
/**
|
|
27
|
+
* The version for the snapshot.
|
|
28
|
+
*/
|
|
29
|
+
version;
|
|
24
30
|
/**
|
|
25
31
|
* The storage key for the snapshot i.e. which entity is being synchronized.
|
|
26
32
|
*/
|
|
@@ -34,9 +40,13 @@ exports.SyncSnapshotEntry = class SyncSnapshotEntry {
|
|
|
34
40
|
*/
|
|
35
41
|
dateModified;
|
|
36
42
|
/**
|
|
37
|
-
* The flag to determine if this is the
|
|
43
|
+
* The flag to determine if this is the snapshot is the local one containing changes for this node.
|
|
44
|
+
*/
|
|
45
|
+
isLocal;
|
|
46
|
+
/**
|
|
47
|
+
* The flag to determine if this is a consolidated snapshot.
|
|
38
48
|
*/
|
|
39
|
-
|
|
49
|
+
isConsolidated;
|
|
40
50
|
/**
|
|
41
51
|
* The ids of the storage for the change sets in the snapshot, if this is not a local snapshot.
|
|
42
52
|
*/
|
|
@@ -50,6 +60,10 @@ __decorate([
|
|
|
50
60
|
entity.property({ type: "string", isPrimary: true }),
|
|
51
61
|
__metadata("design:type", String)
|
|
52
62
|
], exports.SyncSnapshotEntry.prototype, "id", void 0);
|
|
63
|
+
__decorate([
|
|
64
|
+
entity.property({ type: "string" }),
|
|
65
|
+
__metadata("design:type", String)
|
|
66
|
+
], exports.SyncSnapshotEntry.prototype, "version", void 0);
|
|
53
67
|
__decorate([
|
|
54
68
|
entity.property({ type: "string", isSecondary: true }),
|
|
55
69
|
__metadata("design:type", String)
|
|
@@ -59,13 +73,17 @@ __decorate([
|
|
|
59
73
|
__metadata("design:type", String)
|
|
60
74
|
], exports.SyncSnapshotEntry.prototype, "dateCreated", void 0);
|
|
61
75
|
__decorate([
|
|
62
|
-
entity.property({ type: "string"
|
|
76
|
+
entity.property({ type: "string" }),
|
|
63
77
|
__metadata("design:type", String)
|
|
64
78
|
], exports.SyncSnapshotEntry.prototype, "dateModified", void 0);
|
|
65
79
|
__decorate([
|
|
66
|
-
entity.property({ type: "boolean"
|
|
80
|
+
entity.property({ type: "boolean" }),
|
|
81
|
+
__metadata("design:type", Boolean)
|
|
82
|
+
], exports.SyncSnapshotEntry.prototype, "isLocal", void 0);
|
|
83
|
+
__decorate([
|
|
84
|
+
entity.property({ type: "boolean" }),
|
|
67
85
|
__metadata("design:type", Boolean)
|
|
68
|
-
], exports.SyncSnapshotEntry.prototype, "
|
|
86
|
+
], exports.SyncSnapshotEntry.prototype, "isConsolidated", void 0);
|
|
69
87
|
__decorate([
|
|
70
88
|
entity.property({ type: "array", itemType: "string", optional: true }),
|
|
71
89
|
__metadata("design:type", Array)
|
|
@@ -102,8 +120,8 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
102
120
|
operationId: "synchronisedStorageSyncChangeSetRequest",
|
|
103
121
|
summary: "Request that the node perform a sync request for a changeset.",
|
|
104
122
|
tag: tagsSynchronisedStorage[0].name,
|
|
105
|
-
method: "
|
|
106
|
-
path: `${baseRouteName}
|
|
123
|
+
method: "POST",
|
|
124
|
+
path: `${baseRouteName}/sync-changeset`,
|
|
107
125
|
handler: async (httpRequestContext, request) => synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request),
|
|
108
126
|
requestType: {
|
|
109
127
|
type: "ISyncChangeSetRequest",
|
|
@@ -111,8 +129,30 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
111
129
|
{
|
|
112
130
|
id: "synchronisedStorageSyncChangeSetRequestExample",
|
|
113
131
|
request: {
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
body: {
|
|
133
|
+
id: "0909090909090909090909090909090909090909090909090909090909090909",
|
|
134
|
+
dateCreated: "2025-05-29T01:00:00.000Z",
|
|
135
|
+
dateModified: "2025-05-29T01:00:00.000Z",
|
|
136
|
+
nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
|
137
|
+
changes: [
|
|
138
|
+
{
|
|
139
|
+
entity: {
|
|
140
|
+
dateModified: "2025-01-01T00:00:00.000Z"
|
|
141
|
+
},
|
|
142
|
+
id: "test-id-1",
|
|
143
|
+
operation: "set"
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
proof: {
|
|
147
|
+
"@context": "https://www.w3.org/ns/credentials/v2",
|
|
148
|
+
created: "2025-05-29T01:00:00.000Z",
|
|
149
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
150
|
+
proofPurpose: "assertionMethod",
|
|
151
|
+
proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
|
|
152
|
+
type: "DataIntegrityProof",
|
|
153
|
+
verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
|
|
154
|
+
},
|
|
155
|
+
storageKey: "test-type"
|
|
116
156
|
}
|
|
117
157
|
}
|
|
118
158
|
}
|
|
@@ -122,9 +162,61 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
122
162
|
{
|
|
123
163
|
type: "INoContentResponse"
|
|
124
164
|
}
|
|
125
|
-
]
|
|
165
|
+
],
|
|
166
|
+
// Authentication is provided by the proof in the request body.
|
|
167
|
+
skipAuth: true
|
|
126
168
|
};
|
|
127
|
-
|
|
169
|
+
const getDecryptionKeyRoute = {
|
|
170
|
+
operationId: "synchronisedStorageGetDecryptionKeyRequest",
|
|
171
|
+
summary: "Request the decryption key.",
|
|
172
|
+
tag: tagsSynchronisedStorage[0].name,
|
|
173
|
+
method: "POST",
|
|
174
|
+
path: `${baseRouteName}/decryption-key`,
|
|
175
|
+
handler: async (httpRequestContext, request) => synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request),
|
|
176
|
+
requestType: {
|
|
177
|
+
type: "ISyncChangeSetRequest",
|
|
178
|
+
examples: [
|
|
179
|
+
{
|
|
180
|
+
id: "synchronisedStorageSyncGetDecryptionKeyRequestExample",
|
|
181
|
+
request: {
|
|
182
|
+
body: {
|
|
183
|
+
nodeIdentity: "did:entity-storage:0xd2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
|
184
|
+
proof: {
|
|
185
|
+
"@context": "https://www.w3.org/ns/credentials/v2",
|
|
186
|
+
created: "2025-05-29T01:00:00.000Z",
|
|
187
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
188
|
+
proofPurpose: "assertionMethod",
|
|
189
|
+
proofValue: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3",
|
|
190
|
+
type: "DataIntegrityProof",
|
|
191
|
+
verificationMethod: "did:entity-storage:0xd0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0#synchronised-storage-assertion"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
responseType: [
|
|
199
|
+
{
|
|
200
|
+
type: "ISyncDecryptionKeyResponse",
|
|
201
|
+
examples: [
|
|
202
|
+
{
|
|
203
|
+
id: "synchronisedStorageSyncGetDecryptionKeyResponseExample",
|
|
204
|
+
response: {
|
|
205
|
+
body: {
|
|
206
|
+
decryptionKey: "z5efBErQs3YBLZoH7jgKMQaRc9YjAxA5XSYKmW3FmTBDw9WionT2NS2x1SMvcRyBvw53cSSoaCT1xQH9tkWngGCX3"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: "IUnauthorizedResponse"
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
// Authentication is provided by the proof in the request body.
|
|
217
|
+
skipAuth: true
|
|
218
|
+
};
|
|
219
|
+
return [syncChangeSetRoute, getDecryptionKeyRoute];
|
|
128
220
|
}
|
|
129
221
|
/**
|
|
130
222
|
* Perform the sync change set operation.
|
|
@@ -135,13 +227,31 @@ function generateRestRoutesSynchronisedStorage(baseRouteName, componentName) {
|
|
|
135
227
|
*/
|
|
136
228
|
async function synchronisedStorageSyncChangeSetRequest(httpRequestContext, componentName, request) {
|
|
137
229
|
core.Guards.object(ROUTES_SOURCE, "request", request);
|
|
138
|
-
core.Guards.object(ROUTES_SOURCE, "request.
|
|
230
|
+
core.Guards.object(ROUTES_SOURCE, "request.body", request.body);
|
|
139
231
|
const component = core.ComponentFactory.get(componentName);
|
|
140
|
-
await component.syncChangeSet(request.
|
|
232
|
+
await component.syncChangeSet(request.body);
|
|
141
233
|
return {
|
|
142
234
|
statusCode: web.HttpStatusCode.noContent
|
|
143
235
|
};
|
|
144
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Request the decryption key.
|
|
239
|
+
* @param httpRequestContext The request context for the API.
|
|
240
|
+
* @param componentName The name of the component to use in the routes.
|
|
241
|
+
* @param request The request.
|
|
242
|
+
* @returns The response object with additional http response properties.
|
|
243
|
+
*/
|
|
244
|
+
async function synchronisedStorageGetDecryptionKeyRequest(httpRequestContext, componentName, request) {
|
|
245
|
+
core.Guards.object(ROUTES_SOURCE, "request", request);
|
|
246
|
+
core.Guards.object(ROUTES_SOURCE, "request.body", request.body);
|
|
247
|
+
const component = core.ComponentFactory.get(componentName);
|
|
248
|
+
const key = await component.getDecryptionKey(request.body.nodeIdentity, request.body.proof);
|
|
249
|
+
return {
|
|
250
|
+
body: {
|
|
251
|
+
decryptionKey: key
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
145
255
|
|
|
146
256
|
const restEntryPoints = [
|
|
147
257
|
{
|
|
@@ -161,6 +271,208 @@ function initSchema() {
|
|
|
161
271
|
entity.EntitySchemaFactory.register("SyncSnapshotEntry", () => entity.EntitySchemaHelper.getSchema(exports.SyncSnapshotEntry));
|
|
162
272
|
}
|
|
163
273
|
|
|
274
|
+
var mainnet = "";
|
|
275
|
+
var testnet = "";
|
|
276
|
+
var devnet = "";
|
|
277
|
+
var verifiableStorageKeys = {
|
|
278
|
+
mainnet: mainnet,
|
|
279
|
+
testnet: testnet,
|
|
280
|
+
devnet: devnet
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Class for performing blob storage operations.
|
|
285
|
+
*/
|
|
286
|
+
class BlobStorageHelper {
|
|
287
|
+
/**
|
|
288
|
+
* Runtime name for the class.
|
|
289
|
+
*/
|
|
290
|
+
CLASS_NAME = "BlobStorageHelper";
|
|
291
|
+
/**
|
|
292
|
+
* The logging connector to use for logging.
|
|
293
|
+
* @internal
|
|
294
|
+
*/
|
|
295
|
+
_logging;
|
|
296
|
+
/**
|
|
297
|
+
* The vault connector.
|
|
298
|
+
* @internal
|
|
299
|
+
*/
|
|
300
|
+
_vaultConnector;
|
|
301
|
+
/**
|
|
302
|
+
* The blob storage connector to use.
|
|
303
|
+
* @internal
|
|
304
|
+
*/
|
|
305
|
+
_blobStorageConnector;
|
|
306
|
+
/**
|
|
307
|
+
* The id of the vault key to use for encrypting/decrypting blobs.
|
|
308
|
+
* @internal
|
|
309
|
+
*/
|
|
310
|
+
_blobStorageEncryptionKeyId;
|
|
311
|
+
/**
|
|
312
|
+
* Is this a trusted node.
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
_isTrustedNode;
|
|
316
|
+
/**
|
|
317
|
+
* Create a new instance of BlobStorageHelper.
|
|
318
|
+
* @param logging The logging connector to use for logging.
|
|
319
|
+
* @param vaultConnector The vault connector to use for for the encryption key.
|
|
320
|
+
* @param blobStorageConnector The blob storage component to use.
|
|
321
|
+
* @param blobStorageEncryptionKeyId The id of the vault key to use for encrypting/decrypting blobs.
|
|
322
|
+
* @param isTrustedNode Is this a trusted node.
|
|
323
|
+
*/
|
|
324
|
+
constructor(logging, vaultConnector, blobStorageConnector, blobStorageEncryptionKeyId, isTrustedNode) {
|
|
325
|
+
this._logging = logging;
|
|
326
|
+
this._vaultConnector = vaultConnector;
|
|
327
|
+
this._blobStorageConnector = blobStorageConnector;
|
|
328
|
+
this._blobStorageEncryptionKeyId = blobStorageEncryptionKeyId;
|
|
329
|
+
this._isTrustedNode = isTrustedNode;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Load a blob from storage.
|
|
333
|
+
* @param blobId The id of the blob to apply.
|
|
334
|
+
* @returns The blob.
|
|
335
|
+
*/
|
|
336
|
+
async loadBlob(blobId) {
|
|
337
|
+
await this._logging?.log({
|
|
338
|
+
level: "info",
|
|
339
|
+
source: this.CLASS_NAME,
|
|
340
|
+
message: "loadBlob",
|
|
341
|
+
data: {
|
|
342
|
+
blobId
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
const encryptedBlob = await this._blobStorageConnector.get(blobId);
|
|
347
|
+
if (core.Is.uint8Array(encryptedBlob)) {
|
|
348
|
+
let compressedBlob;
|
|
349
|
+
// If this is a trusted node, we can decrypt the blob using the vault
|
|
350
|
+
if (this._isTrustedNode) {
|
|
351
|
+
compressedBlob = await this._vaultConnector.decrypt(this._blobStorageEncryptionKeyId, vaultModels.VaultEncryptionType.Rsa2048, encryptedBlob);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// Otherwise we need the public key stored as a secret in the vault
|
|
355
|
+
const key = await this._vaultConnector.getSecret(this._blobStorageEncryptionKeyId);
|
|
356
|
+
const rsa = new crypto.RSA(core.Converter.base64ToBytes(key));
|
|
357
|
+
compressedBlob = rsa.decrypt(encryptedBlob);
|
|
358
|
+
}
|
|
359
|
+
const decompressedBlob = await core.Compression.decompress(compressedBlob, core.CompressionType.Gzip);
|
|
360
|
+
await this._logging?.log({
|
|
361
|
+
level: "info",
|
|
362
|
+
source: this.CLASS_NAME,
|
|
363
|
+
message: "loadedBlob",
|
|
364
|
+
data: {
|
|
365
|
+
blobId
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return core.ObjectHelper.fromBytes(decompressedBlob);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
await this._logging?.log({
|
|
373
|
+
level: "error",
|
|
374
|
+
source: this.CLASS_NAME,
|
|
375
|
+
message: "loadBlobFailed",
|
|
376
|
+
data: {
|
|
377
|
+
blobId
|
|
378
|
+
},
|
|
379
|
+
error: core.BaseError.fromError(error)
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
await this._logging?.log({
|
|
383
|
+
level: "info",
|
|
384
|
+
source: this.CLASS_NAME,
|
|
385
|
+
message: "loadBlobEmpty",
|
|
386
|
+
data: {
|
|
387
|
+
blobId
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Save a blob.
|
|
393
|
+
* @param blob The blob to save.
|
|
394
|
+
* @returns The id of the blob.
|
|
395
|
+
*/
|
|
396
|
+
async saveBlob(blob) {
|
|
397
|
+
await this._logging?.log({
|
|
398
|
+
level: "info",
|
|
399
|
+
source: this.CLASS_NAME,
|
|
400
|
+
message: "saveBlob"
|
|
401
|
+
});
|
|
402
|
+
if (!this._isTrustedNode) {
|
|
403
|
+
throw new core.GeneralError(this.CLASS_NAME, "notTrustedNode");
|
|
404
|
+
}
|
|
405
|
+
const compressedBlob = await core.Compression.compress(core.ObjectHelper.toBytes(blob), core.CompressionType.Gzip);
|
|
406
|
+
const encryptedBlob = await this._vaultConnector.encrypt(this._blobStorageEncryptionKeyId, vaultModels.VaultEncryptionType.Rsa2048, compressedBlob);
|
|
407
|
+
try {
|
|
408
|
+
const blobId = await this._blobStorageConnector.set(encryptedBlob);
|
|
409
|
+
await this._logging?.log({
|
|
410
|
+
level: "info",
|
|
411
|
+
source: this.CLASS_NAME,
|
|
412
|
+
message: "savedBlob",
|
|
413
|
+
data: {
|
|
414
|
+
blobId
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
return blobId;
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
await this._logging?.log({
|
|
421
|
+
level: "error",
|
|
422
|
+
source: this.CLASS_NAME,
|
|
423
|
+
message: "saveBlobFailed",
|
|
424
|
+
error: core.BaseError.fromError(error)
|
|
425
|
+
});
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Remove a blob from storage.
|
|
431
|
+
* @param blobId The id of the blob to remove.
|
|
432
|
+
* @returns Nothing.
|
|
433
|
+
*/
|
|
434
|
+
async removeBlob(blobId) {
|
|
435
|
+
await this._logging?.log({
|
|
436
|
+
level: "info",
|
|
437
|
+
source: this.CLASS_NAME,
|
|
438
|
+
message: "removeBlob",
|
|
439
|
+
data: {
|
|
440
|
+
blobId
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
try {
|
|
444
|
+
await this._blobStorageConnector.remove(blobId);
|
|
445
|
+
await this._logging?.log({
|
|
446
|
+
level: "info",
|
|
447
|
+
source: this.CLASS_NAME,
|
|
448
|
+
message: "removedBlob",
|
|
449
|
+
data: {
|
|
450
|
+
blobId
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
await this._logging?.log({
|
|
456
|
+
level: "error",
|
|
457
|
+
source: this.CLASS_NAME,
|
|
458
|
+
message: "removeBlobFailed",
|
|
459
|
+
data: {
|
|
460
|
+
blobId
|
|
461
|
+
},
|
|
462
|
+
error: core.BaseError.fromError(error)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
await this._logging?.log({
|
|
466
|
+
level: "info",
|
|
467
|
+
source: this.CLASS_NAME,
|
|
468
|
+
message: "removeBlobEmpty",
|
|
469
|
+
data: {
|
|
470
|
+
blobId
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
164
476
|
// Copyright 2024 IOTA Stiftung.
|
|
165
477
|
// SPDX-License-Identifier: Apache-2.0.
|
|
166
478
|
/**
|
|
@@ -182,10 +494,10 @@ class ChangeSetHelper {
|
|
|
182
494
|
*/
|
|
183
495
|
_eventBusComponent;
|
|
184
496
|
/**
|
|
185
|
-
* The blob storage
|
|
497
|
+
* The blob storage helper to use for remote sync states.
|
|
186
498
|
* @internal
|
|
187
499
|
*/
|
|
188
|
-
|
|
500
|
+
_blobStorageHelper;
|
|
189
501
|
/**
|
|
190
502
|
* The identity connector to use for signing/verifying changesets.
|
|
191
503
|
* @internal
|
|
@@ -196,37 +508,73 @@ class ChangeSetHelper {
|
|
|
196
508
|
* @internal
|
|
197
509
|
*/
|
|
198
510
|
_decentralisedStorageMethodId;
|
|
511
|
+
/**
|
|
512
|
+
* The identity of the node that is performing the update.
|
|
513
|
+
* @internal
|
|
514
|
+
*/
|
|
515
|
+
_nodeIdentity;
|
|
199
516
|
/**
|
|
200
517
|
* Create a new instance of ChangeSetHelper.
|
|
201
518
|
* @param logging The logging connector to use for logging.
|
|
202
519
|
* @param eventBusComponent The event bus component to use for events.
|
|
203
|
-
* @param blobStorageComponent The blob storage component to use for remote sync states.
|
|
204
520
|
* @param identityConnector The identity connector to use for signing/verifying changesets.
|
|
521
|
+
* @param blobStorageHelper The blob storage component to use for remote sync states.
|
|
205
522
|
* @param decentralisedStorageMethodId The id of the identity method to use when signing/verifying changesets.
|
|
206
523
|
*/
|
|
207
|
-
constructor(logging, eventBusComponent,
|
|
524
|
+
constructor(logging, eventBusComponent, identityConnector, blobStorageHelper, decentralisedStorageMethodId) {
|
|
208
525
|
this._logging = logging;
|
|
209
526
|
this._eventBusComponent = eventBusComponent;
|
|
210
527
|
this._decentralisedStorageMethodId = decentralisedStorageMethodId;
|
|
211
|
-
this.
|
|
528
|
+
this._blobStorageHelper = blobStorageHelper;
|
|
212
529
|
this._identityConnector = identityConnector;
|
|
213
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Set the node identity to use for signing changesets.
|
|
533
|
+
* @param nodeIdentity The identity of the node that is performing the update.
|
|
534
|
+
*/
|
|
535
|
+
setNodeIdentity(nodeIdentity) {
|
|
536
|
+
this._nodeIdentity = nodeIdentity;
|
|
537
|
+
}
|
|
214
538
|
/**
|
|
215
539
|
* Get and verify a changeset.
|
|
216
540
|
* @param changeSetStorageId The id of the sync changeset to apply.
|
|
217
541
|
* @returns The changeset if it was verified.
|
|
218
542
|
*/
|
|
219
543
|
async getAndVerifyChangeset(changeSetStorageId) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
544
|
+
await this._logging?.log({
|
|
545
|
+
level: "info",
|
|
546
|
+
source: this.CLASS_NAME,
|
|
547
|
+
message: "getChangeSet",
|
|
548
|
+
data: {
|
|
549
|
+
changeSetStorageId
|
|
550
|
+
}
|
|
224
551
|
});
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
552
|
+
try {
|
|
553
|
+
const syncChangeSet = await this._blobStorageHelper.loadBlob(changeSetStorageId);
|
|
554
|
+
if (core.Is.object(syncChangeSet)) {
|
|
555
|
+
const verified = await this.verifyChangesetProof(syncChangeSet);
|
|
556
|
+
return verified ? syncChangeSet : undefined;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
await this._logging?.log({
|
|
561
|
+
level: "warn",
|
|
562
|
+
source: this.CLASS_NAME,
|
|
563
|
+
message: "getChangeSetError",
|
|
564
|
+
data: {
|
|
565
|
+
changeSetStorageId
|
|
566
|
+
},
|
|
567
|
+
error: core.BaseError.fromError(error)
|
|
568
|
+
});
|
|
229
569
|
}
|
|
570
|
+
await this._logging?.log({
|
|
571
|
+
level: "info",
|
|
572
|
+
source: this.CLASS_NAME,
|
|
573
|
+
message: "getChangeSetEmpty",
|
|
574
|
+
data: {
|
|
575
|
+
changeSetStorageId
|
|
576
|
+
}
|
|
577
|
+
});
|
|
230
578
|
}
|
|
231
579
|
/**
|
|
232
580
|
* Apply a sync changeset.
|
|
@@ -235,7 +583,9 @@ class ChangeSetHelper {
|
|
|
235
583
|
*/
|
|
236
584
|
async getAndApplyChangeset(changeSetStorageId) {
|
|
237
585
|
const syncChangeset = await this.getAndVerifyChangeset(changeSetStorageId);
|
|
238
|
-
|
|
586
|
+
// Only apply changesets from other nodes, we don't want to overwrite
|
|
587
|
+
// any changes we have made to local entity storage
|
|
588
|
+
if (!core.Is.empty(syncChangeset) && syncChangeset.nodeIdentity !== this._nodeIdentity) {
|
|
239
589
|
await this.applyChangeset(syncChangeset);
|
|
240
590
|
}
|
|
241
591
|
return syncChangeset;
|
|
@@ -260,13 +610,18 @@ class ChangeSetHelper {
|
|
|
260
610
|
switch (change.operation) {
|
|
261
611
|
case synchronisedStorageModels.SyncChangeOperation.Set:
|
|
262
612
|
if (!core.Is.empty(change.entity)) {
|
|
263
|
-
// The
|
|
613
|
+
// The id was stripped from the entity as it is part of the operation
|
|
614
|
+
// we make sure we reinstate it in the publish
|
|
615
|
+
// Also the node identity was stripped when stored in the changeset
|
|
264
616
|
// as the changeset is signed with the node identity.
|
|
265
617
|
// so we need to restore it here.
|
|
266
|
-
change.entity.nodeIdentity = syncChangeset.nodeIdentity;
|
|
267
618
|
await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.RemoteItemSet, {
|
|
268
619
|
storageKey: syncChangeset.storageKey,
|
|
269
|
-
entity:
|
|
620
|
+
entity: {
|
|
621
|
+
...change.entity,
|
|
622
|
+
id: change.id,
|
|
623
|
+
nodeIdentity: syncChangeset.nodeIdentity
|
|
624
|
+
}
|
|
270
625
|
});
|
|
271
626
|
}
|
|
272
627
|
break;
|
|
@@ -274,7 +629,8 @@ class ChangeSetHelper {
|
|
|
274
629
|
if (!core.Is.empty(change.id)) {
|
|
275
630
|
await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.RemoteItemRemove, {
|
|
276
631
|
storageKey: syncChangeset.storageKey,
|
|
277
|
-
id: change.id
|
|
632
|
+
id: change.id,
|
|
633
|
+
nodeIdentity: syncChangeset.nodeIdentity
|
|
278
634
|
});
|
|
279
635
|
}
|
|
280
636
|
break;
|
|
@@ -285,10 +641,9 @@ class ChangeSetHelper {
|
|
|
285
641
|
/**
|
|
286
642
|
* Store the changeset.
|
|
287
643
|
* @param syncChangeSet The sync change set to store.
|
|
288
|
-
* @param nodeIdentity The node identity to use for the changeset.
|
|
289
644
|
* @returns The id of the change set.
|
|
290
645
|
*/
|
|
291
|
-
async storeChangeSet(syncChangeSet
|
|
646
|
+
async storeChangeSet(syncChangeSet) {
|
|
292
647
|
await this._logging?.log({
|
|
293
648
|
level: "info",
|
|
294
649
|
source: this.CLASS_NAME,
|
|
@@ -297,12 +652,7 @@ class ChangeSetHelper {
|
|
|
297
652
|
id: syncChangeSet.id
|
|
298
653
|
}
|
|
299
654
|
});
|
|
300
|
-
|
|
301
|
-
// the blob storage also needs to be publicly accessible so that other nodes can retrieve it
|
|
302
|
-
return this._blobStorageComponent.create(core.Converter.bytesToBase64(core.ObjectHelper.toBytes(syncChangeSet)), undefined, undefined, undefined, {
|
|
303
|
-
disableEncryption: true,
|
|
304
|
-
compress: blobStorageModels.BlobStorageCompressionType.Gzip
|
|
305
|
-
}, undefined, nodeIdentity);
|
|
655
|
+
return this._blobStorageHelper.saveBlob(syncChangeSet);
|
|
306
656
|
}
|
|
307
657
|
/**
|
|
308
658
|
* Verify the proof of a sync changeset.
|
|
@@ -321,6 +671,32 @@ class ChangeSetHelper {
|
|
|
321
671
|
});
|
|
322
672
|
return false;
|
|
323
673
|
}
|
|
674
|
+
// If the proof or verification method is missing, the proof is invalid
|
|
675
|
+
const verificationMethod = syncChangeset.proof?.verificationMethod;
|
|
676
|
+
if (!core.Is.stringValue(verificationMethod)) {
|
|
677
|
+
await this._logging?.log({
|
|
678
|
+
level: "error",
|
|
679
|
+
source: this.CLASS_NAME,
|
|
680
|
+
message: "verifyChangeSetProofMissing",
|
|
681
|
+
data: {
|
|
682
|
+
id: syncChangeset.id
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
// Parse the verification method and extract the node identity
|
|
687
|
+
// this should match the node identity of the changeset
|
|
688
|
+
// otherwise you could sign a changeset for another node
|
|
689
|
+
const changeSetNodeIdentity = identityModels.DocumentHelper.parseId(verificationMethod ?? "");
|
|
690
|
+
if (changeSetNodeIdentity.id !== syncChangeset.nodeIdentity) {
|
|
691
|
+
await this._logging?.log({
|
|
692
|
+
level: "error",
|
|
693
|
+
source: this.CLASS_NAME,
|
|
694
|
+
message: "verifyChangeSetProofNodeIdentityMismatch",
|
|
695
|
+
data: {
|
|
696
|
+
id: syncChangeset.id
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
324
700
|
const changeSetWithoutProof = core.ObjectHelper.clone(syncChangeset);
|
|
325
701
|
delete changeSetWithoutProof.proof;
|
|
326
702
|
const isValid = await this._identityConnector.verifyProof(changeSetWithoutProof, syncChangeset.proof);
|
|
@@ -352,9 +728,10 @@ class ChangeSetHelper {
|
|
|
352
728
|
* @returns The proof.
|
|
353
729
|
*/
|
|
354
730
|
async createChangeSetProof(syncChangeset) {
|
|
731
|
+
core.Guards.stringValue(this.CLASS_NAME, "nodeIdentity", this._nodeIdentity);
|
|
355
732
|
const changeSetWithoutProof = core.ObjectHelper.clone(syncChangeset);
|
|
356
733
|
delete changeSetWithoutProof.proof;
|
|
357
|
-
const proof = await this._identityConnector.createProof(
|
|
734
|
+
const proof = await this._identityConnector.createProof(this._nodeIdentity, identityModels.DocumentHelper.joinId(this._nodeIdentity, this._decentralisedStorageMethodId), standardsW3cDid.ProofTypes.DataIntegrityProof, changeSetWithoutProof);
|
|
358
735
|
await this._logging?.log({
|
|
359
736
|
level: "info",
|
|
360
737
|
source: this.CLASS_NAME,
|
|
@@ -366,8 +743,65 @@ class ChangeSetHelper {
|
|
|
366
743
|
});
|
|
367
744
|
return proof;
|
|
368
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Copy a change set.
|
|
748
|
+
* @param syncChangeSet The sync changeset to copy.
|
|
749
|
+
* @returns The id of the updated change set.
|
|
750
|
+
*/
|
|
751
|
+
async copyChangeset(syncChangeSet) {
|
|
752
|
+
if (core.Is.stringValue(this._nodeIdentity)) {
|
|
753
|
+
const verified = await this.verifyChangesetProof(syncChangeSet);
|
|
754
|
+
if (verified) {
|
|
755
|
+
await this._logging?.log({
|
|
756
|
+
level: "info",
|
|
757
|
+
source: this.CLASS_NAME,
|
|
758
|
+
message: "copyChangeSet",
|
|
759
|
+
data: {
|
|
760
|
+
changeSetStorageId: syncChangeSet.id
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
// Allocate a new id to the changeset copy and re-create a proof using this nodes identity
|
|
764
|
+
const copy = core.ObjectHelper.clone(syncChangeSet);
|
|
765
|
+
copy.id = core.Converter.bytesToHex(core.RandomHelper.generate(32));
|
|
766
|
+
copy.proof = await this.createChangeSetProof(copy);
|
|
767
|
+
// Store the copy
|
|
768
|
+
return {
|
|
769
|
+
syncChangeSet: copy,
|
|
770
|
+
changeSetStorageId: await this.storeChangeSet(copy)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Reset the storage for a given storage key.
|
|
777
|
+
* @param storageKey The key of the storage to reset.
|
|
778
|
+
* @param resetMode The reset mode, this will use the nodeIdentity in the entities to determine which are local/remote.
|
|
779
|
+
* @returns Nothing.
|
|
780
|
+
*/
|
|
781
|
+
async reset(storageKey, resetMode) {
|
|
782
|
+
// If we are applying a consolidation we need to reset the local db
|
|
783
|
+
// but keep any entries from the local node, as they might have been updated
|
|
784
|
+
await this._logging?.log({
|
|
785
|
+
level: "info",
|
|
786
|
+
source: this.CLASS_NAME,
|
|
787
|
+
message: "storageReset",
|
|
788
|
+
data: {
|
|
789
|
+
storageKey
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.Reset, {
|
|
793
|
+
storageKey,
|
|
794
|
+
resetMode
|
|
795
|
+
});
|
|
796
|
+
}
|
|
369
797
|
}
|
|
370
798
|
|
|
799
|
+
// Copyright 2024 IOTA Stiftung.
|
|
800
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
801
|
+
const SYNC_STATE_VERSION = "1";
|
|
802
|
+
const SYNC_POINTER_STORE_VERSION = "1";
|
|
803
|
+
const SYNC_SNAPSHOT_VERSION = "1";
|
|
804
|
+
|
|
371
805
|
// Copyright 2024 IOTA Stiftung.
|
|
372
806
|
// SPDX-License-Identifier: Apache-2.0.
|
|
373
807
|
/**
|
|
@@ -387,7 +821,7 @@ class LocalSyncStateHelper {
|
|
|
387
821
|
* The storage connector for the sync snapshot entries.
|
|
388
822
|
* @internal
|
|
389
823
|
*/
|
|
390
|
-
|
|
824
|
+
_snapshotEntryEntityStorage;
|
|
391
825
|
/**
|
|
392
826
|
* The change set helper to use for applying changesets.
|
|
393
827
|
* @internal
|
|
@@ -396,12 +830,12 @@ class LocalSyncStateHelper {
|
|
|
396
830
|
/**
|
|
397
831
|
* Create a new instance of LocalSyncStateHelper.
|
|
398
832
|
* @param logging The logging connector to use for logging.
|
|
399
|
-
* @param
|
|
833
|
+
* @param snapshotEntryEntityStorage The storage connector for the sync snapshot entries.
|
|
400
834
|
* @param changeSetHelper The change set helper to use for applying changesets.
|
|
401
835
|
*/
|
|
402
|
-
constructor(logging,
|
|
836
|
+
constructor(logging, snapshotEntryEntityStorage, changeSetHelper) {
|
|
403
837
|
this._logging = logging;
|
|
404
|
-
this.
|
|
838
|
+
this._snapshotEntryEntityStorage = snapshotEntryEntityStorage;
|
|
405
839
|
this._changeSetHelper = changeSetHelper;
|
|
406
840
|
}
|
|
407
841
|
/**
|
|
@@ -422,40 +856,44 @@ class LocalSyncStateHelper {
|
|
|
422
856
|
id
|
|
423
857
|
}
|
|
424
858
|
});
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
localChangeSnapshot.changes.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
859
|
+
const localChangeSnapshots = await this.getSnapshots(storageKey, true);
|
|
860
|
+
if (localChangeSnapshots.length > 0) {
|
|
861
|
+
const localChangeSnapshot = localChangeSnapshots[0];
|
|
862
|
+
localChangeSnapshot.changes ??= [];
|
|
863
|
+
// If we already have a change for this id we are
|
|
864
|
+
// about to supersede it, we remove the previous change
|
|
865
|
+
// to avoid having multiple changes for the same id
|
|
866
|
+
const previousChangeIndex = localChangeSnapshot.changes.findIndex(change => change.id === id);
|
|
867
|
+
if (previousChangeIndex !== -1) {
|
|
868
|
+
localChangeSnapshot.changes.splice(previousChangeIndex, 1);
|
|
869
|
+
}
|
|
870
|
+
if (localChangeSnapshot.changes.length > 0) {
|
|
871
|
+
localChangeSnapshot.dateModified = new Date(Date.now()).toISOString();
|
|
872
|
+
}
|
|
873
|
+
localChangeSnapshot.changes.push({ operation, id });
|
|
874
|
+
await this.setLocalChangeSnapshot(localChangeSnapshot);
|
|
436
875
|
}
|
|
437
|
-
localChangeSnapshot.changes.push({ operation, id });
|
|
438
|
-
await this.setLocalChangeSnapshot(localChangeSnapshot);
|
|
439
876
|
}
|
|
440
877
|
/**
|
|
441
|
-
* Get the
|
|
878
|
+
* Get the snapshot which contains just the changes for this node.
|
|
442
879
|
* @param storageKey The storage key of the snapshot to get.
|
|
880
|
+
* @param isLocal Whether to get the local snapshot or not.
|
|
443
881
|
* @returns The local snapshot entry.
|
|
444
882
|
*/
|
|
445
|
-
async
|
|
883
|
+
async getSnapshots(storageKey, isLocal) {
|
|
446
884
|
await this._logging?.log({
|
|
447
885
|
level: "info",
|
|
448
886
|
source: this.CLASS_NAME,
|
|
449
|
-
message: "
|
|
887
|
+
message: "getSnapshots",
|
|
450
888
|
data: {
|
|
451
889
|
storageKey
|
|
452
890
|
}
|
|
453
891
|
});
|
|
454
|
-
const queryResult = await this.
|
|
892
|
+
const queryResult = await this._snapshotEntryEntityStorage.query({
|
|
455
893
|
conditions: [
|
|
456
894
|
{
|
|
457
|
-
property: "
|
|
458
|
-
value:
|
|
895
|
+
property: "isLocal",
|
|
896
|
+
value: isLocal,
|
|
459
897
|
comparison: entity.ComparisonOperator.Equals
|
|
460
898
|
},
|
|
461
899
|
{
|
|
@@ -469,31 +907,37 @@ class LocalSyncStateHelper {
|
|
|
469
907
|
await this._logging?.log({
|
|
470
908
|
level: "info",
|
|
471
909
|
source: this.CLASS_NAME,
|
|
472
|
-
message: "
|
|
910
|
+
message: "getSnapshotsExists",
|
|
473
911
|
data: {
|
|
474
912
|
storageKey
|
|
475
913
|
}
|
|
476
914
|
});
|
|
477
|
-
return queryResult.entities
|
|
915
|
+
return queryResult.entities;
|
|
478
916
|
}
|
|
479
917
|
await this._logging?.log({
|
|
480
918
|
level: "info",
|
|
481
919
|
source: this.CLASS_NAME,
|
|
482
|
-
message: "
|
|
920
|
+
message: "getSnapshotsDoesNotExist",
|
|
483
921
|
data: {
|
|
484
922
|
storageKey
|
|
485
923
|
}
|
|
486
924
|
});
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
925
|
+
const now = new Date(Date.now()).toISOString();
|
|
926
|
+
return [
|
|
927
|
+
{
|
|
928
|
+
version: SYNC_SNAPSHOT_VERSION,
|
|
929
|
+
id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
|
|
930
|
+
storageKey,
|
|
931
|
+
dateCreated: now,
|
|
932
|
+
dateModified: now,
|
|
933
|
+
changeSetStorageIds: [],
|
|
934
|
+
isLocal,
|
|
935
|
+
isConsolidated: false
|
|
936
|
+
}
|
|
937
|
+
];
|
|
494
938
|
}
|
|
495
939
|
/**
|
|
496
|
-
* Set the current local snapshot.
|
|
940
|
+
* Set the current local snapshot with changes for this node.
|
|
497
941
|
* @param localChangeSnapshot The local change snapshot to set.
|
|
498
942
|
* @returns Nothing.
|
|
499
943
|
*/
|
|
@@ -506,10 +950,10 @@ class LocalSyncStateHelper {
|
|
|
506
950
|
storageKey: localChangeSnapshot.storageKey
|
|
507
951
|
}
|
|
508
952
|
});
|
|
509
|
-
await this.
|
|
953
|
+
await this._snapshotEntryEntityStorage.set(localChangeSnapshot);
|
|
510
954
|
}
|
|
511
955
|
/**
|
|
512
|
-
* Get the current local snapshot.
|
|
956
|
+
* Get the current local snapshot with the changes for this node.
|
|
513
957
|
* @param localChangeSnapshot The local change snapshot to remove.
|
|
514
958
|
* @returns Nothing.
|
|
515
959
|
*/
|
|
@@ -522,85 +966,155 @@ class LocalSyncStateHelper {
|
|
|
522
966
|
snapshotId: localChangeSnapshot.id
|
|
523
967
|
}
|
|
524
968
|
});
|
|
525
|
-
await this.
|
|
969
|
+
await this._snapshotEntryEntityStorage.remove(localChangeSnapshot.id);
|
|
526
970
|
}
|
|
527
971
|
/**
|
|
528
|
-
*
|
|
972
|
+
* Apply a sync state to the local node.
|
|
529
973
|
* @param storageKey The storage key of the snapshot to sync with.
|
|
530
|
-
* @param
|
|
974
|
+
* @param syncState The sync state to sync with.
|
|
531
975
|
* @returns Nothing.
|
|
532
976
|
*/
|
|
533
|
-
async
|
|
977
|
+
async applySyncState(storageKey, syncState) {
|
|
534
978
|
await this._logging?.log({
|
|
535
979
|
level: "info",
|
|
536
980
|
source: this.CLASS_NAME,
|
|
537
|
-
message: "
|
|
981
|
+
message: "applySyncState",
|
|
538
982
|
data: {
|
|
539
|
-
snapshotCount:
|
|
983
|
+
snapshotCount: syncState.snapshots.length
|
|
540
984
|
}
|
|
541
985
|
});
|
|
986
|
+
// Get all the existing snapshots that we have processed previously
|
|
987
|
+
const existingRemoteSnapshots = await this.getSnapshots(storageKey, false);
|
|
542
988
|
// Sort from newest to oldest
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
989
|
+
const sortedSnapshots = syncState.snapshots.sort((a, b) => new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime());
|
|
990
|
+
// If we have no existing snapshots we can't have yet synced
|
|
991
|
+
// in this case we need to find the most recent consolidation
|
|
992
|
+
// and use that to build a complete DB table
|
|
993
|
+
if (existingRemoteSnapshots.length === 0) {
|
|
547
994
|
await this._logging?.log({
|
|
548
995
|
level: "info",
|
|
549
996
|
source: this.CLASS_NAME,
|
|
550
|
-
message: "
|
|
997
|
+
message: "applySnapshotNoExisting",
|
|
551
998
|
data: {
|
|
552
|
-
|
|
553
|
-
dateCreated: new Date(remoteSnapshot.dateCreated).toISOString()
|
|
999
|
+
storageKey
|
|
554
1000
|
}
|
|
555
1001
|
});
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
modifiedSnapshots.push({
|
|
568
|
-
localSnapshot,
|
|
569
|
-
remoteSnapshot: remoteSnapshotWithContext
|
|
1002
|
+
const firstConsolidated = sortedSnapshots.find(snapshot => snapshot.isConsolidated);
|
|
1003
|
+
if (firstConsolidated) {
|
|
1004
|
+
// We found a consolidated snapshot, we can use it
|
|
1005
|
+
await this._logging?.log({
|
|
1006
|
+
level: "info",
|
|
1007
|
+
source: this.CLASS_NAME,
|
|
1008
|
+
message: "applySnapshotFoundConsolidated",
|
|
1009
|
+
data: {
|
|
1010
|
+
storageKey,
|
|
1011
|
+
snapshotId: firstConsolidated.id
|
|
1012
|
+
}
|
|
570
1013
|
});
|
|
1014
|
+
// We need to reset the entity storage and remove all the remote items
|
|
1015
|
+
// so that we use just the ones from the consolidation
|
|
1016
|
+
await this._changeSetHelper.reset(storageKey, synchronisedStorageModels.SyncNodeIdentityMode.Remote);
|
|
1017
|
+
await this.processNewSnapshots([
|
|
1018
|
+
{
|
|
1019
|
+
...firstConsolidated,
|
|
1020
|
+
storageKey,
|
|
1021
|
+
isLocal: false
|
|
1022
|
+
}
|
|
1023
|
+
]);
|
|
571
1024
|
}
|
|
572
1025
|
else {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1026
|
+
await this._logging?.log({
|
|
1027
|
+
level: "info",
|
|
1028
|
+
source: this.CLASS_NAME,
|
|
1029
|
+
message: "applySnapshotNoConsolidated",
|
|
1030
|
+
data: {
|
|
1031
|
+
storageKey
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
// Create a lookup map for the existing snapshots
|
|
1038
|
+
const existingSnapshots = {};
|
|
1039
|
+
for (const snapshot of existingRemoteSnapshots) {
|
|
1040
|
+
existingSnapshots[snapshot.id] = snapshot;
|
|
1041
|
+
}
|
|
1042
|
+
const newSnapshots = [];
|
|
1043
|
+
const modifiedSnapshots = [];
|
|
1044
|
+
const referencedExistingSnapshots = Object.keys(existingSnapshots);
|
|
1045
|
+
for (const snapshot of sortedSnapshots) {
|
|
1046
|
+
await this._logging?.log({
|
|
1047
|
+
level: "info",
|
|
1048
|
+
source: this.CLASS_NAME,
|
|
1049
|
+
message: "applySnapshot",
|
|
1050
|
+
data: {
|
|
1051
|
+
snapshotId: snapshot.id,
|
|
1052
|
+
dateCreated: new Date(snapshot.dateCreated).toISOString()
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
// See if we have the local snapshot
|
|
1056
|
+
const currentSnapshot = existingSnapshots[snapshot.id];
|
|
1057
|
+
// As we are referencing an existing snapshot, we need to remove it from the list
|
|
1058
|
+
// to allow us to cleanup any unreferenced snapshots later
|
|
1059
|
+
const idx = referencedExistingSnapshots.indexOf(snapshot.id);
|
|
1060
|
+
if (idx !== -1) {
|
|
1061
|
+
referencedExistingSnapshots.splice(idx, 1);
|
|
1062
|
+
}
|
|
1063
|
+
const updatedSnapshot = {
|
|
1064
|
+
...snapshot,
|
|
1065
|
+
storageKey,
|
|
1066
|
+
isLocal: false
|
|
1067
|
+
};
|
|
1068
|
+
if (core.Is.empty(currentSnapshot)) {
|
|
1069
|
+
// We don't have the snapshot locally, so we need to process it
|
|
1070
|
+
newSnapshots.push(updatedSnapshot);
|
|
1071
|
+
}
|
|
1072
|
+
else if (currentSnapshot.dateModified !== snapshot.dateModified) {
|
|
1073
|
+
// If the local snapshot has a different dateModified, we need to update it
|
|
1074
|
+
modifiedSnapshots.push({
|
|
1075
|
+
currentSnapshot,
|
|
1076
|
+
updatedSnapshot
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
// we sorted the snapshots from newest to oldest, so if we found a local snapshot
|
|
1081
|
+
// with the same dateModified as the remote snapshot, we can stop processing further
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// We reverse the order of the snapshots to process them from oldest to newest
|
|
1086
|
+
// because we want to apply the changes in the order they were created
|
|
1087
|
+
await this.processModifiedSnapshots(modifiedSnapshots.reverse());
|
|
1088
|
+
await this.processNewSnapshots(newSnapshots.reverse());
|
|
1089
|
+
// Any ids remaining in this list are no longer referenced in the global state
|
|
1090
|
+
// so we should remove them from the local storage as they will never be updated again
|
|
1091
|
+
for (const referencedSnapshotId of referencedExistingSnapshots) {
|
|
1092
|
+
await this._snapshotEntryEntityStorage.remove(referencedSnapshotId);
|
|
576
1093
|
}
|
|
577
1094
|
}
|
|
578
|
-
// We reverse the order of the snapshots to process them from oldest to newest
|
|
579
|
-
// because we want to apply the changes in the order they were created
|
|
580
|
-
await this.processModifiedSnapshots(modifiedSnapshots.reverse());
|
|
581
|
-
await this.processNewSnapshots(newSnapshots.reverse());
|
|
582
1095
|
}
|
|
583
1096
|
/**
|
|
584
1097
|
* Process the modified snapshots and store them in the local storage.
|
|
585
1098
|
* @param modifiedSnapshots The modified snapshots to process.
|
|
586
1099
|
* @returns Nothing.
|
|
1100
|
+
* @internal
|
|
587
1101
|
*/
|
|
588
1102
|
async processModifiedSnapshots(modifiedSnapshots) {
|
|
589
1103
|
for (const modifiedSnapshot of modifiedSnapshots) {
|
|
590
1104
|
await this._logging?.log({
|
|
591
1105
|
level: "info",
|
|
592
1106
|
source: this.CLASS_NAME,
|
|
593
|
-
message: "
|
|
1107
|
+
message: "processModifiedSnapshot",
|
|
594
1108
|
data: {
|
|
595
|
-
snapshotId: modifiedSnapshot.
|
|
596
|
-
localModified: new Date(modifiedSnapshot.
|
|
597
|
-
modifiedSnapshot.
|
|
598
|
-
remoteModified: new Date(modifiedSnapshot.
|
|
599
|
-
modifiedSnapshot.
|
|
1109
|
+
snapshotId: modifiedSnapshot.updatedSnapshot.id,
|
|
1110
|
+
localModified: new Date(modifiedSnapshot.currentSnapshot.dateModified ??
|
|
1111
|
+
modifiedSnapshot.currentSnapshot.dateCreated).toISOString(),
|
|
1112
|
+
remoteModified: new Date(modifiedSnapshot.updatedSnapshot.dateModified ??
|
|
1113
|
+
modifiedSnapshot.updatedSnapshot.dateCreated).toISOString()
|
|
600
1114
|
}
|
|
601
1115
|
});
|
|
602
|
-
const remoteChangeSetStorageIds = modifiedSnapshot.
|
|
603
|
-
const localChangeSetStorageIds = modifiedSnapshot.
|
|
1116
|
+
const remoteChangeSetStorageIds = modifiedSnapshot.updatedSnapshot.changeSetStorageIds;
|
|
1117
|
+
const localChangeSetStorageIds = modifiedSnapshot.currentSnapshot.changeSetStorageIds ?? [];
|
|
604
1118
|
if (core.Is.arrayValue(remoteChangeSetStorageIds)) {
|
|
605
1119
|
for (const storageId of remoteChangeSetStorageIds) {
|
|
606
1120
|
// Check if the local snapshot does not have the storageId
|
|
@@ -609,23 +1123,24 @@ class LocalSyncStateHelper {
|
|
|
609
1123
|
}
|
|
610
1124
|
}
|
|
611
1125
|
}
|
|
612
|
-
await this.
|
|
1126
|
+
await this._snapshotEntryEntityStorage.set(modifiedSnapshot.updatedSnapshot);
|
|
613
1127
|
}
|
|
614
1128
|
}
|
|
615
1129
|
/**
|
|
616
1130
|
* Process the new snapshots and store them in the local storage.
|
|
617
1131
|
* @param newSnapshots The new snapshots to process.
|
|
618
1132
|
* @returns Nothing.
|
|
1133
|
+
* @internal
|
|
619
1134
|
*/
|
|
620
1135
|
async processNewSnapshots(newSnapshots) {
|
|
621
1136
|
for (const newSnapshot of newSnapshots) {
|
|
622
1137
|
await this._logging?.log({
|
|
623
1138
|
level: "info",
|
|
624
1139
|
source: this.CLASS_NAME,
|
|
625
|
-
message: "
|
|
1140
|
+
message: "processNewSnapshot",
|
|
626
1141
|
data: {
|
|
627
1142
|
snapshotId: newSnapshot.id,
|
|
628
|
-
|
|
1143
|
+
dateCreated: newSnapshot.dateCreated
|
|
629
1144
|
}
|
|
630
1145
|
});
|
|
631
1146
|
const newSnapshotChangeSetStorageIds = newSnapshot.changeSetStorageIds ?? [];
|
|
@@ -634,7 +1149,7 @@ class LocalSyncStateHelper {
|
|
|
634
1149
|
await this._changeSetHelper.getAndApplyChangeset(storageId);
|
|
635
1150
|
}
|
|
636
1151
|
}
|
|
637
|
-
await this.
|
|
1152
|
+
await this._snapshotEntryEntityStorage.set(newSnapshot);
|
|
638
1153
|
}
|
|
639
1154
|
}
|
|
640
1155
|
}
|
|
@@ -660,10 +1175,10 @@ class RemoteSyncStateHelper {
|
|
|
660
1175
|
*/
|
|
661
1176
|
_eventBusComponent;
|
|
662
1177
|
/**
|
|
663
|
-
* The blob storage
|
|
1178
|
+
* The blob storage helper.
|
|
664
1179
|
* @internal
|
|
665
1180
|
*/
|
|
666
|
-
|
|
1181
|
+
_blobStorageHelper;
|
|
667
1182
|
/**
|
|
668
1183
|
* The verifiable storage connector to use for storing sync pointers.
|
|
669
1184
|
* @internal
|
|
@@ -694,22 +1209,34 @@ class RemoteSyncStateHelper {
|
|
|
694
1209
|
* @internal
|
|
695
1210
|
*/
|
|
696
1211
|
_nodeIdentity;
|
|
1212
|
+
/**
|
|
1213
|
+
* Whether the node is trusted or not.
|
|
1214
|
+
* @internal
|
|
1215
|
+
*/
|
|
1216
|
+
_isTrustedNode;
|
|
1217
|
+
/**
|
|
1218
|
+
* Maximum number of consolidations to keep in storage.
|
|
1219
|
+
* @internal
|
|
1220
|
+
*/
|
|
1221
|
+
_maxConsolidations;
|
|
697
1222
|
/**
|
|
698
1223
|
* Create a new instance of DecentralisedEntityStorageConnector.
|
|
699
1224
|
* @param logging The logging connector to use for logging.
|
|
700
1225
|
* @param eventBusComponent The event bus component to use for events.
|
|
701
|
-
* @param blobStorageComponent The blob storage component to use for remote sync states.
|
|
702
1226
|
* @param verifiableSyncPointerStorageConnector The verifiable storage connector to use for storing sync pointers.
|
|
1227
|
+
* @param blobStorageHelper The blob storage helper to use for remote sync states.
|
|
703
1228
|
* @param changeSetHelper The change set helper to use for managing changesets.
|
|
704
|
-
* @param
|
|
1229
|
+
* @param isTrustedNode Whether the node is trusted or not.
|
|
1230
|
+
* @param maxConsolidations The maximum number of consolidations to keep in storage.
|
|
705
1231
|
*/
|
|
706
|
-
constructor(logging, eventBusComponent,
|
|
1232
|
+
constructor(logging, eventBusComponent, verifiableSyncPointerStorageConnector, blobStorageHelper, changeSetHelper, isTrustedNode, maxConsolidations) {
|
|
707
1233
|
this._logging = logging;
|
|
708
1234
|
this._eventBusComponent = eventBusComponent;
|
|
709
|
-
this._blobStorageComponent = blobStorageComponent;
|
|
710
1235
|
this._verifiableSyncPointerStorageConnector = verifiableSyncPointerStorageConnector;
|
|
711
1236
|
this._changeSetHelper = changeSetHelper;
|
|
712
|
-
this.
|
|
1237
|
+
this._blobStorageHelper = blobStorageHelper;
|
|
1238
|
+
this._isTrustedNode = isTrustedNode;
|
|
1239
|
+
this._maxConsolidations = maxConsolidations;
|
|
713
1240
|
this._batchResponseStorageIds = {};
|
|
714
1241
|
this._populateFullChanges = {};
|
|
715
1242
|
this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.BatchResponse, async (response) => {
|
|
@@ -727,17 +1254,24 @@ class RemoteSyncStateHelper {
|
|
|
727
1254
|
this._nodeIdentity = nodeIdentity;
|
|
728
1255
|
}
|
|
729
1256
|
/**
|
|
730
|
-
*
|
|
1257
|
+
* Set the synchronised storage key.
|
|
1258
|
+
* @param synchronisedStorageKey The synchronised storage key to use.
|
|
1259
|
+
*/
|
|
1260
|
+
setSynchronisedStorageKey(synchronisedStorageKey) {
|
|
1261
|
+
this._synchronisedStorageKey = synchronisedStorageKey;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Build a changeset.
|
|
731
1265
|
* @param storageKey The storage key of the change set.
|
|
732
1266
|
* @param changes The changes to apply.
|
|
733
1267
|
* @param completeCallback The callback to call when the changeset is created and stored.
|
|
734
1268
|
* @returns The storage id of the change set if created.
|
|
735
1269
|
*/
|
|
736
|
-
async
|
|
1270
|
+
async buildChangeSet(storageKey, changes, completeCallback) {
|
|
737
1271
|
await this._logging?.log({
|
|
738
1272
|
level: "info",
|
|
739
1273
|
source: this.CLASS_NAME,
|
|
740
|
-
message: "
|
|
1274
|
+
message: "buildingChangeSet",
|
|
741
1275
|
data: {
|
|
742
1276
|
storageKey,
|
|
743
1277
|
changeCount: changes.length
|
|
@@ -796,15 +1330,19 @@ class RemoteSyncStateHelper {
|
|
|
796
1330
|
for (const change of changes) {
|
|
797
1331
|
change.entity = this._populateFullChanges[storageKey].entities[change.id] ?? change.entity;
|
|
798
1332
|
if (change.operation === synchronisedStorageModels.SyncChangeOperation.Set && core.Is.objectValue(change.entity)) {
|
|
1333
|
+
// Remove the id from the entity as this is stored in the operation
|
|
1334
|
+
// and will be reinstated when the changeset is reconstituted
|
|
1335
|
+
core.ObjectHelper.propertyDelete(change.entity, "id");
|
|
799
1336
|
// Remove the node identity as the changeset has this stored at the top level
|
|
800
1337
|
// and we do not want to store it in the change itself to reduce redundancy
|
|
801
1338
|
core.ObjectHelper.propertyDelete(change.entity, "nodeIdentity");
|
|
802
1339
|
}
|
|
803
1340
|
}
|
|
804
|
-
|
|
1341
|
+
const now = new Date(Date.now()).toISOString();
|
|
805
1342
|
const syncChangeSet = {
|
|
806
1343
|
id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
|
|
807
|
-
dateCreated:
|
|
1344
|
+
dateCreated: now,
|
|
1345
|
+
dateModified: now,
|
|
808
1346
|
storageKey,
|
|
809
1347
|
changes,
|
|
810
1348
|
nodeIdentity: this._nodeIdentity
|
|
@@ -812,9 +1350,12 @@ class RemoteSyncStateHelper {
|
|
|
812
1350
|
try {
|
|
813
1351
|
// And sign it with the node identity
|
|
814
1352
|
syncChangeSet.proof = await this._changeSetHelper.createChangeSetProof(syncChangeSet);
|
|
815
|
-
//
|
|
816
|
-
|
|
817
|
-
|
|
1353
|
+
// If this is a trusted node, we also store the changeset
|
|
1354
|
+
let changeSetStorageId;
|
|
1355
|
+
if (this._isTrustedNode) {
|
|
1356
|
+
changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
|
|
1357
|
+
}
|
|
1358
|
+
await completeCallback(syncChangeSet, changeSetStorageId);
|
|
818
1359
|
}
|
|
819
1360
|
catch (err) {
|
|
820
1361
|
await this._logging?.log({
|
|
@@ -853,27 +1394,33 @@ class RemoteSyncStateHelper {
|
|
|
853
1394
|
const syncPointerStore = await this.getVerifiableSyncPointerStore();
|
|
854
1395
|
let syncState;
|
|
855
1396
|
if (!core.Is.empty(syncPointerStore.syncPointers[storageKey])) {
|
|
856
|
-
syncState = await this.
|
|
1397
|
+
syncState = await this.getSyncState(syncPointerStore.syncPointers[storageKey]);
|
|
857
1398
|
}
|
|
858
1399
|
// No current sync state, so we create a new one
|
|
859
1400
|
if (core.Is.empty(syncState)) {
|
|
860
|
-
syncState = { snapshots: [] };
|
|
1401
|
+
syncState = { version: SYNC_STATE_VERSION, storageKey, snapshots: [] };
|
|
861
1402
|
}
|
|
862
1403
|
// Sort the snapshots so the newest snapshot is last in the array
|
|
863
1404
|
const sortedSnapshots = syncState.snapshots.sort((a, b) => a.dateCreated.localeCompare(b.dateCreated));
|
|
864
1405
|
// Get the current snapshot, if it does not exist we create a new one
|
|
865
1406
|
let currentSnapshot = sortedSnapshots[sortedSnapshots.length - 1];
|
|
866
|
-
|
|
1407
|
+
const now = new Date(Date.now()).toISOString();
|
|
1408
|
+
// If there is no snapshot or the current one is a consolidation
|
|
1409
|
+
// we start a new snapshot
|
|
1410
|
+
if (core.Is.empty(currentSnapshot) || currentSnapshot.isConsolidated) {
|
|
867
1411
|
currentSnapshot = {
|
|
1412
|
+
version: SYNC_SNAPSHOT_VERSION,
|
|
868
1413
|
id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
|
|
869
|
-
dateCreated:
|
|
1414
|
+
dateCreated: now,
|
|
1415
|
+
dateModified: now,
|
|
1416
|
+
isConsolidated: false,
|
|
870
1417
|
changeSetStorageIds: []
|
|
871
1418
|
};
|
|
872
1419
|
syncState.snapshots.push(currentSnapshot);
|
|
873
1420
|
}
|
|
874
1421
|
else {
|
|
875
1422
|
// Snapshot exists, we update the dateModified
|
|
876
|
-
currentSnapshot.dateModified =
|
|
1423
|
+
currentSnapshot.dateModified = now;
|
|
877
1424
|
}
|
|
878
1425
|
// Add the changeset storage id to the current snapshot
|
|
879
1426
|
currentSnapshot.changeSetStorageIds.push(changeSetStorageId);
|
|
@@ -888,57 +1435,61 @@ class RemoteSyncStateHelper {
|
|
|
888
1435
|
* @param batchSize The batch size to use for consolidation.
|
|
889
1436
|
* @returns Nothing.
|
|
890
1437
|
*/
|
|
891
|
-
async
|
|
1438
|
+
async consolidationStart(storageKey, batchSize) {
|
|
892
1439
|
await this._logging?.log({
|
|
893
1440
|
level: "info",
|
|
894
1441
|
source: this.CLASS_NAME,
|
|
895
1442
|
message: "consolidationStarting"
|
|
896
1443
|
});
|
|
897
|
-
|
|
1444
|
+
// Perform a batch request to start the consolidation
|
|
1445
|
+
await this._eventBusComponent.publish(synchronisedStorageModels.SynchronisedStorageTopics.BatchRequest, { storageKey, batchSize, requestMode: synchronisedStorageModels.SyncNodeIdentityMode.All });
|
|
898
1446
|
}
|
|
899
1447
|
/**
|
|
900
1448
|
* Get the sync pointer store.
|
|
901
1449
|
* @returns The sync pointer store.
|
|
902
1450
|
*/
|
|
903
1451
|
async getVerifiableSyncPointerStore() {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
level: "info",
|
|
907
|
-
source: this.CLASS_NAME,
|
|
908
|
-
message: "verifiableSyncPointerStoreRetrieving",
|
|
909
|
-
data: {
|
|
910
|
-
key: this._synchronisedStorageKey
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
const syncPointerStore = await this._verifiableSyncPointerStorageConnector.get(this._synchronisedStorageKey, { includeData: true });
|
|
914
|
-
if (core.Is.uint8Array(syncPointerStore.data)) {
|
|
915
|
-
const syncPointer = core.ObjectHelper.fromBytes(syncPointerStore.data);
|
|
1452
|
+
if (core.Is.stringValue(this._synchronisedStorageKey)) {
|
|
1453
|
+
try {
|
|
916
1454
|
await this._logging?.log({
|
|
917
1455
|
level: "info",
|
|
918
1456
|
source: this.CLASS_NAME,
|
|
919
|
-
message: "
|
|
1457
|
+
message: "verifiableSyncPointerStoreRetrieving",
|
|
920
1458
|
data: {
|
|
921
1459
|
key: this._synchronisedStorageKey
|
|
922
1460
|
}
|
|
923
1461
|
});
|
|
924
|
-
|
|
1462
|
+
const syncPointerStore = await this._verifiableSyncPointerStorageConnector.get(this._synchronisedStorageKey, { includeData: true });
|
|
1463
|
+
if (core.Is.uint8Array(syncPointerStore.data)) {
|
|
1464
|
+
const syncPointer = core.ObjectHelper.fromBytes(syncPointerStore.data);
|
|
1465
|
+
await this._logging?.log({
|
|
1466
|
+
level: "info",
|
|
1467
|
+
source: this.CLASS_NAME,
|
|
1468
|
+
message: "verifiableSyncPointerStoreRetrieved",
|
|
1469
|
+
data: {
|
|
1470
|
+
key: this._synchronisedStorageKey
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
return syncPointer;
|
|
1474
|
+
}
|
|
925
1475
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1476
|
+
catch (err) {
|
|
1477
|
+
if (!core.BaseError.someErrorName(err, core.NotFoundError.CLASS_NAME)) {
|
|
1478
|
+
throw err;
|
|
1479
|
+
}
|
|
930
1480
|
}
|
|
1481
|
+
await this._logging?.log({
|
|
1482
|
+
level: "info",
|
|
1483
|
+
source: this.CLASS_NAME,
|
|
1484
|
+
message: "verifiableSyncPointerStoreNotFound",
|
|
1485
|
+
data: {
|
|
1486
|
+
key: this._synchronisedStorageKey
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
931
1489
|
}
|
|
932
|
-
await this._logging?.log({
|
|
933
|
-
level: "info",
|
|
934
|
-
source: this.CLASS_NAME,
|
|
935
|
-
message: "verifiableSyncPointerStoreNotFound",
|
|
936
|
-
data: {
|
|
937
|
-
key: this._synchronisedStorageKey
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
1490
|
// If no sync pointer store exists, we return an empty one
|
|
941
1491
|
return {
|
|
1492
|
+
version: SYNC_POINTER_STORE_VERSION,
|
|
942
1493
|
syncPointers: {}
|
|
943
1494
|
};
|
|
944
1495
|
}
|
|
@@ -948,7 +1499,7 @@ class RemoteSyncStateHelper {
|
|
|
948
1499
|
* @returns Nothing.
|
|
949
1500
|
*/
|
|
950
1501
|
async storeVerifiableSyncPointerStore(syncPointerStore) {
|
|
951
|
-
if (this._nodeIdentity) {
|
|
1502
|
+
if (core.Is.stringValue(this._nodeIdentity) && core.Is.stringValue(this._synchronisedStorageKey)) {
|
|
952
1503
|
await this._logging?.log({
|
|
953
1504
|
level: "info",
|
|
954
1505
|
source: this.CLASS_NAME,
|
|
@@ -970,39 +1521,55 @@ class RemoteSyncStateHelper {
|
|
|
970
1521
|
await this._logging?.log({
|
|
971
1522
|
level: "info",
|
|
972
1523
|
source: this.CLASS_NAME,
|
|
973
|
-
message: "
|
|
1524
|
+
message: "syncStateStoring",
|
|
974
1525
|
data: {
|
|
975
1526
|
snapshotCount: syncState.snapshots.length
|
|
976
1527
|
}
|
|
977
1528
|
});
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
|
|
1529
|
+
// Limits the number of consolidations in the list so that we can shrink decentralised
|
|
1530
|
+
// storage requirements, sort from newest to oldest so that we can easily find the
|
|
1531
|
+
// oldest snapshots to remove.
|
|
1532
|
+
const snapshots = syncState.snapshots.sort((a, b) => new Date(a.dateCreated).getTime() - new Date(b.dateCreated).getTime());
|
|
1533
|
+
// Find all the consolidation indexes
|
|
1534
|
+
const consolidationIndexes = [];
|
|
1535
|
+
for (let i = 0; i < snapshots.length; i++) {
|
|
1536
|
+
const snapshot = snapshots[i];
|
|
1537
|
+
if (snapshot.isConsolidated) {
|
|
1538
|
+
consolidationIndexes.push(i);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (consolidationIndexes.length > this._maxConsolidations) {
|
|
1542
|
+
// Once we have reached the max for consolidations we need to remove
|
|
1543
|
+
// all the snapshots, including non consolidated ones, beyond this point
|
|
1544
|
+
const toRemove = snapshots.slice(consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1545
|
+
syncState.snapshots = snapshots.slice(0, consolidationIndexes[this._maxConsolidations - 1] + 1);
|
|
1546
|
+
for (const snapshot of toRemove) {
|
|
1547
|
+
await this._blobStorageHelper.removeBlob(snapshot.id);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return this._blobStorageHelper.saveBlob(syncState);
|
|
981
1551
|
}
|
|
982
1552
|
/**
|
|
983
1553
|
* Get the remote sync state.
|
|
984
1554
|
* @param syncPointerId The id of the sync pointer to retrieve the state for.
|
|
985
1555
|
* @returns The remote sync state.
|
|
986
1556
|
*/
|
|
987
|
-
async
|
|
1557
|
+
async getSyncState(syncPointerId) {
|
|
988
1558
|
try {
|
|
989
1559
|
await this._logging?.log({
|
|
990
1560
|
level: "info",
|
|
991
1561
|
source: this.CLASS_NAME,
|
|
992
|
-
message: "
|
|
1562
|
+
message: "syncStateRetrieving",
|
|
993
1563
|
data: {
|
|
994
1564
|
syncPointerId
|
|
995
1565
|
}
|
|
996
1566
|
});
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
}, undefined, this._nodeIdentity);
|
|
1000
|
-
if (core.Is.stringBase64(blobEntry.blob)) {
|
|
1001
|
-
const syncState = core.ObjectHelper.fromBytes(core.Converter.base64ToBytes(blobEntry.blob));
|
|
1567
|
+
const syncState = await this._blobStorageHelper.loadBlob(syncPointerId);
|
|
1568
|
+
if (core.Is.object(syncState)) {
|
|
1002
1569
|
await this._logging?.log({
|
|
1003
1570
|
level: "info",
|
|
1004
1571
|
source: this.CLASS_NAME,
|
|
1005
|
-
message: "
|
|
1572
|
+
message: "syncStateRetrieved",
|
|
1006
1573
|
data: {
|
|
1007
1574
|
syncPointerId,
|
|
1008
1575
|
snapshotCount: syncState.snapshots.length
|
|
@@ -1011,33 +1578,41 @@ class RemoteSyncStateHelper {
|
|
|
1011
1578
|
return syncState;
|
|
1012
1579
|
}
|
|
1013
1580
|
}
|
|
1014
|
-
catch (
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1581
|
+
catch (error) {
|
|
1582
|
+
await this._logging?.log({
|
|
1583
|
+
level: "warn",
|
|
1584
|
+
source: this.CLASS_NAME,
|
|
1585
|
+
message: "getSyncStateError",
|
|
1586
|
+
data: {
|
|
1587
|
+
syncPointerId
|
|
1588
|
+
},
|
|
1589
|
+
error: core.BaseError.fromError(error)
|
|
1590
|
+
});
|
|
1018
1591
|
}
|
|
1019
1592
|
await this._logging?.log({
|
|
1020
1593
|
level: "info",
|
|
1021
1594
|
source: this.CLASS_NAME,
|
|
1022
|
-
message: "
|
|
1595
|
+
message: "syncStateNotFound",
|
|
1023
1596
|
data: {
|
|
1024
1597
|
syncPointerId
|
|
1025
1598
|
}
|
|
1026
1599
|
});
|
|
1027
1600
|
}
|
|
1028
1601
|
/**
|
|
1029
|
-
* Handle the batch response.
|
|
1602
|
+
* Handle the batch response which is triggered from a consolidation request.
|
|
1030
1603
|
* @param response The batch response to handle.
|
|
1031
1604
|
*/
|
|
1032
1605
|
async handleBatchResponse(response) {
|
|
1033
1606
|
if (core.Is.stringValue(this._nodeIdentity)) {
|
|
1607
|
+
const now = new Date(Date.now()).toISOString();
|
|
1034
1608
|
// Create a new snapshot entry for the current batch
|
|
1035
1609
|
const syncChangeSet = {
|
|
1036
1610
|
id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
|
|
1037
|
-
dateCreated:
|
|
1611
|
+
dateCreated: now,
|
|
1612
|
+
dateModified: now,
|
|
1038
1613
|
changes: response.entities.map(change => ({
|
|
1039
1614
|
operation: synchronisedStorageModels.SyncChangeOperation.Set,
|
|
1040
|
-
id: change
|
|
1615
|
+
id: change.id
|
|
1041
1616
|
})),
|
|
1042
1617
|
storageKey: response.storageKey,
|
|
1043
1618
|
nodeIdentity: this._nodeIdentity
|
|
@@ -1045,25 +1620,42 @@ class RemoteSyncStateHelper {
|
|
|
1045
1620
|
// And sign it with the node identity
|
|
1046
1621
|
syncChangeSet.proof = await this._changeSetHelper.createChangeSetProof(syncChangeSet);
|
|
1047
1622
|
// Store the changeset in the blob storage
|
|
1048
|
-
const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet
|
|
1623
|
+
const changeSetStorageId = await this._changeSetHelper.storeChangeSet(syncChangeSet);
|
|
1049
1624
|
// Add the changeset storage id to the snapshot ids
|
|
1050
1625
|
this._batchResponseStorageIds[response.storageKey] ??= [];
|
|
1051
1626
|
this._batchResponseStorageIds[response.storageKey].push(changeSetStorageId);
|
|
1627
|
+
// If this is the last entry in the batch response, we can create the consolidated snapshot
|
|
1052
1628
|
if (response.lastEntry) {
|
|
1053
|
-
|
|
1629
|
+
// Get the current sync pointer store
|
|
1630
|
+
const syncPointerStore = await this.getVerifiableSyncPointerStore();
|
|
1631
|
+
let syncState;
|
|
1632
|
+
if (core.Is.stringValue(syncPointerStore.syncPointers[response.storageKey])) {
|
|
1633
|
+
// If the sync pointer exists, we load the current sync state
|
|
1634
|
+
syncState = await this.getSyncState(syncPointerStore.syncPointers[response.storageKey]);
|
|
1635
|
+
}
|
|
1636
|
+
// If the sync state does not exist, we create a new one
|
|
1637
|
+
syncState ??= {
|
|
1638
|
+
version: SYNC_STATE_VERSION,
|
|
1639
|
+
storageKey: response.storageKey,
|
|
1640
|
+
snapshots: []
|
|
1641
|
+
};
|
|
1054
1642
|
const batchSnapshot = {
|
|
1643
|
+
version: SYNC_SNAPSHOT_VERSION,
|
|
1055
1644
|
id: core.Converter.bytesToHex(core.RandomHelper.generate(32)),
|
|
1056
|
-
dateCreated:
|
|
1645
|
+
dateCreated: now,
|
|
1646
|
+
dateModified: now,
|
|
1647
|
+
isConsolidated: true,
|
|
1057
1648
|
changeSetStorageIds: this._batchResponseStorageIds[response.storageKey]
|
|
1058
1649
|
};
|
|
1059
1650
|
syncState.snapshots.push(batchSnapshot);
|
|
1060
|
-
// Store the sync state
|
|
1651
|
+
// Store the updated sync state
|
|
1061
1652
|
const syncStateId = await this.storeRemoteSyncState(syncState);
|
|
1062
|
-
// Get the current sync pointer store
|
|
1063
|
-
const syncPointerStore = await this.getVerifiableSyncPointerStore();
|
|
1064
1653
|
syncPointerStore.syncPointers[response.storageKey] = syncStateId;
|
|
1065
1654
|
// Store the verifiable sync pointer in the verifiable storage
|
|
1066
1655
|
await this.storeVerifiableSyncPointerStore(syncPointerStore);
|
|
1656
|
+
// Remove the batch response storage ids for the storage key
|
|
1657
|
+
// as we have consolidated the changes
|
|
1658
|
+
delete this._batchResponseStorageIds[response.storageKey];
|
|
1067
1659
|
await this._logging?.log({
|
|
1068
1660
|
level: "info",
|
|
1069
1661
|
source: this.CLASS_NAME,
|
|
@@ -1086,11 +1678,14 @@ class RemoteSyncStateHelper {
|
|
|
1086
1678
|
id: response.id
|
|
1087
1679
|
}
|
|
1088
1680
|
});
|
|
1681
|
+
// We have received a response to an item request, find the right storage
|
|
1682
|
+
// for the request id
|
|
1089
1683
|
if (!core.Is.empty(this._populateFullChanges[response.storageKey])) {
|
|
1090
1684
|
const idx = this._populateFullChanges[response.storageKey].requestIds.indexOf(response.id);
|
|
1091
1685
|
if (idx !== -1) {
|
|
1092
1686
|
this._populateFullChanges[response.storageKey].requestIds.splice(idx, 1);
|
|
1093
1687
|
this._populateFullChanges[response.storageKey].entities[response.id] = response.entity;
|
|
1688
|
+
// If there are no request ids remaining we can complete the population
|
|
1094
1689
|
if (this._populateFullChanges[response.storageKey].requestIds.length === 0) {
|
|
1095
1690
|
await this._populateFullChanges[response.storageKey].completeCallback();
|
|
1096
1691
|
}
|
|
@@ -1118,6 +1713,11 @@ class SynchronisedStorageService {
|
|
|
1118
1713
|
* @internal
|
|
1119
1714
|
*/
|
|
1120
1715
|
static _DEFAULT_CONSOLIDATION_BATCH_SIZE = 100;
|
|
1716
|
+
/**
|
|
1717
|
+
* The default max number of consolidations to keep in storage.
|
|
1718
|
+
* @internal
|
|
1719
|
+
*/
|
|
1720
|
+
static _DEFAULT_MAX_CONSOLIDATIONS = 5;
|
|
1121
1721
|
/**
|
|
1122
1722
|
* Runtime name for the class.
|
|
1123
1723
|
*/
|
|
@@ -1132,16 +1732,21 @@ class SynchronisedStorageService {
|
|
|
1132
1732
|
* @internal
|
|
1133
1733
|
*/
|
|
1134
1734
|
_eventBusComponent;
|
|
1735
|
+
/**
|
|
1736
|
+
* The vault connector.
|
|
1737
|
+
* @internal
|
|
1738
|
+
*/
|
|
1739
|
+
_vaultConnector;
|
|
1135
1740
|
/**
|
|
1136
1741
|
* The storage connector for the sync snapshot entries.
|
|
1137
1742
|
* @internal
|
|
1138
1743
|
*/
|
|
1139
1744
|
_localSyncSnapshotEntryEntityStorage;
|
|
1140
1745
|
/**
|
|
1141
|
-
* The blob storage
|
|
1746
|
+
* The blob storage connector to use for remote sync states.
|
|
1142
1747
|
* @internal
|
|
1143
1748
|
*/
|
|
1144
|
-
|
|
1749
|
+
_blobStorageConnector;
|
|
1145
1750
|
/**
|
|
1146
1751
|
* The verifiable storage connector to use for storing sync pointers.
|
|
1147
1752
|
* @internal
|
|
@@ -1162,6 +1767,11 @@ class SynchronisedStorageService {
|
|
|
1162
1767
|
* @internal
|
|
1163
1768
|
*/
|
|
1164
1769
|
_trustedSynchronisedStorageComponent;
|
|
1770
|
+
/**
|
|
1771
|
+
* The blob storage helper.
|
|
1772
|
+
* @internal
|
|
1773
|
+
*/
|
|
1774
|
+
_blobStorageHelper;
|
|
1165
1775
|
/**
|
|
1166
1776
|
* The change set helper.
|
|
1167
1777
|
* @internal
|
|
@@ -1182,6 +1792,11 @@ class SynchronisedStorageService {
|
|
|
1182
1792
|
* @internal
|
|
1183
1793
|
*/
|
|
1184
1794
|
_config;
|
|
1795
|
+
/**
|
|
1796
|
+
* The synchronised storage key to use for the remote synchronised storage.
|
|
1797
|
+
* @internal
|
|
1798
|
+
*/
|
|
1799
|
+
_synchronisedStorageKey;
|
|
1185
1800
|
/**
|
|
1186
1801
|
* The flag to determine if the service has been started.
|
|
1187
1802
|
* @internal
|
|
@@ -1206,13 +1821,13 @@ class SynchronisedStorageService {
|
|
|
1206
1821
|
core.Guards.object(this.CLASS_NAME, "options.config", options.config);
|
|
1207
1822
|
this._eventBusComponent = core.ComponentFactory.get(options.eventBusComponentType ?? "event-bus");
|
|
1208
1823
|
this._logging = loggingModels.LoggingConnectorFactory.getIfExists(options.loggingConnectorType ?? "logging");
|
|
1824
|
+
this._vaultConnector = vaultModels.VaultConnectorFactory.get(options.vaultConnectorType ?? "vault");
|
|
1209
1825
|
this._localSyncSnapshotEntryEntityStorage = entityStorageModels.EntityStorageConnectorFactory.get(options.syncSnapshotStorageConnectorType ?? "sync-snapshot-entry");
|
|
1210
1826
|
this._verifiableSyncPointerStorageConnector = verifiableStorageModels.VerifiableStorageConnectorFactory.get(options.verifiableStorageConnectorType ?? "verifiable-storage");
|
|
1211
|
-
this.
|
|
1827
|
+
this._blobStorageConnector = blobStorageModels.BlobStorageConnectorFactory.get(options.blobStorageConnectorType ?? "blob-storage");
|
|
1212
1828
|
this._identityConnector = identityModels.IdentityConnectorFactory.get(options.identityConnectorType ?? "identity");
|
|
1213
1829
|
this._taskSchedulerComponent = core.ComponentFactory.get(options.taskSchedulerComponentType ?? "task-scheduler");
|
|
1214
1830
|
this._config = {
|
|
1215
|
-
synchronisedStorageKey: options.config.synchronisedStorageKey,
|
|
1216
1831
|
synchronisedStorageMethodId: options.config.synchronisedStorageMethodId ?? "synchronised-storage-assertion",
|
|
1217
1832
|
entityUpdateIntervalMinutes: options.config.entityUpdateIntervalMinutes ??
|
|
1218
1833
|
SynchronisedStorageService._DEFAULT_ENTITY_UPDATE_INTERVAL_MINUTES,
|
|
@@ -1220,8 +1835,13 @@ class SynchronisedStorageService {
|
|
|
1220
1835
|
consolidationIntervalMinutes: options.config.consolidationIntervalMinutes ??
|
|
1221
1836
|
SynchronisedStorageService._DEFAULT_CONSOLIDATION_INTERVAL_MINUTES,
|
|
1222
1837
|
consolidationBatchSize: options.config.consolidationBatchSize ??
|
|
1223
|
-
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE
|
|
1838
|
+
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE,
|
|
1839
|
+
maxConsolidations: options.config.maxConsolidations ?? SynchronisedStorageService._DEFAULT_MAX_CONSOLIDATIONS,
|
|
1840
|
+
blobStorageEncryptionKeyId: options.config.blobStorageEncryptionKeyId ?? "synchronised-storage-blob-encryption-key",
|
|
1841
|
+
verifiableStorageKeyId: options.config.verifiableStorageKeyId
|
|
1224
1842
|
};
|
|
1843
|
+
this._synchronisedStorageKey =
|
|
1844
|
+
verifiableStorageKeys[options.config.verifiableStorageKeyId] ?? options.config.verifiableStorageKeyId;
|
|
1225
1845
|
// If this is not a trusted node, we need to use a synchronised storage service
|
|
1226
1846
|
// to synchronise with a trusted node.
|
|
1227
1847
|
if (!this._config.isTrustedNode) {
|
|
@@ -1229,13 +1849,19 @@ class SynchronisedStorageService {
|
|
|
1229
1849
|
this._trustedSynchronisedStorageComponent =
|
|
1230
1850
|
core.ComponentFactory.get(options.trustedSynchronisedStorageComponentType);
|
|
1231
1851
|
}
|
|
1232
|
-
this.
|
|
1852
|
+
this._blobStorageHelper = new BlobStorageHelper(this._logging, this._vaultConnector, this._blobStorageConnector, this._config.blobStorageEncryptionKeyId, this._config.isTrustedNode);
|
|
1853
|
+
this._changeSetHelper = new ChangeSetHelper(this._logging, this._eventBusComponent, this._identityConnector, this._blobStorageHelper, this._config.synchronisedStorageMethodId);
|
|
1233
1854
|
this._localSyncStateHelper = new LocalSyncStateHelper(this._logging, this._localSyncSnapshotEntryEntityStorage, this._changeSetHelper);
|
|
1234
|
-
this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this.
|
|
1855
|
+
this._remoteSyncStateHelper = new RemoteSyncStateHelper(this._logging, this._eventBusComponent, this._verifiableSyncPointerStorageConnector, this._blobStorageHelper, this._changeSetHelper, this._config.isTrustedNode, this._config.maxConsolidations);
|
|
1235
1856
|
this._serviceStarted = false;
|
|
1236
1857
|
this._activeStorageKeys = {};
|
|
1237
|
-
this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.
|
|
1238
|
-
this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.LocalItemChange, async (event) =>
|
|
1858
|
+
this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.RegisterStorageKey, async (event) => this.registerStorageKey(event.data));
|
|
1859
|
+
this._eventBusComponent.subscribe(synchronisedStorageModels.SynchronisedStorageTopics.LocalItemChange, async (event) => {
|
|
1860
|
+
// Make sure the change event is from this node
|
|
1861
|
+
if (core.Is.stringValue(this._nodeIdentity) && this._nodeIdentity === event.data.nodeIdentity) {
|
|
1862
|
+
await this._localSyncStateHelper.addLocalChange(event.data.storageKey, event.data.operation, event.data.id);
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1239
1865
|
}
|
|
1240
1866
|
/**
|
|
1241
1867
|
* The component needs to be started when the node is initialized.
|
|
@@ -1247,7 +1873,16 @@ class SynchronisedStorageService {
|
|
|
1247
1873
|
async start(nodeIdentity, nodeLoggingConnectorType, componentState) {
|
|
1248
1874
|
this._nodeIdentity = nodeIdentity;
|
|
1249
1875
|
this._remoteSyncStateHelper.setNodeIdentity(nodeIdentity);
|
|
1876
|
+
this._changeSetHelper.setNodeIdentity(nodeIdentity);
|
|
1877
|
+
this._remoteSyncStateHelper.setSynchronisedStorageKey(this._synchronisedStorageKey);
|
|
1250
1878
|
this._serviceStarted = true;
|
|
1879
|
+
// If this is not a trusted node we need to request the decryption key from a trusted node
|
|
1880
|
+
if (!this._config.isTrustedNode && !core.Is.empty(this._trustedSynchronisedStorageComponent)) {
|
|
1881
|
+
const proof = await this._identityConnector.createProof(this._nodeIdentity, identityModels.DocumentHelper.joinId(this._nodeIdentity, this._config.synchronisedStorageMethodId), standardsW3cDid.ProofTypes.DataIntegrityProof, { nodeIdentity });
|
|
1882
|
+
const decryptionKey = await this._trustedSynchronisedStorageComponent.getDecryptionKey(this._nodeIdentity, proof);
|
|
1883
|
+
// We don't have the private key so instead we store the key as a secret in the vault
|
|
1884
|
+
await this._vaultConnector.setSecret(this._config.blobStorageEncryptionKeyId, decryptionKey);
|
|
1885
|
+
}
|
|
1251
1886
|
// If there are already storage keys registered, we need to activate them
|
|
1252
1887
|
for (const storageKey in this._activeStorageKeys) {
|
|
1253
1888
|
await this.activateStorageKey(storageKey);
|
|
@@ -1268,24 +1903,59 @@ class SynchronisedStorageService {
|
|
|
1268
1903
|
}
|
|
1269
1904
|
}
|
|
1270
1905
|
/**
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1906
|
+
* Get the decryption key for the synchronised storage.
|
|
1907
|
+
* This is used to decrypt the data stored in the synchronised storage.
|
|
1908
|
+
* @param nodeIdentity The identity of the node requesting the decryption key.
|
|
1909
|
+
* @param proof The proof of the request so we know the request is from the specified node.
|
|
1910
|
+
* @returns The decryption key.
|
|
1911
|
+
*/
|
|
1912
|
+
async getDecryptionKey(nodeIdentity, proof) {
|
|
1913
|
+
if (!this._config.isTrustedNode) {
|
|
1914
|
+
throw new core.GeneralError(this.CLASS_NAME, "notTrustedNode");
|
|
1915
|
+
}
|
|
1916
|
+
core.Guards.stringValue(this.CLASS_NAME, "nodeIdentity", nodeIdentity);
|
|
1917
|
+
core.Guards.object(this.CLASS_NAME, "proof", proof);
|
|
1918
|
+
const isValid = await this._identityConnector.verifyProof({ nodeIdentity }, proof);
|
|
1919
|
+
if (!isValid) {
|
|
1920
|
+
throw new core.UnauthorizedError(this.CLASS_NAME, "invalidProof");
|
|
1921
|
+
}
|
|
1922
|
+
// TODO: We need to check if the node has permissions to access the decryption key
|
|
1923
|
+
// using rights-management
|
|
1924
|
+
const key = await this._vaultConnector.getKey(this._config.blobStorageEncryptionKeyId);
|
|
1925
|
+
if (core.Is.undefined(key.publicKey)) {
|
|
1926
|
+
throw new core.UnauthorizedError(this.CLASS_NAME, "decryptionKeyNotFound");
|
|
1927
|
+
}
|
|
1928
|
+
return core.Converter.bytesToBase64(key.publicKey);
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Synchronise a set of changes from an untrusted node, assumes this is a trusted node.
|
|
1932
|
+
* @param syncChangeSet The change set to synchronise.
|
|
1273
1933
|
* @returns Nothing.
|
|
1274
1934
|
*/
|
|
1275
|
-
async syncChangeSet(
|
|
1935
|
+
async syncChangeSet(syncChangeSet) {
|
|
1276
1936
|
if (!this._config.isTrustedNode) {
|
|
1277
1937
|
throw new core.GeneralError(this.CLASS_NAME, "notTrustedNode");
|
|
1278
1938
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1939
|
+
core.Guards.object(this.CLASS_NAME, "syncChangeSet", syncChangeSet);
|
|
1940
|
+
await this._logging?.log({
|
|
1941
|
+
level: "info",
|
|
1942
|
+
source: this.CLASS_NAME,
|
|
1943
|
+
message: "syncChangeSetForRemoteNode",
|
|
1944
|
+
data: {
|
|
1945
|
+
changeSetStorageId: syncChangeSet.id
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1281
1948
|
// TODO: The change set has a proof signed by the originating node identity
|
|
1282
1949
|
// The proof is verified that the change set is valid and has not been tampered with.
|
|
1283
1950
|
// but we also need to check that the originating node has permissions
|
|
1284
1951
|
// to store the change set in the synchronised storage.
|
|
1285
1952
|
// This will be performed using rights-management
|
|
1286
|
-
const
|
|
1287
|
-
if (!core.Is.empty(
|
|
1288
|
-
|
|
1953
|
+
const copy = await this._changeSetHelper.copyChangeset(syncChangeSet);
|
|
1954
|
+
if (!core.Is.empty(copy)) {
|
|
1955
|
+
// Apply the changes to this node
|
|
1956
|
+
await this._changeSetHelper.applyChangeset(copy.syncChangeSet);
|
|
1957
|
+
// And update the sync state with the latest changes
|
|
1958
|
+
await this._remoteSyncStateHelper.addChangeSetToSyncState(copy.syncChangeSet.storageKey, copy.changeSetStorageId);
|
|
1289
1959
|
}
|
|
1290
1960
|
}
|
|
1291
1961
|
/**
|
|
@@ -1338,10 +2008,10 @@ class SynchronisedStorageService {
|
|
|
1338
2008
|
if (!core.Is.empty(verifiableSyncPointerStore.syncPointers[storageKey])) {
|
|
1339
2009
|
// Load the sync state from the remote blob storage using the sync pointer
|
|
1340
2010
|
// to load the sync state
|
|
1341
|
-
const remoteSyncState = await this._remoteSyncStateHelper.
|
|
2011
|
+
const remoteSyncState = await this._remoteSyncStateHelper.getSyncState(verifiableSyncPointerStore.syncPointers[storageKey]);
|
|
1342
2012
|
// If we got the sync state we can try and sync from it
|
|
1343
2013
|
if (!core.Is.undefined(remoteSyncState)) {
|
|
1344
|
-
await this._localSyncStateHelper.
|
|
2014
|
+
await this._localSyncStateHelper.applySyncState(storageKey, remoteSyncState);
|
|
1345
2015
|
}
|
|
1346
2016
|
}
|
|
1347
2017
|
}
|
|
@@ -1359,51 +2029,67 @@ class SynchronisedStorageService {
|
|
|
1359
2029
|
storageKey
|
|
1360
2030
|
}
|
|
1361
2031
|
});
|
|
1362
|
-
const
|
|
1363
|
-
if (
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
2032
|
+
const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
|
|
2033
|
+
if (localChangeSnapshots.length > 0) {
|
|
2034
|
+
const localChangeSnapshot = localChangeSnapshots[0];
|
|
2035
|
+
if (core.Is.arrayValue(localChangeSnapshot.changes)) {
|
|
2036
|
+
await this._remoteSyncStateHelper.buildChangeSet(storageKey, localChangeSnapshot.changes, async (syncChangeSet, changeSetStorageId) => {
|
|
2037
|
+
if (core.Is.empty(syncChangeSet) && core.Is.empty(changeSetStorageId)) {
|
|
2038
|
+
await this._logging?.log({
|
|
2039
|
+
level: "info",
|
|
2040
|
+
source: this.CLASS_NAME,
|
|
2041
|
+
message: "builtStorageChangeSetNone",
|
|
2042
|
+
data: {
|
|
2043
|
+
storageKey
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
else {
|
|
2048
|
+
await this._logging?.log({
|
|
2049
|
+
level: "info",
|
|
2050
|
+
source: this.CLASS_NAME,
|
|
2051
|
+
message: "builtStorageChangeSet",
|
|
2052
|
+
data: {
|
|
2053
|
+
storageKey,
|
|
2054
|
+
changeSetStorageId
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
// Send the local changes to the remote storage if we are a trusted node
|
|
2058
|
+
if (this._config.isTrustedNode && core.Is.stringValue(changeSetStorageId)) {
|
|
2059
|
+
// If we are a trusted node, we can add the change set to the sync state
|
|
2060
|
+
// and remove the local change snapshot
|
|
2061
|
+
await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
|
|
2062
|
+
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
2063
|
+
}
|
|
2064
|
+
else if (!core.Is.empty(this._trustedSynchronisedStorageComponent) &&
|
|
2065
|
+
core.Is.object(syncChangeSet)) {
|
|
2066
|
+
// If we are not a trusted node, we need to send the changes to the trusted node
|
|
2067
|
+
// and then remove the local change snapshot
|
|
2068
|
+
await this._logging?.log({
|
|
2069
|
+
level: "info",
|
|
2070
|
+
source: this.CLASS_NAME,
|
|
2071
|
+
message: "sendingChangeSetToTrustedNode",
|
|
2072
|
+
data: {
|
|
2073
|
+
storageKey,
|
|
2074
|
+
changeSetStorageId
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
await this._trustedSynchronisedStorageComponent.syncChangeSet(syncChangeSet);
|
|
2078
|
+
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
1373
2079
|
}
|
|
1374
|
-
});
|
|
1375
|
-
// Send the local changes to the remote storage if we are a trusted node
|
|
1376
|
-
if (this._config.isTrustedNode) {
|
|
1377
|
-
await this._remoteSyncStateHelper.addChangeSetToSyncState(storageKey, changeSetStorageId);
|
|
1378
|
-
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
1379
2080
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
else {
|
|
2084
|
+
await this._logging?.log({
|
|
2085
|
+
level: "info",
|
|
2086
|
+
source: this.CLASS_NAME,
|
|
2087
|
+
message: "updateFromLocalSyncStateNoChanges",
|
|
2088
|
+
data: {
|
|
2089
|
+
storageKey
|
|
1384
2090
|
}
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
await this._logging?.log({
|
|
1388
|
-
level: "info",
|
|
1389
|
-
source: this.CLASS_NAME,
|
|
1390
|
-
message: "createdStorageChangeSetNone",
|
|
1391
|
-
data: {
|
|
1392
|
-
storageKey
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
else {
|
|
1399
|
-
await this._logging?.log({
|
|
1400
|
-
level: "info",
|
|
1401
|
-
source: this.CLASS_NAME,
|
|
1402
|
-
message: "updateFromLocalSyncStateNoChanges",
|
|
1403
|
-
data: {
|
|
1404
|
-
storageKey
|
|
1405
|
-
}
|
|
1406
|
-
});
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
1407
2093
|
}
|
|
1408
2094
|
}
|
|
1409
2095
|
/**
|
|
@@ -1415,17 +2101,17 @@ class SynchronisedStorageService {
|
|
|
1415
2101
|
async startConsolidationSync(storageKey) {
|
|
1416
2102
|
let localChangeSnapshot;
|
|
1417
2103
|
try {
|
|
1418
|
-
// If we are performing a consolidation, we can remove the local
|
|
1419
|
-
|
|
2104
|
+
// If we are performing a consolidation, we can remove the local change snapshot
|
|
2105
|
+
// as we are going to create a complete changeset from the DB
|
|
2106
|
+
const localChangeSnapshots = await this._localSyncStateHelper.getSnapshots(storageKey, true);
|
|
2107
|
+
localChangeSnapshot = localChangeSnapshots[0];
|
|
1420
2108
|
if (!core.Is.empty(localChangeSnapshot)) {
|
|
1421
2109
|
await this._localSyncStateHelper.removeLocalChangeSnapshot(localChangeSnapshot);
|
|
1422
2110
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
localChangeSnapshot = undefined;
|
|
1428
|
-
}
|
|
2111
|
+
await this._remoteSyncStateHelper.consolidationStart(storageKey, this._config.consolidationBatchSize ??
|
|
2112
|
+
SynchronisedStorageService._DEFAULT_CONSOLIDATION_BATCH_SIZE);
|
|
2113
|
+
// The consolidation was successful, so we can remove the local change snapshot permanently
|
|
2114
|
+
localChangeSnapshot = undefined;
|
|
1429
2115
|
}
|
|
1430
2116
|
catch (error) {
|
|
1431
2117
|
if (localChangeSnapshot) {
|
|
@@ -1442,22 +2128,22 @@ class SynchronisedStorageService {
|
|
|
1442
2128
|
}
|
|
1443
2129
|
/**
|
|
1444
2130
|
* Register a new sync type.
|
|
1445
|
-
* @param
|
|
2131
|
+
* @param syncRegisterStorageKey The sync register type to register.
|
|
1446
2132
|
* @internal
|
|
1447
2133
|
*/
|
|
1448
|
-
async
|
|
2134
|
+
async registerStorageKey(syncRegisterStorageKey) {
|
|
1449
2135
|
await this._logging?.log({
|
|
1450
2136
|
level: "info",
|
|
1451
2137
|
source: this.CLASS_NAME,
|
|
1452
|
-
message: "
|
|
2138
|
+
message: "registerStorageKey",
|
|
1453
2139
|
data: {
|
|
1454
|
-
storageKey:
|
|
2140
|
+
storageKey: syncRegisterStorageKey.storageKey
|
|
1455
2141
|
}
|
|
1456
2142
|
});
|
|
1457
|
-
if (core.Is.empty(this._activeStorageKeys[
|
|
1458
|
-
this._activeStorageKeys[
|
|
2143
|
+
if (core.Is.empty(this._activeStorageKeys[syncRegisterStorageKey.storageKey])) {
|
|
2144
|
+
this._activeStorageKeys[syncRegisterStorageKey.storageKey] = false;
|
|
1459
2145
|
if (this._serviceStarted) {
|
|
1460
|
-
await this.activateStorageKey(
|
|
2146
|
+
await this.activateStorageKey(syncRegisterStorageKey.storageKey);
|
|
1461
2147
|
}
|
|
1462
2148
|
}
|
|
1463
2149
|
}
|
|
@@ -1471,7 +2157,7 @@ class SynchronisedStorageService {
|
|
|
1471
2157
|
await this._logging?.log({
|
|
1472
2158
|
level: "info",
|
|
1473
2159
|
source: this.CLASS_NAME,
|
|
1474
|
-
message: "
|
|
2160
|
+
message: "activateStorageKey",
|
|
1475
2161
|
data: {
|
|
1476
2162
|
storageKey
|
|
1477
2163
|
}
|
|
@@ -1501,5 +2187,6 @@ exports.SynchronisedStorageService = SynchronisedStorageService;
|
|
|
1501
2187
|
exports.generateRestRoutesSynchronisedStorage = generateRestRoutesSynchronisedStorage;
|
|
1502
2188
|
exports.initSchema = initSchema;
|
|
1503
2189
|
exports.restEntryPoints = restEntryPoints;
|
|
2190
|
+
exports.synchronisedStorageGetDecryptionKeyRequest = synchronisedStorageGetDecryptionKeyRequest;
|
|
1504
2191
|
exports.synchronisedStorageSyncChangeSetRequest = synchronisedStorageSyncChangeSetRequest;
|
|
1505
2192
|
exports.tagsSynchronisedStorage = tagsSynchronisedStorage;
|