@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.
@@ -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.databaseEvents.initialiseDatabase(true, true, true);
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.replicateAllToRemote(true);
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.replicateAllToRemote(true);
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
- await this.setting.saveSettingData();
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.databaseEvents.initialiseDatabase(true, true, true);
317
+ await this.prepareLocalDatabaseForRebuild();
276
318
  } else {
277
319
  }
278
320
  await this.replication.markResolved();
279
321
  await delay(500);
280
- await this.replication.replicateAllFromRemote(true);
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.replicateAllFromRemote(true);
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.resumeReflectingDatabase(true);
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
- * Finish rebuild process with resuming the reflection.
463
+ * Complete the final rebuild phases and establish application readiness.
416
464
  *
417
- * @param ignoreMinIO Whether to ignore minio for resuming the reflection.
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
- await this.resumeReflectingDatabase(ignoreMinIO);
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.resetDatabase();
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.vault.isTargetFile(path)) {
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
- const documentIds = await Promise.all(eventPaths.map((eventPath) => this.path.path2id(eventPath)));
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
- this._localDatabase = new LiveSyncLocalDB(vaultName, env);
64
- await this.onOpenDatabase(vaultName);
65
- return await this.localDatabase.initializeDatabase();
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
- return await this._localDatabase.resetDatabase();
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>;