@vrtmrz/livesync-commonlib 0.1.9 → 0.1.11

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.
@@ -32,6 +32,7 @@ export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicato
32
32
  updateMaxTransferredSeqOnChunks(localDB: PouchDB.Database, remoteID: number, seqStatusMap: Record<number, boolean>): Promise<ReturnType<typeof this.getEmptyMaxEntry>>;
33
33
  sendChunks(setting: RemoteDBSettings, remoteDB: PouchDB.Database<EntryDoc> | undefined, showResult: boolean, fromSeq?: number | string): Promise<boolean>;
34
34
  openOneShotReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean, syncMode: "sync" | "pullOnly" | "pushOnly", ignoreCleanLock?: boolean): Promise<boolean>;
35
+ private closeRemoteDatabase;
35
36
  replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise<boolean>;
36
37
  replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise<boolean>;
37
38
  checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean, ignoreCleanLock?: boolean): Promise<false | {
@@ -540,80 +540,94 @@ ${sendingDocs}/(${sentDocsCount} done)`,
540
540
  );
541
541
  return false;
542
542
  }
543
- this.maxPullSeq = Number(`${ret.info.update_seq}`.split("-")[0]);
544
- this.maxPushSeq = Number(`${(await localDB.info()).update_seq}`.split("-")[0]);
545
- if (showResult) {
546
- Logger(this.translate("liveSyncReplicator.checkingLastSyncPoint"), LOG_LEVEL_NOTICE, "sync");
547
- }
548
543
  const { db, syncOptionBase } = ret;
549
- this.syncStatus = "STARTED";
550
- this.updateInfo();
551
- const docArrivedOnStart = this.docArrived;
552
- const docSentOnStart = this.docSent;
553
- if (!retrying) {
554
- this.originalSetting = setting;
555
- }
556
- this.terminateSync();
557
- const syncHandler = syncMode == "sync" ? localDB.sync(db, { ...syncOptionBase }) : syncMode == "pullOnly" ? localDB.replicate.from(db, {
558
- ...syncOptionBase,
559
- ...setting.readChunksOnline ? selectorOnDemandPull : {}
560
- }) : syncMode == "pushOnly" ? localDB.replicate.to(db, { ...syncOptionBase }) : void 0;
561
- const syncResult = await this.processSync(
562
- syncHandler,
563
- showResult,
564
- docSentOnStart,
565
- docArrivedOnStart,
566
- syncMode,
567
- retrying,
568
- false
569
- );
570
- if (syncResult == "DONE") {
571
- return true;
572
- }
573
- if (syncResult == "CANCELLED") {
574
- return false;
575
- }
576
- if (syncResult == "FAILED") {
577
- return false;
578
- }
579
- if (syncResult == "NEED_RESURRECT") {
544
+ try {
545
+ this.maxPullSeq = Number(`${ret.info.update_seq}`.split("-")[0]);
546
+ this.maxPushSeq = Number(`${(await localDB.info()).update_seq}`.split("-")[0]);
547
+ if (showResult) {
548
+ Logger(this.translate("liveSyncReplicator.checkingLastSyncPoint"), LOG_LEVEL_NOTICE, "sync");
549
+ }
550
+ this.syncStatus = "STARTED";
551
+ this.updateInfo();
552
+ const docArrivedOnStart = this.docArrived;
553
+ const docSentOnStart = this.docSent;
554
+ if (!retrying) {
555
+ this.originalSetting = setting;
556
+ }
580
557
  this.terminateSync();
581
- return async () => await this.openOneShotReplication(
582
- this.originalSetting,
558
+ const syncHandler = syncMode == "sync" ? localDB.sync(db, { ...syncOptionBase }) : syncMode == "pullOnly" ? localDB.replicate.from(db, {
559
+ ...syncOptionBase,
560
+ ...setting.readChunksOnline ? selectorOnDemandPull : {}
561
+ }) : syncMode == "pushOnly" ? localDB.replicate.to(db, { ...syncOptionBase }) : void 0;
562
+ const syncResult = await this.processSync(
563
+ syncHandler,
583
564
  showResult,
584
- false,
565
+ docSentOnStart,
566
+ docArrivedOnStart,
585
567
  syncMode,
586
- ignoreCleanLock
568
+ retrying,
569
+ false
587
570
  );
588
- }
589
- if (syncResult == "NEED_RETRY") {
590
- const tempSetting = JSON.parse(JSON.stringify(setting));
591
- tempSetting.batch_size = Math.ceil(tempSetting.batch_size / 2) + 2;
592
- tempSetting.batches_limit = Math.ceil(tempSetting.batches_limit / 2) + 2;
593
- if (tempSetting.batch_size <= 5 && tempSetting.batches_limit <= 5) {
594
- Logger(
595
- this.translate("liveSyncReplicator.cantReplicateLowerValue"),
596
- showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
597
- );
571
+ if (syncResult == "DONE") {
572
+ return true;
573
+ }
574
+ if (syncResult == "CANCELLED") {
598
575
  return false;
599
- } else {
600
- Logger(
601
- this.translate("liveSyncReplicator.retryLowerBatchSize", {
602
- batch_size: tempSetting.batch_size.toString(),
603
- batches_limit: tempSetting.batches_limit.toString()
604
- }),
605
- showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
576
+ }
577
+ if (syncResult == "FAILED") {
578
+ return false;
579
+ }
580
+ if (syncResult == "NEED_RESURRECT") {
581
+ this.terminateSync();
582
+ return async () => await this.openOneShotReplication(
583
+ this.originalSetting,
584
+ showResult,
585
+ false,
586
+ syncMode,
587
+ ignoreCleanLock
606
588
  );
607
- return async () => await this.openOneShotReplication(tempSetting, showResult, true, syncMode, ignoreCleanLock);
608
589
  }
590
+ if (syncResult == "NEED_RETRY") {
591
+ const tempSetting = JSON.parse(JSON.stringify(setting));
592
+ tempSetting.batch_size = Math.ceil(tempSetting.batch_size / 2) + 2;
593
+ tempSetting.batches_limit = Math.ceil(tempSetting.batches_limit / 2) + 2;
594
+ if (tempSetting.batch_size <= 5 && tempSetting.batches_limit <= 5) {
595
+ Logger(
596
+ this.translate("liveSyncReplicator.cantReplicateLowerValue"),
597
+ showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
598
+ );
599
+ return false;
600
+ } else {
601
+ Logger(
602
+ this.translate("liveSyncReplicator.retryLowerBatchSize", {
603
+ batch_size: tempSetting.batch_size.toString(),
604
+ batches_limit: tempSetting.batches_limit.toString()
605
+ }),
606
+ showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
607
+ );
608
+ return async () => await this.openOneShotReplication(tempSetting, showResult, true, syncMode, ignoreCleanLock);
609
+ }
610
+ }
611
+ return false;
612
+ } finally {
613
+ await this.closeRemoteDatabase(db);
609
614
  }
610
- return false;
611
615
  });
612
616
  if (typeof next === "boolean") {
613
617
  return next;
614
618
  }
615
619
  return await next();
616
620
  }
621
+ // One-shot attempts own their temporary remote handle. Continuous replication
622
+ // keeps its handle until the replication controller terminates it.
623
+ async closeRemoteDatabase(db) {
624
+ try {
625
+ await db.close();
626
+ } catch (ex) {
627
+ Logger("Failed to close remote database.", LOG_LEVEL_INFO, "sync");
628
+ Logger(ex, LOG_LEVEL_VERBOSE, "sync");
629
+ }
630
+ }
617
631
  replicateAllToServer(setting, showingNotice) {
618
632
  return this.openOneShotReplication(setting, showingNotice ?? false, false, "pushOnly");
619
633
  }
@@ -638,72 +652,87 @@ ${sendingDocs}/(${sentDocsCount} done)`,
638
652
  );
