@vrtmrz/livesync-commonlib 0.1.21 → 0.1.22

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.
@@ -2,6 +2,9 @@ import { type DocumentID, type FilePathWithPrefix, type FilePathWithPrefixLC, ty
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
+ import { VaultScanResults, type VaultScanResult } from "../services/base/VaultScanResult.js";
6
+ export { VaultScanResults };
7
+ export type { VaultScanResult };
5
8
  /**
6
9
  * Outcome of processing one storage/database pair during an offline scan.
7
10
  *
@@ -200,6 +203,12 @@ export interface FullScanOptions {
200
203
  omitEvents?: boolean;
201
204
  showingNotice?: boolean;
202
205
  ignoreSuspending?: boolean;
206
+ /**
207
+ * Allow a completed scan containing individual file-pair failures to
208
+ * satisfy ordinary application readiness. Scan preconditions remain
209
+ * strict, and failed pairs remain retryable.
210
+ */
211
+ continueOnFileFailure?: boolean;
203
212
  }
204
213
  export type FullScanMode = (typeof FullScanModes)[keyof typeof FullScanModes];
205
214
  type FilePair = {
@@ -225,9 +234,9 @@ export declare function resolveFilePairAction(state: FilePairState, options: Ful
225
234
  * @param log Logging function
226
235
  * @param errorManager Error manager
227
236
  * @param options Full scan options
228
- * @returns True when every pair completed or was deliberately skipped; false when any pair failed
237
+ * @returns The scan result for all file pairs
229
238
  */
230
- export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<boolean>;
239
+ export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise<VaultScanResult>;
231
240
  export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial<FullScanOptions> | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions;
232
241
  /**
233
242
  * Remove one path from the Offline Scanner's durable last-seen state.
@@ -242,16 +251,15 @@ export declare function clearOfflineScannerLastSeen(host: NecessaryServices<"set
242
251
  * @param host Services container
243
252
  * @param log Logging function
244
253
  * @param errorManager Error manager
245
- * @param showingNotice Whether to show notices during scanning
246
- * @param ignoreSuspending Whether to ignore suspension settings
247
- * @returns True when the scan was permitted and no selected pair failed
254
+ * @param showingNoticeOrOptions Full-scan options, or the legacy notice flag
255
+ * @param ignoreSuspending Legacy suspension flag used with the notice flag
256
+ * @returns The scan outcome, including accepted individual file failures
248
257
  */
249
- export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<boolean>;
250
- export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<boolean>;
258
+ export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial<FullScanOptions>): Promise<VaultScanResult>;
259
+ export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<VaultScanResult>;
251
260
  /**
252
261
  * Associate the initialiser file feature with the app lifecycle events.
253
262
  * This function binds initialization handlers to the appropriate lifecycle events.
254
263
  * @param host Services container with required dependencies
255
264
  */
256
265
  export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "keyValueDB" | "replicator", "storageAccess" | "fileHandler">): void;
257
- export {};
@@ -15,6 +15,7 @@ import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.
15
15
  import { compatGlobal } from "../common/coreEnvFunctions.js";
