@vrtmrz/livesync-commonlib 0.1.19-rc.0 → 0.1.20
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 +249 -0
- package/dist/replication/ReplicatorProvider.js +145 -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 +70 -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 +72 -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 +248 -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 +83 -0
- package/dist/services/base/ReplicationService.typedReplication.js +191 -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,91 @@ 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
|
+
USER_INITIATED_REPLICATION_AUTHORITY,
|
|
15
|
+
isActiveReplicatorContextBoundToSetting,
|
|
16
|
+
isReplicationCompleted,
|
|
17
|
+
replicationBlocked,
|
|
18
|
+
replicationFailed,
|
|
19
|
+
outcomeFromFiniteOpenReplication
|
|
20
|
+
} from "../../replication/ReplicatorProvider.js";
|
|
10
21
|
import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
|
|
11
22
|
import { isLockAcquired, shareRunningResult } from "octagonal-wheels/concurrency/lock";
|
|
23
|
+
import {
|
|
24
|
+
createReplicationReadinessEvaluator
|
|
25
|
+
} from "./ReplicationService.readiness.js";
|
|
26
|
+
import { TypedReplicationCoordinator } from "./ReplicationService.typedReplication.js";
|
|
27
|
+
import { asCopy } from "../../common/utils.object.js";
|
|
12
28
|
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5e3;
|
|
29
|
+
const DIRECTIONAL_REPLICATION = Object.freeze({
|
|
30
|
+
UPLOAD: "upload",
|
|
31
|
+
DOWNLOAD: "download"
|
|
32
|
+
});
|
|
33
|
+
const DIRECTIONAL_REPLICATION_ADMISSION = Object.freeze({
|
|
34
|
+
ORDINARY: "ordinary",
|
|
35
|
+
EXPLICIT_REBUILD: "explicit-rebuild"
|
|
36
|
+
});
|
|
37
|
+
function canUploadAll(replicator) {
|
|
38
|
+
return "replicateAllToServer" in replicator && typeof replicator.replicateAllToServer === "function";
|
|
39
|
+
}
|
|
40
|
+
function canUploadAllWithOutcome(replicator) {
|
|
41
|
+
return "replicateAllToServerWithOutcome" in replicator && typeof replicator.replicateAllToServerWithOutcome === "function";
|
|
42
|
+
}
|
|
43
|
+
function canDownloadAll(replicator) {
|
|
44
|
+
return "replicateAllFromServer" in replicator && typeof replicator.replicateAllFromServer === "function";
|
|
45
|
+
}
|
|
46
|
+
function canDownloadAllWithOutcome(replicator) {
|
|
47
|
+
return "replicateAllFromServerWithOutcome" in replicator && typeof replicator.replicateAllFromServerWithOutcome === "function";
|
|
48
|
+
}
|
|
49
|
+
function canAdministerLegacyCentralRemote(replicator) {
|
|
50
|
+
return "markRemoteLocked" in replicator && typeof replicator.markRemoteLocked === "function" && "markRemoteResolved" in replicator && typeof replicator.markRemoteResolved === "function";
|
|
51
|
+
}
|
|
52
|
+
async function stopActiveTransferForDirectionalReplication(context) {
|
|
53
|
+
const capability = context.provider.stopActiveTransfer;
|
|
54
|
+
if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
|
55
|
+
return replicationBlocked(capability.reason);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const outcome = await capability.run(context.replicator);
|
|
59
|
+
return outcome.status === "failed" ? replicationBlocked("not-ready") : outcome;
|
|
60
|
+
} catch {
|
|
61
|
+
return replicationBlocked("not-ready");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function runDirectionalReplication(context, setting, direction, showingNotice) {
|
|
65
|
+
if (!isActiveReplicatorContextBoundToSetting(context, setting)) {
|
|
66
|
+
return replicationBlocked("not-ready");
|
|
67
|
+
}
|
|
68
|
+
const stopOutcome = await stopActiveTransferForDirectionalReplication(context);
|
|
69
|
+
if (!isReplicationCompleted(stopOutcome)) return stopOutcome;
|
|
70
|
+
try {
|
|
71
|
+
let result;
|
|
72
|
+
if (direction === DIRECTIONAL_REPLICATION.UPLOAD) {
|
|
73
|
+
if (canUploadAllWithOutcome(context.replicator)) {
|
|
74
|
+
return await context.replicator.replicateAllToServerWithOutcome(setting, showingNotice);
|
|
75
|
+
}
|
|
76
|
+
if (!canUploadAll(context.replicator)) return replicationBlocked("capability-not-applicable");
|
|
77
|
+
result = await context.replicator.replicateAllToServer(setting, showingNotice);
|
|
78
|
+
} else {
|
|
79
|
+
if (canDownloadAllWithOutcome(context.replicator)) {
|
|
80
|
+
return await context.replicator.replicateAllFromServerWithOutcome(setting, showingNotice);
|
|
81
|
+
}
|
|
82
|
+
if (!canDownloadAll(context.replicator)) return replicationBlocked("capability-not-applicable");
|
|
83
|
+
result = await context.replicator.replicateAllFromServer(setting, showingNotice);
|
|
84
|
+
}
|
|
85
|
+
return outcomeFromFiniteOpenReplication(result);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return replicationFailed(error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
13
90
|
class ReplicationService extends ServiceBase {
|
|
14
91
|
_unresolvedErrorManager;
|
|
92
|
+
_evaluateReadiness;
|
|
93
|
+
_typedReplication;
|
|
15
94
|
showError(msg, max_log_level = LOG_LEVEL_NOTICE) {
|
|
16
95
|
this._unresolvedErrorManager.showError(msg, max_log_level);
|
|
17
96
|
}
|
|
@@ -38,6 +117,36 @@ class ReplicationService extends ServiceBase {
|
|
|
38
117
|
dependencies.appLifecycleService,
|
|
39
118
|
this.context.events
|
|
40
119
|
);
|
|
120
|
+
this._evaluateReadiness = createReplicationReadinessEvaluator({
|
|
121
|
+
gates: {
|
|
122
|
+
isApplicationReady: () => this.appLifecycleService.isReady(),
|
|
123
|
+
runPolicyChecks: (showMessage) => this.onCheckReplicationReady(showMessage),
|
|
124
|
+
currentSettings: () => this.settingService.currentSettings(),
|
|
125
|
+
isCleanupRunning: () => isLockAcquired("cleanup"),
|
|
126
|
+
isOnline: () => this.APIService.isOnline
|
|
127
|
+
},
|
|
128
|
+
preparation: {
|
|
129
|
+
commitPendingFileEvents: () => this.fileProcessing.commitPendingFileEvents(),
|
|
130
|
+
prepareCentralRemote: (showMessage) => this.onPrepareCentralRemoteReplication(showMessage),
|
|
131
|
+
runBeforeReplicate: (showMessage) => this.onBeforeReplicate(showMessage)
|
|
132
|
+
},
|
|
133
|
+
diagnostics: {
|
|
134
|
+
getUnresolvedMessages: () => this.appLifecycleService.getUnresolvedMessages(),
|
|
135
|
+
translate: (key) => this.context.translate(key),
|
|
136
|
+
log: this._log,
|
|
137
|
+
showError: (message, maxLogLevel) => this.showError(message, maxLogLevel),
|
|
138
|
+
clearErrors: () => this.clearErrors()
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
this._typedReplication = new TypedReplicationCoordinator({
|
|
142
|
+
replicatorService: this.replicatorService,
|
|
143
|
+
currentSettings: () => this.settingService.currentSettings(),
|
|
144
|
+
checkReadiness: (showMessage, readiness) => this.isReplicationReady(showMessage, readiness),
|
|
145
|
+
handleFailure: (request) => this.onReplicationFailed(request),
|
|
146
|
+
recordFiniteAttempt: () => {
|
|
147
|
+
this.previousReplicated = Date.now();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
41
150
|
}
|
|
42
151
|
/**
|
|
43
152
|
* Process a synchronisation result document.
|
|
@@ -62,6 +171,10 @@ class ReplicationService extends ServiceBase {
|
|
|
62
171
|
* An event triggered before starting replication.
|
|
63
172
|
*/
|
|
64
173
|
onBeforeReplicate = handlers().bailFirstFailure("onBeforeReplicate");
|
|
174
|
+
/** Provider preparation which applies only to the central remote. */
|
|
175
|
+
onPrepareCentralRemoteReplication = handlers().bailFirstFailure(
|
|
176
|
+
"onPrepareCentralRemoteReplication"
|
|
177
|
+
);
|
|
65
178
|
/**
|
|
66
179
|
* Lightweight, repeatable policy checks shared by every replication entry point.
|
|
67
180
|
* Handlers must remain idempotent because a high-level replication may cross
|
|
@@ -69,47 +182,61 @@ class ReplicationService extends ServiceBase {
|
|
|
69
182
|
*/
|
|
70
183
|
onCheckReplicationReady = handlers().bailFirstFailure("onCheckReplicationReady");
|
|
71
184
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
185
|
+
* Evaluate the ordered readiness conditions for an operation.
|
|
186
|
+
*
|
|
187
|
+
* This method does not acquire a Replicator or begin activity accounting.
|
|
188
|
+
* Provider requirements select whether central-remote preparation applies.
|
|
189
|
+
*
|
|
190
|
+
* @param showMessage Whether condition-specific diagnostics may be prominent.
|
|
191
|
+
* @param readiness Provider-owned preparation requirements.
|
|
74
192
|
*/
|
|
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;
|
|
193
|
+
async isReplicationReady(showMessage = false, readiness = CENTRAL_REMOTE_REPLICATION_READINESS) {
|
|
194
|
+
return (await this._evaluateReadiness({ showMessage, requirements: readiness })).ready;
|
|
111
195
|
}
|
|
112
196
|
onReplicationFailed = handlers().bailFirstFailure("onReplicationFailed");
|
|
197
|
+
async replicateUserInitiated(request = {
|
|
198
|
+
trigger: "manual",
|
|
199
|
+
interaction: USER_INITIATED_REPLICATION_AUTHORITY
|
|
200
|
+
}) {
|
|
201
|
+
return await this._typedReplication.runUserInitiated(request);
|
|
202
|
+
}
|
|
203
|
+
async replicateUnattended(request) {
|
|
204
|
+
return await this._typedReplication.runUnattended(request);
|
|
205
|
+
}
|
|
206
|
+
replicateUnattendedByEvent(request) {
|
|
207
|
+
return shareRunningResult(`replication`, async () => {
|
|
208
|
+
const currentSettings = this.settingService.currentSettings();
|
|
209
|
+
const least = currentSettings.syncMinimumInterval;
|
|
210
|
+
if (least > 0) {
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
const elapsed = now - this.previousReplicated;
|
|
213
|
+
if (elapsed < least) {
|
|
214
|
+
this._log(
|
|
215
|
+
`Replication triggered by event is rate limited. Elapsed: ${elapsed}ms, Least interval: ${least}ms`,
|
|
216
|
+
LOG_LEVEL_VERBOSE
|
|
217
|
+
);
|
|
218
|
+
return replicationBlocked("rate-limited");
|
|
219
|
+
}
|
|
220
|
+
this.previousReplicated = now + REPLICATION_ON_EVENT_FORECASTED_TIME;
|
|
221
|
+
}
|
|
222
|
+
return await this.replicateUnattended(request);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async startContinuous(request) {
|
|
226
|
+
return await this._typedReplication.startContinuous(request);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Stop finite transfer work on the active provider without entering the
|
|
230
|
+
* replication readiness or activity accounting paths.
|
|
231
|
+
*
|
|
232
|
+
* The provider and replicator are obtained together so that a concurrent
|
|
233
|
+
* replacement cannot pair a capability from one provider with a
|
|
234
|
+
* replicator from another. Stopping is an operator action, not a failed
|
|
235
|
+
* replication, so it does not invoke failure-recovery handlers.
|
|
236
|
+
*/
|
|
237
|
+
async stopActiveTransfer() {
|
|
238
|
+
return await this._typedReplication.stopActiveTransfer();
|
|
239
|
+
}
|
|
113
240
|
async performReplicationRequest(showMessage) {
|
|
114
241
|
const activeReplicator = this.replicatorService.getActiveReplicator();
|
|
115
242
|
if (!activeReplicator) {
|
|
@@ -125,7 +252,9 @@ class ReplicationService extends ServiceBase {
|
|
|
125
252
|
*/
|
|
126
253
|
async performReplication(showMessage) {
|
|
127
254
|
const result = await this.performReplicationRequest(showMessage);
|
|
128
|
-
if (!result)
|
|
255
|
+
if (!result) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
129
258
|
return result;
|
|
130
259
|
}
|
|
131
260
|
/**
|
|
@@ -133,6 +262,13 @@ class ReplicationService extends ServiceBase {
|
|
|
133
262
|
* @param showMessage Whether to show messages to the user.
|
|
134
263
|
*/
|
|
135
264
|
async replicate(showMessage) {
|
|
265
|
+
if (await this.replicatorService.acquireActiveReplicatorContext()) {
|
|
266
|
+
const outcome = showMessage ? await this.replicateUserInitiated() : await this.replicateUnattended({
|
|
267
|
+
trigger: "database-event",
|
|
268
|
+
interaction: NO_INTERACTION
|
|
269
|
+
});
|
|
270
|
+
return outcome.status === "completed";
|
|
271
|
+
}
|
|
136
272
|
try {
|
|
137
273
|
const checkBeforeReplicate = await this.isReplicationReady(showMessage);
|
|
138
274
|
if (!checkBeforeReplicate) return false;
|
|
@@ -140,7 +276,9 @@ class ReplicationService extends ServiceBase {
|
|
|
140
276
|
() => this.performReplicationRequest(showMessage),
|
|
141
277
|
{ label: "replication" }
|
|
142
278
|
);
|
|
143
|
-
if (!result)
|
|
279
|
+
if (!result) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
144
282
|
return result;
|
|
145
283
|
} finally {
|
|
146
284
|
this.previousReplicated = Date.now();
|
|
@@ -151,7 +289,13 @@ class ReplicationService extends ServiceBase {
|
|
|
151
289
|
* Start the replication process triggered by an event (e.g., file change).
|
|
152
290
|
* @param showMessage Whether to show messages to the user.
|
|
153
291
|
*/
|
|
154
|
-
replicateByEvent(showMessage) {
|
|
292
|
+
async replicateByEvent(showMessage) {
|
|
293
|
+
if (await this.replicatorService.acquireActiveReplicatorContext()) {
|
|
294
|
+
return await this.replicateUnattendedByEvent({
|
|
295
|
+
trigger: "database-event",
|
|
296
|
+
interaction: NO_INTERACTION
|
|
297
|
+
}).then((outcome) => outcome.status === "completed");
|
|
298
|
+
}
|
|
155
299
|
return shareRunningResult(`replication`, async () => {
|
|
156
300
|
const currentSettings = this.settingService.currentSettings();
|
|
157
301
|
const least = currentSettings.syncMinimumInterval;
|
|
@@ -178,51 +322,53 @@ class ReplicationService extends ServiceBase {
|
|
|
178
322
|
databaseQueueCount = reactiveSource(0);
|
|
179
323
|
storageApplyingCount = reactiveSource(0);
|
|
180
324
|
replicationResultCount = reactiveSource(0);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
async
|
|
190
|
-
if (!await this.onBeforeReplicate(showingNotice)) {
|
|
325
|
+
/**
|
|
326
|
+
* Dispatch one directional full transfer through one admitted publication.
|
|
327
|
+
*
|
|
328
|
+
* Each attempt owns a separate reservation. Compatibility recovery runs
|
|
329
|
+
* between those reservations, and a retry is admitted only when the exact
|
|
330
|
+
* first-attempt context remains active. Both attempts use one detached
|
|
331
|
+
* settings snapshot.
|
|
332
|
+
*/
|
|
333
|
+
async performDirectionalReplication(direction, showingNotice, admission = DIRECTIONAL_REPLICATION_ADMISSION.ORDINARY) {
|
|
334
|
+
if (direction === DIRECTIONAL_REPLICATION.UPLOAD && !await this.onBeforeReplicate(showingNotice)) {
|
|
191
335
|
this._log(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
|
|
192
336
|
return false;
|
|
193
337
|
}
|
|
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;
|
|
338
|
+
const detachedSetting = asCopy(this.settingService.currentSettings());
|
|
339
|
+
if (admission === DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD) {
|
|
340
|
+
detachedSetting.versionUpFlash = "";
|
|
210
341
|
}
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
342
|
+
const setting = Object.freeze(detachedSetting);
|
|
343
|
+
let expectedContext;
|
|
344
|
+
const run = async () => {
|
|
345
|
+
const admitted = await this.replicatorService.runWithActiveReplicatorContext((context) => {
|
|
346
|
+
if (expectedContext && context !== expectedContext) {
|
|
347
|
+
return replicationBlocked("not-ready");
|
|
348
|
+
}
|
|
349
|
+
expectedContext ??= context;
|
|
350
|
+
return runDirectionalReplication(context, setting, direction, showingNotice);
|
|
351
|
+
});
|
|
352
|
+
if (admitted) return admitted;
|
|
353
|
+
this._log(`Active replicator not found during directional ${direction}`, LOG_LEVEL_NOTICE);
|
|
354
|
+
return replicationBlocked("no-active-replicator");
|
|
355
|
+
};
|
|
356
|
+
const outcome = await run();
|
|
357
|
+
if (isReplicationCompleted(outcome)) return true;
|
|
358
|
+
if (outcome.status !== "failed") return false;
|
|
359
|
+
const failedContext = expectedContext;
|
|
360
|
+
if (!failedContext) return false;
|
|
361
|
+
const failure = Object.freeze({ context: failedContext, setting, outcome });
|
|
362
|
+
const checkResult = await this.checkConnectionFailure(failure);
|
|
363
|
+
return checkResult === "CHECKAGAIN" && isReplicationCompleted(await run());
|
|
218
364
|
}
|
|
219
365
|
async replicateAllToRemote(showingNotice = false) {
|
|
220
366
|
if (!this.appLifecycleService.isReady()) return false;
|
|
221
|
-
return await this.
|
|
367
|
+
return await this.performDirectionalReplication(DIRECTIONAL_REPLICATION.UPLOAD, showingNotice);
|
|
222
368
|
}
|
|
223
369
|
async replicateAllFromRemote(showingNotice = false) {
|
|
224
370
|
if (!this.appLifecycleService.isReady()) return false;
|
|
225
|
-
return await this.
|
|
371
|
+
return await this.performDirectionalReplication(DIRECTIONAL_REPLICATION.DOWNLOAD, showingNotice);
|
|
226
372
|
}
|
|
227
373
|
/**
|
|
228
374
|
* Perform a full upload owned by an active rebuild while the physical database is ready.
|
|
@@ -235,7 +381,11 @@ class ReplicationService extends ServiceBase {
|
|
|
235
381
|
this._log("The selected local database is not ready for the rebuild upload.", LOG_LEVEL_NOTICE);
|
|
236
382
|
return false;
|
|
237
383
|
}
|
|
238
|
-
return await this.
|
|
384
|
+
return await this.performDirectionalReplication(
|
|
385
|
+
DIRECTIONAL_REPLICATION.UPLOAD,
|
|
386
|
+
showingNotice,
|
|
387
|
+
DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD
|
|
388
|
+
);
|
|
239
389
|
}
|
|
240
390
|
/**
|
|
241
391
|
* Perform a full download owned by an active rebuild while the physical database is ready.
|
|
@@ -248,38 +398,45 @@ class ReplicationService extends ServiceBase {
|
|
|
248
398
|
this._log("The selected local database is not ready for the rebuild download.", LOG_LEVEL_NOTICE);
|
|
249
399
|
return false;
|
|
250
400
|
}
|
|
251
|
-
return await this.
|
|
401
|
+
return await this.performDirectionalReplication(
|
|
402
|
+
DIRECTIONAL_REPLICATION.DOWNLOAD,
|
|
403
|
+
showingNotice,
|
|
404
|
+
DIRECTIONAL_REPLICATION_ADMISSION.EXPLICIT_REBUILD
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
getActiveReplicatorFor(usage) {
|
|
408
|
+
const activeReplicator = this.replicatorService.getActiveReplicator();
|
|
409
|
+
if (!activeReplicator) {
|
|
410
|
+
this._log(`Active replicator not found during ${usage}`, LOG_LEVEL_NOTICE);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
return activeReplicator;
|
|
252
414
|
}
|
|
253
415
|
_getReplicatorAndPerform(action, perform) {
|
|
254
416
|
const activeReplicator = this.getActiveReplicatorFor(action);
|
|
255
|
-
if (!activeReplicator)
|
|
417
|
+
if (!activeReplicator) return Promise.resolve();
|
|
418
|
+
if (!canAdministerLegacyCentralRemote(activeReplicator)) {
|
|
419
|
+
this._log(`Active replicator does not support ${action}`, LOG_LEVEL_NOTICE);
|
|
256
420
|
return Promise.resolve();
|
|
257
421
|
}
|
|
258
|
-
|
|
259
|
-
return perform(currentSettings, activeReplicator);
|
|
422
|
+
return perform(this.settingService.currentSettings(), activeReplicator);
|
|
260
423
|
}
|
|
261
424
|
async markLocked(lockByClean = false) {
|
|
262
425
|
return await this._getReplicatorAndPerform(
|
|
263
426
|
"marking remote locked",
|
|
264
|
-
async (currentSettings, activeReplicator) =>
|
|
265
|
-
return await activeReplicator.markRemoteLocked(currentSettings, true, lockByClean);
|
|
266
|
-
}
|
|
427
|
+
async (currentSettings, activeReplicator) => await activeReplicator.markRemoteLocked(currentSettings, true, lockByClean)
|
|
267
428
|
);
|
|
268
429
|
}
|
|
269
430
|
async markUnlocked() {
|
|
270
431
|
return await this._getReplicatorAndPerform(
|
|
271
432
|
"marking remote unlocked",
|
|
272
|
-
async (currentSettings, activeReplicator) =>
|
|
273
|
-
return await activeReplicator.markRemoteLocked(currentSettings, false, false);
|
|
274
|
-
}
|
|
433
|
+
async (currentSettings, activeReplicator) => await activeReplicator.markRemoteLocked(currentSettings, false, false)
|
|
275
434
|
);
|
|
276
435
|
}
|
|
277
436
|
async markResolved() {
|
|
278
437
|
return await this._getReplicatorAndPerform(
|
|
279
438
|
"marking remote resolved",
|
|
280
|
-
async (currentSettings, activeReplicator) =>
|
|
281
|
-
return await activeReplicator.markRemoteResolved(currentSettings);
|
|
282
|
-
}
|
|
439
|
+
async (currentSettings, activeReplicator) => await activeReplicator.markRemoteResolved(currentSettings)
|
|
283
440
|
);
|
|
284
441
|
}
|
|
285
442
|
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ObsidianLiveSyncSettings } from "../../common/types.js";
|
|
2
|
+
import { type ContinuousReplicationRequest, type ReplicationFailureRequest, type ReplicationOutcome, type ReplicationReadinessRequirements, type UnattendedOneShotRequest, type UserInitiatedOneShotRequest } from "../../replication/ReplicatorProvider.js";
|
|
3
|
+
import type { IReplicatorService } from "./IService.js";
|
|
4
|
+
/** ReplicationService collaborators required to execute typed provider roles. */
|
|
5
|
+
export interface TypedReplicationCoordinatorDependencies {
|
|
6
|
+
readonly replicatorService: Pick<IReplicatorService, "acquireActiveReplicatorContext" | "hasActiveReplicator" | "runWithActiveReplicatorContext" | "runFiniteReplicationActivity">;
|
|
7
|
+
readonly currentSettings: () => ObsidianLiveSyncSettings;
|
|
8
|
+
readonly checkReadiness: (showMessage: boolean, readiness: ReplicationReadinessRequirements) => Promise<boolean>;
|
|
9
|
+
readonly handleFailure: (request: ReplicationFailureRequest) => Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* Advance the host's finite-attempt clock after readiness admits dispatch.
|
|
12
|
+
*
|
|
13
|
+
* This includes a provider-declared blocked capability, because the legacy
|
|
14
|
+
* service advanced its event rate-limit clock at the same boundary.
|
|
15
|
+
*/
|
|
16
|
+
readonly recordFiniteAttempt: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Execute typed provider capabilities against one captured active context.
|
|
20
|
+
*
|
|
21
|
+
* The coordinator owns capability selection, readiness dispatch, finite
|
|
22
|
+
* activity accounting, and failure notification. It does not own event rate
|
|
23
|
+
* limiting, legacy Replicator calls, or the active Replicator lifecycle.
|
|
24
|
+
*
|
|
25
|
+
* Each role is deliberately expressed as a separate control flow. Readiness,
|
|
26
|
+
* interaction authority, activity accounting, and failure handling differ by
|
|
27
|
+
* role, so a flag-driven capability table would conceal lifecycle decisions
|
|
28
|
+
* which callers and reviewers need to see explicitly.
|
|
29
|
+
*/
|
|
30
|
+
export declare class TypedReplicationCoordinator {
|
|
31
|
+
private readonly dependencies;
|
|
32
|
+
constructor(dependencies: TypedReplicationCoordinatorDependencies);
|
|
33
|
+
/**
|
|
34
|
+
* Execute a user-initiated finite role with the request's interaction authority.
|
|
35
|
+
*
|
|
36
|
+
* The active context is acquired atomically, readiness runs before
|
|
37
|
+
* capability dispatch, and the settings snapshot is captured before the
|
|
38
|
+
* finite activity begins. A failed result invokes recovery only after that
|
|
39
|
+
* activity has settled.
|
|
40
|
+
*/
|
|
41
|
+
runUserInitiated(request: UserInitiatedOneShotRequest): Promise<ReplicationOutcome>;
|
|
42
|
+
/**
|
|
43
|
+
* Execute an unattended finite role without granting interaction authority.
|
|
44
|
+
*
|
|
45
|
+
* Requests carrying interaction authority are rejected before context
|
|
46
|
+
* acquisition. Accepted requests otherwise retain the same readiness,
|
|
47
|
+
* settings-snapshot, activity, and recovery ordering as user work.
|
|
48
|
+
*/
|
|
49
|
+
runUnattended(request: UnattendedOneShotRequest): Promise<ReplicationOutcome>;
|
|
50
|
+
/**
|
|
51
|
+
* Start the provider's continuous role outside finite activity accounting.
|
|
52
|
+
*
|
|
53
|
+
* Capability applicability is established before readiness, because an
|
|
54
|
+
* inapplicable long-lived role must not trigger unrelated preparation or
|
|
55
|
+
* diagnostics. After readiness, the short startup call is admitted against
|
|
56
|
+
* that exact publication. The provider owns the registered long-lived task
|
|
57
|
+
* after startup settles; it does not retain this admission for its lifetime.
|
|
58
|
+
* Startup failures become typed outcomes without invoking the finite-operation
|
|
59
|
+
* recovery handler.
|
|
60
|
+
*/
|
|
61
|
+
startContinuous(request: ContinuousReplicationRequest): Promise<ReplicationOutcome>;
|
|
62
|
+
/**
|
|
63
|
+
* Request transfer cancellation on one atomically acquired active context.
|
|
64
|
+
*
|
|
65
|
+
* Cancellation deliberately bypasses readiness, activity accounting, and
|
|
66
|
+
* failure-recovery handlers: it is an operator control over existing work,
|
|
67
|
+
* not a request to begin new remote work.
|
|
68
|
+
*/
|
|
69
|
+
stopActiveTransfer(): Promise<ReplicationOutcome>;
|
|
70
|
+
private finishFiniteAttempt;
|
|
71
|
+
private acquireContext;
|
|
72
|
+
private acquireReadyContext;
|
|
73
|
+
private runFiniteActivity;
|
|
74
|
+
/**
|
|
75
|
+
* Reserve the readiness-tested publication for provider dispatch.
|
|
76
|
+
*
|
|
77
|
+
* Readiness and settings capture intentionally happen before admission. If
|
|
78
|
+
* an ownership transition completes in between, the newly admitted context
|
|
79
|
+
* is rejected instead of running work prepared for an older publication.
|
|
80
|
+
* Failure recovery runs only after the reservation has been released.
|
|
81
|
+
*/
|
|
82
|
+
private runAdmittedFiniteActivity;
|
|
83
|
+
}
|