639
653
  return false;
640
654
  }
641
- if (!skipCheck) {
642
- if (!await checkRemoteVersion(dbRet.db, this.migrate.bind(this), VER)) {
643
- Logger(this.translate("liveSyncReplicator.remoteDbCorrupted"), LOG_LEVEL_NOTICE);
644
- return false;
645
- }
646
- this.remoteCleaned = false;
647
- this.remoteLocked = false;
648
- this.remoteLockedAndDeviceNotAccepted = false;
649
- this.tweakSettingsMismatched = false;
650
- this.preferredTweakValue = void 0;
651
- const progress = `${dbRet.info.update_seq}`;
652
- const info = {
653
- app_version: this.env.services.API.getAppVersion(),
654
- plugin_version: this.env.services.API.getPluginVersion(),
655
- vault_name: this.env.services.vault.vaultName(),
656
- device_name: this.env.services.vault.getVaultName(),
657
- progress
658
- };
659
- const ensure = await ensureDatabaseIsCompatible(dbRet.db, setting, this.nodeid, currentVersionRange, info);
660
- if (ensure == "INCOMPATIBLE") {
661
- Logger(
662
- "The remote database has no compatibility with the running version. Please upgrade the plugin.",
663
- LOG_LEVEL_NOTICE
664
- );
665
- return false;
666
- } else if (ensure == "NODE_LOCKED") {
667
- Logger(
668
- "The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
669
- LOG_LEVEL_NOTICE
655
+ let ownershipTransferred = false;
656
+ try {
657
+ if (!skipCheck) {
658
+ if (!await checkRemoteVersion(dbRet.db, this.migrate.bind(this), VER)) {
659
+ Logger(this.translate("liveSyncReplicator.remoteDbCorrupted"), LOG_LEVEL_NOTICE);
660
+ return false;
661
+ }
662
+ this.remoteCleaned = false;
663
+ this.remoteLocked = false;
664
+ this.remoteLockedAndDeviceNotAccepted = false;
665
+ this.tweakSettingsMismatched = false;
666
+ this.preferredTweakValue = void 0;
667
+ const progress = `${dbRet.info.update_seq}`;
668
+ const info = {
669
+ app_version: this.env.services.API.getAppVersion(),
670
+ plugin_version: this.env.services.API.getPluginVersion(),
671
+ vault_name: this.env.services.vault.vaultName(),
672
+ device_name: this.env.services.vault.getVaultName(),
673
+ progress
674
+ };
675
+ const ensure = await ensureDatabaseIsCompatible(
676
+ dbRet.db,
677
+ setting,
678
+ this.nodeid,
679
+ currentVersionRange,
680
+ info
670
681
  );
671
- this.remoteLockedAndDeviceNotAccepted = true;
672
- this.remoteLocked = true;
673
- return false;
674
- } else if (ensure == "LOCKED") {
675
- this.remoteLocked = true;
676
- } else if (ensure == "NODE_CLEANED") {
677
- if (ignoreCleanLock) {
678
- this.remoteLocked = true;
679
- } else {
682
+ if (ensure == "INCOMPATIBLE") {
683
+ Logger(
684
+ "The remote database has no compatibility with the running version. Please upgrade the plugin.",
685
+ LOG_LEVEL_NOTICE
686
+ );
687
+ return false;
688
+ } else if (ensure == "NODE_LOCKED") {
680
689
  Logger(
681
- "The remote database has been cleaned up. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
690
+ "The remote database has been rebuilt or corrupted since we have synchronized last time. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
682
691
  LOG_LEVEL_NOTICE
683
692
  );
684
693
  this.remoteLockedAndDeviceNotAccepted = true;
685
694
  this.remoteLocked = true;
686
- this.remoteCleaned = true;
695
+ return false;
696
+ } else if (ensure == "LOCKED") {
697
+ this.remoteLocked = true;
698
+ } else if (ensure == "NODE_CLEANED") {
699
+ if (ignoreCleanLock) {
700
+ this.remoteLocked = true;
701
+ } else {
702
+ Logger(
703
+ "The remote database has been cleaned up. Fetch rebuilt DB, explicit unlocking or chunk clean-up is required.",
704
+ LOG_LEVEL_NOTICE
705
+ );
706
+ this.remoteLockedAndDeviceNotAccepted = true;
707
+ this.remoteLocked = true;
708
+ this.remoteCleaned = true;
709
+ return false;
710
+ }
711
+ } else if (ensure == "OK") {
712
+ } else if (ensure[0] == "MISMATCHED") {
713
+ Logger(this.translate("liveSyncReplicator.mismatchedTweakDetected"), LOG_LEVEL_NOTICE);
714
+ this.tweakSettingsMismatched = true;
715
+ this.preferredTweakValue = ensure[1];
687
716
  return false;
688
717
  }
689
- } else if (ensure == "OK") {
690
- } else if (ensure[0] == "MISMATCHED") {
691
- Logger(this.translate("liveSyncReplicator.mismatchedTweakDetected"), LOG_LEVEL_NOTICE);
692
- this.tweakSettingsMismatched = true;
693
- this.preferredTweakValue = ensure[1];
694
- return false;
718
+ }
719
+ const syncOptionBase = {
720
+ batches_limit: setting.batches_limit,
721
+ batch_size: setting.batch_size
722
+ };
723
+ syncOptionBase.push = {};
724
+ if (setting.readChunksOnline) {
725
+ syncOptionBase.pull = { ...selectorOnDemandPull };
726
+ }
727
+ const syncOption = keepAlive ? { live: true, retry: true, heartbeat: setting.useTimeouts ? false : 3e4, ...syncOptionBase } : { ...syncOptionBase };
728
+ const connection = { db: dbRet.db, info: dbRet.info, syncOptionBase, syncOption };
729
+ ownershipTransferred = true;
730
+ return connection;
731
+ } finally {
732
+ if (!ownershipTransferred) {
733
+ await this.closeRemoteDatabase(dbRet.db);
695
734
  }
696
735
  }
697
- const syncOptionBase = {
698
- batches_limit: setting.batches_limit,
699
- batch_size: setting.batch_size
700
- };
701
- syncOptionBase.push = {};
702
- if (setting.readChunksOnline) {
703
- syncOptionBase.pull = { ...selectorOnDemandPull };
704
- }
705
- const syncOption = keepAlive ? { live: true, retry: true, heartbeat: setting.useTimeouts ? false : 3e4, ...syncOptionBase } : { ...syncOptionBase };
706
- return { db: dbRet.db, info: dbRet.info, syncOptionBase, syncOption };
707
736
  }
708
737
  async openContinuousReplication(setting, showResult, retrying) {
709
738
  const next = await shareRunningResult("continuousReplication", async () => {
@@ -2,6 +2,19 @@ import { type FilePathWithPrefix, type FilePathWithPrefixLC, type MetaEntry, typ
2
2
  import { type LogFunction } from "../services/lib/logUtils.js";
3
3
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
4
4
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
5
+ /**
6
+ * Outcome of processing one storage/database pair during an offline scan.
7
+ *
8
+ * A skipped pair is intentionally left unchanged by scanner policy. It is not
9
+ * a scan failure, but it must not receive success side effects which would
10
+ * claim that database content was reflected to storage.
11
+ */
12
+ export declare const FilePairProcessResults: {
13
+ readonly COMPLETED: "completed";
14
+ readonly SKIPPED: "skipped";
15
+ readonly FAILED: "failed";
16
+ };
17
+ export type FilePairProcessResult = (typeof FilePairProcessResults)[keyof typeof FilePairProcessResults];
5
18
  /**
6
19
  * Collect deleted files that have expired according to retention policy.
7
20
  * @param host Services container
@@ -23,7 +36,7 @@ export declare function getPathFromEntry(host: NecessaryServices<"path", never>,
23
36
  * @param file Storage file information
24
37
  * @param doc Database entry
25
38
  */
26
- export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise<void>;
39
+ export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise<FilePairProcessResult>;
27
40
  export declare function canProceedScan(host: NecessaryServices<"keyValueDB" | "setting", never>, errorManager: UnresolvedErrorManager, log: LogFunction, showingNotice?: boolean, ignoreSuspending?: boolean): boolean;
28
41
  /**
29
42
  * Convert file path to lower case if the settings indicate that filename case should be handled insensitively.
@@ -46,9 +59,9 @@ export declare function collectDatabaseFiles(host: NecessaryServices<"database"
46
59
  databaseFileNames: FilePathWithPrefix[];
47
60
  databaseFileNameCI2CS: Record<FilePathWithPrefix, FilePathWithPrefixLC>;
48
61
  }>;
49
- export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise<void>;
50
- export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise<void>;
51
- export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise<void>;
62
+ export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise<FilePairProcessResult>;
63
+ export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise<FilePairProcessResult>;
64
+ export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise<FilePairProcessResult>;
52
65
  export declare const FullScanModes: {
53
66
  readonly DB_APPLY: "db-apply";
54
67
  readonly NEWER_WINS: "newer-wins";
@@ -105,6 +118,7 @@ export declare function resolveFilePairAction(state: FilePairState, options: Ful
105
118
  * @param log Logging function
106
119
  * @param errorManager Error manager
107
120
  * @param options Full scan options
121
+ * @returns True when every pair completed or was deliberately skipped; false when any pair failed
108
122
  */
109
123
  export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<boolean>;
110
124
  export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial<FullScanOptions> | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions;
@@ -115,7 +129,7 @@ export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial
115
129
  * @param errorManager Error manager
116
130
  * @param showingNotice Whether to show notices during scanning
117
131
  * @param ignoreSuspending Whether to ignore suspension settings
118
- * @returns True if scan completed successfully
132
+ * @returns True when the scan was permitted and no selected pair failed
119
133
  */
120
134
  export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<boolean>;
121
135
  export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<boolean>;
@@ -13,6 +13,11 @@ import { createInstanceLogFunction } from "../services/lib/logUtils.js";
13
13
  import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "../common/models/shared.const.symbols.js";
14
14
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
15
15
  import { compatGlobal } from "../common/coreEnvFunctions.js";
16
+ const FilePairProcessResults = {
17
+ COMPLETED: "completed",
18
+ SKIPPED: "skipped",
19
+ FAILED: "failed"
20
+ };
16
21
  async function collectDeletedFiles(host, log) {
17
22
  const limitDays = host.services.setting.currentSettings().automaticallyDeleteMetadataOfDeletedFiles;
18
23
  if (limitDays <= 0) return;
@@ -59,13 +64,14 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
59
64
  if (!host.services.vault.isFileSizeTooLarge(file.stat.size)) {
60
65
  log("STORAGE -> DB :" + file.path);
61
66
  await host.serviceModules.fileHandler.storeFileToDB(file);
67
+ return FilePairProcessResults.COMPLETED;
62
68
  } else {
63
69
  log(
64
70
  `STORAGE -> DB : ${file.path} has been skipped due to file size exceeding the limit`,
65
71
  LOG_LEVEL_NOTICE
66
72
  );
73
+ return FilePairProcessResults.SKIPPED;
67
74
  }
68
- break;
69
75
  case TARGET_IS_NEW:
70
76
  if (!host.services.vault.isFileSizeTooLarge(doc.size)) {
71
77
  log("STORAGE <- DB :" + docPath);
@@ -74,21 +80,24 @@ async function syncFileBetweenDBandStorage(host, log, file, doc) {
74
80
  file: file.path,
75
81
  automated: true
76
82
  });
83
+ return FilePairProcessResults.COMPLETED;
77
84
  } else {
78
85
  log(`STORAGE <- DB : Cloud not read ${file.path}, possibly deleted`, LOG_LEVEL_NOTICE);
86
+ return FilePairProcessResults.FAILED;
79
87
  }
80
88
  } else {
81
89
  log(
82
90
  `STORAGE <- DB : ${file.path} has been skipped due to file size exceeding the limit`,
83
91
  LOG_LEVEL_NOTICE
84
92
  );
93
+ return FilePairProcessResults.SKIPPED;
85
94
  }
86
- break;
87
95
  case EVEN:
88
96
  log("STORAGE == DB :" + file.path, LOG_LEVEL_DEBUG);
89
- break;
97
+ return FilePairProcessResults.COMPLETED;
90
98
  default:
91
99
  log("STORAGE ?? DB :" + file.path + " Something got weird");
100
+ return FilePairProcessResults.FAILED;
92
101
  }
93
102
  }
94
103
  function canProceedScan(host, errorManager, log, showingNotice = false, ignoreSuspending = false) {
@@ -167,8 +176,10 @@ async function updateToDatabase(host, log, logLevel, file) {
167
176
  const path = file.path;
168
177
  await host.serviceModules.fileHandler.storeFileToDB(file);
169
178
  host.services.context.events.emitEvent("event-file-changed", { file: path, automated: true });
179
+ return FilePairProcessResults.COMPLETED;
170
180
  } else {
171
181
  log(`UPDATE DATABASE: ${file.path} has been skipped due to file size exceeding the limit`, logLevel);
182
+ return FilePairProcessResults.SKIPPED;
172
183
  }
173
184
  }
174
185
  async function updateToStorage(host, log, logLevel, w) {
@@ -177,35 +188,41 @@ async function updateToStorage(host, log, logLevel, w) {
177
188
  if (!host.services.vault.isFileSizeTooLarge(w.size)) {
178
189
  if ((w._conflicts?.length ?? 0) > 0) {
179
190
  log(`UPDATE STORAGE: ${path} has conflicts. skipped (x)`, LOG_LEVEL_INFO);
180
- return;
191
+ return FilePairProcessResults.SKIPPED;
181
192
  }
182
- await host.serviceModules.fileHandler.dbToStorage(path, null, true);
193
+ const reflected = await host.serviceModules.fileHandler.dbToStorage(path, null, true);
194
+ if (!reflected) return FilePairProcessResults.FAILED;
183
195
  host.services.context.events.emitEvent("event-file-changed", {
184
196
  file: path,
185
197
  automated: true
186
198
  });
187
199
  log(`Check or pull from db:${path} OK`);
200
+ return FilePairProcessResults.COMPLETED;
188
201
  } else {
189
202
  log(`UPDATE STORAGE: ${path} has been skipped due to file size exceeding the limit`, logLevel);
203
+ return FilePairProcessResults.SKIPPED;
190
204
  }
191
205
  } else if (w) {
192
206
  log(`Deletion history skipped: ${path}`, LOG_LEVEL_VERBOSE);
207
+ return FilePairProcessResults.SKIPPED;
193
208
  } else {
194
209
  log(`entry not found: ${path}`);
210
+ return FilePairProcessResults.FAILED;
195
211
  }
196
212
  }
197
213
  async function syncStorageAndDatabase(host, log, file, logLevel, doc) {
198
214
  if ((doc._conflicts?.length ?? 0) > 0) {
199
215
  log(`SYNC DATABASE AND STORAGE: ${file.path} has conflicts. skipped`, LOG_LEVEL_INFO);
200
- return;
216
+ return FilePairProcessResults.SKIPPED;
201
217
  }
202
218
  if (!host.services.vault.isFileSizeTooLarge(file.stat.size) && !host.services.vault.isFileSizeTooLarge(doc.size)) {
203
- await syncFileBetweenDBandStorage(host, log, file, doc);
219
+ return await syncFileBetweenDBandStorage(host, log, file, doc);
204
220
  } else {
205
221
  log(
206
222
  `SYNC DATABASE AND STORAGE: ${getPathFromEntry(host, doc)} has been skipped due to file size exceeding the limit`,
207
223
  logLevel
208
224
  );
225
+ return FilePairProcessResults.SKIPPED;
209
226
  }
210
227
  }
211
228
  const FullScanModes = {
@@ -308,7 +325,7 @@ async function processFilePair(host, log, pair, options) {
308
325
  }
309
326
  if (doc && (doc._conflicts?.length ?? 0) > 0) {
310
327
  log(`SKIP ${options.mode}: ${path} has conflicts`, LOG_LEVEL_INFO);
311
- return true;
328
+ return FilePairProcessResults.SKIPPED;
312
329
  }
313
330
  const state = getFilePairState(pair);
314
331
  let action = resolveFilePairAction(state, options);
@@ -328,32 +345,35 @@ async function processFilePair(host, log, pair, options) {
328
345
  if (!file) {
329
346
  throw new Error(`Missing storage file for ${path}`);
330
347
  }
331
- await updateToDatabase(host, log, LOG_LEVEL_INFO, file);
332
- return true;
348
+ return await updateToDatabase(host, log, LOG_LEVEL_INFO, file);
333
349
  case "update-storage":
334
350
  if (!doc) {
335
351
  throw new Error(`Missing database entry for ${path}`);
336
352
  }
337
- await updateToStorage(host, log, LOG_LEVEL_INFO, doc);
338
- updateFileMTimeInMap(host, fileMapKey, doc.mtime);
339
- return true;
353
+ const updateStorageResult = await updateToStorage(host, log, LOG_LEVEL_INFO, doc);
354
+ if (updateStorageResult === FilePairProcessResults.COMPLETED) {
355
+ updateFileMTimeInMap(host, fileMapKey, doc.mtime);
356
+ }
357
+ return updateStorageResult;
340
358
  case "sync-newer":
341
359
  if (!file || !doc) {
342
360
  throw new Error(`Cannot compare freshness for ${path}`);
343
361
  }
344
- await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc);
345
- updateFileMTimeInMap(host, fileMapKey, Math.max(file.stat.mtime, doc.mtime));
346
- return true;
362
+ const syncResult = await syncStorageAndDatabase(host, log, file, LOG_LEVEL_INFO, doc);
363
+ if (syncResult === FilePairProcessResults.COMPLETED) {
364
+ updateFileMTimeInMap(host, fileMapKey, Math.max(file.stat.mtime, doc.mtime));
365
+ }
366
+ return syncResult;
347
367
  case "delete-local":
348
368
  if (!file) {
349
369
  log(`DELETE LOCAL: ${path} is already absent from storage`, LOG_LEVEL_VERBOSE);
350
- return true;
370
+ return FilePairProcessResults.COMPLETED;
351
371
  }
352
372
  log(`DELETE LOCAL: ${file.path}`, LOG_LEVEL_INFO);
353
373
  await host.serviceModules.storageAccess.delete(file.path, true);
354
374
  fileMaps.delete(fileMapKey);
355
375
  saveFileStatus(host);
356
- return true;
376
+ return FilePairProcessResults.COMPLETED;
357
377
  case "delete-db": {
358
378
  if (!doc) {
359
379
  throw new Error(`Missing database entry for ${path}`);
@@ -363,22 +383,23 @@ async function processFilePair(host, log, pair, options) {
363
383
  if (dbDeleted) {
364
384
  fileMaps.delete(fileMapKey);
365
385
  saveFileStatus(host);
386
+ return FilePairProcessResults.COMPLETED;
366
387
  } else {
367
388
  log(
368
389
  `DELETE DATABASE did not delete ${path}; keeping last-seen record to avoid resurrecting the file`,
369
390
  LOG_LEVEL_NOTICE
370
391
  );
392
+ return FilePairProcessResults.FAILED;
371
393
  }
372
- return true;
373
394
  }
374
395
  case "skip":
375
396
  log(`SKIP ${options.mode}: ${path} (${state})`, LOG_LEVEL_VERBOSE);
376
- return true;
397
+ return FilePairProcessResults.SKIPPED;
377
398
  }
378
399
  } catch (ex) {
379
400
  log(`Error processing ${path} with action ${action}`, LOG_LEVEL_NOTICE);
380
401
  log(ex, LOG_LEVEL_VERBOSE);
381
- return false;
402
+ return FilePairProcessResults.FAILED;
382
403
  }
383
404
  }
384
405
  async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, options) {
@@ -405,7 +426,9 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
405
426
  pairs.push(pair);
406
427
  }
407
428
  log(`Total files to synchronise: ${pairs.length}`, LOG_LEVEL_VERBOSE, "syncAll");
408
- let successCount = 0;
429
+ let completedCount = 0;
430
+ let skippedCount = 0;
431
+ let failedCount = 0;
409
432
  let processedCount = 0;
410
433
  for await (const result of withConcurrency(
411
434
  pairs,
@@ -415,14 +438,22 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
415
438
  } catch (ex) {
416
439
  log(`Error while synchronising files`, LOG_LEVEL_NOTICE);
417
440
  log(ex, LOG_LEVEL_VERBOSE);
418
- return false;
441
+ return FilePairProcessResults.FAILED;
419
442
  }
420
443
  },
421
444
  10
422
445
  )) {
423
446
  processedCount++;
424
- if (result) {
425
- successCount++;
447
+ switch (result) {
448
+ case FilePairProcessResults.COMPLETED:
449
+ completedCount++;
450
+ break;
451
+ case FilePairProcessResults.SKIPPED:
452
+ skippedCount++;
453
+ break;
454
+ case FilePairProcessResults.FAILED:
455
+ failedCount++;
456
+ break;
426
457
  }
427
458
  if (processedCount % 25 === 0) {
428
459
  log(
@@ -433,12 +464,12 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
433
464
  }
434
465
  }
435
466
  log(
436
- `Synchronisation completed: ${successCount}/${processedCount} files processed successfully`,
467
+ `Synchronisation completed: ${processedCount} files processed (${completedCount} completed, ${skippedCount} skipped, ${failedCount} failed)`,
437
468
  showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO,
438
469
  "syncAll"
439
470
  );
440
471
  saveFileStatus(host, true);
441
- return successCount === processedCount;
472
+ return failedCount === 0;
442
473
  }
443
474
  function normaliseFullScanOptions(showingNoticeOrOptions, ignoreSuspending = false) {
444
475
  if (typeof showingNoticeOrOptions === "object") {
@@ -513,7 +544,7 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
513
544
  log("Initialize and checking database files");
514
545
  log("Checking deleted files");
515
546
  await collectDeletedFiles(host, log);
516
- await synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, options);
547
+ const scanResult = await synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, options);
517
548
  log("Initialized, NOW TRACKING!");
518
549
  if (!isInitialized) {
519
550
  await host.services.keyValueDB.kvDB.set("initialized", true);
@@ -521,7 +552,7 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
521
552
  if (showingNotice) {
522
553
  log("Initialize done!", LOG_LEVEL_NOTICE, "syncAll");
523
554
  }
524
- return true;
555
+ return scanResult;
525
556
  }
526
557
  function useOfflineScanner(host) {
527
558
  const log = createInstanceLogFunction("SF:OfflineScanner", host.services.API);
@@ -534,6 +565,7 @@ function useOfflineScanner(host) {
534
565
  export {
535
566
  ExtraOnLocal,
536
567
  ExtraOnRemote,
568
+ FilePairProcessResults,
537
569
  FullScanModes,
538
570
  canProceedScan,
539
571
  collectDatabaseFiles,
@@ -262,6 +262,7 @@ Are you sure you wish to proceed?`;
262
262
  await this.suspendReflectingDatabase(!autoResume);
263
263
  await this.control.applySettings();
264
264
  await this.resetLocalDatabase();
265
+ this.clearFastFetchCheckpoint();
265
266
  await delay(1e3);
266
267
  await this.database.openDatabase({
267
268
  databaseEvents: this.databaseEvents,
@@ -300,6 +301,14 @@ Are you sure you wish to proceed?`;
300
301
  await this.fetchLocal(false, true, autoResume);
301
302
  return;
302
303
  }
304
+ if (settings.useRequestAPI) {
305
+ this._log(
306
+ "Fast database fetch is unavailable while 'Use Internal API' is enabled. Falling back to Standard Fetch.",
307
+ LOG_LEVEL_NOTICE
308
+ );
309
+ await this.fetchLocal(false, true, autoResume);
310
+ return;
311
+ }
303
312
  await this.replicator.runBoundedRemoteActivity(() => this.performFetchLocalDBFast(settings, autoResume), {
304
313
  label: "fast-fetch"
305
314
  });
@@ -57,7 +57,7 @@ The settings lifecycle has an additional focused unit table in `src/common/model
57
57
 
58
58
  The multiple-remote profile contract has focused unit coverage in `src/serviceFeatures/remoteConfig.unit.spec.ts`. Extend it whenever profile identity, generated display names, activation, P2P selection, URI serialisation, or legacy migration changes. New hosts should import the reviewed primitives through `/remote-configurations`; `compat/serviceFeatures/remoteConfig` remains the wider migration surface used by existing clients.
59
59
 
60
- The full offline scanner applies both the host's target-file policy and Commonlib's built-in `shouldBeIgnored` policy before constructing storage/database pairs. This keeps legacy LiveSync log files and flag files outside ordinary create, restore, and delete decisions even when a host accepts their file extension. Lower-level write guards remain a defence for direct calls; they are not the normal full-scan filter. Extend both storage-collection and database-collection tests whenever the built-in exclusions change.
60
+ The full offline scanner applies both the host's target-file policy and Commonlib's built-in `shouldBeIgnored` policy before constructing storage/database pairs. This keeps legacy LiveSync log files and flag files outside ordinary create, restore, and delete decisions even when a host accepts their file extension. Lower-level write guards remain a defence for direct calls; they are not the normal full-scan filter. The complete pair-state decision matrix and database-to-storage reflection result contract are documented in [Full offline scan](offline-scanner.md). Extend that document, the Cartesian action test, and both collection-side tests whenever the policy changes.
61
61
 
62
62
  The revision-tree safety rules are documented in [Conflict resolution and file provenance](conflict-resolution.md). `ConflictManager.unit.spec.ts` owns common-ancestor and conservative merge behaviour, deterministic pair selection for multiple live leaves, and reconstruction from the committed tree between stages. `ServiceDatabaseFileAccessBase.unit.spec.ts` owns all-branch content discovery, short-circuiting history existence checks after an exact match, and exact-revision logical deletion. `ServiceFileHandlerBase.unit.spec.ts` owns the injected provenance policy for edit, deletion, and rename operations, together with user-selected reflection, child creation, strict matching, matching-provenance cleanup after exact branch deletion, and stale-revision refusal. `LiveSyncLocalDB.unit.spec.ts` owns chunk reachability across current winners, shared chunks, live conflicts, available merge ancestry, conflict resolution, and PouchDB replication of collection and recreation. Extend the relevant sides when changing conflict resolution, compaction handling, provenance, chunk collection, or database-to-storage overwrite protection.
63
63
 
@@ -22,6 +22,8 @@ Commonlib's injected CouchDB tests verify that a mixed remote chunk batch preser
22
22
 
23
23
  Commonlib's remote-preference result contract distinguishes available synchronisation settings, a remote which has not stored those settings yet, a failed read, and a remote type which does not support central preferences. Focused CouchDB and Object Storage tests cover absent milestone data separately from transport failures, while Self-hosted LiveSync owns the initialisation choices, cancellation policy, and user-facing explanation.
24
24
 
25
+ Commonlib's full offline scanner distinguishes completed, deliberately skipped, and failed storage/database pairs. It continues processing after individual failures, returns a failed aggregate only for actual failures, and records a database mtime as local provenance only after database content has been reflected successfully. Self-hosted LiveSync consumes the aggregate directly during Fast Setup, where an actual reflection failure reaches the host-owned recovery choice without treating conflict or size-policy skips as failures.
26
+
25
27
  Most replication and storage services used by the plug-in still enter through explicit `compat/*` paths. Those imports prove that the published compatibility boundary supports the current migration; they are not examples of a finished high-level client API.
26
28
 
27
29
  The plug-in's manual onboarding and Remote Databases pane use the focused `/remote-configurations` entry to create opaque profile IDs, retain user-visible names, and select main and P2P remotes. Commonlib owns the in-memory profile result contract; Self-hosted LiveSync adds dialogue, Setup URI classification, Fetch or Rebuild scheduling, persistence, restart ordering, and real-Obsidian presentation checks.
@@ -38,6 +40,8 @@ The CLI filesystem composition receives a configured base directory and construc
38
40
 
39
41
  The maintained CLI checks include unit tests, Deno-driven CLI E2E, Compose-based compatibility runs, and the CLI-to-real-Obsidian synchronisation scenario in `test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts`. The latter is the strongest current evidence that the CLI and plug-in compose compatible Commonlib behaviour across two hosts.
40
42
 
43
+ The CLI mirror command returns Commonlib's full-scan aggregate, and daemon start-up stops when that aggregate reports an actual pair failure. Deliberately skipped pairs remain a successful scan result. Commonlib owns the pair classification and aggregate; the CLI owns its command and process policy.
44
+
41
45
  ## WebApp
42
46
 
43
47
  WebApp receives an already authorised `FileSystemDirectoryHandle` from its host and constructs `FileSystemAccessStorageAdapter` inside `src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts`. The adapter does not call `showDirectoryPicker()`, choose a directory, or persist the handle. This is the maintained application example for the browser entry's root-ownership rule.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrtmrz/livesync-commonlib",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",