16
16
  import { ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
17
17
  import { serialized } from "octagonal-wheels/concurrency/lock";
18
+ import { VaultScanResults } from "../services/base/VaultScanResult.js";
18
19
  const FilePairProcessResults = {
19
20
  COMPLETED: "completed",
20
21
  SKIPPED: "skipped",
@@ -749,15 +750,16 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
749
750
  let skippedCount = 0;
750
751
  let failedCount = 0;
751
752
  let processedCount = 0;
752
- for await (const result of withConcurrency(
753
+ for await (const { path, result } of withConcurrency(
753
754
  pairs,
754
755
  async (e) => {
756
+ const path2 = e.file?.path ?? getPathFromEntry(host, e.doc);
755
757
  try {
756
- return await processFilePair(host, log, e, options);
758
+ return { path: path2, result: await processFilePair(host, log, e, options) };
757
759
  } catch (ex) {
758
760
  log(`Error while synchronising files`, LOG_LEVEL_NOTICE);
759
761
  log(ex, LOG_LEVEL_VERBOSE);
760
- return FilePairProcessResults.FAILED;
762
+ return { path: path2, result: FilePairProcessResults.FAILED };
761
763
  }
762
764
  },
763
765
  10
@@ -772,6 +774,10 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
772
774
  break;
773
775
  case FilePairProcessResults.FAILED:
774
776
  failedCount++;
777
+ log(
778
+ `Offline scan failed to synchronise ${path} between storage and the local database; this path remains eligible for a later scan.`,
779
+ LOG_LEVEL_VERBOSE
780
+ );
775
781
  break;
776
782
  }
777
783
  if (processedCount % 25 === 0) {
@@ -788,7 +794,8 @@ async function synchroniseAllFilesBetweenDBandStorage(host, log, errorManager, o
788
794
  "syncAll"
789
795
  );
790
796
  saveFileStatus(host, true);
791
- return failedCount === 0;
797
+ if (failedCount === 0) return VaultScanResults.COMPLETED;
798
+ return options.continueOnFileFailure === true ? VaultScanResults.COMPLETED_WITH_FILE_FAILURES : VaultScanResults.FAILED;
792
799
  }
793
800
  function normaliseFullScanOptions(showingNoticeOrOptions, ignoreSuspending = false) {
794
801
  if (typeof showingNoticeOrOptions === "object") {
@@ -898,8 +905,12 @@ async function performFullScan(host, log, errorManager, showingNoticeOrOptions =
898
905
  function useOfflineScanner(host) {
899
906
  const log = createInstanceLogFunction("SF:OfflineScanner", host.services.API);
900
907
  const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
901
- const handleScanVault = async (showingNotice, ignoreSuspending = false) => {
902
- return await performFullScan(host, log, errorManager, showingNotice, ignoreSuspending);
908
+ const handleScanVault = async (showingNotice, ignoreSuspending = false, continueOnFileFailure = false) => {
909
+ return await performFullScan(host, log, errorManager, {
910
+ showingNotice,
911
+ ignoreSuspending,
912
+ continueOnFileFailure
913
+ });
903
914
  };
904
915
  host.services.vault.scanVault.addHandler(handleScanVault);
905
916
  }
@@ -912,6 +923,7 @@ export {
912
923
  MetadataDocumentNamespaces,
913
924
  MetadataDocumentRepairResults,
914
925
  OfflineScanUnresolvedReasons,
926
+ VaultScanResults,
915
927
  canProceedScan,
916
928
  clearOfflineScannerLastSeen,
917
929
  collectDatabaseFiles,
@@ -1,6 +1,7 @@
1
1
  import type { NecessaryServices } from "../interfaces/ServiceModule.js";
2
2
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
3
3
  import { type LogFunction } from "../services/lib/logUtils.js";
4
+ import { type VaultScanResult } from "../services/base/VaultScanResult.js";
4
5
  /**
5
6
  * Initialise the database and trigger a full vault scan.
6
7
  * @param host Services container
@@ -9,9 +10,10 @@ import { type LogFunction } from "../services/lib/logUtils.js";
9
10
  * @param showingNotice Whether to show notices during initialisation
10
11
  * @param reopenDatabase Whether to reopen the database connection
11
12
  * @param ignoreSuspending Whether to ignore suspension settings
12
- * @returns True if initialisation succeeded
13
+ * @param continueOnFileFailure Whether individual file failures may satisfy application readiness
14
+ * @returns The initialisation outcome, including accepted individual file failures
13
15
  */
14
- export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean): Promise<boolean>;
16
+ export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean): Promise<VaultScanResult>;
15
17
  /**
16
18
  * Associate the initialiser file feature with the app lifecycle events.
17
19
  * This function binds initialization handlers to the appropriate lifecycle events.
@@ -1,38 +1,72 @@
1
- import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
1
+ import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
2
2
  import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
3
3
  import { createInstanceLogFunction } from "../services/lib/logUtils.js";
4
- async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false) {
4
+ import { VaultScanResults } from "../services/base/VaultScanResult.js";
5
+ function reportPreparationFailure(log, message) {
6
+ log(message, LOG_LEVEL_VERBOSE);
7
+ return false;
8
+ }
9
+ async function prepareDatabaseForUse(host, log, errorManager, showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) {
5
10
  const appLifecycle = host.services.appLifecycle;
6
11
  appLifecycle.resetIsReady();
7
- if (reopenDatabase && !await host.services.database.openDatabase({
8
- databaseEvents: host.services.databaseEvents,
9
- replicator: host.services.replicator
10
- })) {
11
- return false;
12
- }
13
- if (!host.services.database.isDatabaseReady()) {
14
- return false;
15
- }
16
- if (!await host.services.vault.scanVault(showingNotice, ignoreSuspending)) {
17
- return false;
18
- }
19
- const ERR_INITIALISATION_FAILED = `Initializing database has been failed on some module!`;
20
- if (!await host.services.databaseEvents.onDatabaseInitialised(showingNotice)) {
21
- errorManager.showError(ERR_INITIALISATION_FAILED, LOG_LEVEL_NOTICE);
22
- return false;
23
- }
24
- errorManager.clearError(ERR_INITIALISATION_FAILED);
25
- if (!await host.services.fileProcessing.commitPendingFileEvents()) {
26
- return false;
12
+ try {
13
+ if (reopenDatabase && !await host.services.database.openDatabase({
14
+ databaseEvents: host.services.databaseEvents,
15
+ replicator: host.services.replicator
16
+ })) {
17
+ return reportPreparationFailure(
18
+ log,
19
+ "Database preparation stopped because the local database could not be opened."
20
+ );
21
+ }
22
+ if (!host.services.database.isDatabaseReady()) {
23
+ return reportPreparationFailure(
24
+ log,
25
+ "Database preparation stopped because the local database is not ready."
26
+ );
27
+ }
28
+ const scanResult = await host.services.vault.scanVault(showingNotice, ignoreSuspending, continueOnFileFailure);
29
+ if (scanResult === VaultScanResults.FAILED) {
30
+ return reportPreparationFailure(log, "Database preparation stopped because the Vault scan failed.");
31
+ }
32
+ if (scanResult === VaultScanResults.COMPLETED_WITH_FILE_FAILURES && continueOnFileFailure !== true) {
33
+ return reportPreparationFailure(
34
+ log,
35
+ "Database preparation stopped because the Vault scan could not process every file."
36
+ );
37
+ }
38
+ if (!await host.services.databaseEvents.onDatabaseInitialised(showingNotice)) {
39
+ return reportPreparationFailure(
40
+ log,
41
+ "Database preparation stopped because an initialisation handler failed."
42
+ );
43
+ }
44
+ if (!await host.services.fileProcessing.commitPendingFileEvents()) {
45
+ return reportPreparationFailure(
46
+ log,
47
+ "Database preparation stopped because pending file events could not be committed."
48
+ );
49
+ }
50
+ appLifecycle.markIsReady();
51
+ return scanResult;
52
+ } catch (error) {
53
+ log(error, LOG_LEVEL_VERBOSE);
54
+ throw error;
27
55
  }
28
- appLifecycle.markIsReady();
29
- return true;
30
56
  }
31
57
  function usePrepareDatabaseForUse(host) {
32
58
  const log = createInstanceLogFunction("SF:prepareDatabaseForUse", host.services.API);
33
59
  const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
34
- const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false) => {
35
- return await prepareDatabaseForUse(host, log, errorManager, showingNotice, reopenDatabase, ignoreSuspending);
60
+ const initialiseDatabaseHandler = async (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) => {
61
+ return await prepareDatabaseForUse(
62
+ host,
63
+ log,
64
+ errorManager,
65
+ showingNotice,
66
+ reopenDatabase,
67
+ ignoreSuspending,
68
+ continueOnFileFailure
69
+ );
36
70
  };
37
71
  host.services.databaseEvents.initialiseDatabase.addHandler(initialiseDatabaseHandler);
38
72
  }
@@ -34,5 +34,5 @@ export declare abstract class DatabaseEventService<T extends ServiceContext = Se
34
34
  * @param reopenDatabase Whether to reopen the database if it is already open.
35
35
  * @param ignoreSuspending Whether to ignore any suspending state.
36
36
  */
37
- readonly initialiseDatabase: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean) => Promise<boolean>>;
37
+ readonly initialiseDatabase: import("../lib/HandlerUtils.js").MultipleHandlerFunction<(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean) => Promise<import("./VaultScanResult.js").VaultScanResult>, unknown>;
38
38
  }
@@ -31,7 +31,7 @@ class DatabaseEventService extends ServiceBase {
31
31
  * @param reopenDatabase Whether to reopen the database if it is already open.
32
32
  * @param ignoreSuspending Whether to ignore any suspending state.
33
33
  */
34
- initialiseDatabase = handlers().bailFirstFailure("initialiseDatabase");
34
+ initialiseDatabase = handlers().bailFirstFailureWithResult("initialiseDatabase");
35
35
  }
36
36
  export {
37
37
  DatabaseEventService
@@ -16,6 +16,7 @@ import type { ActiveReplicatorContext, ContinuousReplicationRequest, Replication
16
16
  import type { RemoteResourceKind, RemoteResourceMap } from "../../replication/RemoteResource.js";
17
17
  import type { CentralRemoteAdministrationRequest, CentralRemoteAdministrationResult } from "../../replication/CentralRemoteAdministration.js";
18
18
  import type { MultipleHandlerFunction } from "../lib/HandlerUtils.js";
19
+ import type { VaultScanResult } from "./VaultScanResult.js";
19
20
  declare global {
20
21
  interface OPTIONAL_SYNC_FEATURES {
21
22
  DISABLE: "DISABLE";
@@ -86,7 +87,7 @@ export interface IDatabaseEventService {
86
87
  onDatabaseInitialised(showNotice: boolean): Promise<boolean>;
87
88
  onDatabaseHasReady(): Promise<boolean>;
88
89
  onResetDatabase(db: LiveSyncLocalDB): Promise<boolean>;
89
- initialiseDatabase(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean): Promise<boolean>;
90
+ initialiseDatabase(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean): Promise<VaultScanResult>;
90
91
  }
91
92
  export interface IKeyValueDBService {
92
93
  openSimpleStore<T>(kind: string): SimpleStore<T>;
@@ -348,7 +349,7 @@ export interface ITweakValueService {
348
349
  export interface IVaultService {
349
350
  vaultName(): string;
350
351
  getVaultName(): string;
351
- scanVault(showingNotice?: boolean, ignoreSuspending?: boolean): Promise<boolean>;
352
+ scanVault(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean): Promise<VaultScanResult>;
352
353
  isIgnoredByIgnoreFile(file: string | UXFileInfoStub): Promise<boolean>;
353
354
  isTargetFile(file: string | UXFileInfoStub): Promise<boolean>;
354
355
  isTargetFileInExtra(file: string | UXFileInfoStub): Promise<boolean>;
@@ -20,7 +20,7 @@ function createReplicationReadinessEvaluator(dependencies) {
20
20
  name: "application-ready",
21
21
  evaluate: () => {
22
22
  if (gates.isApplicationReady()) return true;
23
- diagnostics.log("Not ready");
23
+ diagnostics.log("Replication is not ready because application initialisation has not completed.");
24
24
  return false;
25
25
  }
26
26
  },
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Outcome of scanning the Vault against the local database.
3
+ *
4
+ * Boolean values preserve the existing success and failure contract. The
5
+ * additional value lets an ordinary start-up remain ready while reporting
6
+ * that one or more individual files still require attention.
7
+ */
8
+ export declare const VaultScanResults: {
9
+ readonly FAILED: false;
10
+ readonly COMPLETED: true;
11
+ readonly COMPLETED_WITH_FILE_FAILURES: "completed-with-file-failures";
12
+ };
13
+ export type VaultScanResult = (typeof VaultScanResults)[keyof typeof VaultScanResults];
@@ -0,0 +1,8 @@
1
+ const VaultScanResults = {
2
+ FAILED: false,
3
+ COMPLETED: true,
4
+ COMPLETED_WITH_FILE_FAILURES: "completed-with-file-failures"
5
+ };
6
+ export {
7
+ VaultScanResults
8
+ };
@@ -26,7 +26,7 @@ export declare abstract class VaultService<T extends ServiceContext = ServiceCon
26
26
  * @param showingNotice Whether to show a notice to the user.
27
27
  * @param ignoreSuspending Whether to ignore any suspending state.
28
28
  */
29
- readonly scanVault: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(showingNotice?: boolean, ignoreSuspending?: boolean) => Promise<boolean>>;
29
+ readonly scanVault: import("../lib/HandlerUtils.js").MultipleHandlerFunction<(showingNotice?: boolean, ignoreSuspending?: boolean, continueOnFileFailure?: boolean) => Promise<import("./VaultScanResult.js").VaultScanResult>, unknown>;
30
30
  /**
31
31
  * Check if a file is ignored by the ignore file (e.g., .gitignore, .obsidianignore).
32
32
  * @param file The file path or file info stub to check.
@@ -28,7 +28,7 @@ class VaultService extends ServiceBase {
28
28
  * @param showingNotice Whether to show a notice to the user.
29
29
  * @param ignoreSuspending Whether to ignore any suspending state.
30
30
  */
31
- scanVault = handlers().bailFirstFailure("scanVault");
31
+ scanVault = handlers().bailFirstFailureWithResult("scanVault");
32
32
  /**
33
33
  * Check if a file is ignored by the ignore file (e.g., .gitignore, .obsidianignore).
34
34
  * @param file The file path or file info stub to check.
@@ -182,6 +182,19 @@ export declare class AllHandler<T extends unknown[]> extends BooleanHandlerBase<
182
182
  */
183
183
  invoke(...args: T): Promise<boolean>;
184
184
  }
185
+ /**
186
+ * A handler that preserves a non-boolean continuation result while continuing
187
+ * to give false results and exceptions veto power.
188
+ */
189
+ export declare class AllWithResultHandler<T extends unknown[], U extends boolean | string> extends MultiBinder<BooleanHandlerFunc<T, U>> implements InvokableHandler<T, U> {
190
+ /**
191
+ * Invokes every handler in order. The first non-boolean continuation result
192
+ * is retained, but every remaining handler can still reject the operation.
193
+ * @param args The arguments to pass to the handlers.
194
+ * @returns False after any failure, otherwise the retained result or true.
195
+ */
196
+ invoke(...args: T): Promise<U>;
197
+ }
185
198
  /**
186
199
  * A handler that invokes every added handler sequentially and reports whether
187
200
  * all of them succeeded.
@@ -276,7 +289,7 @@ export interface MultipleHandlerFunction<TFunc extends (...args: any[]) => U | P
276
289
  * @param callback The handler function to add.
277
290
  * @returns A function to remove the added handler.
278
291
  */
279
- addHandler: (callback: TFunc) => () => void;
292
+ addHandler: (callback: TFunc, priority?: number) => () => void;
280
293
  /**
281
294
  * Removes a handler function.
282
295
  * @param callback The handler function to remove.
@@ -334,6 +347,7 @@ export interface BooleanMultiBinderInstance<T extends unknown[]> extends Invokab
334
347
  export declare function allFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
335
348
  export declare function allSettledFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
336
349
  export declare function bailFirstFailureFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
350
+ export declare function bailFirstFailureWithResultFunction<TFunc extends (...args: any[]) => Promise<boolean | string>>(name?: string): MultipleHandlerFunction<TFunc>;
337
351
  export declare function allParallelFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
338
352
  export declare function anySuccessFunction<TFunc extends (...args: any[]) => Promise<boolean>>(name?: string): BooleanMultipleHandlerFunction<TFunc>;
339
353
  export declare function firstResultFunction<TFunc extends (...args: any[]) => Promise<unknown>>(name?: string): MultipleHandlerFunction<TFunc>;
@@ -369,6 +383,14 @@ export declare function handlers<T extends object>(): {
369
383
  * @returns
370
384
  */
371
385
  bailFirstFailure<K extends FunctionKeys<T>>(name: K): BooleanMultipleHandlerFunction<Extract<T[K], (...args: any[]) => Promise<boolean>>>;
386
+ /**
387
+ * Create a handler that preserves a non-boolean continuation result, but
388
+ * still invokes later handlers so that false results and exceptions
389
+ * can reject the operation.
390
+ * @param name
391
+ * @returns
392
+ */
393
+ bailFirstFailureWithResult<K extends FunctionKeys<T>>(name: K): MultipleHandlerFunction<Extract<T[K], (...args: any[]) => Promise<boolean | string>>>;
372
394
  /**
373
395
  * Create a handler that invokes all added handler functions sequentially until one returns true.
374
396
  * @param name
@@ -207,6 +207,34 @@ class AllHandler extends BooleanHandlerBase {
207
207
  return true;
208
208
  }
209
209
  }
210
+ class AllWithResultHandler extends MultiBinder {
211
+ /**
212
+ * Invokes every handler in order. The first non-boolean continuation result
213
+ * is retained, but every remaining handler can still reject the operation.
214
+ * @param args The arguments to pass to the handlers.
215
+ * @returns False after any failure, otherwise the retained result or true.
216
+ */
217
+ async invoke(...args) {
218
+ let aggregateResult = true;
219
+ const callbacks = [...this._callbacks];
220
+ for (const callback of callbacks) {
221
+ try {
222
+ const result = await Promise.resolve(callback(...args));
223
+ if (result === false) {
224
+ return false;
225
+ }
226
+ if (aggregateResult === true && result !== true) {
227
+ aggregateResult = result;
228
+ }
229
+ } catch (error) {
230
+ Logger(`AllWithResultHandler ${this._name} treated error as failure!`, LOG_LEVEL_VERBOSE);
231
+ Logger(error, LOG_LEVEL_VERBOSE);
232
+ return false;
233
+ }
234
+ }
235
+ return aggregateResult;
236
+ }
237
+ }
210
238
  class SettledAllHandler extends BooleanHandlerBase {
211
239
  /**
212
240
  * Invokes every handler in order, treating a false result or an exception
@@ -321,6 +349,12 @@ function bailFirstFailureFunction(name) {
321
349
  const handler = new AllHandler(name ?? "bailFirstFailureFunction");
322
350
  return getMultipleBound(handler);
323
351
  }
352
+ function bailFirstFailureWithResultFunction(name) {
353
+ const handler = new AllWithResultHandler(
354
+ name ?? "bailFirstFailureWithResultFunction"
355
+ );
356
+ return getMultipleBound(handler);
357
+ }
324
358
  function allParallelFunction(name) {
325
359
  const handler = new ParallelAllHandler(name ?? "allParallelFunction");
326
360
  return getMultipleBound(handler);
@@ -390,6 +424,18 @@ function handlers() {
390
424
  bailFirstFailure(name) {
391
425
  return bailFirstFailureFunction(String(name));
392
426
  },
427
+ /**
428
+ * Create a handler that preserves a non-boolean continuation result, but
429
+ * still invokes later handlers so that false results and exceptions
430
+ * can reject the operation.
431
+ * @param name
432
+ * @returns
433
+ */
434
+ bailFirstFailureWithResult(name) {
435
+ return bailFirstFailureWithResultFunction(
436
+ String(name)
437
+ );
438
+ },
393
439
  /**
394
440
  * Create a handler that invokes all added handler functions sequentially until one returns true.
395
441
  * @param name
@@ -429,6 +475,7 @@ function handlers() {
429
475
  }
430
476
  export {
431
477
  AllHandler,
478
+ AllWithResultHandler,
432
479
  AnySuccessHandler,
433
480
  Binder,
434
481
  BooleanHandlerBase,
@@ -444,6 +491,7 @@ export {
444
491
  allSettledFunction,
445
492
  anySuccessFunction,
446
493
  bailFirstFailureFunction,
494
+ bailFirstFailureWithResultFunction,
447
495
  bindableFunction,
448
496
  dispatchParallelFunction,
449
497
  firstResultFunction,
@@ -24,9 +24,11 @@ Commonlib's injected remote-service tests verify that an owning operation can ab
24
24
 
25
25
  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.
26
26
 
27
- Commonlib's full offline scanner distinguishes completed, deliberately skipped, and failed storage/database pairs. Before pair construction, it withholds Metadata whose actual document ID disagrees with its recorded path, without reflection, deletion, or last-seen side effects. A consistently addressed entry for the same logical path continues through the established path-based flow; only a path without such an entry also withholds storage processing. The read-only actual-ID Inspector and separately invoked one-entry repair retain ambiguous states for review, revalidate an exact source revision, verify a target-first copy, and preserve the source on failure. Snapshot-restoration tests additionally verify that restored inclusions use current storage content, stale deletions are suppressed, rename halves are admitted from their current path states, and an inspection failure does not become evidence of absence. The following full scan remains the final reconciliation step. Self-hosted LiveSync keeps the existing Fast Setup aggregate policy and owns confirmation and presentation in its file/database repair interface.
27
+ Commonlib's full offline scanner distinguishes completed, deliberately skipped, and failed storage/database pairs. Before pair construction, it withholds Metadata whose actual document ID disagrees with its recorded path, without reflection, deletion, or last-seen side effects. A consistently addressed entry for the same logical path continues through the established path-based flow; only a path without such an entry also withholds storage processing. The read-only actual-ID Inspector and separately invoked one-entry repair retain ambiguous states for review, revalidate an exact source revision, verify a target-first copy, and preserve the source on failure. Snapshot-restoration tests additionally verify that restored inclusions use current storage content, stale deletions are suppressed, rename halves are admitted from their current path states, and an inspection failure does not become evidence of absence. The following full scan remains the final reconciliation step.
28
28
 
29
- Commonlib's local-database lifecycle tests distinguish the settings-selected database from the active wrapper. They verify replacement before a changed-suffix reset, fail-closed cleanup after rejected physical preparation, application readiness after the requested scan and completion hooks, and the prevention of rebuilt events and later remote mutation after a failed local reset. Self-hosted LiveSync owns settings persistence, setup flags, recovery dialogue, and real-Obsidian rebuild confirmation; those host results remain downstream evidence rather than Commonlib guarantees.
29
+ The strict aggregate remains the default for Fast Setup, Fetch, Rebuild, direct scans, and CLI mirror or daemon paths. Self-hosted LiveSync opts only ordinary Obsidian start-up into continuing after individual pair failures. Commonlib returns the distinct `completed-with-file-failures` result, retains each failed result and retry state, records each affected path at verbose level, and leaves any user-facing notice to the host. Focused Commonlib tests cover the default and an explicitly continuing scan; downstream type checks, unit tests, and builds exercise the exact packed implementation and host call. Android-specific storage behaviour remains real-device and reporter-environment evidence rather than a Commonlib guarantee.
30
+
31
+ Commonlib's local-database lifecycle tests distinguish the settings-selected database from the active wrapper. They verify replacement before a changed-suffix reset, fail-closed cleanup after rejected physical preparation, application readiness after the requested scan and completion hooks, verbose diagnostics for rejected preparation stages, and the prevention of rebuilt events and later remote mutation after a failed local reset. Replication readiness tests verify that a descriptive application-initialisation diagnostic replaces the former bare diagnostic. Self-hosted LiveSync owns settings persistence, setup flags, user-facing notices, recovery dialogue, and real-Obsidian rebuild confirmation; those host results remain downstream evidence rather than Commonlib guarantees.
30
32
 
31
33
  The focused `/replication` entry defines the provider capability, configuration identity, same-kind reconciliation, interaction authority, and truthful outcome contracts used when a host composes replication. A host selects its fixed provider subset and registers each definition during composition; an omitted role remains an explicit unsupported result rather than falling back to another provider. Commonlib serialises active replacement, rejects a candidate whose configuration changes before publication, and provides a transition-aware context acquisition boundary. The entry does not own user dialogue, P2P room policy, or selection of unknown providers.
32
34
 
@@ -1,8 +1,8 @@
1
1
  ---
2
- date: 2026-08-30
3
- commonlib-version: "0.1.20"
4
- self-hosted-livesync-version: "1.0.21"
5
- status: accepted
2
+ date: 2026-09-04
3
+ commonlib-version: "0.1.21"
4
+ self-hosted-livesync-version: "1.0.24"
5
+ status: unreleased
6
6
  ---
7
7
 
8
8
  # Service feature composition
@@ -76,8 +76,9 @@ export async function prepareDatabaseForUse(
76
76
  errorManager: UnresolvedErrorManager,
77
77
  showingNotice = false,
78
78
  reopenDatabase = true,
79
- ignoreSuspending = false
80
- ): Promise<boolean> {
79
+ ignoreSuspending = false,
80
+ continueOnFileFailure = false
81
+ ): Promise<VaultScanResult> {
81
82
  // Open, scan, run completion handlers, commit pending events, and mark ready.
82
83
  }
83
84
  ```
@@ -90,14 +91,26 @@ export function usePrepareDatabaseForUse(host: PrepareDatabaseHost): void {
90
91
  const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events);
91
92
 
92
93
  host.services.databaseEvents.initialiseDatabase.addHandler(
93
- (showingNotice = false, reopenDatabase = true, ignoreSuspending = false) =>
94
- prepareDatabaseForUse(host, log, errorManager, showingNotice, reopenDatabase, ignoreSuspending)
94
+ (showingNotice = false, reopenDatabase = true, ignoreSuspending = false, continueOnFileFailure = false) =>
95
+ prepareDatabaseForUse(
96
+ host,
97
+ log,
98
+ errorManager,
99
+ showingNotice,
100
+ reopenDatabase,
101
+ ignoreSuspending,
102
+ continueOnFileFailure
103
+ )
95
104
  );
96
105
  }
97
106
  ```
98
107
 
99
108
  Test the operation's sequencing and failure short-circuiting separately from the handler registration. This keeps a lifecycle rename from forcing every domain test through an application-shaped fixture.
100
109
 
110
+ Database preparation and full scanning remain strict by default. A host may set `continueOnFileFailure` only when its ordinary start-up policy deliberately permits readiness after individual file-pair failures. In that case, the operation returns `completed-with-file-failures` after all later initialisation steps succeed; it does not collapse the result into `true`. That option does not admit a failed scan precondition or a later initialisation step, and explicit recovery workflows retain the default.
111
+
112
+ The `scanVault` and `initialiseDatabase` services use a failure-veto result aggregator for this contract. It preserves the first non-boolean continuation result but still invokes later handlers, so a later `false` result or exception rejects the operation. `firstResult` is not suitable here because it would return the partial result before those handlers can veto it.
113
+
101
114
  ## Keep feature-local state private
102
115
 
103
116
  State which exists for one predicate, cache, queue, or scheduling policy can remain in a factory or private context. It does not need to become a property on the application core.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrtmrz/livesync-commonlib",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",