@vrtmrz/livesync-commonlib 0.1.19 → 0.1.21
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/README.md +5 -1
- package/dist/common/models/setting.p2p.d.ts +2 -0
- package/dist/common/models/setting.p2p.js +5 -0
- package/dist/interfaces/ServiceModule.d.ts +13 -4
- package/dist/managers/ChunkFetcher.js +10 -0
- package/dist/p2p/P2PService.d.ts +123 -0
- package/dist/p2p/P2PService.js +271 -0
- package/dist/p2p/index.d.ts +8 -0
- package/dist/p2p/index.js +8 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
- package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
- package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
- package/dist/pouchdb/ReplicatorShim.js +78 -5
- package/dist/replication/CentralCompatibility.d.ts +38 -0
- package/dist/replication/CentralCompatibility.js +39 -0
- package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
- package/dist/replication/CentralRemoteAdministration.js +71 -0
- package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
- package/dist/replication/ProviderCapability.d.ts +32 -0
- package/dist/replication/ProviderCapability.js +27 -0
- package/dist/replication/RemoteResource.d.ts +85 -0
- package/dist/replication/RemoteResource.js +17 -0
- package/dist/replication/ReplicatorInstance.d.ts +23 -0
- package/dist/replication/ReplicatorProvider.d.ts +262 -0
- package/dist/replication/ReplicatorProvider.js +150 -0
- package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
- package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
- package/dist/replication/index.d.ts +10 -0
- package/dist/replication/index.js +72 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
- package/dist/replication/journal/JournalSyncCore.js +18 -5
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
- package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
- package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
- package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
- package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
- package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
- package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
- package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
- package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
- package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
- package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
- package/dist/replication/trystero/P2PReplicationWire.js +50 -0
- package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
- package/dist/replication/trystero/P2PRoomSession.js +101 -0
- package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
- package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
- package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
- package/dist/replication/trystero/TrysteroReplicator.js +273 -113
- package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
- package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
- package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
- package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
- package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
- package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
- package/dist/replication/trystero/types.d.ts +18 -0
- package/dist/replication/trystero/types.js +5 -1
- package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
- package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
- package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
- package/dist/replication/trystero/useP2PReplicatorFeature.js +73 -55
- package/dist/rpc/RpcRoom.d.ts +15 -1
- package/dist/rpc/RpcRoom.js +176 -41
- package/dist/rpc/RpcSession.d.ts +3 -1
- package/dist/rpc/RpcSession.js +2 -2
- package/dist/rpc/index.d.ts +1 -1
- package/dist/rpc/transports/trysteroUtils.js +2 -1
- package/dist/rpc/types.d.ts +26 -1
- package/dist/serviceFeatures/checkRemoteSize.js +5 -2
- package/dist/serviceModules/Rebuilder.js +66 -56
- package/dist/services/base/AppLifecycleService.d.ts +4 -0
- package/dist/services/base/AppLifecycleService.js +4 -0
- package/dist/services/base/ControlService.js +0 -4
- package/dist/services/base/DatabaseEventService.js +1 -1
- package/dist/services/base/IService.d.ts +105 -8
- package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
- package/dist/services/base/ReadinessEvaluator.js +16 -0
- package/dist/services/base/RemoteConnection.d.ts +3 -0
- package/dist/services/base/RemoteService.js +2 -2
- package/dist/services/base/ReplicationService.d.ts +45 -10
- package/dist/services/base/ReplicationService.js +250 -91
- package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
- package/dist/services/base/ReplicationService.readiness.js +87 -0
- package/dist/services/base/ReplicationService.typedReplication.d.ts +93 -0
- package/dist/services/base/ReplicationService.typedReplication.js +214 -0
- package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
- package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
- package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
- package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
- package/dist/services/base/ReplicatorService.d.ts +75 -14
- package/dist/services/base/ReplicatorService.js +232 -53
- package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
- package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
- package/dist/services/base/TweakValueService.d.ts +3 -2
- package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
- package/dist/services/lib/HandlerUtils.d.ts +24 -0
- package/dist/services/lib/HandlerUtils.js +40 -0
- package/docs/development.md +40 -7
- package/docs/p2p-transport-lifecycle.md +135 -11
- package/docs/proven-in-use.md +5 -1
- package/docs/remote-configurations.md +4 -0
- package/docs/service-feature-composition.md +250 -0
- package/package.json +17 -1
- package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
- package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
- package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
- package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
- package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
- /package/dist/replication/{trystero/P2PReplicatorBase.js → ReplicatorInstance.js} +0 -0
|
@@ -6,12 +6,92 @@ import {
|
|
|
6
6
|
import { handlers } from "../lib/HandlerUtils.js";
|
|
7
7
|
import { ServiceBase } from "./ServiceBase.js";
|
|
8
8
|
import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
|
|
9
|
-
import { createInstanceLogFunction
|
|
9
|
+
import { createInstanceLogFunction } from "../lib/logUtils.js";
|
|
10
|
+
import {
|
|
11
|
+
CAPABILITY_SUPPORT_KINDS,
|
|
12
|
+
NO_INTERACTION,
|
|
13
|
+
CENTRAL_REMOTE_REPLICATION_READINESS,
|
|
14
|
+
REPLICATION_PROGRESS_PRESENTATIONS,
|
|
15
|
+
USER_INITIATED_REPLICATION_AUTHORITY,
|
|
16
|
+
isActiveReplicatorContextBoundToSetting,
|
|
17
|
+
isReplicationCompleted,
|
|
18
|
+
replicationBlocked,
|
|
19
|
+
replicationFailed,
|
|
20
|
+
outcomeFromFiniteOpenReplication
|
|
21
|
+
} from "../../replication/ReplicatorProvider.js";
|
|
10
22
|
import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
|
|
11
23
|
import { isLockAcquired, shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
|
24
|
+
import {
|
|
25
|
+
createReplicationReadinessEvaluator
|
|
26
|
+
} from "./ReplicationService.readiness.js";
|
|
27
|
+
import { TypedReplicationCoordinator } from "./ReplicationService.typedReplication.js";
|
|
28
|
+
import { asCopy } from "../../common/utils.object.js";
|
|
12
29
|
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5e3;
|
|
30
|
+
const DIRECTIONAL_REPLICATION = Object.freeze({
|
|
31
|
+
UPLOAD: "upload",
|
|
32
|
+
DOWNLOAD: "download"
|
|
33
|
+
});
|
|
34
|
+
const DIRECTIONAL_REPLICATION_ADMISSION = Object.freeze({
|
|
35
|
+
ORDINARY: "ordinary",
|
|
36
|
+
EXPLICIT_REBUILD: "explicit-rebuild"
|
|
37
|
+
});
|
|
38
|
+
function canUploadAll(replicator) {
|
|
39
|
+
return "replicateAllToServer" in replicator && typeof replicator.replicateAllToServer === "function";
|
|
40
|
+
}
|
|
41
|
+
function canUploadAllWithOutcome(replicator) {
|
|
42
|
+
return "replicateAllToServerWithOutcome" in replicator && typeof replicator.replicateAllToServerWithOutcome === "function";
|
|
43
|
+
}
|
|
44
|
+
function canDownloadAll(replicator) {
|
|
45
|
+
return "replicateAllFromServer" in replicator && typeof replicator.replicateAllFromServer === "function";
|
|
46
|
+
}
|
|
47
|
+
function canDownloadAllWithOutcome(replicator) {
|
|
48
|
+
return "replicateAllFromServerWithOutcome" in replicator && typeof replicator.replicateAllFromServerWithOutcome === "function";
|
|
49
|
+
}
|
|
50
|
+
function canAdministerLegacyCentralRemote(replicator) {
|
|
51
|
+
return "markRemoteLocked" in replicator && typeof replicator.markRemoteLocked === "function" && "markRemoteResolved" in replicator && typeof replicator.markRemoteResolved === "function";
|
|
52
|
+
}
|
|
53
|
+
async function stopActiveTransferForDirectionalReplication(context) {
|
|
54
|
+
const capability = context.provider.stopActiveTransfer;
|
|
55
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
56
|
+
return replicationBlocked(capability.reason);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const outcome = await capability.run(context.replicator);
|
|
60
|
+
return outcome.status === "failed" ? replicationBlocked("not-ready") : outcome;
|
|
61
|
+
} catch {
|
|
62
|
+
return replicationBlocked("not-ready");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function runDirectionalReplication(context, setting, direction, showingNotice) {
|
|
66
|
+
if (!isActiveReplicatorContextBoundToSetting(context, setting)) {
|
|
67
|
+
return replicationBlocked("not-ready");
|
|
68
|
+
}
|
|
69
|
+
const stopOutcome = await stopActiveTransferForDirectionalReplication(context);
|
|
70
|
+
if (!isReplicationCompleted(stopOutcome)) return stopOutcome;
|
|
71
|
+
try {
|
|
72
|
+
let result;
|
|
73
|
+
if (direction === DIRECTIONAL_REPLICATION.UPLOAD) {
|
|
74
|
+
if (canUploadAllWithOutcome(context.replicator)) {
|
|
75
|
+
return await context.replicator.replicateAllToServerWithOutcome(setting, showingNotice);
|
|
76
|
+
}
|
|
77
|
+
if (!canUploadAll(context.replicator)) return replicationBlocked("capability-not-applicable");
|
|
78
|
+
result = await context.replicator.replicateAllToServer(setting, showingNotice);
|
|
79
|
+
} else {
|
|
80
|
+
if (canDownloadAllWithOutcome(context.replicator)) {
|
|
81
|
+
return await context.replicator.replicateAllFromServerWithOutcome(setting, showingNotice);
|
|
82
|
+
}
|
|
83
|
+
if (!canDownloadAll(context.replicator)) return replicationBlocked("capability-not-applicable");
|
|
84
|
+
result = await context.replicator.replicateAllFromServer(setting, showingNotice);
|
|
85
|
+
}
|
|
86
|
+
return outcomeFromFiniteOpenReplication(result);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return replicationFailed(error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
13
91
|
class ReplicationService extends ServiceBase {
|
|
14
92
|
_unresolvedErrorManager;
|
|
93
|
+
_evaluateReadiness;
|
|
94
|
+
_typedReplication;
|
|
15
95
|
showError(msg, max_log_level = LOG_LEVEL_NOTICE) {
|
|
16
96
|
this._unresolvedErrorManager.showError(msg, max_log_level);
|
|
17
97
|
}
|
|
@@ -38,6 +118,36 @@ class ReplicationService extends ServiceBase {
|
|
|
38
118
|
dependencies.appLifecycleService,
|
|
39
119
|
this.context.events
|
|
40
120
|
);
|
|
121
|
+
this._evaluateReadiness = createReplicationReadinessEvaluator({
|
|
122
|
+
gates: {
|
|
123
|
+
isApplicationReady: () => this.appLifecycleService.isReady(),
|
|
124
|
+
runPolicyChecks: (showMessage) => this.onCheckReplicationReady(showMessage),
|
|
125
|
+
currentSettings: () => this.settingService.currentSettings(),
|
|
126
|
+
isCleanupRunning: () => isLockAcquired("cleanup"),
|
|
127
|
+
isOnline: () => this.APIService.isOnline
|
|
128
|
+
},
|
|
129
|
+
preparation: {
|
|
130
|
+
commitPendingFileEvents: () => this.fileProcessing.commitPendingFileEvents(),
|
|
131
|
+
prepareCentralRemote: (showMessage) => this.onPrepareCentralRemoteReplication(showMessage),
|
|
132
|
+
runBeforeReplicate: (showMessage) => this.onBeforeReplicate(showMessage)
|
|
133
|
+
},
|
|
134
|
+
diagnostics: {
|
|
135
|
+
getUnresolvedMessages: () => this.appLifecycleService.getUnresolvedMessages(),
|
|
136
|
+
translate: (key) => this.context.translate(key),
|
|
137
|
+
log: this._log,
|
|
138
|
+
showError: (message, maxLogLevel) => this.showError(message, maxLogLevel),
|
|
139
|
+
clearErrors: () => this.clearErrors()
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
this._typedReplication = new TypedReplicationCoordinator({
|
|
143
|
+
replicatorService: this.replicatorService,
|
|
144
|
+
currentSettings: () => this.settingService.currentSettings(),
|
|
145
|
+
checkReadiness: (showMessage, readiness) => this.isReplicationReady(showMessage, readiness),
|
|
146
|
+
handleFailure: (request) => this.onReplicationFailed(request),
|
|
147
|
+
recordFiniteAttempt: () => {
|
|
148
|
+
this.previousReplicated = Date.now();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
41
151
|
}
|
|
42
152
|
/**
|
|
43
153
|
* Process a synchronisation result document.
|
|
@@ -62,6 +172,10 @@ class ReplicationService extends ServiceBase {
|
|
|
62
172
|
* An event triggered before starting replication.
|
|
63
173
|
*/
|
|
64
174
|
onBeforeReplicate = handlers().bailFirstFailure("onBeforeReplicate");
|
|
175
|
+
/** Provider preparation which applies only to the central remote. */
|
|
176
|
+
onPrepareCentralRemoteReplication = handlers().bailFirstFailure(
|
|
177
|
+
"onPrepareCentralRemoteReplication"
|
|
178
|
+
);
|
|
65
179
|
/**
|
|
66
180
|
* Lightweight, repeatable policy checks shared by every replication entry point.
|
|
67
181
|
* Handlers must remain idempotent because a high-level replication may cross
|
|
@@ -69,47 +183,62 @@ class ReplicationService extends ServiceBase {
|
|
|
69
183
|
*/
|
|
70
184
|
onCheckReplicationReady = handlers().bailFirstFailure("onCheckReplicationReady");
|
|
71
185
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
186
|
+
* Evaluate the ordered readiness conditions for an operation.
|
|
187
|
+
*
|
|
188
|
+
* This method does not acquire a Replicator or begin activity accounting.
|
|
189
|
+
* Provider requirements select whether central-remote preparation applies.
|
|
190
|
+
*
|
|
191
|
+
* @param showMessage Whether condition-specific diagnostics may be prominent.
|
|
192
|
+
* @param readiness Provider-owned preparation requirements.
|
|
74
193
|
*/
|
|
75
|
-
async isReplicationReady(showMessage = false) {
|
|
76
|
-
|
|
77
|
-
this._log(`Not ready`);
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
if (!await this.onCheckReplicationReady(showMessage)) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
const currentSettings = this.settingService.currentSettings();
|
|
84
|
-
if (isLockAcquired("cleanup")) {
|
|
85
|
-
this._log(this.context.translate("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
if (currentSettings.versionUpFlash != "") {
|
|
89
|
-
this._log(this.context.translate("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
if (!await this.fileProcessing.commitPendingFileEvents()) {
|
|
93
|
-
this.showError(this.context.translate("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
if (!this.APIService.isOnline) {
|
|
97
|
-
this.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
if (!await this.onBeforeReplicate(showMessage)) {
|
|
101
|
-
const hasNetworkError = (await this.appLifecycleService.getUnresolvedMessages()).flat().some((e) => typeof e == "string" && e.indexOf(MARK_LOG_NETWORK_ERROR) !== -1);
|
|
102
|
-
if (!hasNetworkError) {
|
|
103
|
-
this.showError(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
|
104
|
-
} else {
|
|
105
|
-
this._log(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_INFO);
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
this.clearErrors();
|
|
110
|
-
return true;
|
|
194
|
+
async isReplicationReady(showMessage = false, readiness = CENTRAL_REMOTE_REPLICATION_READINESS) {
|
|
195
|
+
return (await this._evaluateReadiness({ showMessage, requirements: readiness })).ready;
|
|
111
196
|
}
|
|
112
197
|
onReplicationFailed = handlers().bailFirstFailure("onReplicationFailed");
|
|
198
|
+
async replicateUserInitiated(request = {
|
|
199
|
+
trigger: "manual",
|
|
200
|
+
progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
|
|
201
|
+
interaction: USER_INITIATED_REPLICATION_AUTHORITY
|
|
202
|
+
}) {
|
|
203
|
+
return await this._typedReplication.runUserInitiated(request);
|
|
204
|
+
}
|
|
205
|
+
async replicateUnattended(request) {
|
|
206
|
+
return await this._typedReplication.runUnattended(request);
|
|
207
|
+
}
|
|
208
|
+
replicateUnattendedByEvent(request) {
|
|
209
|
+
return shareRunningResult(`replication`, async () => {
|
|
210
|
+
const currentSettings = this.settingService.currentSettings();
|
|
211
|
+
const least = currentSettings.syncMinimumInterval;
|
|
212
|
+
if (least > 0) {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const elapsed = now - this.previousReplicated;
|
|
215
|
+
if (elapsed < least) {
|
|
216
|
+
this._log(
|
|
217
|
+
`Replication triggered by event is rate limited. Elapsed: ${elapsed}ms, Least interval: ${least}ms`,
|
|
218
|
+
LOG_LEVEL_VERBOSE
|
|
219
|
+
);
|
|
220
|
+
return replicationBlocked("rate-limited");
|
|
221
|
+
}
|
|
222
|
+
this.previousReplicated = now + REPLICATION_ON_EVENT_FORECASTED_TIME;
|
|
223
|
+
}
|
|
224
|
+
return await this.replicateUnattended(request);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async startContinuous(request) {
|
|
228
|
+
return await this._typedReplication.startContinuous(request);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Stop finite transfer work on the active provider without entering the
|
|
232
|
+
* replication readiness or activity accounting paths.
|
|
233
|
+
*
|
|
234
|
+
* The provider and replicator are obtained together so that a concurrent
|
|
235
|
+
* replacement cannot pair a capability from one provider with a
|
|
236
|
+
* replicator from another. Stopping is an operator action, not a failed
|
|
237
|
+
* replication, so it does not invoke failure-recovery handlers.
|
|
238
|
+
*/
|
|
239
|
+
async stopActiveTransfer() {
|
|
240
|
+
return await this._typedReplication.stopActiveTransfer();
|
|
241
|
+
}
|
|
113
242
|
async performReplicationRequest(showMessage) {
|
|
114
243
|
const activeReplicator = this.replicatorService.getActiveReplicator();
|
|
115
244
|
if (!activeReplicator) {
|
|
@@ -125,7 +254,9 @@ class ReplicationService extends ServiceBase {
|
|
|
125
254
|
*/
|
|
126
255
|
async performReplication(showMessage) {
|
|
127
256
|
const result = await this.performReplicationRequest(showMessage);
|
|
128
|
-
if (!result)
|
|
257
|
+
if (!result) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
129
260
|
return result;
|
|
130
261
|
}
|
|
131
262
|
/**
|
|
@@ -133,6 +264,13 @@ class ReplicationService extends ServiceBase {
|
|
|
133
264
|
* @param showMessage Whether to show messages to the user.
|
|
134
265
|
*/
|
|
135
266
|
async replicate(showMessage) {
|
|
267
|
+
if (await this.replicatorService.acquireActiveReplicatorContext()) {
|
|
268
|
+
const outcome = showMessage ? await this.replicateUserInitiated() : await this.replicateUnattended({
|
|
269
|
+
trigger: "database-event",
|
|
270
|
+
interaction: NO_INTERACTION
|
|
271
|
+
});
|
|
272
|
+
return outcome.status === "completed";
|
|
273
|
+
}
|
|
136
274
|
try {
|
|
137
275
|
const checkBeforeReplicate = await this.isReplicationReady(showMessage);
|
|
138
276
|
if (!checkBeforeReplicate) return false;
|
|
@@ -140,7 +278,9 @@ class ReplicationService extends ServiceBase {
|
|
|
140
278
|
() => this.performReplicationRequest(showMessage),
|
|
141
279
|
{ label: "replication" }
|
|
142
280
|
);
|
|
143
|
-
if (!result)
|
|
281
|
+
if (!result) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
144
284
|
return result;
|
|
145
285
|
} finally {
|
|
146
286
|
this.previousReplicated = Date.now();
|
|
@@ -151,7 +291,13 @@ class ReplicationService extends ServiceBase {
|
|
|
151
291
|
* Start the replication process triggered by an event (e.g., file change).
|
|
152
292
|
* @param showMessage Whether to show messages to the user.
|
|
153
293
|
*/
|
|
154
|
-
replicateByEvent(showMessage) {
|
|
294
|
+
async replicateByEvent(showMessage) {
|
|
295
|
+
if (await this.replicatorService.acquireActiveReplicatorContext()) {
|
|
296
|
+
return await this.replicateUnattendedByEvent({
|
|
297
|
+
trigger: "database-event",
|
|
298
|
+
interaction: NO_INTERACTION
|
|
299
|
+
}).then((outcome) => outcome.status === "completed");
|
|
300
|
+
}
|
|
155
301
|
return shareRunningResult(`replication`, async () => {
|
|
156
302
|
const currentSettings = this.settingService.currentSettings();
|
|
157
303
|
const least = currentSettings.syncMinimumInterval;
|
|
@@ -178,51 +324,53 @@ class ReplicationService extends ServiceBase {
|
|
|
178
324
|
databaseQueueCount = reactiveSource(0);
|
|
179
325
|
storageApplyingCount = reactiveSource(0);
|
|
180
326
|
replicationResultCount = reactiveSource(0);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
async
|
|
190
|
-
if (!await this.onBeforeReplicate(showingNotice)) {
|
|
327
|
+
/**
|
|
328
|
+
* Dispatch one directional full transfer through one admitted publication.
|
|
329
|
+
*
|
|
330
|
+
* Each attempt owns a separate reservation. Compatibility recovery runs
|
|
331
|
+
* between those reservations, and a retry is admitted only when the exact
|
|
332
|
+
* first-attempt context remains active. Both attempts use one detached
|
|
333
|
+
* settings snapshot.
|
|
334
|
+
*/
|
|
335
|
+
async performDirectionalReplication(direction, showingNotice, admission = DIRECTIONAL_REPLICATION_ADMISSION.ORDINARY) {
|
|
336
|
+
if (direction === DIRECTIONAL_REPLICATION.UPLOAD && !await this.onBeforeReplicate(showingNotice)) {
|
|
191
337
|
this._log(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
|
192
338
|
return false;
|
|
193
339
|
}
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
const ret = await activeReplicator.replicateAllToServer(currentSettings, showingNotice);
|
|
200
|
-
if (ret) return true;
|
|
201
|
-
const checkResult = await this.checkConnectionFailure();
|
|
202
|
-
if (checkResult == "CHECKAGAIN")
|
|
203
|
-
return await activeReplicator.replicateAllToServer(currentSettings, showingNotice);
|
|
204
|
-
return !checkResult;
|
|
205
|
-
}
|
|
206
|
-
async performReplicateAllFromRemote(showingNotice) {
|
|
207
|
-
const activeReplicator = this.getActiveReplicatorFor("fetching data from remote");
|
|
208
|
-
if (!activeReplicator) {
|
|
209
|
-
return false;
|
|
340
|
+
const detachedSetting = asCopy(this.settingService.currentSettings());
|
|
341
|
+
if (admission === DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD) {
|
|
342
|
+
detachedSetting.versionUpFlash = "";
|
|
210
343
|
}
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
344
|
+
const setting = Object.freeze(detachedSetting);
|
|
345
|
+
let expectedContext;
|
|
346
|
+
const run = async () => {
|
|
347
|
+
const admitted = await this.replicatorService.runWithActiveReplicatorContext((context) => {
|
|
348
|
+
if (expectedContext && context !== expectedContext) {
|
|
349
|
+
return replicationBlocked("not-ready");
|
|
350
|
+
}
|
|
351
|
+
expectedContext ??= context;
|
|
352
|
+
return runDirectionalReplication(context, setting, direction, showingNotice);
|
|
353
|
+
});
|
|
354
|
+
if (admitted) return admitted;
|
|
355
|
+
this._log(`Active replicator not found during directional ${direction}`, LOG_LEVEL_NOTICE);
|
|
356
|
+
return replicationBlocked("no-active-replicator");
|
|
357
|
+
};
|
|
358
|
+
const outcome = await run();
|
|
359
|
+
if (isReplicationCompleted(outcome)) return true;
|
|
360
|
+
if (outcome.status !== "failed") return false;
|
|
361
|
+
const failedContext = expectedContext;
|
|
362
|
+
if (!failedContext) return false;
|
|
363
|
+
const failure = Object.freeze({ context: failedContext, setting, outcome });
|
|
364
|
+
const checkResult = await this.checkConnectionFailure(failure);
|
|
365
|
+
return checkResult === "CHECKAGAIN" && isReplicationCompleted(await run());
|
|
218
366
|
}
|
|
219
367
|
async replicateAllToRemote(showingNotice = false) {
|
|
220
368
|
if (!this.appLifecycleService.isReady()) return false;
|
|
221
|
-
return await this.
|
|
369
|
+
return await this.performDirectionalReplication(DIRECTIONAL_REPLICATION.UPLOAD, showingNotice);
|
|
222
370
|
}
|
|
223
371
|
async replicateAllFromRemote(showingNotice = false) {
|
|
224
372
|
if (!this.appLifecycleService.isReady()) return false;
|
|
225
|
-
return await this.
|
|
373
|
+
return await this.performDirectionalReplication(DIRECTIONAL_REPLICATION.DOWNLOAD, showingNotice);
|
|
226
374
|
}
|
|
227
375
|
/**
|
|
228
376
|
* Perform a full upload owned by an active rebuild while the physical database is ready.
|
|
@@ -235,7 +383,11 @@ class ReplicationService extends ServiceBase {
|
|
|
235
383
|
this._log("The selected local database is not ready for the rebuild upload.", LOG_LEVEL_NOTICE);
|
|
236
384
|
return false;
|
|
237
385
|
}
|
|
238
|
-
return await this.
|
|
386
|
+
return await this.performDirectionalReplication(
|
|
387
|
+
DIRECTIONAL_REPLICATION.UPLOAD,
|
|
388
|
+
showingNotice,
|
|
389
|
+
DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD
|
|
390
|
+
);
|
|
239
391
|
}
|
|
240
392
|
/**
|
|
241
393
|
* Perform a full download owned by an active rebuild while the physical database is ready.
|
|
@@ -248,38 +400,45 @@ class ReplicationService extends ServiceBase {
|
|
|
248
400
|
this._log("The selected local database is not ready for the rebuild download.", LOG_LEVEL_NOTICE);
|
|
249
401
|
return false;
|
|
250
402
|
}
|
|
251
|
-
return await this.
|
|
403
|
+
return await this.performDirectionalReplication(
|
|
404
|
+
DIRECTIONAL_REPLICATION.DOWNLOAD,
|
|
405
|
+
showingNotice,
|
|
406
|
+
DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
getActiveReplicatorFor(usage) {
|
|
410
|
+
const activeReplicator = this.replicatorService.getActiveReplicator();
|
|
411
|
+
if (!activeReplicator) {
|
|
412
|
+
this._log(`Active replicator not found during ${usage}`, LOG_LEVEL_NOTICE);
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
return activeReplicator;
|
|
252
416
|
}
|
|
253
417
|
_getReplicatorAndPerform(action, perform) {
|
|
254
418
|
const activeReplicator = this.getActiveReplicatorFor(action);
|
|
255
|
-
if (!activeReplicator)
|
|
419
|
+
if (!activeReplicator) return Promise.resolve();
|
|
420
|
+
if (!canAdministerLegacyCentralRemote(activeReplicator)) {
|
|
421
|
+
this._log(`Active replicator does not support ${action}`, LOG_LEVEL_NOTICE);
|
|
256
422
|
return Promise.resolve();
|
|
257
423
|
}
|
|
258
|
-
|
|
259
|
-
return perform(currentSettings, activeReplicator);
|
|
424
|
+
return perform(this.settingService.currentSettings(), activeReplicator);
|
|
260
425
|
}
|
|
261
426
|
async markLocked(lockByClean = false) {
|
|
262
427
|
return await this._getReplicatorAndPerform(
|
|
263
428
|
"marking remote locked",
|
|
264
|
-
async (currentSettings, activeReplicator) =>
|
|
265
|
-
return await activeReplicator.markRemoteLocked(currentSettings, true, lockByClean);
|
|
266
|
-
}
|
|
429
|
+
async (currentSettings, activeReplicator) => await activeReplicator.markRemoteLocked(currentSettings, true, lockByClean)
|
|
267
430
|
);
|
|
268
431
|
}
|
|
269
432
|
async markUnlocked() {
|
|
270
433
|
return await this._getReplicatorAndPerform(
|
|
271
434
|
"marking remote unlocked",
|
|
272
|
-
async (currentSettings, activeReplicator) =>
|
|
273
|
-
return await activeReplicator.markRemoteLocked(currentSettings, false, false);
|
|
274
|
-
}
|
|
435
|
+
async (currentSettings, activeReplicator) => await activeReplicator.markRemoteLocked(currentSettings, false, false)
|
|
275
436
|
);
|
|
276
437
|
}
|
|
277
438
|
async markResolved() {
|
|
278
439
|
return await this._getReplicatorAndPerform(
|
|
279
440
|
"marking remote resolved",
|
|
280
|
-
async (currentSettings, activeReplicator) =>
|
|
281
|
-
return await activeReplicator.markRemoteResolved(currentSettings);
|
|
282
|
-
}
|
|
441
|
+
async (currentSettings, activeReplicator) => await activeReplicator.markRemoteResolved(currentSettings)
|
|
283
442
|
);
|
|
284
443
|
}
|
|
285
444
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type LOG_LEVEL, type ObsidianLiveSyncSettings } from "../../common/types.js";
|
|
2
|
+
import { type ReplicationReadinessRequirements } from "../../replication/ReplicatorProvider.js";
|
|
3
|
+
import { type LogFunction } from "../lib/logUtils.js";
|
|
4
|
+
import type { MessageTranslator } from "./MessageTranslator.js";
|
|
5
|
+
import { type ReadinessEvaluationResult } from "./ReadinessEvaluator.js";
|
|
6
|
+
/** State and policy gates consulted without beginning remote preparation. */
|
|
7
|
+
export interface ReplicationReadinessGates {
|
|
8
|
+
readonly isApplicationReady: () => boolean;
|
|
9
|
+
readonly runPolicyChecks: (showMessage: boolean) => Promise<boolean>;
|
|
10
|
+
readonly currentSettings: () => Pick<ObsidianLiveSyncSettings, "versionUpFlash">;
|
|
11
|
+
readonly isCleanupRunning: () => boolean;
|
|
12
|
+
readonly isOnline: () => boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Ordered operational gates which may release or prepare replication work. */
|
|
15
|
+
export interface ReplicationReadinessPreparation {
|
|
16
|
+
readonly commitPendingFileEvents: () => Promise<boolean>;
|
|
17
|
+
readonly prepareCentralRemote: (showMessage: boolean) => Promise<boolean>;
|
|
18
|
+
readonly runBeforeReplicate: (showMessage: boolean) => Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
/** Diagnostic state, presentation, and unresolved-error output for readiness. */
|
|
21
|
+
export interface ReplicationReadinessDiagnostics {
|
|
22
|
+
readonly getUnresolvedMessages: () => Promise<(string | Error)[][]>;
|
|
23
|
+
readonly translate: MessageTranslator;
|
|
24
|
+
readonly log: LogFunction;
|
|
25
|
+
readonly showError: (message: string, maxLogLevel?: LOG_LEVEL) => void;
|
|
26
|
+
readonly clearErrors: () => void;
|
|
27
|
+
}
|
|
28
|
+
/** Role-grouped collaborators for the common replication readiness sequence. */
|
|
29
|
+
export interface ReplicationReadinessDependencies {
|
|
30
|
+
readonly gates: ReplicationReadinessGates;
|
|
31
|
+
readonly preparation: ReplicationReadinessPreparation;
|
|
32
|
+
readonly diagnostics: ReplicationReadinessDiagnostics;
|
|
33
|
+
}
|
|
34
|
+
export interface ReplicationReadinessContext {
|
|
35
|
+
readonly showMessage: boolean;
|
|
36
|
+
readonly requirements: ReplicationReadinessRequirements;
|
|
37
|
+
}
|
|
38
|
+
/** Optional inputs accepted by the host-facing replication readiness evaluator. */
|
|
39
|
+
export interface ReplicationReadinessRequest {
|
|
40
|
+
readonly showMessage?: boolean;
|
|
41
|
+
readonly requirements?: ReplicationReadinessRequirements;
|
|
42
|
+
}
|
|
43
|
+
export type ReplicationReadinessEvaluator = (context?: ReplicationReadinessRequest) => Promise<ReadinessEvaluationResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Compose the ordered readiness conditions shared by replication entry points.
|
|
46
|
+
*
|
|
47
|
+
* The returned evaluator owns readiness decisions and their diagnostics only.
|
|
48
|
+
* It does not acquire a Replicator, run provider capabilities, or account for
|
|
49
|
+
* remote activity. Provider-specific preparation is selected by the supplied
|
|
50
|
+
* requirements while the common conditions retain their declaration order.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createReplicationReadinessEvaluator(dependencies: ReplicationReadinessDependencies): ReplicationReadinessEvaluator;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE } from "../../common/types.js";
|
|
2
|
+
import {
|
|
3
|
+
CENTRAL_REMOTE_REPLICATION_READINESS
|
|
4
|
+
} from "../../replication/ReplicatorProvider.js";
|
|
5
|
+
import { MARK_LOG_NETWORK_ERROR } from "../lib/logUtils.js";
|
|
6
|
+
import { evaluateReadiness } from "./ReadinessEvaluator.js";
|
|
7
|
+
function createReplicationReadinessEvaluator(dependencies) {
|
|
8
|
+
const { diagnostics, gates, preparation } = dependencies;
|
|
9
|
+
const reportPreparationFailure = async () => {
|
|
10
|
+
const hasNetworkError = (await diagnostics.getUnresolvedMessages()).flat().some((entry) => typeof entry == "string" && entry.includes(MARK_LOG_NETWORK_ERROR));
|
|
11
|
+
const message = diagnostics.translate("Replicator.Message.SomeModuleFailed");
|
|
12
|
+
if (hasNetworkError) {
|
|
13
|
+
diagnostics.log(message, LOG_LEVEL_INFO);
|
|
14
|
+
} else {
|
|
15
|
+
diagnostics.showError(message, LOG_LEVEL_NOTICE);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const conditions = [
|
|
19
|
+
{
|
|
20
|
+
name: "application-ready",
|
|
21
|
+
evaluate: () => {
|
|
22
|
+
if (gates.isApplicationReady()) return true;
|
|
23
|
+
diagnostics.log("Not ready");
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "replication-policy-approved",
|
|
29
|
+
evaluate: ({ showMessage }) => gates.runPolicyChecks(showMessage)
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "local-replication-state-ready",
|
|
33
|
+
evaluate: () => {
|
|
34
|
+
const currentSettings = gates.currentSettings();
|
|
35
|
+
if (gates.isCleanupRunning()) {
|
|
36
|
+
diagnostics.log(diagnostics.translate("Replicator.Message.Cleaned"), LOG_LEVEL_NOTICE);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (currentSettings.versionUpFlash != "") {
|
|
40
|
+
diagnostics.log(diagnostics.translate("Replicator.Message.VersionUpFlash"), LOG_LEVEL_NOTICE);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "pending-file-events-committed",
|
|
48
|
+
evaluate: () => preparation.commitPendingFileEvents(),
|
|
49
|
+
onRejected: () => {
|
|
50
|
+
diagnostics.showError(diagnostics.translate("Replicator.Message.Pending"), LOG_LEVEL_NOTICE);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "network-online",
|
|
55
|
+
evaluate: () => gates.isOnline(),
|
|
56
|
+
onRejected: ({ showMessage }) => {
|
|
57
|
+
diagnostics.showError("Network is offline", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "central-remote-prepared",
|
|
62
|
+
appliesTo: ({ requirements }) => requirements.centralRemotePreparation === "required",
|
|
63
|
+
evaluate: ({ showMessage }) => preparation.prepareCentralRemote(showMessage),
|
|
64
|
+
onRejected: reportPreparationFailure
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "general-replication-prepared",
|
|
68
|
+
evaluate: ({ showMessage }) => preparation.runBeforeReplicate(showMessage),
|
|
69
|
+
onRejected: reportPreparationFailure
|
|
70
|
+
}
|
|
71
|
+
];
|
|
72
|
+
return async (context = {}) => {
|
|
73
|
+
const result = await evaluateReadiness({
|
|
74
|
+
purpose: "replication",
|
|
75
|
+
context: {
|
|
76
|
+
showMessage: context.showMessage ?? false,
|
|
77
|
+
requirements: context.requirements ?? CENTRAL_REMOTE_REPLICATION_READINESS
|
|
78
|
+
},
|
|
79
|
+
conditions
|
|
80
|
+
});
|
|
81
|
+
if (result.ready) diagnostics.clearErrors();
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
createReplicationReadinessEvaluator
|
|
87
|
+
};
|