@vrtmrz/livesync-commonlib 0.1.18 → 0.1.19
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/API/DirectFileManipulatorV2.js +3 -1
- package/dist/common/models/fileaccess.type.d.ts +7 -0
- package/dist/interfaces/DatabaseRebuilder.d.ts +4 -2
- package/dist/managers/StorageEventManager.d.ts +16 -5
- package/dist/managers/StorageEventManager.js +46 -8
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
- package/dist/pouchdb/LiveSyncLocalDB.js +120 -38
- package/dist/serviceFeatures/prepareDatabaseForUse.js +18 -15
- package/dist/serviceModules/Rebuilder.d.ts +27 -4
- package/dist/serviceModules/Rebuilder.js +113 -35
- package/dist/serviceModules/ServiceFileHandlerBase.d.ts +17 -0
- package/dist/serviceModules/ServiceFileHandlerBase.js +145 -9
- package/dist/services/base/DatabaseService.d.ts +11 -3
- package/dist/services/base/DatabaseService.js +63 -7
- package/dist/services/base/IService.d.ts +1 -0
- package/dist/services/base/ReplicationService.d.ts +16 -0
- package/dist/services/base/ReplicationService.js +36 -4
- package/dist/services/base/ReplicatorService.d.ts +1 -0
- package/dist/services/base/ReplicatorService.js +32 -13
- package/docs/development.md +2 -0
- package/docs/proven-in-use.md +3 -1
- package/docs/releasing.md +22 -0
- package/package.json +1 -1
|
@@ -26,6 +26,7 @@ class ServiceRebuilder extends ServiceModuleBase {
|
|
|
26
26
|
replication;
|
|
27
27
|
database;
|
|
28
28
|
fileHandler;
|
|
29
|
+
fileProcessing;
|
|
29
30
|
control;
|
|
30
31
|
constructor(services) {
|
|
31
32
|
super(services);
|
|
@@ -42,6 +43,7 @@ class ServiceRebuilder extends ServiceModuleBase {
|
|
|
42
43
|
this.replication = services.replication;
|
|
43
44
|
this.database = services.database;
|
|
44
45
|
this.fileHandler = services.fileHandler;
|
|
46
|
+
this.fileProcessing = services.fileProcessing;
|
|
45
47
|
this.control = services.control;
|
|
46
48
|
services.database.onDatabaseReset.addHandler(this._onResetLocalDatabase.bind(this));
|
|
47
49
|
services.setting.suspendAllSync.addHandler(this._allSuspendAllSync.bind(this));
|
|
@@ -100,6 +102,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
100
102
|
await this.informOptionalFeatures();
|
|
101
103
|
}
|
|
102
104
|
async performRebuildEverything() {
|
|
105
|
+
this.appLifecycle.resetIsReady();
|
|
103
106
|
await this.setting.suspendExtraSync();
|
|
104
107
|
await this.setting.applyPartial({
|
|
105
108
|
isConfigured: true,
|
|
@@ -108,8 +111,11 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
108
111
|
await this.control.applySettings();
|
|
109
112
|
await this.resetLocalDatabase();
|
|
110
113
|
await delay(1e3);
|
|
111
|
-
await this.
|
|
114
|
+
await this.prepareLocalDatabaseForRebuild();
|
|
112
115
|
if (this.setting.currentSettings().remoteType === REMOTE_P2P) {
|
|
116
|
+
if (!await this.completePreparedRebuild()) {
|
|
117
|
+
throw new Error("The local P2P rebuild could not be finalised.");
|
|
118
|
+
}
|
|
113
119
|
return;
|
|
114
120
|
}
|
|
115
121
|
await this.replication.markLocked();
|
|
@@ -117,9 +123,16 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
117
123
|
await this.replication.markLocked();
|
|
118
124
|
await delay(500);
|
|
119
125
|
await delay(1e3);
|
|
120
|
-
await this.replication.
|
|
126
|
+
if (!await this.replication.replicateAllToRemoteForRebuild(true)) {
|
|
127
|
+
throw new Error("The first rebuild upload did not complete.");
|
|
128
|
+
}
|
|
121
129
|
await delay(1e3);
|
|
122
|
-
await this.replication.
|
|
130
|
+
if (!await this.replication.replicateAllToRemoteForRebuild(true)) {
|
|
131
|
+
throw new Error("The final rebuild upload did not complete.");
|
|
132
|
+
}
|
|
133
|
+
if (!await this.completePreparedRebuild()) {
|
|
134
|
+
throw new Error("The rebuild could not be finalised.");
|
|
135
|
+
}
|
|
123
136
|
}
|
|
124
137
|
$rebuildEverything() {
|
|
125
138
|
return this.rebuildEverything();
|
|
@@ -208,18 +221,51 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
208
221
|
});
|
|
209
222
|
await this.setting.saveSettingData();
|
|
210
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Stage reflection resumption and perform the final reconciliation checks.
|
|
226
|
+
*
|
|
227
|
+
* File watching is resumed in memory before the final scan so that changes
|
|
228
|
+
* made while the scan is running can enter the storage-event queue. This
|
|
229
|
+
* phase deliberately does not persist the resumed settings. The owning
|
|
230
|
+
* completion boundary persists them only after the scan, replication
|
|
231
|
+
* pre-check, and current batch-wait release have succeeded.
|
|
232
|
+
*
|
|
233
|
+
* @param ignoreMinIO Whether to resume reflection for a MinIO remote.
|
|
234
|
+
* @returns `true` when the applicable reconciliation checks succeeded.
|
|
235
|
+
*/
|
|
211
236
|
async resumeReflectingDatabase(ignoreMinIO = false) {
|
|
212
237
|
const settings = this.setting.currentSettings();
|
|
213
|
-
if (settings.doNotSuspendOnFetching) return;
|
|
214
|
-
if (!ignoreMinIO && settings.remoteType == REMOTE_MINIO) return;
|
|
238
|
+
if (settings.doNotSuspendOnFetching) return true;
|
|
239
|
+
if (!ignoreMinIO && settings.remoteType == REMOTE_MINIO) return true;
|
|
215
240
|
this._log(`Database and storage reflection has been resumed!`, LOG_LEVEL_NOTICE);
|
|
216
241
|
await this.setting.applyPartial({
|
|
217
242
|
suspendParseReplicationResult: false,
|
|
218
243
|
suspendFileWatching: false
|
|
219
244
|
});
|
|
220
|
-
await this.vault.scanVault(true);
|
|
221
|
-
await this.replication.onBeforeReplicate(false);
|
|
222
|
-
|
|
245
|
+
if (!await this.vault.scanVault(true)) return false;
|
|
246
|
+
if (!await this.replication.onBeforeReplicate(false)) return false;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
async prepareLocalDatabaseForRebuild() {
|
|
250
|
+
if (!this.database.isDatabaseReady()) {
|
|
251
|
+
throw new Error("The selected local database is not ready for rebuild preparation.");
|
|
252
|
+
}
|
|
253
|
+
if (!await this.vault.scanVault(true, true)) {
|
|
254
|
+
throw new Error("The Vault could not be scanned for rebuild preparation.");
|
|
255
|
+
}
|
|
256
|
+
if (!await this.databaseEvents.onDatabaseInitialised(true)) {
|
|
257
|
+
throw new Error("The local database completion hooks failed during rebuild preparation.");
|
|
258
|
+
}
|
|
259
|
+
if (!await this.fileProcessing.commitPendingFileEvents()) {
|
|
260
|
+
throw new Error("The current file-event batch could not be released for rebuild preparation.");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async completePreparedRebuild() {
|
|
264
|
+
this.appLifecycle.resetIsReady();
|
|
265
|
+
if (!this.database.isDatabaseReady()) return false;
|
|
266
|
+
if (!await this.fileProcessing.commitPendingFileEvents()) return false;
|
|
267
|
+
this.appLifecycle.markIsReady();
|
|
268
|
+
return true;
|
|
223
269
|
}
|
|
224
270
|
async fetchLocal(makeLocalChunkBeforeSync, preventMakeLocalFilesBeforeSync, autoResume = true) {
|
|
225
271
|
await this.setting.suspendExtraSync();
|
|
@@ -259,31 +305,33 @@ Are you sure you wish to proceed?`;
|
|
|
259
305
|
);
|
|
260
306
|
}
|
|
261
307
|
async performFetchLocal(makeLocalChunkBeforeSync, preventMakeLocalFilesBeforeSync, autoResume = true) {
|
|
308
|
+
this.appLifecycle.resetIsReady();
|
|
262
309
|
await this.suspendReflectingDatabase(!autoResume);
|
|
263
310
|
await this.control.applySettings();
|
|
264
311
|
await this.resetLocalDatabase();
|
|
265
312
|
this.clearFastFetchCheckpoint();
|
|
266
313
|
await delay(1e3);
|
|
267
|
-
await this.database.openDatabase({
|
|
268
|
-
databaseEvents: this.databaseEvents,
|
|
269
|
-
replicator: this.replicator
|
|
270
|
-
});
|
|
271
|
-
this.appLifecycle.markIsReady();
|
|
272
314
|
if (makeLocalChunkBeforeSync) {
|
|
273
315
|
await this.fileHandler.createAllChunks(true);
|
|
274
316
|
} else if (!preventMakeLocalFilesBeforeSync) {
|
|
275
|
-
await this.
|
|
317
|
+
await this.prepareLocalDatabaseForRebuild();
|
|
276
318
|
} else {
|
|
277
319
|
}
|
|
278
320
|
await this.replication.markResolved();
|
|
279
321
|
await delay(500);
|
|
280
|
-
await this.replication.
|
|
322
|
+
if (!await this.replication.replicateAllFromRemoteForRebuild(true)) {
|
|
323
|
+
throw new Error("The first Standard Fetch pass did not complete.");
|
|
324
|
+
}
|
|
281
325
|
if (this.setting.currentSettings().remoteType !== REMOTE_P2P) {
|
|
282
326
|
await delay(1e3);
|
|
283
|
-
await this.replication.
|
|
327
|
+
if (!await this.replication.replicateAllFromRemoteForRebuild(true)) {
|
|
328
|
+
throw new Error("The final Standard Fetch pass did not complete.");
|
|
329
|
+
}
|
|
284
330
|
}
|
|
285
331
|
if (autoResume) {
|
|
286
|
-
await this.finishRebuild()
|
|
332
|
+
if (!await this.finishRebuild()) {
|
|
333
|
+
throw new Error("Standard Fetch could not be finalised.");
|
|
334
|
+
}
|
|
287
335
|
}
|
|
288
336
|
}
|
|
289
337
|
async fetchLocalDBFast(autoResume) {
|
|
@@ -314,6 +362,7 @@ Are you sure you wish to proceed?`;
|
|
|
314
362
|
});
|
|
315
363
|
}
|
|
316
364
|
async performFetchLocalDBFast(settings, autoResume) {
|
|
365
|
+
this.appLifecycle.resetIsReady();
|
|
317
366
|
const remote = settings.couchDB_URI.replace(/\/+$/, "") + (settings.couchDB_DBNAME == "" ? "" : "/" + settings.couchDB_DBNAME);
|
|
318
367
|
let checkpoint = this.getFastFetchCheckpoint(remote);
|
|
319
368
|
await this.suspendReflectingDatabase();
|
|
@@ -325,15 +374,16 @@ Are you sure you wish to proceed?`;
|
|
|
325
374
|
LOG_LEVEL_NOTICE,
|
|
326
375
|
"fetch-init-resume"
|
|
327
376
|
);
|
|
377
|
+
if (!await this.database.openDatabase({
|
|
378
|
+
databaseEvents: this.databaseEvents,
|
|
379
|
+
replicator: this.replicator
|
|
380
|
+
})) {
|
|
381
|
+
throw new Error("The selected local database could not be opened for Fast Fetch.");
|
|
382
|
+
}
|
|
328
383
|
} else {
|
|
329
384
|
await this.resetLocalDatabase();
|
|
330
385
|
await delay(1e3);
|
|
331
386
|
}
|
|
332
|
-
await this.database.openDatabase({
|
|
333
|
-
databaseEvents: this.databaseEvents,
|
|
334
|
-
replicator: this.replicator
|
|
335
|
-
});
|
|
336
|
-
this.appLifecycle.markIsReady();
|
|
337
387
|
let localDB = this.database.localDatabase.localDatabase;
|
|
338
388
|
if (checkpoint && (await localDB.info()).doc_count == 0) {
|
|
339
389
|
this._log(
|
|
@@ -344,10 +394,6 @@ Are you sure you wish to proceed?`;
|
|
|
344
394
|
this.clearFastFetchCheckpoint();
|
|
345
395
|
await this.resetLocalDatabase();
|
|
346
396
|
await delay(1e3);
|
|
347
|
-
await this.database.openDatabase({
|
|
348
|
-
databaseEvents: this.databaseEvents,
|
|
349
|
-
replicator: this.replicator
|
|
350
|
-
});
|
|
351
397
|
localDB = this.database.localDatabase.localDatabase;
|
|
352
398
|
since = "0";
|
|
353
399
|
}
|
|
@@ -407,17 +453,47 @@ Total bytes fetched: ${sizeToHumanReadable(progress.totalBytes)}`,
|
|
|
407
453
|
);
|
|
408
454
|
await this.replication.markResolved();
|
|
409
455
|
if (autoResume) {
|
|
410
|
-
await this.
|
|
456
|
+
if (!await this.finishRebuild()) {
|
|
457
|
+
throw new Error("Fast Fetch could not be finalised.");
|
|
458
|
+
}
|
|
411
459
|
}
|
|
412
460
|
this.clearFastFetchCheckpoint();
|
|
413
461
|
}
|
|
414
462
|
/**
|
|
415
|
-
*
|
|
463
|
+
* Complete the final rebuild phases and establish application readiness.
|
|
416
464
|
*
|
|
417
|
-
*
|
|
465
|
+
* A failed completion restores the previous reflection settings in memory.
|
|
466
|
+
* Persistence atomicity remains a responsibility of the host setting store;
|
|
467
|
+
* Commonlib cannot roll back a store which writes successfully and then
|
|
468
|
+
* reports an error.
|
|
469
|
+
*
|
|
470
|
+
* @param ignoreMinIO Whether to resume reflection for a MinIO remote.
|
|
471
|
+
* @returns `true` when finalisation completed; otherwise, `false`.
|
|
418
472
|
*/
|
|
419
473
|
async finishRebuild(ignoreMinIO = true) {
|
|
420
|
-
|
|
474
|
+
this.appLifecycle.resetIsReady();
|
|
475
|
+
const settings = this.setting.currentSettings();
|
|
476
|
+
const controlsReflection = !settings.doNotSuspendOnFetching && (ignoreMinIO || settings.remoteType !== REMOTE_MINIO);
|
|
477
|
+
const previousReflectionState = {
|
|
478
|
+
suspendParseReplicationResult: settings.suspendParseReplicationResult,
|
|
479
|
+
suspendFileWatching: settings.suspendFileWatching
|
|
480
|
+
};
|
|
481
|
+
let completed = false;
|
|
482
|
+
try {
|
|
483
|
+
if (!this.database.isDatabaseReady()) return false;
|
|
484
|
+
if (!await this.resumeReflectingDatabase(ignoreMinIO)) return false;
|
|
485
|
+
if (!await this.fileProcessing.commitPendingFileEvents()) return false;
|
|
486
|
+
if (controlsReflection) {
|
|
487
|
+
await this.setting.saveSettingData();
|
|
488
|
+
}
|
|
489
|
+
this.appLifecycle.markIsReady();
|
|
490
|
+
completed = true;
|
|
491
|
+
return true;
|
|
492
|
+
} finally {
|
|
493
|
+
if (!completed && controlsReflection) {
|
|
494
|
+
await this.setting.applyPartial(previousReflectionState);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
421
497
|
}
|
|
422
498
|
/**
|
|
423
499
|
* Fetch local database with making all chunks.
|
|
@@ -433,13 +509,15 @@ Total bytes fetched: ${sizeToHumanReadable(progress.totalBytes)}`,
|
|
|
433
509
|
return true;
|
|
434
510
|
}
|
|
435
511
|
async resetLocalDatabase() {
|
|
436
|
-
const settings = this.setting.currentSettings();
|
|
437
|
-
if (settings.isConfigured && settings.additionalSuffixOfDatabaseName == "") {
|
|
438
|
-
await this.database.resetDatabase();
|
|
439
|
-
}
|
|
440
512
|
const suffix = this.API.getAppID() || "";
|
|
441
513
|
await this.setting.applyPartial({ additionalSuffixOfDatabaseName: suffix });
|
|
442
|
-
await this.database.
|
|
514
|
+
const reset = await this.database.resetDatabaseForCurrentSettings({
|
|
515
|
+
databaseEvents: this.databaseEvents,
|
|
516
|
+
replicator: this.replicator
|
|
517
|
+
});
|
|
518
|
+
if (!reset) {
|
|
519
|
+
throw new Error("The local database selected by the current settings could not be reset.");
|
|
520
|
+
}
|
|
443
521
|
this.events.emitEvent(EVENT_DATABASE_REBUILT);
|
|
444
522
|
}
|
|
445
523
|
getFastFetchCheckpoint(remote) {
|
|
@@ -68,6 +68,23 @@ export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase<S
|
|
|
68
68
|
private applyDatabaseEntryToStorage;
|
|
69
69
|
private preserveUnsyncedStorageAsConflict;
|
|
70
70
|
private _anyHandlerProcessesFileEvent;
|
|
71
|
+
private serializedByFileEventPaths;
|
|
72
|
+
/**
|
|
73
|
+
* Revalidate a persisted storage operation against the current storage state.
|
|
74
|
+
*
|
|
75
|
+
* Snapshot entries preserve operation intent and ordering only. Their file
|
|
76
|
+
* stub, timestamps, and existence assumptions can be stale after a restart.
|
|
77
|
+
* Current inclusion is therefore read from storage, while destructive work
|
|
78
|
+
* is allowed only after current absence has been observed.
|
|
79
|
+
*/
|
|
80
|
+
private _processRestoredFileEvent;
|
|
81
|
+
private _planRestoredFileEvent;
|
|
82
|
+
private _planRestoredRename;
|
|
83
|
+
private getExactCurrentFile;
|
|
84
|
+
private isCurrentFileSelected;
|
|
85
|
+
private isCurrentPathSelected;
|
|
86
|
+
private canApplyRestoredDeletion;
|
|
87
|
+
private logRestoredEventValidationFailure;
|
|
71
88
|
_anyProcessReplicatedDoc(entry: MetaEntry): Promise<boolean>;
|
|
72
89
|
createAllChunks(showingNotice?: boolean): Promise<void>;
|
|
73
90
|
}
|
|
@@ -644,15 +644,13 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
644
644
|
return true;
|
|
645
645
|
}
|
|
646
646
|
async _anyHandlerProcessesFileEvent(item) {
|
|
647
|
+
if (item.restoredFromPreviousRuntime) {
|
|
648
|
+
return await this._processRestoredFileEvent(item);
|
|
649
|
+
}
|
|
647
650
|
const eventItem = item.args;
|
|
648
651
|
const type = item.type;
|
|
649
652
|
const path = eventItem.file.path;
|
|
650
|
-
if (!await this.
|
|
651
|
-
this._log(`File ${path} is not the target file`, LOG_LEVEL_VERBOSE);
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
if (shouldBeIgnored(path)) {
|
|
655
|
-
this._log(`File ${path} should be ignored`, LOG_LEVEL_VERBOSE);
|
|
653
|
+
if (!await this.isCurrentPathSelected(path)) {
|
|
656
654
|
return false;
|
|
657
655
|
}
|
|
658
656
|
if (type === "RENAME" && !eventItem.oldPath) {
|
|
@@ -660,9 +658,7 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
660
658
|
return false;
|
|
661
659
|
}
|
|
662
660
|
const eventPaths = type === "RENAME" ? [path, eventItem.oldPath] : [path];
|
|
663
|
-
|
|
664
|
-
const lockKeys = [...new Set(documentIds)].sort().map((documentId) => `processFileEvent-${documentId}`);
|
|
665
|
-
return await serializedByKeys(lockKeys, async () => {
|
|
661
|
+
return await this.serializedByFileEventPaths(eventPaths, async () => {
|
|
666
662
|
switch (type) {
|
|
667
663
|
case "CREATE":
|
|
668
664
|
case "CHANGED":
|
|
@@ -682,6 +678,146 @@ class ServiceFileHandlerBase extends ServiceModuleBase {
|
|
|
682
678
|
}
|
|
683
679
|
});
|
|
684
680
|
}
|
|
681
|
+
async serializedByFileEventPaths(eventPaths, callback) {
|
|
682
|
+
const documentIds = await Promise.all(eventPaths.map((eventPath) => this.path.path2id(eventPath)));
|
|
683
|
+
const lockKeys = [...new Set(documentIds)].sort().map((documentId) => `processFileEvent-${documentId}`);
|
|
684
|
+
return await serializedByKeys(
|
|
685
|
+
lockKeys,
|
|
686
|
+
() => callback(documentIds.length === 2 && documentIds[0] === documentIds[1])
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Revalidate a persisted storage operation against the current storage state.
|
|
691
|
+
*
|
|
692
|
+
* Snapshot entries preserve operation intent and ordering only. Their file
|
|
693
|
+
* stub, timestamps, and existence assumptions can be stale after a restart.
|
|
694
|
+
* Current inclusion is therefore read from storage, while destructive work
|
|
695
|
+
* is allowed only after current absence has been observed.
|
|
696
|
+
*/
|
|
697
|
+
async _processRestoredFileEvent(item) {
|
|
698
|
+
const eventItem = item.args;
|
|
699
|
+
const type = item.type;
|
|
700
|
+
const path = eventItem.file.path;
|
|
701
|
+
if (type === "RENAME" && !eventItem.oldPath) {
|
|
702
|
+
this._log(`Rename event for ${path} has no source path`, LOG_LEVEL_VERBOSE);
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
const eventPaths = type === "RENAME" ? [path, eventItem.oldPath] : [path];
|
|
706
|
+
return await this.serializedByFileEventPaths(eventPaths, async (isSameDocument) => {
|
|
707
|
+
let action;
|
|
708
|
+
try {
|
|
709
|
+
action = await this._planRestoredFileEvent(item, isSameDocument);
|
|
710
|
+
} catch (ex) {
|
|
711
|
+
this.logRestoredEventValidationFailure(path, ex);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
switch (action.kind) {
|
|
715
|
+
case "none":
|
|
716
|
+
return true;
|
|
717
|
+
case "store":
|
|
718
|
+
return await this.storeFileToDB(action.file);
|
|
719
|
+
case "delete":
|
|
720
|
+
return await this.deleteFileFromDB(action.path);
|
|
721
|
+
case "rename":
|
|
722
|
+
return await this.renameFileInDB(action.file, action.oldPath);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
async _planRestoredFileEvent(item, isSameDocument) {
|
|
727
|
+
const path = item.args.file.path;
|
|
728
|
+
switch (item.type) {
|
|
729
|
+
case "CREATE":
|
|
730
|
+
case "CHANGED": {
|
|
731
|
+
const current = this.getExactCurrentFile(await this.storage.getStub(path), path);
|
|
732
|
+
return current && await this.isCurrentFileSelected(current) ? { kind: "store", file: current } : { kind: "none" };
|
|
733
|
+
}
|
|
734
|
+
case "DELETE": {
|
|
735
|
+
const current = await this.storage.getStub(path);
|
|
736
|
+
return current === null && await this.canApplyRestoredDeletion(path) ? { kind: "delete", path } : { kind: "none" };
|
|
737
|
+
}
|
|
738
|
+
case "RENAME":
|
|
739
|
+
return await this._planRestoredRename(path, item.args.oldPath, isSameDocument);
|
|
740
|
+
case "INTERNAL":
|
|
741
|
+
return { kind: "none" };
|
|
742
|
+
default:
|
|
743
|
+
this._log(`Unsupported event type: ${item.type}`, LOG_LEVEL_VERBOSE);
|
|
744
|
+
return { kind: "none" };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async _planRestoredRename(newPath, oldPath, isSameDocument) {
|
|
748
|
+
if (isSameDocument) {
|
|
749
|
+
const currentTarget2 = this.getExactCurrentFile(await this.storage.getStub(newPath), newPath);
|
|
750
|
+
if (!currentTarget2 || !await this.isCurrentFileSelected(currentTarget2)) {
|
|
751
|
+
return { kind: "none" };
|
|
752
|
+
}
|
|
753
|
+
return { kind: "rename", file: currentTarget2, oldPath };
|
|
754
|
+
}
|
|
755
|
+
const currentTargetItem = await this.storage.getStub(newPath);
|
|
756
|
+
let currentSourceItem = null;
|
|
757
|
+
let sourceWasInspected = true;
|
|
758
|
+
try {
|
|
759
|
+
currentSourceItem = await this.storage.getStub(oldPath);
|
|
760
|
+
} catch (ex) {
|
|
761
|
+
sourceWasInspected = false;
|
|
762
|
+
this.logRestoredEventValidationFailure(oldPath, ex);
|
|
763
|
+
}
|
|
764
|
+
const currentTarget = this.getExactCurrentFile(currentTargetItem, newPath);
|
|
765
|
+
if (currentTargetItem !== null) {
|
|
766
|
+
if (!currentTarget || !await this.isCurrentFileSelected(currentTarget)) {
|
|
767
|
+
return { kind: "none" };
|
|
768
|
+
}
|
|
769
|
+
if (!sourceWasInspected || currentSourceItem !== null || !await this.canApplyRestoredDeletion(oldPath)) {
|
|
770
|
+
return { kind: "store", file: currentTarget };
|
|
771
|
+
}
|
|
772
|
+
return { kind: "rename", file: currentTarget, oldPath };
|
|
773
|
+
}
|
|
774
|
+
if (!sourceWasInspected || currentSourceItem !== null || !await this.canApplyRestoredDeletion(oldPath)) {
|
|
775
|
+
return { kind: "none" };
|
|
776
|
+
}
|
|
777
|
+
return { kind: "delete", path: oldPath };
|
|
778
|
+
}
|
|
779
|
+
getExactCurrentFile(current, expectedPath) {
|
|
780
|
+
if (current === null || isFolderInfo(current)) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
return this.storage.normalisePath(current.path) === this.storage.normalisePath(expectedPath) ? current : null;
|
|
784
|
+
}
|
|
785
|
+
async isCurrentFileSelected(file) {
|
|
786
|
+
if (!await this.isCurrentPathSelected(file.path)) {
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
if (this.vault.isFileSizeTooLarge(file.stat.size)) {
|
|
790
|
+
this._log(`File ${file.path} exceeds the current maximum size`, LOG_LEVEL_VERBOSE);
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
async isCurrentPathSelected(path) {
|
|
796
|
+
if (!await this.vault.isTargetFile(path)) {
|
|
797
|
+
this._log(`File ${path} is not the target file`, LOG_LEVEL_VERBOSE);
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
if (shouldBeIgnored(path)) {
|
|
801
|
+
this._log(`File ${path} should be ignored`, LOG_LEVEL_VERBOSE);
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
async canApplyRestoredDeletion(path) {
|
|
807
|
+
try {
|
|
808
|
+
return await this.isCurrentPathSelected(path);
|
|
809
|
+
} catch (ex) {
|
|
810
|
+
this.logRestoredEventValidationFailure(path, ex);
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
logRestoredEventValidationFailure(path, ex) {
|
|
815
|
+
this._log(
|
|
816
|
+
`Could not validate the saved storage operation for ${path} against current storage; the Offline Scanner will reconcile the current state`,
|
|
817
|
+
LOG_LEVEL_NOTICE
|
|
818
|
+
);
|
|
819
|
+
this._log(ex, LOG_LEVEL_VERBOSE);
|
|
820
|
+
}
|
|
685
821
|
async _anyProcessReplicatedDoc(entry) {
|
|
686
822
|
return await serialized(`processReplicatedDoc-${entry._id}`, async () => {
|
|
687
823
|
if (!await this.vault.isTargetFile(entry.path)) {
|
|
@@ -23,9 +23,7 @@ export declare abstract class DatabaseService<T extends ServiceContext = Service
|
|
|
23
23
|
protected _localDatabase: LiveSyncLocalDB | null;
|
|
24
24
|
protected services: DatabaseServiceDependencies;
|
|
25
25
|
onOpenDatabase: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(vaultName: string) => Promise<boolean>>;
|
|
26
|
-
/**
|
|
27
|
-
* Called after the local database has been reset.
|
|
28
|
-
*/
|
|
26
|
+
/** Called after the active database has been reset and successfully reinitialised. */
|
|
29
27
|
onDatabaseReset: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<() => Promise<boolean>>;
|
|
30
28
|
get localDatabase(): LiveSyncLocalDB;
|
|
31
29
|
get localDatabaseDirect(): LiveSyncLocalDB;
|
|
@@ -36,5 +34,15 @@ export declare abstract class DatabaseService<T extends ServiceContext = Service
|
|
|
36
34
|
createPouchDBInstance<T extends object>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T>;
|
|
37
35
|
openDatabase(params: openDatabaseParameters): Promise<boolean>;
|
|
38
36
|
isDatabaseReady(): boolean;
|
|
37
|
+
private discardFailedActiveDatabase;
|
|
39
38
|
resetDatabase(): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Reset the database selected by the current settings.
|
|
41
|
+
*
|
|
42
|
+
* A `LiveSyncLocalDB` captures its name when constructed, so changing a
|
|
43
|
+
* suffix does not retarget the active instance. This operation selects and
|
|
44
|
+
* opens the settings-derived database before resetting it when necessary.
|
|
45
|
+
* The formerly active physical database is closed but not destroyed.
|
|
46
|
+
*/
|
|
47
|
+
resetDatabaseForCurrentSettings(params: openDatabaseParameters): Promise<boolean>;
|
|
40
48
|
}
|
|
@@ -3,6 +3,7 @@ import { LiveSyncLocalDB } from "../../pouchdb/LiveSyncLocalDB.js";
|
|
|
3
3
|
import { handlers } from "../lib/HandlerUtils.js";
|
|
4
4
|
import { createInstanceLogFunction } from "../lib/logUtils.js";
|
|
5
5
|
import { ExtraSuffixIndexedDB } from "../../common/models/shared.const.js";
|
|
6
|
+
import { LOG_LEVEL_VERBOSE } from "../../common/logger.js";
|
|
6
7
|
class DatabaseService extends ServiceBase {
|
|
7
8
|
_log = createInstanceLogFunction("InjectableDatabaseService");
|
|
8
9
|
constructor(context, dependencies) {
|
|
@@ -13,9 +14,7 @@ class DatabaseService extends ServiceBase {
|
|
|
13
14
|
services;
|
|
14
15
|
// Additional process when opening database, such as initialising managers or local database instance.
|
|
15
16
|
onOpenDatabase = handlers().bailFirstFailure("onOpenDatabase");
|
|
16
|
-
/**
|
|
17
|
-
* Called after the local database has been reset.
|
|
18
|
-
*/
|
|
17
|
+
/** Called after the active database has been reset and successfully reinitialised. */
|
|
19
18
|
onDatabaseReset = handlers().bailFirstFailure("onDatabaseReset");
|
|
20
19
|
get localDatabase() {
|
|
21
20
|
if (!this._localDatabase) {
|
|
@@ -60,18 +59,75 @@ class DatabaseService extends ServiceBase {
|
|
|
60
59
|
database: this
|
|
61
60
|
}
|
|
62
61
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
const selectedDatabase = new LiveSyncLocalDB(vaultName, env);
|
|
63
|
+
this._localDatabase = selectedDatabase;
|
|
64
|
+
try {
|
|
65
|
+
await this.onOpenDatabase(vaultName);
|
|
66
|
+
const initialised = await selectedDatabase.initializeDatabase();
|
|
67
|
+
if (!initialised && this._localDatabase === selectedDatabase) {
|
|
68
|
+
this._localDatabase = null;
|
|
69
|
+
}
|
|
70
|
+
return initialised;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (this._localDatabase === selectedDatabase) {
|
|
73
|
+
this._localDatabase = null;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
66
77
|
}
|
|
67
78
|
isDatabaseReady() {
|
|
68
79
|
return this._localDatabase != null && this._localDatabase.isReady;
|
|
69
80
|
}
|
|
81
|
+
async discardFailedActiveDatabase(database) {
|
|
82
|
+
if (this._localDatabase === database) {
|
|
83
|
+
this._localDatabase = null;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await database.close();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this._log("Failed to close the active database after a rejected lifecycle transition.", LOG_LEVEL_VERBOSE);
|
|
89
|
+
this._log(error, LOG_LEVEL_VERBOSE);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
70
92
|
async resetDatabase() {
|
|
71
93
|
if (!this._localDatabase) {
|
|
72
94
|
return Promise.resolve(true);
|
|
73
95
|
}
|
|
74
|
-
|
|
96
|
+
const activeDatabase = this._localDatabase;
|
|
97
|
+
try {
|
|
98
|
+
if (!await activeDatabase.resetDatabase()) {
|
|
99
|
+
await this.discardFailedActiveDatabase(activeDatabase);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (!await this.onDatabaseReset()) {
|
|
103
|
+
await this.discardFailedActiveDatabase(activeDatabase);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
await this.discardFailedActiveDatabase(activeDatabase);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Reset the database selected by the current settings.
|
|
114
|
+
*
|
|
115
|
+
* A `LiveSyncLocalDB` captures its name when constructed, so changing a
|
|
116
|
+
* suffix does not retarget the active instance. This operation selects and
|
|
117
|
+
* opens the settings-derived database before resetting it when necessary.
|
|
118
|
+
* The formerly active physical database is closed but not destroyed.
|
|
119
|
+
*/
|
|
120
|
+
async resetDatabaseForCurrentSettings(params) {
|
|
121
|
+
const selectedDatabaseName = this.services.vault.getVaultName();
|
|
122
|
+
if (this._localDatabase?.dbname !== selectedDatabaseName) {
|
|
123
|
+
if (!await this.openDatabase(params)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (this._localDatabase?.dbname !== selectedDatabaseName) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return await this.resetDatabase();
|
|
75
131
|
}
|
|
76
132
|
}
|
|
77
133
|
export {
|
|
@@ -70,6 +70,7 @@ export interface IDatabaseService {
|
|
|
70
70
|
createPouchDBInstance<T extends object>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T>;
|
|
71
71
|
openDatabase(params: openDatabaseParameters): Promise<boolean>;
|
|
72
72
|
resetDatabase(): Promise<boolean>;
|
|
73
|
+
resetDatabaseForCurrentSettings(params: openDatabaseParameters): Promise<boolean>;
|
|
73
74
|
onDatabaseReset: () => Promise<boolean>;
|
|
74
75
|
onOpenDatabase: (vaultName: string) => Promise<boolean>;
|
|
75
76
|
isDatabaseReady(): boolean;
|
|
@@ -85,8 +85,24 @@ export declare abstract class ReplicationService<T extends ServiceContext = Serv
|
|
|
85
85
|
storageApplyingCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<number>;
|
|
86
86
|
replicationResultCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<number>;
|
|
87
87
|
getActiveReplicatorFor(usage: string): false | LiveSyncAbstractReplicator;
|
|
88
|
+
private performReplicateAllToRemote;
|
|
89
|
+
private performReplicateAllFromRemote;
|
|
88
90
|
replicateAllToRemote(showingNotice?: boolean): Promise<boolean>;
|
|
89
91
|
replicateAllFromRemote(showingNotice?: boolean): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* Perform a full upload owned by an active rebuild while the physical database is ready.
|
|
94
|
+
*
|
|
95
|
+
* This concrete maintenance entry point is intentionally absent from `IReplicationService`;
|
|
96
|
+
* it is not a general application-readiness bypass.
|
|
97
|
+
*/
|
|
98
|
+
replicateAllToRemoteForRebuild(showingNotice?: boolean): Promise<boolean>;
|
|
99
|
+
/**
|
|
100
|
+
* Perform a full download owned by an active rebuild while the physical database is ready.
|
|
101
|
+
*
|
|
102
|
+
* This concrete maintenance entry point is intentionally absent from `IReplicationService`;
|
|
103
|
+
* it is not a general application-readiness bypass.
|
|
104
|
+
*/
|
|
105
|
+
replicateAllFromRemoteForRebuild(showingNotice?: boolean): Promise<boolean>;
|
|
90
106
|
private _getReplicatorAndPerform;
|
|
91
107
|
markLocked(lockByClean?: boolean): Promise<void>;
|
|
92
108
|
markUnlocked(): Promise<void>;
|