@vrtmrz/livesync-commonlib 0.1.19 → 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.
Files changed (114) hide show
  1. package/README.md +5 -1
  2. package/dist/common/models/setting.p2p.d.ts +2 -0
  3. package/dist/common/models/setting.p2p.js +5 -0
  4. package/dist/interfaces/ServiceModule.d.ts +13 -4
  5. package/dist/managers/ChunkFetcher.js +10 -0
  6. package/dist/p2p/P2PService.d.ts +123 -0
  7. package/dist/p2p/P2PService.js +271 -0
  8. package/dist/p2p/index.d.ts +8 -0
  9. package/dist/p2p/index.js +8 -0
  10. package/dist/pouchdb/LiveSyncLocalDB.d.ts +3 -0
  11. package/dist/pouchdb/LiveSyncLocalDB.js +23 -3
  12. package/dist/pouchdb/ReplicatorShim.d.ts +43 -4
  13. package/dist/pouchdb/ReplicatorShim.js +78 -5
  14. package/dist/replication/CentralCompatibility.d.ts +38 -0
  15. package/dist/replication/CentralCompatibility.js +39 -0
  16. package/dist/replication/CentralRemoteAdministration.d.ts +84 -0
  17. package/dist/replication/CentralRemoteAdministration.js +71 -0
  18. package/dist/replication/LiveSyncAbstractReplicator.d.ts +19 -6
  19. package/dist/replication/ProviderCapability.d.ts +32 -0
  20. package/dist/replication/ProviderCapability.js +27 -0
  21. package/dist/replication/RemoteResource.d.ts +85 -0
  22. package/dist/replication/RemoteResource.js +17 -0
  23. package/dist/replication/ReplicatorInstance.d.ts +23 -0
  24. package/dist/replication/ReplicatorProvider.d.ts +249 -0
  25. package/dist/replication/ReplicatorProvider.js +145 -0
  26. package/dist/replication/couchdb/LiveSyncReplicator.d.ts +54 -8
  27. package/dist/replication/couchdb/LiveSyncReplicator.js +411 -158
  28. package/dist/replication/index.d.ts +10 -0
  29. package/dist/replication/index.js +70 -0
  30. package/dist/replication/journal/JournalSyncCore.d.ts +2 -0
  31. package/dist/replication/journal/JournalSyncCore.js +18 -5
  32. package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +38 -10
  33. package/dist/replication/journal/LiveSyncJournalReplicator.js +271 -63
  34. package/dist/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +8 -0
  35. package/dist/replication/journal/objectstore/JournalStorageAdapter.d.ts +2 -0
  36. package/dist/replication/journal/objectstore/MinioStorageAdapter.d.ts +2 -0
  37. package/dist/replication/journal/objectstore/MinioStorageAdapter.js +10 -1
  38. package/dist/replication/trystero/LiveSyncTrysteroReplicator.d.ts +12 -40
  39. package/dist/replication/trystero/LiveSyncTrysteroReplicator.js +52 -152
  40. package/dist/replication/trystero/P2PAutomationCoordinator.d.ts +20 -0
  41. package/dist/replication/trystero/P2PAutomationCoordinator.js +43 -0
  42. package/dist/replication/trystero/P2PConnectionProbeAdmission.d.ts +32 -0
  43. package/dist/replication/trystero/P2PConnectionProbeAdmission.js +10 -0
  44. package/dist/replication/trystero/P2PReplicationWire.d.ts +31 -0
  45. package/dist/replication/trystero/P2PReplicationWire.js +50 -0
  46. package/dist/replication/trystero/P2PRoomSession.d.ts +32 -0
  47. package/dist/replication/trystero/P2PRoomSession.js +101 -0
  48. package/dist/replication/trystero/P2PRoomSessionOwner.d.ts +61 -0
  49. package/dist/replication/trystero/P2PRoomSessionOwner.js +249 -0
  50. package/dist/replication/trystero/TrysteroReplicator.d.ts +88 -36
  51. package/dist/replication/trystero/TrysteroReplicator.js +273 -113
  52. package/dist/replication/trystero/TrysteroReplicatorP2PClient.d.ts +7 -4
  53. package/dist/replication/trystero/TrysteroReplicatorP2PClient.js +21 -14
  54. package/dist/replication/trystero/TrysteroReplicatorP2PServer.d.ts +26 -3
  55. package/dist/replication/trystero/TrysteroReplicatorP2PServer.js +84 -39
  56. package/dist/replication/trystero/UseP2PReplicatorResult.d.ts +10 -6
  57. package/dist/replication/trystero/addP2PEventHandlers.d.ts +8 -12
  58. package/dist/replication/trystero/addP2PEventHandlers.js +19 -19
  59. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.d.ts +9 -0
  60. package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +30 -0
  61. package/dist/replication/trystero/types.d.ts +18 -0
  62. package/dist/replication/trystero/types.js +5 -1
  63. package/dist/replication/trystero/useP2PReplicatorCommands.d.ts +3 -3
  64. package/dist/replication/trystero/useP2PReplicatorCommands.js +6 -8
  65. package/dist/replication/trystero/useP2PReplicatorFeature.d.ts +16 -7
  66. package/dist/replication/trystero/useP2PReplicatorFeature.js +72 -55
  67. package/dist/rpc/RpcRoom.d.ts +15 -1
  68. package/dist/rpc/RpcRoom.js +176 -41
  69. package/dist/rpc/RpcSession.d.ts +3 -1
  70. package/dist/rpc/RpcSession.js +2 -2
  71. package/dist/rpc/index.d.ts +1 -1
  72. package/dist/rpc/transports/trysteroUtils.js +2 -1
  73. package/dist/rpc/types.d.ts +26 -1
  74. package/dist/serviceFeatures/checkRemoteSize.js +5 -2
  75. package/dist/serviceModules/Rebuilder.js +66 -56
  76. package/dist/services/base/AppLifecycleService.d.ts +4 -0
  77. package/dist/services/base/AppLifecycleService.js +4 -0
  78. package/dist/services/base/ControlService.js +0 -4
  79. package/dist/services/base/DatabaseEventService.js +1 -1
  80. package/dist/services/base/IService.d.ts +105 -8
  81. package/dist/services/base/ReadinessEvaluator.d.ts +40 -0
  82. package/dist/services/base/ReadinessEvaluator.js +16 -0
  83. package/dist/services/base/RemoteConnection.d.ts +3 -0
  84. package/dist/services/base/RemoteService.js +2 -2
  85. package/dist/services/base/ReplicationService.d.ts +45 -10
  86. package/dist/services/base/ReplicationService.js +248 -91
  87. package/dist/services/base/ReplicationService.readiness.d.ts +52 -0
  88. package/dist/services/base/ReplicationService.readiness.js +87 -0
  89. package/dist/services/base/ReplicationService.typedReplication.d.ts +83 -0
  90. package/dist/services/base/ReplicationService.typedReplication.js +191 -0
  91. package/dist/services/base/ReplicatorService.activeReplicatorState.d.ts +47 -0
  92. package/dist/services/base/ReplicatorService.activeReplicatorState.js +107 -0
  93. package/dist/services/base/ReplicatorService.centralRemoteAdministration.d.ts +5 -0
  94. package/dist/services/base/ReplicatorService.centralRemoteAdministration.js +44 -0
  95. package/dist/services/base/ReplicatorService.d.ts +75 -14
  96. package/dist/services/base/ReplicatorService.js +232 -53
  97. package/dist/services/base/ReplicatorService.remoteResourceResolver.d.ts +4 -0
  98. package/dist/services/base/ReplicatorService.remoteResourceResolver.js +11 -0
  99. package/dist/services/base/TweakValueService.d.ts +3 -2
  100. package/dist/services/implements/injectable/InjectableTweakValueService.d.ts +1 -1
  101. package/dist/services/lib/HandlerUtils.d.ts +24 -0
  102. package/dist/services/lib/HandlerUtils.js +40 -0
  103. package/docs/development.md +40 -7
  104. package/docs/p2p-transport-lifecycle.md +135 -11
  105. package/docs/proven-in-use.md +5 -1
  106. package/docs/remote-configurations.md +4 -0
  107. package/docs/service-feature-composition.md +250 -0
  108. package/package.json +17 -1
  109. package/dist/replication/trystero/P2PReplicatorBase.d.ts +0 -19
  110. package/dist/replication/trystero/P2PReplicatorCore.d.ts +0 -8
  111. package/dist/replication/trystero/P2PReplicatorCore.js +0 -56
  112. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +0 -2
  113. package/dist/replication/trystero/TrysteroReplicatorP2PConnection.js +0 -4
  114. /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, MARK_LOG_NETWORK_ERROR } from "../lib/logUtils.js";
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
- * Check if the replication is ready to start.
73
- * @param showMessage Whether to show messages to the user.
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
- if (!this.appLifecycleService.isReady()) {
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) return await this.onReplicationFailed(showMessage);
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) return await this.onReplicationFailed(showMessage);
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
- getActiveReplicatorFor(usage) {
182
- const activeReplicator = this.replicatorService.getActiveReplicator();
183
- if (!activeReplicator) {
184
- this._log(`Active replicator not found during ${usage}`, LOG_LEVEL_NOTICE);
185
- return false;
186
- }
187
- return activeReplicator;
188
- }
189
- async performReplicateAllToRemote(showingNotice) {
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 currentSettings = this.settingService.currentSettings();
195
- const activeReplicator = this.getActiveReplicatorFor("sending data to remote");
196
- if (!activeReplicator) {
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 currentSettings = this.settingService.currentSettings();
212
- const ret = await activeReplicator.replicateAllFromServer(currentSettings, showingNotice);
213
- if (ret) return true;
214
- const checkResult = await this.checkConnectionFailure();
215
- if (checkResult == "CHECKAGAIN")
216
- return await activeReplicator.replicateAllFromServer(currentSettings, showingNotice);
217
- return !checkResult;
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.performReplicateAllToRemote(showingNotice);
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.performReplicateAllFromRemote(showingNotice);
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.performReplicateAllToRemote(showingNotice);
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.performReplicateAllFromRemote(showingNotice);
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
- const currentSettings = this.settingService.currentSettings();
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
+ }