@vrtmrz/livesync-commonlib 0.1.20 → 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.
- package/dist/replication/ReplicatorProvider.d.ts +16 -3
- package/dist/replication/ReplicatorProvider.js +6 -1
- package/dist/replication/index.d.ts +2 -2
- package/dist/replication/index.js +2 -0
- package/dist/replication/trystero/useP2PReplicatorFeature.js +2 -1
- package/dist/serviceFeatures/offlineScanner.d.ts +16 -8
- package/dist/serviceFeatures/offlineScanner.js +18 -6
- package/dist/serviceFeatures/prepareDatabaseForUse.d.ts +4 -2
- package/dist/serviceFeatures/prepareDatabaseForUse.js +60 -26
- package/dist/services/base/DatabaseEventService.d.ts +1 -1
- package/dist/services/base/DatabaseEventService.js +1 -1
- package/dist/services/base/IService.d.ts +3 -2
- package/dist/services/base/ReplicationService.js +2 -0
- package/dist/services/base/ReplicationService.readiness.js +1 -1
- package/dist/services/base/ReplicationService.typedReplication.d.ts +10 -0
- package/dist/services/base/ReplicationService.typedReplication.js +58 -35
- package/dist/services/base/VaultScanResult.d.ts +13 -0
- package/dist/services/base/VaultScanResult.js +8 -0
- package/dist/services/base/VaultService.d.ts +1 -1
- package/dist/services/base/VaultService.js +1 -1
- package/dist/services/lib/HandlerUtils.d.ts +23 -1
- package/dist/services/lib/HandlerUtils.js +48 -0
- package/docs/proven-in-use.md +4 -2
- package/docs/service-feature-composition.md +21 -8
- package/package.json +1 -1
|
@@ -68,9 +68,22 @@ export interface UserInitiatedReplicationAuthority {
|
|
|
68
68
|
export type ReplicationInteraction = NoInteraction | UserInitiatedReplicationAuthority;
|
|
69
69
|
/** Authority carried through a replication request and its failure path. */
|
|
70
70
|
export type InteractionAuthority = ReplicationInteraction;
|
|
71
|
-
/**
|
|
71
|
+
/** Stable host presentation choices for routine OneShot progress. */
|
|
72
|
+
export declare const REPLICATION_PROGRESS_PRESENTATIONS: Readonly<{
|
|
73
|
+
readonly QUIET: "quiet";
|
|
74
|
+
readonly NOTICE: "notice";
|
|
75
|
+
}>;
|
|
76
|
+
export type ReplicationProgressPresentation = (typeof REPLICATION_PROGRESS_PRESENTATIONS)[keyof typeof REPLICATION_PROGRESS_PRESENTATIONS];
|
|
77
|
+
/**
|
|
78
|
+
* A request made by an explicitly user-initiated one-shot command.
|
|
79
|
+
*
|
|
80
|
+
* Progress presentation is independent of interaction authority: a quiet
|
|
81
|
+
* command may still open an authorised peer-selection or failure-recovery
|
|
82
|
+
* dialogue when the operation requires a decision.
|
|
83
|
+
*/
|
|
72
84
|
export interface UserInitiatedOneShotRequest {
|
|
73
85
|
readonly trigger: "manual";
|
|
86
|
+
readonly progressPresentation: ReplicationProgressPresentation;
|
|
74
87
|
readonly interaction: InteractionAuthority;
|
|
75
88
|
}
|
|
76
89
|
/** A request made by an automatic finite replication trigger. */
|
|
@@ -193,9 +206,9 @@ export interface ReplicationAttemptFailure {
|
|
|
193
206
|
readonly setting: ObsidianLiveSyncSettings;
|
|
194
207
|
readonly outcome: ReplicationFailed;
|
|
195
208
|
}
|
|
196
|
-
/** Immutable failure source plus
|
|
209
|
+
/** Immutable failure source plus presentation and interaction policy from its original request. */
|
|
197
210
|
export interface ReplicationFailureRequest extends ReplicationAttemptFailure {
|
|
198
|
-
readonly
|
|
211
|
+
readonly progressPresentation: ReplicationProgressPresentation;
|
|
199
212
|
readonly interaction: InteractionAuthority;
|
|
200
213
|
}
|
|
201
214
|
/** Return whether one detached setting still belongs to the admitted publication. */
|
|
@@ -16,6 +16,10 @@ const USER_INITIATED_REPLICATION_AUTHORITY = Object.freeze({
|
|
|
16
16
|
failureRecovery: true
|
|
17
17
|
})
|
|
18
18
|
});
|
|
19
|
+
const REPLICATION_PROGRESS_PRESENTATIONS = Object.freeze({
|
|
20
|
+
QUIET: "quiet",
|
|
21
|
+
NOTICE: "notice"
|
|
22
|
+
});
|
|
19
23
|
const REPLICATION_COMPLETED = Object.freeze({ status: "completed" });
|
|
20
24
|
const REPLICATION_CANCELLED = Object.freeze({ status: "cancelled" });
|
|
21
25
|
const CENTRAL_REMOTE_REPLICATION_READINESS = Object.freeze({
|
|
@@ -74,7 +78,7 @@ function supportedOpenReplicationOneShot() {
|
|
|
74
78
|
const result = await replicator.openReplication(
|
|
75
79
|
setting,
|
|
76
80
|
false,
|
|
77
|
-
request.
|
|
81
|
+
request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
|
|
78
82
|
false
|
|
79
83
|
);
|
|
80
84
|
return outcomeFromFiniteOpenReplication(result);
|
|
@@ -129,6 +133,7 @@ export {
|
|
|
129
133
|
PEER_REPLICATION_READINESS,
|
|
130
134
|
REPLICATION_CANCELLED,
|
|
131
135
|
REPLICATION_COMPLETED,
|
|
136
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
132
137
|
USER_INITIATED_REPLICATION_AUTHORITY,
|
|
133
138
|
defineReplicatorProviderDefinitions,
|
|
134
139
|
isActiveReplicatorContextBoundToSetting,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
|
|
1
|
+
export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, REPLICATION_PROGRESS_PRESENTATIONS, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
|
|
2
2
|
export type { ReplicatorInstance } from "./ReplicatorInstance.js";
|
|
3
|
-
export type { ActiveReplicatorContext, CapabilitySupport, ContinuousReplicationRequest, ContinuousReplicationTrigger, ContinuousRunner, InteractionAuthority, InteractionPermissions, NotApplicableCapability, NotImplementedCapability, NoInteraction, PermittedInteractionPermissions, ReplicationBlockReason, ReplicationAttemptFailure, ReplicationCancelled, ReplicationCompleted, ReplicationFailed, ReplicationInteraction, ReplicationOutcome, ReplicationReadinessRequirements, ReplicationPartial, ReplicationFailureRequest, ReplicatorConfigurationIdentity, ReplicatorProviderDefinition, ReplicatorProviderDefinitionMap, StopActiveTransferRunner, SupportedCapability, UnattendedOneShotRequest, UnattendedReplicationTrigger, UnattendedOneShotRunner, UserInitiatedOneShotRequest, UserInitiatedReplicationAuthority, UserInitiatedOneShotRunner, } from "./ReplicatorProvider.js";
|
|
3
|
+
export type { ActiveReplicatorContext, CapabilitySupport, ContinuousReplicationRequest, ContinuousReplicationTrigger, ContinuousRunner, InteractionAuthority, InteractionPermissions, NotApplicableCapability, NotImplementedCapability, NoInteraction, PermittedInteractionPermissions, ReplicationBlockReason, ReplicationAttemptFailure, ReplicationCancelled, ReplicationCompleted, ReplicationFailed, ReplicationInteraction, ReplicationOutcome, ReplicationProgressPresentation, ReplicationReadinessRequirements, ReplicationPartial, ReplicationFailureRequest, ReplicatorConfigurationIdentity, ReplicatorProviderDefinition, ReplicatorProviderDefinitionMap, StopActiveTransferRunner, SupportedCapability, UnattendedOneShotRequest, UnattendedReplicationTrigger, UnattendedOneShotRunner, UserInitiatedOneShotRequest, UserInitiatedReplicationAuthority, UserInitiatedOneShotRunner, } from "./ReplicatorProvider.js";
|
|
4
4
|
export { CAPABILITY_SUPPORT_KINDS, CAPABILITY_UNAVAILABLE_REASONS } from "./ProviderCapability.js";
|
|
5
5
|
export { NO_REMOTE_RESOURCE_CAPABILITIES, REMOTE_RESOURCE_KINDS } from "./RemoteResource.js";
|
|
6
6
|
export type { ConnectionProbeFactory, PreferredTweakProbe, PreferredTweakProbeFactory, RemoteConnectionProbe, RemoteConnectionProbeOptions, RemoteConnectionProbeResult, RemoteResourceCapabilities, RemoteResourceFactoryMap, RemoteResourceKind, RemoteResourceMap, SecuritySeedResource, SecuritySeedResourceFactory, SynchronisationInformationResource, SynchronisationInformationResourceFactory, } from "./RemoteResource.js";
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
PEER_REPLICATION_READINESS,
|
|
7
7
|
REPLICATION_CANCELLED,
|
|
8
8
|
REPLICATION_COMPLETED,
|
|
9
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
9
10
|
USER_INITIATED_REPLICATION_AUTHORITY,
|
|
10
11
|
defineReplicatorProviderDefinitions,
|
|
11
12
|
isReplicationCompleted,
|
|
@@ -50,6 +51,7 @@ export {
|
|
|
50
51
|
REMOTE_RESOURCE_KINDS,
|
|
51
52
|
REPLICATION_CANCELLED,
|
|
52
53
|
REPLICATION_COMPLETED,
|
|
54
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
53
55
|
USER_INITIATED_REPLICATION_AUTHORITY,
|
|
54
56
|
applyCentralRemoteAdministrationMutation,
|
|
55
57
|
centralRemoteAdministrationVerificationFailed,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CAPABILITY_NOT_APPLICABLE,
|
|
7
7
|
NO_REMOTE_RESOURCE_CAPABILITIES,
|
|
8
8
|
PEER_REPLICATION_READINESS,
|
|
9
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
9
10
|
defineReplicatorProviderDefinitions,
|
|
10
11
|
outcomeFromFiniteOpenReplication,
|
|
11
12
|
replicationBlocked,
|
|
@@ -38,7 +39,7 @@ function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFa
|
|
|
38
39
|
const result = await instance.openReplication(
|
|
39
40
|
setting,
|
|
40
41
|
false,
|
|
41
|
-
request.
|
|
42
|
+
request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
|
|
42
43
|
false
|
|
43
44
|
);
|
|
44
45
|
return outcomeFromFiniteOpenReplication(result);
|
|
@@ -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
|
|
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<
|
|
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
|
|
246
|
-
* @param ignoreSuspending
|
|
247
|
-
* @returns
|
|
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<
|
|
250
|
-
export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise<
|
|
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
|
-
|
|
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,
|
|
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
|
-
* @
|
|
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<
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
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").
|
|
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().
|
|
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<
|
|
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<
|
|
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>;
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
CAPABILITY_SUPPORT_KINDS,
|
|
12
12
|
NO_INTERACTION,
|
|
13
13
|
CENTRAL_REMOTE_REPLICATION_READINESS,
|
|
14
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
14
15
|
USER_INITIATED_REPLICATION_AUTHORITY,
|
|
15
16
|
isActiveReplicatorContextBoundToSetting,
|
|
16
17
|
isReplicationCompleted,
|
|
@@ -196,6 +197,7 @@ class ReplicationService extends ServiceBase {
|
|
|
196
197
|
onReplicationFailed = handlers().bailFirstFailure("onReplicationFailed");
|
|
197
198
|
async replicateUserInitiated(request = {
|
|
198
199
|
trigger: "manual",
|
|
200
|
+
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
|
|
199
201
|
interaction: USER_INITIATED_REPLICATION_AUTHORITY
|
|
200
202
|
}) {
|
|
201
203
|
return await this._typedReplication.runUserInitiated(request);
|
|
@@ -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("
|
|
23
|
+
diagnostics.log("Replication is not ready because application initialisation has not completed.");
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
},
|
|
@@ -29,6 +29,7 @@ export interface TypedReplicationCoordinatorDependencies {
|
|
|
29
29
|
*/
|
|
30
30
|
export declare class TypedReplicationCoordinator {
|
|
31
31
|
private readonly dependencies;
|
|
32
|
+
private oneShotInProgress;
|
|
32
33
|
constructor(dependencies: TypedReplicationCoordinatorDependencies);
|
|
33
34
|
/**
|
|
34
35
|
* Execute a user-initiated finite role with the request's interaction authority.
|
|
@@ -67,6 +68,15 @@ export declare class TypedReplicationCoordinator {
|
|
|
67
68
|
* not a request to begin new remote work.
|
|
68
69
|
*/
|
|
69
70
|
stopActiveTransfer(): Promise<ReplicationOutcome>;
|
|
71
|
+
/**
|
|
72
|
+
* Admit one complete typed OneShot attempt for this service instance.
|
|
73
|
+
*
|
|
74
|
+
* Ownership is recorded before the operation reaches its first await and is
|
|
75
|
+
* retained through readiness, provider work, activity settlement, and
|
|
76
|
+
* failure handling. Later requests are neither queued nor joined: they
|
|
77
|
+
* settle immediately so repeated UI actions cannot replay after the owner.
|
|
78
|
+
*/
|
|
79
|
+
private runOneShot;
|
|
70
80
|
private finishFiniteAttempt;
|
|
71
81
|
private acquireContext;
|
|
72
82
|
private acquireReadyContext;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CAPABILITY_SUPPORT_KINDS,
|
|
3
3
|
NO_INTERACTION,
|
|
4
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
4
5
|
isActiveReplicatorContextBoundToSetting,
|
|
5
6
|
replicationBlocked,
|
|
6
7
|
replicationFailed
|
|
@@ -11,6 +12,7 @@ class TypedReplicationCoordinator {
|
|
|
11
12
|
this.dependencies = dependencies;
|
|
12
13
|
}
|
|
13
14
|
dependencies;
|
|
15
|
+
oneShotInProgress = false;
|
|
14
16
|
/**
|
|
15
17
|
* Execute a user-initiated finite role with the request's interaction authority.
|
|
16
18
|
*
|
|
@@ -20,23 +22,25 @@ class TypedReplicationCoordinator {
|
|
|
20
22
|
* activity has settled.
|
|
21
23
|
*/
|
|
22
24
|
async runUserInitiated(request) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
return await this.runOneShot(async () => {
|
|
26
|
+
const showProgress = request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE;
|
|
27
|
+
const ready = await this.acquireReadyContext(showProgress);
|
|
28
|
+
if ("status" in ready) return ready;
|
|
29
|
+
const capability = ready.provider.userInitiatedOneShot;
|
|
30
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
31
|
+
return this.finishFiniteAttempt(replicationBlocked(capability.reason));
|
|
32
|
+
}
|
|
33
|
+
const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
|
|
34
|
+
return this.finishFiniteAttempt(
|
|
35
|
+
await this.runAdmittedFiniteActivity(
|
|
36
|
+
ready,
|
|
37
|
+
(context) => capability.run(context.replicator, settings, request),
|
|
38
|
+
settings,
|
|
39
|
+
request.interaction,
|
|
40
|
+
request.progressPresentation
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
});
|
|
40
44
|
}
|
|
41
45
|
/**
|
|
42
46
|
* Execute an unattended finite role without granting interaction authority.
|
|
@@ -49,22 +53,24 @@ class TypedReplicationCoordinator {
|
|
|
49
53
|
if (request.interaction.kind !== NO_INTERACTION.kind) {
|
|
50
54
|
return replicationBlocked("interaction-required");
|
|
51
55
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
return await this.runOneShot(async () => {
|
|
57
|
+
const ready = await this.acquireReadyContext(false);
|
|
58
|
+
if ("status" in ready) return ready;
|
|
59
|
+
const capability = ready.provider.unattendedOneShot;
|
|
60
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
61
|
+
return this.finishFiniteAttempt(replicationBlocked(capability.reason));
|
|
62
|
+
}
|
|
63
|
+
const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
|
|
64
|
+
return this.finishFiniteAttempt(
|
|
65
|
+
await this.runAdmittedFiniteActivity(
|
|
66
|
+
ready,
|
|
67
|
+
(context) => capability.run(context.replicator, settings, request),
|
|
68
|
+
settings,
|
|
69
|
+
NO_INTERACTION,
|
|
70
|
+
REPLICATION_PROGRESS_PRESENTATIONS.QUIET
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
70
76
|
* Start the provider's continuous role outside finite activity accounting.
|
|
@@ -130,6 +136,23 @@ class TypedReplicationCoordinator {
|
|
|
130
136
|
);
|
|
131
137
|
return admitted ?? replicationBlocked("no-active-replicator");
|
|
132
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Admit one complete typed OneShot attempt for this service instance.
|
|
141
|
+
*
|
|
142
|
+
* Ownership is recorded before the operation reaches its first await and is
|
|
143
|
+
* retained through readiness, provider work, activity settlement, and
|
|
144
|
+
* failure handling. Later requests are neither queued nor joined: they
|
|
145
|
+
* settle immediately so repeated UI actions cannot replay after the owner.
|
|
146
|
+
*/
|
|
147
|
+
async runOneShot(run) {
|
|
148
|
+
if (this.oneShotInProgress) return replicationBlocked("replication-in-progress");
|
|
149
|
+
this.oneShotInProgress = true;
|
|
150
|
+
try {
|
|
151
|
+
return await run();
|
|
152
|
+
} finally {
|
|
153
|
+
this.oneShotInProgress = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
133
156
|
finishFiniteAttempt(outcome) {
|
|
134
157
|
this.dependencies.recordFiniteAttempt();
|
|
135
158
|
return outcome;
|
|
@@ -164,7 +187,7 @@ class TypedReplicationCoordinator {
|
|
|
164
187
|
* is rejected instead of running work prepared for an older publication.
|
|
165
188
|
* Failure recovery runs only after the reservation has been released.
|
|
166
189
|
*/
|
|
167
|
-
async runAdmittedFiniteActivity(expectedContext, run, setting, interaction,
|
|
190
|
+
async runAdmittedFiniteActivity(expectedContext, run, setting, interaction, progressPresentation) {
|
|
168
191
|
const admitted = await this.dependencies.replicatorService.runWithActiveReplicatorContext((context) => {
|
|
169
192
|
if (context !== expectedContext || !isActiveReplicatorContextBoundToSetting(context, setting)) {
|
|
170
193
|
return replicationBlocked("not-ready");
|
|
@@ -178,7 +201,7 @@ class TypedReplicationCoordinator {
|
|
|
178
201
|
context: expectedContext,
|
|
179
202
|
setting,
|
|
180
203
|
outcome: result,
|
|
181
|
-
|
|
204
|
+
progressPresentation,
|
|
182
205
|
interaction
|
|
183
206
|
})
|
|
184
207
|
);
|
|
@@ -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];
|
|
@@ -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").
|
|
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().
|
|
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,
|
package/docs/proven-in-use.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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-
|
|
3
|
-
commonlib-version: "0.1.
|
|
4
|
-
self-hosted-livesync-version: "1.0.
|
|
5
|
-
status:
|
|
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
|
-
|
|
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(
|
|
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